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 BuildWasm {
27 #[arg(long, required = true)]
29 out_dir: String,
30
31 #[arg(long, required = true, value_enum)]
33 target: Target,
34
35 #[arg(long)]
37 rust_version: Option<String>,
38 },
39
40 BuildKimchiStubs {
42 #[arg(long)]
44 target_dir: Option<String>,
45
46 #[arg(long, short, action, default_value_t = false)]
47 offline: bool,
48 },
49
50 Release {
52 #[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 Nodejs,
69 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 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 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 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 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 #[cfg(not(target_arch = "x86_64"))]
177 let cpu_supports_adx_bmi2 = false;
178
179 let rustflags = match (optimisations_enabled, cpu_supports_adx_bmi2) {
182 (true, true) => {
183 Some("-C target-feature=+bmi2,+adx".to_string())
186 }
187 (false, true) => {
188 Some("-C target-feature=-bmi2,-adx".to_string())
191 }
192 (true, false) => {
193 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 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}