Skip to content

Commit e6300da

Browse files
authored
[tools]validation basic Xcode settings for build ipa (#113412)
1 parent 92a6668 commit e6300da

File tree

5 files changed

+158
-10
lines changed

5 files changed

+158
-10
lines changed

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import '../convert.dart';
1515
import '../globals.dart' as globals;
1616
import '../ios/application_package.dart';
1717
import '../ios/mac.dart';
18+
import '../ios/plist_parser.dart';
1819
import '../runner/flutter_command.dart';
1920
import 'build.dart';
2021

@@ -129,12 +130,49 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
129130
return super.validateCommand();
130131
}
131132

133+
Future<void> _validateXcodeBuildSettingsAfterArchive() async {
134+
final BuildableIOSApp app = await buildableIOSApp;
135+
136+
final String plistPath = app.builtInfoPlistPathAfterArchive;
137+
138+
if (!globals.fs.file(plistPath).existsSync()) {
139+
globals.printError('Invalid iOS archive. Does not contain Info.plist.');
140+
return;
141+
}
142+
143+
final Map<String, String?> xcodeProjectSettingsMap = <String, String?>{};
144+
145+
xcodeProjectSettingsMap['Version Number'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleShortVersionStringKey);
146+
xcodeProjectSettingsMap['Build Number'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleVersionKey);
147+
xcodeProjectSettingsMap['Display Name'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleDisplayNameKey);
148+
xcodeProjectSettingsMap['Deployment Target'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kMinimumOSVersionKey);
149+
xcodeProjectSettingsMap['Bundle Identifier'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleIdentifierKey);
150+
151+
final StringBuffer buffer = StringBuffer();
152+
xcodeProjectSettingsMap.forEach((String title, String? info) {
153+
buffer.writeln('$title: ${info ?? "Missing"}');
154+
});
155+
156+
final String message;
157+
if (xcodeProjectSettingsMap.values.any((String? element) => element == null)) {
158+
buffer.writeln('\nYou must set up the missing settings');
159+
buffer.write('Instructions: https://docs.flutter.dev/deployment/ios');
160+
message = buffer.toString();
161+
} else {
162+
// remove the new line
163+
message = buffer.toString().trim();
164+
}
165+
globals.printBox(message, title: 'App Settings');
166+
}
167+
132168
@override
133169
Future<FlutterCommandResult> runCommand() async {
134170
final BuildInfo buildInfo = await cachedBuildInfo;
135171
displayNullSafetyMode(buildInfo);
136172
final FlutterCommandResult xcarchiveResult = await super.runCommand();
137173

174+
await _validateXcodeBuildSettingsAfterArchive();
175+
138176
// xcarchive failed or not at expected location.
139177
if (xcarchiveResult.exitStatus != ExitStatus.success) {
140178
globals.printStatus('Skipping IPA.');
@@ -289,6 +327,7 @@ abstract class _BuildIOSSubCommand extends BuildSubCommand {
289327
/// The result of the Xcode build command. Null until it finishes.
290328
@protected
291329
XcodeBuildResult? xcodeBuildResult;
330+
292331
EnvironmentType get environmentType;
293332
bool get configOnly;
294333

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,12 @@ class BuildableIOSApp extends IOSApp {
145145
String get archiveBundleOutputPath =>
146146
globals.fs.path.setExtension(archiveBundlePath, '.xcarchive');
147147

148+
String get builtInfoPlistPathAfterArchive => globals.fs.path.join(archiveBundleOutputPath,
149+
'Products',
150+
'Applications',
151+
_hostAppBundleName == null ? 'Runner.app' : _hostAppBundleName!,
152+
'Info.plist');
153+
148154
String get ipaOutputPath =>
149155
globals.fs.path.join(getIosBuildDirectory(), 'ipa');
150156

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ class PlistParser {
2626

2727
static const String kCFBundleIdentifierKey = 'CFBundleIdentifier';
2828
static const String kCFBundleShortVersionStringKey = 'CFBundleShortVersionString';
29-
static const String kCFBundleExecutable = 'CFBundleExecutable';
29+
static const String kCFBundleExecutableKey = 'CFBundleExecutable';
30+
static const String kCFBundleVersionKey = 'CFBundleVersion';
31+
static const String kCFBundleDisplayNameKey = 'CFBundleDisplayName';
32+
static const String kMinimumOSVersionKey = 'MinimumOSVersion';
3033

3134
/// Returns the content, converted to XML, of the plist file located at
3235
/// [plistFilePath].

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ abstract class MacOSApp extends ApplicationPackage {
8787
}
8888
final Map<String, dynamic> propertyValues = globals.plistParser.parseFile(plistPath);
8989
final String? id = propertyValues[PlistParser.kCFBundleIdentifierKey] as String?;
90-
final String? executableName = propertyValues[PlistParser.kCFBundleExecutable] as String?;
90+
final String? executableName = propertyValues[PlistParser.kCFBundleExecutableKey] as String?;
9191
if (id == null) {
9292
globals.printError('Invalid prebuilt macOS app. Info.plist does not contain bundle identifier');
9393
return null;

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

Lines changed: 108 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import 'package:flutter_tools/src/base/platform.dart';
99
import 'package:flutter_tools/src/cache.dart';
1010
import 'package:flutter_tools/src/commands/build.dart';
1111
import 'package:flutter_tools/src/commands/build_ios.dart';
12+
import 'package:flutter_tools/src/ios/plist_parser.dart';
1213
import 'package:flutter_tools/src/ios/xcodeproj.dart';
1314
import 'package:flutter_tools/src/reporting/reporting.dart';
15+
import 'package:test/fake.dart';
1416

1517
import '../../general.shard/ios/xcresult_test_data.dart';
1618
import '../../src/common.dart';
@@ -50,10 +52,20 @@ final Platform notMacosPlatform = FakePlatform(
5052
}
5153
);
5254

55+
class FakePlistUtils extends Fake implements PlistParser {
56+
final Map<String, Map<String, Object>> fileContents = <String, Map<String, Object>>{};
57+
58+
@override
59+
String? getStringValueFromFile(String plistFilePath, String key) {
60+
return fileContents[plistFilePath]![key] as String?;
61+
}
62+
}
63+
5364
void main() {
5465
late FileSystem fileSystem;
5566
late TestUsage usage;
5667
late FakeProcessManager fakeProcessManager;
68+
late FakePlistUtils plistUtils;
5769

5870
setUpAll(() {
5971
Cache.disableLocking();
@@ -63,6 +75,7 @@ void main() {
6375
fileSystem = MemoryFileSystem.test();
6476
usage = TestUsage();
6577
fakeProcessManager = FakeProcessManager.empty();
78+
plistUtils = FakePlistUtils();
6679
});
6780

6881
// Sets up the minimal mock project files necessary to look like a Flutter project.
@@ -246,8 +259,7 @@ void main() {
246259
FileSystem: () => fileSystem,
247260
ProcessManager: () => FakeProcessManager.any(),
248261
Platform: () => macosPlatform,
249-
XcodeProjectInterpreter: () =>
250-
FakeXcodeProjectInterpreterWithBuildSettings(),
262+
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
251263
});
252264

253265
testUsingContext('ipa build fails when --export-options-plist and --export-method are used together', () async {
@@ -270,8 +282,7 @@ void main() {
270282
FileSystem: () => fileSystem,
271283
ProcessManager: () => FakeProcessManager.any(),
272284
Platform: () => macosPlatform,
273-
XcodeProjectInterpreter: () =>
274-
FakeXcodeProjectInterpreterWithBuildSettings(),
285+
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
275286
});
276287

277288
testUsingContext('ipa build reports when IPA fails', () async {
@@ -521,8 +532,7 @@ void main() {
521532
FileSystem: () => fileSystem,
522533
ProcessManager: () => FakeProcessManager.any(),
523534
Platform: () => macosPlatform,
524-
XcodeProjectInterpreter: () =>
525-
FakeXcodeProjectInterpreterWithBuildSettings(),
535+
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
526536
});
527537

528538
testUsingContext('Performs code size analysis and sends analytics', () async {
@@ -601,8 +611,7 @@ void main() {
601611
FileSystem: () => fileSystem,
602612
ProcessManager: () => fakeProcessManager,
603613
Platform: () => macosPlatform,
604-
XcodeProjectInterpreter: () =>
605-
FakeXcodeProjectInterpreterWithBuildSettings(),
614+
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
606615
});
607616

608617
testUsingContext('Trace error if xcresult is empty.', () async {
@@ -735,6 +744,97 @@ void main() {
735744
Platform: () => macosPlatform,
736745
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
737746
});
747+
748+
testUsingContext(
749+
'Validate basic Xcode settings with missing settings', () async {
750+
751+
const String plistPath = 'build/ios/archive/Runner.xcarchive/Products/Applications/Runner.app/Info.plist';
752+
fakeProcessManager.addCommands(<FakeCommand>[
753+
xattrCommand,
754+
setUpFakeXcodeBuildHandler(onRun: () {
755+
fileSystem.file(plistPath).createSync(recursive: true);
756+
}),
757+
exportArchiveCommand(exportOptionsPlist: _exportOptionsPlist),
758+
]);
759+
760+
createMinimalMockProjectFiles();
761+
762+
plistUtils.fileContents[plistPath] = <String,String>{
763+
'CFBundleIdentifier': 'io.flutter.someProject',
764+
};
765+
766+
final BuildCommand command = BuildCommand();
767+
await createTestCommandRunner(command).run(
768+
<String>['build', 'ipa', '--no-pub']);
769+
770+
expect(
771+
testLogger.statusText,
772+
contains(
773+
'┌─ App Settings ────────────────────────────────────────┐\n'
774+
'│ Version Number: Missing │\n'
775+
'│ Build Number: Missing │\n'
776+
'│ Display Name: Missing │\n'
777+
'│ Deployment Target: Missing │\n'
778+
'│ Bundle Identifier: io.flutter.someProject │\n'
779+
'│ │\n'
780+
'│ You must set up the missing settings │\n'
781+
'│ Instructions: https://docs.flutter.dev/deployment/ios │\n'
782+
'└───────────────────────────────────────────────────────┘'
783+
)
784+
);
785+
}, overrides: <Type, Generator>{
786+
FileSystem: () => fileSystem,
787+
ProcessManager: () => fakeProcessManager,
788+
Platform: () => macosPlatform,
789+
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
790+
PlistParser: () => plistUtils,
791+
});
792+
793+
testUsingContext(
794+
'Validate basic Xcode settings with full settings', () async {
795+
const String plistPath = 'build/ios/archive/Runner.xcarchive/Products/Applications/Runner.app/Info.plist';
796+
fakeProcessManager.addCommands(<FakeCommand>[
797+
xattrCommand,
798+
setUpFakeXcodeBuildHandler(onRun: () {
799+
fileSystem.file(plistPath).createSync(recursive: true);
800+
}),
801+
exportArchiveCommand(exportOptionsPlist: _exportOptionsPlist),
802+
]);
803+
804+
createMinimalMockProjectFiles();
805+
806+
plistUtils.fileContents[plistPath] = <String,String>{
807+
'CFBundleIdentifier': 'io.flutter.someProject',
808+
'CFBundleDisplayName': 'Awesome Gallery',
809+
'MinimumOSVersion': '11.0',
810+
'CFBundleVersion': '666',
811+
'CFBundleShortVersionString': '12.34.56',
812+
};
813+
814+
final BuildCommand command = BuildCommand();
815+
await createTestCommandRunner(command).run(
816+
<String>['build', 'ipa', '--no-pub']);
817+
818+
expect(
819+
testLogger.statusText,
820+
contains(
821+
'┌─ App Settings ────────────────────────────┐\n'
822+
'│ Version Number: 12.34.56 │\n'
823+
'│ Build Number: 666 │\n'
824+
'│ Display Name: Awesome Gallery │\n'
825+
'│ Deployment Target: 11.0 │\n'
826+
'│ Bundle Identifier: io.flutter.someProject │\n'
827+
'└───────────────────────────────────────────┘\n'
828+
)
829+
);
830+
}, overrides: <Type, Generator>{
831+
FileSystem: () => fileSystem,
832+
ProcessManager: () => fakeProcessManager,
833+
Platform: () => macosPlatform,
834+
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
835+
PlistParser: () => plistUtils,
836+
});
837+
738838
}
739839

740840
const String _xcBundleFilePath = '/.tmp_rand0/flutter_ios_build_temp_dirrand0/temporary_xcresult_bundle';

0 commit comments

Comments
 (0)