blob: 62765bc6ad59705cf1ad9567360fb64305d759e8 [file] [log] [blame] [edit]
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. 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:io';
import 'package:code_assets/code_assets.dart';
import 'package:dartdev/src/native_assets_bundling.dart';
final _rpathUri = Uri.file('@rpath/');
Future<void> rewriteInstallNames(
List<Uri> dylibs, {
required bool relocatable,
}) async {
final oldToNewInstallNames = <String, String>{};
final dylibInfos = <(Uri, String)>[];
await Future.wait(dylibs.map((dylib) async {
final newInstallName = relocatable
? _rpathUri
.resolveUri(libOutputDirectoryUri)
.resolve(dylib.pathSegments.last)
.toFilePath()
: dylib.toFilePath();
final oldInstallName = await _getInstallName(dylib);
oldToNewInstallNames[oldInstallName] = newInstallName;
dylibInfos.add((dylib, newInstallName));
}));
await Future.wait(dylibInfos.map((info) async {
final (dylib, newInstallName) = info;
await _setInstallNames(dylib, newInstallName, oldToNewInstallNames);
await _codeSignDylib(dylib);
}));
}
Future<String> _getInstallName(Uri dylib) async {
final otoolResult = await Process.run(
'otool',
[
'-D',
dylib.toFilePath(),
],
);
if (otoolResult.exitCode != 0) {
throw Exception(
'Failed to get install name for dylib $dylib: ${otoolResult.stderr}',
);
}
final architectureSections =
parseOtoolArchitectureSections(otoolResult.stdout);
if (architectureSections.length != 1) {
throw Exception(
'Expected a single architecture section in otool output: $otoolResult',
);
}
return architectureSections.values.first.single;
}
Future<void> _setInstallNames(
Uri dylib,
String newInstallName,
Map<String, String> oldToNewInstallNames,
) async {
final installNameToolResult = await Process.run(
'install_name_tool',
[
'-id',
newInstallName,
for (final entry in oldToNewInstallNames.entries) ...[
'-change',
entry.key,
entry.value,
],
dylib.toFilePath(),
],
);
if (installNameToolResult.exitCode != 0) {
throw Exception(
'Failed to set install names for dylib $dylib:\n'
'id -> $newInstallName\n'
'dependencies -> $oldToNewInstallNames\n'
'${installNameToolResult.stderr}',
);
}
}
Future<void> _codeSignDylib(Uri dylib) async {
final codesignResult = await Process.run(
'codesign',
[
'--force',
'--sign',
'-',
dylib.toFilePath(),
],
);
if (codesignResult.exitCode != 0) {
throw Exception(
'Failed to codesign dylib $dylib: ${codesignResult.stderr}',
);
}
}
Map<Architecture?, List<String>> 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<String, Architecture> outputArchitectures = <String, Architecture>{
'arm': Architecture.arm,
'arm64': Architecture.arm64,
'x86_64': Architecture.x64,
};
final RegExp architectureHeaderPattern =
RegExp(r'^[^(]+( \(architecture (.+)\))?:$');
final Iterator<String> lines = output.trim().split('\n').iterator;
Architecture? currentArchitecture;
final Map<Architecture?, List<String>> architectureSections =
<Architecture?, List<String>>{};
while (lines.moveNext()) {
final String line = lines.current;
final Match? architectureHeader =
architectureHeaderPattern.firstMatch(line);
if (architectureHeader != null) {
if (architectureSections.containsKey(null)) {
throw Exception(
'Expected a single architecture section in otool output: $output',
);
}
final String? architectureString = architectureHeader.group(2);
if (architectureString != null) {
currentArchitecture = outputArchitectures[architectureString];
if (currentArchitecture == null) {
throw Exception(
'Unknown architecture in otool output: $architectureString',
);
}
}
architectureSections[currentArchitecture] = <String>[];
continue;
} else {
architectureSections[currentArchitecture]!.add(line.trim());
}
}
return architectureSections;
}
/// Rewrites the include path for an executable on MacOS.
///
/// The directory structure for `dart build cli` is
///
/// ```
/// bundle/
/// bin/
/// <executable>
/// lib/
/// <dylibs>
/// ```
///
/// So, the `bundle/` directory must be added to the include path to allow for
/// loading dylibs.
Future<void> rewriteInstallPath(Uri executable) async {
final result = await Process.run(
'install_name_tool',
['-add_rpath', '@executable_path/..', executable.toFilePath()],
);
if (result.exitCode != 0) {
throw Exception(
'Failed to add rpath: ${result.stderr}',
);
}
// Resign after modifying.
final codesignResult = await Process.run(
'codesign',
[
'--force',
'--sign',
'-',
executable.toFilePath(),
],
);
if (codesignResult.exitCode != 0) {
throw Exception(
'Failed to codesign dylib $executable: ${codesignResult.stderr}',
);
}
}