From b4d27498a14c8e8c7ce91a88ba736a81c2ec43cc Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 24 Jun 2024 12:25:56 -0500 Subject: [PATCH] Enable depending on this crate as a library (#27) * Move `main.rs` to `lib.rs` Also main the `main` function public * Re-add `src/main.rs` with `main` reexport --- src/lib.rs | 655 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 656 +--------------------------------------------------- 2 files changed, 656 insertions(+), 655 deletions(-) create mode 100644 src/lib.rs diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..26e31e2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,655 @@ +use anyhow::{bail, Context, Result}; +use clap::{ArgAction, CommandFactory, FromArgMatches}; +use lexopt::Arg; +use std::env; +use std::ffi::OsString; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::str::FromStr; +use wasmparser::Payload; + +/// Representation of a flag passed to `wasm-ld` +/// +/// Note that the parsing of flags in `wasm-ld` is not as uniform as parsing +/// arguments via `clap`. For example if `--foo bar` is supported that doesn't +/// mean that `--foo=bar` is supported. Similarly some options such as `--foo` +/// support optional values as `--foo=bar` but can't be specified as +/// `--foo bar`. +/// +/// Finally there's currently only one "weird" flag which is `-shared` which has +/// a single dash but a long name. That's specially handled elsewhere. +/// +/// The general goal here is that we want to inherit `wasm-ld`'s CLI but also +/// want to be able to reserve CLI flags for this linker itself, so `wasm-ld`'s +/// arguments are parsed where our own are intermixed. +struct LldFlag { + clap_name: &'static str, + long: Option<&'static str>, + short: Option, + value: FlagValue, +} + +enum FlagValue { + /// This option has no value, e.g. `-f` or `--foo` + None, + + /// This option's value must be specified with `=`, for example `--foo=bar` + RequiredEqual(&'static str), + + /// This option's value must be specified with ` `, for example `--foo bar`. + /// + /// I think that `wasm-ld` supports both `-f foo` and `-ffoo` for + /// single-character flags, but I haven't tested as putting a space seems to + /// work. + RequiredSpace(&'static str), + + /// This option's value is optional but if specified it must use an `=` for + /// example `--foo=bar` or `--foo`. + Optional(&'static str), +} + +/// This is a large macro which is intended to take CLI-looking syntax and turn +/// each individual flag into a `LldFlag` specified above. +macro_rules! flag { + // Long options specified as: + // + // -f / --foo + // + // or just + // + // --foo + // + // Options can look like `--foo`, `--foo=bar`, `--foo[=bar]`, or + // `--foo bar` to match the kinds of flags that LLD supports. + ($(-$short:ident /)? --$($flag:tt)*) => { + LldFlag { + clap_name: concat!("long_", $(stringify!($flag),)*), + long: Some(flag!(@name [] $($flag)*)), + short: flag!(@short $($short)?), + value: flag!(@value $($flag)*), + } + }; + + // Short options specified as `-f` or `-f foo`. + (-$flag:tt $($val:tt)*) => { + LldFlag { + clap_name: concat!("short_", stringify!($flag)), + long: None, + short: Some(flag!(@char $flag)), + value: flag!(@value $flag $($val)*), + } + }; + + // Generates the long name of a flag, collected within the `[]` argument to + // this macro. This will iterate over the flag given as the rest of the + // macro arguments and collect values into `[...]` and recurse. + // + // The first recursion case handles `foo-bar-baz=..` where Rust tokenizes + // this as `foo` then `-` then `bar` then ... If this is found then `foo-` + // is added to the name and then the macro recurses. + (@name [$($name:tt)*] $n:ident-$($rest:tt)*) => (flag!(@name [$($name)* $n-] $($rest)*)); + // These are the ways options are represented, either `--foo bar`, + // `--foo=bar`, `--foo=bar`, or `--foo`. In all these cases discard the + // value itself and then recurse. + (@name [$($name:tt)*] $n:ident $_value:ident) => (flag!(@name [$($name)* $n])); + (@name [$($name:tt)*] $n:ident=$_value:ident) => (flag!(@name [$($name)* $n])); + (@name [$($name:tt)*] $n:ident[=$_value:ident]) => (flag!(@name [$($name)* $n])); + (@name [$($name:tt)*] $n:ident) => (flag!(@name [$($name)* $n])); + // If there's nothing left then the `$name` has collected everything so + // it's stringifyied and caoncatenated. + (@name [$($name:tt)*]) => (concat!($(stringify!($name),)*)); + + // This parses the value-style of the flag given. The recursion here looks + // similar to `@name` above. except that the four terminal cases all + // correspond to different variants of `FlagValue`. + (@value $n:ident - $($rest:tt)*) => (flag!(@value $($rest)*)); + (@value $_flag:ident = $name:ident) => (FlagValue::RequiredEqual(stringify!($name))); + (@value $_flag:ident $name:ident) => (FlagValue::RequiredSpace(stringify!($name))); + (@value $_flag:ident [= $name:ident]) => (FlagValue::Optional(stringify!($name))); + (@value $_flag:ident) => (FlagValue::None); + + // Helper for flags that have both a long and a short form to parse whether + // a short form was provided. + (@short) => (None); + (@short $name:ident) => (Some(flag!(@char $name))); + + // Helper for getting the `char` of a short flag. + (@char $name:ident) => ({ + let name = stringify!($name); + assert!(name.len() == 1); + name.as_bytes()[0] as char + }); +} + +const LLD_FLAGS: &[LldFlag] = &[ + flag! { --allow-undefined-file=PATH }, + flag! { --allow-undefined }, + flag! { --Bdynamic }, + flag! { --Bstatic }, + flag! { --Bsymbolic }, + flag! { --build-id[=VAL] }, + flag! { --call_shared }, + flag! { --check-features }, + flag! { --color-diagnostics[=VALUE] }, + flag! { --compress-relocations }, + flag! { --demangle }, + flag! { --dn }, + flag! { --dy }, + flag! { --emit-relocs }, + flag! { --end-lib }, + flag! { --entry SYM }, + flag! { --error-limit=N }, + flag! { --error-unresolved-symbols }, + flag! { --experimental-pic }, + flag! { --export-all }, + flag! { -E / --export-dynamic }, + flag! { --export-if-defined=SYM }, + flag! { --export-memory[=NAME] }, + flag! { --export-table }, + flag! { --export=SYM }, + flag! { --extra-features=LIST }, + flag! { --fatal-warnings }, + flag! { --features=LIST }, + flag! { --gc-sections }, + flag! { --global-base=VALUE }, + flag! { --growable-table }, + flag! { --import-memory[=NAME] }, + flag! { --import-table }, + flag! { --import-undefined }, + flag! { --initial-heap=SIZE }, + flag! { --initial-memory=SIZE }, + flag! { --keep-section=NAME }, + flag! { --lto-CGO=LEVEL }, + flag! { --lto-debug-pass-manager }, + flag! { --lto-O=LEVEL }, + flag! { --lto-partitions=NUM }, + flag! { -L PATH }, + flag! { -l LIB }, + flag! { --Map=FILE }, + flag! { --max-memory=SIZE }, + flag! { --merge-data-segments }, + flag! { --mllvm=FLAG }, + flag! { -m ARCH }, + flag! { --no-check-features }, + flag! { --no-color-diagnostics }, + flag! { --no-demangle }, + flag! { --no-entry }, + flag! { --no-export-dynamic }, + flag! { --no-gc-sections }, + flag! { --no-merge-data-segments }, + flag! { --no-pie }, + flag! { --no-print-gc-sections }, + flag! { --no-whole-archive }, + flag! { --non_shared }, + flag! { -O LEVEL }, + flag! { --pie }, + flag! { --print-gc-sections }, + flag! { -M / --print-map }, + flag! { --relocatable }, + flag! { --save-temps }, + flag! { --shared-memory }, + flag! { --shared }, + flag! { --soname=VALUE }, + flag! { --stack-first }, + flag! { --start-lib }, + flag! { --static }, + flag! { -s / --strip-all }, + flag! { -S / --strip-debug }, + flag! { --table-base=VALUE }, + flag! { --thinlto-cache-dir=PATH }, + flag! { --thinlto-cache-policy=VALUE }, + flag! { --thinlto-jobs=N }, + flag! { --threads=N }, + flag! { -y / --trace-symbol=SYM }, + flag! { -t / --trace }, + flag! { --undefined=SYM }, + flag! { --unresolved-symbols=VALUE }, + flag! { --warn-unresolved-symbols }, + flag! { --whole-archive }, + flag! { --why-extract=MEMBER }, + flag! { --wrap=VALUE }, + flag! { -z OPT }, +]; + +const LLD_LONG_FLAGS_NONSTANDARD: &[&str] = &["-shared"]; + +#[derive(Default)] +struct App { + component: ComponentLdArgs, + lld_args: Vec, + shared: bool, +} + +/// A linker to create a Component from input object files and libraries. +/// +/// This application is an equivalent of `wasm-ld` except that it produces a +/// component instead of a core wasm module. This application behaves very +/// similarly to `wasm-ld` in that it takes the same inputs and flags, and it +/// will internally invoke `wasm-ld`. After `wasm-ld` has been invoked the core +/// wasm module will be turned into a component using component tooling and +/// embedded information in the core wasm module. +#[derive(clap::Parser, Default)] +#[command(version)] +struct ComponentLdArgs { + /// Which default WASI adapter, if any, to use when creating the output + /// component. + #[clap(long, name = "command|reactor|proxy|none")] + wasi_adapter: Option, + + /// Location of where to find `wasm-ld`. + /// + /// If not specified this is automatically detected. + #[clap(long, name = "PATH")] + wasm_ld_path: Option, + + /// Quoting syntax for response files. + #[clap(long, name = "STYLE")] + rsp_quoting: Option, + + /// Where to place the component output. + #[clap(short, long)] + output: Option, + + /// Print verbose output. + #[clap(long)] + verbose: bool, + + /// Whether or not the output component is validated. + /// + /// This defaults to `true`. + #[clap(long)] + validate_component: Option, + + /// Adapters to use when creating the final component. + #[clap(long = "adapt", value_name = "[NAME=]MODULE", value_parser = parse_adapter)] + adapters: Vec<(String, Vec)>, +} + +fn parse_adapter(s: &str) -> Result<(String, Vec)> { + let (name, path) = parse_optionally_name_file(s); + let wasm = wat::parse_file(path)?; + Ok((name.to_string(), wasm)) +} + +fn parse_optionally_name_file(s: &str) -> (&str, &str) { + let mut parts = s.splitn(2, '='); + let name_or_path = parts.next().unwrap(); + match parts.next() { + Some(path) => (name_or_path, path), + None => { + let name = Path::new(name_or_path) + .file_name() + .unwrap() + .to_str() + .unwrap(); + let name = match name.find('.') { + Some(i) => &name[..i], + None => name, + }; + (name, name_or_path) + } + } +} + +#[derive(Debug, Copy, Clone)] +enum WasiAdapter { + Command, + Reactor, + Proxy, + None, +} + +impl FromStr for WasiAdapter { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "none" => Ok(WasiAdapter::None), + "command" => Ok(WasiAdapter::Command), + "reactor" => Ok(WasiAdapter::Reactor), + "proxy" => Ok(WasiAdapter::Proxy), + _ => bail!("unknown wasi adapter {s}, must be one of: none, command, reactor, proxy"), + } + } +} + +pub fn main() { + let err = match run() { + Ok(()) => return, + Err(e) => e, + }; + eprintln!("error: {err}"); + if err.chain().len() > 1 { + eprintln!("\nCaused by:"); + for (i, err) in err.chain().skip(1).enumerate() { + eprintln!("{i:>5}: {}", err.to_string().replace("\n", "\n ")); + } + } + + std::process::exit(1); +} + +fn run() -> Result<()> { + App::parse()?.run() +} + +impl App { + /// Parse the CLI arguments into an `App` to run the linker. + /// + /// This is unfortunately nontrivial because the way `wasm-ld` takes + /// arguments is not compatible with `clap`. Namely flags like + /// `--whole-archive` are positional are processed in a stateful manner. + /// This means that the relative ordering of flags to `wasm-ld` needs to be + /// preserved. Additionally there are flags like `-shared` which clap does + /// not support. + /// + /// To handle this the `lexopt` crate is used to perform low-level argument + /// parsing. That's then used to determine whether the argument is intended + /// for `wasm-component-ld` or `wasm-ld`, so arguments are filtered into two + /// lists. Using these lists the arguments to `wasm-component-ld` are then + /// parsed. On failure a help message is presented with all `wasm-ld` + /// arguments added as well. + /// + /// This means that functionally it looks like `clap` parses everything when + /// in fact `lexopt` is used to filter out `wasm-ld` arguments and `clap` + /// only parses arguments specific to `wasm-component-ld`. + fn parse() -> Result { + let mut args = env::args_os().collect::>(); + + // First remove `-flavor wasm` in case this is invoked as a generic LLD + // driver. We can safely ignore that going forward. + if let Some([flavor, wasm]) = args.get(1..3) { + if flavor == "-flavor" && wasm == "wasm" { + args.remove(1); + args.remove(1); + } + } + + let mut command = ComponentLdArgs::command(); + let mut lld_args = Vec::new(); + let mut component_ld_args = vec![std::env::args_os().nth(0).unwrap()]; + let mut shared = false; + let mut parser = lexopt::Parser::from_iter(args); + + fn handle_lld_arg( + lld: &LldFlag, + parser: &mut lexopt::Parser, + lld_args: &mut Vec, + ) -> Result<()> { + let mut arg = OsString::new(); + match (lld.short, lld.long) { + (_, Some(long)) => { + arg.push("--"); + arg.push(long); + } + (Some(short), _) => { + arg.push("-"); + arg.push(short.encode_utf8(&mut [0; 5])); + } + (None, None) => unreachable!(), + } + match lld.value { + FlagValue::None => { + lld_args.push(arg); + } + + FlagValue::RequiredSpace(_) => { + lld_args.push(arg); + lld_args.push(parser.value()?); + } + + FlagValue::RequiredEqual(_) => { + arg.push("="); + arg.push(&parser.value()?); + lld_args.push(arg); + } + + // If the value is optional then the argument must have an `=` + // in the argument itself. + FlagValue::Optional(_) => { + match parser.optional_value() { + Some(val) => { + arg.push("="); + arg.push(&val); + } + None => {} + } + lld_args.push(arg); + } + } + Ok(()) + } + + loop { + if let Some(mut args) = parser.try_raw_args() { + if let Some(arg) = args.peek() { + let for_lld = LLD_LONG_FLAGS_NONSTANDARD.iter().any(|s| arg == *s); + if for_lld { + lld_args.push(arg.to_owned()); + if arg == "-shared" { + shared = true; + } + args.next(); + continue; + } + } + } + + match parser.next()? { + Some(Arg::Value(obj)) => { + lld_args.push(obj); + } + Some(Arg::Short(c)) => match LLD_FLAGS.iter().find(|f| f.short == Some(c)) { + Some(lld) => { + handle_lld_arg(lld, &mut parser, &mut lld_args)?; + } + None => { + component_ld_args.push(format!("-{c}").into()); + if let Some(arg) = + command.get_arguments().find(|a| a.get_short() == Some(c)) + { + if let ArgAction::Set = arg.get_action() { + component_ld_args.push(parser.value()?); + } + } + } + }, + Some(Arg::Long(c)) => match LLD_FLAGS.iter().find(|f| f.long == Some(c)) { + Some(lld) => { + handle_lld_arg(lld, &mut parser, &mut lld_args)?; + } + None => { + component_ld_args.push(format!("--{c}").into()); + if let Some(arg) = command.get_arguments().find(|a| a.get_long() == Some(c)) + { + if let ArgAction::Set = arg.get_action() { + component_ld_args.push(parser.value()?); + } + } + } + }, + None => break, + } + } + + match command.try_get_matches_from_mut(component_ld_args.clone()) { + Ok(matches) => Ok(App { + component: ComponentLdArgs::from_arg_matches(&matches)?, + lld_args, + shared, + }), + Err(_) => { + add_wasm_ld_options(ComponentLdArgs::command()).get_matches_from(component_ld_args); + unreachable!(); + } + } + } + + fn run(&mut self) -> Result<()> { + let mut cmd = self.lld(); + let linker = cmd.get_program().to_owned(); + + let lld_output = + tempfile::NamedTempFile::new().context("failed to create temp output file")?; + + // Shared libraries don't get wit-component run below so place the + // output directly at the desired output location. Otherwise output to a + // temporary location for wit-component to read and then the real output + // is created after wit-component runs. + if self.shared { + cmd.arg("-o").arg(self.component.output.as_ref().unwrap()); + } else { + cmd.arg("-o").arg(lld_output.path()); + } + + if self.component.verbose { + eprintln!("running LLD: {cmd:?}"); + } + let status = cmd + .status() + .with_context(|| format!("failed to spawn {linker:?}"))?; + if !status.success() { + bail!("failed to invoke LLD: {status}"); + } + + // Skip componentization with `--shared` since that's creating a shared + // library that's not a component yet. + if self.shared { + return Ok(()); + } + + let reactor_adapter = include_bytes!("wasi_snapshot_preview1.reactor.wasm"); + let command_adapter = include_bytes!("wasi_snapshot_preview1.command.wasm"); + let proxy_adapter = include_bytes!("wasi_snapshot_preview1.proxy.wasm"); + let core_module = std::fs::read(lld_output.path()) + .with_context(|| format!("failed to read {linker:?} output"))?; + + // Inspect the output module to see if it's a command or reactor. + let mut exports_start = false; + for payload in wasmparser::Parser::new(0).parse_all(&core_module) { + match payload { + Ok(Payload::ExportSection(e)) => { + for export in e { + if let Ok(e) = export { + if e.name == "_start" { + exports_start = true; + break; + } + } + } + } + _ => {} + } + } + + let mut encoder = wit_component::ComponentEncoder::default() + .module(&core_module) + .context("failed to parse core wasm for componentization")? + .validate(self.component.validate_component.unwrap_or(true)); + let adapter = self.component.wasi_adapter.unwrap_or(if exports_start { + WasiAdapter::Command + } else { + WasiAdapter::Reactor + }); + let adapter = match adapter { + WasiAdapter::Command => Some(&command_adapter[..]), + WasiAdapter::Reactor => Some(&reactor_adapter[..]), + WasiAdapter::Proxy => Some(&proxy_adapter[..]), + WasiAdapter::None => None, + }; + + if let Some(adapter) = adapter { + encoder = encoder + .adapter("wasi_snapshot_preview1", adapter) + .context("failed to inject adapter")?; + } + + for (name, adapter) in self.component.adapters.iter() { + encoder = encoder + .adapter(name, adapter) + .with_context(|| format!("failed to inject adapter {name:?}"))?; + } + + let component = encoder.encode().context("failed to encode component")?; + + std::fs::write(self.component.output.as_ref().unwrap(), &component) + .context("failed to write output file")?; + + Ok(()) + } + + fn lld(&self) -> Command { + let mut lld = self.find_lld(); + lld.args(&self.lld_args); + if self.component.verbose { + lld.arg("--verbose"); + } + lld + } + + fn find_lld(&self) -> Command { + if let Some(path) = &self.component.wasm_ld_path { + return Command::new(path); + } + + // Search for the first of `wasm-ld` or `rust-lld` in `$PATH` + let wasm_ld = format!("wasm-ld{}", env::consts::EXE_SUFFIX); + let rust_lld = format!("rust-lld{}", env::consts::EXE_SUFFIX); + for entry in env::split_paths(&env::var_os("PATH").unwrap_or_default()) { + if entry.join(&wasm_ld).is_file() { + return Command::new(wasm_ld); + } + if entry.join(&rust_lld).is_file() { + let mut ret = Command::new(rust_lld); + ret.arg("-flavor").arg("wasm"); + return ret; + } + } + + // Fall back to `wasm-ld` if the search failed to get an error message + // that indicates that `wasm-ld` was attempted to be found but couldn't + // be found. + Command::new("wasm-ld") + } +} + +fn add_wasm_ld_options(mut command: clap::Command) -> clap::Command { + use clap::Arg; + + command = command.arg( + Arg::new("objects") + .action(ArgAction::Append) + .help("objects to pass to `wasm-ld`"), + ); + + for flag in LLD_FLAGS { + let mut arg = Arg::new(flag.clap_name).help("forwarded to `wasm-ld`"); + if let Some(short) = flag.short { + arg = arg.short(short); + } + if let Some(long) = flag.long { + arg = arg.long(long); + } + arg = match flag.value { + FlagValue::RequiredEqual(name) | FlagValue::RequiredSpace(name) => { + arg.action(ArgAction::Set).value_name(name) + } + FlagValue::Optional(name) => arg + .action(ArgAction::Set) + .value_name(name) + .num_args(0..=1) + .require_equals(true), + FlagValue::None => arg.action(ArgAction::SetTrue), + }; + arg = arg.help_heading("Options forwarded to `wasm-ld`"); + command = command.arg(arg); + } + + command +} + +#[test] +fn verify_app() { + ComponentLdArgs::command().debug_assert(); + add_wasm_ld_options(ComponentLdArgs::command()).debug_assert(); +} diff --git a/src/main.rs b/src/main.rs index 5561431..35c3f8b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,655 +1 @@ -use anyhow::{bail, Context, Result}; -use clap::{ArgAction, CommandFactory, FromArgMatches}; -use lexopt::Arg; -use std::env; -use std::ffi::OsString; -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::str::FromStr; -use wasmparser::Payload; - -/// Representation of a flag passed to `wasm-ld` -/// -/// Note that the parsing of flags in `wasm-ld` is not as uniform as parsing -/// arguments via `clap`. For example if `--foo bar` is supported that doesn't -/// mean that `--foo=bar` is supported. Similarly some options such as `--foo` -/// support optional values as `--foo=bar` but can't be specified as -/// `--foo bar`. -/// -/// Finally there's currently only one "weird" flag which is `-shared` which has -/// a single dash but a long name. That's specially handled elsewhere. -/// -/// The general goal here is that we want to inherit `wasm-ld`'s CLI but also -/// want to be able to reserve CLI flags for this linker itself, so `wasm-ld`'s -/// arguments are parsed where our own are intermixed. -struct LldFlag { - clap_name: &'static str, - long: Option<&'static str>, - short: Option, - value: FlagValue, -} - -enum FlagValue { - /// This option has no value, e.g. `-f` or `--foo` - None, - - /// This option's value must be specified with `=`, for example `--foo=bar` - RequiredEqual(&'static str), - - /// This option's value must be specified with ` `, for example `--foo bar`. - /// - /// I think that `wasm-ld` supports both `-f foo` and `-ffoo` for - /// single-character flags, but I haven't tested as putting a space seems to - /// work. - RequiredSpace(&'static str), - - /// This option's value is optional but if specified it must use an `=` for - /// example `--foo=bar` or `--foo`. - Optional(&'static str), -} - -/// This is a large macro which is intended to take CLI-looking syntax and turn -/// each individual flag into a `LldFlag` specified above. -macro_rules! flag { - // Long options specified as: - // - // -f / --foo - // - // or just - // - // --foo - // - // Options can look like `--foo`, `--foo=bar`, `--foo[=bar]`, or - // `--foo bar` to match the kinds of flags that LLD supports. - ($(-$short:ident /)? --$($flag:tt)*) => { - LldFlag { - clap_name: concat!("long_", $(stringify!($flag),)*), - long: Some(flag!(@name [] $($flag)*)), - short: flag!(@short $($short)?), - value: flag!(@value $($flag)*), - } - }; - - // Short options specified as `-f` or `-f foo`. - (-$flag:tt $($val:tt)*) => { - LldFlag { - clap_name: concat!("short_", stringify!($flag)), - long: None, - short: Some(flag!(@char $flag)), - value: flag!(@value $flag $($val)*), - } - }; - - // Generates the long name of a flag, collected within the `[]` argument to - // this macro. This will iterate over the flag given as the rest of the - // macro arguments and collect values into `[...]` and recurse. - // - // The first recursion case handles `foo-bar-baz=..` where Rust tokenizes - // this as `foo` then `-` then `bar` then ... If this is found then `foo-` - // is added to the name and then the macro recurses. - (@name [$($name:tt)*] $n:ident-$($rest:tt)*) => (flag!(@name [$($name)* $n-] $($rest)*)); - // These are the ways options are represented, either `--foo bar`, - // `--foo=bar`, `--foo=bar`, or `--foo`. In all these cases discard the - // value itself and then recurse. - (@name [$($name:tt)*] $n:ident $_value:ident) => (flag!(@name [$($name)* $n])); - (@name [$($name:tt)*] $n:ident=$_value:ident) => (flag!(@name [$($name)* $n])); - (@name [$($name:tt)*] $n:ident[=$_value:ident]) => (flag!(@name [$($name)* $n])); - (@name [$($name:tt)*] $n:ident) => (flag!(@name [$($name)* $n])); - // If there's nothing left then the `$name` has collected everything so - // it's stringifyied and caoncatenated. - (@name [$($name:tt)*]) => (concat!($(stringify!($name),)*)); - - // This parses the value-style of the flag given. The recursion here looks - // similar to `@name` above. except that the four terminal cases all - // correspond to different variants of `FlagValue`. - (@value $n:ident - $($rest:tt)*) => (flag!(@value $($rest)*)); - (@value $_flag:ident = $name:ident) => (FlagValue::RequiredEqual(stringify!($name))); - (@value $_flag:ident $name:ident) => (FlagValue::RequiredSpace(stringify!($name))); - (@value $_flag:ident [= $name:ident]) => (FlagValue::Optional(stringify!($name))); - (@value $_flag:ident) => (FlagValue::None); - - // Helper for flags that have both a long and a short form to parse whether - // a short form was provided. - (@short) => (None); - (@short $name:ident) => (Some(flag!(@char $name))); - - // Helper for getting the `char` of a short flag. - (@char $name:ident) => ({ - let name = stringify!($name); - assert!(name.len() == 1); - name.as_bytes()[0] as char - }); -} - -const LLD_FLAGS: &[LldFlag] = &[ - flag! { --allow-undefined-file=PATH }, - flag! { --allow-undefined }, - flag! { --Bdynamic }, - flag! { --Bstatic }, - flag! { --Bsymbolic }, - flag! { --build-id[=VAL] }, - flag! { --call_shared }, - flag! { --check-features }, - flag! { --color-diagnostics[=VALUE] }, - flag! { --compress-relocations }, - flag! { --demangle }, - flag! { --dn }, - flag! { --dy }, - flag! { --emit-relocs }, - flag! { --end-lib }, - flag! { --entry SYM }, - flag! { --error-limit=N }, - flag! { --error-unresolved-symbols }, - flag! { --experimental-pic }, - flag! { --export-all }, - flag! { -E / --export-dynamic }, - flag! { --export-if-defined=SYM }, - flag! { --export-memory[=NAME] }, - flag! { --export-table }, - flag! { --export=SYM }, - flag! { --extra-features=LIST }, - flag! { --fatal-warnings }, - flag! { --features=LIST }, - flag! { --gc-sections }, - flag! { --global-base=VALUE }, - flag! { --growable-table }, - flag! { --import-memory[=NAME] }, - flag! { --import-table }, - flag! { --import-undefined }, - flag! { --initial-heap=SIZE }, - flag! { --initial-memory=SIZE }, - flag! { --keep-section=NAME }, - flag! { --lto-CGO=LEVEL }, - flag! { --lto-debug-pass-manager }, - flag! { --lto-O=LEVEL }, - flag! { --lto-partitions=NUM }, - flag! { -L PATH }, - flag! { -l LIB }, - flag! { --Map=FILE }, - flag! { --max-memory=SIZE }, - flag! { --merge-data-segments }, - flag! { --mllvm=FLAG }, - flag! { -m ARCH }, - flag! { --no-check-features }, - flag! { --no-color-diagnostics }, - flag! { --no-demangle }, - flag! { --no-entry }, - flag! { --no-export-dynamic }, - flag! { --no-gc-sections }, - flag! { --no-merge-data-segments }, - flag! { --no-pie }, - flag! { --no-print-gc-sections }, - flag! { --no-whole-archive }, - flag! { --non_shared }, - flag! { -O LEVEL }, - flag! { --pie }, - flag! { --print-gc-sections }, - flag! { -M / --print-map }, - flag! { --relocatable }, - flag! { --save-temps }, - flag! { --shared-memory }, - flag! { --shared }, - flag! { --soname=VALUE }, - flag! { --stack-first }, - flag! { --start-lib }, - flag! { --static }, - flag! { -s / --strip-all }, - flag! { -S / --strip-debug }, - flag! { --table-base=VALUE }, - flag! { --thinlto-cache-dir=PATH }, - flag! { --thinlto-cache-policy=VALUE }, - flag! { --thinlto-jobs=N }, - flag! { --threads=N }, - flag! { -y / --trace-symbol=SYM }, - flag! { -t / --trace }, - flag! { --undefined=SYM }, - flag! { --unresolved-symbols=VALUE }, - flag! { --warn-unresolved-symbols }, - flag! { --whole-archive }, - flag! { --why-extract=MEMBER }, - flag! { --wrap=VALUE }, - flag! { -z OPT }, -]; - -const LLD_LONG_FLAGS_NONSTANDARD: &[&str] = &["-shared"]; - -#[derive(Default)] -struct App { - component: ComponentLdArgs, - lld_args: Vec, - shared: bool, -} - -/// A linker to create a Component from input object files and libraries. -/// -/// This application is an equivalent of `wasm-ld` except that it produces a -/// component instead of a core wasm module. This application behaves very -/// similarly to `wasm-ld` in that it takes the same inputs and flags, and it -/// will internally invoke `wasm-ld`. After `wasm-ld` has been invoked the core -/// wasm module will be turned into a component using component tooling and -/// embedded information in the core wasm module. -#[derive(clap::Parser, Default)] -#[command(version)] -struct ComponentLdArgs { - /// Which default WASI adapter, if any, to use when creating the output - /// component. - #[clap(long, name = "command|reactor|proxy|none")] - wasi_adapter: Option, - - /// Location of where to find `wasm-ld`. - /// - /// If not specified this is automatically detected. - #[clap(long, name = "PATH")] - wasm_ld_path: Option, - - /// Quoting syntax for response files. - #[clap(long, name = "STYLE")] - rsp_quoting: Option, - - /// Where to place the component output. - #[clap(short, long)] - output: Option, - - /// Print verbose output. - #[clap(long)] - verbose: bool, - - /// Whether or not the output component is validated. - /// - /// This defaults to `true`. - #[clap(long)] - validate_component: Option, - - /// Adapters to use when creating the final component. - #[clap(long = "adapt", value_name = "[NAME=]MODULE", value_parser = parse_adapter)] - adapters: Vec<(String, Vec)>, -} - -fn parse_adapter(s: &str) -> Result<(String, Vec)> { - let (name, path) = parse_optionally_name_file(s); - let wasm = wat::parse_file(path)?; - Ok((name.to_string(), wasm)) -} - -fn parse_optionally_name_file(s: &str) -> (&str, &str) { - let mut parts = s.splitn(2, '='); - let name_or_path = parts.next().unwrap(); - match parts.next() { - Some(path) => (name_or_path, path), - None => { - let name = Path::new(name_or_path) - .file_name() - .unwrap() - .to_str() - .unwrap(); - let name = match name.find('.') { - Some(i) => &name[..i], - None => name, - }; - (name, name_or_path) - } - } -} - -#[derive(Debug, Copy, Clone)] -enum WasiAdapter { - Command, - Reactor, - Proxy, - None, -} - -impl FromStr for WasiAdapter { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s { - "none" => Ok(WasiAdapter::None), - "command" => Ok(WasiAdapter::Command), - "reactor" => Ok(WasiAdapter::Reactor), - "proxy" => Ok(WasiAdapter::Proxy), - _ => bail!("unknown wasi adapter {s}, must be one of: none, command, reactor, proxy"), - } - } -} - -fn main() { - let err = match run() { - Ok(()) => return, - Err(e) => e, - }; - eprintln!("error: {err}"); - if err.chain().len() > 1 { - eprintln!("\nCaused by:"); - for (i, err) in err.chain().skip(1).enumerate() { - eprintln!("{i:>5}: {}", err.to_string().replace("\n", "\n ")); - } - } - - std::process::exit(1); -} - -fn run() -> Result<()> { - App::parse()?.run() -} - -impl App { - /// Parse the CLI arguments into an `App` to run the linker. - /// - /// This is unfortunately nontrivial because the way `wasm-ld` takes - /// arguments is not compatible with `clap`. Namely flags like - /// `--whole-archive` are positional are processed in a stateful manner. - /// This means that the relative ordering of flags to `wasm-ld` needs to be - /// preserved. Additionally there are flags like `-shared` which clap does - /// not support. - /// - /// To handle this the `lexopt` crate is used to perform low-level argument - /// parsing. That's then used to determine whether the argument is intended - /// for `wasm-component-ld` or `wasm-ld`, so arguments are filtered into two - /// lists. Using these lists the arguments to `wasm-component-ld` are then - /// parsed. On failure a help message is presented with all `wasm-ld` - /// arguments added as well. - /// - /// This means that functionally it looks like `clap` parses everything when - /// in fact `lexopt` is used to filter out `wasm-ld` arguments and `clap` - /// only parses arguments specific to `wasm-component-ld`. - fn parse() -> Result { - let mut args = env::args_os().collect::>(); - - // First remove `-flavor wasm` in case this is invoked as a generic LLD - // driver. We can safely ignore that going forward. - if let Some([flavor, wasm]) = args.get(1..3) { - if flavor == "-flavor" && wasm == "wasm" { - args.remove(1); - args.remove(1); - } - } - - let mut command = ComponentLdArgs::command(); - let mut lld_args = Vec::new(); - let mut component_ld_args = vec![std::env::args_os().nth(0).unwrap()]; - let mut shared = false; - let mut parser = lexopt::Parser::from_iter(args); - - fn handle_lld_arg( - lld: &LldFlag, - parser: &mut lexopt::Parser, - lld_args: &mut Vec, - ) -> Result<()> { - let mut arg = OsString::new(); - match (lld.short, lld.long) { - (_, Some(long)) => { - arg.push("--"); - arg.push(long); - } - (Some(short), _) => { - arg.push("-"); - arg.push(short.encode_utf8(&mut [0; 5])); - } - (None, None) => unreachable!(), - } - match lld.value { - FlagValue::None => { - lld_args.push(arg); - } - - FlagValue::RequiredSpace(_) => { - lld_args.push(arg); - lld_args.push(parser.value()?); - } - - FlagValue::RequiredEqual(_) => { - arg.push("="); - arg.push(&parser.value()?); - lld_args.push(arg); - } - - // If the value is optional then the argument must have an `=` - // in the argument itself. - FlagValue::Optional(_) => { - match parser.optional_value() { - Some(val) => { - arg.push("="); - arg.push(&val); - } - None => {} - } - lld_args.push(arg); - } - } - Ok(()) - } - - loop { - if let Some(mut args) = parser.try_raw_args() { - if let Some(arg) = args.peek() { - let for_lld = LLD_LONG_FLAGS_NONSTANDARD.iter().any(|s| arg == *s); - if for_lld { - lld_args.push(arg.to_owned()); - if arg == "-shared" { - shared = true; - } - args.next(); - continue; - } - } - } - - match parser.next()? { - Some(Arg::Value(obj)) => { - lld_args.push(obj); - } - Some(Arg::Short(c)) => match LLD_FLAGS.iter().find(|f| f.short == Some(c)) { - Some(lld) => { - handle_lld_arg(lld, &mut parser, &mut lld_args)?; - } - None => { - component_ld_args.push(format!("-{c}").into()); - if let Some(arg) = - command.get_arguments().find(|a| a.get_short() == Some(c)) - { - if let ArgAction::Set = arg.get_action() { - component_ld_args.push(parser.value()?); - } - } - } - }, - Some(Arg::Long(c)) => match LLD_FLAGS.iter().find(|f| f.long == Some(c)) { - Some(lld) => { - handle_lld_arg(lld, &mut parser, &mut lld_args)?; - } - None => { - component_ld_args.push(format!("--{c}").into()); - if let Some(arg) = command.get_arguments().find(|a| a.get_long() == Some(c)) - { - if let ArgAction::Set = arg.get_action() { - component_ld_args.push(parser.value()?); - } - } - } - }, - None => break, - } - } - - match command.try_get_matches_from_mut(component_ld_args.clone()) { - Ok(matches) => Ok(App { - component: ComponentLdArgs::from_arg_matches(&matches)?, - lld_args, - shared, - }), - Err(_) => { - add_wasm_ld_options(ComponentLdArgs::command()).get_matches_from(component_ld_args); - unreachable!(); - } - } - } - - fn run(&mut self) -> Result<()> { - let mut cmd = self.lld(); - let linker = cmd.get_program().to_owned(); - - let lld_output = - tempfile::NamedTempFile::new().context("failed to create temp output file")?; - - // Shared libraries don't get wit-component run below so place the - // output directly at the desired output location. Otherwise output to a - // temporary location for wit-component to read and then the real output - // is created after wit-component runs. - if self.shared { - cmd.arg("-o").arg(self.component.output.as_ref().unwrap()); - } else { - cmd.arg("-o").arg(lld_output.path()); - } - - if self.component.verbose { - eprintln!("running LLD: {cmd:?}"); - } - let status = cmd - .status() - .with_context(|| format!("failed to spawn {linker:?}"))?; - if !status.success() { - bail!("failed to invoke LLD: {status}"); - } - - // Skip componentization with `--shared` since that's creating a shared - // library that's not a component yet. - if self.shared { - return Ok(()); - } - - let reactor_adapter = include_bytes!("wasi_snapshot_preview1.reactor.wasm"); - let command_adapter = include_bytes!("wasi_snapshot_preview1.command.wasm"); - let proxy_adapter = include_bytes!("wasi_snapshot_preview1.proxy.wasm"); - let core_module = std::fs::read(lld_output.path()) - .with_context(|| format!("failed to read {linker:?} output"))?; - - // Inspect the output module to see if it's a command or reactor. - let mut exports_start = false; - for payload in wasmparser::Parser::new(0).parse_all(&core_module) { - match payload { - Ok(Payload::ExportSection(e)) => { - for export in e { - if let Ok(e) = export { - if e.name == "_start" { - exports_start = true; - break; - } - } - } - } - _ => {} - } - } - - let mut encoder = wit_component::ComponentEncoder::default() - .module(&core_module) - .context("failed to parse core wasm for componentization")? - .validate(self.component.validate_component.unwrap_or(true)); - let adapter = self.component.wasi_adapter.unwrap_or(if exports_start { - WasiAdapter::Command - } else { - WasiAdapter::Reactor - }); - let adapter = match adapter { - WasiAdapter::Command => Some(&command_adapter[..]), - WasiAdapter::Reactor => Some(&reactor_adapter[..]), - WasiAdapter::Proxy => Some(&proxy_adapter[..]), - WasiAdapter::None => None, - }; - - if let Some(adapter) = adapter { - encoder = encoder - .adapter("wasi_snapshot_preview1", adapter) - .context("failed to inject adapter")?; - } - - for (name, adapter) in self.component.adapters.iter() { - encoder = encoder - .adapter(name, adapter) - .with_context(|| format!("failed to inject adapter {name:?}"))?; - } - - let component = encoder.encode().context("failed to encode component")?; - - std::fs::write(self.component.output.as_ref().unwrap(), &component) - .context("failed to write output file")?; - - Ok(()) - } - - fn lld(&self) -> Command { - let mut lld = self.find_lld(); - lld.args(&self.lld_args); - if self.component.verbose { - lld.arg("--verbose"); - } - lld - } - - fn find_lld(&self) -> Command { - if let Some(path) = &self.component.wasm_ld_path { - return Command::new(path); - } - - // Search for the first of `wasm-ld` or `rust-lld` in `$PATH` - let wasm_ld = format!("wasm-ld{}", env::consts::EXE_SUFFIX); - let rust_lld = format!("rust-lld{}", env::consts::EXE_SUFFIX); - for entry in env::split_paths(&env::var_os("PATH").unwrap_or_default()) { - if entry.join(&wasm_ld).is_file() { - return Command::new(wasm_ld); - } - if entry.join(&rust_lld).is_file() { - let mut ret = Command::new(rust_lld); - ret.arg("-flavor").arg("wasm"); - return ret; - } - } - - // Fall back to `wasm-ld` if the search failed to get an error message - // that indicates that `wasm-ld` was attempted to be found but couldn't - // be found. - Command::new("wasm-ld") - } -} - -fn add_wasm_ld_options(mut command: clap::Command) -> clap::Command { - use clap::Arg; - - command = command.arg( - Arg::new("objects") - .action(ArgAction::Append) - .help("objects to pass to `wasm-ld`"), - ); - - for flag in LLD_FLAGS { - let mut arg = Arg::new(flag.clap_name).help("forwarded to `wasm-ld`"); - if let Some(short) = flag.short { - arg = arg.short(short); - } - if let Some(long) = flag.long { - arg = arg.long(long); - } - arg = match flag.value { - FlagValue::RequiredEqual(name) | FlagValue::RequiredSpace(name) => { - arg.action(ArgAction::Set).value_name(name) - } - FlagValue::Optional(name) => arg - .action(ArgAction::Set) - .value_name(name) - .num_args(0..=1) - .require_equals(true), - FlagValue::None => arg.action(ArgAction::SetTrue), - }; - arg = arg.help_heading("Options forwarded to `wasm-ld`"); - command = command.arg(arg); - } - - command -} - -#[test] -fn verify_app() { - ComponentLdArgs::command().debug_assert(); - add_wasm_ld_options(ComponentLdArgs::command()).debug_assert(); -} +use wasm_component_ld::main;