Skip to content

Commit b93d46e

Browse files
jmagmanTytaniumDev
authored andcommitted
Use Xcode build setting PRODUCT_NAME to find app and archive paths (flutter#140242)
1. Instead of getting the `FULL_PRODUCT_NAME` Xcode build setting (`Runner.app`) instead use `PRODUCT_NAME` since most places really want the product name, and the extension stripping wasn't correct when the name contained periods. 2. Don't instruct the user to open the `xcarchive` in Xcode if it doesn't exist. Fixes flutter#140212
1 parent 6a662bb commit b93d46e

File tree

9 files changed

+66
-43
lines changed

9 files changed

+66
-43
lines changed

packages/flutter_tools/lib/src/commands/build_ios.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,14 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
517517

518518
globals.printError('Encountered error while creating the IPA:');
519519
globals.printError(errorMessage.toString());
520-
globals.printError('Try distributing the app in Xcode: "open $absoluteArchivePath"');
520+
521+
final FileSystemEntityType type = globals.fs.typeSync(absoluteArchivePath);
522+
globals.printError('Try distributing the app in Xcode:');
523+
if (type == FileSystemEntityType.notFound) {
524+
globals.printError('open ios/Runner.xcworkspace', indent: 2);
525+
} else {
526+
globals.printError('open $absoluteArchivePath', indent: 2);
527+
}
521528

522529
// Even though the IPA step didn't succeed, the xcarchive did.
523530
// Still count this as success since the user has been instructed about how to

packages/flutter_tools/lib/src/ios/application_package.dart

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -108,25 +108,25 @@ abstract class IOSApp extends ApplicationPackage {
108108
}
109109

110110
class BuildableIOSApp extends IOSApp {
111-
BuildableIOSApp(this.project, String projectBundleId, String? hostAppBundleName)
112-
: _hostAppBundleName = hostAppBundleName,
111+
BuildableIOSApp(this.project, String projectBundleId, String? productName)
112+
: _appProductName = productName,
113113
super(projectBundleId: projectBundleId);
114114

115115
static Future<BuildableIOSApp?> fromProject(IosProject project, BuildInfo? buildInfo) async {
116-
final String? hostAppBundleName = await project.hostAppBundleName(buildInfo);
116+
final String? productName = await project.productName(buildInfo);
117117
final String? projectBundleId = await project.productBundleIdentifier(buildInfo);
118118
if (projectBundleId != null) {
119-
return BuildableIOSApp(project, projectBundleId, hostAppBundleName);
119+
return BuildableIOSApp(project, projectBundleId, productName);
120120
}
121121
return null;
122122
}
123123

124124
final IosProject project;
125125

126-
final String? _hostAppBundleName;
126+
final String? _appProductName;
127127

128128
@override
129-
String? get name => _hostAppBundleName;
129+
String? get name => _appProductName;
130130

131131
@override
132132
String get simulatorBundlePath => _buildAppPath('iphonesimulator');
@@ -141,16 +141,15 @@ class BuildableIOSApp extends IOSApp {
141141
// not a top-level output directory.
142142
// Specifying `build/ios/archive/Runner` will result in `build/ios/archive/Runner.xcarchive`.
143143
String get archiveBundlePath => globals.fs.path.join(getIosBuildDirectory(), 'archive',
144-
_hostAppBundleName == null ? 'Runner' : globals.fs.path.withoutExtension(_hostAppBundleName));
144+
_appProductName ?? 'Runner');
145145

146146
// The output xcarchive bundle path `build/ios/archive/Runner.xcarchive`.
147-
String get archiveBundleOutputPath =>
148-
globals.fs.path.setExtension(archiveBundlePath, '.xcarchive');
147+
String get archiveBundleOutputPath => '$archiveBundlePath.xcarchive';
149148

150149
String get builtInfoPlistPathAfterArchive => globals.fs.path.join(archiveBundleOutputPath,
151150
'Products',
152151
'Applications',
153-
_hostAppBundleName ?? 'Runner.app',
152+
_appProductName != null ? '$_appProductName.app' : 'Runner.app',
154153
'Info.plist');
155154

156155
String get projectAppIconDirName => _projectImageAssetDirName(_appIconAsset);
@@ -173,7 +172,7 @@ class BuildableIOSApp extends IOSApp {
173172
globals.fs.path.join(getIosBuildDirectory(), 'ipa');
174173

175174
String _buildAppPath(String type) {
176-
return globals.fs.path.join(getIosBuildDirectory(), type, _hostAppBundleName);
175+
return globals.fs.path.join(getIosBuildDirectory(), type, '$_appProductName.app');
177176
}
178177

179178
String _projectImageAssetDirName(String asset)

packages/flutter_tools/lib/src/xcode_project.dart

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ class IosProject extends XcodeBasedProject {
174174
static const String kProductBundleIdKey = 'PRODUCT_BUNDLE_IDENTIFIER';
175175
static const String kTeamIdKey = 'DEVELOPMENT_TEAM';
176176
static const String kEntitlementFilePathKey = 'CODE_SIGN_ENTITLEMENTS';
177-
static const String kHostAppBundleNameKey = 'FULL_PRODUCT_NAME';
177+
static const String kProductNameKey = 'PRODUCT_NAME';
178178

179179
static final RegExp _productBundleIdPattern = RegExp('^\\s*$kProductBundleIdKey\\s*=\\s*(["\']?)(.*?)\\1;\\s*\$');
180180
static const String _kProductBundleIdVariable = '\$($kProductBundleIdKey)';
@@ -397,16 +397,16 @@ class IosProject extends XcodeBasedProject {
397397
return const <String>[];
398398
}
399399

400-
/// The bundle name of the host app, `My App.app`.
401-
Future<String?> hostAppBundleName(BuildInfo? buildInfo) async {
400+
/// The product name of the app, `My App`.
401+
Future<String?> productName(BuildInfo? buildInfo) async {
402402
if (!existsSync()) {
403403
return null;
404404
}
405-
return _hostAppBundleName ??= await _parseHostAppBundleName(buildInfo);
405+
return _productName ??= await _parseProductName(buildInfo);
406406
}
407-
String? _hostAppBundleName;
407+
String? _productName;
408408

409-
Future<String> _parseHostAppBundleName(BuildInfo? buildInfo) async {
409+
Future<String> _parseProductName(BuildInfo? buildInfo) async {
410410
// The product name and bundle name are derived from the display name, which the user
411411
// is instructed to change in Xcode as part of deploying to the App Store.
412412
// https://flutter.dev/to/xcode-name-config
@@ -415,13 +415,13 @@ class IosProject extends XcodeBasedProject {
415415
if (globals.xcodeProjectInterpreter?.isInstalled ?? false) {
416416
final Map<String, String>? xcodeBuildSettings = await buildSettingsForBuildInfo(buildInfo);
417417
if (xcodeBuildSettings != null) {
418-
productName = xcodeBuildSettings[kHostAppBundleNameKey];
418+
productName = xcodeBuildSettings[kProductNameKey];
419419
}
420420
}
421421
if (productName == null) {
422-
globals.printTrace('$kHostAppBundleNameKey not present, defaulting to $hostAppProjectName');
422+
globals.printTrace('$kProductNameKey not present, defaulting to $hostAppProjectName');
423423
}
424-
return productName ?? '${XcodeBasedProject._defaultHostAppName}.app';
424+
return productName ?? XcodeBasedProject._defaultHostAppName;
425425
}
426426

427427
/// The build settings for the host app of this project, as a detached map.

packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -654,14 +654,18 @@ void main() {
654654
]);
655655
createMinimalMockProjectFiles();
656656

657+
fileSystem.directory('build/ios/archive/Runner.xcarchive').createSync(recursive: true);
658+
657659
await createTestCommandRunner(command).run(
658660
const <String>['build', 'ipa', '--no-pub']
659661
);
660662

661-
expect(logger.statusText, contains('build/ios/archive/Runner.xcarchive'));
663+
expect(logger.statusText, contains('Built build/ios/archive/Runner.xcarchive'));
662664
expect(logger.statusText, contains('Building App Store IPA'));
663665
expect(logger.errorText, contains('Encountered error while creating the IPA:'));
664666
expect(logger.errorText, contains('error: exportArchive: "Runner.app" requires a provisioning profile.'));
667+
expect(logger.errorText, contains('Try distributing the app in Xcode:'));
668+
expect(logger.errorText, contains('open /build/ios/archive/Runner.xcarchive'));
665669
expect(fakeProcessManager, hasNoRemainingExpectations);
666670
}, overrides: <Type, Generator>{
667671
FileSystem: () => fileSystem,
@@ -1231,7 +1235,7 @@ void main() {
12311235
});
12321236

12331237

1234-
testUsingContext('Extra error message for provision profile issue in xcresulb bundle.', () async {
1238+
testUsingContext('Extra error message for provision profile issue in xcresult bundle.', () async {
12351239
final BuildCommand command = BuildCommand(
12361240
artifacts: artifacts,
12371241
androidSdk: FakeAndroidSdk(),

packages/flutter_tools/test/general.shard/application_package_test.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,19 @@ void main() {
463463
expect(iosApp, null);
464464
}, overrides: overrides);
465465

466+
testUsingContext('handles project paths with periods in app name', () async {
467+
final BuildableIOSApp iosApp = BuildableIOSApp(
468+
IosProject.fromFlutter(FlutterProject.fromDirectory(globals.fs.currentDirectory)),
469+
'com.foo.bar',
470+
'Name.With.Dots',
471+
);
472+
expect(iosApp.name, 'Name.With.Dots');
473+
expect(iosApp.archiveBundleOutputPath, 'build/ios/archive/Name.With.Dots.xcarchive');
474+
expect(iosApp.deviceBundlePath, 'build/ios/iphoneos/Name.With.Dots.app');
475+
expect(iosApp.simulatorBundlePath, 'build/ios/iphonesimulator/Name.With.Dots.app');
476+
expect(iosApp.builtInfoPlistPathAfterArchive, 'build/ios/archive/Name.With.Dots.xcarchive/Products/Applications/Name.With.Dots.app/Info.plist');
477+
}, overrides: overrides);
478+
466479
testUsingContext('returns project app icon dirname', () async {
467480
final BuildableIOSApp iosApp = BuildableIOSApp(
468481
IosProject.fromFlutter(FlutterProject.fromDirectory(globals.fs.currentDirectory)),

packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ void main() {
127127
);
128128
setUpIOSProject(fileSystem);
129129
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
130-
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
130+
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App');
131131

132132
processManager.addCommand(FakeCommand(command: _xattrArgs(flutterProject)));
133133
processManager.addCommand(const FakeCommand(command: kRunReleaseArgs));
@@ -171,7 +171,7 @@ void main() {
171171
);
172172
setUpIOSProject(fileSystem);
173173
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
174-
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
174+
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App');
175175

176176
final LaunchResult launchResult = await iosDevice.startApp(
177177
buildableIOSApp,
@@ -199,7 +199,7 @@ void main() {
199199
);
200200
setUpIOSProject(fileSystem);
201201
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
202-
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
202+
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App');
203203
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
204204

205205
processManager.addCommand(FakeCommand(command: _xattrArgs(flutterProject)));
@@ -257,7 +257,7 @@ void main() {
257257
);
258258
setUpIOSProject(fileSystem);
259259
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
260-
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
260+
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App');
261261

262262
processManager.addCommand(FakeCommand(command: _xattrArgs(flutterProject)));
263263
// The first xcrun call should fail with a
@@ -346,7 +346,7 @@ void main() {
346346
);
347347
setUpIOSProject(fileSystem);
348348
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
349-
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
349+
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App');
350350
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
351351

352352
final LaunchResult launchResult = await iosDevice.startApp(
@@ -380,7 +380,7 @@ void main() {
380380
);
381381
setUpIOSProject(fileSystem);
382382
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
383-
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
383+
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App');
384384
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
385385

386386
final LaunchResult launchResult = await iosDevice.startApp(
@@ -414,7 +414,7 @@ void main() {
414414
);
415415
setUpIOSProject(fileSystem);
416416
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
417-
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
417+
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App');
418418
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
419419

420420
final LaunchResult launchResult = await iosDevice.startApp(
@@ -447,7 +447,7 @@ void main() {
447447
);
448448
setUpIOSProject(fileSystem);
449449
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
450-
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
450+
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App');
451451
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
452452

453453
final LaunchResult launchResult = await iosDevice.startApp(
@@ -496,7 +496,7 @@ void main() {
496496

497497
setUpIOSProject(fileSystem);
498498
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
499-
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
499+
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App');
500500
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
501501

502502
final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
@@ -571,7 +571,7 @@ void main() {
571571

572572
setUpIOSProject(fileSystem);
573573
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
574-
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
574+
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App');
575575
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
576576

577577
final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
@@ -639,7 +639,7 @@ void main() {
639639

640640
setUpIOSProject(fileSystem);
641641
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
642-
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
642+
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App');
643643
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
644644

645645
final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
@@ -702,7 +702,7 @@ void main() {
702702
);
703703
setUpIOSProject(fileSystem);
704704
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
705-
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
705+
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App');
706706
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
707707

708708
final LaunchResult launchResult = await iosDevice.startApp(
@@ -741,7 +741,7 @@ void main() {
741741
);
742742
setUpIOSProject(fileSystem, createWorkspace: false);
743743
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
744-
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
744+
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App');
745745
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
746746

747747
final LaunchResult launchResult = await iosDevice.startApp(
@@ -780,7 +780,7 @@ void main() {
780780
);
781781
setUpIOSProject(fileSystem);
782782
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
783-
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
783+
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App');
784784
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
785785

786786
final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();

packages/flutter_tools/test/general.shard/ios/mac_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -797,7 +797,7 @@ class FakeIosProject extends Fake implements IosProject {
797797
File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj');
798798

799799
@override
800-
Future<String> hostAppBundleName(BuildInfo? buildInfo) async => 'UnitTestRunner.app';
800+
Future<String> productName(BuildInfo? buildInfo) async => 'UnitTestRunner';
801801

802802
@override
803803
Directory get xcodeProject => hostAppRoot.childDirectory('Runner.xcodeproj');

packages/flutter_tools/test/general.shard/ios/simulators_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1332,7 +1332,7 @@ class FakeIosProject extends Fake implements IosProject {
13321332
Future<String> productBundleIdentifier(BuildInfo? buildInfo) async => 'com.example.test';
13331333

13341334
@override
1335-
Future<String> hostAppBundleName(BuildInfo? buildInfo) async => 'My Super Awesome App.app';
1335+
Future<String> productName(BuildInfo? buildInfo) async => 'My Super Awesome App';
13361336
}
13371337

13381338
class FakeSimControl extends Fake implements SimControl {

packages/flutter_tools/test/general.shard/project_test.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,9 +1188,9 @@ plugins {
11881188
mockXcodeProjectInterpreter = FakeXcodeProjectInterpreter();
11891189
});
11901190

1191-
testUsingContext('app product name defaults to Runner.app', () async {
1191+
testUsingContext('app product name defaults to Runner', () async {
11921192
final FlutterProject project = await someProject();
1193-
expect(await project.ios.hostAppBundleName(null), 'Runner.app');
1193+
expect(await project.ios.productName(null), 'Runner');
11941194
}, overrides: <Type, Generator>{
11951195
FileSystem: () => fs,
11961196
ProcessManager: () => FakeProcessManager.any(),
@@ -1202,11 +1202,11 @@ plugins {
12021202
project.ios.xcodeProject.createSync();
12031203
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
12041204
mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
1205-
'FULL_PRODUCT_NAME': 'My App.app',
1205+
'PRODUCT_NAME': 'My App',
12061206
};
12071207
mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
12081208

1209-
expect(await project.ios.hostAppBundleName(null), 'My App.app');
1209+
expect(await project.ios.productName(null), 'My App');
12101210
}, overrides: <Type, Generator>{
12111211
FileSystem: () => fs,
12121212
ProcessManager: () => FakeProcessManager.any(),

0 commit comments

Comments
 (0)