1use anyhow::{bail, Context, Result};
2use clap::{ArgAction, CommandFactory, FromArgMatches};
3use lexopt::Arg;
4use std::env;
5use std::ffi::OsString;
6use std::path::{Path, PathBuf};
7use std::process::{Command, ExitStatus};
8use std::str::FromStr;
9use wasmparser::Payload;
10use wit_component::StringEncoding;
11use wit_parser::{Resolve, WorldId};
12
13mod argfile;
14
15struct LldFlag {
30 clap_name: &'static str,
31 long: Option<&'static str>,
32 short: Option<char>,
33 value: FlagValue,
34}
35
36enum FlagValue {
37 None,
39
40 RequiredEqual(&'static str),
42
43 RequiredSpace(&'static str),
49
50 Optional(&'static str),
53}
54
55macro_rules! flag {
58 ($(-$short:ident /)? --$($flag:tt)*) => {
69 LldFlag {
70 clap_name: concat!("long_", $(stringify!($flag),)*),
71 long: Some(flag!(@name [] $($flag)*)),
72 short: flag!(@short $($short)?),
73 value: flag!(@value $($flag)*),
74 }
75 };
76
77 (-$flag:tt $($val:tt)*) => {
79 LldFlag {
80 clap_name: concat!("short_", stringify!($flag)),
81 long: None,
82 short: Some(flag!(@char $flag)),
83 value: flag!(@value $flag $($val)*),
84 }
85 };
86
87 (@name [$($name:tt)*] $n:ident-$($rest:tt)*) => (flag!(@name [$($name)* $n-] $($rest)*));
95 (@name [$($name:tt)*] $n:ident $_value:ident) => (flag!(@name [$($name)* $n]));
99 (@name [$($name:tt)*] $n:ident=$_value:ident) => (flag!(@name [$($name)* $n]));
100 (@name [$($name:tt)*] $n:ident[=$_value:ident]) => (flag!(@name [$($name)* $n]));
101 (@name [$($name:tt)*] $n:ident) => (flag!(@name [$($name)* $n]));
102 (@name [$($name:tt)*]) => (concat!($(stringify!($name),)*));
105
106 (@value $n:ident - $($rest:tt)*) => (flag!(@value $($rest)*));
110 (@value $_flag:ident = $name:ident) => (FlagValue::RequiredEqual(stringify!($name)));
111 (@value $_flag:ident $name:ident) => (FlagValue::RequiredSpace(stringify!($name)));
112 (@value $_flag:ident [= $name:ident]) => (FlagValue::Optional(stringify!($name)));
113 (@value $_flag:ident) => (FlagValue::None);
114
115 (@short) => (None);
118 (@short $name:ident) => (Some(flag!(@char $name)));
119
120 (@char $name:ident) => ({
122 let name = stringify!($name);
123 assert!(name.len() == 1);
124 name.as_bytes()[0] as char
125 });
126}
127
128const LLD_FLAGS: &[LldFlag] = &[
129 flag! { --allow-multiple-definition },
130 flag! { --allow-undefined-file=PATH },
131 flag! { --allow-undefined },
132 flag! { --Bdynamic },
133 flag! { --Bstatic },
134 flag! { --Bsymbolic },
135 flag! { --build-id[=VAL] },
136 flag! { --call_shared },
137 flag! { --check-features },
138 flag! { --color-diagnostics[=VALUE] },
139 flag! { --compress-relocations },
140 flag! { --demangle },
141 flag! { --dn },
142 flag! { --dy },
143 flag! { --emit-relocs },
144 flag! { --end-lib },
145 flag! { --entry SYM },
146 flag! { --error-limit=N },
147 flag! { --error-unresolved-symbols },
148 flag! { --experimental-pic },
149 flag! { --export-all },
150 flag! { -E / --export-dynamic },
151 flag! { --export-if-defined=SYM },
152 flag! { --export-memory[=NAME] },
153 flag! { --export-table },
154 flag! { --export=SYM },
155 flag! { --extra-features=LIST },
156 flag! { --fatal-warnings },
157 flag! { --features=LIST },
158 flag! { --gc-sections },
159 flag! { --global-base=VALUE },
160 flag! { --growable-table },
161 flag! { --import-memory[=NAME] },
162 flag! { --import-table },
163 flag! { --import-undefined },
164 flag! { --initial-heap=SIZE },
165 flag! { --initial-memory=SIZE },
166 flag! { --keep-section=NAME },
167 flag! { --lto-CGO=LEVEL },
168 flag! { --lto-debug-pass-manager },
169 flag! { --lto-O=LEVEL },
170 flag! { --lto-partitions=NUM },
171 flag! { -L PATH },
172 flag! { -l LIB },
173 flag! { --Map=FILE },
174 flag! { --max-memory=SIZE },
175 flag! { --merge-data-segments },
176 flag! { --mllvm=FLAG },
177 flag! { -m ARCH },
178 flag! { --no-allow-multiple-definition },
179 flag! { --no-check-features },
180 flag! { --no-color-diagnostics },
181 flag! { --no-demangle },
182 flag! { --no-entry },
183 flag! { --no-export-dynamic },
184 flag! { --no-fatal-warnings },
185 flag! { --no-gc-sections },
186 flag! { --no-growable-memory },
187 flag! { --no-merge-data-segments },
188 flag! { --no-pie },
189 flag! { --no-print-gc-sections },
190 flag! { --no-shlib-sigcheck },
191 flag! { --no-whole-archive },
192 flag! { --noinhibit-exec },
193 flag! { --non_shared },
194 flag! { -O LEVEL },
195 flag! { --page-size=VALUE },
196 flag! { --pie },
197 flag! { --print-gc-sections },
198 flag! { -M / --print-map },
199 flag! { --relocatable },
200 flag! { --reproduce=VALUE },
201 flag! { --rpath=VALUE },
202 flag! { --save-temps },
203 flag! { --shared-memory },
204 flag! { --shared },
205 flag! { --soname=VALUE },
206 flag! { --stack-first },
207 flag! { --start-lib },
208 flag! { --static },
209 flag! { -s / --strip-all },
210 flag! { -S / --strip-debug },
211 flag! { --table-base=VALUE },
212 flag! { --thinlto-cache-dir=PATH },
213 flag! { --thinlto-cache-policy=VALUE },
214 flag! { --thinlto-jobs=N },
215 flag! { --threads=N },
216 flag! { -y / --trace-symbol=SYM },
217 flag! { -t / --trace },
218 flag! { --undefined=SYM },
219 flag! { --unresolved-symbols=VALUE },
220 flag! { --warn-unresolved-symbols },
221 flag! { --whole-archive },
222 flag! { --why-extract=MEMBER },
223 flag! { --wrap=VALUE },
224 flag! { -z OPT },
225];
226
227const LLD_LONG_FLAGS_NONSTANDARD: &[&str] = &["-shared"];
228
229#[derive(Default)]
230struct App {
231 component: ComponentLdArgs,
232 lld_args: Vec<OsString>,
233 shared: bool,
234}
235
236#[derive(clap::Parser, Default)]
245#[command(version)]
246struct ComponentLdArgs {
247 #[clap(long, name = "command|reactor|proxy|none")]
250 wasi_adapter: Option<WasiAdapter>,
251
252 #[clap(long, name = "PATH")]
256 wasm_ld_path: Option<PathBuf>,
257
258 #[clap(long, name = "STYLE")]
260 rsp_quoting: Option<String>,
261
262 #[clap(short, long)]
264 output: PathBuf,
265
266 #[clap(short, long)]
268 verbose: bool,
269
270 #[clap(long)]
274 validate_component: Option<bool>,
275
276 #[clap(long)]
281 merge_imports_based_on_semver: Option<bool>,
282
283 #[clap(long = "adapt", value_name = "[NAME=]MODULE", value_parser = parse_adapter)]
285 adapters: Vec<(String, Vec<u8>)>,
286
287 #[clap(long)]
295 reject_legacy_names: bool,
296
297 #[clap(long)]
306 realloc_via_memory_grow: bool,
307
308 #[clap(long = "component-type", value_name = "WIT_FILE")]
314 component_types: Vec<PathBuf>,
315
316 #[clap(long, value_parser = parse_encoding, default_value = "utf8")]
321 string_encoding: StringEncoding,
322
323 #[clap(long)]
325 skip_wit_component: bool,
326
327 #[clap(long)]
330 append_lld_flag: Vec<OsString>,
331}
332
333fn parse_adapter(s: &str) -> Result<(String, Vec<u8>)> {
334 let (name, path) = parse_optionally_name_file(s);
335 let wasm = wat::parse_file(path)?;
336 Ok((name.to_string(), wasm))
337}
338
339fn parse_encoding(s: &str) -> Result<StringEncoding> {
340 Ok(match s {
341 "utf8" => StringEncoding::UTF8,
342 "utf16" => StringEncoding::UTF16,
343 "compact-utf16" => StringEncoding::CompactUTF16,
344 _ => bail!("unknown string encoding: {s:?}"),
345 })
346}
347
348fn parse_optionally_name_file(s: &str) -> (&str, &str) {
349 let mut parts = s.splitn(2, '=');
350 let name_or_path = parts.next().unwrap();
351 match parts.next() {
352 Some(path) => (name_or_path, path),
353 None => {
354 let name = Path::new(name_or_path)
355 .file_name()
356 .unwrap()
357 .to_str()
358 .unwrap();
359 let name = match name.find('.') {
360 Some(i) => &name[..i],
361 None => name,
362 };
363 (name, name_or_path)
364 }
365 }
366}
367
368#[derive(Debug, Copy, Clone)]
369enum WasiAdapter {
370 Command,
371 Reactor,
372 Proxy,
373 None,
374}
375
376impl FromStr for WasiAdapter {
377 type Err = anyhow::Error;
378
379 fn from_str(s: &str) -> Result<Self, Self::Err> {
380 match s {
381 "none" => Ok(WasiAdapter::None),
382 "command" => Ok(WasiAdapter::Command),
383 "reactor" => Ok(WasiAdapter::Reactor),
384 "proxy" => Ok(WasiAdapter::Proxy),
385 _ => bail!("unknown wasi adapter {s}, must be one of: none, command, reactor, proxy"),
386 }
387 }
388}
389
390pub fn main() {
391 let err = match run() {
392 Ok(()) => return,
393 Err(e) => e,
394 };
395 eprintln!("error: {err}");
396 if err.chain().len() > 1 {
397 eprintln!("\nCaused by:");
398 for (i, err) in err.chain().skip(1).enumerate() {
399 eprintln!("{i:>5}: {}", err.to_string().replace("\n", "\n "));
400 }
401 }
402
403 std::process::exit(1);
404}
405
406fn run() -> Result<()> {
407 App::parse()?.run()
408}
409
410impl App {
411 fn parse() -> Result<App> {
431 let mut args = argfile::expand().context("failed to expand @-response files")?;
432
433 if let Some([flavor, wasm]) = args.get(1..3) {
436 if flavor == "-flavor" && wasm == "wasm" {
437 args.remove(1);
438 args.remove(1);
439 }
440 }
441
442 let mut command = ComponentLdArgs::command();
443 let mut lld_args = Vec::new();
444 let mut component_ld_args = vec![std::env::args_os().nth(0).unwrap()];
445 let mut shared = false;
446 let mut parser = lexopt::Parser::from_iter(args);
447
448 fn handle_lld_arg(
449 lld: &LldFlag,
450 parser: &mut lexopt::Parser,
451 lld_args: &mut Vec<OsString>,
452 ) -> Result<()> {
453 let mut arg = OsString::new();
454 match (lld.short, lld.long) {
455 (_, Some(long)) => {
456 arg.push("--");
457 arg.push(long);
458 }
459 (Some(short), _) => {
460 arg.push("-");
461 arg.push(short.encode_utf8(&mut [0; 5]));
462 }
463 (None, None) => unreachable!(),
464 }
465 match lld.value {
466 FlagValue::None => {
467 lld_args.push(arg);
468 }
469
470 FlagValue::RequiredSpace(_) => {
471 lld_args.push(arg);
472 lld_args.push(parser.value()?);
473 }
474
475 FlagValue::RequiredEqual(_) => {
476 arg.push("=");
477 arg.push(&parser.value()?);
478 lld_args.push(arg);
479 }
480
481 FlagValue::Optional(_) => {
484 match parser.optional_value() {
485 Some(val) => {
486 arg.push("=");
487 arg.push(&val);
488 }
489 None => {}
490 }
491 lld_args.push(arg);
492 }
493 }
494 Ok(())
495 }
496
497 loop {
498 if let Some(mut args) = parser.try_raw_args() {
499 if let Some(arg) = args.peek() {
500 let for_lld = LLD_LONG_FLAGS_NONSTANDARD.iter().any(|s| arg == *s);
501 if for_lld {
502 lld_args.push(arg.to_owned());
503 if arg == "-shared" {
504 shared = true;
505 }
506 args.next();
507 continue;
508 }
509 }
510 }
511
512 match parser.next()? {
513 Some(Arg::Value(obj)) => {
514 lld_args.push(obj);
515 }
516 Some(Arg::Short(c)) => match LLD_FLAGS.iter().find(|f| f.short == Some(c)) {
517 Some(lld) => {
518 handle_lld_arg(lld, &mut parser, &mut lld_args)?;
519 }
520 None => {
521 component_ld_args.push(format!("-{c}").into());
522 if let Some(arg) =
523 command.get_arguments().find(|a| a.get_short() == Some(c))
524 {
525 if let ArgAction::Set = arg.get_action() {
526 component_ld_args.push(parser.value()?);
527 }
528 }
529 }
530 },
531 Some(Arg::Long(c)) => match LLD_FLAGS.iter().find(|f| f.long == Some(c)) {
532 Some(lld) => {
533 handle_lld_arg(lld, &mut parser, &mut lld_args)?;
534 }
535 None => {
536 let mut flag = OsString::from(format!("--{c}"));
537 if let Some(arg) = command.get_arguments().find(|a| a.get_long() == Some(c))
538 {
539 match arg.get_action() {
540 ArgAction::Set | ArgAction::Append => {
541 flag.push("=");
542 flag.push(parser.value()?);
543 }
544 _ => (),
545 }
546 }
547 component_ld_args.push(flag);
548 }
549 },
550 None => break,
551 }
552 }
553
554 match command.try_get_matches_from_mut(component_ld_args.clone()) {
555 Ok(matches) => Ok(App {
556 component: ComponentLdArgs::from_arg_matches(&matches)?,
557 lld_args,
558 shared,
559 }),
560 Err(_) => {
561 add_wasm_ld_options(ComponentLdArgs::command()).get_matches_from(component_ld_args);
562 unreachable!();
563 }
564 }
565 }
566
567 fn run(&mut self) -> Result<()> {
568 let mut lld = self.lld();
569
570 let temp_dir = match self.component.output.parent() {
574 Some(parent) => tempfile::TempDir::new_in(parent)?,
575 None => tempfile::TempDir::new()?,
576 };
577 let temp_output = match self.component.output.file_name() {
578 Some(name) => temp_dir.path().join(name),
579 None => bail!(
580 "output of {:?} does not have a file name",
581 self.component.output
582 ),
583 };
584
585 if self.skip_wit_component() {
590 lld.output(&self.component.output);
591 } else {
592 lld.output(&temp_output);
593 }
594
595 let linker = &lld.exe;
596 let lld_flags = self
597 .lld_args
598 .iter()
599 .chain(&self.component.append_lld_flag)
600 .collect::<Vec<_>>();
601 let status = lld
602 .status(&temp_dir, &lld_flags)
603 .with_context(|| format!("failed to spawn {linker:?}"))?;
604 if !status.success() {
605 bail!("failed to invoke LLD: {status}");
606 }
607
608 if self.skip_wit_component() {
609 return Ok(());
610 }
611
612 let reactor_adapter =
613 wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_REACTOR_ADAPTER;
614 let command_adapter =
615 wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_COMMAND_ADAPTER;
616 let proxy_adapter =
617 wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_PROXY_ADAPTER;
618 let mut core_module = std::fs::read(&temp_output)
619 .with_context(|| format!("failed to read {linker:?} output: {temp_output:?}"))?;
620
621 let mut exports_start = false;
623 for payload in wasmparser::Parser::new(0).parse_all(&core_module) {
624 match payload {
625 Ok(Payload::ExportSection(e)) => {
626 for export in e {
627 if let Ok(e) = export {
628 if e.name == "_start" {
629 exports_start = true;
630 break;
631 }
632 }
633 }
634 }
635 _ => {}
636 }
637 }
638
639 if !self.component.component_types.is_empty() {
640 let mut merged = None::<(Resolve, WorldId)>;
641 for wit_file in &self.component.component_types {
642 let mut resolve = Resolve::default();
643 let (package, _) = resolve
644 .push_path(wit_file)
645 .with_context(|| format!("unable to add component type {wit_file:?}"))?;
646
647 let world = resolve.select_world(&[package], None)?;
648
649 if let Some((merged_resolve, merged_world)) = &mut merged {
650 let world = merged_resolve.merge(resolve)?.map_world(world, None)?;
651 merged_resolve.merge_worlds(world, *merged_world)?;
652 } else {
653 merged = Some((resolve, world));
654 }
655 }
656
657 let Some((resolve, world)) = merged else {
658 unreachable!()
659 };
660
661 wit_component::embed_component_metadata(
662 &mut core_module,
663 &resolve,
664 world,
665 self.component.string_encoding,
666 )?;
667 }
668
669 let mut encoder = wit_component::ComponentEncoder::default()
670 .reject_legacy_names(self.component.reject_legacy_names)
671 .realloc_via_memory_grow(self.component.realloc_via_memory_grow);
672 if let Some(validate) = self.component.validate_component {
673 encoder = encoder.validate(validate);
674 }
675 if let Some(merge) = self.component.merge_imports_based_on_semver {
676 encoder = encoder.merge_imports_based_on_semver(merge);
677 }
678 encoder = encoder
679 .module(&core_module)
680 .context("failed to parse core wasm for componentization")?;
681 let adapter = self.component.wasi_adapter.unwrap_or(if exports_start {
682 WasiAdapter::Command
683 } else {
684 WasiAdapter::Reactor
685 });
686 let adapter = match adapter {
687 WasiAdapter::Command => Some(&command_adapter[..]),
688 WasiAdapter::Reactor => Some(&reactor_adapter[..]),
689 WasiAdapter::Proxy => Some(&proxy_adapter[..]),
690 WasiAdapter::None => None,
691 };
692
693 if let Some(adapter) = adapter {
694 encoder = encoder
695 .adapter("wasi_snapshot_preview1", adapter)
696 .context("failed to inject adapter")?;
697 }
698
699 for (name, adapter) in self.component.adapters.iter() {
700 encoder = encoder
701 .adapter(name, adapter)
702 .with_context(|| format!("failed to inject adapter {name:?}"))?;
703 }
704
705 let component = encoder.encode().context("failed to encode component")?;
706
707 std::fs::write(&self.component.output, &component).context(format!(
708 "failed to write output file: {:?}",
709 self.component.output
710 ))?;
711
712 Ok(())
713 }
714
715 fn skip_wit_component(&self) -> bool {
716 self.component.skip_wit_component
717 || self.shared
720 }
721
722 fn lld(&self) -> Lld {
723 let mut lld = self.find_lld();
724 if self.component.verbose {
725 lld.verbose = true
726 }
727 lld
728 }
729
730 fn find_lld(&self) -> Lld {
731 if let Some(path) = &self.component.wasm_ld_path {
732 return Lld::new(path);
733 }
734
735 let wasm_ld = format!("wasm-ld{}", env::consts::EXE_SUFFIX);
737 let rust_lld = format!("rust-lld{}", env::consts::EXE_SUFFIX);
738 for entry in env::split_paths(&env::var_os("PATH").unwrap_or_default()) {
739 if entry.join(&wasm_ld).is_file() {
740 return Lld::new(wasm_ld);
741 }
742 if entry.join(&rust_lld).is_file() {
743 let mut lld = Lld::new(rust_lld);
744 lld.needs_flavor = true;
745 return lld;
746 }
747 }
748
749 Lld::new("wasm-ld")
753 }
754}
755
756struct Lld {
758 exe: PathBuf,
759 needs_flavor: bool,
760 verbose: bool,
761 output: Option<PathBuf>,
762}
763
764impl Lld {
765 fn new(exe: impl Into<PathBuf>) -> Lld {
766 Lld {
767 exe: exe.into(),
768 needs_flavor: false,
769 verbose: false,
770 output: None,
771 }
772 }
773
774 fn output(&mut self, dst: impl Into<PathBuf>) {
775 self.output = Some(dst.into());
776 }
777
778 fn status(&self, tmpdir: &tempfile::TempDir, args: &[&OsString]) -> Result<ExitStatus> {
779 if !self.probably_too_big(args) {
782 match self.run(args) {
783 Err(ref e) if self.command_line_too_big(e) => {
786 if self.verbose {
787 eprintln!("command line was too large, trying again...");
788 }
789 }
790 other => return Ok(other?),
791 }
792 } else if self.verbose {
793 eprintln!("arguments probably too large {args:?}");
794 }
795
796 let mut argfile = Vec::new();
803 for arg in args {
804 for byte in arg.as_encoded_bytes() {
805 if *byte == b'\\' || *byte == b' ' {
806 argfile.push(b'\\');
807 }
808 argfile.push(*byte);
809 }
810 argfile.push(b'\n');
811 }
812 let path = tmpdir.path().join("argfile_tmp");
813 std::fs::write(&path, &argfile).with_context(|| format!("failed to write {path:?}"))?;
814 let mut argfile_arg = OsString::from("@");
815 argfile_arg.push(&path);
816 let status = self.run(&[&"--rsp-quoting=posix".into(), &argfile_arg])?;
817 Ok(status)
818 }
819
820 fn probably_too_big(&self, args: &[&OsString]) -> bool {
825 let args_size = args
826 .iter()
827 .map(|s| s.as_encoded_bytes().len())
828 .sum::<usize>();
829 cfg!(windows) && args_size > 6 * 1024
830 }
831
832 fn command_line_too_big(&self, err: &std::io::Error) -> bool {
835 #[cfg(unix)]
836 return err.raw_os_error() == Some(libc::E2BIG);
837 #[cfg(windows)]
838 return err.raw_os_error()
839 == Some(windows_sys::Win32::Foundation::ERROR_FILENAME_EXCED_RANGE as i32);
840 #[cfg(not(any(unix, windows)))]
841 {
842 let _ = err;
843 return false;
844 }
845 }
846
847 fn run(&self, args: &[&OsString]) -> std::io::Result<ExitStatus> {
848 let mut cmd = Command::new(&self.exe);
849 if self.needs_flavor {
850 cmd.arg("-flavor").arg("wasm");
851 }
852 cmd.args(args);
853 if self.verbose {
854 cmd.arg("--verbose");
855 }
856 if let Some(output) = &self.output {
857 cmd.arg("-o").arg(output);
858 }
859 if self.verbose {
860 eprintln!("running {cmd:?}");
861 }
862 cmd.status()
863 }
864}
865
866fn add_wasm_ld_options(mut command: clap::Command) -> clap::Command {
867 use clap::Arg;
868
869 command = command.arg(
870 Arg::new("objects")
871 .action(ArgAction::Append)
872 .help("objects to pass to `wasm-ld`"),
873 );
874
875 for flag in LLD_FLAGS {
876 let mut arg = Arg::new(flag.clap_name).help("forwarded to `wasm-ld`");
877 if let Some(short) = flag.short {
878 arg = arg.short(short);
879 }
880 if let Some(long) = flag.long {
881 arg = arg.long(long);
882 }
883 arg = match flag.value {
884 FlagValue::RequiredEqual(name) | FlagValue::RequiredSpace(name) => {
885 arg.action(ArgAction::Set).value_name(name)
886 }
887 FlagValue::Optional(name) => arg
888 .action(ArgAction::Set)
889 .value_name(name)
890 .num_args(0..=1)
891 .require_equals(true),
892 FlagValue::None => arg.action(ArgAction::SetTrue),
893 };
894 arg = arg.help_heading("Options forwarded to `wasm-ld`");
895 command = command.arg(arg);
896 }
897
898 command
899}
900
901#[test]
902fn verify_app() {
903 ComponentLdArgs::command().debug_assert();
904 add_wasm_ld_options(ComponentLdArgs::command()).debug_assert();
905}