flutter/packages/flutter_tools/test/general.shard/xcode_backend_test.dart
Victoria Ashworth 8d100a6416
Get flavor/scheme in assemble command from the build configuration (#162907)
This moves the logic for `FLUTTER_APP_FLAVOR` into `flutter assemble`,
so that it also works when ran through Xcode and not just through the
Flutter CLI.

However, there's no definitive way to get the the flavor/scheme in
`flutter assemble`, so this makes a best effort to get it by parsing it
out of the `CONFIGURATION`. `CONFIGURATION` should have the name of the
scheme in it, although, this is only
[semi-enforced](1d85de0fc8/packages/flutter_tools/lib/src/ios/mac.dart (L201-L203)),
so may not always work. If it's unable to get the scheme name from the
`CONFIGURATION`, it falls back to using the `FLAVOR` environment
variable, which is set by the Flutter CLI and used currently.

Verified `Mac_ios flavors_test_ios` passes:
https://ci.chromium.org/ui/p/flutter/builders/prod.shadow/Mac_ios%20flavors_test_ios/7/overview

Verified `Mac flavors_test_macos` passes:
https://ci.chromium.org/ui/p/flutter/builders/try.shadow/Mac%20flavors_test_macos/2/overview

Fixes https://github.com/flutter/flutter/issues/155951.

## 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].
- [x] 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-02-19 20:37:35 +00:00

596 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()}',
'-dConfiguration=$buildMode',
'-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-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,
},
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',
'-dConfiguration=$buildMode-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()}',
'-dConfiguration=$buildMode',
'-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',
'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,
},
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',
'-dConfiguration=$buildMode-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()}',
'-dConfiguration=$buildMode',
'-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()}',
'-dConfiguration=$buildMode',
'-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()}',
'-dConfiguration=$buildMode',
'-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');
}
}