mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Fixes Swift_force_load - check #175905 for more info *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Victoria Ashworth <15619084+vashworth@users.noreply.github.com>
722 lines
24 KiB
Dart
722 lines
24 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:flutter_devicelab/framework/framework.dart';
|
|
import 'package:flutter_devicelab/framework/host_agent.dart';
|
|
import 'package:flutter_devicelab/framework/ios.dart';
|
|
import 'package:flutter_devicelab/framework/task_result.dart';
|
|
import 'package:flutter_devicelab/framework/utils.dart';
|
|
import 'package:path/path.dart' as path;
|
|
|
|
/// Tests that the Flutter module project template works and supports
|
|
/// adding Flutter to an existing iOS app.
|
|
Future<void> main() async {
|
|
await task(() async {
|
|
// This variable cannot be `late`, as we reference it in the `finally` block
|
|
// which may execute before this field has been initialized.
|
|
String? simulatorDeviceId;
|
|
section('Create Flutter module project');
|
|
|
|
final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.');
|
|
final projectDir = Directory(path.join(tempDir.path, 'hello'));
|
|
try {
|
|
await inDirectory(tempDir, () async {
|
|
await flutter(
|
|
'create',
|
|
options: <String>['--org', 'io.flutter.devicelab', '--template=module', 'hello'],
|
|
);
|
|
});
|
|
|
|
// Copy test dart files to new module app.
|
|
final flutterModuleLibSource = Directory(
|
|
path.join(
|
|
flutterDirectory.path,
|
|
'dev',
|
|
'integration_tests',
|
|
'ios_host_app',
|
|
'flutterapp',
|
|
'lib',
|
|
),
|
|
);
|
|
final flutterModuleLibDestination = Directory(path.join(projectDir.path, 'lib'));
|
|
|
|
// These test files don't have a .dart extension so the analyzer will ignore them. They aren't in a
|
|
// package and don't work on their own outside of the test module just created.
|
|
final main = File(path.join(flutterModuleLibSource.path, 'main'));
|
|
main.copySync(path.join(flutterModuleLibDestination.path, 'main.dart'));
|
|
|
|
final marquee = File(path.join(flutterModuleLibSource.path, 'marquee'));
|
|
marquee.copySync(path.join(flutterModuleLibDestination.path, 'marquee.dart'));
|
|
|
|
final resize = File(path.join(flutterModuleLibSource.path, 'resize'));
|
|
resize.copySync(path.join(flutterModuleLibDestination.path, 'resize.dart'));
|
|
|
|
section('Create package with native assets');
|
|
|
|
const ffiPackageName = 'ffi_package';
|
|
await createFfiPackage(ffiPackageName, tempDir);
|
|
|
|
section('Add FFI package');
|
|
|
|
final pubspec = File(path.join(projectDir.path, 'pubspec.yaml'));
|
|
String content = await pubspec.readAsString();
|
|
content = content.replaceFirst('dependencies:\n', '''
|
|
dependencies:
|
|
$ffiPackageName:
|
|
path: ../$ffiPackageName
|
|
''');
|
|
await pubspec.writeAsString(content, flush: true);
|
|
await inDirectory(projectDir, () async {
|
|
await flutter('packages', options: <String>['get']);
|
|
});
|
|
|
|
section('Build ephemeral host app in release mode without CocoaPods');
|
|
|
|
await inDirectory(projectDir, () async {
|
|
await flutter('build', options: <String>['ios', '--no-codesign']);
|
|
});
|
|
|
|
// Check the tool is no longer copying to the legacy xcframework location.
|
|
checkDirectoryNotExists(
|
|
path.join(projectDir.path, '.ios', 'Flutter', 'engine', 'Flutter.xcframework'),
|
|
);
|
|
|
|
final ephemeralIOSHostApp = Directory(
|
|
path.join(projectDir.path, 'build', 'ios', 'iphoneos', 'Runner.app'),
|
|
);
|
|
|
|
if (!exists(ephemeralIOSHostApp)) {
|
|
return TaskResult.failure('Failed to build ephemeral host .app');
|
|
}
|
|
|
|
if (!await _isAppAotBuild(ephemeralIOSHostApp)) {
|
|
return TaskResult.failure(
|
|
'Ephemeral host app ${ephemeralIOSHostApp.path} was not a release build as expected',
|
|
);
|
|
}
|
|
|
|
section('Build ephemeral host app in profile mode without CocoaPods');
|
|
|
|
await inDirectory(projectDir, () async {
|
|
await flutter('build', options: <String>['ios', '--no-codesign', '--profile']);
|
|
});
|
|
|
|
if (!exists(ephemeralIOSHostApp)) {
|
|
return TaskResult.failure('Failed to build ephemeral host .app');
|
|
}
|
|
|
|
if (!await _isAppAotBuild(ephemeralIOSHostApp)) {
|
|
return TaskResult.failure(
|
|
'Ephemeral host app ${ephemeralIOSHostApp.path} was not a profile build as expected',
|
|
);
|
|
}
|
|
|
|
section('Clean build');
|
|
|
|
await inDirectory(projectDir, () async {
|
|
await flutter('clean');
|
|
});
|
|
|
|
section('Build ephemeral host app in debug mode for simulator without CocoaPods');
|
|
|
|
await inDirectory(projectDir, () async {
|
|
await flutter('build', options: <String>['ios', '--no-codesign', '--simulator', '--debug']);
|
|
});
|
|
|
|
final ephemeralSimulatorHostApp = Directory(
|
|
path.join(projectDir.path, 'build', 'ios', 'iphonesimulator', 'Runner.app'),
|
|
);
|
|
|
|
if (!exists(ephemeralSimulatorHostApp)) {
|
|
return TaskResult.failure('Failed to build ephemeral host .app');
|
|
}
|
|
checkFileExists(
|
|
path.join(ephemeralSimulatorHostApp.path, 'Frameworks', 'Flutter.framework', 'Flutter'),
|
|
);
|
|
|
|
if (!exists(
|
|
File(
|
|
path.join(
|
|
ephemeralSimulatorHostApp.path,
|
|
'Frameworks',
|
|
'App.framework',
|
|
'flutter_assets',
|
|
'isolate_snapshot_data',
|
|
),
|
|
),
|
|
)) {
|
|
return TaskResult.failure(
|
|
'Ephemeral host app ${ephemeralSimulatorHostApp.path} was not a debug build as expected',
|
|
);
|
|
}
|
|
|
|
section('Clean build');
|
|
|
|
await inDirectory(projectDir, () async {
|
|
await flutter('clean');
|
|
});
|
|
|
|
// Make a fake Dart-only plugin, since there are no existing examples.
|
|
section('Create local plugin');
|
|
|
|
const dartPluginName = 'dartplugin';
|
|
await _createFakeDartPlugin(dartPluginName, tempDir);
|
|
|
|
section('Add plugins');
|
|
|
|
content = content.replaceFirst(
|
|
'dependencies:\n',
|
|
// One framework, one Dart-only, one that does not support iOS, and one with a resource bundle.
|
|
'''
|
|
dependencies:
|
|
url_launcher: 6.0.20
|
|
android_alarm_manager: 2.0.2
|
|
google_sign_in_ios: 5.5.0
|
|
$dartPluginName:
|
|
path: ../$dartPluginName
|
|
''',
|
|
);
|
|
await pubspec.writeAsString(content, flush: true);
|
|
await inDirectory(projectDir, () async {
|
|
await flutter('packages', options: <String>['get']);
|
|
});
|
|
|
|
section('Build ephemeral host app with CocoaPods');
|
|
|
|
await inDirectory(projectDir, () async {
|
|
await flutter('build', options: <String>['ios', '--no-codesign', '-v']);
|
|
});
|
|
|
|
final bool ephemeralHostAppWithCocoaPodsBuilt = exists(ephemeralIOSHostApp);
|
|
|
|
if (!ephemeralHostAppWithCocoaPodsBuilt) {
|
|
return TaskResult.failure('Failed to build ephemeral host .app with CocoaPods');
|
|
}
|
|
|
|
final podfileLockFile = File(path.join(projectDir.path, '.ios', 'Podfile.lock'));
|
|
final String podfileLockOutput = podfileLockFile.readAsStringSync();
|
|
if (!podfileLockOutput.contains(':path: Flutter') ||
|
|
!podfileLockOutput.contains(':path: Flutter/FlutterPluginRegistrant') ||
|
|
!podfileLockOutput.contains(':path: ".symlinks/plugins/url_launcher_ios/ios"') ||
|
|
podfileLockOutput.contains('android_alarm_manager') ||
|
|
podfileLockOutput.contains(dartPluginName)) {
|
|
print(podfileLockOutput);
|
|
return TaskResult.failure(
|
|
'Building ephemeral host app Podfile.lock does not contain expected pods',
|
|
);
|
|
}
|
|
|
|
checkFileExists(
|
|
path.join(
|
|
ephemeralIOSHostApp.path,
|
|
'Frameworks',
|
|
'url_launcher_ios.framework',
|
|
'url_launcher_ios',
|
|
),
|
|
);
|
|
// Resources should be embedded.
|
|
checkDirectoryExists(
|
|
path.join(
|
|
ephemeralIOSHostApp.path,
|
|
'Frameworks',
|
|
'GoogleSignIn.framework',
|
|
'GoogleSignIn.bundle',
|
|
),
|
|
);
|
|
checkFileExists(
|
|
path.join(ephemeralIOSHostApp.path, 'Frameworks', 'Flutter.framework', 'Flutter'),
|
|
);
|
|
|
|
// Android-only, no embedded framework.
|
|
checkDirectoryNotExists(
|
|
path.join(ephemeralIOSHostApp.path, 'Frameworks', 'android_alarm_manager.framework'),
|
|
);
|
|
|
|
// Dart-only, no embedded framework.
|
|
checkDirectoryNotExists(
|
|
path.join(ephemeralIOSHostApp.path, 'Frameworks', '$dartPluginName.framework'),
|
|
);
|
|
|
|
// Native assets embedded, no embedded framework.
|
|
checkFileExists(
|
|
path.join(
|
|
ephemeralIOSHostApp.path,
|
|
'Frameworks',
|
|
'$ffiPackageName.framework',
|
|
ffiPackageName,
|
|
),
|
|
);
|
|
|
|
section('Clean and pub get module');
|
|
|
|
await inDirectory(projectDir, () async {
|
|
await flutter('clean');
|
|
});
|
|
|
|
await inDirectory(projectDir, () async {
|
|
await flutter('build', options: <String>['ios', '--config-only']);
|
|
});
|
|
|
|
section('Add to existing iOS Objective-C app');
|
|
|
|
final objectiveCHostApp = Directory(path.join(tempDir.path, 'hello_host_app'));
|
|
mkdir(objectiveCHostApp);
|
|
recursiveCopy(
|
|
Directory(path.join(flutterDirectory.path, 'dev', 'integration_tests', 'ios_host_app')),
|
|
objectiveCHostApp,
|
|
);
|
|
|
|
final objectiveCBuildDirectory = Directory(path.join(tempDir.path, 'build-objc'));
|
|
|
|
await inDirectory(objectiveCHostApp, () async {
|
|
section('Validate iOS Objective-C host app Podfile');
|
|
|
|
final podfile = File(path.join(objectiveCHostApp.path, 'Podfile'));
|
|
final String correctPodfileContents = await podfile.readAsString();
|
|
final podfileMissingPostInstall = File(
|
|
path.join(objectiveCHostApp.path, 'PodfileMissingPostInstall'),
|
|
);
|
|
|
|
podfile.writeAsStringSync(podfileMissingPostInstall.readAsStringSync());
|
|
|
|
final String podFailure = await eval(
|
|
'pod',
|
|
<String>['install'],
|
|
environment: <String, String>{'LANG': 'en_US.UTF-8'},
|
|
canFail: true,
|
|
);
|
|
|
|
if (!podFailure.contains(
|
|
'Missing `flutter_post_install(installer)` in Podfile `post_install` block',
|
|
) ||
|
|
!podFailure.contains(
|
|
'Add `flutter_post_install(installer)` to your Podfile `post_install` block to build Flutter plugins',
|
|
)) {
|
|
print(podfile.readAsStringSync());
|
|
throw TaskResult.failure(
|
|
'pod install unexpectedly succeed without "flutter_post_install" post_install block',
|
|
);
|
|
}
|
|
var podfileContent = correctPodfileContents;
|
|
await podfile.writeAsString(podfileContent, flush: true);
|
|
|
|
await exec(
|
|
'pod',
|
|
<String>['install'],
|
|
environment: <String, String>{'LANG': 'en_US.UTF-8'},
|
|
);
|
|
|
|
var hostPodfileLockFile = File(path.join(objectiveCHostApp.path, 'Podfile.lock'));
|
|
String hostPodfileLockOutput = hostPodfileLockFile.readAsStringSync();
|
|
if (!hostPodfileLockOutput.contains(':path: "../hello/.ios/Flutter"') ||
|
|
!hostPodfileLockOutput.contains(
|
|
':path: "../hello/.ios/Flutter/FlutterPluginRegistrant"',
|
|
) ||
|
|
!hostPodfileLockOutput.contains(
|
|
':path: "../hello/.ios/.symlinks/plugins/url_launcher_ios/ios"',
|
|
) ||
|
|
hostPodfileLockOutput.contains('android_alarm_manager') ||
|
|
hostPodfileLockOutput.contains(dartPluginName)) {
|
|
print(hostPodfileLockOutput);
|
|
throw TaskResult.failure('Building host app Podfile.lock does not contain expected pods');
|
|
}
|
|
|
|
section(
|
|
'Validate install_flutter_[engine_pod|plugin_pods|application_pod] methods in the Podfile can be executed normally',
|
|
);
|
|
|
|
podfileContent = podfileContent.replaceFirst(
|
|
'''
|
|
install_all_flutter_pods flutter_application_path
|
|
''',
|
|
'''
|
|
install_flutter_engine_pod(flutter_application_path)
|
|
install_flutter_plugin_pods(flutter_application_path)
|
|
install_flutter_application_pod(flutter_application_path)
|
|
''',
|
|
);
|
|
await podfile.writeAsString(podfileContent, flush: true);
|
|
|
|
await exec(
|
|
'pod',
|
|
<String>['install'],
|
|
environment: <String, String>{'LANG': 'en_US.UTF-8'},
|
|
);
|
|
|
|
hostPodfileLockFile = File(path.join(objectiveCHostApp.path, 'Podfile.lock'));
|
|
hostPodfileLockOutput = hostPodfileLockFile.readAsStringSync();
|
|
if (!hostPodfileLockOutput.contains(':path: "../hello/.ios/Flutter"') ||
|
|
!hostPodfileLockOutput.contains(
|
|
':path: "../hello/.ios/Flutter/FlutterPluginRegistrant"',
|
|
) ||
|
|
!hostPodfileLockOutput.contains(
|
|
':path: "../hello/.ios/.symlinks/plugins/url_launcher_ios/ios"',
|
|
) ||
|
|
hostPodfileLockOutput.contains('android_alarm_manager') ||
|
|
hostPodfileLockOutput.contains(dartPluginName)) {
|
|
print(hostPodfileLockOutput);
|
|
throw TaskResult.failure('Building host app Podfile.lock does not contain expected pods');
|
|
}
|
|
|
|
// Check the tool is no longer copying to the legacy App.framework location.
|
|
final dummyAppFramework = File(
|
|
path.join(projectDir.path, '.ios', 'Flutter', 'App.framework', 'App'),
|
|
);
|
|
checkFileNotExists(dummyAppFramework.path);
|
|
|
|
section('Build iOS Objective-C host app');
|
|
|
|
await exec(
|
|
'xcodebuild',
|
|
<String>[
|
|
'-workspace',
|
|
'Host.xcworkspace',
|
|
'-scheme',
|
|
'Host',
|
|
'-configuration',
|
|
'Debug',
|
|
'CODE_SIGNING_ALLOWED=NO',
|
|
'CODE_SIGNING_REQUIRED=NO',
|
|
'CODE_SIGN_IDENTITY=-',
|
|
'EXPANDED_CODE_SIGN_IDENTITY=-',
|
|
'BUILD_DIR=${objectiveCBuildDirectory.path}',
|
|
'COMPILER_INDEX_STORE_ENABLE=NO',
|
|
],
|
|
environment: <String, String>{'FLUTTER_SUPPRESS_ANALYTICS': 'true'},
|
|
);
|
|
});
|
|
|
|
final String hostAppDirectory = path.join(
|
|
objectiveCBuildDirectory.path,
|
|
'Debug-iphoneos',
|
|
'Host.app',
|
|
);
|
|
|
|
final bool existingAppBuilt = exists(File(path.join(hostAppDirectory, 'Host')));
|
|
if (!existingAppBuilt) {
|
|
return TaskResult.failure('Failed to build existing Objective-C app .app');
|
|
}
|
|
|
|
final String hostFrameworksDirectory = path.join(hostAppDirectory, 'Frameworks');
|
|
|
|
checkFileExists(path.join(hostFrameworksDirectory, 'Flutter.framework', 'Flutter'));
|
|
|
|
checkFileExists(
|
|
path.join(
|
|
hostFrameworksDirectory,
|
|
'App.framework',
|
|
'flutter_assets',
|
|
'isolate_snapshot_data',
|
|
),
|
|
);
|
|
|
|
checkFileExists(
|
|
path.join(hostFrameworksDirectory, '$ffiPackageName.framework', ffiPackageName),
|
|
);
|
|
|
|
section('Check the NOTICE file is correct');
|
|
|
|
final String licenseFilePath = path.join(
|
|
hostFrameworksDirectory,
|
|
'App.framework',
|
|
'flutter_assets',
|
|
'NOTICES.Z',
|
|
);
|
|
checkFileExists(licenseFilePath);
|
|
|
|
await inDirectory(objectiveCBuildDirectory, () async {
|
|
final Uint8List licenseData = File(licenseFilePath).readAsBytesSync();
|
|
final String licenseString = utf8.decode(gzip.decode(licenseData));
|
|
if (!licenseString.contains('skia') || !licenseString.contains('Flutter Authors')) {
|
|
return TaskResult.failure('License content missing');
|
|
}
|
|
});
|
|
|
|
section('Archive iOS Objective-C host app');
|
|
|
|
await inDirectory(objectiveCHostApp, () async {
|
|
final objectiveCBuildArchiveDirectory = Directory(
|
|
path.join(tempDir.path, 'build-objc-archive'),
|
|
);
|
|
await exec(
|
|
'xcodebuild',
|
|
<String>[
|
|
'-workspace',
|
|
'Host.xcworkspace',
|
|
'-scheme',
|
|
'Host',
|
|
'-configuration',
|
|
'Release',
|
|
'CODE_SIGNING_ALLOWED=NO',
|
|
'CODE_SIGNING_REQUIRED=NO',
|
|
'CODE_SIGN_IDENTITY=-',
|
|
'EXPANDED_CODE_SIGN_IDENTITY=-',
|
|
'-archivePath',
|
|
objectiveCBuildArchiveDirectory.path,
|
|
'COMPILER_INDEX_STORE_ENABLE=NO',
|
|
'archive',
|
|
],
|
|
environment: <String, String>{'FLUTTER_SUPPRESS_ANALYTICS': 'true'},
|
|
);
|
|
|
|
final String archivedAppPath = path.join(
|
|
'${objectiveCBuildArchiveDirectory.path}.xcarchive',
|
|
'Products',
|
|
'Applications',
|
|
'Host.app',
|
|
);
|
|
|
|
checkFileExists(path.join(archivedAppPath, 'Host'));
|
|
|
|
checkFileNotExists(
|
|
path.join(
|
|
archivedAppPath,
|
|
'Frameworks',
|
|
'App.framework',
|
|
'flutter_assets',
|
|
'isolate_snapshot_data',
|
|
),
|
|
);
|
|
|
|
final String builtFlutterBinary = path.join(
|
|
archivedAppPath,
|
|
'Frameworks',
|
|
'Flutter.framework',
|
|
'Flutter',
|
|
);
|
|
checkFileExists(builtFlutterBinary);
|
|
if ((await fileType(builtFlutterBinary)).contains('armv7')) {
|
|
throw TaskResult.failure('Unexpected armv7 architecture slice in $builtFlutterBinary');
|
|
}
|
|
|
|
final String builtAppBinary = path.join(
|
|
archivedAppPath,
|
|
'Frameworks',
|
|
'App.framework',
|
|
'App',
|
|
);
|
|
checkFileExists(builtAppBinary);
|
|
if ((await fileType(builtAppBinary)).contains('armv7')) {
|
|
throw TaskResult.failure('Unexpected armv7 architecture slice in $builtAppBinary');
|
|
}
|
|
|
|
// Check native assets are bundled.
|
|
checkFileExists(
|
|
path.join(archivedAppPath, 'Frameworks', '$ffiPackageName.framework', ffiPackageName),
|
|
);
|
|
|
|
// With use_frameworks! in the Podfile (required for Xcode 26+ Swift compatibility),
|
|
// plugins are built as dynamic frameworks. Verify url_launcher_ios.framework exists.
|
|
checkDirectoryExists(
|
|
path.join(archivedAppPath, 'Frameworks', 'url_launcher_ios.framework'),
|
|
);
|
|
|
|
checkFileExists(
|
|
path.join(
|
|
'${objectiveCBuildArchiveDirectory.path}.xcarchive',
|
|
'dSYMs',
|
|
'App.framework.dSYM',
|
|
'Contents',
|
|
'Resources',
|
|
'DWARF',
|
|
'App',
|
|
),
|
|
);
|
|
});
|
|
|
|
section('Run platform unit tests');
|
|
|
|
final String resultBundleTemp = Directory.systemTemp
|
|
.createTempSync('flutter_module_test_ios_xcresult.')
|
|
.path;
|
|
await testWithNewIOSSimulator('TestAdd2AppSim', (String deviceId) async {
|
|
simulatorDeviceId = deviceId;
|
|
final String resultBundlePath = path.join(resultBundleTemp, 'result');
|
|
|
|
final int testResultExit = await exec(
|
|
'xcodebuild',
|
|
<String>[
|
|
'-workspace',
|
|
'Host.xcworkspace',
|
|
'-scheme',
|
|
'Host',
|
|
'-configuration',
|
|
'Debug',
|
|
'-destination',
|
|
'id=$deviceId',
|
|
'-resultBundlePath',
|
|
resultBundlePath,
|
|
'test',
|
|
'COMPILER_INDEX_STORE_ENABLE=NO',
|
|
],
|
|
workingDirectory: objectiveCHostApp.path,
|
|
canFail: true,
|
|
);
|
|
|
|
if (testResultExit != 0) {
|
|
final Directory? dumpDirectory = hostAgent.dumpDirectory;
|
|
if (dumpDirectory != null) {
|
|
// Zip the test results to the artifacts directory for upload.
|
|
await inDirectory(resultBundleTemp, () {
|
|
final String zipPath = path.join(
|
|
dumpDirectory.path,
|
|
'module_test_ios-objc-${DateTime.now().toLocal().toIso8601String()}.zip',
|
|
);
|
|
return exec(
|
|
'zip',
|
|
<String>['-r', '-9', '-q', zipPath, 'result.xcresult'],
|
|
canFail: true, // Best effort to get the logs.
|
|
);
|
|
});
|
|
}
|
|
|
|
throw TaskResult.failure('Platform unit tests failed');
|
|
}
|
|
});
|
|
|
|
section('Fail building existing Objective-C iOS app if flutter script fails');
|
|
final String xcodebuildOutput = await inDirectory<String>(
|
|
objectiveCHostApp,
|
|
() => eval('xcodebuild', <String>[
|
|
'-workspace',
|
|
'Host.xcworkspace',
|
|
'-scheme',
|
|
'Host',
|
|
'-configuration',
|
|
'Debug',
|
|
'FLUTTER_ENGINE=bogus', // Force a Flutter error.
|
|
'CODE_SIGNING_ALLOWED=NO',
|
|
'CODE_SIGNING_REQUIRED=NO',
|
|
'CODE_SIGN_IDENTITY=-',
|
|
'EXPANDED_CODE_SIGN_IDENTITY=-',
|
|
'BUILD_DIR=${objectiveCBuildDirectory.path}',
|
|
'COMPILER_INDEX_STORE_ENABLE=NO',
|
|
], canFail: true),
|
|
);
|
|
|
|
if (!xcodebuildOutput.contains(
|
|
RegExp('flutter.*--local-engine-src-path=bogus assemble'),
|
|
) || // Verbose output
|
|
!xcodebuildOutput.contains(
|
|
'Unable to detect a Flutter engine build directory in bogus',
|
|
)) {
|
|
return TaskResult.failure(
|
|
'Host Objective-C app build succeeded though flutter script failed',
|
|
);
|
|
}
|
|
|
|
section('Add to existing iOS Swift app');
|
|
|
|
final swiftHostApp = Directory(path.join(tempDir.path, 'hello_host_app_swift'));
|
|
mkdir(swiftHostApp);
|
|
recursiveCopy(
|
|
Directory(
|
|
path.join(flutterDirectory.path, 'dev', 'integration_tests', 'ios_host_app_swift'),
|
|
),
|
|
swiftHostApp,
|
|
);
|
|
|
|
final swiftBuildDirectory = Directory(path.join(tempDir.path, 'build-swift'));
|
|
|
|
await inDirectory(swiftHostApp, () async {
|
|
await exec(
|
|
'pod',
|
|
<String>['install'],
|
|
environment: <String, String>{'LANG': 'en_US.UTF-8'},
|
|
);
|
|
await exec(
|
|
'xcodebuild',
|
|
<String>[
|
|
'-workspace',
|
|
'Host.xcworkspace',
|
|
'-scheme',
|
|
'Host',
|
|
'-configuration',
|
|
'Debug',
|
|
'CODE_SIGNING_ALLOWED=NO',
|
|
'CODE_SIGNING_REQUIRED=NO',
|
|
'CODE_SIGN_IDENTITY=-',
|
|
'EXPANDED_CODE_SIGN_IDENTITY=-',
|
|
'BUILD_DIR=${swiftBuildDirectory.path}',
|
|
'COMPILER_INDEX_STORE_ENABLE=NO',
|
|
],
|
|
environment: <String, String>{'FLUTTER_SUPPRESS_ANALYTICS': 'true'},
|
|
);
|
|
});
|
|
|
|
final bool existingSwiftAppBuilt = exists(
|
|
File(path.join(swiftBuildDirectory.path, 'Debug-iphoneos', 'Host.app', 'Host')),
|
|
);
|
|
if (!existingSwiftAppBuilt) {
|
|
return TaskResult.failure('Failed to build existing Swift app .app');
|
|
}
|
|
|
|
return TaskResult.success(null);
|
|
} catch (e, stackTrace) {
|
|
print('Task exception stack trace:\n$stackTrace');
|
|
return TaskResult.failure(e.toString());
|
|
} finally {
|
|
unawaited(removeIOSSimulator(simulatorDeviceId));
|
|
rmTree(tempDir);
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<bool> _isAppAotBuild(Directory app) async {
|
|
final String binary = path.join(app.path, 'Frameworks', 'App.framework', 'App');
|
|
|
|
final String symbolTable = await dumpSymbolTable(binary);
|
|
|
|
return symbolTable.contains('kDartIsolateSnapshotInstructions');
|
|
}
|
|
|
|
Future<void> _createFakeDartPlugin(String name, Directory parent) async {
|
|
// Start from a standard plugin template.
|
|
await inDirectory(parent, () async {
|
|
await flutter(
|
|
'create',
|
|
options: <String>[
|
|
'--org',
|
|
'io.flutter.devicelab',
|
|
'--template=plugin',
|
|
'--platforms=ios',
|
|
name,
|
|
],
|
|
);
|
|
});
|
|
|
|
final String pluginDir = path.join(parent.path, name);
|
|
|
|
// Convert the metadata to Dart-only.
|
|
final dartPluginClass = 'DartClassFor$name';
|
|
final pubspec = File(path.join(pluginDir, 'pubspec.yaml'));
|
|
String content = await pubspec.readAsString();
|
|
content = content.replaceAll(
|
|
RegExp(r' pluginClass: .*?\n'),
|
|
' dartPluginClass: $dartPluginClass\n',
|
|
);
|
|
await pubspec.writeAsString(content, flush: true);
|
|
|
|
// Add the Dart registration hook that the build will generate a call to.
|
|
final dartCode = File(path.join(pluginDir, 'lib', '$name.dart'));
|
|
content = await dartCode.readAsString();
|
|
content =
|
|
'''
|
|
$content
|
|
|
|
class $dartPluginClass {
|
|
static void registerWith() {}
|
|
}
|
|
''';
|
|
await dartCode.writeAsString(content, flush: true);
|
|
|
|
// Remove the native plugin code.
|
|
await Directory(path.join(pluginDir, 'ios')).delete(recursive: true);
|
|
}
|