blob: dcc3a76508a562960a35e728d5072bf3647ea362 [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.
// Shared logic between iOS and macOS implementations of native assets.
import 'package:native_assets_cli/native_assets_cli_internal.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<void> 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(<String>[
'''
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$name</string>
<key>CFBundleIdentifier</key>
<string>$bundleIdentifier</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$name</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
''',
if (minimumIOSVersion != null)
'''
<key>MinimumOSVersion</key>
<string>$minimumIOSVersion</string>
''',
'''
</dict>
</plist>'''
].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<void> lipoDylibs(File target, List<Uri> sources) async {
final ProcessResult lipoResult = await globals.processManager.run(
<String>[
'lipo',
'-create',
'-output',
target.path,
for (final Uri source in sources) source.toFilePath(),
],
);
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 name 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`.
Future<void> setInstallNameDylib(File dylibFile) async {
final String fileName = dylibFile.basename;
final ProcessResult installNameResult = await globals.processManager.run(
<String>[
'install_name_tool',
'-id',
'@rpath/$fileName.framework/$fileName',
dylibFile.path,
],
);
if (installNameResult.exitCode != 0) {
throwToolExit(
'Failed to change the install name of $dylibFile:\n${installNameResult.stderr}',
);
}
}
Future<void> codesignDylib(
String? codesignIdentity,
BuildMode buildMode,
FileSystemEntity target,
) async {
if (codesignIdentity == null || codesignIdentity.isEmpty) {
codesignIdentity = '-';
}
final List<String> codesignCommand = <String>[
'codesign',
'--force',
'--sign',
codesignIdentity,
if (buildMode != BuildMode.release) ...<String>[
// 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<CCompilerConfigImpl> cCompilerConfigMacOS() async {
final ProcessResult xcrunResult = await globals.processManager.run(
<String>['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 CCompilerConfigImpl(
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<String> alreadyTakenNames) {
final List<String> 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');
}