[c_compiler] Support setting the install name of dylibs (#55)

diff --git a/pkgs/c_compiler/lib/src/cbuilder/cbuilder.dart b/pkgs/c_compiler/lib/src/cbuilder/cbuilder.dart
index 00006b0..e8ec280 100644
--- a/pkgs/c_compiler/lib/src/cbuilder/cbuilder.dart
+++ b/pkgs/c_compiler/lib/src/cbuilder/cbuilder.dart
@@ -5,6 +5,7 @@
 import 'dart:io';
 
 import 'package:logging/logging.dart';
+import 'package:meta/meta.dart';
 import 'package:native_assets_cli/native_assets_cli.dart';
 
 import 'run_cbuilder.dart';
@@ -51,11 +52,17 @@
   /// Used to output the [BuildOutput.dependencies].
   final List<String> dartBuildFiles;
 
+  /// TODO(https://github.com/dart-lang/native/issues/54): Move to [BuildConfig]
+  /// or hide in public API.
+  @visibleForTesting
+  final Uri? installName;
+
   CBuilder.library({
     required this.name,
     required this.assetName,
     this.sources = const [],
     this.dartBuildFiles = const ['build.dart'],
+    @visibleForTesting this.installName,
   }) : _type = _CBuilderType.library;
 
   CBuilder.executable({
@@ -63,7 +70,8 @@
     this.sources = const [],
     this.dartBuildFiles = const ['build.dart'],
   })  : _type = _CBuilderType.executable,
-        assetName = null;
+        assetName = null,
+        installName = null;
 
   /// Runs the C Compiler with on this C build spec.
   ///
@@ -103,6 +111,7 @@
               ? libUri
               : null,
       executable: _type == _CBuilderType.executable ? exeUri : null,
+      installName: installName,
     );
     await task.run();
 
diff --git a/pkgs/c_compiler/lib/src/cbuilder/run_cbuilder.dart b/pkgs/c_compiler/lib/src/cbuilder/run_cbuilder.dart
index 3fe0725..7c8eb08 100644
--- a/pkgs/c_compiler/lib/src/cbuilder/run_cbuilder.dart
+++ b/pkgs/c_compiler/lib/src/cbuilder/run_cbuilder.dart
@@ -25,6 +25,13 @@
   final Uri outDir;
   final Target target;
 
+  /// The install of the [dynamicLibrary].
+  ///
+  /// Can be inspected with `otool -D <path-to-dylib>`.
+  ///
+  /// Can be modified with `install_name_tool`.
+  final Uri? installName;
+
   RunCBuilder({
     required this.buildConfig,
     this.logger,
@@ -32,6 +39,7 @@
     this.executable,
     this.dynamicLibrary,
     this.staticLibrary,
+    this.installName,
   })  : outDir = buildConfig.outDir,
         target = buildConfig.target,
         assert([executable, dynamicLibrary, staticLibrary]
@@ -110,6 +118,10 @@
           '-isysroot',
           (await macosSdk(logger: logger)).toFilePath(),
         ],
+        if (installName != null) ...[
+          '-install_name',
+          installName!.toFilePath(),
+        ],
         ...sources.map((e) => e.toFilePath()),
         if (executable != null) ...[
           '-o',
diff --git a/pkgs/c_compiler/lib/src/native_toolchain/apple_clang.dart b/pkgs/c_compiler/lib/src/native_toolchain/apple_clang.dart
index 7d90283..9c7eb4c 100644
--- a/pkgs/c_compiler/lib/src/native_toolchain/apple_clang.dart
+++ b/pkgs/c_compiler/lib/src/native_toolchain/apple_clang.dart
@@ -45,3 +45,16 @@
     ),
   ]),
 );
