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-whole-archive },
191 flag! { --noinhibit-exec },
192 flag! { --non_shared },
193 flag! { -O LEVEL },
194 flag! { --page-size=VALUE },
195 flag! { --pie },
196 flag! { --print-gc-sections },
197 flag! { -M / --print-map },
198 flag! { --relocatable },
199 flag! { --reproduce=VALUE },
200 flag! { --rpath=VALUE },
201 flag! { --save-temps },
202 flag! { --shared-memory },
203 flag! { --shared },
204 flag! { --soname=VALUE },
205 flag! { --stack-first },
206 flag! { --start-lib },
207 flag! { --static },
208 flag! { -s / --strip-all },
209 flag! { -S / --strip-debug },
210 flag! { --table-base=VALUE },
211 flag! { --thinlto-cache-dir=PATH },
212 flag! { --thinlto-cache-policy=VALUE },
213 flag! { --thinlto-jobs=N },
214 flag! { --threads=N },
215 flag! { -y / --trace-symbol=SYM },
216 flag! { -t / --trace },
217 flag! { --undefined=SYM },
218 flag! { --unresolved-symbols=VALUE },
219 flag! { --warn-unresolved-symbols },
220 flag! { --whole-archive },
221 flag! { --why-extract=MEMBER },
222 flag! { --wrap=VALUE },
223 flag! { -z OPT },
224];
225
226const LLD_LONG_FLAGS_NONSTANDARD: &[&str] = &["-shared"];
227
228#[derive(Default)]
229struct App {
230 component: ComponentLdArgs,
231 lld_args: Vec<OsString>,
232 shared: bool,
233}
234
235#[derive(clap::Parser, Default)]
244#[command(version)]
245struct ComponentLdArgs {
246 #[clap(long, name = "command|reactor|proxy|none")]
249 wasi_adapter: Option<WasiAdapter>,
250
251 #[clap(long, name = "PATH")]
255 wasm_ld_path: Option<PathBuf>,
256
257 #[clap(long, name = "STYLE")]
259 rsp_quoting: Option<String>,
260
261 #[clap(short, long)]
263 output: PathBuf,
264
265 #[clap(short, long)]
267 verbose: bool,
268
269 #[clap(long)]
273 validate_component: Option<bool>,
274
275 #[clap(long)]
280 merge_imports_based_on_semver: Option<bool>,
281
282 #[clap(long = "adapt", value_name = "[NAME=]MODULE", value_parser = parse_adapter)]
284 adapters: Vec<(String, Vec<u8>)>,
285
286 #[clap(long)]
294 reject_legacy_names: bool,
295
296 #[clap(long)]
305 realloc_via_memory_grow: bool,
306
307 #[clap(long = "component-type", value_name = "WIT_FILE")]
313 component_types: Vec<PathBuf>,
314
315 #[clap(long, value_parser = parse_encoding, default_value = "utf8")]
320 string_encoding: StringEncoding,
321
322 #[clap(long)]
324 skip_wit_component: bool,
325
326 #[clap(long)]
329 append_lld_flag: Vec<OsString>,
330}
331
332fn parse_adapter(s: &str) -> Result<(String, Vec<u8>)> {
333 let (name, path) = parse_optionally_name_file(s);
334 let wasm = wat::parse_file(path)?;
335 Ok((name.to_string(), wasm))
336}
337
338fn parse_encoding(s: &str) -> Result<StringEncoding> {
339 Ok(match s {
340 "utf8" => StringEncoding::UTF8,
341 "utf16" => StringEncoding::UTF16,
342 "compact-utf16" => StringEncoding::CompactUTF16,
343 _ => bail!("unknown string encoding: {s:?}"),
344 })
345}
346
347fn parse_optionally_name_file(s: &str) -> (&str, &str) {
348 let mut parts = s.splitn(2, '=');
349 let name_or_path = parts.next().unwrap();
350 match parts.next() {
351 Some(path) => (name_or_path, path),
352 None => {
353 let name = Path::new(name_or_path)
354 .file_name()
355 .unwrap()
356 .to_str()
357 .unwrap();
358 let name = match name.find('.') {
359 Some(i) => &name[..i],
360 None => name,
361 };
362 (name, name_or_path)
363 }
364 }
365}
366
367#[derive(Debug, Copy, Clone)]
368enum WasiAdapter {
369 Command,
370 Reactor,
371 Proxy,
372 None,
373}
374
375impl FromStr for WasiAdapter {
376 type Err = anyhow::Error;
377
378 fn from_str(s: &str) -> Result<Self, Self::Err> {
379 match s {
380 "none" => Ok(WasiAdapter::None),
381 "command" => Ok(WasiAdapter::Command),
382 "reactor" => Ok(WasiAdapter::Reactor),
383 "proxy" => Ok(WasiAdapter::Proxy),
384 _ => bail!("unknown wasi adapter {s}, must be one of: none, command, reactor, proxy"),
385 }
386 }
387}
388
389pub fn main() {
390 let err = match run() {
391 Ok(()) => return,
392 Err(e) => e,
393 };
394 eprintln!("error: {err}");
395 if err.chain().len() > 1 {
396 eprintln!("\nCaused by:");
397 for (i, err) in err.chain().skip(1).enumerate() {
398 eprintln!("{i:>5}: {}", err.to_string().replace("\n", "\n "));
399 }
400 }
401
402 std::process::exit(1);
403}
404
405fn run() -> Result<()> {
406 App::parse()?.run()
407}
408
409impl App {
410 fn parse() -> Result<App> {
430 let mut args = argfile::expand().context("failed to expand @-response files")?;
431
432 if let Some([flavor, wasm]) = args.get(1..3) {
435 if flavor == "-flavor" && wasm == "wasm" {
436 args.remove(1);
437 args.remove(1);
438 }
439 }
440
441 let mut command = ComponentLdArgs::command();
442 let mut lld_args = Vec::new();
443 let mut component_ld_args = vec![std::env::args_os().nth(0).unwrap()];
444 let mut shared = false;
445 let mut parser = lexopt::Parser::from_iter(args);
446
447 fn handle_lld_arg(
448 lld: &LldFlag,
449 parser: &mut lexopt::Parser,
450 lld_args: &mut Vec<OsString>,
451 ) -> Result<()> {
452 let mut arg = OsString::new();
453 match (lld.short, lld.long) {
454 (_, Some(long)) => {
455 arg.push("--");
456 arg.push(long);
457 }
458 (Some(short), _) => {
459 arg.push("-");
460 arg.push(short.encode_utf8(&mut [0; 5]));
461 }
462 (None, None) => unreachable!(),
463 }
464 match lld.value {
465 FlagValue::None => {
466 lld_args.push(arg);
467 }
468
469 FlagValue::RequiredSpace(_) => {
470 lld_args.push(arg);
471 lld_args.push(parser.value()?);
472 }
473
474 FlagValue::RequiredEqual(_) => {
475 arg.push("=");
476 arg.push(&parser.value()?);
477 lld_args.push(arg);
478 }
479
480 FlagValue::Optional(_) => {
483 match parser.optional_value() {
484 Some(val) => {
485 arg.push("=");
486 arg.push(&val);
487 }
488 None => {}
489 }
490 lld_args.push(arg);
491 }
492 }
493 Ok(())
494 }
495
496 loop {
497 if let Some(mut args) = parser.try_raw_args() {
498 if let Some(arg) = args.peek() {
499 let for_lld = LLD_LONG_FLAGS_NONSTANDARD.iter().any(|s| arg == *s);
500 if for_lld {
501 lld_args.push(arg.to_owned());
502 if arg == "-shared" {
503 shared = true;
504 }
505 args.next();
506 continue;
507 }
508 }
509 }
510
511 match parser.next()? {
512 Some(Arg::Value(obj)) => {
513 lld_args.push(obj);
514 }
515 Some(Arg::Short(c)) => match LLD_FLAGS.iter().find(|f| f.short == Some(c)) {
516 Some(lld) => {
517 handle_lld_arg(lld, &mut parser, &mut lld_args)?;
518 }
519 None => {
520 component_ld_args.push(format!("-{c}").into());
521 if let Some(arg) =
522 command.get_arguments().find(|a| a.get_short() == Some(c))
523 {
524 if let ArgAction::Set = arg.get_action() {
525 component_ld_args.push(parser.value()?);
526 }
527 }
528 }
529 },
530 Some(Arg::Long(c)) => match LLD_FLAGS.iter().find(|f| f.long == Some(c)) {
531 Some(lld) => {
532 handle_lld_arg(lld, &mut parser, &mut lld_args)?;
533 }
534 None => {
535 let mut flag = OsString::from(format!("--{c}"));
536 if let Some(arg) = command.get_arguments().find(|a| a.get_long() == Some(c))
537 {
538 match arg.get_action() {
539 ArgAction::Set | ArgAction::Append => {
540 flag.push("=");
541 flag.push(parser.value()?);
542 }
543 _ => (),
544 }
545 }
546 component_ld_args.push(flag);
547 }
548 },
549 None => break,
550 }
551 }
552
553 match command.try_get_matches_from_mut(component_ld_args.clone()) {
554 Ok(matches) => Ok(App {
555 component: ComponentLdArgs::from_arg_matches(&matches)?,
556 lld_args,
557 shared,
558 }),
559 Err(_) => {
560 add_wasm_ld_options(ComponentLdArgs::command()).get_matches_from(component_ld_args);
561 unreachable!();
562 }
563 }
564 }
565
566 fn run(&mut self) -> Result<()> {
567 let mut lld = self.lld();
568
569 let temp_dir = match self.component.output.parent() {
573 Some(parent) => tempfile::TempDir::new_in(parent)?,
574 None => tempfile::TempDir::new()?,
575 };
576 let temp_output = match self.component.output.file_name() {
577 Some(name) => temp_dir.path().join(name),
578 None => bail!(
579 "output of {:?} does not have a file name",
580 self.component.output
581 ),
582 };
583
584 if self.skip_wit_component() {
589 lld.output(&self.component.output);
590 } else {
591 lld.output(&temp_output);
592 }
593
594 let linker = &lld.exe;
595 let lld_flags = self
596 .lld_args
597 .iter()
598 .chain(&self.component.append_lld_flag)
599 .collect::<Vec<_>>();
600 let status = lld
601 .status(&temp_dir, &lld_flags)
602 .with_context(|| format!("failed to spawn {linker:?}"))?;
603 if !status.success() {
604 bail!("failed to invoke LLD: {status}");
605 }
606
607 if self.skip_wit_component() {
608 return Ok(());
609 }
610
611 let reactor_adapter =
612 wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_REACTOR_ADAPTER;
613 let command_adapter =
614 wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_COMMAND_ADAPTER;
615 let proxy_adapter =
616 wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_PROXY_ADAPTER;
617 let mut core_module = std::fs::read(&temp_output)
618 .with_context(|| format!("failed to read {linker:?} output: {temp_output:?}"))?;
619
620 let mut exports_start = false;
622 for payload in wasmparser::Parser::new(0).parse_all(&core_module) {
623 match payload {
624 Ok(Payload::ExportSection(e)) => {
625 for export in e {
626 if let Ok(e) = export {
627 if e.name == "_start" {
628 exports_start = true;
629 break;
630 }
631 }
632 }
633 }
634 _ => {}
635 }
636 }
637
638 if !self.component.component_types.is_empty() {
639 let mut merged = None::<(Resolve, WorldId)>;
640 for wit_file in &self.component.component_types {
641 let mut resolve = Resolve::default();
642 let (package, _) = resolve
643 .push_path(wit_file)
644 .with_context(|| format!("unable to add component type {wit_file:?}"))?;
645
646 let world = resolve.select_world(package, None)?;
647
648 if let Some((merged_resolve, merged_world)) = &mut merged {
649 let world = merged_resolve.merge(resolve)?.map_world(world, None)?;
650 merged_resolve.merge_worlds(world, *merged_world)?;
651 } else {
652 merged = Some((resolve, world));
653 }
654 }
655
656 let Some((resolve, world)) = merged else {
657 unreachable!()
658 };
659
660 wit_component::embed_component_metadata(
661 &mut core_module,
662 &resolve,
663 world,
664 self.component.string_encoding,
665 )?;
666 }
667
668 let mut encoder = wit_component::ComponentEncoder::default()
669 .reject_legacy_names(self.component.reject_legacy_names)
670 .realloc_via_memory_grow(self.component.realloc_via_memory_grow);
671 if let Some(validate) = self.component.validate_component {
672 encoder = encoder.validate(validate);
673 }
674 if let Some(merge) = self.component.merge_imports_based_on_semver {
675 encoder = encoder.merge_imports_based_on_semver(merge);
676 }
677 encoder = encoder
678 .module(&core_module)
679 .context("failed to parse core wasm for componentization")?;
680 let adapter = self.component.wasi_adapter.unwrap_or(if exports_start {
681 WasiAdapter::Command
682 } else {
683 WasiAdapter::Reactor
684 });
685 let adapter = match adapter {
686 WasiAdapter::Command => Some(&command_adapter[..]),
687 WasiAdapter::Reactor => Some(&reactor_adapter[..]),
688 WasiAdapter::Proxy => Some(&proxy_adapter[..]),
689 WasiAdapter::None => None,
690 };
691
692 if let Some(adapter) = adapter {
693 encoder = encoder
694 .adapter("wasi_snapshot_preview1", adapter)
695 .context("failed to inject adapter")?;
696 }
697
698 for (name, adapter) in self.component.adapters.iter() {
699 encoder = encoder
700 .adapter(name, adapter)
701 .with_context(|| format!("failed to inject adapter {name:?}"))?;
702 }
703
704 let component = encoder.encode().context("failed to encode component")?;
705
706 std::fs::write(&self.component.output, &component).context(format!(
707 "failed to write output file: {:?}",
708 self.component.output
709 ))?;
710
711 Ok(())
712 }
713
714 fn skip_wit_component(&self) -> bool {
715 self.component.skip_wit_component
716 || self.shared
719 }
720
721 fn lld(&self) -> Lld {
722 let mut lld = self.find_lld();
723 if self.component.verbose {
724 lld.verbose = true
725 }
726 lld
727 }
728
729 fn find_lld(&self) -> Lld {
730 if let Some(path) = &self.component.wasm_ld_path {
731 return Lld::new(path);
732 }
733
734 let wasm_ld = format!("wasm-ld{}", env::consts::EXE_SUFFIX);
736 let rust_lld = format!("rust-lld{}", env::consts::EXE_SUFFIX);
737 for entry in env::split_paths(&env::var_os("PATH").unwrap_or_default()) {
738 if entry.join(&wasm_ld).is_file() {
739 return Lld::new(wasm_ld);
740 }
741 if entry.join(&rust_lld).is_file() {
742 let mut lld = Lld::new(rust_lld);
743 lld.needs_flavor = true;
744 return lld;
745 }
746 }
747
748 Lld::new("wasm-ld")
752 }
753}
754
755struct Lld {
757 exe: PathBuf,
758 needs_flavor: bool,
759 verbose: bool,
760 output: Option<PathBuf>,
761}
762
763impl Lld {
764 fn new(exe: impl Into<PathBuf>) -> Lld {
765 Lld {
766 exe: exe.into(),
767 needs_flavor: false,
768 verbose: false,
769 output: None,
770 }
771 }
772
773 fn output(&mut self, dst: impl Into<PathBuf>) {
774 self.output = Some(dst.into());
775 }
776
777 fn status(&self, tmpdir: &tempfile::TempDir, args: &[&OsString]) -> Result<ExitStatus> {
778 if !self.probably_too_big(args) {
781 match self.run(args) {
782 Err(ref e) if self.command_line_too_big(e) => {
785 if self.verbose {
786 eprintln!("command line was too large, trying again...");
787 }
788 }
789 other => return Ok(other?),
790 }
791 } else if self.verbose {
792 eprintln!("arguments probably too large {args:?}");
793 }
794
795 let mut argfile = Vec::new();
802 for arg in args {
803 for byte in arg.as_encoded_bytes() {
804 if *byte == b'\\' || *byte == b' ' {
805 argfile.push(b'\\');
806 }
807 argfile.push(*byte);
808 }
809 argfile.push(b'\n');
810 }
811 let path = tmpdir.path().join("argfile_tmp");
812 std::fs::write(&path, &argfile).with_context(|| format!("failed to write {path:?}"))?;
813 let mut argfile_arg = OsString::from("@");
814 argfile_arg.push(&path);
815 let status = self.run(&[&"--rsp-quoting=posix".into(), &argfile_arg])?;
816 Ok(status)
817 }
818
819 fn probably_too_big(&self, args: &[&OsString]) -> bool {
824 let args_size = args
825 .iter()
826 .map(|s| s.as_encoded_bytes().len())
827 .sum::<usize>();
828 cfg!(windows) && args_size > 6 * 1024
829 }
830
831 fn command_line_too_big(&self, err: &std::io::Error) -> bool {
834 #[cfg(unix)]
835 return err.raw_os_error() == Some(libc::E2BIG);
836 #[cfg(windows)]
837 return err.raw_os_error()
838 == Some(windows_sys::Win32::Foundation::ERROR_FILENAME_EXCED_RANGE as i32);
839 #[cfg(not(any(unix, windows)))]
840 {
841 let _ = err;
842 return false;
843 }
844 }
845
846 fn run(&self, args: &[&OsString]) -> std::io::Result<ExitStatus> {
847 let mut cmd = Command::new(&self.exe);
848 if self.needs_flavor {
849 cmd.arg("-flavor").arg("wasm");
850 }
851 cmd.args(args);
852 if self.verbose {
853 cmd.arg("--verbose");
854 }
855 if let Some(output) = &self.output {
856 cmd.arg("-o").arg(output);
857 }
858 if self.verbose {
859 eprintln!("running {cmd:?}");
860 }
861 cmd.status()
862 }
863}
864
865fn add_wasm_ld_options(mut command: clap::Command) -> clap::Command {
866 use clap::Arg;
867
868 command = command.arg(
869 Arg::new("objects")
870 .action(ArgAction::Append)
871 .help("objects to pass to `wasm-ld`"),
872 );
873
874 for flag in LLD_FLAGS {
875 let mut arg = Arg::new(flag.clap_name).help("forwarded to `wasm-ld`");
876 if let Some(short) = flag.short {
877 arg = arg.short(short);
878 }
879 if let Some(long) = flag.long {
880 arg = arg.long(long);
881 }
882 arg = match flag.value {
883 FlagValue::RequiredEqual(name) | FlagValue::RequiredSpace(name) => {
884 arg.action(ArgAction::Set).value_name(name)
885 }
886 FlagValue::Optional(name) => arg
887 .action(ArgAction::Set)
888 .value_name(name)
889 .num_args(0..=1)
890 .require_equals(true),
891 FlagValue::None => arg.action(ArgAction::SetTrue),
892 };
893 arg = arg.help_heading("Options forwarded to `wasm-ld`");
894 command = command.arg(arg);
895 }
896
897 command
898}
899
900#[test]
901fn verify_app() {
902 ComponentLdArgs::command().debug_assert();
903 add_wasm_ld_options(ComponentLdArgs::command()).debug_assert();
904}