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