flutter/packages/flutter_tools/test/integration.shard/xcode_verify_configuration_test.dart
Victoria Ashworth fe1c9d61a7
Validate build configuration during Xcode build phase (#169395)
You can run iOS/macOS Flutter apps through either the Flutter CLI or
Xcode. However, it's often required to first run the Flutter CLI to
generate required files and settings. Some of these settings/files (like
dev dependencies) are specific to the build mode. However, you can
change the build mode through Xcode too. When you change the build mode
through Xcode, the Flutter-generated files and setting may not be
correct.

This PR checks if the current build mode matches the one last used by
the Flutter CLI. If it doesn't, it'll print a warning like this:

![Screenshot 2025-05-23 at 5 14
58 PM](https://github.com/user-attachments/assets/47d15cc4-f05d-4034-8be6-67f37828aa61)

If the build action is `install`, which indicates the app is being built
for distribution, this will print as an error and fail the build:

![Screenshot 2025-05-23 at 5 16
12 PM](https://github.com/user-attachments/assets/339b65bd-6425-4595-b26b-a60c722bbcf9)


## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-05-29 19:00:56 +00:00

589 lines
18 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import '../src/common.dart';
import 'test_utils.dart';
void main() {
late Directory tempDir;
late Directory projectDir;
setUpAll(() async {
tempDir = createResolvedTempDirectorySync('xcode_dev_dependencies_test.');
projectDir = tempDir.childDirectory('project')..createSync();
final Directory tempPluginADir = tempDir.childDirectory('plugin_a')..createSync();
// Create a Flutter project.
await processManager.run(<String>[
flutterBin,
'create',
projectDir.path,
'--project-name=testapp',
], workingDirectory: projectDir.path);
// Create a Flutter plugin to add as a dev dependency to the Flutter project.
await processManager.run(<String>[
flutterBin,
'create',
tempPluginADir.path,
'--template=plugin',
'--project-name=plugin_a',
'--platforms=ios,macos',
], workingDirectory: tempPluginADir.path);
// Add a dev dependency on plugin_a
await processManager.run(<String>[
flutterBin,
'pub',
'add',
'dev:plugin_a',
'--path',
tempPluginADir.path,
], workingDirectory: projectDir.path);
});
tearDownAll(() {
tryToDelete(tempDir);
});
group(
'Xcode build iOS app',
() {
test(
'succeeds when Flutter CLI last used configuration matches Xcode configuration',
() async {
final List<String> flutterCommand = <String>[
flutterBin,
...getLocalEngineArguments(),
'build',
'ios',
'--config-only',
'--debug',
];
final ProcessResult flutterResult = await processManager.run(
flutterCommand,
workingDirectory: projectDir.path,
);
expect(flutterResult, const ProcessResultMatcher());
final List<String> xcodeCommand = <String>[
'xcodebuild',
'-workspace',
'ios/Runner.xcworkspace',
'-scheme',
'Runner',
'-destination',
'generic/platform=iOS Simulator',
'CODE_SIGNING_ALLOWED=NO',
'CODE_SIGNING_REQUIRED=NO',
'CODE_SIGN_IDENTITY=-',
'EXPANDED_CODE_SIGN_IDENTITY=-',
'COMPILER_INDEX_STORE_ENABLE=NO',
'VERBOSE_SCRIPT_LOGGING=true',
'-configuration',
'Debug',
];
final ProcessResult xcodeResult = await processManager.run(
xcodeCommand,
workingDirectory: projectDir.path,
);
expect(xcodeResult, const ProcessResultMatcher(stdoutPattern: '** BUILD SUCCEEDED **'));
},
);
test(
'fails if Flutter CLI last used configuration does not match Xcode configuration when archiving',
() async {
final List<String> flutterCommand = <String>[
flutterBin,
...getLocalEngineArguments(),
'build',
'ios',
'--config-only',
'--debug',
];
final ProcessResult flutterResult = await processManager.run(
flutterCommand,
workingDirectory: projectDir.path,
);
expect(flutterResult, const ProcessResultMatcher());
final List<String> xcodeCommand = <String>[
'xcodebuild',
'archive',
'-workspace',
'ios/Runner.xcworkspace',
'-scheme',
'Runner',
'-destination',
'generic/platform=iOS',
'CODE_SIGNING_ALLOWED=NO',
'CODE_SIGNING_REQUIRED=NO',
'CODE_SIGN_IDENTITY=-',
'EXPANDED_CODE_SIGN_IDENTITY=-',
'COMPILER_INDEX_STORE_ENABLE=NO',
'VERBOSE_SCRIPT_LOGGING=true',
'-configuration',
'Release',
];
final ProcessResult xcodeResult = await processManager.run(
xcodeCommand,
workingDirectory: projectDir.path,
);
expect(
xcodeResult,
const ProcessResultMatcher(
exitCode: 65,
stdoutPattern: 'error: Your Flutter project is currently configured for debug mode.',
stderrPattern: '** ARCHIVE FAILED **',
),
);
},
);
test(
'warns if Flutter CLI last used configuration does not match Xcode configuration when building',
() async {
final List<String> flutterCommand = <String>[
flutterBin,
...getLocalEngineArguments(),
'build',
'ios',
'--config-only',
'--release',
];
final ProcessResult flutterResult = await processManager.run(
flutterCommand,
workingDirectory: projectDir.path,
);
expect(flutterResult, const ProcessResultMatcher());
final List<String> xcodeCommand = <String>[
'xcodebuild',
'-workspace',
'ios/Runner.xcworkspace',
'-scheme',
'Runner',
'-destination',
'generic/platform=iOS',
'CODE_SIGNING_ALLOWED=NO',
'CODE_SIGNING_REQUIRED=NO',
'CODE_SIGN_IDENTITY=-',
'EXPANDED_CODE_SIGN_IDENTITY=-',
'COMPILER_INDEX_STORE_ENABLE=NO',
'VERBOSE_SCRIPT_LOGGING=true',
'-configuration',
'Debug',
];
final ProcessResult xcodeResult = await processManager.run(
xcodeCommand,
workingDirectory: projectDir.path,
);
expect(
xcodeResult,
const ProcessResultMatcher(
stdoutPattern:
'warning: Your Flutter project is currently configured for release mode.',
),
);
},
);
},
skip: !platform.isMacOS, // [intended] iOS builds only work on macos.
);
group(
'Xcode build iOS module',
() {
test(
'succeeds when Flutter CLI last used configuration matches Xcode configuration',
() async {
final Directory moduleDirectory = projectDir.childDirectory('hello');
await processManager.run(<String>[
flutterBin,
'create',
moduleDirectory.path,
'--template=module',
'--project-name=hello',
], workingDirectory: projectDir.path);
final List<String> flutterCommand = <String>[
flutterBin,
...getLocalEngineArguments(),
'build',
'ios',
'--config-only',
'--debug',
];
final ProcessResult flutterResult = await processManager.run(
flutterCommand,
workingDirectory: moduleDirectory.path,
);
expect(flutterResult, const ProcessResultMatcher());
final Directory hostAppDirectory = projectDir.childDirectory('hello_host_app');
hostAppDirectory.createSync();
copyDirectory(
fileSystem.directory(
fileSystem.path.join(getFlutterRoot(), 'dev', 'integration_tests', 'ios_host_app'),
),
hostAppDirectory,
);
final ProcessResult podResult = await processManager.run(
const <String>['pod', 'install'],
workingDirectory: hostAppDirectory.path,
environment: const <String, String>{'LANG': 'en_US.UTF-8'},
);
expect(podResult, const ProcessResultMatcher());
final List<String> xcodeCommand = <String>[
'xcodebuild',
'-workspace',
'Host.xcworkspace',
'-scheme',
'Host',
'-destination',
'generic/platform=iOS',
'CODE_SIGNING_ALLOWED=NO',
'CODE_SIGNING_REQUIRED=NO',
'CODE_SIGN_IDENTITY=-',
'EXPANDED_CODE_SIGN_IDENTITY=-',
'COMPILER_INDEX_STORE_ENABLE=NO',
'VERBOSE_SCRIPT_LOGGING=true',
'-configuration',
'Debug',
];
final ProcessResult xcodeResult = await processManager.run(
xcodeCommand,
workingDirectory: hostAppDirectory.path,
);
expect(xcodeResult, const ProcessResultMatcher(stdoutPattern: '** BUILD SUCCEEDED **'));
},
);
test(
'fails if Flutter CLI last used configuration does not match Xcode configuration when archiving',
() async {
final Directory moduleDirectory = projectDir.childDirectory('hello');
await processManager.run(<String>[
flutterBin,
'create',
moduleDirectory.path,
'--template=module',
'--project-name=hello',
], workingDirectory: projectDir.path);
final List<String> flutterCommand = <String>[
flutterBin,
...getLocalEngineArguments(),
'build',
'ios',
'--config-only',
'--debug',
];
final ProcessResult flutterResult = await processManager.run(
flutterCommand,
workingDirectory: moduleDirectory.path,
);
expect(flutterResult, const ProcessResultMatcher());
final Directory hostAppDirectory = projectDir.childDirectory('hello_host_app');
hostAppDirectory.createSync();
copyDirectory(
fileSystem.directory(
fileSystem.path.join(getFlutterRoot(), 'dev', 'integration_tests', 'ios_host_app'),
),
hostAppDirectory,
);
final ProcessResult podResult = await processManager.run(
const <String>['pod', 'install'],
workingDirectory: hostAppDirectory.path,
environment: const <String, String>{'LANG': 'en_US.UTF-8'},
);
expect(podResult, const ProcessResultMatcher());
final List<String> xcodeCommand = <String>[
'xcodebuild',
'archive',
'-workspace',
'Host.xcworkspace',
'-scheme',
'Host',
'-destination',
'generic/platform=iOS',
'CODE_SIGNING_ALLOWED=NO',
'CODE_SIGNING_REQUIRED=NO',
'CODE_SIGN_IDENTITY=-',
'EXPANDED_CODE_SIGN_IDENTITY=-',
'COMPILER_INDEX_STORE_ENABLE=NO',
'VERBOSE_SCRIPT_LOGGING=true',
'-configuration',
'Release',
];
final ProcessResult xcodeResult = await processManager.run(
xcodeCommand,
workingDirectory: hostAppDirectory.path,
);
expect(
xcodeResult,
const ProcessResultMatcher(
exitCode: 65,
stdoutPattern: 'error: Your Flutter project is currently configured for debug mode.',
stderrPattern: '** ARCHIVE FAILED **',
),
);
},
);
test(
'warns if Flutter CLI last used configuration does not match Xcode configuration when building',
() async {
final Directory moduleDirectory = projectDir.childDirectory('hello');
await processManager.run(<String>[
flutterBin,
'create',
moduleDirectory.path,
'--template=module',
'--project-name=hello',
], workingDirectory: projectDir.path);
final List<String> flutterCommand = <String>[
flutterBin,
...getLocalEngineArguments(),
'build',
'ios',
'--config-only',
];
final ProcessResult flutterResult = await processManager.run(
flutterCommand,
workingDirectory: moduleDirectory.path,
);
expect(flutterResult, const ProcessResultMatcher());
final Directory hostAppDirectory = projectDir.childDirectory('hello_host_app');
hostAppDirectory.createSync();
copyDirectory(
fileSystem.directory(
fileSystem.path.join(getFlutterRoot(), 'dev', 'integration_tests', 'ios_host_app'),
),
hostAppDirectory,
);
final ProcessResult podResult = await processManager.run(
const <String>['pod', 'install'],
workingDirectory: hostAppDirectory.path,
environment: const <String, String>{'LANG': 'en_US.UTF-8'},
);
expect(podResult, const ProcessResultMatcher());
final List<String> xcodeCommand = <String>[
'xcodebuild',
'-workspace',
'Host.xcworkspace',
'-scheme',
'Host',
'-destination',
'generic/platform=iOS',
'CODE_SIGNING_ALLOWED=NO',
'CODE_SIGNING_REQUIRED=NO',
'CODE_SIGN_IDENTITY=-',
'EXPANDED_CODE_SIGN_IDENTITY=-',
'COMPILER_INDEX_STORE_ENABLE=NO',
'VERBOSE_SCRIPT_LOGGING=true',
'-configuration',
'Debug',
];
final ProcessResult xcodeResult = await processManager.run(
xcodeCommand,
workingDirectory: hostAppDirectory.path,
);
expect(
xcodeResult,
const ProcessResultMatcher(
stdoutPattern:
'warning: Your Flutter project is currently configured for release mode.',
),
);
},
);
},
skip: !platform.isMacOS, // [intended] iOS builds only work on macos.
);
group(
'Xcode build macOS app',
() {
test(
'succeeds when Flutter CLI last used configuration matches Xcode configuration',
() async {
final List<String> flutterCommand = <String>[
flutterBin,
...getLocalEngineArguments(),
'build',
'macos',
'--config-only',
'--debug',
];
final ProcessResult flutterResult = await processManager.run(
flutterCommand,
workingDirectory: projectDir.path,
);
expect(flutterResult, const ProcessResultMatcher());
final List<String> xcodeCommand = <String>[
'xcodebuild',
'-workspace',
'macos/Runner.xcworkspace',
'-scheme',
'Runner',
'-destination',
'platform=macOS',
'CODE_SIGNING_ALLOWED=NO',
'CODE_SIGNING_REQUIRED=NO',
'CODE_SIGN_IDENTITY=-',
'EXPANDED_CODE_SIGN_IDENTITY=-',
'COMPILER_INDEX_STORE_ENABLE=NO',
'VERBOSE_SCRIPT_LOGGING=true',
'-configuration',
'Debug',
];
final ProcessResult xcodeResult = await processManager.run(
xcodeCommand,
workingDirectory: projectDir.path,
);
expect(xcodeResult, const ProcessResultMatcher(stdoutPattern: '** BUILD SUCCEEDED **'));
},
);
test(
'fails if Flutter CLI last used configuration does not match Xcode configuration when archiving',
() async {
final List<String> flutterCommand = <String>[
flutterBin,
...getLocalEngineArguments(),
'build',
'macos',
'--config-only',
'--debug',
];
final ProcessResult flutterResult = await processManager.run(
flutterCommand,
workingDirectory: projectDir.path,
);
expect(flutterResult, const ProcessResultMatcher());
final List<String> xcodeCommand = <String>[
'xcodebuild',
'archive',
'-workspace',
'macos/Runner.xcworkspace',
'-scheme',
'Runner',
'-destination',
'platform=macOS',
'CODE_SIGNING_ALLOWED=NO',
'CODE_SIGNING_REQUIRED=NO',
'CODE_SIGN_IDENTITY=-',
'EXPANDED_CODE_SIGN_IDENTITY=-',
'COMPILER_INDEX_STORE_ENABLE=NO',
'VERBOSE_SCRIPT_LOGGING=true',
'-configuration',
'Release',
];
final ProcessResult xcodeResult = await processManager.run(
xcodeCommand,
workingDirectory: projectDir.path,
);
expect(
xcodeResult,
const ProcessResultMatcher(
exitCode: 65,
stdoutPattern: 'error: Your Flutter project is currently configured for debug mode.',
stderrPattern: '** ARCHIVE FAILED **',
),
);
},
);
test(
'warns if Flutter CLI last used configuration does not match Xcode configuration when building',
() async {
final List<String> flutterCommand = <String>[
flutterBin,
...getLocalEngineArguments(),
'build',
'macos',
'--config-only',
'--release',
];
final ProcessResult flutterResult = await processManager.run(
flutterCommand,
workingDirectory: projectDir.path,
);
expect(flutterResult, const ProcessResultMatcher());
final List<String> xcodeCommand = <String>[
'xcodebuild',
'-workspace',
'macos/Runner.xcworkspace',
'-scheme',
'Runner',
'-destination',
'platform=macOS',
'CODE_SIGNING_ALLOWED=NO',
'CODE_SIGNING_REQUIRED=NO',
'CODE_SIGN_IDENTITY=-',
'EXPANDED_CODE_SIGN_IDENTITY=-',
'COMPILER_INDEX_STORE_ENABLE=NO',
'VERBOSE_SCRIPT_LOGGING=true',
'-configuration',
'Debug',
];
final ProcessResult xcodeResult = await processManager.run(
xcodeCommand,
workingDirectory: projectDir.path,
);
expect(
xcodeResult,
const ProcessResultMatcher(
stdoutPattern:
'warning: Your Flutter project is currently configured for release mode.',
),
);
},
);
},
skip: !platform.isMacOS, // [intended] iOS builds only work on macos.
);
}