+
+/// The Mach-O dumping tool.
+///
+/// https://llvm.org/docs/CommandGuide/llvm-otool.html
+final Tool otool = Tool(
+  name: 'otool',
+  defaultResolver: CliVersionResolver(
+    wrappedResolver: PathToolResolver(
+      toolName: 'otool',
+      executableName: 'otool',
+    ),
+  ),
+);
diff --git a/pkgs/c_compiler/pubspec.yaml b/pkgs/c_compiler/pubspec.yaml
index abb5769..90d6ea4 100644
--- a/pkgs/c_compiler/pubspec.yaml
+++ b/pkgs/c_compiler/pubspec.yaml
@@ -12,6 +12,7 @@
   cli_config: ^0.1.1
   glob: ^2.1.1
   logging: ^1.1.1
+  meta: ^1.9.1
   # TODO(dacoharkes): Publish native_assets_cli first.
   native_assets_cli:
     path: ../native_assets_cli/
diff --git a/pkgs/c_compiler/test/cbuilder/cbuilder_cross_ios_test.dart b/pkgs/c_compiler/test/cbuilder/cbuilder_cross_ios_test.dart
index 415740e..2c05b9f 100644
--- a/pkgs/c_compiler/test/cbuilder/cbuilder_cross_ios_test.dart
+++ b/pkgs/c_compiler/test/cbuilder/cbuilder_cross_ios_test.dart
@@ -34,54 +34,81 @@
     Target.iOSX64: '64-bit x86-64',
   };
 
