| // 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}', |
| ); |
| } |
| } |