1use crate::cache::Cache;
2use crate::config::Config;
3use crate::utils::CommandExt;
4use anyhow::{bail, Context, Result};
5use std::env;
6use std::fs;
7use std::io;
8use std::io::Read;
9use std::path::{Path, PathBuf};
10use std::process::{Command, Stdio};
11use std::time::Duration;
12use tool_path::ToolPath;
13
14mod cache;
15mod config;
16mod dependencies;
17mod internal;
18mod tool_path;
19mod toolchain;
20mod utils;
21
22const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(30);
24
25pub fn main() {
26 if env::var("__CARGO_WASIX_RUNNER_SHIM").is_ok() {
28 let args = env::args().skip(1).collect();
29 println!(
30 "{}",
31 serde_json::to_string(&CargoMessage::RunWithArgs { args }).unwrap(),
32 );
33 return;
34 }
35
36 let mut config = Config::new();
37 match rmain(&mut config) {
38 Ok(()) => {}
39 Err(e) => {
40 config.print_error(&e);
41 std::process::exit(1);
42 }
43 }
44}
45
46#[derive(Debug)]
47enum Subcommand {
48 Build,
49 DownloadToolchain,
50 Run,
51 Test,
52 Bench,
53 Check,
54 Tree,
55 Fix,
56}
57
58fn rmain(config: &mut Config) -> Result<()> {
59 config.load_cache()?;
60
61 let mut no_message_format = false;
63 let mut args = env::args_os().skip(2);
64
65 let subcommand = args.next().and_then(|s| s.into_string().ok());
66 let subcommand = match subcommand.as_deref() {
67 Some("build") => Subcommand::Build,
68 Some("download-toolchain") => Subcommand::DownloadToolchain,
69 Some("run") => Subcommand::Run,
70 Some("test") => Subcommand::Test,
71 Some("bench") => Subcommand::Bench,
72 Some("check") => Subcommand::Check,
73 Some("tree") => {
74 no_message_format = true;
75 Subcommand::Tree
76 }
77 Some("fix") => Subcommand::Fix,
78 Some("self") => return internal::main(&args.collect::<Vec<_>>(), config),
79 Some("version") | Some("-V") | Some("--version") => {
80 let git_info = match option_env!("GIT_INFO") {
81 Some(s) => format!(" ({})", s),
82 None => String::new(),
83 };
84 println!("cargo-wasix {}{}", env!("CARGO_PKG_VERSION"), git_info);
85 std::process::exit(0)
86 }
87 _ => print_help(),
88 };
89
90 let mut cargo = Command::new("cargo");
91 cargo.arg("+wasix");
92 cargo.arg(match subcommand {
93 Subcommand::Build => "build",
94 Subcommand::DownloadToolchain => "download-toolchain",
95 Subcommand::Check => "check",
96 Subcommand::Fix => "fix",
97 Subcommand::Test => "test",
98 Subcommand::Tree => "tree",
99 Subcommand::Bench => "bench",
100 Subcommand::Run => "run",
101 });
102
103 let manifest_config = if matches!(subcommand, Subcommand::DownloadToolchain) {
104 Default::default()
105 } else {
106 read_manifest_config()?
107 };
108
109 let target = if manifest_config.dl.unwrap_or(false) {
110 "wasm32-wasmer-wasi-dl"
111 } else {
112 "wasm32-wasmer-wasi"
113 };
114
115 cargo.arg("--target").arg(target);
116 if !no_message_format {
117 cargo.arg("--message-format").arg("json-render-diagnostics");
118 }
119
120 let args = args.collect::<Vec<_>>();
121 for arg in args.clone() {
122 if let Some(arg) = arg.to_str() {
123 if arg.starts_with("--verbose") || arg.starts_with("-v") {
124 config.set_verbose(true);
125 }
126 }
127
128 cargo.arg(arg);
129 }
130
131 let runner_env_var = format!(
132 "CARGO_TARGET_{}_RUNNER",
133 target.to_uppercase().replace('-', "_")
134 );
135
136 let (wasix_runner, using_default) = env::var(&runner_env_var)
153 .map(|runner_override| (runner_override, false))
154 .unwrap_or_else(|_| ("wasmer".to_string(), true));
155
156 let mut check_deps = false;
157 match subcommand {
158 Subcommand::DownloadToolchain => {
159 let version = args
160 .first()
161 .cloned()
162 .map(|v| v.into_string().unwrap().into())
163 .unwrap_or(toolchain::ToolchainSpec::Latest);
164 let _lock = Config::acquire_lock()?;
165 let chain = toolchain::install_prebuilt_toolchain(&Config::toolchain_dir()?, version)?;
166 config.info(&format!(
167 "Toolchain {} downloaded and installed to path {}.\nThe wasix toolchain is now ready to use.",
168 chain.name,
169 chain.path.display(),
170 ));
171 return Ok(());
172 }
173 Subcommand::Run | Subcommand::Bench | Subcommand::Test => {
174 check_deps = true;
175 if !using_default {
176 if !(Path::new(&wasix_runner).exists() || which::which(&wasix_runner).is_ok()) {
178 bail!(
179 "failed to find `{}` (specified by ${runner_env_var}) \
180 on the filesytem or in $PATH, you'll want to fix the path or unset \
181 the ${runner_env_var} environment variable before \
182 running this command\n",
183 &wasix_runner
184 );
185 }
186 } else if which::which(&wasix_runner).is_err() {
187 let mut msg = format!(
188 "failed to find `{}` in $PATH, you'll want to \
189 install `{}` before running this command\n",
190 wasix_runner, wasix_runner
191 );
192 msg.push_str("you can also install through a shell:\n\n");
195 msg.push_str("\tcurl https://get.wasmer.io -sSfL | sh\n");
196 bail!("{}", msg);
197 }
198 cargo.env("__CARGO_WASIX_RUNNER_SHIM", "1");
199 cargo.env(runner_env_var, env::current_exe()?);
200 }
201 Subcommand::Build | Subcommand::Check => check_deps = true,
202 Subcommand::Tree | Subcommand::Fix => {}
203 }
204
205 let update_check_opt = if config.is_offline {
206 Some(internal::UpdateCheck::new(config))
207 } else {
208 None
209 };
210 let toolchain = toolchain::ensure_toolchain(config)?;
211
212 std::env::set_var("RUSTUP_TOOLCHAIN", &toolchain.name);
213
214 if std::env::var("RUSTFLAGS").is_err() {
216 env::set_var("RUSTFLAGS", "-C target-feature=+atomics");
217 }
218
219 if check_deps {
221 if let Err(err) = dependencies::check(config, target) {
222 config.warn(&format!("failed to check dependencies: {err}"));
223 }
224 }
225
226 let build = execute_cargo(&mut cargo, config, manifest_config)?;
228
229 config.info("Post-processing WebAssembly files");
230
231 for (wasm, profile, fresh) in build.wasms.iter() {
232 let temporary_rustc = wasm.with_extension("rustc.wasm");
245 let temporary_wasi = wasm.with_extension("wasi.wasm");
246
247 drop(fs::remove_file(&temporary_rustc));
248 fs::rename(wasm, &temporary_rustc)?;
249 if !*fresh || !temporary_wasi.exists() {
250 let result = process_wasm(&temporary_wasi, &temporary_rustc, profile, &build, config);
251 result.with_context(|| {
252 format!("failed to process wasm at `{}`", temporary_rustc.display())
253 })?;
254 }
255 drop(fs::remove_file(wasm));
256 fs::hard_link(&temporary_wasi, wasm)
257 .or_else(|_| fs::copy(&temporary_wasi, wasm).map(|_| ()))?;
258 }
259
260 for run in build.runs.iter() {
261 config.status("Running", &format!("`{}`", run.join(" ")));
262 let mut cmd = Command::new(&wasix_runner);
263
264 if wasix_runner == "wasmer" {
265 cmd.arg("--enable-threads");
266 }
267
268 cmd.arg("--")
269 .args(run.iter())
270 .run()
271 .map_err(|e| utils::hide_normal_process_exit(e, config))?;
272 }
273
274 if let Some(check) = update_check_opt {
275 check.print();
276 }
277 Ok(())
278}
279
280pub const HELP: &str = include_str!("txt/help.txt");
281
282fn print_help() -> ! {
283 println!("{}", HELP);
284 std::process::exit(0)
285}
286
287#[derive(Default, Debug)]
288struct CargoBuild {
289 wasm_bindgen: Option<String>,
291 wasms: Vec<(PathBuf, Profile, bool)>,
295 runs: Vec<Vec<String>>,
297 manifest_config: ManifestConfig,
300}
301
302#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
303struct Profile {
304 opt_level: String,
305 debuginfo: Option<u32>,
306 test: bool,
307}
308
309#[derive(serde::Deserialize, Debug, Default)]
310#[serde(rename_all = "kebab-case")]
311struct ManifestConfig {
312 dl: Option<bool>,
313 wasm_opt: Option<bool>,
314 wasm_name_section: Option<bool>,
315 wasm_producers_section: Option<bool>,
316}
317
318#[derive(serde::Deserialize)]
319struct CargoMetadata {
320 workspace_root: String,
321}
322
323#[derive(serde::Deserialize, Debug)]
324struct CargoManifest {
325 package: Option<CargoPackage>,
326}
327
328#[derive(serde::Deserialize, Debug)]
329struct CargoPackage {
330 metadata: Option<ManifestConfig>,
331}
332
333#[derive(serde::Deserialize, serde::Serialize)]
334#[serde(tag = "reason", rename_all = "kebab-case")]
335enum CargoMessage {
336 CompilerArtifact {
337 filenames: Vec<String>,
338 package_id: String,
339 profile: Profile,
340 fresh: bool,
341 },
342 BuildScriptExecuted,
343 RunWithArgs {
344 args: Vec<String>,
345 },
346 BuildFinished,
347}
348
349impl CargoBuild {
350 fn enable_name_section(&self, profile: &Profile) -> bool {
351 match profile.debuginfo {
352 Some(0) | None => self.manifest_config.wasm_name_section.unwrap_or(true),
353 Some(_) => true,
354 }
355 }
356
357 fn enable_producers_section(&self, profile: &Profile) -> bool {
358 match profile.debuginfo {
359 Some(0) | None => self.manifest_config.wasm_producers_section.unwrap_or(true),
360 Some(_) => true,
361 }
362 }
363}
364
365fn process_wasm(
367 wasm: &Path,
368 temp: &Path,
369 profile: &Profile,
370 build: &CargoBuild,
371 config: &Config,
372) -> Result<()> {
373 config.verbose(|| {
374 config.status("Processing", &temp.display().to_string());
375 });
376 run_wasm_opt(wasm, temp, profile, build, config)?;
377 Ok(())
378}
379
380fn run_wasm_opt(
381 wasm: &Path,
382 temp: &Path,
383 profile: &Profile,
384 build: &CargoBuild,
385 config: &Config,
386) -> Result<()> {
387 if build.manifest_config.wasm_opt == Some(false) {
389 std::fs::rename(temp, wasm).context("failed to rename build output")?;
390 return Ok(());
391 }
392
393 config.status("Optimizing", "with wasm-opt");
394 let wasm_opt = config.get_wasm_opt();
395
396 let mut cmd = Command::new(wasm_opt.bin_path());
397 cmd.arg(temp);
398 cmd.arg(format!("-O{}", profile.opt_level));
399 cmd.arg("-o").arg(wasm);
400 cmd.arg("--enable-bulk-memory");
401 cmd.arg("--enable-threads");
402 cmd.arg("--enable-reference-types");
403 cmd.arg("--no-validation");
404 cmd.arg("--translate-to-exnref");
405
406 if !build.enable_producers_section(profile) {
407 cmd.arg("--strip-producers");
408 }
409
410 match profile.debuginfo {
411 Some(0) | None => {
412 if build.enable_name_section(profile) {
414 cmd.arg("--debuginfo");
415 } else {
416 cmd.arg("--strip-debug");
417 }
418 }
419 Some(_) if profile.opt_level == "0" => {
420 cmd.arg("--debuginfo");
422 }
423 _ => {
424 cmd.arg("--strip-debug");
426 }
427 }
428
429 run_or_download(
430 wasm_opt.bin_path(),
431 wasm_opt.is_overridden(),
432 &mut cmd,
433 config,
434 || install_wasm_opt(&wasm_opt, config),
435 )
436 .context("`wasm-opt` failed to execute")?;
437 Ok(())
438}
439
440fn read_manifest_config() -> Result<ManifestConfig> {
441 let output = Command::new("cargo")
442 .arg("metadata")
443 .arg("--no-deps")
444 .arg("--format-version=1")
445 .capture_stdout()?;
446 let metadata = serde_json::from_str::<CargoMetadata>(&output)
447 .context("failed to deserialize `cargo metadata`")?;
448
449 let manifest = Path::new(&metadata.workspace_root).join("Cargo.toml");
450 let toml = fs::read_to_string(&manifest)
451 .context(format!("failed to read manifest: {}", manifest.display()))?;
452 let toml = toml::from_str::<CargoManifest>(&toml).context(format!(
453 "failed to deserialize as TOML: {}",
454 manifest.display()
455 ))?;
456
457 if let Some(meta) = toml.package.and_then(|p| p.metadata) {
458 Ok(meta)
459 } else {
460 Ok(ManifestConfig::default())
461 }
462}
463
464fn execute_cargo(
467 cargo: &mut Command,
468 config: &Config,
469 manifest_config: ManifestConfig,
470) -> Result<CargoBuild> {
471 config.verbose(|| config.status("Running", &format!("{:?}", cargo)));
472 let mut process = cargo
473 .stdout(Stdio::piped())
474 .spawn()
475 .context("failed to spawn `cargo`")?;
476 let mut json = String::new();
477 process
478 .stdout
479 .take()
480 .unwrap()
481 .read_to_string(&mut json)
482 .context("failed to read cargo stdout into a json string")?;
483 let status = process.wait().context("failed to wait on `cargo`")?;
484 utils::check_success(cargo, &status, &[], &[])
485 .map_err(|e| utils::hide_normal_process_exit(e, config))?;
486
487 let mut build = CargoBuild::default();
488
489 for line in json.lines() {
490 if !line.starts_with('{') {
491 println!("{}", line);
492 continue;
493 }
494 match serde_json::from_str(line) {
495 Ok(CargoMessage::CompilerArtifact {
496 filenames,
497 profile,
498 package_id,
499 fresh,
500 }) => {
501 let mut parts = package_id.split_whitespace();
502 if parts.next() == Some("wasm-bindgen") {
503 if let Some(version) = parts.next() {
504 build.wasm_bindgen = Some(version.to_string());
505 }
506 }
507 for file in filenames {
508 let file = PathBuf::from(file);
509 if file.extension().and_then(|s| s.to_str()) == Some("wasm") {
510 build.wasms.push((file, profile.clone(), fresh));
511 }
512 }
513 }
514 Ok(CargoMessage::RunWithArgs { args }) => build.runs.push(args),
515 Ok(CargoMessage::BuildScriptExecuted) => {}
516 Ok(CargoMessage::BuildFinished) => {}
517 Err(e) => bail!("failed to parse {}: {}", line, e),
518 }
519 }
520
521 build.manifest_config = manifest_config;
522
523 Ok(build)
524}
525
526fn run_or_download(
535 requested: &Path,
536 is_overridden: bool,
537 cmd: &mut Command,
538 config: &Config,
539 download: impl FnOnce() -> Result<()>,
540) -> Result<()> {
541 config.verbose(|| {
545 if requested.exists() {
546 config.status("Running", &format!("{:?}", cmd));
547 }
548 });
549
550 let err = match cmd.run() {
551 Ok(()) => return Ok(()),
552 Err(e) => e,
553 };
554 let rerun_after_download = err.chain().any(|e| {
555 if let Some(err) = e.downcast_ref::<io::Error>() {
559 return err.kind() == io::ErrorKind::NotFound
560 || err.kind() == io::ErrorKind::PermissionDenied;
561 }
562 false
563 });
564
565 if !rerun_after_download || is_overridden {
569 return Err(err);
570 }
571
572 download()?;
573 config.verbose(|| {
574 config.status("Running", &format!("{:?}", cmd));
575 });
576 cmd.run()
577}
578
579fn install_wasm_opt(path: &ToolPath, config: &Config) -> Result<()> {
580 let tag = "version_123";
581 let binaryen_url = |target: &str| {
582 let mut url = "https://github.com/WebAssembly/binaryen/releases/download/".to_string();
583 url.push_str(tag);
584 url.push_str("/binaryen-");
585 url.push_str(tag);
586 url.push('-');
587 url.push_str(target);
588 url.push_str(".tar.gz");
589 url
590 };
591
592 let url = if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") {
593 binaryen_url("x86_64-linux")
594 } else if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") {
595 binaryen_url("x86_64-macos")
596 } else if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") {
597 binaryen_url("arm64-macos")
598 } else if cfg!(target_os = "windows") && cfg!(target_arch = "x86_64") {
599 binaryen_url("x86_64-windows")
600 } else {
601 bail!(
602 "no precompiled binaries of `wasm-opt` are available for this \
603 platform, you'll want to set `$WASM_OPT` to a preinstalled \
604 `wasm-opt` command or disable via `wasm-opt = false` in \
605 your manifest"
606 )
607 };
608
609 let (base_path, sub_paths) = path.cache_paths().unwrap();
610 download(
611 &url,
612 &format!("precompiled wasm-opt {}", tag),
613 base_path,
614 sub_paths,
615 config,
616 )
617}
618
619fn download(
620 url: &str,
621 name: &str,
622 parent: &Path,
623 sub_paths: &Vec<PathBuf>,
624 config: &Config,
625) -> Result<()> {
626 let _flock = utils::flock(&config.cache().root().join("downloading"));
631 if sub_paths
632 .iter()
633 .all(|sub_path| parent.join(sub_path).exists())
634 {
635 return Ok(());
636 }
637
638 config.status("Downloading", name);
640 config.verbose(|| config.status("Get", url));
641
642 let response = utils::get(url, DOWNLOAD_TIMEOUT)?;
643 (|| -> Result<()> {
644 fs::create_dir_all(parent)
645 .context(format!("failed to create directory `{}`", parent.display()))?;
646
647 let decompressed = flate2::read::GzDecoder::new(response);
648 let mut tar = tar::Archive::new(decompressed);
649 for entry in tar.entries()? {
650 let mut entry = entry?;
651 let path = entry.path()?.into_owned();
652 for sub_path in sub_paths {
653 if path.ends_with(sub_path) {
654 let entry_path = parent.join(sub_path);
655 let dir = entry_path.parent().unwrap();
656 if !dir.exists() {
657 fs::create_dir_all(dir)
658 .context(format!("failed to create directory `{}`", dir.display()))?;
659 }
660 entry.unpack(entry_path)?;
661 }
662 }
663 }
664
665 if let Some(missing) = sub_paths
666 .iter()
667 .find(|sub_path| !parent.join(sub_path).exists())
668 {
669 bail!("failed to find {:?} in archive", missing);
670 }
671 Ok(())
672 })()
673 .context(format!("failed to extract tarball from {}", url))
674}