1use anyhow::{Context, Result};
2use clap::{Parser, Subcommand, ValueEnum};
3#[cfg(target_arch = "x86_64")]
4use raw_cpuid::CpuId;
5use std::{
6 env,
7 ffi::OsString,
8 ops::{Deref, DerefMut},
9 path::PathBuf,
10 process::Command,
11};
12
13#[derive(Parser)]
14#[command(author, version, about, long_about = None)]
15struct Cli {
16 #[command(subcommand)]
17 command: Commands,
18}
19
20#[derive(Subcommand)]
21enum Commands {
22 BuildWasm {
24 #[arg(long, required = true)]
26 out_dir: String,
27
28 #[arg(long, required = true, value_enum)]
30 target: Target,
31
32 #[arg(long)]
34 rust_version: Option<String>,
35 },
36
37 BuildKimchiStubs {
39 #[arg(long)]
41 target_dir: Option<String>,
42
43 #[arg(long, short, action, default_value_t = false)]
44 offline: bool,
45 },
46}
47
48#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
49enum Target {
50 Nodejs,
52 Web,
54}
55
56impl From<Target> for &'static str {
57 fn from(target: Target) -> &'static str {
58 match target {
59 Target::Nodejs => "nodejs",
60 Target::Web => "web",
61 }
62 }
63}
64
65fn main() -> Result<()> {
66 let cli = Cli::parse();
67
68 match &cli.command {
69 Commands::BuildWasm {
70 out_dir,
71 target,
72 rust_version,
73 } => build_wasm(out_dir, *target, rust_version.as_deref()),
74 Commands::BuildKimchiStubs {
75 target_dir,
76 offline,
77 } => build_kimchi_stubs(target_dir.as_deref(), *offline),
78 }
79}
80
81type RustVersion<'a> = Option<&'a str>;
82
83fn build_kimchi_stubs(target_dir: Option<&str>, offline: bool) -> Result<()> {
84 let optimisations_enabled = env::var("RUST_TARGET_FEATURE_OPTIMISATIONS")
88 .map(|v| ["y", "1", "true"].contains(&v.to_lowercase().as_str()))
89 .unwrap_or(true);
90
91 #[cfg(target_arch = "x86_64")]
92 let cpu_supports_adx_bmi2 = {
93 let cpuid = CpuId::new();
94 cpuid
95 .get_extended_feature_info()
96 .is_some_and(|f| f.has_adx() && f.has_bmi2())
97 };
98 #[cfg(not(target_arch = "x86_64"))]
100 let cpu_supports_adx_bmi2 = false;
101
102 let rustflags = match (optimisations_enabled, cpu_supports_adx_bmi2) {
105 (true, true) => {
106 Some("-C target-feature=+bmi2,+adx".to_string())
109 }
110 (false, true) => {
111 Some("-C target-feature=-bmi2,-adx".to_string())
114 }
115 (true, false) => {
116 None
122 }
123 (false, false) => None,
124 };
125
126 let target_dir = target_dir.unwrap_or("target/kimchi_stubs_build");
127
128 let mut cmd = Command::new("cargo");
129 cmd.args([
130 "build",
131 "--release",
132 "-p",
133 "kimchi-stubs",
134 "--target-dir",
135 target_dir,
136 ]);
137
138 if offline {
139 cmd.arg("--offline");
140 }
141
142 if let Some(rustflags) = rustflags {
143 cmd.env("RUSTFLAGS", rustflags);
144 }
145
146 let status = cmd.status().context("Failed to build kimchi-stubs")?;
147
148 if !status.success() {
149 anyhow::bail!("kimchi-stubs build failed");
150 }
151
152 Ok(())
153}
154
155fn build_wasm(out_dir: &str, target: Target, rust_version: RustVersion) -> Result<()> {
156 let cargo_target_dir = env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string());
157 let artifact_dir = PathBuf::from(format!("{cargo_target_dir}/bin"));
158
159 let mut cmd = RustVersionCommand::for_cargo(rust_version);
160
161 let args = [
162 "build",
163 "--release",
164 "--package=wasm-pack",
165 "--bin=wasm-pack",
166 "--artifact-dir",
167 artifact_dir.to_str().unwrap(),
168 "-Z=unstable-options",
169 ];
170
171 let status = cmd
172 .args(args)
173 .env("CARGO_TARGET_DIR", &cargo_target_dir)
174 .status()
175 .context("Failed to build wasm-pack")?;
176
177 if !status.success() {
178 anyhow::bail!("wasm-pack build failed");
179 }
180
181 let wasm_pack_path = artifact_dir.join("wasm-pack");
182 let mut cmd = RustVersionCommand::for_wasm_pack(wasm_pack_path, rust_version);
183
184 let args = [
186 "build",
187 "--target",
188 target.into(),
189 "--out-dir",
190 out_dir,
191 "plonk-wasm",
192 "--",
193 "-Z",
194 "build-std=panic_abort,std",
195 ];
196
197 let target_args: &[_] = if target == Target::Nodejs {
198 &["--features", "nodejs"]
199 } else {
200 &[]
201 };
202
203 let status = cmd
204 .args(args)
205 .args(target_args)
206 .status()
207 .context("Failed to execute wasm-pack")?;
208
209 if !status.success() {
210 anyhow::bail!("wasm-pack build for {} failed", <&str>::from(target));
211 }
212
213 Ok(())
214}
215
216struct RustVersionCommand<'a> {
217 cmd: Command,
218 rustup_args: Option<(OsString, &'a str)>,
219}
220
221impl<'a> RustVersionCommand<'a> {
222 fn for_cargo(rustup_args: Option<&'a str>) -> Self {
223 let (cmd, rustup_args) = if let Some(version) = rustup_args {
224 (
225 Command::new("rustup"),
226 Some((OsString::from("cargo"), version)),
227 )
228 } else {
229 (Command::new("cargo"), None)
230 };
231
232 Self { cmd, rustup_args }
233 }
234
235 fn for_wasm_pack(wasm_path: PathBuf, rustup_args: Option<&'a str>) -> Self {
236 let (cmd, rustup_args) = if let Some(version) = rustup_args {
237 let cmd = Command::new("rustup");
238 let rustup_args = Some((wasm_path.into_os_string(), version));
239
240 (cmd, rustup_args)
241 } else {
242 (Command::new(wasm_path), None)
243 };
244
245 Self { cmd, rustup_args }
246 }
247}
248
249impl Deref for RustVersionCommand<'_> {
250 type Target = Command;
251
252 fn deref(&self) -> &Self::Target {
253 &self.cmd
254 }
255}
256
257impl DerefMut for RustVersionCommand<'_> {
258 fn deref_mut(&mut self) -> &mut Self::Target {
259 let Some((program, version)) = self.rustup_args.take() else {
260 return &mut self.cmd;
261 };
262
263 self.cmd.arg("run").arg(version).arg(program)
264 }
265}