From c1b70d3afc2e21bfa9011108fa51e2bb90146eea Mon Sep 17 00:00:00 2001 From: Ben Lovy Date: Mon, 20 Jan 2025 13:24:25 -0500 Subject: [PATCH] refactor(std/wrap): rollup (#143) --- packages/rust/proxy/Cargo.lock | 40 +- packages/std/Cargo.lock | 42 +- packages/std/packages/ld_proxy/src/main.rs | 62 ++- packages/std/packages/wrapper/src/main.rs | 1 + packages/std/tangram.ts | 1 + packages/std/wrap.tg.ts | 530 ++++++++++++--------- 6 files changed, 372 insertions(+), 304 deletions(-) diff --git a/packages/rust/proxy/Cargo.lock b/packages/rust/proxy/Cargo.lock index 82ea0066..ce055a1e 100644 --- a/packages/rust/proxy/Cargo.lock +++ b/packages/rust/proxy/Cargo.lock @@ -105,9 +105,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "bitvec" @@ -222,9 +222,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.9" +version = "1.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" +checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" dependencies = [ "shlex", ] @@ -351,15 +351,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" [[package]] name = "data-encoding-macro" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1559b6cba622276d6d63706db152618eeb15b89b3e4041446b05876e352e639" +checksum = "5b16d9d0d88a5273d830dac8b78ceb217ffc9b1d5404e5597a3542515329405b" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -367,12 +367,12 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332d754c0af53bc87c108fed664d121ecf59207ec4196041f04d6ab9002ad33f" +checksum = "1145d32e826a7748b69ee8fc62d3e6355ff7f1051df53141e7048162fc90481b" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.96", ] [[package]] @@ -975,9 +975,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "lsp-types" @@ -1006,9 +1006,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" dependencies = [ "adler2", ] @@ -1299,7 +1299,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.8.0", ] [[package]] @@ -2009,18 +2009,18 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4" +checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" dependencies = [ "getrandom", ] [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "version_check" diff --git a/packages/std/Cargo.lock b/packages/std/Cargo.lock index 695cbb22..b5466f33 100644 --- a/packages/std/Cargo.lock +++ b/packages/std/Cargo.lock @@ -105,9 +105,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "bitvec" @@ -222,9 +222,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.9" +version = "1.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" +checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" dependencies = [ "shlex", ] @@ -351,15 +351,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" [[package]] name = "data-encoding-macro" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1559b6cba622276d6d63706db152618eeb15b89b3e4041446b05876e352e639" +checksum = "5b16d9d0d88a5273d830dac8b78ceb217ffc9b1d5404e5597a3542515329405b" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -367,12 +367,12 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332d754c0af53bc87c108fed664d121ecf59207ec4196041f04d6ab9002ad33f" +checksum = "1145d32e826a7748b69ee8fc62d3e6355ff7f1051df53141e7048162fc90481b" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.96", ] [[package]] @@ -1008,9 +1008,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "lsp-types" @@ -1039,9 +1039,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" dependencies = [ "adler2", ] @@ -1338,7 +1338,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.8.0", ] [[package]] @@ -1436,7 +1436,7 @@ version = "0.38.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.8.0", "errno", "libc", "linux-raw-sys", @@ -2163,18 +2163,18 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4" +checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" dependencies = [ "getrandom", ] [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "version_check" diff --git a/packages/std/packages/ld_proxy/src/main.rs b/packages/std/packages/ld_proxy/src/main.rs index baecc839..e7d2871f 100644 --- a/packages/std/packages/ld_proxy/src/main.rs +++ b/packages/std/packages/ld_proxy/src/main.rs @@ -94,8 +94,8 @@ struct Options { /// The path to the injection library. injection_path: Option, - /// Library path optimization level. Select `none`, `filter`, `resolve`, `isolate`, or `combine`. Defaults to `isolate`. - library_path_optimization: LibraryPathOptimizationLevel, + /// Library path optimization strategy. Select `none`, `filter`, `resolve`, `isolate`, or `combine`. Defaults to `isolate`. + library_path_strategy: LibraryPathStrategy, /// The library paths. library_paths: Vec, @@ -162,7 +162,7 @@ fn read_options() -> tg::Result { // Get the option to disable combining library paths. Enabled by default. let mut library_path_optimization = std::env::var("TANGRAM_LINKER_LIBRARY_PATH_OPT_LEVEL") .ok() - .map_or(LibraryPathOptimizationLevel::default(), |s| { + .map_or(LibraryPathStrategy::default(), |s| { s.parse().unwrap_or_default() }); @@ -183,7 +183,7 @@ fn read_options() -> tg::Result { if arg.starts_with("--tg-library-path-opt-level=") { let option = arg.strip_prefix("--tg-library-path-opt-level=").unwrap(); library_path_optimization = - LibraryPathOptimizationLevel::from_str(option).unwrap_or_default(); + LibraryPathStrategy::from_str(option).unwrap_or_default(); } else if arg.starts_with("--tg-max-depth=") { let option = arg.strip_prefix("--tg-max-depth=").unwrap(); if let Ok(max_depth_arg) = option.parse() { @@ -253,7 +253,7 @@ fn read_options() -> tg::Result { interpreter_path, interpreter_args, injection_path, - library_path_optimization, + library_path_strategy: library_path_optimization, library_paths, max_depth, output_path, @@ -417,7 +417,7 @@ async fn create_wrapper(options: &Options) -> tg::Result<()> { let library_paths: HashSet, Hasher> = library_paths.into_iter().collect(); - let strategy = options.library_path_optimization; + let strategy = options.library_path_strategy; tracing::trace!( ?library_paths, ?needed_libraries, @@ -430,7 +430,7 @@ async fn create_wrapper(options: &Options) -> tg::Result<()> { &output_file, library_paths, &mut needed_libraries, - options.library_path_optimization, + options.library_path_strategy, options.max_depth, options.disallow_missing, ) @@ -781,39 +781,31 @@ enum InterpreterRequirement { } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -enum LibraryPathOptimizationLevel { - /// Do not optimize library paths. - None = 0, +enum LibraryPathStrategy { + /// Do not manipulate library paths. + None, /// Only retain paths containing needed libraries. - Filter = 1, + Filter, /// Resolve any artifacts with subpaths to their innermost directory. - Resolve = 2, + Resolve, /// Create individual library paths for each library. #[default] - Isolate = 3, + Isolate, /// Combine library paths into a single directory. - Combine = 4, + Combine, } -impl std::str::FromStr for LibraryPathOptimizationLevel { +impl std::str::FromStr for LibraryPathStrategy { type Err = tg::Error; fn from_str(s: &str) -> Result { match s.to_ascii_lowercase().as_str() { - "none" | "0" => Ok(Self::None), - "filter" | "1" => Ok(Self::Filter), - "resolve" | "2" => Ok(Self::Resolve), - "isolate" | "3" => Ok(Self::Isolate), - "combine" | "4" => Ok(Self::Combine), - _ => { - // If the string is a digit greater than 4, fall back to 4. - if let Ok(level) = s.parse::() { - if level > 4 { - return Ok(Self::Combine); - } - } - Err(tg::error!("invalid library path optimization strategy {s}")) - }, + "none" => Ok(Self::None), + "filter" => Ok(Self::Filter), + "resolve" => Ok(Self::Resolve), + "isolate" => Ok(Self::Isolate), + "combine" => Ok(Self::Combine), + _ => Err(tg::error!("invalid library path optimization strategy {s}")), } } } @@ -925,11 +917,11 @@ async fn optimize_library_paths( file: &tg::File, library_paths: HashSet, H>, needed_libraries: &mut HashMap>, H>, - strategy: LibraryPathOptimizationLevel, + strategy: LibraryPathStrategy, max_depth: usize, disallow_missing: bool, ) -> tg::Result, H>> { - if matches!(strategy, LibraryPathOptimizationLevel::None) || library_paths.is_empty() { + if matches!(strategy, LibraryPathStrategy::None) || library_paths.is_empty() { return Ok(library_paths); } @@ -941,7 +933,7 @@ async fn optimize_library_paths( let filtered_library_paths = needed_libraries.values().flatten().cloned().collect(); tracing::debug!(?filtered_library_paths, "post-filter"); - if matches!(strategy, LibraryPathOptimizationLevel::Filter) { + if matches!(strategy, LibraryPathStrategy::Filter) { return finalize_library_paths( tg, disallow_missing, @@ -952,7 +944,7 @@ async fn optimize_library_paths( } match strategy { - LibraryPathOptimizationLevel::Resolve => { + LibraryPathStrategy::Resolve => { let resolved_library_paths: HashSet, H> = resolve_directories(tg, &filtered_library_paths).await?; tracing::trace!(?resolved_library_paths, "post-resolve"); @@ -964,7 +956,7 @@ async fn optimize_library_paths( ) .await; }, - LibraryPathOptimizationLevel::Isolate => { + LibraryPathStrategy::Isolate => { // Create an individual library path for every found library. let mut isolated_library_paths: HashSet, H> = HashSet::default(); @@ -990,7 +982,7 @@ async fn optimize_library_paths( ) .await; }, - LibraryPathOptimizationLevel::Combine => { + LibraryPathStrategy::Combine => { // Create a directory combining all located library files. let mut entries = BTreeMap::new(); for (name, dir_id_referent) in needed_libraries.iter() { diff --git a/packages/std/packages/wrapper/src/main.rs b/packages/std/packages/wrapper/src/main.rs index 1158c18e..c19d7f66 100644 --- a/packages/std/packages/wrapper/src/main.rs +++ b/packages/std/packages/wrapper/src/main.rs @@ -17,6 +17,7 @@ fn main_inner() -> std::io::Result<()> { // Get the wrapper path. let wrapper_path = std::env::current_exe()?.canonicalize()?; + #[cfg(feature = "tracing")] tracing::trace!(?wrapper_path); // Read the manifest. diff --git a/packages/std/tangram.ts b/packages/std/tangram.ts index 5e5190a6..bfe9472b 100644 --- a/packages/std/tangram.ts +++ b/packages/std/tangram.ts @@ -76,6 +76,7 @@ const testActions = (): Record Promise> => { wrapBasic: wrap.testSingleArgObjectNoMutations, wrapContent: wrap.testContentExecutable, wrapContentVariadic: wrap.testContentExecutableVariadic, + wrapDylib: wrap.testDylibPath, wrap: wrap.test, env: env.test, proxyBasic: sdk.proxy.testBasic, diff --git a/packages/std/wrap.tg.ts b/packages/std/wrap.tg.ts index 99bc88a0..15034f96 100644 --- a/packages/std/wrap.tg.ts +++ b/packages/std/wrap.tg.ts @@ -33,12 +33,13 @@ export async function wrap( ? await gnu.toolchain({ host: detectedBuild, target: host }) : await bootstrap.sdk.env(host); - const manifestInterpreter = arg.interpreter - ? await manifestInterpreterFromArg(arg.interpreter, buildToolchain) - : await manifestInterpreterFromExecutableArg( - arg.executable, - buildToolchain, - ); + // Construct the interpreter. + const manifestInterpreter = await manifestInterpreterFromWrapArgObject({ + buildToolchain, + interpreter: arg.interpreter, + executable: arg.executable, + libraryPaths: arg.libraryPaths, + }); // Ensure we're not building an identity=executable wrapper for an unwrapped statically-linked executable. if ( @@ -63,17 +64,6 @@ export async function wrap( } } - // Add remaining library paths. - if (manifestInterpreter && "libraryPaths" in manifestInterpreter) { - let paths = manifestInterpreter.libraryPaths ?? []; - if (arg.libraryPaths) { - paths = paths.concat( - await Promise.all(arg.libraryPaths.map(manifestTemplateFromArg)), - ); - } - manifestInterpreter.libraryPaths = paths; - } - const manifestEnv = await wrap.manifestEnvFromEnvObject( arg.env as std.env.EnvObject, ); @@ -149,6 +139,8 @@ export namespace wrap { | DyLdInterpreter; export type NormalInterpreter = { + kind: "normal"; + /** The interpreter executable. */ executable: tg.File | tg.Symlink; @@ -405,9 +397,11 @@ export namespace wrap { if (manifestInterpreter === undefined) { return undefined; } - switch (manifestInterpreter.kind) { + const kind = manifestInterpreter.kind; + switch (kind) { case "normal": { return { + kind, executable: await fileOrSymlinkFromManifestTemplate( manifestInterpreter.path, ), @@ -421,7 +415,7 @@ export namespace wrap { } case "ld-linux": { return { - kind: "ld-linux", + kind, executable: await fileOrSymlinkFromManifestTemplate( manifestInterpreter.path, ), @@ -451,7 +445,7 @@ export namespace wrap { } case "ld-musl": { return { - kind: "ld-musl", + kind, executable: await fileOrSymlinkFromManifestTemplate( manifestInterpreter.path, ), @@ -481,7 +475,7 @@ export namespace wrap { } case "dyld": { return { - kind: "dyld", + kind, libraryPaths: manifestInterpreter.libraryPaths === undefined ? undefined @@ -554,6 +548,50 @@ export namespace wrap { return { kind: "set", value }; }; + /** Attempt to obtain the needed libraries of the wrapped exectuable of a wrapper. */ + export const tryNeededLibraries = async ( + file: tg.File, + ): Promise | undefined> => { + try { + return await neededLibraries(file); + } catch (_) { + return undefined; + } + }; + + /** Obtain the needed libraries of the wrapped executable of a wrapper. */ + export const neededLibraries = async ( + file: tg.File, + ): Promise> => { + const manifest = await wrap.Manifest.read(file); + if (!manifest) { + throw new Error( + `Cannot determine needed libraries for ${await file.id()}: not a Tangram wrapper.`, + ); + } + tg.assert( + manifest.interpreter !== undefined, + `cannot determine needed libraries for a wrapper without an interpreter`, + ); + tg.assert( + manifest.interpreter.kind !== "normal", + `cannot determine needed libraries for a normal interpreter`, + ); + const wrappedExecutable = manifest.executable; + tg.assert( + wrappedExecutable.kind !== "content", + "cannot determine needed libraries for a content executable", + ); + const wrappedExecutableFile = await fileOrSymlinkFromManifestTemplate( + manifest.executable.value, + ); + tg.assert( + wrappedExecutableFile instanceof tg.File, + `executable must be a file, received ${await wrappedExecutableFile.id()}`, + ); + return await getNeededLibraries(wrappedExecutableFile); + }; + /** Attempt to unwrap a wrapped executable. Returns undefined if the input was not a Tangram wrapper. */ export const tryUnwrap = async ( file: tg.File, @@ -840,233 +878,250 @@ const isManifestExecutable = ( ); }; -const manifestInterpreterFromArg = async ( - arg: +/** The subset of `wrap.ArgObject` relevant to producing a `wrap.Manifest.Interpreter`. */ +type ManifestInterpreterArg = { + buildToolchain?: std.env.Arg; + interpreter?: | tg.File | tg.Symlink | tg.Template | wrap.Interpreter - | wrap.Manifest.Interpreter, - buildToolchainArg?: std.env.Arg, + | undefined; + executable: string | tg.Template | tg.File | tg.Symlink; + libraryPaths?: Array | undefined; +}; + +/** Produce the manifest interpreter object given a set of parameters. */ +const manifestInterpreterFromWrapArgObject = async ( + arg: ManifestInterpreterArg, +): Promise => { + let interpreter = arg.interpreter + ? await interpreterFromArg(arg.interpreter, arg.buildToolchain) + : await interpreterFromExecutableArg(arg.executable, arg.buildToolchain); + if (interpreter === undefined) { + return undefined; + } + + return manifestInterpreterFromWrapInterpreter(interpreter); +}; + +/** Serialize an interpreter into its manifest form. */ +const manifestInterpreterFromWrapInterpreter = async ( + interpreter: wrap.Interpreter, ): Promise => { - if (isManifestInterpreter(arg)) { - return arg; + // Process each field present in the incoming object. + const { kind } = interpreter; + + // Process all fields concurrently + const [path, libraryPaths, preloads, args] = await Promise.all([ + // Only process executable if it exists + "executable" in interpreter + ? manifestTemplateFromArg(interpreter.executable) + : Promise.resolve(undefined), + + // Only process libraryPaths if it exists + "libraryPaths" in interpreter && interpreter.libraryPaths !== undefined + ? Promise.all(interpreter.libraryPaths.map(manifestTemplateFromArg)) + : Promise.resolve(undefined), + + // Only process preloads if it exists + "preloads" in interpreter && interpreter.preloads !== undefined + ? Promise.all(interpreter.preloads.map(manifestTemplateFromArg)) + : Promise.resolve(undefined), + + // Only process args if it exists + "args" in interpreter && interpreter.args !== undefined + ? Promise.all(interpreter.args.map(manifestTemplateFromArg)) + : Promise.resolve(undefined), + ]); + + // COnstruct a `manifest.Interpreter` using only fields that are not `undefined`. + switch (kind) { + case "normal": { + return { + kind, + path: path!, + ...(args && { args }), + }; + } + case "ld-linux": + case "ld-musl": { + return { + kind, + path: path!, + ...(libraryPaths && { libraryPaths }), + ...(preloads && { preloads }), + ...(args && { args }), + }; + } + case "dyld": { + return { + kind, + ...(libraryPaths && { libraryPaths }), + ...(preloads && { preloads }), + }; + } + default: { + return tg.unreachable(`unrecognized kind ${kind}`); + } } +}; +/** Given an interpreter arg, produce an interpreter object with all fields populated. */ +const interpreterFromArg = async ( + arg: tg.File | tg.Symlink | tg.Template | wrap.Interpreter, + buildToolchainArg?: std.env.Arg, +): Promise => { // If the arg is an executable, then wrap it and create a normal interpreter. if ( arg instanceof tg.File || arg instanceof tg.Symlink || arg instanceof tg.Template ) { - const interpreter = await std.wrap({ + const executable = await std.wrap({ buildToolchain: buildToolchainArg, executable: arg, }); - const path = await manifestTemplateFromArg(interpreter); return { kind: "normal", - path, + executable, args: [], }; } - // Otherwise, create the interpreter specified by the arg object. - if ("kind" in arg && arg.kind === "ld-linux") { - // Handle an ld-linux interpreter. - const path = await manifestTemplateFromArg(arg.executable); - const libraryPaths = arg.libraryPaths - ? await Promise.all( - arg.libraryPaths.map(async (arg) => - manifestTemplateFromArg(await tg.template(arg)), - ), - ) - : undefined; - - // Build an injection dylib to match the interpreter. - const interpreterFile = - arg.executable instanceof tg.Symlink - ? await arg.executable.resolve() - : arg.executable; - if (!interpreterFile || interpreterFile instanceof tg.Directory) { - throw new Error("Could not resolve the symlink to the interpreter."); - } - tg.File.assert(interpreterFile); - const interpreterMetadata = - await std.file.executableMetadata(interpreterFile); - if (interpreterMetadata.format !== "elf") { - return tg.unreachable( - "Cannot build an ld-linux interpreter for a non-ELF executable.", - ); - } - - const preloads = arg.preloads - ? await Promise.all( - arg.preloads?.map(async (arg) => - manifestTemplateFromArg(await tg.template(arg)), - ), - ) - : []; - - // If no preload is defined, add the default injection preload. - if (preloads.length === 0) { - const arch = interpreterMetadata.arch; - const host = `${arch}-unknown-linux-gnu`; - const detectedBuild = await std.triple.host(); - const buildOs = std.triple.os(detectedBuild); - const buildToolchain = buildToolchainArg - ? buildToolchainArg - : gnu.toolchain({ host }); - const injectionLibrary = await injection.default({ - buildToolchain, - build: buildOs === "darwin" ? detectedBuild : undefined, - host, - }); + // We now have a `wrap.Interpreter` object. Fill in any missing fields. + tg.assert("kind" in arg); + const kind = arg.kind; + switch (kind) { + case "ld-linux": { + const libraryPaths = arg.libraryPaths; + const args = arg.args; + const preloads = arg.preloads ?? []; + + // Find the artifact for the interpreter executable. + const executable = + arg.executable instanceof tg.Symlink + ? await arg.executable.resolve() + : arg.executable; + if (!executable || executable instanceof tg.Directory) { + throw new Error("Could not resolve the symlink to the interpreter."); + } + tg.File.assert(executable); + const interpreterMetadata = await std.file.executableMetadata(executable); + if (interpreterMetadata.format !== "elf") { + return tg.unreachable( + "Cannot build an ld-linux interpreter for a non-ELF executable.", + ); + } - const injectionManifestSymlink = - await manifestTemplateFromArg(injectionLibrary); - preloads.push(injectionManifestSymlink); - } + // If no preload is defined, add the default injection preload. + if (preloads.length === 0) { + const arch = interpreterMetadata.arch; + const host = `${arch}-unknown-linux-gnu`; + const detectedBuild = await std.triple.host(); + const buildOs = std.triple.os(detectedBuild); + const buildToolchain = buildToolchainArg + ? buildToolchainArg + : gnu.toolchain({ host }); + const injectionLibrary = await injection.default({ + buildToolchain, + build: buildOs === "darwin" ? detectedBuild : undefined, + host, + }); + + preloads.push(injectionLibrary); + } - const args = arg.args - ? await Promise.all(arg.args.map(manifestTemplateFromArg)) - : undefined; - return { - kind: "ld-linux", - path, - libraryPaths, - preloads, - args, - }; - } else if ("kind" in arg && arg.kind === "ld-musl") { - // Handle an ld-musl interpreter. - const path = await manifestTemplateFromArg(arg.executable); - const libraryPaths = arg.libraryPaths - ? await Promise.all( - arg.libraryPaths.map(async (arg) => - manifestTemplateFromArg(await tg.template(arg)), - ), - ) - : undefined; - - // Build an injection dylib to match the interpreter. - const interpreterFile = - arg.executable instanceof tg.Symlink - ? await arg.executable.resolve() - : arg.executable; - if (!interpreterFile || interpreterFile instanceof tg.Directory) { - throw new Error("Could not resolve the symlink to the interpreter."); - } - tg.File.assert(interpreterFile); - const interpreterMetadata = - await std.file.executableMetadata(interpreterFile); - if (interpreterMetadata.format !== "elf") { - return tg.unreachable( - "Cannot build an ld-musl interpreter for a non-ELF executable.", - ); + return { + kind, + executable, + libraryPaths, + preloads, + args, + }; } + case "ld-musl": { + const libraryPaths = arg.libraryPaths; + const args = arg.args; + const preloads = arg.preloads ?? []; + + // Find the artifact for the interpreter executable. + const executable = + arg.executable instanceof tg.Symlink + ? await arg.executable.resolve() + : arg.executable; + if (!executable || executable instanceof tg.Directory) { + throw new Error("Could not resolve the symlink to the interpreter."); + } + tg.File.assert(executable); + const interpreterMetadata = await std.file.executableMetadata(executable); + if (interpreterMetadata.format !== "elf") { + return tg.unreachable( + "Cannot build an ld-musl interpreter for a non-ELF executable.", + ); + } - const preloads = arg.preloads - ? await Promise.all( - arg.preloads?.map(async (arg) => - manifestTemplateFromArg(await tg.template(arg)), - ), - ) - : []; - - // If no preload is defined, add the default injection preload. - if (preloads.length === 0) { - const arch = interpreterMetadata.arch; - const host = `${arch}-linux-musl`; - const buildToolchain = bootstrap.sdk.env(host); - const injectionLibrary = await injection.default({ - buildToolchain, - host, - }); + // If no preload is defined, add the default injection preload. + if (preloads.length === 0) { + const arch = interpreterMetadata.arch; + const host = `${arch}-linux-musl`; + const buildToolchain = bootstrap.sdk.env(host); + const injectionLibrary = await injection.default({ + buildToolchain, + host, + }); + preloads.push(injectionLibrary); + } - const injectionManifestSymlink = - await manifestTemplateFromArg(injectionLibrary); - preloads.push(injectionManifestSymlink); + return { + kind, + executable, + libraryPaths, + preloads, + args, + }; } + case "dyld": { + const libraryPaths = arg.libraryPaths; + const preloads = arg.preloads ?? []; + + // If no preload is defined, add the default injection preload. + if (preloads.length === 0) { + const host = await std.triple.host(); + const buildToolchain = buildToolchainArg + ? buildToolchainArg + : bootstrap.sdk.env(host); + const injectionLibrary = await injection.default({ + buildToolchain, + host, + }); + preloads.push(injectionLibrary); + } - const args = arg.args - ? await Promise.all(arg.args.map(manifestTemplateFromArg)) - : undefined; - return { - kind: "ld-musl", - path, - libraryPaths, - preloads, - args, - }; - } else if ("kind" in arg && arg.kind === "dyld") { - // Handle a dyld interpreter. - const libraryPaths = arg.libraryPaths - ? await Promise.all( - arg.libraryPaths.map(async (arg) => - manifestTemplateFromArg(await tg.template(arg)), - ), - ) - : undefined; - const preloads = arg.preloads - ? await Promise.all( - arg.preloads?.map(async (arg) => - manifestTemplateFromArg(await tg.template(arg)), - ), - ) - : []; - - // If no preload is defined, add the default injection preload. - if (preloads.length === 0) { - const host = await std.triple.host(); - const buildToolchain = buildToolchainArg - ? buildToolchainArg - : bootstrap.sdk.env(host); - const injectionLibrary = await injection.default({ - buildToolchain, - host, - }); - preloads.push(await manifestTemplateFromArg(injectionLibrary)); + return { + kind, + libraryPaths, + preloads, + }; + } + case "normal": { + return { + kind, + executable: arg.executable, + args: arg.args, + }; + } + default: { + return tg.unreachable(`unrecognized kind ${kind}`); } - return { - kind: "dyld", - libraryPaths, - preloads, - }; - } else { - // Handle a normal interpreter. - const path = await manifestTemplateFromArg(arg.executable); - const args = await Promise.all( - arg.args?.map(manifestTemplateFromArg) ?? [], - ); - return { - kind: "normal", - path, - args, - }; } }; -const isManifestInterpreter = ( - arg: unknown, -): arg is wrap.Manifest.Interpreter => { - return ( - arg !== undefined && - arg !== null && - typeof arg === "object" && - "kind" in arg && - (arg.kind === "normal" || - arg.kind === "ld-linux" || - arg.kind === "ld-musl" || - arg.kind === "dyld") && - "path" in arg - ); -}; - -const manifestInterpreterFromExecutableArg = async ( +/** Inspect the executable and produce the corresponding interpreter. */ +const interpreterFromExecutableArg = async ( arg: string | tg.Template | tg.File | tg.Symlink, buildToolchainArg?: std.env.Arg, -): Promise => { +): Promise => { // If the arg is a string or template, there is no interpreter. if (typeof arg === "string" || arg instanceof tg.Template) { return undefined; @@ -1085,7 +1140,7 @@ const manifestInterpreterFromExecutableArg = async ( // Handle the executable by its format. switch (metadata.format) { case "elf": { - return manifestInterpreterFromElf(metadata, buildToolchainArg); + return interpreterFromElf(metadata, buildToolchainArg); } case "mach-o": { const arch = std.triple.arch(await std.triple.host()); @@ -1098,12 +1153,12 @@ const manifestInterpreterFromExecutableArg = async ( return { kind: "dyld", libraryPaths: undefined, - preloads: [await manifestTemplateFromArg(injectionDylib)], + preloads: [injectionDylib], }; } case "shebang": { if (metadata.interpreter === undefined) { - return manifestInterpreterFromArg( + return interpreterFromArg( await wrap.defaultShell({ buildToolchain: buildToolchainArg }), buildToolchainArg, ); @@ -1114,10 +1169,11 @@ const manifestInterpreterFromExecutableArg = async ( } }; -const manifestInterpreterFromElf = async ( +/** Inspect an ELF file and produce the correct interpreter. */ +const interpreterFromElf = async ( metadata: std.file.ElfExecutableMetadata, buildToolchainArg?: std.env.Arg, -): Promise => { +): Promise => { // If there is no interpreter, this is a statically-linked executable. Nothing to do. if (metadata.interpreter === undefined) { return undefined; @@ -1156,29 +1212,47 @@ const manifestInterpreterFromElf = async ( ); return { kind: "ld-linux", - path: await manifestTemplateFromArg(ldso), - libraryPaths: [await manifestTemplateFromArg(libDir)], - preloads: [await manifestTemplateFromArg(injectionLib)], - args: undefined, + executable: ldso, + libraryPaths: [libDir], + preloads: [injectionLib], }; } else if (metadata.interpreter?.includes("ld-musl")) { // Handle an ld-musl interpreter. host = std.triple.create(host, { environment: "musl" }); const muslArtifact = await bootstrap.musl.build({ host }); - const libDir = tg.Directory.expect(await muslArtifact.get("lib")); - const ldso = tg.File.expect(await libDir.get("libc.so")); + const libDir = await muslArtifact.get("lib").then(tg.Directory.expect); + const ldso = await libDir.get("libc.so").then(tg.File.expect); return { kind: "ld-musl", - path: await manifestTemplateFromArg(ldso), - libraryPaths: [await manifestTemplateFromArg(libDir)], - preloads: [await manifestTemplateFromArg(injectionLib)], - args: undefined, + executable: ldso, + libraryPaths: [libDir], + preloads: [injectionLib], }; } else { throw new Error(`Unsupported interpreter: "${metadata.interpreter}".`); } }; +const getNeededLibraries = async ( + executable: tg.File, +): Promise> => { + const metadata = await std.file.executableMetadata(executable); + const fileName = (path: string) => path.split("/").pop(); + if (metadata.format === "mach-o") { + return (metadata.dependencies ?? []) + .map(fileName) + .filter((el) => el !== undefined); + } else if (metadata.format === "elf") { + return (metadata.needed ?? []) + .map(fileName) + .filter((el) => el !== undefined); + } else { + throw new Error( + "cannot determine needed libraries for non-ELF or Mach-O file", + ); + } +}; + const valueIsTemplateLike = ( value: tg.Value, ): value is string | tg.Template | tg.Artifact => {