From 2e229be2ff1df5dcfbcc3872c8447dbe9d7b4fcd Mon Sep 17 00:00:00 2001 From: Daco Harkes Date: Fri, 19 Jan 2024 21:29:13 +0100 Subject: [PATCH] Native assets: package in framework on iOS and MacOS (#140907) Packages the native assets for iOS and MacOS in frameworks. Issue: * https://github.com/flutter/flutter/issues/140544 * https://github.com/flutter/flutter/issues/129757 ## Details * [x] This packages dylibs from the native assets feature in frameworks. It packages every dylib in a separate framework. * [x] The dylib name is updated to use `@rpath` instead of `@executable_path`. * [x] The dylibs for flutter-tester are no longer modified to change the install name. (Previously it was wrongly updating the install name to the location the dylib would have once deployed in an app.) * [x] Use symlinking on MacOS. --- dev/devicelab/bin/tasks/module_test_ios.dart | 10 +- .../lib/src/ios/native_assets.dart | 67 +++++++- .../lib/src/macos/native_assets.dart | 162 +++++++++++++++++- .../lib/src/macos/native_assets_host.dart | 162 ++++++++++++------ .../targets/native_assets_test.dart | 4 +- .../general.shard/ios/native_assets_test.dart | 16 +- .../macos/native_assets_host_test.dart | 68 ++++++++ .../macos/native_assets_test.dart | 48 ++++-- .../integration.shard/native_assets_test.dart | 49 +++++- 9 files changed, 479 insertions(+), 107 deletions(-) create mode 100644 packages/flutter_tools/test/general.shard/macos/native_assets_host_test.dart diff --git a/dev/devicelab/bin/tasks/module_test_ios.dart b/dev/devicelab/bin/tasks/module_test_ios.dart index 1ddbc653e3e..b0875d7ac51 100644 --- a/dev/devicelab/bin/tasks/module_test_ios.dart +++ b/dev/devicelab/bin/tasks/module_test_ios.dart @@ -250,9 +250,7 @@ dependencies: checkDirectoryNotExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', '$dartPluginName.framework')); // Native assets embedded, no embedded framework. - const String libFfiPackageDylib = 'lib$ffiPackageName.dylib'; - checkFileExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', libFfiPackageDylib)); - checkDirectoryNotExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', '$ffiPackageName.framework')); + checkFileExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', '$ffiPackageName.framework', ffiPackageName)); section('Clean and pub get module'); @@ -385,7 +383,8 @@ end checkFileExists(path.join( hostFrameworksDirectory, - libFfiPackageDylib, + '$ffiPackageName.framework', + ffiPackageName, )); section('Check the NOTICE file is correct'); @@ -491,7 +490,8 @@ end checkFileExists(path.join( archivedAppPath, 'Frameworks', - libFfiPackageDylib, + '$ffiPackageName.framework', + ffiPackageName, )); // The host app example builds plugins statically, url_launcher_ios.framework diff --git a/packages/flutter_tools/lib/src/ios/native_assets.dart b/packages/flutter_tools/lib/src/ios/native_assets.dart index a90b484e9e0..7b9c84a290a 100644 --- a/packages/flutter_tools/lib/src/ios/native_assets.dart +++ b/packages/flutter_tools/lib/src/ios/native_assets.dart @@ -106,7 +106,7 @@ Future> buildNativeAssetsIOS({ ensureNoLinkModeStatic(nativeAssets); globals.logger.printTrace('Building native assets for $targets done.'); final Map> fatAssetTargetLocations = _fatAssetTargetLocations(nativeAssets); - await copyNativeAssetsMacOSHost( + await _copyNativeAssetsIOS( buildUri, fatAssetTargetLocations, codesignIdentity, @@ -145,21 +145,25 @@ Target _getNativeTarget(DarwinArch darwinArch) { } Map> _fatAssetTargetLocations(List nativeAssets) { + final Set alreadyTakenNames = {}; final Map> result = >{}; for (final Asset asset in nativeAssets) { - final AssetPath path = _targetLocationIOS(asset).path; + final AssetPath path = _targetLocationIOS(asset, alreadyTakenNames).path; result[path] ??= []; result[path]!.add(asset); } return result; } -Map _assetTargetLocations(List nativeAssets) => { - for (final Asset asset in nativeAssets) - asset: _targetLocationIOS(asset), -}; +Map _assetTargetLocations(List nativeAssets) { + final Set alreadyTakenNames = {}; + return { + for (final Asset asset in nativeAssets) + asset: _targetLocationIOS(asset, alreadyTakenNames), + }; +} -Asset _targetLocationIOS(Asset asset) { +Asset _targetLocationIOS(Asset asset, Set alreadyTakenNames) { final AssetPath path = asset.path; switch (path) { case AssetSystemPath _: @@ -168,7 +172,52 @@ Asset _targetLocationIOS(Asset asset) { return asset; case AssetAbsolutePath _: final String fileName = path.uri.pathSegments.last; - return asset.copyWith(path: AssetAbsolutePath(Uri(path: fileName))); + return asset.copyWith( + path: AssetAbsolutePath(frameworkUri(fileName, alreadyTakenNames)), + ); + } + throw Exception( + 'Unsupported asset path type ${path.runtimeType} in asset $asset'); +} + +/// Copies native assets into a framework per dynamic library. +/// +/// For `flutter run -release` a multi-architecture solution is needed. So, +/// `lipo` is used to combine all target architectures into a single file. +/// +/// The install name is set so that it matches what the place it will +/// be bundled in the final app. +/// +/// Code signing is also done here, so that it doesn't have to be done in +/// in xcode_backend.dart. +Future _copyNativeAssetsIOS( + Uri buildUri, + Map> assetTargetLocations, + String? codesignIdentity, + BuildMode buildMode, + FileSystem fileSystem, +) async { + if (assetTargetLocations.isNotEmpty) { + globals.logger + .printTrace('Copying native assets to ${buildUri.toFilePath()}.'); + for (final MapEntry> assetMapping + in assetTargetLocations.entries) { + final Uri target = (assetMapping.key as AssetAbsolutePath).uri; + final List sources = [ + for (final Asset source in assetMapping.value) + (source.path as AssetAbsolutePath).uri + ]; + final Uri targetUri = buildUri.resolveUri(target); + final File dylibFile = fileSystem.file(targetUri); + final Directory frameworkDir = dylibFile.parent; + if (!await frameworkDir.exists()) { + await frameworkDir.create(recursive: true); + } + await lipoDylibs(dylibFile, sources); + await setInstallNameDylib(dylibFile); + await createInfoPlist(targetUri.pathSegments.last, frameworkDir); + await codesignDylib(codesignIdentity, buildMode, frameworkDir); + } + globals.logger.printTrace('Copying native assets done.'); } - throw Exception('Unsupported asset path type ${path.runtimeType} in asset $asset'); } diff --git a/packages/flutter_tools/lib/src/macos/native_assets.dart b/packages/flutter_tools/lib/src/macos/native_assets.dart index 8811371fef2..30eed141404 100644 --- a/packages/flutter_tools/lib/src/macos/native_assets.dart +++ b/packages/flutter_tools/lib/src/macos/native_assets.dart @@ -108,7 +108,23 @@ Future<(Uri? nativeAssetsYaml, List dependencies)> buildNativeAssetsMacOS({ final Uri? absolutePath = flutterTester ? buildUri : null; final Map assetTargetLocations = _assetTargetLocations(nativeAssets, absolutePath); final Map> fatAssetTargetLocations = _fatAssetTargetLocations(nativeAssets, absolutePath); - await copyNativeAssetsMacOSHost(buildUri, fatAssetTargetLocations, codesignIdentity, buildMode, fileSystem); + if (flutterTester) { + await _copyNativeAssetsMacOSFlutterTester( + buildUri, + fatAssetTargetLocations, + codesignIdentity, + buildMode, + fileSystem, + ); + } else { + await _copyNativeAssetsMacOS( + buildUri, + fatAssetTargetLocations, + codesignIdentity, + buildMode, + fileSystem, + ); + } final Uri nativeAssetsUri = await writeNativeAssetsYaml(assetTargetLocations.values, yamlParentDirectory ?? buildUri, fileSystem); return (nativeAssetsUri, dependencies.toList()); } @@ -125,22 +141,40 @@ Target _getNativeTarget(DarwinArch darwinArch) { } } -Map> _fatAssetTargetLocations(List nativeAssets, Uri? absolutePath) { +Map> _fatAssetTargetLocations( + List nativeAssets, + Uri? absolutePath, +) { + final Set alreadyTakenNames = {}; final Map> result = >{}; for (final Asset asset in nativeAssets) { - final AssetPath path = _targetLocationMacOS(asset, absolutePath).path; + final AssetPath path = _targetLocationMacOS( + asset, + absolutePath, + alreadyTakenNames, + ).path; result[path] ??= []; result[path]!.add(asset); } return result; } -Map _assetTargetLocations(List nativeAssets, Uri? absolutePath) => { - for (final Asset asset in nativeAssets) - asset: _targetLocationMacOS(asset, absolutePath), -}; +Map _assetTargetLocations( + List nativeAssets, + Uri? absolutePath, +) { + final Set alreadyTakenNames = {}; + return { + for (final Asset asset in nativeAssets) + asset: _targetLocationMacOS(asset, absolutePath, alreadyTakenNames), + }; +} -Asset _targetLocationMacOS(Asset asset, Uri? absolutePath) { +Asset _targetLocationMacOS( + Asset asset, + Uri? absolutePath, + Set alreadyTakenNames, +) { final AssetPath path = asset.path; switch (path) { case AssetSystemPath _: @@ -157,9 +191,119 @@ Asset _targetLocationMacOS(Asset asset, Uri? absolutePath) { // Flutter Desktop needs "absolute" paths inside the app. // "relative" in the context of native assets would be relative to the // kernel or aot snapshot. - uri = Uri(path: fileName); + uri = frameworkUri(fileName, alreadyTakenNames); + } return asset.copyWith(path: AssetAbsolutePath(uri)); } throw Exception('Unsupported asset path type ${path.runtimeType} in asset $asset'); } + +/// Copies native assets into a framework per dynamic library. +/// +/// The framework contains symlinks according to +/// https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/FrameworkAnatomy.html +/// +/// For `flutter run -release` a multi-architecture solution is needed. So, +/// `lipo` is used to combine all target architectures into a single file. +/// +/// The install name is set so that it matches what the place it will +/// be bundled in the final app. +/// +/// Code signing is also done here, so that it doesn't have to be done in +/// in macos_assemble.sh. +Future _copyNativeAssetsMacOS( + Uri buildUri, + Map> assetTargetLocations, + String? codesignIdentity, + BuildMode buildMode, + FileSystem fileSystem, +) async { + if (assetTargetLocations.isNotEmpty) { + globals.logger.printTrace('Copying native assets to ${buildUri.toFilePath()}.'); + for (final MapEntry> assetMapping in assetTargetLocations.entries) { + final Uri target = (assetMapping.key as AssetAbsolutePath).uri; + final List sources = [ + for (final Asset source in assetMapping.value) + (source.path as AssetAbsolutePath).uri, + ]; + final Uri targetUri = buildUri.resolveUri(target); + final String name = targetUri.pathSegments.last; + final Directory frameworkDir = fileSystem.file(targetUri).parent; + if (await frameworkDir.exists()) { + await frameworkDir.delete(recursive: true); + } + // MyFramework.framework/ frameworkDir + // MyFramework -> Versions/Current/MyFramework dylibLink + // Resources -> Versions/Current/Resources resourcesLink + // Versions/ versionsDir + // A/ versionADir + // MyFramework dylibFile + // Resources/ resourcesDir + // Info.plist + // Current -> A currentLink + final Directory versionsDir = frameworkDir.childDirectory('Versions'); + final Directory versionADir = versionsDir.childDirectory('A'); + final Directory resourcesDir = versionADir.childDirectory('Resources'); + await resourcesDir.create(recursive: true); + final File dylibFile = versionADir.childFile(name); + final Link currentLink = versionsDir.childLink('Current'); + await currentLink.create(fileSystem.path.relative( + versionADir.path, + from: currentLink.parent.path, + )); + final Link resourcesLink = frameworkDir.childLink('Resources'); + await resourcesLink.create(fileSystem.path.relative( + resourcesDir.path, + from: resourcesLink.parent.path, + )); + await lipoDylibs(dylibFile, sources); + final Link dylibLink = frameworkDir.childLink(name); + await dylibLink.create(fileSystem.path.relative( + versionsDir.childDirectory('Current').childFile(name).path, + from: dylibLink.parent.path, + )); + await setInstallNameDylib(dylibFile); + await createInfoPlist(name, resourcesDir); + await codesignDylib(codesignIdentity, buildMode, frameworkDir); + } + globals.logger.printTrace('Copying native assets done.'); + } +} + + +/// Copies native assets for flutter tester. +/// +/// For `flutter run -release` a multi-architecture solution is needed. So, +/// `lipo` is used to combine all target architectures into a single file. +/// +/// In contrast to [_copyNativeAssetsMacOS], it does not set the install name. +/// +/// Code signing is also done here. +Future _copyNativeAssetsMacOSFlutterTester( + Uri buildUri, + Map> assetTargetLocations, + String? codesignIdentity, + BuildMode buildMode, + FileSystem fileSystem, +) async { + if (assetTargetLocations.isNotEmpty) { + globals.logger.printTrace('Copying native assets to ${buildUri.toFilePath()}.'); + for (final MapEntry> assetMapping in assetTargetLocations.entries) { + final Uri target = (assetMapping.key as AssetAbsolutePath).uri; + final List sources = [ + for (final Asset source in assetMapping.value) + (source.path as AssetAbsolutePath).uri, + ]; + final Uri targetUri = buildUri.resolveUri(target); + final File dylibFile = fileSystem.file(targetUri); + final Directory targetParent = dylibFile.parent; + if (!await targetParent.exists()) { + await targetParent.create(recursive: true); + } + await lipoDylibs(dylibFile, sources); + await codesignDylib(codesignIdentity, buildMode, dylibFile); + } + globals.logger.printTrace('Copying native assets done.'); + } +} diff --git a/packages/flutter_tools/lib/src/macos/native_assets_host.dart b/packages/flutter_tools/lib/src/macos/native_assets_host.dart index 107ac9045c1..ce370991a57 100644 --- a/packages/flutter_tools/lib/src/macos/native_assets_host.dart +++ b/packages/flutter_tools/lib/src/macos/native_assets_host.dart @@ -13,41 +13,40 @@ import '../build_info.dart'; import '../convert.dart'; import '../globals.dart' as globals; -/// The target location for native assets on macOS. +/// Create an `Info.plist` in [target] for a framework with a single dylib. /// -/// Because we need to have a multi-architecture solution for -/// `flutter run --release`, we use `lipo` to combine all target architectures -/// into a single file. -/// -/// We need to set the install name so that it matches what the place it will -/// be bundled in the final app. -/// -/// Code signing is also done here, so that we don't have to worry about it -/// in xcode_backend.dart and macos_assemble.sh. -Future copyNativeAssetsMacOSHost( - Uri buildUri, - Map> assetTargetLocations, - String? codesignIdentity, - BuildMode buildMode, - FileSystem fileSystem, +/// The framework must be named [name].framework and the dylib [name]. +Future createInfoPlist( + String name, + Directory target, ) async { - if (assetTargetLocations.isNotEmpty) { - globals.logger.printTrace('Copying native assets to ${buildUri.toFilePath()}.'); - final Directory buildDir = fileSystem.directory(buildUri.toFilePath()); - if (!buildDir.existsSync()) { - buildDir.createSync(recursive: true); - } - for (final MapEntry> assetMapping in assetTargetLocations.entries) { - final Uri target = (assetMapping.key as AssetAbsolutePath).uri; - final List sources = [for (final Asset source in assetMapping.value) (source.path as AssetAbsolutePath).uri]; - final Uri targetUri = buildUri.resolveUri(target); - final String targetFullPath = targetUri.toFilePath(); - await lipoDylibs(targetFullPath, sources); - await setInstallNameDylib(targetUri); - await codesignDylib(codesignIdentity, buildMode, targetFullPath); - } - globals.logger.printTrace('Copying native assets done.'); - } + final File infoPlistFile = target.childFile('Info.plist'); + await infoPlistFile.writeAsString(''' + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $name + CFBundleIdentifier + io.flutter.flutter.native_assets.$name + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $name + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + + '''); } /// Combines dylibs from [sources] into a fat binary at [targetFullPath]. @@ -55,13 +54,13 @@ Future copyNativeAssetsMacOSHost( /// The dylibs must have different architectures. E.g. a dylib targeting /// arm64 ios simulator cannot be combined with a dylib targeting arm64 /// ios device or macos arm64. -Future lipoDylibs(String targetFullPath, List sources) async { +Future lipoDylibs(File target, List sources) async { final ProcessResult lipoResult = await globals.processManager.run( [ 'lipo', '-create', '-output', - targetFullPath, + target.path, for (final Uri source in sources) source.toFilePath(), ], ); @@ -78,25 +77,27 @@ Future lipoDylibs(String targetFullPath, List sources) async { /// dylib itself does not correspond to the path that the file is at. Therefore, /// native assets copied into their final location also need their install name /// updated with the `install_name_tool`. -Future setInstallNameDylib(Uri targetUri) async { - final String fileName = targetUri.pathSegments.last; +Future setInstallNameDylib(File dylibFile) async { + final String fileName = dylibFile.basename; final ProcessResult installNameResult = await globals.processManager.run( [ 'install_name_tool', '-id', - '@executable_path/Frameworks/$fileName', - targetUri.toFilePath(), + '@rpath/$fileName.framework/$fileName', + dylibFile.path, ], ); if (installNameResult.exitCode != 0) { - throwToolExit('Failed to change the install name of $targetUri:\n${installNameResult.stderr}'); + throwToolExit( + 'Failed to change the install name of $dylibFile:\n${installNameResult.stderr}', + ); } } Future codesignDylib( String? codesignIdentity, BuildMode buildMode, - String targetFullPath, + FileSystemEntity target, ) async { if (codesignIdentity == null || codesignIdentity.isEmpty) { codesignIdentity = '-'; @@ -110,12 +111,17 @@ Future codesignDylib( // Mimic Xcode's timestamp codesigning behavior on non-release binaries. '--timestamp=none', ], - targetFullPath, + target.path, ]; globals.logger.printTrace(codesignCommand.join(' ')); - final ProcessResult codesignResult = await globals.processManager.run(codesignCommand); + final ProcessResult codesignResult = await globals.processManager.run( + codesignCommand, + ); if (codesignResult.exitCode != 0) { - throwToolExit('Failed to code sign binary:\n${codesignResult.stderr}'); + throwToolExit( + 'Failed to code sign binary: exit code: ${codesignResult.exitCode} ' + '${codesignResult.stdout} ${codesignResult.stderr}', + ); } globals.logger.printTrace(codesignResult.stdout as String); globals.logger.printTrace(codesignResult.stderr as String); @@ -125,17 +131,77 @@ Future codesignDylib( /// /// Use the `clang`, `ar`, and `ld` that would be used if run with `xcrun`. Future cCompilerConfigMacOS() async { - final ProcessResult xcrunResult = await globals.processManager.run(['xcrun', 'clang', '--version']); + final ProcessResult xcrunResult = await globals.processManager.run( + ['xcrun', 'clang', '--version'], + ); if (xcrunResult.exitCode != 0) { throwToolExit('Failed to find clang with xcrun:\n${xcrunResult.stderr}'); } final String installPath = LineSplitter.split(xcrunResult.stdout as String) - .firstWhere((String s) => s.startsWith('InstalledDir: ')) - .split(' ') - .last; + .firstWhere((String s) => s.startsWith('InstalledDir: ')) + .split(' ') + .last; return CCompilerConfig( cc: Uri.file('$installPath/clang'), ar: Uri.file('$installPath/ar'), ld: Uri.file('$installPath/ld'), ); } + +/// Converts [fileName] into a suitable framework name. +/// +/// On MacOS and iOS, dylibs need to be packaged in a framework. +/// +/// In order for resolution to work, the file name inside the framework must be +/// equal to the framework name. +/// +/// Dylib names on MacOS/iOS usually have a dylib extension. If so, remove it. +/// +/// Dylib names on MacOS/iOS are usually prefixed with 'lib'. So, if the file is +/// a dylib, try to remove the prefix. +/// +/// The bundle ID string must contain only alphanumeric characters +/// (A–Z, a–z, and 0–9), hyphens (-), and periods (.). +/// https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier +/// +/// This name can contain up to 15 characters. +/// https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundlename +/// +/// The [alreadyTakenNames] are used to ensure that the framework name does not +/// conflict with previously chosen names. +Uri frameworkUri(String fileName, Set alreadyTakenNames) { + final List splitFileName = fileName.split('.'); + final bool isDylib; + if (splitFileName.length >= 2) { + isDylib = splitFileName.last == 'dylib'; + if (isDylib) { + fileName = splitFileName.sublist(0, splitFileName.length - 1).join('.'); + } + } else { + isDylib = false; + } + if (isDylib && fileName.startsWith('lib')) { + fileName = fileName.replaceFirst('lib', ''); + } + fileName = fileName.replaceAll(RegExp(r'[^A-Za-z0-9_-]'), ''); + if (fileName.length > 15) { + fileName = fileName.substring(0, 15); + } + if (alreadyTakenNames.contains(fileName)) { + if (fileName.length > 12) { + fileName = fileName.substring(0, 12); + } + final String prefixName = fileName; + for (int i = 1; i < 1000; i++) { + fileName = '$prefixName$i'; + if (!alreadyTakenNames.contains(fileName)) { + break; + } + } + if (alreadyTakenNames.contains(fileName)) { + throwToolExit('Failed to rename $fileName in native assets packaging.'); + } + } + alreadyTakenNames.add(fileName); + return Uri(path: '$fileName.framework/$fileName'); +} diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/native_assets_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/native_assets_test.dart index a17ab8ce88f..24f7d6c7571 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/native_assets_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/native_assets_test.dart @@ -135,7 +135,7 @@ void main() { linkMode: native_assets_cli.LinkMode.dynamic, target: native_assets_cli.Target.iOSArm64, path: native_assets_cli.AssetAbsolutePath( - Uri.file('libfoo.dylib'), + Uri.file('foo.framework/foo'), ), ) ], dependencies: [ @@ -165,7 +165,7 @@ void main() { nativeAssetsYaml.readAsStringSync(), stringContainsInOrder([ 'package:foo/foo.dart', - 'libfoo.dylib', + 'foo.framework', ]), ); }, diff --git a/packages/flutter_tools/test/general.shard/ios/native_assets_test.dart b/packages/flutter_tools/test/general.shard/ios/native_assets_test.dart index eb08c67bf12..1552791142a 100644 --- a/packages/flutter_tools/test/general.shard/ios/native_assets_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/native_assets_test.dart @@ -130,13 +130,13 @@ void main() { id: 'package:bar/bar.dart', linkMode: LinkMode.dynamic, target: native_assets_cli.Target.macOSArm64, - path: AssetAbsolutePath(Uri.file('bar.dylib')), + path: AssetAbsolutePath(Uri.file('libbar.dylib')), ), Asset( id: 'package:bar/bar.dart', linkMode: LinkMode.dynamic, target: native_assets_cli.Target.macOSX64, - path: AssetAbsolutePath(Uri.file('bar.dylib')), + path: AssetAbsolutePath(Uri.file('libbar.dylib')), ), ], ), @@ -219,16 +219,16 @@ void main() { 'lipo', '-create', '-output', - '/build/native_assets/ios/bar.dylib', - 'bar.dylib', + '/build/native_assets/ios/bar.framework/bar', + 'libbar.dylib', ], ), const FakeCommand( command: [ 'install_name_tool', '-id', - '@executable_path/Frameworks/bar.dylib', - '/build/native_assets/ios/bar.dylib', + '@rpath/bar.framework/bar', + '/build/native_assets/ios/bar.framework/bar' ], ), const FakeCommand( @@ -238,7 +238,7 @@ void main() { '--sign', '-', '--timestamp=none', - '/build/native_assets/ios/bar.dylib', + '/build/native_assets/ios/bar.framework', ], ), ], @@ -267,7 +267,7 @@ void main() { id: 'package:bar/bar.dart', linkMode: LinkMode.dynamic, target: native_assets_cli.Target.iOSArm64, - path: AssetAbsolutePath(Uri.file('bar.dylib')), + path: AssetAbsolutePath(Uri.file('libbar.dylib')), ), ], ), diff --git a/packages/flutter_tools/test/general.shard/macos/native_assets_host_test.dart b/packages/flutter_tools/test/general.shard/macos/native_assets_host_test.dart new file mode 100644 index 00000000000..4df6c6362d5 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/macos/native_assets_host_test.dart @@ -0,0 +1,68 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_tools/src/macos/native_assets_host.dart'; + +import '../../src/common.dart'; + +void main() { + test('framework name', () { + expect( + frameworkUri('libfoo.dylib', {}), + equals(Uri.file('foo.framework/foo')), + ); + expect( + frameworkUri('foo', {}), + equals(Uri.file('foo.framework/foo')), + ); + expect( + frameworkUri('foo_foo', {}), + equals(Uri.file('foo_foo.framework/foo_foo')), + ); + expect( + frameworkUri('foo-foo', {}), + equals(Uri.file('foo-foo.framework/foo-foo')), + ); + expect( + frameworkUri(r'foo$foo', {}), + equals(Uri.file('foofoo.framework/foofoo')), + ); + expect( + frameworkUri('foo.foo', {}), + equals(Uri.file('foofoo.framework/foofoo')), + ); + expect( + frameworkUri('libatoolongfilenameforaframework.dylib', {}), + equals(Uri.file('atoolongfilenam.framework/atoolongfilenam')), + ); + }); + + test('framework name name confilicts', () { + final Set alreadyTakenNames = {}; + expect( + frameworkUri('libfoo.dylib', alreadyTakenNames), + equals(Uri.file('foo.framework/foo')), + ); + expect( + frameworkUri('libfoo.dylib', alreadyTakenNames), + equals(Uri.file('foo1.framework/foo1')), + ); + expect( + frameworkUri('libfoo.dylib', alreadyTakenNames), + equals(Uri.file('foo2.framework/foo2')), + ); + expect( + frameworkUri('libatoolongfilenameforaframework.dylib', alreadyTakenNames), + equals(Uri.file('atoolongfilenam.framework/atoolongfilenam')), + ); + expect( + frameworkUri('libatoolongfilenameforaframework.dylib', alreadyTakenNames), + equals(Uri.file('atoolongfile1.framework/atoolongfile1')), + ); + expect( + frameworkUri('libatoolongfilenameforaframework.dylib', alreadyTakenNames), + equals(Uri.file('atoolongfile2.framework/atoolongfile2')), + ); + }); +} diff --git a/packages/flutter_tools/test/general.shard/macos/native_assets_test.dart b/packages/flutter_tools/test/general.shard/macos/native_assets_test.dart index d206680f6f7..770766d8fe4 100644 --- a/packages/flutter_tools/test/general.shard/macos/native_assets_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/native_assets_test.dart @@ -149,13 +149,13 @@ void main() { id: 'package:bar/bar.dart', linkMode: LinkMode.dynamic, target: native_assets_cli.Target.macOSArm64, - path: AssetAbsolutePath(Uri.file('bar.dylib')), + path: AssetAbsolutePath(Uri.file('libbar.dylib')), ), Asset( id: 'package:bar/bar.dart', linkMode: LinkMode.dynamic, target: native_assets_cli.Target.macOSX64, - path: AssetAbsolutePath(Uri.file('bar.dylib')), + path: AssetAbsolutePath(Uri.file('libbar.dylib')), ), ], ), @@ -236,35 +236,47 @@ void main() { if (flutterTester) { testName += ' flutter tester'; } + final String dylibPath; + final String signPath; + if (flutterTester) { + // Just the dylib. + dylibPath = '/build/native_assets/macos/libbar.dylib'; + signPath = '/build/native_assets/macos/libbar.dylib'; + } else { + // Packaged in framework. + dylibPath = '/build/native_assets/macos/bar.framework/Versions/A/bar'; + signPath = '/build/native_assets/macos/bar.framework'; + } testUsingContext('build with assets$testName', overrides: { FeatureFlags: () => TestFeatureFlags(isNativeAssetsEnabled: true), ProcessManager: () => FakeProcessManager.list( [ - const FakeCommand( + FakeCommand( command: [ 'lipo', '-create', '-output', - '/build/native_assets/macos/bar.dylib', - 'bar.dylib', + dylibPath, + 'libbar.dylib', ], ), - const FakeCommand( - command: [ - 'install_name_tool', - '-id', - '@executable_path/Frameworks/bar.dylib', - '/build/native_assets/macos/bar.dylib', - ], - ), - const FakeCommand( + if (!flutterTester) + FakeCommand( + command: [ + 'install_name_tool', + '-id', + '@rpath/bar.framework/bar', + dylibPath, + ], + ), + FakeCommand( command: [ 'codesign', '--force', '--sign', '-', '--timestamp=none', - '/build/native_assets/macos/bar.dylib', + signPath, ], ), ], @@ -292,7 +304,7 @@ void main() { id: 'package:bar/bar.dart', linkMode: LinkMode.dynamic, target: native_assets_cli.Target.macOSArm64, - path: AssetAbsolutePath(Uri.file('bar.dylib')), + path: AssetAbsolutePath(Uri.file('libbar.dylib')), ), ], ), @@ -315,10 +327,10 @@ void main() { 'package:bar/bar.dart', if (flutterTester) // Tests run on host system, so the have the full path on the system. - '- ${projectUri.resolve('build/native_assets/macos/bar.dylib').toFilePath()}' + '- ${projectUri.resolve('build/native_assets/macos/libbar.dylib').toFilePath()}' else // Apps are a bundle with the dylibs on their dlopen path. - '- bar.dylib', + '- bar.framework/bar', ]), ); }); diff --git a/packages/flutter_tools/test/integration.shard/native_assets_test.dart b/packages/flutter_tools/test/integration.shard/native_assets_test.dart index ae577cca1b0..4402a690688 100644 --- a/packages/flutter_tools/test/integration.shard/native_assets_test.dart +++ b/packages/flutter_tools/test/integration.shard/native_assets_test.dart @@ -292,18 +292,48 @@ void main() { void expectDylibIsBundledMacOS(Directory appDirectory, String buildMode) { final Directory appBundle = appDirectory.childDirectory('build/$hostOs/Build/Products/${buildMode.upperCaseFirst()}/$exampleAppName.app'); expect(appBundle, exists); - final Directory dylibsFolder = appBundle.childDirectory('Contents/Frameworks'); - expect(dylibsFolder, exists); - final File dylib = dylibsFolder.childFile(OS.macOS.dylibFileName(packageName)); - expect(dylib, exists); + final Directory frameworksFolder = + appBundle.childDirectory('Contents/Frameworks'); + expect(frameworksFolder, exists); + + // MyFramework.framework/ + // MyFramework -> Versions/Current/MyFramework + // Resources -> Versions/Current/Resources + // Versions/ + // A/ + // MyFramework + // Resources/ + // Info.plist + // Current -> A + final String frameworkName = packageName.substring(0, 15); + final Directory frameworkDir = + frameworksFolder.childDirectory('$frameworkName.framework'); + final Directory versionsDir = frameworkDir.childDirectory('Versions'); + final Directory versionADir = versionsDir.childDirectory('A'); + final Directory resourcesDir = versionADir.childDirectory('Resources'); + expect(resourcesDir, exists); + final File dylibFile = versionADir.childFile(frameworkName); + expect(dylibFile, exists); + final Link currentLink = versionsDir.childLink('Current'); + expect(currentLink, exists); + expect(currentLink.resolveSymbolicLinksSync(), versionADir.path); + final Link resourcesLink = frameworkDir.childLink('Resources'); + expect(resourcesLink, exists); + expect(resourcesLink.resolveSymbolicLinksSync(), resourcesDir.path); + final Link dylibLink = frameworkDir.childLink(frameworkName); + expect(dylibLink, exists); + expect(dylibLink.resolveSymbolicLinksSync(), dylibFile.path); } void expectDylibIsBundledIos(Directory appDirectory, String buildMode) { final Directory appBundle = appDirectory.childDirectory('build/ios/${buildMode.upperCaseFirst()}-iphoneos/Runner.app'); expect(appBundle, exists); - final Directory dylibsFolder = appBundle.childDirectory('Frameworks'); - expect(dylibsFolder, exists); - final File dylib = dylibsFolder.childFile(OS.iOS.dylibFileName(packageName)); + final Directory frameworksFolder = appBundle.childDirectory('Frameworks'); + expect(frameworksFolder, exists); + final String frameworkName = packageName.substring(0, 15); + final File dylib = frameworksFolder + .childDirectory('$frameworkName.framework') + .childFile(frameworkName); expect(dylib, exists); } @@ -379,7 +409,10 @@ void expectDylibIsBundledAndroid(Directory appDirectory, String buildMode) { void expectDylibIsBundledWithFrameworks(Directory appDirectory, String buildMode, String os) { final Directory frameworksFolder = appDirectory.childDirectory('build/$os/framework/${buildMode.upperCaseFirst()}'); expect(frameworksFolder, exists); - final File dylib = frameworksFolder.childFile(OS.macOS.dylibFileName(packageName)); + final String frameworkName = packageName.substring(0, 15); + final File dylib = frameworksFolder + .childDirectory('$frameworkName.framework') + .childFile(frameworkName); expect(dylib, exists); }