flutter/packages/flutter_tools/test/general.shard/xcode_backend_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

1098 lines
41 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:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/io.dart';
import '../../bin/xcode_backend.dart';
import '../src/common.dart' hide Context;
import '../src/fake_process_manager.dart';
void main() {
late MemoryFileSystem fileSystem;
setUp(() {
fileSystem = MemoryFileSystem();
});
test('prints warning and defaults to iOS if unknown platform', () {
final Directory buildDir = fileSystem.directory('/path/to/builds')..createSync(recursive: true);
final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
..createSync(recursive: true);
const String buildMode = 'Debug';
final TestContext context = TestContext(
<String>['build'],
<String, String>{
'BUILT_PRODUCTS_DIR': buildDir.path,
'CONFIGURATION': buildMode,
'FLUTTER_ROOT': flutterRoot.path,
'INFOPLIST_PATH': 'Info.plist',
},
commands: <FakeCommand>[
FakeCommand(
command: <String>[
'${flutterRoot.path}/bin/flutter',
'assemble',
'--no-version-check',
'--output=${buildDir.path}/',
'-dTargetPlatform=ios',
'-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}',
'-dConfiguration=$buildMode',
'-dIosArchs=',
'-dSdkRoot=',
'-dSplitDebugInfo=',
'-dTreeShakeIcons=',
'-dTrackWidgetCreation=',
'-dDartObfuscation=',
'-dAction=',
'-dFrontendServerStarterPath=',
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dSrcRoot=',
'-dTargetDeviceOSVersion=',
'debug_ios_bundle_flutter_assets',
],
),
],
fileSystem: fileSystem,
)..run();
expect(context.stderr, contains('warning: Unrecognized platform: null. Defaulting to iOS.\n'));
});
const List<TargetPlatform> platforms = TargetPlatform.values;
for (final TargetPlatform platform in platforms) {
final String platformName = platform.name;
group('build for $platformName', () {
test('exits with useful error message when build mode not set', () {
final Directory buildDir = fileSystem.directory('/path/to/builds')
..createSync(recursive: true);
final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
..createSync(recursive: true);
final TestContext context = TestContext(
<String>['build', platformName],
<String, String>{
'ACTION': 'build',
'BUILT_PRODUCTS_DIR': buildDir.path,
'FLUTTER_ROOT': flutterRoot.path,
'INFOPLIST_PATH': 'Info.plist',
},
commands: <FakeCommand>[],
fileSystem: fileSystem,
);
expect(() => context.run(), throwsException);
expect(context.stderr, contains('ERROR: Unknown FLUTTER_BUILD_MODE: null.\n'));
});
test('calls flutter assemble', () {
final String targetPlatform = platform == TargetPlatform.ios ? 'Ios' : 'Darwin';
final Directory buildDir = fileSystem.directory('/path/to/builds')
..createSync(recursive: true);
final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
..createSync(recursive: true);
final File pipe = fileSystem.file('/tmp/pipe')..createSync(recursive: true);
const String buildMode = 'Debug';
final TestContext context = TestContext(
<String>['build', platformName],
<String, String>{
'BUILT_PRODUCTS_DIR': buildDir.path,
'CONFIGURATION': buildMode,
'FLUTTER_ROOT': flutterRoot.path,
'INFOPLIST_PATH': 'Info.plist',
'FLUTTER_CLI_BUILD_MODE': buildMode.toLowerCase(),
},
commands: <FakeCommand>[
FakeCommand(
command: <String>[
'${flutterRoot.path}/bin/flutter',
'assemble',
'--no-version-check',
'--output=${buildDir.path}/',
'-dTargetPlatform=${targetPlatform.toLowerCase()}',
'-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}',
'-dConfiguration=$buildMode',
'-d${targetPlatform}Archs=',
'-dSdkRoot=',
'-dSplitDebugInfo=',
'-dTreeShakeIcons=',
'-dTrackWidgetCreation=',
'-dDartObfuscation=',
'-dAction=',
'-dFrontendServerStarterPath=',
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dSrcRoot=',
if (platform == TargetPlatform.ios) ...<String>['-dTargetDeviceOSVersion='],
if (platform == TargetPlatform.macos) ...<String>[
'--build-inputs=/Flutter/ephemeral/FlutterInputs.xcfilelist',
'--build-outputs=/Flutter/ephemeral/FlutterOutputs.xcfilelist',
],
'debug_${platformName}_bundle_flutter_assets',
],
),
],
fileSystem: fileSystem,
scriptOutputStreamFile: pipe,
)..run();
final List<String> streamedLines = pipe.readAsLinesSync();
// Ensure after line splitting, the exact string 'done' appears
expect(streamedLines, contains('done'));
expect(streamedLines, contains(' └─Compiling, linking and signing...'));
expect(context.stdout, contains('built and packaged successfully.'));
expect(context.stderr, isEmpty);
});
test('forwards all env variables to flutter assemble', () {
final String targetPlatform = platform == TargetPlatform.ios ? 'Ios' : 'Darwin';
final Directory buildDir = fileSystem.directory('/path/to/builds')
..createSync(recursive: true);
final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
..createSync(recursive: true);
const String archs = 'arm64';
const String buildMode = 'Release';
const String dartObfuscation = 'false';
const String dartDefines = 'flutter.inspector.structuredErrors%3Dtrue';
const String expandedCodeSignIdentity = 'F1326572E0B71C3C8442805230CB4B33B708A2E2';
const String extraFrontEndOptions = '--some-option';
const String extraGenSnapshotOptions = '--obfuscate';
const String frontendServerStarterPath = '/path/to/frontend_server_starter.dart';
const String sdkRoot = '/path/to/sdk';
const String splitDebugInfo = '/path/to/split/debug/info';
const String trackWidgetCreation = 'true';
const String treeShake = 'true';
const String srcRoot = '/path/to/project';
const String iOSVersion = '18.3.1';
final TestContext context = TestContext(
<String>['build', platformName],
<String, String>{
'ACTION': 'install',
'ARCHS': archs,
'BUILT_PRODUCTS_DIR': buildDir.path,
'CODE_SIGNING_REQUIRED': 'YES',
'CONFIGURATION': '$buildMode-strawberry',
'DART_DEFINES': dartDefines,
'DART_OBFUSCATION': dartObfuscation,
'EXPANDED_CODE_SIGN_IDENTITY': expandedCodeSignIdentity,
'EXTRA_FRONT_END_OPTIONS': extraFrontEndOptions,
'EXTRA_GEN_SNAPSHOT_OPTIONS': extraGenSnapshotOptions,
'FLUTTER_ROOT': flutterRoot.path,
'FRONTEND_SERVER_STARTER_PATH': frontendServerStarterPath,
'INFOPLIST_PATH': 'Info.plist',
'SDKROOT': sdkRoot,
'FLAVOR': 'strawberry',
'SPLIT_DEBUG_INFO': splitDebugInfo,
'TRACK_WIDGET_CREATION': trackWidgetCreation,
'TREE_SHAKE_ICONS': treeShake,
'SRCROOT': srcRoot,
'TARGET_DEVICE_OS_VERSION': iOSVersion,
'FLUTTER_CLI_BUILD_MODE': buildMode.toLowerCase(),
},
commands: <FakeCommand>[
FakeCommand(
command: <String>[
'${flutterRoot.path}/bin/flutter',
'assemble',
'--no-version-check',
'--output=${buildDir.path}/',
'-dTargetPlatform=${targetPlatform.toLowerCase()}',
'-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}',
'-dFlavor=strawberry',
'-dConfiguration=$buildMode-strawberry',
'-d${targetPlatform}Archs=$archs',
'-dSdkRoot=$sdkRoot',
'-dSplitDebugInfo=$splitDebugInfo',
'-dTreeShakeIcons=$treeShake',
'-dTrackWidgetCreation=$trackWidgetCreation',
'-dDartObfuscation=$dartObfuscation',
'-dAction=install',
'-dFrontendServerStarterPath=$frontendServerStarterPath',
'--ExtraGenSnapshotOptions=$extraGenSnapshotOptions',
'--DartDefines=$dartDefines',
'--ExtraFrontEndOptions=$extraFrontEndOptions',
'-dSrcRoot=$srcRoot',
if (platform == TargetPlatform.ios) ...<String>[
'-dTargetDeviceOSVersion=$iOSVersion',
'-dCodesignIdentity=$expandedCodeSignIdentity',
],
if (platform == TargetPlatform.macos) ...<String>[
'--build-inputs=/Flutter/ephemeral/FlutterInputs.xcfilelist',
'--build-outputs=/Flutter/ephemeral/FlutterOutputs.xcfilelist',
],
'release_${platformName}_bundle_flutter_assets',
],
),
],
fileSystem: fileSystem,
)..run();
expect(context.stdout, contains('built and packaged successfully.'));
expect(context.stderr, isEmpty);
});
test(
'exits with useful error message when missing FLUTTER_CLI_BUILD_MODE during archive',
() {
final Directory buildDir = fileSystem.directory('/path/to/builds')
..createSync(recursive: true);
final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
..createSync(recursive: true);
const String buildMode = 'Release';
final TestContext context = TestContext(
<String>['build', platformName],
<String, String>{
'ACTION': 'install',
'CONFIGURATION': buildMode,
'BUILT_PRODUCTS_DIR': buildDir.path,
'FLUTTER_ROOT': flutterRoot.path,
'INFOPLIST_PATH': 'Info.plist',
},
commands: <FakeCommand>[],
fileSystem: fileSystem,
);
expect(() => context.run(), throwsException);
expect(
context.stderr,
contains(
'error: Your Flutter build settings are outdated. '
'Please run "flutter build ${platform.name} --config-only --release" '
'in your Flutter project and try again.',
),
);
},
);
test('exits with useful error message when build mode mismatches during archive', () {
final Directory buildDir = fileSystem.directory('/path/to/builds')
..createSync(recursive: true);
final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
..createSync(recursive: true);
const String buildMode = 'Release';
final TestContext context = TestContext(
<String>['build', platformName],
<String, String>{
'ACTION': 'install',
'CONFIGURATION': buildMode,
'BUILT_PRODUCTS_DIR': buildDir.path,
'FLUTTER_ROOT': flutterRoot.path,
'INFOPLIST_PATH': 'Info.plist',
'FLUTTER_CLI_BUILD_MODE': 'debug',
},
commands: <FakeCommand>[],
fileSystem: fileSystem,
);
expect(() => context.run(), throwsException);
expect(
context.stderr,
contains(
'error: Your Flutter project is currently configured for debug mode. '
'Please run `flutter build ${platform.name} --config-only --release` '
'in your Flutter project to update your settings.',
),
);
});
test('prints useful warning message when missing FLUTTER_CLI_BUILD_MODE', () {
final Directory buildDir = fileSystem.directory('/path/to/builds')
..createSync(recursive: true);
final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
..createSync(recursive: true);
const String buildMode = 'Release';
final TestContext context = TestContext(
<String>['build', platformName],
<String, String>{
'ACTION': 'build',
'CONFIGURATION': buildMode,
'BUILT_PRODUCTS_DIR': buildDir.path,
'FLUTTER_ROOT': flutterRoot.path,
'INFOPLIST_PATH': 'Info.plist',
},
commands: <FakeCommand>[],
fileSystem: fileSystem,
fakeProcessManager: FakeProcessManager.any(),
);
context.run();
expect(
context.stderr,
contains(
'warning: Your Flutter build settings are outdated. '
'Please run "flutter build ${platform.name} --config-only --release" '
'in your Flutter project and try again.',
),
);
});
test('prints useful warning message when build mode mismatches', () {
final Directory buildDir = fileSystem.directory('/path/to/builds')
..createSync(recursive: true);
final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
..createSync(recursive: true);
const String buildMode = 'Release';
final TestContext context = TestContext(
<String>['build', platformName],
<String, String>{
'ACTION': 'debug',
'CONFIGURATION': buildMode,
'BUILT_PRODUCTS_DIR': buildDir.path,
'FLUTTER_ROOT': flutterRoot.path,
'INFOPLIST_PATH': 'Info.plist',
'FLUTTER_CLI_BUILD_MODE': 'debug',
},
commands: <FakeCommand>[],
fileSystem: fileSystem,
fakeProcessManager: FakeProcessManager.any(),
);
context.run();
expect(
context.stderr,
contains(
'warning: Your Flutter project is currently configured for debug mode. '
'Please run `flutter build ${platform.name} --config-only --release` '
'in your Flutter project to update your settings.',
),
);
});
});
}
group('test_vm_service_bonjour_service', () {
test('handles when the Info.plist is missing', () {
final Directory buildDir = fileSystem.directory('/path/to/builds');
buildDir.createSync(recursive: true);
final TestContext context = TestContext(
<String>['test_vm_service_bonjour_service'],
<String, String>{
'CONFIGURATION': 'Debug',
'BUILT_PRODUCTS_DIR': buildDir.path,
'INFOPLIST_PATH': 'Info.plist',
},
commands: <FakeCommand>[],
fileSystem: fileSystem,
)..run();
expect(
context.stdout,
contains(
'Info.plist does not exist. Skipping _dartVmService._tcp NSBonjourServices insertion.',
),
);
});
});
for (final TargetPlatform platform in platforms) {
final String platformName = platform.name;
group('prepare for $platformName', () {
test('exits with useful error message when build mode not set', () {
final String targetPlatform = platform == TargetPlatform.ios ? 'Ios' : 'Darwin';
final Directory buildDir = fileSystem.directory('/path/to/builds')
..createSync(recursive: true);
final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
..createSync(recursive: true);
final File pipe = fileSystem.file('/tmp/pipe')..createSync(recursive: true);
const String buildMode = 'Debug';
final TestContext context = TestContext(
<String>['prepare', platformName],
<String, String>{
'ACTION': 'build',
'BUILT_PRODUCTS_DIR': buildDir.path,
'FLUTTER_ROOT': flutterRoot.path,
'INFOPLIST_PATH': 'Info.plist',
'FLUTTER_CLI_BUILD_MODE': buildMode.toLowerCase(),
},
commands: <FakeCommand>[
FakeCommand(
command: <String>[
'${flutterRoot.path}/bin/flutter',
'assemble',
'--no-version-check',
'--output=${buildDir.path}/',
'-dTargetPlatform=${targetPlatform.toLowerCase()}',
'-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}',
'-d${targetPlatform}Archs=',
'-dSdkRoot=',
'-dSplitDebugInfo=',
'-dTreeShakeIcons=',
'-dTrackWidgetCreation=',
'-dDartObfuscation=',
'-dAction=build',
'-dFrontendServerStarterPath=',
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dPreBuildAction=PrepareFramework',
'-dSrcRoot=',
if (platform == TargetPlatform.ios) ...<String>['-dTargetDeviceOSVersion='],
'debug_unpack_$platformName',
],
),
],
fileSystem: fileSystem,
scriptOutputStreamFile: pipe,
);
expect(() => context.run(), throwsException);
expect(context.stderr, contains('ERROR: Unknown FLUTTER_BUILD_MODE: null.\n'));
});
test('calls flutter assemble', () {
final String targetPlatform = platform == TargetPlatform.ios ? 'Ios' : 'Darwin';
final Directory buildDir = fileSystem.directory('/path/to/builds')
..createSync(recursive: true);
final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
..createSync(recursive: true);
final File pipe = fileSystem.file('/tmp/pipe')..createSync(recursive: true);
const String buildMode = 'Debug';
final TestContext context = TestContext(
<String>['prepare', platformName],
<String, String>{
'BUILT_PRODUCTS_DIR': buildDir.path,
'CONFIGURATION': buildMode,
'FLUTTER_ROOT': flutterRoot.path,
'INFOPLIST_PATH': 'Info.plist',
'FLUTTER_CLI_BUILD_MODE': buildMode.toLowerCase(),
},
commands: <FakeCommand>[
FakeCommand(
command: <String>[
'${flutterRoot.path}/bin/flutter',
'assemble',
'--no-version-check',
'--output=${buildDir.path}/',
'-dTargetPlatform=${targetPlatform.toLowerCase()}',
'-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}',
'-dConfiguration=$buildMode',
'-d${targetPlatform}Archs=',
'-dSdkRoot=',
'-dSplitDebugInfo=',
'-dTreeShakeIcons=',
'-dTrackWidgetCreation=',
'-dDartObfuscation=',
'-dAction=',
'-dFrontendServerStarterPath=',
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dSrcRoot=',
if (platform == TargetPlatform.ios) ...<String>['-dTargetDeviceOSVersion='],
'-dPreBuildAction=PrepareFramework',
'debug_unpack_$platformName',
],
),
],
fileSystem: fileSystem,
scriptOutputStreamFile: pipe,
)..run();
expect(context.stderr, isEmpty);
});
test('forwards all env variables to flutter assemble', () {
final String targetPlatform = platform == TargetPlatform.ios ? 'Ios' : 'Darwin';
final Directory buildDir = fileSystem.directory('/path/to/builds')
..createSync(recursive: true);
final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
..createSync(recursive: true);
const String archs = 'arm64';
const String buildMode = 'Release';
const String dartObfuscation = 'false';
const String dartDefines = 'flutter.inspector.structuredErrors%3Dtrue';
const String expandedCodeSignIdentity = 'F1326572E0B71C3C8442805230CB4B33B708A2E2';
const String extraFrontEndOptions = '--some-option';
const String extraGenSnapshotOptions = '--obfuscate';
const String frontendServerStarterPath = '/path/to/frontend_server_starter.dart';
const String sdkRoot = '/path/to/sdk';
const String splitDebugInfo = '/path/to/split/debug/info';
const String trackWidgetCreation = 'true';
const String treeShake = 'true';
const String srcRoot = '/path/to/project';
const String iOSVersion = '18.3.1';
final TestContext context = TestContext(
<String>['prepare', platformName],
<String, String>{
'ACTION': 'install',
'ARCHS': archs,
'BUILT_PRODUCTS_DIR': buildDir.path,
'CODE_SIGNING_REQUIRED': 'YES',
'DART_DEFINES': dartDefines,
'DART_OBFUSCATION': dartObfuscation,
'EXPANDED_CODE_SIGN_IDENTITY': expandedCodeSignIdentity,
'EXTRA_FRONT_END_OPTIONS': extraFrontEndOptions,
'EXTRA_GEN_SNAPSHOT_OPTIONS': extraGenSnapshotOptions,
'FLUTTER_ROOT': flutterRoot.path,
'FRONTEND_SERVER_STARTER_PATH': frontendServerStarterPath,
'INFOPLIST_PATH': 'Info.plist',
'SDKROOT': sdkRoot,
'CONFIGURATION': '$buildMode-strawberry',
'FLAVOR': 'strawberry',
'SPLIT_DEBUG_INFO': splitDebugInfo,
'TRACK_WIDGET_CREATION': trackWidgetCreation,
'TREE_SHAKE_ICONS': treeShake,
'SRCROOT': srcRoot,
'TARGET_DEVICE_OS_VERSION': iOSVersion,
'FLUTTER_CLI_BUILD_MODE': buildMode.toLowerCase(),
},
commands: <FakeCommand>[
FakeCommand(
command: <String>[
'${flutterRoot.path}/bin/flutter',
'assemble',
'--no-version-check',
'--output=${buildDir.path}/',
'-dTargetPlatform=${targetPlatform.toLowerCase()}',
'-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}',
'-dFlavor=strawberry',
'-dConfiguration=$buildMode-strawberry',
'-d${targetPlatform}Archs=$archs',
'-dSdkRoot=$sdkRoot',
'-dSplitDebugInfo=$splitDebugInfo',
'-dTreeShakeIcons=$treeShake',
'-dTrackWidgetCreation=$trackWidgetCreation',
'-dDartObfuscation=$dartObfuscation',
'-dAction=install',
'-dFrontendServerStarterPath=$frontendServerStarterPath',
'--ExtraGenSnapshotOptions=$extraGenSnapshotOptions',
'--DartDefines=$dartDefines',
'--ExtraFrontEndOptions=$extraFrontEndOptions',
'-dSrcRoot=$srcRoot',
if (platform == TargetPlatform.ios) ...<String>[
'-dTargetDeviceOSVersion=$iOSVersion',
'-dCodesignIdentity=$expandedCodeSignIdentity',
],
'-dPreBuildAction=PrepareFramework',
'release_unpack_$platformName',
],
),
],
fileSystem: fileSystem,
)..run();
expect(context.stderr, isEmpty);
});
test('assumes ARCHS based on NATIVE_ARCH if ONLY_ACTIVE_ARCH is YES', () {
final String targetPlatform = platform == TargetPlatform.ios ? 'Ios' : 'Darwin';
final Directory buildDir = fileSystem.directory('/path/to/builds')
..createSync(recursive: true);
final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
..createSync(recursive: true);
final File pipe = fileSystem.file('/tmp/pipe')..createSync(recursive: true);
const String buildMode = 'Debug';
final TestContext context = TestContext(
<String>['prepare', platformName],
<String, String>{
'BUILT_PRODUCTS_DIR': buildDir.path,
'CONFIGURATION': buildMode,
'FLUTTER_ROOT': flutterRoot.path,
'INFOPLIST_PATH': 'Info.plist',
'ARCHS': 'arm64 x86_64',
'ONLY_ACTIVE_ARCH': 'YES',
'NATIVE_ARCH': 'arm64e',
'FLUTTER_CLI_BUILD_MODE': buildMode.toLowerCase(),
},
commands: <FakeCommand>[
FakeCommand(
command: <String>[
'${flutterRoot.path}/bin/flutter',
'assemble',
'--no-version-check',
'--output=${buildDir.path}/',
'-dTargetPlatform=${targetPlatform.toLowerCase()}',
'-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}',
'-dConfiguration=$buildMode',
'-d${targetPlatform}Archs=arm64',
'-dSdkRoot=',
'-dSplitDebugInfo=',
'-dTreeShakeIcons=',
'-dTrackWidgetCreation=',
'-dDartObfuscation=',
'-dAction=',
'-dFrontendServerStarterPath=',
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dSrcRoot=',
if (platform == TargetPlatform.ios) ...<String>['-dTargetDeviceOSVersion='],
'-dPreBuildAction=PrepareFramework',
'debug_unpack_$platformName',
],
),
],
fileSystem: fileSystem,
scriptOutputStreamFile: pipe,
)..run();
expect(context.stderr, isEmpty);
});
test('does not assumes ARCHS if ARCHS and NATIVE_ARCH are different', () {
final String targetPlatform = platform == TargetPlatform.ios ? 'Ios' : 'Darwin';
final Directory buildDir = fileSystem.directory('/path/to/builds')
..createSync(recursive: true);
final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
..createSync(recursive: true);
final File pipe = fileSystem.file('/tmp/pipe')..createSync(recursive: true);
const String buildMode = 'Debug';
final TestContext context = TestContext(
<String>['prepare', platformName],
<String, String>{
'BUILT_PRODUCTS_DIR': buildDir.path,
'CONFIGURATION': buildMode,
'FLUTTER_ROOT': flutterRoot.path,
'INFOPLIST_PATH': 'Info.plist',
'ARCHS': 'arm64',
'ONLY_ACTIVE_ARCH': 'YES',
'NATIVE_ARCH': 'x86_64',
'FLUTTER_CLI_BUILD_MODE': buildMode.toLowerCase(),
},
commands: <FakeCommand>[
FakeCommand(
command: <String>[
'${flutterRoot.path}/bin/flutter',
'assemble',
'--no-version-check',
'--output=${buildDir.path}/',
'-dTargetPlatform=${targetPlatform.toLowerCase()}',
'-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}',
'-dConfiguration=$buildMode',
'-d${targetPlatform}Archs=arm64',
'-dSdkRoot=',
'-dSplitDebugInfo=',
'-dTreeShakeIcons=',
'-dTrackWidgetCreation=',
'-dDartObfuscation=',
'-dAction=',
'-dFrontendServerStarterPath=',
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dSrcRoot=',
if (platform == TargetPlatform.ios) ...<String>['-dTargetDeviceOSVersion='],
'-dPreBuildAction=PrepareFramework',
'debug_unpack_$platformName',
],
),
],
fileSystem: fileSystem,
scriptOutputStreamFile: pipe,
)..run();
expect(context.stderr, isEmpty);
});
test('does not assumes ARCHS if ONLY_ACTIVE_ARCH is not YES', () {
final String targetPlatform = platform == TargetPlatform.ios ? 'Ios' : 'Darwin';
final Directory buildDir = fileSystem.directory('/path/to/builds')
..createSync(recursive: true);
final Directory flutterRoot = fileSystem.directory('/path/to/flutter')
..createSync(recursive: true);
final File pipe = fileSystem.file('/tmp/pipe')..createSync(recursive: true);
const String buildMode = 'Debug';
final TestContext context = TestContext(
<String>['prepare', platformName],
<String, String>{
'BUILT_PRODUCTS_DIR': buildDir.path,
'CONFIGURATION': buildMode,
'FLUTTER_ROOT': flutterRoot.path,
'INFOPLIST_PATH': 'Info.plist',
'ARCHS': 'arm64 x86_64',
'NATIVE_ARCH': 'arm64e',
'FLUTTER_CLI_BUILD_MODE': buildMode.toLowerCase(),
},
commands: <FakeCommand>[
FakeCommand(
command: <String>[
'${flutterRoot.path}/bin/flutter',
'assemble',
'--no-version-check',
'--output=${buildDir.path}/',
'-dTargetPlatform=${targetPlatform.toLowerCase()}',
'-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}',
'-dConfiguration=$buildMode',
'-d${targetPlatform}Archs=arm64 x86_64',
'-dSdkRoot=',
'-dSplitDebugInfo=',
'-dTreeShakeIcons=',
'-dTrackWidgetCreation=',
'-dDartObfuscation=',
'-dAction=',
'-dFrontendServerStarterPath=',
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dSrcRoot=',
if (platform == TargetPlatform.ios) ...<String>['-dTargetDeviceOSVersion='],
'-dPreBuildAction=PrepareFramework',
'debug_unpack_$platformName',
],
),
],
fileSystem: fileSystem,
scriptOutputStreamFile: pipe,
)..run();
expect(context.stderr, isEmpty);
});
});
}
test('embed for iOS copies frameworks', () {
final Directory buildDir = fileSystem.directory('/path/to/Build/Products/Debug-iphoneos')
..createSync(recursive: true);
final Directory targetBuildDir = fileSystem.directory('/path/to/Build/Products/Debug-iphoneos')
..createSync(recursive: true);
const String appPath = '/path/to/my_flutter_app';
const String platformDirPath = '$appPath/ios';
const String frameworksFolderPath = 'Runner.app/Frameworks';
const String flutterBuildDir = 'build';
final Directory nativeAssetsDir = fileSystem.directory(
'/path/to/my_flutter_app/$flutterBuildDir/native_assets/ios/',
);
nativeAssetsDir.createSync(recursive: true);
const String ffiPackageName = 'package_a';
final Directory ffiPackageDir = nativeAssetsDir.childDirectory('$ffiPackageName.framework')
..createSync();
nativeAssetsDir.childFile('random.txt').createSync();
const String infoPlistPath = 'Runner.app/Info.plist';
final File infoPlist = fileSystem.file('${buildDir.path}/$infoPlistPath');
infoPlist.createSync(recursive: true);
const String buildMode = 'Debug';
final TestContext testContext = TestContext(
<String>['embed_and_thin', 'ios'],
<String, String>{
'BUILT_PRODUCTS_DIR': buildDir.path,
'CONFIGURATION': buildMode,
'INFOPLIST_PATH': infoPlistPath,
'SOURCE_ROOT': platformDirPath,
'FLUTTER_APPLICATION_PATH': appPath,
'FLUTTER_BUILD_DIR': flutterBuildDir,
'TARGET_BUILD_DIR': targetBuildDir.path,
'FRAMEWORKS_FOLDER_PATH': frameworksFolderPath,
'EXPANDED_CODE_SIGN_IDENTITY': '12312313',
},
commands: <FakeCommand>[
FakeCommand(
command: <String>[
'mkdir',
'-p',
'--',
targetBuildDir.childDirectory(frameworksFolderPath).path,
],
),
FakeCommand(
command: <String>[
'rsync',
'-8',
'-av',
'--delete',
'--filter',
'- .DS_Store',
buildDir.childDirectory('App.framework').path,
targetBuildDir.childDirectory(frameworksFolderPath).path,
],
),
FakeCommand(
command: <String>[
'rsync',
'-8',
'-av',
'--delete',
'--filter',
'- .DS_Store',
buildDir.childDirectory('Flutter.framework').path,
'${targetBuildDir.childDirectory(frameworksFolderPath).path}/',
],
),
FakeCommand(
command: <String>[
'rsync',
'-8',
'-av',
'--delete',
'--filter',
'- .DS_Store',
'--filter',
'- native_assets.yaml',
'--filter',
'- native_assets.json',
ffiPackageDir.path,
targetBuildDir.childDirectory(frameworksFolderPath).path,
],
),
FakeCommand(
command: <String>[
'plutil',
'-extract',
'NSBonjourServices',
'xml1',
'-o',
'-',
infoPlist.path,
],
),
FakeCommand(
command: <String>[
'plutil',
'-insert',
'NSBonjourServices.0',
'-string',
'_dartVmService._tcp',
infoPlist.path,
],
),
FakeCommand(
command: <String>[
'plutil',
'-extract',
'NSLocalNetworkUsageDescription',
'xml1',
'-o',
'-',
infoPlist.path,
],
),
],
fileSystem: fileSystem,
)..run();
expect(testContext.processManager.hasRemainingExpectations, isFalse);
});
test('embed for macos copies and codesigns frameworks', () {
final Directory buildDir = fileSystem.directory('/path/to/Build/Products/Debug')
..createSync(recursive: true);
final Directory targetBuildDir = fileSystem.directory('/path/to/Build/Products/Debug')
..createSync(recursive: true);
const String appPath = '/path/to/my_flutter_app';
const String platformDirPath = '$appPath/macos';
const String frameworksFolderPath = 'Runner.app/Frameworks';
const String flutterBuildDir = 'build';
final Directory nativeAssetsDir = fileSystem.directory(
'/path/to/my_flutter_app/$flutterBuildDir/native_assets/macos/',
);
nativeAssetsDir.createSync(recursive: true);
const String ffiPackageName = 'package_a';
final Directory ffiPackageDir = nativeAssetsDir.childDirectory('$ffiPackageName.framework')
..createSync();
nativeAssetsDir.childFile('random.txt').createSync();
const String infoPlistPath = 'Runner.app/Info.plist';
final File infoPlist = fileSystem.file('${buildDir.path}/$infoPlistPath');
infoPlist.createSync(recursive: true);
const String buildMode = 'Debug';
const String codesignIdentity = '12312313';
final TestContext testContext = TestContext(
<String>['embed_and_thin', 'macos'],
<String, String>{
'BUILT_PRODUCTS_DIR': buildDir.path,
'CONFIGURATION': buildMode,
'INFOPLIST_PATH': infoPlistPath,
'SOURCE_ROOT': platformDirPath,
'FLUTTER_APPLICATION_PATH': appPath,
'FLUTTER_BUILD_DIR': flutterBuildDir,
'TARGET_BUILD_DIR': targetBuildDir.path,
'FRAMEWORKS_FOLDER_PATH': frameworksFolderPath,
'EXPANDED_CODE_SIGN_IDENTITY': codesignIdentity,
},
commands: <FakeCommand>[
FakeCommand(
command: <String>[
'mkdir',
'-p',
'--',
targetBuildDir.childDirectory(frameworksFolderPath).path,
],
),
FakeCommand(
command: <String>[
'rsync',
'-8',
'-av',
'--delete',
'--filter',
'- .DS_Store',
buildDir.childDirectory('App.framework').path,
targetBuildDir.childDirectory(frameworksFolderPath).path,
],
),
FakeCommand(
command: <String>[
'rsync',
'-8',
'-av',
'--delete',
'--filter',
'- .DS_Store',
'--filter',
'- Headers',
'--filter',
'- Modules',
buildDir.childDirectory('FlutterMacOS.framework').path,
'${targetBuildDir.childDirectory(frameworksFolderPath).path}/',
],
),
FakeCommand(
command: <String>[
'codesign',
'--force',
'--verbose',
'--sign',
codesignIdentity,
'--',
targetBuildDir.childDirectory(frameworksFolderPath).childFile('App.framework/App').path,
],
),
FakeCommand(
command: <String>[
'codesign',
'--force',
'--verbose',
'--sign',
codesignIdentity,
'--',
targetBuildDir
.childDirectory(frameworksFolderPath)
.childFile('FlutterMacOS.framework/FlutterMacOS')
.path,
],
),
FakeCommand(
command: <String>[
'rsync',
'-8',
'-av',
'--delete',
'--filter',
'- .DS_Store',
'--filter',
'- native_assets.yaml',
'--filter',
'- native_assets.json',
ffiPackageDir.path,
targetBuildDir.childDirectory(frameworksFolderPath).path,
],
),
FakeCommand(
command: <String>[
'codesign',
'--force',
'--verbose',
'--sign',
codesignIdentity,
'--',
targetBuildDir
.childDirectory(frameworksFolderPath)
.childFile('$ffiPackageName.framework/$ffiPackageName')
.path,
],
),
],
fileSystem: fileSystem,
)..run();
expect(testContext.processManager.hasRemainingExpectations, isFalse);
});
group('parseFrameworkNameFromDirectory', () {
test('Non-framework directory', () {
expect(
Context.parseFrameworkNameFromDirectory(fileSystem.directory('path/to/directory')),
isNull,
);
});
test('Framework directory', () {
expect(
Context.parseFrameworkNameFromDirectory(
fileSystem.directory('path/to/directory.framework'),
),
'directory',
);
});
});
}
class TestContext extends Context {
TestContext(
List<String> arguments,
Map<String, String> environment, {
required this.fileSystem,
required List<FakeCommand> commands,
File? scriptOutputStreamFile,
FakeProcessManager? fakeProcessManager,
}) : processManager = fakeProcessManager ?? FakeProcessManager.list(commands),
super(
arguments: arguments,
environment: environment,
scriptOutputStreamFile: scriptOutputStreamFile,
);
final FileSystem fileSystem;
final FakeProcessManager processManager;
String stdout = '';
String stderr = '';
@override
bool existsFile(String path) {
return fileSystem.file(path).existsSync();
}
@override
Directory directoryFromPath(String path) {
return fileSystem.directory(path);
}
@override
ProcessResult runSync(
String bin,
List<String> args, {
bool verbose = false,
bool allowFail = false,
String? workingDirectory,
}) {
return processManager.runSync(
<dynamic>[bin, ...args],
workingDirectory: workingDirectory,
environment: environment,
);
}
@override
void echoError(String message) {
stderr += '$message\n';
}
@override
void echoXcodeError(String message) {
stderr += 'error: $message';
}
@override
void echoXcodeWarning(String message) {
stderr += 'warning: $message\n';
}
@override
void echo(String message) {
stdout += message;
}
@override
Never exitApp(int code) {
// This is an exception for the benefit of unit tests.
// The real implementation calls `exit(code)`.
throw Exception('App exited with code $code');
}
}