Use Xcode build setting PRODUCT_NAME to find app and archive paths (#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 https://github.com/flutter/flutter/issues/140212
This commit is contained in:
Jenn Magder 2024-07-22 16:54:24 -07:00 committed by GitHub
parent ebe53d570a
commit f33ffc00ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 66 additions and 43 deletions

View File

@ -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

View File

@ -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<BuildableIOSApp?> 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)

View File

@ -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 <String>[];
}
/// The bundle name of the host app, `My App.app`.
Future<String?> hostAppBundleName(BuildInfo? buildInfo) async {
/// The product name of the app, `My App`.
Future<String?> productName(BuildInfo? buildInfo) async {
if (!existsSync()) {
return null;
}
return _hostAppBundleName ??= await _parseHostAppBundleName(buildInfo);
return _productName ??= await _parseProductName(buildInfo);
}
String? _hostAppBundleName;
String? _productName;
Future<String> _parseHostAppBundleName(BuildInfo? buildInfo) async {
Future<String> _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<String, String>? 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.

View File

@ -654,14 +654,18 @@ void main() {
]);
createMinimalMockProjectFiles();
fileSystem.directory('build/ios/archive/Runner.xcarchive').createSync(recursive: true);
await createTestCommandRunner(command).run(
const <String>['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: <Type, Generator>{
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(),

View File

@ -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)),

View File

@ -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();

View File

@ -797,7 +797,7 @@ class FakeIosProject extends Fake implements IosProject {
File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj');
@override
Future<String> hostAppBundleName(BuildInfo? buildInfo) async => 'UnitTestRunner.app';
Future<String> productName(BuildInfo? buildInfo) async => 'UnitTestRunner';
@override
Directory get xcodeProject => hostAppRoot.childDirectory('Runner.xcodeproj');

View File

@ -1332,7 +1332,7 @@ class FakeIosProject extends Fake implements IosProject {
Future<String> productBundleIdentifier(BuildInfo? buildInfo) async => 'com.example.test';
@override
Future<String> hostAppBundleName(BuildInfo? buildInfo) async => 'My Super Awesome App.app';
Future<String> productName(BuildInfo? buildInfo) async => 'My Super Awesome App';
}
class FakeSimControl extends Fake implements SimControl {

View File

@ -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: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
@ -1202,11 +1202,11 @@ plugins {
project.ios.xcodeProject.createSync();
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
'FULL_PRODUCT_NAME': 'My App.app',
'PRODUCT_NAME': 'My App',
};
mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
expect(await project.ios.hostAppBundleName(null), 'My App.app');
expect(await project.ios.productName(null), 'My App');
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),