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