// 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. // Shared logic between iOS and macOS implementations of native assets. import 'package:native_assets_cli/code_assets_builder.dart'; import '../../../base/common.dart'; import '../../../base/file_system.dart'; import '../../../base/io.dart'; import '../../../build_info.dart'; import '../../../convert.dart'; import '../../../globals.dart' as globals; /// Create an `Info.plist` in [target] for a framework with a single dylib. /// /// The framework must be named [name].framework and the dylib [name]. Future createInfoPlist(String name, Directory target, {String? minimumIOSVersion}) async { final File infoPlistFile = target.childFile('Info.plist'); final String bundleIdentifier = 'io.flutter.flutter.native_assets.$name'.replaceAll('_', '-'); await infoPlistFile.writeAsString( [ ''' CFBundleDevelopmentRegion en CFBundleExecutable $name CFBundleIdentifier $bundleIdentifier CFBundleInfoDictionaryVersion 6.0 CFBundleName $name CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 ''', if (minimumIOSVersion != null) ''' MinimumOSVersion $minimumIOSVersion ''', ''' ''', ].join(), ); } /// Combines dylibs from [sources] into a fat binary at [targetFullPath]. /// /// 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(File target, List sources) async { final ProcessResult lipoResult = await globals.processManager.run([ 'lipo', '-create', '-output', target.path, for (final File source in sources) source.path, ]); if (lipoResult.exitCode != 0) { throwToolExit('Failed to create universal binary:\n${lipoResult.stderr}'); } globals.logger.printTrace(lipoResult.stdout as String); globals.logger.printTrace(lipoResult.stderr as String); } /// Sets the install names in a dylib with a Mach-O format. /// /// On macOS and iOS, opening a dylib at runtime fails if the path inside the /// 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`. /// /// [oldToNewInstallNames] is a map from old to new install names, that should /// be applied to the dependencies of the dylib. Entries in this map for /// dependencies that are not present in the dylib are ignored. The install /// name of a dependencies needs to be updated if the location of the dependency /// has changed. Future setInstallNamesDylib( File dylibFile, String newInstallName, Map oldToNewInstallNames, ) async { final ProcessResult setInstallNamesResult = await globals.processManager.run([ 'install_name_tool', '-id', newInstallName, for (final MapEntry entry in oldToNewInstallNames.entries) ...[ '-change', entry.key, entry.value, ], dylibFile.path, ]); if (setInstallNamesResult.exitCode != 0) { throwToolExit( 'Failed to change install names in $dylibFile:\n' 'id -> $newInstallName\n' 'dependencies -> $newInstallName\n' '${setInstallNamesResult.stderr}', ); } } Future> getInstallNamesDylib(File dylibFile) async { final ProcessResult installNameResult = await globals.processManager.run([ 'otool', '-D', dylibFile.path, ]); if (installNameResult.exitCode != 0) { throwToolExit('Failed to get the install name of $dylibFile:\n${installNameResult.stderr}'); } return { for (final List architectureSection in parseOtoolArchitectureSections(installNameResult.stdout as String).values) // For each architecture, a separate install name is reported, which are // not necessarily the same. architectureSection.single, }; } Future codesignDylib( String? codesignIdentity, BuildMode buildMode, FileSystemEntity target, ) async { if (codesignIdentity == null || codesignIdentity.isEmpty) { codesignIdentity = '-'; } final List codesignCommand = [ 'codesign', '--force', '--sign', codesignIdentity, if (buildMode != BuildMode.release) ...[ // Mimic Xcode's timestamp codesigning behavior on non-release binaries. '--timestamp=none', ], target.path, ]; globals.logger.printTrace(codesignCommand.join(' ')); final ProcessResult codesignResult = await globals.processManager.run(codesignCommand); if (codesignResult.exitCode != 0) { 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); } /// Flutter expects `xcrun` to be on the path on macOS hosts. /// /// 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', ]); 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; return CCompilerConfig( compiler: Uri.file('$installPath/clang'), archiver: Uri.file('$installPath/ar'), linker: 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 /// /// 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 (alreadyTakenNames.contains(fileName)) { 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'); } Map> parseOtoolArchitectureSections(String output) { // The output of `otool -D`, for example, looks like below. For each // architecture, there is a separate section. // // /build/native_assets/ios/buz.framework/buz (architecture x86_64): // @rpath/libbuz.dylib // /build/native_assets/ios/buz.framework/buz (architecture arm64): // @rpath/libbuz.dylib // // Some versions of `otool` don't print the architecture name if the // binary only has one architecture: // // /build/native_assets/ios/buz.framework/buz: // @rpath/libbuz.dylib const Map outputArchitectures = { 'arm': Architecture.arm, 'arm64': Architecture.arm64, 'x86_64': Architecture.x64, }; final RegExp architectureHeaderPattern = RegExp(r'^[^(]+( \(architecture (.+)\))?:$'); final Iterator lines = output.trim().split('\n').iterator; Architecture? currentArchitecture; final Map> architectureSections = >{}; while (lines.moveNext()) { final String line = lines.current; final Match? architectureHeader = architectureHeaderPattern.firstMatch(line); if (architectureHeader != null) { if (architectureSections.containsKey(null)) { throwToolExit('Expected a single architecture section in otool output: $output'); } final String? architectureString = architectureHeader.group(2); if (architectureString != null) { currentArchitecture = outputArchitectures[architectureString]; if (currentArchitecture == null) { throwToolExit('Unknown architecture in otool output: $architectureString'); } } architectureSections[currentArchitecture] = []; continue; } else { architectureSections[currentArchitecture]!.add(line.trim()); } } return architectureSections; }