diff --git a/packages/flutter_tools/lib/src/commands/build_ios.dart b/packages/flutter_tools/lib/src/commands/build_ios.dart index 7d4f9f152c4..366f6f4f7ff 100644 --- a/packages/flutter_tools/lib/src/commands/build_ios.dart +++ b/packages/flutter_tools/lib/src/commands/build_ios.dart @@ -517,7 +517,14 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { globals.printError('Encountered error while creating the IPA:'); globals.printError(errorMessage.toString()); - globals.printError('Try distributing the app in Xcode: "open $absoluteArchivePath"'); + + final FileSystemEntityType type = globals.fs.typeSync(absoluteArchivePath); + globals.printError('Try distributing the app in Xcode:'); + if (type == FileSystemEntityType.notFound) { + globals.printError('open ios/Runner.xcworkspace', indent: 2); + } else { + globals.printError('open $absoluteArchivePath', indent: 2); + } // Even though the IPA step didn't succeed, the xcarchive did. // Still count this as success since the user has been instructed about how to diff --git a/packages/flutter_tools/lib/src/ios/application_package.dart b/packages/flutter_tools/lib/src/ios/application_package.dart index 5472bb6d1fc..777871e8d13 100644 --- a/packages/flutter_tools/lib/src/ios/application_package.dart +++ b/packages/flutter_tools/lib/src/ios/application_package.dart @@ -108,25 +108,25 @@ abstract class IOSApp extends ApplicationPackage { } class BuildableIOSApp extends IOSApp { - BuildableIOSApp(this.project, String projectBundleId, String? hostAppBundleName) - : _hostAppBundleName = hostAppBundleName, + BuildableIOSApp(this.project, String projectBundleId, String? productName) + : _appProductName = productName, super(projectBundleId: projectBundleId); static Future fromProject(IosProject project, BuildInfo? buildInfo) async { - final String? hostAppBundleName = await project.hostAppBundleName(buildInfo); + final String? productName = await project.productName(buildInfo); final String? projectBundleId = await project.productBundleIdentifier(buildInfo); if (projectBundleId != null) { - return BuildableIOSApp(project, projectBundleId, hostAppBundleName); + return BuildableIOSApp(project, projectBundleId, productName); } return null; } final IosProject project; - final String? _hostAppBundleName; + final String? _appProductName; @override - String? get name => _hostAppBundleName; + String? get name => _appProductName; @override String get simulatorBundlePath => _buildAppPath('iphonesimulator'); @@ -141,16 +141,15 @@ class BuildableIOSApp extends IOSApp { // not a top-level output directory. // Specifying `build/ios/archive/Runner` will result in `build/ios/archive/Runner.xcarchive`. String get archiveBundlePath => globals.fs.path.join(getIosBuildDirectory(), 'archive', - _hostAppBundleName == null ? 'Runner' : globals.fs.path.withoutExtension(_hostAppBundleName)); + _appProductName ?? 'Runner'); // The output xcarchive bundle path `build/ios/archive/Runner.xcarchive`. - String get archiveBundleOutputPath => - globals.fs.path.setExtension(archiveBundlePath, '.xcarchive'); + String get archiveBundleOutputPath => '$archiveBundlePath.xcarchive'; String get builtInfoPlistPathAfterArchive => globals.fs.path.join(archiveBundleOutputPath, 'Products', 'Applications', - _hostAppBundleName ?? 'Runner.app', + _appProductName != null ? '$_appProductName.app' : 'Runner.app', 'Info.plist'); String get projectAppIconDirName => _projectImageAssetDirName(_appIconAsset); @@ -173,7 +172,7 @@ class BuildableIOSApp extends IOSApp { globals.fs.path.join(getIosBuildDirectory(), 'ipa'); String _buildAppPath(String type) { - return globals.fs.path.join(getIosBuildDirectory(), type, _hostAppBundleName); + return globals.fs.path.join(getIosBuildDirectory(), type, '$_appProductName.app'); } String _projectImageAssetDirName(String asset) diff --git a/packages/flutter_tools/lib/src/xcode_project.dart b/packages/flutter_tools/lib/src/xcode_project.dart index bdfb3d89b5f..375db04a3dc 100644 --- a/packages/flutter_tools/lib/src/xcode_project.dart +++ b/packages/flutter_tools/lib/src/xcode_project.dart @@ -174,7 +174,7 @@ class IosProject extends XcodeBasedProject { static const String kProductBundleIdKey = 'PRODUCT_BUNDLE_IDENTIFIER'; static const String kTeamIdKey = 'DEVELOPMENT_TEAM'; static const String kEntitlementFilePathKey = 'CODE_SIGN_ENTITLEMENTS'; - static const String kHostAppBundleNameKey = 'FULL_PRODUCT_NAME'; + static const String kProductNameKey = 'PRODUCT_NAME'; static final RegExp _productBundleIdPattern = RegExp('^\\s*$kProductBundleIdKey\\s*=\\s*(["\']?)(.*?)\\1;\\s*\$'); static const String _kProductBundleIdVariable = '\$($kProductBundleIdKey)'; @@ -397,16 +397,16 @@ class IosProject extends XcodeBasedProject { return const []; } - /// The bundle name of the host app, `My App.app`. - Future hostAppBundleName(BuildInfo? buildInfo) async { + /// The product name of the app, `My App`. + Future productName(BuildInfo? buildInfo) async { if (!existsSync()) { return null; } - return _hostAppBundleName ??= await _parseHostAppBundleName(buildInfo); + return _productName ??= await _parseProductName(buildInfo); } - String? _hostAppBundleName; + String? _productName; - Future _parseHostAppBundleName(BuildInfo? buildInfo) async { + Future _parseProductName(BuildInfo? buildInfo) async { // The product name and bundle name are derived from the display name, which the user // is instructed to change in Xcode as part of deploying to the App Store. // https://flutter.dev/to/xcode-name-config @@ -415,13 +415,13 @@ class IosProject extends XcodeBasedProject { if (globals.xcodeProjectInterpreter?.isInstalled ?? false) { final Map? xcodeBuildSettings = await buildSettingsForBuildInfo(buildInfo); if (xcodeBuildSettings != null) { - productName = xcodeBuildSettings[kHostAppBundleNameKey]; + productName = xcodeBuildSettings[kProductNameKey]; } } if (productName == null) { - globals.printTrace('$kHostAppBundleNameKey not present, defaulting to $hostAppProjectName'); + globals.printTrace('$kProductNameKey not present, defaulting to $hostAppProjectName'); } - return productName ?? '${XcodeBasedProject._defaultHostAppName}.app'; + return productName ?? XcodeBasedProject._defaultHostAppName; } /// The build settings for the host app of this project, as a detached map. diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart index 8da16f3a722..68de868604d 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart @@ -654,14 +654,18 @@ void main() { ]); createMinimalMockProjectFiles(); + fileSystem.directory('build/ios/archive/Runner.xcarchive').createSync(recursive: true); + await createTestCommandRunner(command).run( const ['build', 'ipa', '--no-pub'] ); - expect(logger.statusText, contains('build/ios/archive/Runner.xcarchive')); + expect(logger.statusText, contains('Built build/ios/archive/Runner.xcarchive')); expect(logger.statusText, contains('Building App Store IPA')); expect(logger.errorText, contains('Encountered error while creating the IPA:')); expect(logger.errorText, contains('error: exportArchive: "Runner.app" requires a provisioning profile.')); + expect(logger.errorText, contains('Try distributing the app in Xcode:')); + expect(logger.errorText, contains('open /build/ios/archive/Runner.xcarchive')); expect(fakeProcessManager, hasNoRemainingExpectations); }, overrides: { FileSystem: () => fileSystem, @@ -1231,7 +1235,7 @@ void main() { }); - testUsingContext('Extra error message for provision profile issue in xcresulb bundle.', () async { + testUsingContext('Extra error message for provision profile issue in xcresult bundle.', () async { final BuildCommand command = BuildCommand( artifacts: artifacts, androidSdk: FakeAndroidSdk(), diff --git a/packages/flutter_tools/test/general.shard/application_package_test.dart b/packages/flutter_tools/test/general.shard/application_package_test.dart index 9989502a04f..c024196838f 100644 --- a/packages/flutter_tools/test/general.shard/application_package_test.dart +++ b/packages/flutter_tools/test/general.shard/application_package_test.dart @@ -463,6 +463,19 @@ void main() { expect(iosApp, null); }, overrides: overrides); + testUsingContext('handles project paths with periods in app name', () async { + final BuildableIOSApp iosApp = BuildableIOSApp( + IosProject.fromFlutter(FlutterProject.fromDirectory(globals.fs.currentDirectory)), + 'com.foo.bar', + 'Name.With.Dots', + ); + expect(iosApp.name, 'Name.With.Dots'); + expect(iosApp.archiveBundleOutputPath, 'build/ios/archive/Name.With.Dots.xcarchive'); + expect(iosApp.deviceBundlePath, 'build/ios/iphoneos/Name.With.Dots.app'); + expect(iosApp.simulatorBundlePath, 'build/ios/iphonesimulator/Name.With.Dots.app'); + expect(iosApp.builtInfoPlistPathAfterArchive, 'build/ios/archive/Name.With.Dots.xcarchive/Products/Applications/Name.With.Dots.app/Info.plist'); + }, overrides: overrides); + testUsingContext('returns project app icon dirname', () async { final BuildableIOSApp iosApp = BuildableIOSApp( IosProject.fromFlutter(FlutterProject.fromDirectory(globals.fs.currentDirectory)), 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 fb9428b2918..9eadb5751f2 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 @@ -127,7 +127,7 @@ void main() { ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); - final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app'); + final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App'); processManager.addCommand(FakeCommand(command: _xattrArgs(flutterProject))); processManager.addCommand(const FakeCommand(command: kRunReleaseArgs)); @@ -171,7 +171,7 @@ void main() { ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); - final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app'); + final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App'); final LaunchResult launchResult = await iosDevice.startApp( buildableIOSApp, @@ -199,7 +199,7 @@ void main() { ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); - final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app'); + final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App'); fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true); processManager.addCommand(FakeCommand(command: _xattrArgs(flutterProject))); @@ -257,7 +257,7 @@ void main() { ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); - final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app'); + final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App'); processManager.addCommand(FakeCommand(command: _xattrArgs(flutterProject))); // The first xcrun call should fail with a @@ -346,7 +346,7 @@ void main() { ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); - final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app'); + final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App'); fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true); final LaunchResult launchResult = await iosDevice.startApp( @@ -380,7 +380,7 @@ void main() { ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); - final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app'); + final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App'); fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true); final LaunchResult launchResult = await iosDevice.startApp( @@ -414,7 +414,7 @@ void main() { ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); - final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app'); + final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App'); fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true); final LaunchResult launchResult = await iosDevice.startApp( @@ -447,7 +447,7 @@ void main() { ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); - final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app'); + final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App'); fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true); final LaunchResult launchResult = await iosDevice.startApp( @@ -496,7 +496,7 @@ void main() { setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); - final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app'); + final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App'); fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true); final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader(); @@ -571,7 +571,7 @@ void main() { setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); - final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app'); + final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App'); fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true); final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader(); @@ -639,7 +639,7 @@ void main() { setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); - final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app'); + final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App'); fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true); final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader(); @@ -702,7 +702,7 @@ void main() { ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); - final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app'); + final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App'); fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true); final LaunchResult launchResult = await iosDevice.startApp( @@ -741,7 +741,7 @@ void main() { ); setUpIOSProject(fileSystem, createWorkspace: false); final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); - final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app'); + final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App'); fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true); final LaunchResult launchResult = await iosDevice.startApp( @@ -780,7 +780,7 @@ void main() { ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); - final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app'); + final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App'); fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true); final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader(); 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 efee0008a34..a21455357cf 100644 --- a/packages/flutter_tools/test/general.shard/ios/mac_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/mac_test.dart @@ -797,7 +797,7 @@ class FakeIosProject extends Fake implements IosProject { File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj'); @override - Future hostAppBundleName(BuildInfo? buildInfo) async => 'UnitTestRunner.app'; + Future productName(BuildInfo? buildInfo) async => 'UnitTestRunner'; @override Directory get xcodeProject => hostAppRoot.childDirectory('Runner.xcodeproj'); diff --git a/packages/flutter_tools/test/general.shard/ios/simulators_test.dart b/packages/flutter_tools/test/general.shard/ios/simulators_test.dart index 1a9d5eec3fd..ce27d1bc9cc 100644 --- a/packages/flutter_tools/test/general.shard/ios/simulators_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/simulators_test.dart @@ -1332,7 +1332,7 @@ class FakeIosProject extends Fake implements IosProject { Future productBundleIdentifier(BuildInfo? buildInfo) async => 'com.example.test'; @override - Future hostAppBundleName(BuildInfo? buildInfo) async => 'My Super Awesome App.app'; + Future productName(BuildInfo? buildInfo) async => 'My Super Awesome App'; } class FakeSimControl extends Fake implements SimControl { diff --git a/packages/flutter_tools/test/general.shard/project_test.dart b/packages/flutter_tools/test/general.shard/project_test.dart index ab404fb616d..f624982cf3a 100644 --- a/packages/flutter_tools/test/general.shard/project_test.dart +++ b/packages/flutter_tools/test/general.shard/project_test.dart @@ -1188,9 +1188,9 @@ plugins { mockXcodeProjectInterpreter = FakeXcodeProjectInterpreter(); }); - testUsingContext('app product name defaults to Runner.app', () async { + testUsingContext('app product name defaults to Runner', () async { final FlutterProject project = await someProject(); - expect(await project.ios.hostAppBundleName(null), 'Runner.app'); + expect(await project.ios.productName(null), 'Runner'); }, overrides: { FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), @@ -1202,11 +1202,11 @@ plugins { project.ios.xcodeProject.createSync(); const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner'); mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = { - 'FULL_PRODUCT_NAME': 'My App.app', + 'PRODUCT_NAME': 'My App', }; mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo([], [], ['Runner'], logger); - expect(await project.ios.hostAppBundleName(null), 'My App.app'); + expect(await project.ios.productName(null), 'My App'); }, overrides: { FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(),