mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
[tools]validation basic Xcode settings for build ipa (#113412)
This commit is contained in:
parent
92a66683a1
commit
e6300da2c3
@ -15,6 +15,7 @@ import '../convert.dart';
|
|||||||
import '../globals.dart' as globals;
|
import '../globals.dart' as globals;
|
||||||
import '../ios/application_package.dart';
|
import '../ios/application_package.dart';
|
||||||
import '../ios/mac.dart';
|
import '../ios/mac.dart';
|
||||||
|
import '../ios/plist_parser.dart';
|
||||||
import '../runner/flutter_command.dart';
|
import '../runner/flutter_command.dart';
|
||||||
import 'build.dart';
|
import 'build.dart';
|
||||||
|
|
||||||
@ -129,12 +130,49 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
|
|||||||
return super.validateCommand();
|
return super.validateCommand();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _validateXcodeBuildSettingsAfterArchive() async {
|
||||||
|
final BuildableIOSApp app = await buildableIOSApp;
|
||||||
|
|
||||||
|
final String plistPath = app.builtInfoPlistPathAfterArchive;
|
||||||
|
|
||||||
|
if (!globals.fs.file(plistPath).existsSync()) {
|
||||||
|
globals.printError('Invalid iOS archive. Does not contain Info.plist.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, String?> xcodeProjectSettingsMap = <String, String?>{};
|
||||||
|
|
||||||
|
xcodeProjectSettingsMap['Version Number'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleShortVersionStringKey);
|
||||||
|
xcodeProjectSettingsMap['Build Number'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleVersionKey);
|
||||||
|
xcodeProjectSettingsMap['Display Name'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleDisplayNameKey);
|
||||||
|
xcodeProjectSettingsMap['Deployment Target'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kMinimumOSVersionKey);
|
||||||
|
xcodeProjectSettingsMap['Bundle Identifier'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleIdentifierKey);
|
||||||
|
|
||||||
|
final StringBuffer buffer = StringBuffer();
|
||||||
|
xcodeProjectSettingsMap.forEach((String title, String? info) {
|
||||||
|
buffer.writeln('$title: ${info ?? "Missing"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
final String message;
|
||||||
|
if (xcodeProjectSettingsMap.values.any((String? element) => element == null)) {
|
||||||
|
buffer.writeln('\nYou must set up the missing settings');
|
||||||
|
buffer.write('Instructions: https://docs.flutter.dev/deployment/ios');
|
||||||
|
message = buffer.toString();
|
||||||
|
} else {
|
||||||
|
// remove the new line
|
||||||
|
message = buffer.toString().trim();
|
||||||
|
}
|
||||||
|
globals.printBox(message, title: 'App Settings');
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FlutterCommandResult> runCommand() async {
|
Future<FlutterCommandResult> runCommand() async {
|
||||||
final BuildInfo buildInfo = await cachedBuildInfo;
|
final BuildInfo buildInfo = await cachedBuildInfo;
|
||||||
displayNullSafetyMode(buildInfo);
|
displayNullSafetyMode(buildInfo);
|
||||||
final FlutterCommandResult xcarchiveResult = await super.runCommand();
|
final FlutterCommandResult xcarchiveResult = await super.runCommand();
|
||||||
|
|
||||||
|
await _validateXcodeBuildSettingsAfterArchive();
|
||||||
|
|
||||||
// xcarchive failed or not at expected location.
|
// xcarchive failed or not at expected location.
|
||||||
if (xcarchiveResult.exitStatus != ExitStatus.success) {
|
if (xcarchiveResult.exitStatus != ExitStatus.success) {
|
||||||
globals.printStatus('Skipping IPA.');
|
globals.printStatus('Skipping IPA.');
|
||||||
@ -289,6 +327,7 @@ abstract class _BuildIOSSubCommand extends BuildSubCommand {
|
|||||||
/// The result of the Xcode build command. Null until it finishes.
|
/// The result of the Xcode build command. Null until it finishes.
|
||||||
@protected
|
@protected
|
||||||
XcodeBuildResult? xcodeBuildResult;
|
XcodeBuildResult? xcodeBuildResult;
|
||||||
|
|
||||||
EnvironmentType get environmentType;
|
EnvironmentType get environmentType;
|
||||||
bool get configOnly;
|
bool get configOnly;
|
||||||
|
|
||||||
|
@ -145,6 +145,12 @@ class BuildableIOSApp extends IOSApp {
|
|||||||
String get archiveBundleOutputPath =>
|
String get archiveBundleOutputPath =>
|
||||||
globals.fs.path.setExtension(archiveBundlePath, '.xcarchive');
|
globals.fs.path.setExtension(archiveBundlePath, '.xcarchive');
|
||||||
|
|
||||||
|
String get builtInfoPlistPathAfterArchive => globals.fs.path.join(archiveBundleOutputPath,
|
||||||
|
'Products',
|
||||||
|
'Applications',
|
||||||
|
_hostAppBundleName == null ? 'Runner.app' : _hostAppBundleName!,
|
||||||
|
'Info.plist');
|
||||||
|
|
||||||
String get ipaOutputPath =>
|
String get ipaOutputPath =>
|
||||||
globals.fs.path.join(getIosBuildDirectory(), 'ipa');
|
globals.fs.path.join(getIosBuildDirectory(), 'ipa');
|
||||||
|
|
||||||
|
@ -26,7 +26,10 @@ class PlistParser {
|
|||||||
|
|
||||||
static const String kCFBundleIdentifierKey = 'CFBundleIdentifier';
|
static const String kCFBundleIdentifierKey = 'CFBundleIdentifier';
|
||||||
static const String kCFBundleShortVersionStringKey = 'CFBundleShortVersionString';
|
static const String kCFBundleShortVersionStringKey = 'CFBundleShortVersionString';
|
||||||
static const String kCFBundleExecutable = 'CFBundleExecutable';
|
static const String kCFBundleExecutableKey = 'CFBundleExecutable';
|
||||||
|
static const String kCFBundleVersionKey = 'CFBundleVersion';
|
||||||
|
static const String kCFBundleDisplayNameKey = 'CFBundleDisplayName';
|
||||||
|
static const String kMinimumOSVersionKey = 'MinimumOSVersion';
|
||||||
|
|
||||||
/// Returns the content, converted to XML, of the plist file located at
|
/// Returns the content, converted to XML, of the plist file located at
|
||||||
/// [plistFilePath].
|
/// [plistFilePath].
|
||||||
|
@ -87,7 +87,7 @@ abstract class MacOSApp extends ApplicationPackage {
|
|||||||
}
|
}
|
||||||
final Map<String, dynamic> propertyValues = globals.plistParser.parseFile(plistPath);
|
final Map<String, dynamic> propertyValues = globals.plistParser.parseFile(plistPath);
|
||||||
final String? id = propertyValues[PlistParser.kCFBundleIdentifierKey] as String?;
|
final String? id = propertyValues[PlistParser.kCFBundleIdentifierKey] as String?;
|
||||||
final String? executableName = propertyValues[PlistParser.kCFBundleExecutable] as String?;
|
final String? executableName = propertyValues[PlistParser.kCFBundleExecutableKey] as String?;
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
globals.printError('Invalid prebuilt macOS app. Info.plist does not contain bundle identifier');
|
globals.printError('Invalid prebuilt macOS app. Info.plist does not contain bundle identifier');
|
||||||
return null;
|
return null;
|
||||||
|
@ -9,8 +9,10 @@ import 'package:flutter_tools/src/base/platform.dart';
|
|||||||
import 'package:flutter_tools/src/cache.dart';
|
import 'package:flutter_tools/src/cache.dart';
|
||||||
import 'package:flutter_tools/src/commands/build.dart';
|
import 'package:flutter_tools/src/commands/build.dart';
|
||||||
import 'package:flutter_tools/src/commands/build_ios.dart';
|
import 'package:flutter_tools/src/commands/build_ios.dart';
|
||||||
|
import 'package:flutter_tools/src/ios/plist_parser.dart';
|
||||||
import 'package:flutter_tools/src/ios/xcodeproj.dart';
|
import 'package:flutter_tools/src/ios/xcodeproj.dart';
|
||||||
import 'package:flutter_tools/src/reporting/reporting.dart';
|
import 'package:flutter_tools/src/reporting/reporting.dart';
|
||||||
|
import 'package:test/fake.dart';
|
||||||
|
|
||||||
import '../../general.shard/ios/xcresult_test_data.dart';
|
import '../../general.shard/ios/xcresult_test_data.dart';
|
||||||
import '../../src/common.dart';
|
import '../../src/common.dart';
|
||||||
@ -50,10 +52,20 @@ final Platform notMacosPlatform = FakePlatform(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
class FakePlistUtils extends Fake implements PlistParser {
|
||||||
|
final Map<String, Map<String, Object>> fileContents = <String, Map<String, Object>>{};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? getStringValueFromFile(String plistFilePath, String key) {
|
||||||
|
return fileContents[plistFilePath]![key] as String?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
late FileSystem fileSystem;
|
late FileSystem fileSystem;
|
||||||
late TestUsage usage;
|
late TestUsage usage;
|
||||||
late FakeProcessManager fakeProcessManager;
|
late FakeProcessManager fakeProcessManager;
|
||||||
|
late FakePlistUtils plistUtils;
|
||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
Cache.disableLocking();
|
Cache.disableLocking();
|
||||||
@ -63,6 +75,7 @@ void main() {
|
|||||||
fileSystem = MemoryFileSystem.test();
|
fileSystem = MemoryFileSystem.test();
|
||||||
usage = TestUsage();
|
usage = TestUsage();
|
||||||
fakeProcessManager = FakeProcessManager.empty();
|
fakeProcessManager = FakeProcessManager.empty();
|
||||||
|
plistUtils = FakePlistUtils();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sets up the minimal mock project files necessary to look like a Flutter project.
|
// Sets up the minimal mock project files necessary to look like a Flutter project.
|
||||||
@ -246,8 +259,7 @@ void main() {
|
|||||||
FileSystem: () => fileSystem,
|
FileSystem: () => fileSystem,
|
||||||
ProcessManager: () => FakeProcessManager.any(),
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
Platform: () => macosPlatform,
|
Platform: () => macosPlatform,
|
||||||
XcodeProjectInterpreter: () =>
|
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
|
||||||
FakeXcodeProjectInterpreterWithBuildSettings(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('ipa build fails when --export-options-plist and --export-method are used together', () async {
|
testUsingContext('ipa build fails when --export-options-plist and --export-method are used together', () async {
|
||||||
@ -270,8 +282,7 @@ void main() {
|
|||||||
FileSystem: () => fileSystem,
|
FileSystem: () => fileSystem,
|
||||||
ProcessManager: () => FakeProcessManager.any(),
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
Platform: () => macosPlatform,
|
Platform: () => macosPlatform,
|
||||||
XcodeProjectInterpreter: () =>
|
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
|
||||||
FakeXcodeProjectInterpreterWithBuildSettings(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('ipa build reports when IPA fails', () async {
|
testUsingContext('ipa build reports when IPA fails', () async {
|
||||||
@ -521,8 +532,7 @@ void main() {
|
|||||||
FileSystem: () => fileSystem,
|
FileSystem: () => fileSystem,
|
||||||
ProcessManager: () => FakeProcessManager.any(),
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
Platform: () => macosPlatform,
|
Platform: () => macosPlatform,
|
||||||
XcodeProjectInterpreter: () =>
|
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
|
||||||
FakeXcodeProjectInterpreterWithBuildSettings(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('Performs code size analysis and sends analytics', () async {
|
testUsingContext('Performs code size analysis and sends analytics', () async {
|
||||||
@ -601,8 +611,7 @@ void main() {
|
|||||||
FileSystem: () => fileSystem,
|
FileSystem: () => fileSystem,
|
||||||
ProcessManager: () => fakeProcessManager,
|
ProcessManager: () => fakeProcessManager,
|
||||||
Platform: () => macosPlatform,
|
Platform: () => macosPlatform,
|
||||||
XcodeProjectInterpreter: () =>
|
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
|
||||||
FakeXcodeProjectInterpreterWithBuildSettings(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('Trace error if xcresult is empty.', () async {
|
testUsingContext('Trace error if xcresult is empty.', () async {
|
||||||
@ -735,6 +744,97 @@ void main() {
|
|||||||
Platform: () => macosPlatform,
|
Platform: () => macosPlatform,
|
||||||
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
|
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testUsingContext(
|
||||||
|
'Validate basic Xcode settings with missing settings', () async {
|
||||||
|
|
||||||
|
const String plistPath = 'build/ios/archive/Runner.xcarchive/Products/Applications/Runner.app/Info.plist';
|
||||||
|
fakeProcessManager.addCommands(<FakeCommand>[
|
||||||
|
xattrCommand,
|
||||||
|
setUpFakeXcodeBuildHandler(onRun: () {
|
||||||
|
fileSystem.file(plistPath).createSync(recursive: true);
|
||||||
|
}),
|
||||||
|
exportArchiveCommand(exportOptionsPlist: _exportOptionsPlist),
|
||||||
|
]);
|
||||||
|
|
||||||
|
createMinimalMockProjectFiles();
|
||||||
|
|
||||||
|
plistUtils.fileContents[plistPath] = <String,String>{
|
||||||
|
'CFBundleIdentifier': 'io.flutter.someProject',
|
||||||
|
};
|
||||||
|
|
||||||
|
final BuildCommand command = BuildCommand();
|
||||||
|
await createTestCommandRunner(command).run(
|
||||||
|
<String>['build', 'ipa', '--no-pub']);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
testLogger.statusText,
|
||||||
|
contains(
|
||||||
|
'┌─ App Settings ────────────────────────────────────────┐\n'
|
||||||
|
'│ Version Number: Missing │\n'
|
||||||
|
'│ Build Number: Missing │\n'
|
||||||
|
'│ Display Name: Missing │\n'
|
||||||
|
'│ Deployment Target: Missing │\n'
|
||||||
|
'│ Bundle Identifier: io.flutter.someProject │\n'
|
||||||
|
'│ │\n'
|
||||||
|
'│ You must set up the missing settings │\n'
|
||||||
|
'│ Instructions: https://docs.flutter.dev/deployment/ios │\n'
|
||||||
|
'└───────────────────────────────────────────────────────┘'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
FileSystem: () => fileSystem,
|
||||||
|
ProcessManager: () => fakeProcessManager,
|
||||||
|
Platform: () => macosPlatform,
|
||||||
|
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
|
||||||
|
PlistParser: () => plistUtils,
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext(
|
||||||
|
'Validate basic Xcode settings with full settings', () async {
|
||||||
|
const String plistPath = 'build/ios/archive/Runner.xcarchive/Products/Applications/Runner.app/Info.plist';
|
||||||
|
fakeProcessManager.addCommands(<FakeCommand>[
|
||||||
|
xattrCommand,
|
||||||
|
setUpFakeXcodeBuildHandler(onRun: () {
|
||||||
|
fileSystem.file(plistPath).createSync(recursive: true);
|
||||||
|
}),
|
||||||
|
exportArchiveCommand(exportOptionsPlist: _exportOptionsPlist),
|
||||||
|
]);
|
||||||
|
|
||||||
|
createMinimalMockProjectFiles();
|
||||||
|
|
||||||
|
plistUtils.fileContents[plistPath] = <String,String>{
|
||||||
|
'CFBundleIdentifier': 'io.flutter.someProject',
|
||||||
|
'CFBundleDisplayName': 'Awesome Gallery',
|
||||||
|
'MinimumOSVersion': '11.0',
|
||||||
|
'CFBundleVersion': '666',
|
||||||
|
'CFBundleShortVersionString': '12.34.56',
|
||||||
|
};
|
||||||
|
|
||||||
|
final BuildCommand command = BuildCommand();
|
||||||
|
await createTestCommandRunner(command).run(
|
||||||
|
<String>['build', 'ipa', '--no-pub']);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
testLogger.statusText,
|
||||||
|
contains(
|
||||||
|
'┌─ App Settings ────────────────────────────┐\n'
|
||||||
|
'│ Version Number: 12.34.56 │\n'
|
||||||
|
'│ Build Number: 666 │\n'
|
||||||
|
'│ Display Name: Awesome Gallery │\n'
|
||||||
|
'│ Deployment Target: 11.0 │\n'
|
||||||
|
'│ Bundle Identifier: io.flutter.someProject │\n'
|
||||||
|
'└───────────────────────────────────────────┘\n'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
FileSystem: () => fileSystem,
|
||||||
|
ProcessManager: () => fakeProcessManager,
|
||||||
|
Platform: () => macosPlatform,
|
||||||
|
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
|
||||||
|
PlistParser: () => plistUtils,
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const String _xcBundleFilePath = '/.tmp_rand0/flutter_ios_build_temp_dirrand0/temporary_xcresult_bundle';
|
const String _xcBundleFilePath = '/.tmp_rand0/flutter_ios_build_temp_dirrand0/temporary_xcresult_bundle';
|
||||||
|
Loading…
Reference in New Issue
Block a user