standard_plugin_cli/
build.rs1use std::path::{Path, PathBuf};
7use std::process::Command;
8
9use crate::project::Plugin;
10
11pub const TARGET: &str = "wasm32-wasip2";
12
13#[derive(Clone, Debug)]
15pub struct Built {
16 pub component: PathBuf,
17 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
25fn 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
83pub 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}