xtask/
main.rs

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    /// Build WASM package
23    BuildWasm {
24        /// Output directory for wasm-pack
25        #[arg(long, required = true)]
26        out_dir: String,
27
28        /// Target platform (nodejs or web)
29        #[arg(long, required = true, value_enum)]
30        target: Target,
31
32        /// Version of `rustc`
33        #[arg(long)]
34        rust_version: Option<String>,
35    },
36
37    /// Build kimchi-stubs with optional CPU optimisations
38    BuildKimchiStubs {
39        /// Target directory for cargo build artifacts
40        #[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    /// Build for NodeJS
51    Nodejs,
52    /// Build for Web
53    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    // Optimisations are enabled by default, but can be disabled by setting the
85    // `RUST_TARGET_FEATURE_OPTIMISATIONS` environment variable to any other
86    // value than "y".
87    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    // ADX and BMI2 are not applicable to other architectures.
99    #[cfg(not(target_arch = "x86_64"))]
100    let cpu_supports_adx_bmi2 = false;
101
102    // If optimisations are enabled and the CPU supports ADX and BMI2, we enable
103    // those features.
104    let rustflags = match (optimisations_enabled, cpu_supports_adx_bmi2) {
105        (true, true) => {
106            // If optimisations are enabled and the CPU supports ADX and BMI2,
107            // we enable them.
108            Some("-C target-feature=+bmi2,+adx".to_string())
109        }
110        (false, true) => {
111            // If optimisations are disabled but the CPU supports ADX and BMI2,
112            // we explicitly disable them.
113            Some("-C target-feature=-bmi2,-adx".to_string())
114        }
115        (true, false) => {
116            // If the CPU does not support ADX and BMI2, we do not set any
117            // target features. It could be handled in the `else` branch, but we
118            // want to be explicit. If the CPU does not support these features, but
119            // we still add the -bmi2 and -adx flags, it will cause a build warning
120            // we want to avoid on the user console.
121            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    // Prepare the command arguments
185    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}