+  const name = 'add';
+
   for (final linkMode in LinkMode.values) {
     for (final targetIOSSdk in IOSSdk.values) {
       for (final target in targets) {
         if (target == Target.iOSX64 && targetIOSSdk == IOSSdk.iPhoneOs) {
           continue;
         }
-        test('Cbuilder $linkMode library $targetIOSSdk $target', () async {
-          await inTempDir((tempUri) async {
-            final addCUri =
-                packageUri.resolve('test/cbuilder/testfiles/add/src/add.c');
-            const name = 'add';
 
-            final buildConfig = BuildConfig(
-              outDir: tempUri,
-              packageRoot: tempUri,
-              target: target,
-              linkModePreference: linkMode == LinkMode.dynamic
-                  ? LinkModePreference.dynamic
-                  : LinkModePreference.static,
-              targetIOSSdk: targetIOSSdk,
-            );
-            final buildOutput = BuildOutput();
+        final libName = target.os.libraryFileName(name, linkMode);
+        for (final installName in [
+          null,
+          if (linkMode == LinkMode.dynamic)
+            Uri.file('@executable_path/Frameworks/$libName'),
+        ]) {
+          test(
+              'Cbuilder $linkMode library $targetIOSSdk $target'
+                      ' ${installName ?? ''}'
+                  .trim(), () async {
+            await inTempDir((tempUri) async {
+              final addCUri =
+                  packageUri.resolve('test/cbuilder/testfiles/add/src/add.c');
+              final buildConfig = BuildConfig(
+                outDir: tempUri,
+                packageRoot: tempUri,
+                target: target,
+                linkModePreference: linkMode == LinkMode.dynamic
+                    ? LinkModePreference.dynamic
+                    : LinkModePreference.static,
+                targetIOSSdk: targetIOSSdk,
+              );
+              final buildOutput = BuildOutput();
 
-            final cbuilder = CBuilder.library(
-              name: name,
-              assetName: name,
-              sources: [addCUri.toFilePath()],
-            );
-            await cbuilder.run(
-              buildConfig: buildConfig,
-              buildOutput: buildOutput,
-              logger: logger,
-            );
+              final cbuilder = CBuilder.library(
+                name: name,
+                assetName: name,
+                sources: [addCUri.toFilePath()],
+                installName: installName,
+              );
+              await cbuilder.run(
+                buildConfig: buildConfig,
+                buildOutput: buildOutput,
+                logger: logger,
+              );
 
-            final libUri =
-                tempUri.resolve(target.os.libraryFileName(name, linkMode));
-            final result = await runProcess(
-              executable: Uri.file('objdump'),
-              arguments: ['-t', libUri.path],
-              logger: logger,
-            );
-            expect(result.exitCode, 0);
-            final machine = result.stdout
-                .split('\n')
-                .firstWhere((e) => e.contains('file format'));
-            expect(machine, contains(objdumpFileFormat[target]));
+              final libUri = tempUri.resolve(libName);
+              final objdumpResult = await runProcess(
+                executable: Uri.file('objdump'),
+                arguments: ['-t', libUri.path],
+                logger: logger,
+              );
+              expect(objdumpResult.exitCode, 0);
+              final machine = objdumpResult.stdout
+                  .split('\n')
+                  .firstWhere((e) => e.contains('file format'));
+              expect(machine, contains(objdumpFileFormat[target]));
+
+              if (linkMode == LinkMode.dynamic) {
+                final libInstallName =
+                    await runOtoolInstallName(libUri, libName);
+                if (installName == null) {
+                  // If no install path is passed, we have an absolute path.
+                  final tempName =
+                      tempUri.pathSegments.lastWhere((e) => e != '');
+                  final pathEnding =
+                      Uri.directory(tempName).resolve(libName).toFilePath();
+                  expect(Uri.file(libInstallName).isAbsolute, true);
+                  expect(libInstallName, contains(pathEnding));
+                } else {
+                  expect(libInstallName, installName.toFilePath());
+                }
+              }
+            });
           });
-        });
+        }
       }
     }
   }
diff --git a/pkgs/c_compiler/test/helpers.dart b/pkgs/c_compiler/test/helpers.dart
index 082e102..4c6be25 100644
--- a/pkgs/c_compiler/test/helpers.dart
+++ b/pkgs/c_compiler/test/helpers.dart
@@ -5,6 +5,8 @@
 import 'dart:async';
 import 'dart:io';
 
+import 'package:c_compiler/src/native_toolchain/apple_clang.dart';
+import 'package:c_compiler/src/utils/run_process.dart';
 import 'package:logging/logging.dart';
 import 'package:native_assets_cli/native_assets_cli.dart';
 import 'package:test/test.dart';
@@ -120,3 +122,25 @@
 extension on String {
   Uri asFileUri() => Uri.file(this);
 }
+
+/// Looks up the install name of a dynamic library at [libraryUri].
+///
+/// Because `otool` output multiple names, [libraryName] as search parameter.
+Future<String> runOtoolInstallName(Uri libraryUri, String libraryName) async {
+  final otoolUri =
+      (await otool.defaultResolver!.resolve(logger: logger)).first.uri;
+  final otoolResult = await runProcess(
+    executable: otoolUri,
+    arguments: ['-l', libraryUri.path],
+    logger: logger,
+  );
+  expect(otoolResult.exitCode, 0);
+  // Leading space on purpose to differentiate from other types of names.
+  const installNameName = ' name ';
+  final installName = otoolResult.stdout
+      .split('\n')
+      .firstWhere((e) => e.contains(installNameName) && e.contains(libraryName))
+      .trim()
+      .split(' ')[1];
+  return installName;
+}
diff --git a/pkgs/c_compiler/test/native_toolchain/apple_clang_test.dart b/pkgs/c_compiler/test/native_toolchain/apple_clang_test.dart
index 998922c..77a7061 100644
--- a/pkgs/c_compiler/test/native_toolchain/apple_clang_test.dart
+++ b/pkgs/c_compiler/test/native_toolchain/apple_clang_test.dart
@@ -47,4 +47,12 @@
     final satisfied = requirement.satisfy(resolved);
     expect(satisfied?.length, 1);
   });
+
+  test('otool test', () async {
+    final requirement = ToolRequirement(otool);
+    final resolved = await otool.defaultResolver!.resolve(logger: logger);
+    expect(resolved.isNotEmpty, true);
+    final satisfied = requirement.satisfy(resolved);
+    expect(satisfied?.length, 1);
+  });
 }