Skip to main content

standard_plugin_cli/
build.rs

1//! `standard-plugin build`: cargo builds the crate for `wasm32-wasip2`
2//! (whose output is already a component), the component is copied into
3//! the bundle under the manifest's `module` name, and the browser artifact
4//! is transpiled when `jco` is installed.
5
6use std::path::{Path, PathBuf};
7use std::process::Command;
8
9use crate::project::Plugin;
10
11pub const TARGET: &str = "wasm32-wasip2";
12
13/// What a build produced.
14#[derive(Clone, Debug)]
15pub struct Built {
16    pub component: PathBuf,
17    /// The transpiled browser artifact directory, or why it was skipped.
18    pub browser: Result<PathBuf, String>,
19}
20
21pub(crate) fn cargo() -> Command {
22    Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()))
23}
24
25/// The crate's cdylib artifact path for a profile.
26fn artifact(dir: &Path, release: bool) -> Result<PathBuf, String> {
27    let output = cargo()
28        .args(["metadata", "--format-version", "1", "--no-deps"])
29        .current_dir(dir)
30        .output()
31        .map_err(|error| format!("cargo metadata: {error}"))?;
32    if !output.status.success() {
33        return Err(format!(
34            "cargo metadata failed: {}",
35            String::from_utf8_lossy(&output.stderr)
36        ));
37    }
38    let metadata: serde_json::Value = serde_json::from_slice(&output.stdout)
39        .map_err(|error| format!("cargo metadata: {error}"))?;
40    let manifest = dir
41        .join("Cargo.toml")
42        .canonicalize()
43        .map_err(|error| format!("{}: {error}", dir.join("Cargo.toml").display()))?;
44    let package = metadata["packages"]
45        .as_array()
46        .into_iter()
47        .flatten()
48        .find(|package| {
49            package["manifest_path"]
50                .as_str()
51                .and_then(|path| Path::new(path).canonicalize().ok())
52                .is_some_and(|path| path == manifest)
53        })
54        .ok_or_else(|| format!("{} is not a package cargo knows", manifest.display()))?;
55    let name = package["targets"]
56        .as_array()
57        .into_iter()
58        .flatten()
59        .find(|target| {
60            target["kind"]
61                .as_array()
62                .is_some_and(|kinds| kinds.iter().any(|kind| kind == "cdylib"))
63        })
64        .and_then(|target| target["name"].as_str())
65        .ok_or_else(|| "the crate has no cdylib library (crate-type = [\"cdylib\"])".to_string())?;
66    let target_dir = metadata["target_directory"]
67        .as_str()
68        .ok_or("cargo metadata has no target directory")?;
69    Ok(Path::new(target_dir)
70        .join(TARGET)
71        .join(if release { "release" } else { "debug" })
72        .join(format!("{}.wasm", name.replace('-', "_"))))
73}
74
75fn find_on_path(program: &str) -> Option<PathBuf> {
76    std::env::var_os("PATH").and_then(|paths| {
77        std::env::split_paths(&paths)
78            .map(|dir| dir.join(program))
79            .find(|path| path.is_file())
80    })
81}
82
83/// Builds one plugin.
84pub fn build(plugin: &Plugin, release: bool) -> Result<Built, String> {
85    let manifest = plugin.manifest()?;
86    let mut command = cargo();
87    command
88        .args(["build", "--lib", "--target", TARGET])
89        .current_dir(&plugin.dir);
90    if release {
91        command.arg("--release");
92    }
93    let status = command
94        .status()
95        .map_err(|error| format!("cargo build: {error}"))?;
96    if !status.success() {
97        return Err(format!("cargo build failed ({status})"));
98    }
99    let built = artifact(&plugin.dir, release)?;
100    plugin
101        .stage_manifest()
102        .map_err(|error| format!("{}: {error}", plugin.bundle.display()))?;
103    let component = plugin.bundle.join(&manifest.module);
104    std::fs::copy(&built, &component)
105        .map_err(|error| format!("{} -> {}: {error}", built.display(), component.display()))?;
106
107    let browser = match find_on_path("jco") {
108        None => {
109            Err("skipped: `jco` is not installed (npm install -g @bytecodealliance/jco)".into())
110        }
111        Some(jco) => {
112            let out = plugin.bundle.join("browser");
113            let status = Command::new(jco)
114                .arg("transpile")
115                .arg(&component)
116                .arg("--out-dir")
117                .arg(&out)
118                .args(["--name", &manifest.id])
119                .status()
120                .map_err(|error| format!("jco: {error}"))?;
121            if status.success() {
122                Ok(out)
123            } else {
124                Err(format!("jco transpile failed ({status})"))
125            }
126        }
127    };
128    Ok(Built { component, browser })
129}