blob: a777fb6c8110f86872b26125db0d9e46d2389244 [file] [log] [blame]
// 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 'dart:convert';
import 'dart:io' as io;
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';
import '../src/io.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 buildMode = 'Debug';
final 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=',
'-dXcodeBuildScript=build',
'-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 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 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 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 buildMode = 'Debug';
final context = TestContext(
<String>['build', platformName],
<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=${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=',
'-dXcodeBuildScript=build',
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 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 archs = 'arm64';
const buildMode = 'Release';
const dartObfuscation = 'false';
const dartDefines = 'flutter.inspector.structuredErrors%3Dtrue';
const expandedCodeSignIdentity = 'F1326572E0B71C3C8442805230CB4B33B708A2E2';
const extraFrontEndOptions = '--some-option';
const extraGenSnapshotOptions = '--obfuscate';
const frontendServerStarterPath = '/path/to/frontend_server_starter.dart';
const sdkRoot = '/path/to/sdk';
const splitDebugInfo = '/path/to/split/debug/info';
const trackWidgetCreation = 'true';
const treeShake = 'true';
const srcRoot = '/path/to/project';
const iOSVersion = '18.3.1';
final 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,
},
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',
'-dXcodeBuildScript=build',
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);
});
});
}
group('test_vm_service_bonjour_service', () {
test('handles when the Info.plist is missing', () {
final Directory buildDir = fileSystem.directory('/path/to/builds')
..createSync(recursive: true);
final 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 verbose in <bool>[true, false]) {
test(
'Missing NSBonjourServices key in Info.plist should not fail Xcode compilation under ${verbose ? 'verbose' : 'non-verbose'} mode',
() {
final Directory buildDir = fileSystem.directory('/path/to/builds')
..createSync(recursive: true);
final File infoPlist = buildDir.childFile('Info.plist')..createSync();
const plutilErrorMessage =
'Could not extract value, error: No value at that key path or invalid key path: NSBonjourServices';
final File pipe = fileSystem.file('/tmp/pipe')..createSync(recursive: true);
final context = TestContext(
<String>['test_vm_service_bonjour_service'],
<String, String>{
'CONFIGURATION': 'Debug',
'BUILT_PRODUCTS_DIR': buildDir.path,
'INFOPLIST_PATH': 'Info.plist',
if (verbose) 'VERBOSE_SCRIPT_LOGGING': 'YES',
},
commands: <FakeCommand>[
FakeCommand(
command: <String>[
'plutil',
'-extract',
'NSBonjourServices',
'xml1',
'-o',
'-',
infoPlist.path,
],
exitCode: 1,
stderr: plutilErrorMessage,
),
FakeCommand(
command: <String>[
'plutil',
'-insert',
'NSBonjourServices',
'-json',
'["_dartVmService._tcp"]',
infoPlist.path,
],
),
FakeCommand(
command: <String>[
'plutil',
'-extract',
'NSLocalNetworkUsageDescription',
'xml1',
'-o',
'-',
infoPlist.path,
],
),
],
fileSystem: fileSystem,
scriptOutputStreamFile: pipe,
)..run();
expect(context.stderr, isNot(startsWith('error: ')));
expect(pipe.readAsStringSync(), isNot(contains(plutilErrorMessage)));
expect(context.stderr, isNot(contains(plutilErrorMessage)));
if (verbose) {
expect(context.stdout, contains(plutilErrorMessage));
} else {
expect(context.stdout, isNot(contains(plutilErrorMessage)));
}
},
);
test(
'Missing NSLocalNetworkUsageDescription in Info.plist should not fail Xcode compilation under ${verbose ? 'verbose' : 'non-verbose'} mode',
() {
final Directory buildDir = fileSystem.directory('/path/to/builds')
..createSync(recursive: true);
final File infoPlist = buildDir.childFile('Info.plist')..createSync();
const plutilErrorMessage =
'Could not extract value, error: No value at that key path or invalid key path: NSLocalNetworkUsageDescription';
final File pipe = fileSystem.file('/tmp/pipe')..createSync(recursive: true);
final context = TestContext(
<String>['test_vm_service_bonjour_service'],
<String, String>{
'CONFIGURATION': 'Debug',
'BUILT_PRODUCTS_DIR': buildDir.path,
'INFOPLIST_PATH': 'Info.plist',
if (verbose) 'VERBOSE_SCRIPT_LOGGING': 'YES',
},
commands: <FakeCommand>[
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,
],
exitCode: 1,
stderr: plutilErrorMessage,
),
FakeCommand(
command: <String>[
'plutil',
'-insert',
'NSLocalNetworkUsageDescription',
'-string',
'Allow Flutter tools on your computer to connect and debug your application. This prompt will not appear on release builds.',
infoPlist.path,
],
),
],
fileSystem: fileSystem,
scriptOutputStreamFile: pipe,
)..run();
expect(context.stderr, isNot(startsWith('error: ')));
expect(pipe.readAsString(), isNot(contains(plutilErrorMessage)));
expect(context.stderr, isNot(contains(plutilErrorMessage)));
if (verbose) {
expect(context.stdout, contains(plutilErrorMessage));
} else {
expect(context.stdout, isNot(contains(plutilErrorMessage)));
}
},
);
}
});
for (final platform in platforms) {
final String platformName = platform.name;
group('prepare for $platformName', () {
test('exits with useful error message when build mode not set', () {
final 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 buildMode = 'Debug';
final context = TestContext(
<String>['prepare', platformName],
<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=${targetPlatform.toLowerCase()}',
'-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}',
'-d${targetPlatform}Archs=',
'-dSdkRoot=',
'-dSplitDebugInfo=',
'-dTreeShakeIcons=',
'-dTrackWidgetCreation=',
'-dDartObfuscation=',
'-dAction=build',
'-dFrontendServerStarterPath=',
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dXcodeBuildScript=prepare',
'-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 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 buildMode = 'Debug';
final context = TestContext(
<String>['prepare', platformName],
<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=${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=',
'-dXcodeBuildScript=prepare',
if (platform == TargetPlatform.ios) ...<String>['-dTargetDeviceOSVersion='],
'debug_unpack_$platformName',
],
),
],
fileSystem: fileSystem,
scriptOutputStreamFile: pipe,
)..run();
expect(context.stderr, isEmpty);
});
test('forwards all env variables to flutter assemble', () {
final 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 archs = 'arm64';
const buildMode = 'Release';
const dartObfuscation = 'false';
const dartDefines = 'flutter.inspector.structuredErrors%3Dtrue';
const expandedCodeSignIdentity = 'F1326572E0B71C3C8442805230CB4B33B708A2E2';
const extraFrontEndOptions = '--some-option';
const extraGenSnapshotOptions = '--obfuscate';
const frontendServerStarterPath = '/path/to/frontend_server_starter.dart';
const sdkRoot = '/path/to/sdk';
const splitDebugInfo = '/path/to/split/debug/info';
const trackWidgetCreation = 'true';
const treeShake = 'true';
const srcRoot = '/path/to/project';
const iOSVersion = '18.3.1';
final 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,
},
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',
'-dXcodeBuildScript=prepare',
if (platform == TargetPlatform.ios) ...<String>[
'-dTargetDeviceOSVersion=$iOSVersion',
'-dCodesignIdentity=$expandedCodeSignIdentity',
],
'release_unpack_$platformName',
],
),
],
fileSystem: fileSystem,
)..run();
expect(context.stderr, isEmpty);
});
test('assumes ARCHS based on NATIVE_ARCH if ONLY_ACTIVE_ARCH is YES', () {
final 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 buildMode = 'Debug';
final 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',
},
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=',
'-dXcodeBuildScript=prepare',
if (platform == TargetPlatform.ios) ...<String>['-dTargetDeviceOSVersion='],
'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 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 buildMode = 'Debug';
final 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',
},
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=',
'-dXcodeBuildScript=prepare',
if (platform == TargetPlatform.ios) ...<String>['-dTargetDeviceOSVersion='],
'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 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 buildMode = 'Debug';
final 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',
},
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=',
'-dXcodeBuildScript=prepare',
if (platform == TargetPlatform.ios) ...<String>['-dTargetDeviceOSVersion='],
'debug_unpack_$platformName',
],
),
],
fileSystem: fileSystem,
scriptOutputStreamFile: pipe,
)..run();
expect(context.stderr, isEmpty);
});
});
}
group('embed for', () {
test('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 appPath = '/path/to/my_flutter_app';
const platformDirPath = '$appPath/ios';
const frameworksFolderPath = 'Runner.app/Frameworks';
final Directory flutterAssetsDir = targetBuildDir.childDirectory(
'$frameworksFolderPath/App.framework/flutter_assets',
)..createSync(recursive: true);
const ffiPackageName = 'package_a';
flutterAssetsDir
.childFile('NativeAssetsManifest.json')
.writeAsStringSync(
jsonEncode({
'format-version': [1, 0, 0],
'native-assets': {
'ios_arm64': {
'package:$ffiPackageName/native_asset.dart': [
'absolute',
'$ffiPackageName.framework/$ffiPackageName',
],
},
},
}),
);
const flutterBuildDir = 'build';
final Directory nativeAssetsDir = fileSystem.directory(
'$appPath/$flutterBuildDir/native_assets/ios/',
);
nativeAssetsDir.createSync(recursive: true);
final Directory ffiPackageDir = nativeAssetsDir.childDirectory('$ffiPackageName.framework')
..createSync();
nativeAssetsDir.childFile('random.txt').createSync();
// In addition to the ffiPackageName framework, create an additional unrelated framework in
// the same directory. It should not get copied since it is not referenced in the manifest.
final Directory unrelatedFramework = nativeAssetsDir.childDirectory('unrelated.framework')
..createSync();
unrelatedFramework.childFile('random.txt').createSync();
const infoPlistPath = 'Runner.app/Info.plist';
final File infoPlist = fileSystem.file('${buildDir.path}/$infoPlistPath');
infoPlist.createSync(recursive: true);
const buildMode = 'Debug';
final 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',
// We should copy $ffiPackageName.framework, but not the unrelated framework path.
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('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 appPath = '/path/to/my_flutter_app';
const platformDirPath = '$appPath/macos';
const frameworksFolderPath = 'Runner.app/Frameworks';
final Directory flutterAssetsDir = targetBuildDir.childDirectory(
'$frameworksFolderPath/App.framework/Resources/flutter_assets',
)..createSync(recursive: true);
const ffiPackageName = 'package_a';
flutterAssetsDir
.childFile('NativeAssetsManifest.json')
.writeAsStringSync(
jsonEncode({
'format-version': [1, 0, 0],
'native-assets': {
'ios_arm64': {
'package:$ffiPackageName/native_asset.dart': [
'absolute',
'$ffiPackageName.framework/$ffiPackageName',
],
},
},
}),
);
const flutterBuildDir = 'build';
final Directory nativeAssetsDir = fileSystem.directory(
'/path/to/my_flutter_app/$flutterBuildDir/native_assets/macos/',
);
nativeAssetsDir.createSync(recursive: true);
final Directory ffiPackageDir = nativeAssetsDir.childDirectory('$ffiPackageName.framework')
..createSync();
nativeAssetsDir.childFile('random.txt').createSync();
// In addition to the ffiPackageName framework, create an additional unrelated framework in
// the same directory. It should not get copied since it is not referenced in the manifest.
final Directory unrelatedFramework = nativeAssetsDir.childDirectory('unrelated.framework')
..createSync();
unrelatedFramework.childFile('random.txt').createSync();
const infoPlistPath = 'Runner.app/Info.plist';
final File infoPlist = fileSystem.file('${buildDir.path}/$infoPlistPath');
infoPlist.createSync(recursive: true);
const buildMode = 'Debug';
const codesignIdentity = '12312313';
final 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',
// We should copy $ffiPackageName.framework, but not the unrelated framework path.
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('when using SwiftPM', () {
test('skips embedding if already valid framework for iOS physical', () async {
final memoryFileSystem = MemoryFileSystem.test();
final flutterIOOverrides = FlutterIOOverrides(fileSystem: memoryFileSystem);
await io.IOOverrides.runWithIOOverrides(() async {
final Directory buildDir = memoryFileSystem.directory(
'/path/to/Build/Products/Debug-iphoneos',
)..createSync(recursive: true);
final Directory targetBuildDir = memoryFileSystem.directory(
'/path/to/Build/Products/Debug-iphoneos',
)..createSync(recursive: true);
final Directory flutterRootDir = memoryFileSystem.directory('/path/to/Flutter')
..createSync(recursive: true);
const appPath = '/path/to/my_flutter_app';
const platformDirPath = '$appPath/ios';
const frameworksFolderPath = 'Runner.app/Frameworks';
final Directory flutterSwiftPackageDir = memoryFileSystem.directory(
'$platformDirPath/Flutter/ephemeral/Packages/.packages/FlutterFramework',
)..createSync(recursive: true);
const matchingInfoPlist = 'asdf';
buildDir.childFile('Flutter.framework/Info.plist')
..createSync(recursive: true)
..writeAsStringSync(matchingInfoPlist);
targetBuildDir.childFile('$frameworksFolderPath/Flutter.framework/Info.plist')
..createSync(recursive: true)
..writeAsStringSync(matchingInfoPlist);
flutterRootDir.childFile(
'${flutterRootDir.path}/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64/Flutter.framework/Info.plist',
)
..createSync(recursive: true)
..writeAsStringSync(matchingInfoPlist);
final Directory flutterAssetsDir = targetBuildDir.childDirectory(
'$frameworksFolderPath/App.framework/flutter_assets',
)..createSync(recursive: true);
const ffiPackageName = 'package_a';
flutterAssetsDir
.childFile('NativeAssetsManifest.json')
.writeAsStringSync(
jsonEncode({
'format-version': [1, 0, 0],
'native-assets': {
'ios_arm64': {
'package:$ffiPackageName/native_asset.dart': [
'absolute',
'$ffiPackageName.framework/$ffiPackageName',
],
},
},
}),
);
const flutterBuildDir = 'build';
final Directory nativeAssetsDir = memoryFileSystem.directory(
'$appPath/$flutterBuildDir/native_assets/ios/',
);
nativeAssetsDir.createSync(recursive: true);
final Directory ffiPackageDir = nativeAssetsDir.childDirectory(
'$ffiPackageName.framework',
)..createSync();
nativeAssetsDir.childFile('random.txt').createSync();
// In addition to the ffiPackageName framework, create an additional unrelated framework in
// the same directory. It should not get copied since it is not referenced in the manifest.
final Directory unrelatedFramework = nativeAssetsDir.childDirectory('unrelated.framework')
..createSync();
unrelatedFramework.childFile('random.txt').createSync();
const infoPlistPath = 'Runner.app/Info.plist';
final File infoPlist = memoryFileSystem.file('${buildDir.path}/$infoPlistPath');
infoPlist.createSync(recursive: true);
const buildMode = 'Debug';
final testContext = TestContext(
<String>['embed_and_thin', 'ios'],
<String, String>{
'BUILT_PRODUCTS_DIR': buildDir.path,
'CONFIGURATION': buildMode,
'FLUTTER_ROOT': flutterRootDir.path,
'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',
'FLUTTER_FRAMEWORK_SWIFT_PACKAGE_PATH': flutterSwiftPackageDir.path,
'SDKROOT': 'iphoneos',
},
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',
'- 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: memoryFileSystem,
)..run();
expect(testContext.processManager.hasRemainingExpectations, isFalse);
}, flutterIOOverrides);
});
test('skips embedding if already valid framework for iOS simulator', () async {
final memoryFileSystem = MemoryFileSystem.test();
final flutterIOOverrides = FlutterIOOverrides(fileSystem: memoryFileSystem);
await io.IOOverrides.runWithIOOverrides(() async {
final Directory buildDir = memoryFileSystem.directory(
'/path/to/Build/Products/Debug-iphonesimulator',
)..createSync(recursive: true);
final Directory targetBuildDir = memoryFileSystem.directory(
'/path/to/Build/Products/Debug-iphonesimulator',
)..createSync(recursive: true);
final Directory flutterRootDir = memoryFileSystem.directory('/path/to/Flutter')
..createSync(recursive: true);
const appPath = '/path/to/my_flutter_app';
const platformDirPath = '$appPath/ios';
const frameworksFolderPath = 'Runner.app/Frameworks';
final Directory flutterSwiftPackageDir = memoryFileSystem.directory(
'$platformDirPath/Flutter/ephemeral/Packages/.packages/FlutterFramework',
)..createSync(recursive: true);
const matchingInfoPlist = 'asdf';
buildDir.childFile('Flutter.framework/Info.plist')
..createSync(recursive: true)
..writeAsStringSync(matchingInfoPlist);
targetBuildDir.childFile('$frameworksFolderPath/Flutter.framework/Info.plist')
..createSync(recursive: true)
..writeAsStringSync(matchingInfoPlist);
flutterRootDir.childFile(
'${flutterRootDir.path}/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Info.plist',
)
..createSync(recursive: true)
..writeAsStringSync(matchingInfoPlist);
final Directory flutterAssetsDir = targetBuildDir.childDirectory(
'$frameworksFolderPath/App.framework/flutter_assets',
)..createSync(recursive: true);
const ffiPackageName = 'package_a';
flutterAssetsDir
.childFile('NativeAssetsManifest.json')
.writeAsStringSync(
jsonEncode({
'format-version': [1, 0, 0],
'native-assets': {
'ios_arm64': {
'package:$ffiPackageName/native_asset.dart': [
'absolute',
'$ffiPackageName.framework/$ffiPackageName',
],
},
},
}),
);
const flutterBuildDir = 'build';
final Directory nativeAssetsDir = memoryFileSystem.directory(
'$appPath/$flutterBuildDir/native_assets/ios/',
);
nativeAssetsDir.createSync(recursive: true);
final Directory ffiPackageDir = nativeAssetsDir.childDirectory(
'$ffiPackageName.framework',
)..createSync();
nativeAssetsDir.childFile('random.txt').createSync();
// In addition to the ffiPackageName framework, create an additional unrelated framework in
// the same directory. It should not get copied since it is not referenced in the manifest.
final Directory unrelatedFramework = nativeAssetsDir.childDirectory('unrelated.framework')
..createSync();
unrelatedFramework.childFile('random.txt').createSync();
const infoPlistPath = 'Runner.app/Info.plist';
final File infoPlist = memoryFileSystem.file('${buildDir.path}/$infoPlistPath');
infoPlist.createSync(recursive: true);
const buildMode = 'Debug';
final testContext = TestContext(
<String>['embed_and_thin', 'ios'],
<String, String>{
'BUILT_PRODUCTS_DIR': buildDir.path,
'CONFIGURATION': buildMode,
'FLUTTER_ROOT': flutterRootDir.path,
'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',
'FLUTTER_FRAMEWORK_SWIFT_PACKAGE_PATH': flutterSwiftPackageDir.path,
'SDKROOT': 'iphonesimulator',
},
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',
'- 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: memoryFileSystem,
)..run();
expect(testContext.processManager.hasRemainingExpectations, isFalse);
}, flutterIOOverrides);
});
test('skips embedding if already valid framework for macos', () async {
final memoryFileSystem = MemoryFileSystem.test();
final flutterIOOverrides = FlutterIOOverrides(fileSystem: memoryFileSystem);
await io.IOOverrides.runWithIOOverrides(() async {
final Directory buildDir = memoryFileSystem.directory(
'/path/to/Build/Products/Debug-macosx',
)..createSync(recursive: true);
final Directory targetBuildDir = memoryFileSystem.directory(
'/path/to/Build/Products/Debug-macosx',
)..createSync(recursive: true);
final Directory flutterRootDir = memoryFileSystem.directory('/path/to/Flutter')
..createSync(recursive: true);
const appPath = '/path/to/my_flutter_app';
const platformDirPath = '$appPath/macos';
const frameworksFolderPath = 'Runner.app/Frameworks';
final Directory flutterSwiftPackageDir = memoryFileSystem.directory(
'$platformDirPath/Flutter/ephemeral/Packages/.packages/FlutterFramework',
)..createSync(recursive: true);
const matchingInfoPlist = 'asdf';
buildDir.childFile('FlutterMacOS.framework/Resources/Info.plist')
..createSync(recursive: true)
..writeAsStringSync(matchingInfoPlist);
targetBuildDir.childFile(
'$frameworksFolderPath/FlutterMacOS.framework/Resources/Info.plist',
)
..createSync(recursive: true)
..writeAsStringSync(matchingInfoPlist);
flutterRootDir.childFile(
'${flutterRootDir.path}/bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.xcframework/macos-arm64_x86_64/FlutterMacOS.framework/Resources/Info.plist',
)
..createSync(recursive: true)
..writeAsStringSync(matchingInfoPlist);
final Directory flutterAssetsDir = targetBuildDir.childDirectory(
'$frameworksFolderPath/App.framework/Resources/flutter_assets',
)..createSync(recursive: true);
const ffiPackageName = 'package_a';
flutterAssetsDir
.childFile('NativeAssetsManifest.json')
.writeAsStringSync(
jsonEncode({
'format-version': [1, 0, 0],
'native-assets': {
'macos_arm64': {
'package:$ffiPackageName/native_asset.dart': [
'absolute',
'$ffiPackageName.framework/$ffiPackageName',
],
},
},
}),
);
const flutterBuildDir = 'build';
final Directory nativeAssetsDir = memoryFileSystem.directory(
'$appPath/$flutterBuildDir/native_assets/macos/',
);
nativeAssetsDir.createSync(recursive: true);
final Directory ffiPackageDir = nativeAssetsDir.childDirectory(
'$ffiPackageName.framework',
)..createSync();
nativeAssetsDir.childFile('random.txt').createSync();
// In addition to the ffiPackageName framework, create an additional unrelated framework in
// the same directory. It should not get copied since it is not referenced in the manifest.
final Directory unrelatedFramework = nativeAssetsDir.childDirectory('unrelated.framework')
..createSync();
unrelatedFramework.childFile('random.txt').createSync();
const infoPlistPath = 'Runner.app/Info.plist';
final File infoPlist = memoryFileSystem.file('${buildDir.path}/$infoPlistPath');
infoPlist.createSync(recursive: true);
const buildMode = 'Debug';
final testContext = TestContext(
<String>['embed_and_thin', 'macos'],
<String, String>{
'BUILT_PRODUCTS_DIR': buildDir.path,
'CONFIGURATION': buildMode,
'FLUTTER_ROOT': flutterRootDir.path,
'INFOPLIST_PATH': infoPlistPath,
'SOURCE_ROOT': platformDirPath,
'FLUTTER_APPLICATION_PATH': appPath,
'FLUTTER_BUILD_DIR': flutterBuildDir,
'TARGET_BUILD_DIR': targetBuildDir.path,
'FRAMEWORKS_FOLDER_PATH': frameworksFolderPath,
'FLUTTER_FRAMEWORK_SWIFT_PACKAGE_PATH': flutterSwiftPackageDir.path,
'SDKROOT': 'macosx',
},
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',
'- native_assets.yaml',
'--filter',
'- native_assets.json',
ffiPackageDir.path,
targetBuildDir.childDirectory(frameworksFolderPath).path,
],
),
],
fileSystem: memoryFileSystem,
)..run();
expect(testContext.processManager.hasRemainingExpectations, isFalse);
}, flutterIOOverrides);
});
test('unpacks before embedding if not valid framework for iOS', () async {
final memoryFileSystem = MemoryFileSystem.test();
final flutterIOOverrides = FlutterIOOverrides(fileSystem: memoryFileSystem);
await io.IOOverrides.runWithIOOverrides(() async {
final Directory buildDir = memoryFileSystem.directory(
'/path/to/Build/Products/Debug-iphoneos',
)..createSync(recursive: true);
final Directory targetBuildDir = memoryFileSystem.directory(
'/path/to/Build/Products/Debug-iphoneos',
)..createSync(recursive: true);
final Directory flutterRootDir = memoryFileSystem.directory('/path/to/Flutter')
..createSync(recursive: true);
const appPath = '/path/to/my_flutter_app';
const platformDirPath = '$appPath/ios';
const frameworksFolderPath = 'Runner.app/Frameworks';
final Directory flutterSwiftPackageDir = memoryFileSystem.directory(
'$platformDirPath/Flutter/ephemeral/Packages/.packages/FlutterFramework',
)..createSync(recursive: true);
const matchingInfoPlist = 'asdf';
buildDir.childFile('Flutter.framework/Info.plist')
..createSync(recursive: true)
..writeAsStringSync('not matching');
targetBuildDir.childFile('$frameworksFolderPath/Flutter.framework/Info.plist')
..createSync(recursive: true)
..writeAsStringSync(matchingInfoPlist);
flutterRootDir.childFile(
'${flutterRootDir.path}/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64/Flutter.framework/Info.plist',
)
..createSync(recursive: true)
..writeAsStringSync(matchingInfoPlist);
final Directory flutterAssetsDir = targetBuildDir.childDirectory(
'$frameworksFolderPath/App.framework/flutter_assets',
)..createSync(recursive: true);
const ffiPackageName = 'package_a';
flutterAssetsDir
.childFile('NativeAssetsManifest.json')
.writeAsStringSync(
jsonEncode({
'format-version': [1, 0, 0],
'native-assets': {
'ios_arm64': {
'package:$ffiPackageName/native_asset.dart': [
'absolute',
'$ffiPackageName.framework/$ffiPackageName',
],
},
},
}),
);
const flutterBuildDir = 'build';
final Directory nativeAssetsDir = memoryFileSystem.directory(
'$appPath/$flutterBuildDir/native_assets/ios/',
);
nativeAssetsDir.createSync(recursive: true);
final Directory ffiPackageDir = nativeAssetsDir.childDirectory(
'$ffiPackageName.framework',
)..createSync();
nativeAssetsDir.childFile('random.txt').createSync();
// In addition to the ffiPackageName framework, create an additional unrelated framework in
// the same directory. It should not get copied since it is not referenced in the manifest.
final Directory unrelatedFramework = nativeAssetsDir.childDirectory('unrelated.framework')
..createSync();
unrelatedFramework.childFile('random.txt').createSync();
const infoPlistPath = 'Runner.app/Info.plist';
final File infoPlist = memoryFileSystem.file('${buildDir.path}/$infoPlistPath');
infoPlist.createSync(recursive: true);
const buildMode = 'Debug';
final testContext = TestContext(
<String>['embed_and_thin', 'ios'],
<String, String>{
'BUILT_PRODUCTS_DIR': buildDir.path,
'CONFIGURATION': buildMode,
'FLUTTER_ROOT': flutterRootDir.path,
'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',
'FLUTTER_FRAMEWORK_SWIFT_PACKAGE_PATH': flutterSwiftPackageDir.path,
'SDKROOT': 'iphoneos',
},
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>[
'${flutterRootDir.path}/bin/flutter',
'assemble',
'--no-version-check',
'--output=${buildDir.path}/',
'-dTargetPlatform=ios',
'-dTargetFile=lib/main.dart',
'-dBuildMode=debug',
'-dConfiguration=Debug',
'-dIosArchs=',
'-dSdkRoot=iphoneos',
'-dSplitDebugInfo=',
'-dTreeShakeIcons=',
'-dTrackWidgetCreation=',
'-dDartObfuscation=',
'-dAction=',
'-dFrontendServerStarterPath=',
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dSrcRoot=',
'-dXcodeBuildScript=embed',
'-dTargetDeviceOSVersion=',
'-dCodesignIdentity=12312313',
'debug_unpack_ios',
],
),
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: memoryFileSystem,
)..run();
expect(testContext.processManager.hasRemainingExpectations, isFalse);
}, flutterIOOverrides);
});
test('unpacks before embedding if not valid framework for macos', () async {
final memoryFileSystem = MemoryFileSystem.test();
final flutterIOOverrides = FlutterIOOverrides(fileSystem: memoryFileSystem);
await io.IOOverrides.runWithIOOverrides(() async {
final Directory buildDir = memoryFileSystem.directory(
'/path/to/Build/Products/Debug-macosx',
)..createSync(recursive: true);
final Directory targetBuildDir = memoryFileSystem.directory(
'/path/to/Build/Products/Debug-macosx',
)..createSync(recursive: true);
final Directory flutterRootDir = memoryFileSystem.directory('/path/to/Flutter')
..createSync(recursive: true);
const appPath = '/path/to/my_flutter_app';
const platformDirPath = '$appPath/macos';
const frameworksFolderPath = 'Runner.app/Frameworks';
final Directory flutterSwiftPackageDir = memoryFileSystem.directory(
'$platformDirPath/Flutter/ephemeral/Packages/.packages/FlutterFramework',
)..createSync(recursive: true);
const matchingInfoPlist = 'asdf';
buildDir.childFile('FlutterMacOS.framework/Resources/Info.plist')
..createSync(recursive: true)
..writeAsStringSync('not matching');
targetBuildDir.childFile(
'$frameworksFolderPath/FlutterMacOS.framework/Resources/Info.plist',
)
..createSync(recursive: true)
..writeAsStringSync(matchingInfoPlist);
flutterRootDir.childFile(
'${flutterRootDir.path}/bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.xcframework/macos-arm64_x86_64/FlutterMacOS.framework/Resources/Info.plist',
)
..createSync(recursive: true)
..writeAsStringSync(matchingInfoPlist);
final Directory flutterAssetsDir = targetBuildDir.childDirectory(
'$frameworksFolderPath/App.framework/Resources/flutter_assets',
)..createSync(recursive: true);
const ffiPackageName = 'package_a';
flutterAssetsDir
.childFile('NativeAssetsManifest.json')
.writeAsStringSync(
jsonEncode({
'format-version': [1, 0, 0],
'native-assets': {
'macos_arm64': {
'package:$ffiPackageName/native_asset.dart': [
'absolute',
'$ffiPackageName.framework/$ffiPackageName',
],
},
},
}),
);
const flutterBuildDir = 'build';
final Directory nativeAssetsDir = memoryFileSystem.directory(
'$appPath/$flutterBuildDir/native_assets/macos/',
);
nativeAssetsDir.createSync(recursive: true);
final Directory ffiPackageDir = nativeAssetsDir.childDirectory(
'$ffiPackageName.framework',
)..createSync();
nativeAssetsDir.childFile('random.txt').createSync();
// In addition to the ffiPackageName framework, create an additional unrelated framework in
// the same directory. It should not get copied since it is not referenced in the manifest.
final Directory unrelatedFramework = nativeAssetsDir.childDirectory('unrelated.framework')
..createSync();
unrelatedFramework.childFile('random.txt').createSync();
const infoPlistPath = 'Runner.app/Info.plist';
final File infoPlist = memoryFileSystem.file('${buildDir.path}/$infoPlistPath');
infoPlist.createSync(recursive: true);
const buildMode = 'Debug';
final testContext = TestContext(
<String>['embed_and_thin', 'macos'],
<String, String>{
'BUILT_PRODUCTS_DIR': buildDir.path,
'CONFIGURATION': buildMode,
'FLUTTER_ROOT': flutterRootDir.path,
'INFOPLIST_PATH': infoPlistPath,
'SOURCE_ROOT': platformDirPath,
'FLUTTER_APPLICATION_PATH': appPath,
'FLUTTER_BUILD_DIR': flutterBuildDir,
'TARGET_BUILD_DIR': targetBuildDir.path,
'FRAMEWORKS_FOLDER_PATH': frameworksFolderPath,
'FLUTTER_FRAMEWORK_SWIFT_PACKAGE_PATH': flutterSwiftPackageDir.path,
'SDKROOT': 'macosx',
},
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>[
'${flutterRootDir.path}/bin/flutter',
'assemble',
'--no-version-check',
'--output=${buildDir.path}/',
'-dTargetPlatform=darwin',
'-dTargetFile=lib/main.dart',
'-dBuildMode=debug',
'-dConfiguration=Debug',
'-dDarwinArchs=',
'-dSdkRoot=macosx',
'-dSplitDebugInfo=',
'-dTreeShakeIcons=',
'-dTrackWidgetCreation=',
'-dDartObfuscation=',
'-dAction=',
'-dFrontendServerStarterPath=',
'--ExtraGenSnapshotOptions=',
'--DartDefines=',
'--ExtraFrontEndOptions=',
'-dSrcRoot=',
'-dXcodeBuildScript=embed',
'debug_unpack_macos',
],
),
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>[
'rsync',
'-8',
'-av',
'--delete',
'--filter',
'- .DS_Store',
'--filter',
'- native_assets.yaml',
'--filter',
'- native_assets.json',
ffiPackageDir.path,
targetBuildDir.childDirectory(frameworksFolderPath).path,
],
),
],
fileSystem: memoryFileSystem,
)..run();
expect(testContext.processManager.hasRemainingExpectations, isFalse);
}, flutterIOOverrides);
});
});
test('reports error message for invalid native assets manifest', () {
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 appPath = '/path/to/my_flutter_app';
const platformDirPath = '$appPath/macos';
const frameworksFolderPath = 'Runner.app/Frameworks';
final Directory flutterAssetsDir = targetBuildDir.childDirectory(
'$frameworksFolderPath/App.framework/Resources/flutter_assets',
)..createSync(recursive: true);
flutterAssetsDir
.childFile('NativeAssetsManifest.json')
.writeAsStringSync(
jsonEncode({
'format-version': [1, 0, 0],
'native-assets': {'ios_arm64': 'invalid content'},
}),
);
const flutterBuildDir = 'build';
const infoPlistPath = 'Runner.app/Info.plist';
final File infoPlist = fileSystem.file('${buildDir.path}/$infoPlistPath');
infoPlist.createSync(recursive: true);
const buildMode = 'Debug';
const codesignIdentity = '12312313';
final 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,
],
),
],
fileSystem: fileSystem,
);
expect(
testContext.run,
throwsA(
isA<Exception>().having(
(e) => e.toString(),
'toString()',
contains('App exited with code -1'),
),
),
);
expect(testContext.processManager.hasRemainingExpectations, isFalse);
expect(testContext.stderr, contains('error: Failed to embed native assets:'));
});
});
}
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
File fileFromPath(String path) {
return fileSystem.file(path);
}
@override
ProcessResult runSyncProcess(String bin, List<String> args, {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');
}
}