flutter/packages/flutter_tools/test/general.shard/xcode_backend_test.dart
Loïc Sharma ea208f824b
Fix flutter run on Mac x64 hosts if Swift Package Manager is enabled (#154645)
### Problem

Enabling the Swift Package Manager feature caused post-submit tests to fail on Mac x64 hosts:

<details>
<summary>Example error...</summary>

https://ci.chromium.org/ui/p/flutter/builders/prod/Mac_ios%20rrect_blur_perf_ios__timeline_summary/575/overview

```
♦ ... flutter --verbose assemble ... -dIosArchs=x86_64 ... profile_unpack_ios

Target profile_unpack_ios failed:
Exception: Binary ... build/ios/Profile-iphoneos/Flutter.framework/Flutter does not contain x86_64.

Running lipo -info:
Non-fat file: ... build/ios/Profile-iphoneos/Flutter.framework/Flutter is architecture: arm64

#0      UnpackIOS._thinFramework (package:flutter_tools/src/build_system/targets/ios.dart:351:7)
<asynchronous suspension>
#1      UnpackIOS.build (package:flutter_tools/src/build_system/targets/ios.dart:298:5)
<asynchronous suspension>
...
```

</details>

### Reproduction

On a mac x64 host:

1. Switch to the latest master channel: `flutter channel master ; flutter upgrade`
1. Disable the Swift Package Manager feature: `flutter config --no-enable-swift-package-manager`
2. Create a Flutter project
2. [Edit the Xcode project manually to add the prepare pre-action](https://docs.flutter.dev/packages-and-plugins/swift-package-manager/for-app-developers#step-2-add-run-prepare-flutter-framework-script-pre-action)
3. Run `flutter run` (`flutter build ios` does not reproduce this issue).

### Background

Previously, the Flutter framework was unpacked in the Xcode target's build. Unfortunately, this happens after Swift packages are built; this prevented Swift packages from using the Flutter framework.

To fix this, we added an Xcode pre-action that unpacks the Flutter framework _before_ Swift Package Manager builds packages. The Xcode target still runs the Flutter framework unpack step, but this step no-ops if the unpack step has the exact same inputs. 

```mermaid
flowchart LR
  A[flutter run -d iphone] --> B(Build Xcode project)
  B --> C(Xcode 'prepare framework' pre-action)
  B --> G[Build Swift packages]
  B --> D(Build 'Runner' target)
  C --> E[Unpack Flutter framework #1]
  D --> F["
  Unpack Flutter framework #2
  (No-ops if inputs are same as #1)
  "]
```

https://github.com/flutter/flutter/pull/150052 added an optimization that made it more likely the second unpack step no-ops by fixing a case where the target architecture input could be different:

> When using SwiftPM, we use `flutter assemble` in an Xcode Pre-action to run the `debug_unpack_macos` (or profile/release) target. This target is also later used in a Run Script build phase. Depending on `ARCHS` build setting, the Flutter/FlutterMacOS binary is thinned. In the Run Script build phase, `ARCHS` is filtered to the active arch. However, in the Pre-action it doesn't always filter to the active arch. As a workaround, assume arm64 if the [`NATIVE_ARCH`](https://developer.apple.com/documentation/xcode/build-settings-reference/#NATIVEARCH) is arm, otherwise assume x86_64.

This optimization is only applied if [`ONLY_ACTIVE_ARCH`](https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/XcodeBuildSettingRef/1-Build_Setting_Reference/build_setting_ref.html#//apple_ref/doc/uid/TP40003931-CH3-SW157) is `YES`.

> [!IMPORTANT]
> [`ONLY_ACTIVE_ARCH`](https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/XcodeBuildSettingRef/1-Build_Setting_Reference/build_setting_ref.html#//apple_ref/doc/uid/TP40003931-CH3-SW157)'s name is misleading. It specifies whether the product includes only object code for the native architecture.
>
> A value of `YES` means the product includes only code for the native architecture ([NATIVE_ARCH](https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/XcodeBuildSettingRef/1-Build_Setting_Reference/build_setting_ref.html#//apple_ref/doc/uid/TP40003931-CH3-SW59)).
>
> A value of `NO` means the product includes code for the architectures specified in [ARCHS (Architectures)](https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/XcodeBuildSettingRef/1-Build_Setting_Reference/build_setting_ref.html#//apple_ref/doc/uid/TP40003931-CH3-SW62).

### Problem

`buildXcodeProject` incorrectly always sets `ONLY_ACTIVE_ARCH` to `YES` if the Xcode built is for a single architecture:

6abef22251/packages/flutter_tools/lib/src/ios/mac.dart (L353-L361)

This is incorrect! If the host architecture is `x64` but the target architecture is `arm64`, [`ONLY_ACTIVE_ARCH`](https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/XcodeBuildSettingRef/1-Build_Setting_Reference/build_setting_ref.html#//apple_ref/doc/uid/TP40003931-CH3-SW157) should be `NO`.

This caused the prepare pre-action to incorrectly use x64 as the target architecture for arm64 devices on an x64 host, which in turn caused builds to fail if Swift Package Manager was enabled.

### Solution

This change updates `buildXcodeProject` to set `ONLY_ACTIVE_ARCH` correctly.

This change also updates the prepare pre-action's to be more conservative in applying the optimization that filters the target architecture. This ensures that the build still works (but without the optimization) if `ONLY_ACTIVE_ARCH` is incorrectly set.

Follow-up PR: https://github.com/flutter/flutter/pull/154649

This unblocks: https://github.com/flutter/flutter/issues/151567

### DeviceLab test

This problem reproduces if you `flutter run` to an iPhone Arm64 device from an x64 mac host with the Swift Package Manager feature enabled.

I ran an affected DeviceLab test to verify the fix works as expected:

Description | CI test | Result
-- | -- | --
SwiftPM enabled without this fix: https://github.com/flutter/flutter/pull/154750 | [Link](https://ci.chromium.org/ui/p/flutter/builders/try.shadow/Mac_ios%20rrect_blur_perf_ios__timeline_summary/7/overview) | ❌ 
SwiftPM enabled with this fix: https://github.com/flutter/flutter/pull/154749 | [Link](https://ci.chromium.org/ui/p/flutter/builders/try.shadow/Mac_ios%20rrect_blur_perf_ios__timeline_summary/8/overview) | âœ
2024-09-11 20:03:22 +00:00

609 lines
22 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();
});
group('build', () {
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 File pipe = fileSystem.file('/tmp/pipe')
..createSync(recursive: true);
const String buildMode = 'Debug';
final TestContext context = TestContext(
<String>['build'],
<String, String>{
'ACTION': 'build',
'BUILT_PRODUCTS_DIR': buildDir.path,
'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()}',
'-dIosArchs=',
'-dSdkRoot=',
'-dSplitDebugInfo=',
'-dTreeShakeIcons=',
'-dTrackWidgetCreation=',
'-dDartObfuscation=',
'-dAction=build',
'-dFrontendServerStarterPath=',
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'debug_ios_bundle_flutter_assets',
],
),
],
fileSystem: fileSystem,
scriptOutputStreamFile: pipe,
);
expect(
() => context.run(),
throwsException,
);
expect(
context.stderr,
contains('ERROR: Unknown FLUTTER_BUILD_MODE: null.\n'),
);
});
test('calls flutter assemble', () {
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'],
<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()}',
'-dIosArchs=',
'-dSdkRoot=',
'-dSplitDebugInfo=',
'-dTreeShakeIcons=',
'-dTrackWidgetCreation=',
'-dDartObfuscation=',
'-dAction=',
'-dFrontendServerStarterPath=',
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'debug_ios_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 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';
final TestContext context = TestContext(
<String>['build'],
<String, String>{
'ACTION': 'install',
'ARCHS': archs,
'BUILT_PRODUCTS_DIR': buildDir.path,
'CODE_SIGNING_REQUIRED': 'YES',
'CONFIGURATION': buildMode,
'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,
},
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()}',
'-dFlavor=strawberry',
'-dIosArchs=$archs',
'-dSdkRoot=$sdkRoot',
'-dSplitDebugInfo=$splitDebugInfo',
'-dTreeShakeIcons=$treeShake',
'-dTrackWidgetCreation=$trackWidgetCreation',
'-dDartObfuscation=$dartObfuscation',
'-dAction=install',
'-dFrontendServerStarterPath=$frontendServerStarterPath',
'--ExtraGenSnapshotOptions=$extraGenSnapshotOptions',
'--DartDefines=$dartDefines',
'--ExtraFrontEndOptions=$extraFrontEndOptions',
'-dCodesignIdentity=$expandedCodeSignIdentity',
'release_ios_bundle_flutter_assets',
],
),
],
fileSystem: fileSystem,
)..run();
expect(
context.stdout,
contains('built and packaged successfully.'),
);
expect(context.stderr, isEmpty);
});
});
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.'),
);
});
});
group('prepare', () {
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 File pipe = fileSystem.file('/tmp/pipe')
..createSync(recursive: true);
const String buildMode = 'Debug';
final TestContext context = TestContext(
<String>['prepare'],
<String, String>{
'ACTION': 'build',
'BUILT_PRODUCTS_DIR': buildDir.path,
'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()}',
'-dIosArchs=',
'-dSdkRoot=',
'-dSplitDebugInfo=',
'-dTreeShakeIcons=',
'-dTrackWidgetCreation=',
'-dDartObfuscation=',
'-dAction=build',
'-dFrontendServerStarterPath=',
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dPreBuildAction=PrepareFramework',
'debug_unpack_ios',
],
),
],
fileSystem: fileSystem,
scriptOutputStreamFile: pipe,
);
expect(
() => context.run(),
throwsException,
);
expect(
context.stderr,
contains('ERROR: Unknown FLUTTER_BUILD_MODE: null.\n'),
);
});
test('calls flutter assemble', () {
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'],
<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()}',
'-dIosArchs=',
'-dSdkRoot=',
'-dSplitDebugInfo=',
'-dTreeShakeIcons=',
'-dTrackWidgetCreation=',
'-dDartObfuscation=',
'-dAction=',
'-dFrontendServerStarterPath=',
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dPreBuildAction=PrepareFramework',
'debug_unpack_ios',
],
),
],
fileSystem: fileSystem,
scriptOutputStreamFile: pipe,
)..run();
expect(context.stderr, isEmpty);
});
test('forwards all env variables to flutter assemble', () {
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';
final TestContext context = TestContext(
<String>['prepare'],
<String, String>{
'ACTION': 'install',
'ARCHS': archs,
'BUILT_PRODUCTS_DIR': buildDir.path,
'CODE_SIGNING_REQUIRED': 'YES',
'CONFIGURATION': buildMode,
'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,
},
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()}',
'-dFlavor=strawberry',
'-dIosArchs=$archs',
'-dSdkRoot=$sdkRoot',
'-dSplitDebugInfo=$splitDebugInfo',
'-dTreeShakeIcons=$treeShake',
'-dTrackWidgetCreation=$trackWidgetCreation',
'-dDartObfuscation=$dartObfuscation',
'-dAction=install',
'-dFrontendServerStarterPath=$frontendServerStarterPath',
'--ExtraGenSnapshotOptions=$extraGenSnapshotOptions',
'--DartDefines=$dartDefines',
'--ExtraFrontEndOptions=$extraFrontEndOptions',
'-dPreBuildAction=PrepareFramework',
'-dCodesignIdentity=$expandedCodeSignIdentity',
'release_unpack_ios',
],
),
],
fileSystem: fileSystem,
)..run();
expect(context.stderr, isEmpty);
});
test('assumes ARCHS based on NATIVE_ARCH if ONLY_ACTIVE_ARCH is YES', () {
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'],
<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'
},
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()}',
'-dIosArchs=arm64',
'-dSdkRoot=',
'-dSplitDebugInfo=',
'-dTreeShakeIcons=',
'-dTrackWidgetCreation=',
'-dDartObfuscation=',
'-dAction=',
'-dFrontendServerStarterPath=',
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dPreBuildAction=PrepareFramework',
'debug_unpack_ios',
],
),
],
fileSystem: fileSystem,
scriptOutputStreamFile: pipe,
)..run();
expect(context.stderr, isEmpty);
});
test('does not assumes ARCHS if ARCHS and NATIVE_ARCH are different', () {
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'],
<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',
},
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()}',
'-dIosArchs=arm64',
'-dSdkRoot=',
'-dSplitDebugInfo=',
'-dTreeShakeIcons=',
'-dTrackWidgetCreation=',
'-dDartObfuscation=',
'-dAction=',
'-dFrontendServerStarterPath=',
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dPreBuildAction=PrepareFramework',
'debug_unpack_ios',
],
),
],
fileSystem: fileSystem,
scriptOutputStreamFile: pipe,
)..run();
expect(context.stderr, isEmpty);
});
test('does not assumes ARCHS if ONLY_ACTIVE_ARCH is not YES', () {
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'],
<String, String>{
'BUILT_PRODUCTS_DIR': buildDir.path,
'CONFIGURATION': buildMode,
'FLUTTER_ROOT': flutterRoot.path,
'INFOPLIST_PATH': 'Info.plist',
'ARCHS': 'arm64 x86_64',
'NATIVE_ARCH': 'arm64e'
},
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()}',
'-dIosArchs=arm64 x86_64',
'-dSdkRoot=',
'-dSplitDebugInfo=',
'-dTreeShakeIcons=',
'-dTrackWidgetCreation=',
'-dDartObfuscation=',
'-dAction=',
'-dFrontendServerStarterPath=',
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dPreBuildAction=PrepareFramework',
'debug_unpack_ios',
],
),
],
fileSystem: fileSystem,
scriptOutputStreamFile: pipe,
)..run();
expect(context.stderr, isEmpty);
});
});
}
class TestContext extends Context {
TestContext(
List<String> arguments,
Map<String, String> environment, {
required this.fileSystem,
required List<FakeCommand> commands,
File? scriptOutputStreamFile,
}) : processManager = 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
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 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');
}
}