From 125451bc2ed1e823b62ad1036735cf98e82fc966 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Wed, 25 Aug 2021 13:31:03 -0700 Subject: [PATCH] Migrate mac.dart to null safety (#88846) --- packages/flutter_tools/lib/src/ios/mac.dart | 127 +++++++++--------- .../flutter_tools/lib/src/ios/xcodeproj.dart | 2 +- .../ios_device_start_nonprebuilt_test.dart | 105 ++++++++++++--- .../test/general.shard/ios/mac_test.dart | 20 ++- 4 files changed, 167 insertions(+), 87 deletions(-) diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index e8dd95fc737..b0cb6cc1a41 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart = 2.8 - import 'package:meta/meta.dart'; import 'package:process/process.dart'; @@ -36,10 +34,10 @@ import 'xcodeproj.dart'; class IMobileDevice { IMobileDevice({ - @required Artifacts artifacts, - @required Cache cache, - @required ProcessManager processManager, - @required Logger logger, + required Artifacts artifacts, + required Cache cache, + required ProcessManager processManager, + required Logger logger, }) : _idevicesyslogPath = artifacts.getHostArtifact(HostArtifact.idevicesyslog).path, _idevicescreenshotPath = artifacts.getHostArtifact(HostArtifact.idevicescreenshot).path, _dyLdLibEntry = cache.dyLdLibEntry, @@ -53,7 +51,7 @@ class IMobileDevice { final ProcessUtils _processUtils; bool get isInstalled => _isInstalled ??= _processManager.canRun(_idevicescreenshotPath); - bool _isInstalled; + bool? _isInstalled; /// Starts `idevicesyslog` and returns the running process. Future startLogger(String deviceID) { @@ -93,13 +91,13 @@ class IMobileDevice { } Future buildXcodeProject({ - BuildableIOSApp app, - BuildInfo buildInfo, - String targetOverride, + required BuildableIOSApp app, + required BuildInfo buildInfo, + required String targetOverride, EnvironmentType environmentType = EnvironmentType.physical, - DarwinArch activeArch, + DarwinArch? activeArch, bool codesign = true, - String deviceID, + String? deviceID, bool configOnly = false, XcodeBuildAction buildAction = XcodeBuildAction.build, }) async { @@ -126,12 +124,16 @@ Future buildXcodeProject({ await removeFinderExtendedAttributes(app.project.parent.directory, globals.processUtils, globals.logger); - final XcodeProjectInfo projectInfo = await app.project.projectInfo(); - final String scheme = projectInfo.schemeFor(buildInfo); + final XcodeProjectInfo? projectInfo = await app.project.projectInfo(); + if (projectInfo == null) { + globals.printError('Xcode project not found.'); + return XcodeBuildResult(success: false); + } + final String? scheme = projectInfo.schemeFor(buildInfo); if (scheme == null) { projectInfo.reportFlavorNotFoundAndExit(); } - final String configuration = projectInfo.buildConfigurationFor(buildInfo, scheme); + final String? configuration = projectInfo.buildConfigurationFor(buildInfo, scheme); if (configuration == null) { globals.printError(''); globals.printError('The Xcode project defines build configurations: ${projectInfo.buildConfigurations.join(', ')}'); @@ -159,14 +161,14 @@ Future buildXcodeProject({ } final FlutterManifest manifest = app.project.parent.manifest; - final String buildName = parsedBuildName(manifest: manifest, buildInfo: buildInfo); + final String? buildName = parsedBuildName(manifest: manifest, buildInfo: buildInfo); final bool buildNameIsMissing = buildName == null || buildName.isEmpty; if (buildNameIsMissing) { globals.printStatus('Warning: Missing build name (CFBundleShortVersionString).'); } - final String buildNumber = parsedBuildNumber(manifest: manifest, buildInfo: buildInfo); + final String? buildNumber = parsedBuildNumber(manifest: manifest, buildInfo: buildInfo); final bool buildNumberIsMissing = buildNumber == null || buildNumber.isEmpty; if (buildNumberIsMissing) { @@ -177,7 +179,7 @@ Future buildXcodeProject({ 'file version field before submitting to the App Store.'); } - Map autoSigningConfigs; + Map? autoSigningConfigs; final Map buildSettings = await app.project.buildSettingsForBuildInfo( buildInfo, @@ -206,7 +208,7 @@ Future buildXcodeProject({ } final List buildCommands = [ - ...globals.xcode.xcrunCommand(), + ...globals.xcode!.xcrunCommand(), 'xcodebuild', '-configuration', configuration, @@ -291,18 +293,18 @@ Future buildXcodeProject({ ); } - Status buildSubStatus; - Status initialBuildStatus; - Directory tempDir; + Status? buildSubStatus; + Status? initialBuildStatus; + Directory? tempDir; - File scriptOutputPipeFile; + File? scriptOutputPipeFile; if (globals.logger.hasTerminal) { tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_build_log_pipe.'); scriptOutputPipeFile = tempDir.childFile('pipe_to_stdout'); globals.os.makePipe(scriptOutputPipeFile.path); Future listenToScriptOutputLine() async { - final List lines = await scriptOutputPipeFile.readAsLines(); + final List lines = await scriptOutputPipeFile!.readAsLines(); for (final String line in lines) { if (line == 'done' || line == 'all done') { buildSubStatus?.stop(); @@ -347,7 +349,7 @@ Future buildXcodeProject({ final Stopwatch sw = Stopwatch()..start(); initialBuildStatus = globals.logger.startProgress('Running Xcode build...'); - final RunResult buildResult = await _runBuildWithRetries(buildCommands, app); + final RunResult? buildResult = await _runBuildWithRetries(buildCommands, app); // Notifies listener that no more output is coming. scriptOutputPipeFile?.writeAsStringSync('all done'); @@ -361,7 +363,7 @@ Future buildXcodeProject({ ); globals.flutterUsage.sendTiming(xcodeBuildActionToString(buildAction), 'xcode-ios', Duration(milliseconds: sw.elapsedMilliseconds)); - if (buildResult.exitCode != 0) { + if (buildResult != null && buildResult.exitCode != 0) { globals.printStatus('Failed to build iOS app'); if (buildResult.stderr.isNotEmpty) { globals.printStatus('Error output from Xcode build:\n↳'); @@ -383,18 +385,22 @@ Future buildXcodeProject({ ), ); } else { - String outputDir; + String? outputDir; if (buildAction == XcodeBuildAction.build) { // If the app contains a watch companion target, the sdk argument of xcodebuild has to be omitted. // For some reason this leads to TARGET_BUILD_DIR always ending in 'iphoneos' even though the // actual directory will end with 'iphonesimulator' for simulator builds. // The value of TARGET_BUILD_DIR is adjusted to accommodate for this effect. - String targetBuildDir = buildSettings['TARGET_BUILD_DIR']; + String? targetBuildDir = buildSettings['TARGET_BUILD_DIR']; + if (targetBuildDir == null) { + globals.printError('Xcode build is missing expected TARGET_BUILD_DIR build setting.'); + return XcodeBuildResult(success: false); + } if (hasWatchCompanion && environmentType == EnvironmentType.simulator) { globals.printTrace('Replacing iphoneos with iphonesimulator in TARGET_BUILD_DIR.'); targetBuildDir = targetBuildDir.replaceFirst('iphoneos', 'iphonesimulator'); } - final String appBundle = buildSettings['WRAPPER_NAME']; + final String? appBundle = buildSettings['WRAPPER_NAME']; final String expectedOutputDirectory = globals.fs.path.join( targetBuildDir, appBundle, @@ -463,11 +469,11 @@ Future removeFinderExtendedAttributes(Directory projectDirectory, ProcessU } } -Future _runBuildWithRetries(List buildCommands, BuildableIOSApp app) async { +Future _runBuildWithRetries(List buildCommands, BuildableIOSApp app) async { int buildRetryDelaySeconds = 1; int remainingTries = 8; - RunResult buildResult; + RunResult? buildResult; while (remainingTries > 0) { remainingTries--; buildRetryDelaySeconds *= 2; @@ -507,13 +513,14 @@ return result.exitCode != 0 && } Future diagnoseXcodeBuildFailure(XcodeBuildResult result, Usage flutterUsage, Logger logger) async { - if (result.xcodeBuildExecution != null && - result.xcodeBuildExecution.environmentType == EnvironmentType.physical && - result.stdout?.toUpperCase()?.contains('BITCODE') == true) { + final XcodeBuildExecution? xcodeBuildExecution = result.xcodeBuildExecution; + if (xcodeBuildExecution != null && + xcodeBuildExecution.environmentType == EnvironmentType.physical && + result.stdout?.toUpperCase().contains('BITCODE') == true) { BuildEvent('xcode-bitcode-failure', type: 'ios', - command: result.xcodeBuildExecution.buildCommands.toString(), - settings: result.xcodeBuildExecution.buildSettings.toString(), + command: xcodeBuildExecution.buildCommands.toString(), + settings: xcodeBuildExecution.buildSettings.toString(), flutterUsage: flutterUsage, ).send(); } @@ -531,9 +538,8 @@ Future diagnoseXcodeBuildFailure(XcodeBuildResult result, Usage flutterUsa logger.printError(' flutter clean'); return; } - - if (result.xcodeBuildExecution != null && - result.xcodeBuildExecution.environmentType == EnvironmentType.physical && + if (xcodeBuildExecution != null && + xcodeBuildExecution.environmentType == EnvironmentType.physical && result.stdout?.contains('BCEROR') == true && // May need updating if Xcode changes its outputs. result.stdout?.contains("Xcode couldn't find a provisioning profile matching") == true) { @@ -543,16 +549,16 @@ Future diagnoseXcodeBuildFailure(XcodeBuildResult result, Usage flutterUsa // Make sure the user has specified one of: // * DEVELOPMENT_TEAM (automatic signing) // * PROVISIONING_PROFILE (manual signing) - if (result.xcodeBuildExecution != null && - result.xcodeBuildExecution.environmentType == EnvironmentType.physical && + if (xcodeBuildExecution != null && + xcodeBuildExecution.environmentType == EnvironmentType.physical && !['DEVELOPMENT_TEAM', 'PROVISIONING_PROFILE'].any( - result.xcodeBuildExecution.buildSettings.containsKey)) { + xcodeBuildExecution.buildSettings.containsKey)) { logger.printError(noDevelopmentTeamInstruction, emphasis: true); return; } - if (result.xcodeBuildExecution != null && - result.xcodeBuildExecution.environmentType == EnvironmentType.physical && - result.xcodeBuildExecution.buildSettings['PRODUCT_BUNDLE_IDENTIFIER']?.contains('com.example') == true) { + if (xcodeBuildExecution != null && + xcodeBuildExecution.environmentType == EnvironmentType.physical && + xcodeBuildExecution.buildSettings['PRODUCT_BUNDLE_IDENTIFIER']?.contains('com.example') == true) { logger.printError(''); logger.printError('It appears that your application still contains the default signing identifier.'); logger.printError("Try replacing 'com.example' with your signing id in Xcode:"); @@ -589,7 +595,7 @@ String xcodeBuildActionToString(XcodeBuildAction action) { class XcodeBuildResult { XcodeBuildResult({ - @required this.success, + required this.success, this.output, this.stdout, this.stderr, @@ -597,20 +603,20 @@ class XcodeBuildResult { }); final bool success; - final String output; - final String stdout; - final String stderr; + final String? output; + final String? stdout; + final String? stderr; /// The invocation of the build that resulted in this result instance. - final XcodeBuildExecution xcodeBuildExecution; + final XcodeBuildExecution? xcodeBuildExecution; } /// Describes an invocation of a Xcode build command. class XcodeBuildExecution { XcodeBuildExecution({ - @required this.buildCommands, - @required this.appDirectory, - @required this.environmentType, - @required this.buildSettings, + required this.buildCommands, + required this.appDirectory, + required this.environmentType, + required this.buildSettings, }); /// The original list of Xcode build commands used to produce this build result. @@ -627,12 +633,13 @@ bool _checkXcodeVersion() { if (!globals.platform.isMacOS) { return false; } - if (!globals.xcodeProjectInterpreter.isInstalled) { + final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter; + if (xcodeProjectInterpreter?.isInstalled != true) { globals.printError('Cannot find "xcodebuild". $_xcodeRequirement'); return false; } - if (!globals.xcode.isRequiredVersionSatisfactory) { - globals.printError('Found "${globals.xcodeProjectInterpreter.versionText}". $_xcodeRequirement'); + if (globals.xcode?.isRequiredVersionSatisfactory != true) { + globals.printError('Found "${xcodeProjectInterpreter?.versionText}". $_xcodeRequirement'); return false; } return true; @@ -649,10 +656,10 @@ bool upgradePbxProjWithFlutterAssets(IosProject project, Logger logger) { final Set printedStatuses = {}; for (final String line in lines) { - final Match match = oldAssets.firstMatch(line); + final Match? match = oldAssets.firstMatch(line); if (match != null) { - if (printedStatuses.add(match.group(1))) { - logger.printStatus('Removing obsolete reference to ${match.group(1)} from ${project.xcodeProject?.basename}'); + if (printedStatuses.add(match.group(1)!)) { + logger.printStatus('Removing obsolete reference to ${match.group(1)} from ${project.xcodeProject.basename}'); } } else { buffer.writeln(line); diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart index 27bdad85311..59b9daaf851 100644 --- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart +++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart @@ -373,7 +373,7 @@ class XcodeProjectBuildContext { /// /// Represents the output of `xcodebuild -list`. class XcodeProjectInfo { - XcodeProjectInfo( + const XcodeProjectInfo( this.targets, this.buildConfigurations, this.schemes, diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart index 95215b26c00..889a95ea858 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart @@ -86,18 +86,88 @@ void main() { BufferLogger logger; Xcode xcode; FakeXcodeProjectInterpreter fakeXcodeProjectInterpreter; + XcodeProjectInfo projectInfo; setUp(() { logger = BufferLogger.test(); fileSystem = MemoryFileSystem.test(); processManager = FakeProcessManager.empty(); - fakeXcodeProjectInterpreter = FakeXcodeProjectInterpreter(); + projectInfo = XcodeProjectInfo( + ['Runner'], + ['Debug', 'Release'], + ['Runner'], + logger, + ); + fakeXcodeProjectInterpreter = FakeXcodeProjectInterpreter(projectInfo: projectInfo); xcode = Xcode.test(processManager: FakeProcessManager.any(), xcodeProjectInterpreter: fakeXcodeProjectInterpreter); fileSystem.file('foo/.packages') ..createSync(recursive: true) ..writeAsStringSync('\n'); }); + testUsingContext('missing TARGET_BUILD_DIR', () async { + final IOSDevice iosDevice = setUpIOSDevice( + fileSystem: fileSystem, + processManager: processManager, + logger: logger, + artifacts: artifacts, + ); + setUpIOSProject(fileSystem); + final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); + final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app'); + + processManager.addCommand(FakeCommand(command: _xattrArgs(flutterProject))); + processManager.addCommand(const FakeCommand(command: kRunReleaseArgs)); + + final LaunchResult launchResult = await iosDevice.startApp( + buildableIOSApp, + debuggingOptions: DebuggingOptions.disabled(BuildInfo.release), + platformArgs: {}, + ); + + expect(launchResult.started, false); + expect(logger.errorText, contains('Xcode build is missing expected TARGET_BUILD_DIR build setting')); + expect(processManager, hasNoRemainingExpectations); + }, overrides: { + ProcessManager: () => processManager, + FileSystem: () => fileSystem, + Logger: () => logger, + Platform: () => macPlatform, + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(buildSettings: const { + 'WRAPPER_NAME': 'My Super Awesome App.app', + 'DEVELOPMENT_TEAM': '3333CCCC33', + }, projectInfo: projectInfo), + Xcode: () => xcode, + }); + + testUsingContext('missing project info', () async { + final IOSDevice iosDevice = setUpIOSDevice( + fileSystem: fileSystem, + processManager: FakeProcessManager.any(), + logger: logger, + artifacts: artifacts, + ); + setUpIOSProject(fileSystem); + final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); + final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app'); + + final LaunchResult launchResult = await iosDevice.startApp( + buildableIOSApp, + debuggingOptions: DebuggingOptions.disabled(BuildInfo.release), + platformArgs: {}, + ); + + expect(launchResult.started, false); + expect(logger.errorText, contains('Xcode project not found')); + }, overrides: { + ProcessManager: () => FakeProcessManager.any(), + FileSystem: () => fileSystem, + Logger: () => logger, + Platform: () => macPlatform, + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(projectInfo: null), + Xcode: () => xcode, + }); + testUsingContext('with buildable app', () async { final IOSDevice iosDevice = setUpIOSDevice( fileSystem: fileSystem, @@ -244,7 +314,8 @@ IOSDevice setUpIOSDevice({ ); logger ??= BufferLogger.test(); - return IOSDevice('123', + return IOSDevice( + '123', name: 'iPhone 1', sdkVersion: sdkVersion, fileSystem: fileSystem ?? MemoryFileSystem.test(), @@ -270,12 +341,27 @@ IOSDevice setUpIOSDevice({ } class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter { + FakeXcodeProjectInterpreter({ + @required this.projectInfo, + this.buildSettings = const { + 'TARGET_BUILD_DIR': 'build/ios/Release-iphoneos', + 'WRAPPER_NAME': 'My Super Awesome App.app', + 'DEVELOPMENT_TEAM': '3333CCCC33', + }, + }); + + final Map buildSettings; + final XcodeProjectInfo projectInfo; + @override final bool isInstalled = true; @override final Version version = Version(1000, 0, 0); + @override + String get versionText => version.toString(); + @override List xcrunCommand() => ['xcrun']; @@ -283,23 +369,12 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete Future getInfo( String projectPath, { String projectFilename, - }) async => - XcodeProjectInfo( - ['Runner'], - ['Debug', 'Release'], - ['Runner'], - BufferLogger.test(), - ); + }) async => projectInfo; @override Future> getBuildSettings( String projectPath, { @required XcodeProjectBuildContext buildContext, Duration timeout = const Duration(minutes: 1), - }) async => - { - 'TARGET_BUILD_DIR': 'build/ios/Release-iphoneos', - 'WRAPPER_NAME': 'My Super Awesome App.app', - 'DEVELOPMENT_TEAM': '3333CCCC33', - }; + }) async => buildSettings; } diff --git a/packages/flutter_tools/test/general.shard/ios/mac_test.dart b/packages/flutter_tools/test/general.shard/ios/mac_test.dart index d252a15a317..682703256b3 100644 --- a/packages/flutter_tools/test/general.shard/ios/mac_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/mac_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart = 2.8 - import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/artifacts.dart'; @@ -23,15 +21,15 @@ import '../../src/fake_process_manager.dart'; import '../../src/fakes.dart'; void main() { - BufferLogger logger; + late BufferLogger logger; setUp(() { logger = BufferLogger.test(); }); group('IMobileDevice', () { - Artifacts artifacts; - Cache cache; + late Artifacts artifacts; + late Cache cache; setUp(() { artifacts = Artifacts.test(); @@ -44,8 +42,8 @@ void main() { }); group('screenshot', () { - FakeProcessManager fakeProcessManager; - File outputFile; + late FakeProcessManager fakeProcessManager; + late File outputFile; setUp(() { fakeProcessManager = FakeProcessManager.empty(); @@ -131,8 +129,8 @@ void main() { }); group('Diagnose Xcode build failure', () { - Map buildSettings; - TestUsage testUsage; + late Map buildSettings; + late TestUsage testUsage; setUp(() { buildSettings = { @@ -445,7 +443,7 @@ Exited (sigterm)''', }); group('remove Finder extended attributes', () { - Directory projectDirectory; + late Directory projectDirectory; setUp(() { final MemoryFileSystem fs = MemoryFileSystem.test(); projectDirectory = fs.directory('flutter_project'); @@ -494,5 +492,5 @@ class FakeIosProject extends Fake implements IosProject { Future hostAppBundleName(BuildInfo buildInfo) async => 'UnitTestRunner.app'; @override - final Directory xcodeProject = null; + Directory get xcodeProject => xcodeProjectInfoFile.fileSystem.directory('Runner.xcodeproj'); }