Skip to main content

xtask/
main.rs

1use anyhow::{Context, Result};
2use clap::{Parser, Subcommand, ValueEnum};
3#[cfg(target_arch = "x86_64")]
4use raw_cpuid::CpuId;
5use semver::Version;
6use std::{
7    env,
8    ffi::OsString,
9    fs,
10    ops::{Deref, DerefMut},
11    path::PathBuf,
12    process::Command,
13};
14use toml_edit::{value, DocumentMut};
15
16#[derive(Parser)]
17#[command(author, version, about, long_about = None)]
18struct Cli {
19    #[command(subcommand)]
20    command: Commands,
21}
22
23#[derive(Subcommand)]
24enum Commands {
25    /// Build WASM package
26    BuildWasm {
27        /// Output directory for wasm-pack
28        #[arg(long, required = true)]
29        out_dir: String,
30
31        /// Target platform (nodejs or web)
32        #[arg(long, required = true, value_enum)]
33        target: Target,
34
35        /// Version of `rustc`
36        #[arg(long)]
37        rust_version: Option<String>,
38    },
39
40    /// Build kimchi-stubs with optional CPU optimisations
41    BuildKimchiStubs {
42        /// Target directory for cargo build artifacts
43        #[arg(long)]
44        target_dir: Option<String>,
45
46        #[arg(long, short, action, default_value_t = false)]
47        offline: bool,
48    },
49
50    /// Release a new version
51    Release {
52        /// Bump type
53        #[arg(value_enum)]
54        bump: BumpType,
55    },
56}
57
58#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
59enum BumpType {
60    Patch,
61    Minor,
62    Major,
63}
64
65#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
66enum Target {
67    /// Build for NodeJS
68    Nodejs,
69    /// Build for Web
70    Web,
71}
72
73impl From<Target> for &'static str {
74    fn from(target: Target) -> &'static str {
75        match target {
76            Target::Nodejs => "nodejs",
77            Target::Web => "web",
78        }
79    }
80}
81
82fn main() -> Result<()> {
83    let cli = Cli::parse();
84
85    match &cli.command {
86        Commands::BuildWasm {
87            out_dir,
88            target,
89            rust_version,
90        } => build_wasm(out_dir, *target, rust_version.as_deref()),
91        Commands::BuildKimchiStubs {
92            target_dir,
93            offline,
94        } => build_kimchi_stubs(target_dir.as_deref(), *offline),
95        Commands::Release { bump } => release(*bump),
96    }
97}
98
99fn release(bump: BumpType) -> Result<()> {
100    // 1. Bump version in Cargo.toml
101    let cargo_toml_path = "Cargo.toml";
102    let cargo_toml_content =
103        fs::read_to_string(cargo_toml_path).context("Failed to read Cargo.toml")?;
104    let mut doc = cargo_toml_content
105        .parse::<DocumentMut>()
106        .context("Failed to parse Cargo.toml")?;
107
108    let version_str = doc["workspace"]["package"]["version"]
109        .as_str()
110        .context("version not found in [workspace.package]")?
111        .to_string();
112    let mut version = Version::parse(&version_str).context("Failed to parse version")?;
113
114    match bump {
115        BumpType::Patch => version.patch += 1,
116        BumpType::Minor => {
117            version.minor += 1;
118            version.patch = 0;
119        }
120        BumpType::Major => {
121            version.major += 1;
122            version.minor = 0;
123            version.patch = 0;
124        }
125    }
126
127    let new_version = version.to_string();
128    doc["workspace"]["package"]["version"] = value(&new_version);
129    fs::write(cargo_toml_path, doc.to_string()).context("Failed to write Cargo.toml")?;
130
131    println!("Bumping version from {} to {}", version_str, new_version);
132
133    // 2. Update CHANGELOG.md
134    let changelog_path = "CHANGELOG.md";
135    let changelog_content =
136        fs::read_to_string(changelog_path).context("Failed to read CHANGELOG.md")?;
137    let new_changelog_content = changelog_content.replace(
138        "## Unreleased",
139        &format!("## Unreleased\n\n## {}", new_version),
140    );
141    fs::write(changelog_path, new_changelog_content).context("Failed to write CHANGELOG.md")?;
142
143    // 3. Update Cargo.lock
144    println!("Updating Cargo.lock...");
145    let status = Command::new("cargo")
146        .arg("check")
147        .status()
148        .context("Failed to update Cargo.lock")?;
149    if !status.success() {
150        anyhow::bail!("cargo check failed");
151    }
152
153    println!("Release preparation for version {} complete!", new_version);
154
155    Ok(())
156}
157
158type RustVersion<'a> = Option<&'a str>;
159
160fn build_kimchi_stubs(target_dir: Option<&str>, offline: bool) -> Result<()> {
161    // Optimisations are enabled by default, but can be disabled by setting the
162    // `RUST_TARGET_FEATURE_OPTIMISATIONS` environment variable to any other
163    // value than "y".
164    let optimisations_enabled = env::var("RUST_TARGET_FEATURE_OPTIMISATIONS")
165        .map(|v| ["y", "1", "true"].contains(&v.to_lowercase().as_str()))
166        .unwrap_or(true);
167
168    #[cfg(target_arch = "x86_64")]
169    let cpu_supports_adx_bmi2 = {
170        let cpuid = CpuId::new();
171        cpuid
172            .get_extended_feature_info()
173            .is_some_and(|f| f.has_adx() && f.has_bmi2())
174    };
175    // ADX and BMI2 are not applicable to other architectures.
176    #[cfg(not(target_arch = "x86_64"))]
177    let cpu_supports_adx_bmi2 = false;
178
179    // If optimisations are enabled and the CPU supports ADX and BMI2, we enable
180    // those features.
181    let rustflags = match (optimisations_enabled, cpu_supports_adx_bmi2) {
182        (true, true) => {
183            // If optimisations are enabled and the CPU supports ADX and BMI2,
184            // we enable them.
185            Some("-C target-feature=+bmi2,+adx".to_string())
186        }
187        (false, true) => {
188            // If optimisations are disabled but the CPU supports ADX and BMI2,
189            // we explicitly disable them.
190            Some("-C target-feature=-bmi2,-adx".to_string())
191        }
192        (true, false) => {
193            // If the CPU does not support ADX and BMI2, we do not set any
194            // target features. It could be handled in the `else` branch, but we
195            // want to be explicit. If the CPU does not support these features, but
196            // we still add the -bmi2 and -adx flags, it will cause a build warning
197            // we want to avoid on the user console.
198            None
199        }
200        (false, false) => None,
201    };
202
203    let target_dir = target_dir.unwrap_or("target/kimchi_stubs_build");
204
205    let mut cmd = Command::new("cargo");
206    cmd.args([
207        "build",
208        "--release",
209        "-p",
210        "kimchi-stubs",
211        "--target-dir",
212        target_dir,
213    ]);
214
215    if offline {
216        cmd.arg("--offline");
217    }
218
219    if let Some(rustflags) = rustflags {
220        cmd.env("RUSTFLAGS", rustflags);
221    }
222
223    let status = cmd.status().context("Failed to build kimchi-stubs")?;
224
225    if !status.success() {
226        anyhow::bail!("kimchi-stubs build failed");
227    }
228
229    Ok(())
230}
231
232fn build_wasm(out_dir: &str, target: Target, rust_version: RustVersion) -> Result<()> {
233    let cargo_target_dir = env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string());
234    let artifact_dir = PathBuf::from(format!("{cargo_target_dir}/bin"));
235
236    let mut cmd = RustVersionCommand::for_cargo(rust_version);
237
238    let args = [
239        "build",
240        "--release",
241        "--package=wasm-pack",
242        "--bin=wasm-pack",
243        "--artifact-dir",
244        artifact_dir.to_str().unwrap(),
245        "-Z=unstable-options",
246    ];
247
248    let status = cmd
249        .args(args)
250        .env("CARGO_TARGET_DIR", &cargo_target_dir)
251        .status()
252        .context("Failed to build wasm-pack")?;
253
254    if !status.success() {
255        anyhow::bail!("wasm-pack build failed");
256    }
257
258    let wasm_pack_path = artifact_dir.join("wasm-pack");
259    let mut cmd = RustVersionCommand::for_wasm_pack(wasm_pack_path, rust_version);
260
261    // Prepare the command arguments
262    let args = [
263        "build",
264        "--target",
265        target.into(),
266        "--out-dir",
267        out_dir,
268        "plonk-wasm",
269        "--",
270        "-Z",
271        "build-std=panic_abort,std",
272    ];
273
274    let target_args: &[_] = if target == Target::Nodejs {
275        &["--features", "nodejs"]
276    } else {
277        &[]
278    };
279
280    let status = cmd
281        .args(args)
282        .args(target_args)
283        .status()
284        .context("Failed to execute wasm-pack")?;
285
286    if !status.success() {
287        anyhow::bail!("wasm-pack build for {} failed", <&str>::from(target));
288    }
289
290    Ok(())
291}
292
293struct RustVersionCommand<'a> {
294    cmd: Command,
295    rustup_args: Option<(OsString, &'a str)>,
296}
297
298impl<'a> RustVersionCommand<'a> {
299    fn for_cargo(rustup_args: Option<&'a str>) -> Self {
300        let (cmd, rustup_args) = if let Some(version) = rustup_args {
301            (
302                Command::new("rustup"),
303                Some((OsString::from("cargo"), version)),
304            )
305        } else {
306            (Command::new("cargo"), None)
307        };
308
309        Self { cmd, rustup_args }
310    }
311
312    fn for_wasm_pack(wasm_path: PathBuf, rustup_args: Option<&'a str>) -> Self {
313        let (cmd, rustup_args) = if let Some(version) = rustup_args {
314            let cmd = Command::new("rustup");
315            let rustup_args = Some((wasm_path.into_os_string(), version));
316
317            (cmd, rustup_args)
318        } else {
319            (Command::new(wasm_path), None)
320        };
321
322        Self { cmd, rustup_args }
323    }
324}
325
326impl Deref for RustVersionCommand<'_> {
327    type Target = Command;
328
329    fn deref(&self) -> &Self::Target {
330        &self.cmd
331    }
332}
333
334impl DerefMut for RustVersionCommand<'_> {
335    fn deref_mut(&mut self) -> &mut Self::Target {
336        let Some((program, version)) = self.rustup_args.take() else {
337            return &mut self.cmd;
338        };
339
340        self.cmd.arg("run").arg(version).arg(program)
341    }
342}