1#![allow(clippy::unwrap_used)]
30
31const TOOLCHAIN: &str = "nightly-2025-05-10";
33
34use codec::Encode;
35use jam_program_blob_common::{ConventionalMetadata, CoreVmProgramBlob, CrateInfo, ProgramBlob};
36use std::{
37 fmt::Display,
38 fs,
39 path::{Path, PathBuf},
40 process::Command,
41 sync::OnceLock,
42};
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
46pub enum BlobType {
47 Service,
49 Authorizer,
51 CoreVmGuest,
53}
54
55impl Display for BlobType {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 std::fmt::Debug::fmt(self, f)
58 }
59}
60
61impl BlobType {
62 pub fn dispatch_table(&self) -> Vec<Vec<u8>> {
63 match self {
64 Self::Service => vec![b"refine_ext".into(), b"accumulate_ext".into()],
65 Self::Authorizer => vec![b"is_authorized_ext".into()],
66 Self::CoreVmGuest => Vec::new(),
67 }
68 }
69
70 pub fn output_file(&self, out_dir: &Path, crate_name: &str) -> PathBuf {
72 let suffix = match self {
73 Self::Service | Self::Authorizer => "jam",
74 Self::CoreVmGuest => "corevm",
75 };
76 out_dir.join(format!("{crate_name}.{suffix}"))
77 }
78}
79
80pub enum ProfileType {
81 Debug,
82 Release,
83 Other(&'static str),
84}
85impl ProfileType {
86 fn as_str(&self) -> &'static str {
87 match self {
88 ProfileType::Debug => "debug",
89 ProfileType::Release => "release",
90 ProfileType::Other(s) => s,
91 }
92 }
93 fn to_arg(&self) -> String {
94 match self {
95 ProfileType::Debug => "--debug".into(),
96 ProfileType::Release => "--release".into(),
97 ProfileType::Other(s) => format!("--profile={s}"),
98 }
99 }
100
101 fn is_release_like(&self) -> bool {
104 !matches!(self, ProfileType::Debug)
105 }
106}
107
108fn build_pvm_blob_in_build_script(crate_dir: &Path, blob_type: BlobType) {
109 let out_dir: PathBuf = std::env::var("OUT_DIR").expect("No OUT_DIR").into();
110 println!("cargo:rerun-if-env-changed=SKIP_PVM_BUILDS");
111 println!("cargo:rerun-if-env-changed=PVM_BUILDER_STRIP");
112 if std::env::var_os("SKIP_PVM_BUILDS").is_some() {
113 let crate_name = get_crate_info(crate_dir).name;
114 let output_file = blob_type.output_file(&out_dir, &crate_name);
115 fs::write(&output_file, []).expect("error creating dummy program blob");
116 println!("cargo:rustc-env=PVM_BINARY_{crate_name}={}", output_file.display());
117 let hash_output_file = out_dir.join("{crate_name}.hash");
118 fs::write(&hash_output_file, [0_u8; 32]).expect("error creating dummy program blob hash");
119 println!("cargo:rustc-env=PVM_BINARY_HASH_{crate_name}={}", hash_output_file.display());
120 } else {
121 println!("cargo:rerun-if-changed={}", crate_dir.to_str().unwrap());
122 let (crate_name, output_file, hash_output_file) =
123 build_pvm_blob(crate_dir, blob_type, &out_dir, false, ProfileType::Other("production"));
124 println!("cargo:rustc-env=PVM_BINARY_{crate_name}={}", output_file.display());
125 println!("cargo:rustc-env=PVM_BINARY_HASH_{crate_name}={}", hash_output_file.display());
126 }
127}
128
129pub fn build_service(crate_dir: &Path) {
137 build_pvm_blob_in_build_script(crate_dir, BlobType::Service);
138}
139
140pub fn build_authorizer(crate_dir: &Path) {
148 build_pvm_blob_in_build_script(crate_dir, BlobType::Authorizer);
149}
150
151pub fn build_corevm_guest(crate_dir: &Path) {
159 build_pvm_blob_in_build_script(crate_dir, BlobType::CoreVmGuest);
160}
161
162fn build_encoded_rustflags(crate_dir: &Path) -> String {
179 let mut flags: Vec<String> = vec!["-C".into(), "panic=abort".into()];
180
181 let home = std::env::var("HOME").ok();
184 if let Some(h) = home.as_deref() {
185 flags.push(format!("--remap-path-prefix={h}=~"));
186 }
187 let rustup = std::env::var("RUSTUP_HOME")
188 .ok()
189 .or_else(|| home.as_deref().map(|h| format!("{h}/.rustup")));
190 if let Some(p) = rustup {
191 flags.push(format!("--remap-path-prefix={p}=~/.rustup"));
192 }
193 let cargo = std::env::var("CARGO_HOME")
194 .ok()
195 .or_else(|| home.as_deref().map(|h| format!("{h}/.cargo")));
196 if let Some(p) = cargo {
197 flags.push(format!("--remap-path-prefix={p}=~/.cargo"));
198 }
199 for (name, path) in workspace_members(crate_dir) {
200 flags.push(format!("--remap-path-prefix={}=/crate/{name}", path.display()));
201 }
202
203 flags.join("\x1f")
204}
205
206fn workspace_members(crate_dir: &Path) -> impl Iterator<Item = (String, PathBuf)> {
208 let packages = (|| -> Option<Vec<serde_json::Value>> {
209 let output = Command::new("cargo")
210 .current_dir(crate_dir)
211 .args(["metadata", "--no-deps", "--format-version", "1"])
212 .output()
213 .ok()
214 .filter(|o| o.status.success())?;
215 let mut meta: serde_json::Value = serde_json::from_slice(&output.stdout).ok()?;
216 match meta.get_mut("packages")?.take() {
217 serde_json::Value::Array(arr) => Some(arr),
218 _ => None,
219 }
220 })()
221 .unwrap_or_default();
222
223 packages.into_iter().filter_map(|pkg| {
224 let name = pkg.get("name")?.as_str()?.to_string();
225 let manifest = pkg.get("manifest_path")?.as_str()?;
226 let path = Path::new(manifest).parent()?.to_path_buf();
227 Some((name, path))
228 })
229}
230
231fn get_crate_info(crate_dir: &Path) -> CrateInfo {
232 let read_manifest_output = Command::new("cargo")
233 .current_dir(crate_dir)
234 .arg("read-manifest")
235 .output()
236 .unwrap_or_else(|err| {
237 panic!("Failed to run `cargo read-manifest` in {}: {err}", crate_dir.display());
238 });
239 if !read_manifest_output.status.success() {
240 panic!(
241 "Failed to read Cargo.toml manifest in {}:\n{}",
242 crate_dir.display(),
243 String::from_utf8_lossy(&read_manifest_output.stderr)
244 );
245 }
246 let man = serde_json::from_slice::<serde_json::Value>(&read_manifest_output.stdout).unwrap();
247 let name = man.get("name").unwrap().as_str().unwrap().to_string();
249 let version = man.get("version").unwrap().as_str().unwrap().to_string();
250 let license = man
252 .get("license")
253 .unwrap()
254 .as_str()
255 .unwrap_or_else(|| {
256 panic!("No license specified in Cargo.toml manifest in {}", crate_dir.display());
257 })
258 .to_string();
259 let authors = man
261 .get("authors")
262 .unwrap()
263 .as_array()
264 .unwrap()
265 .iter()
266 .map(|x| x.as_str().unwrap().to_owned())
267 .collect::<Vec<String>>();
268 CrateInfo { name, version, license, authors }
269}
270
271pub fn build_pvm_blob(
283 crate_dir: &Path,
284 blob_type: BlobType,
285 out_dir: &Path,
286 install_rustc: bool,
287 profile: ProfileType,
288) -> (String, PathBuf, PathBuf) {
289 let mut args = polkavm_linker::TargetJsonArgs::default();
290 args.is_64_bit = true;
291 args.rustc_version = polkavm_linker::RustcVersion::Legacy;
292
293 let (target_name, target_json_path) =
294 ("riscv64emac-unknown-none-polkavm", polkavm_linker::target_json_path(args).unwrap());
295
296 println!("🪤 PVM module type: {blob_type}");
297 println!("🎯 Target name: {target_name}");
298
299 let rustup_installed = if Command::new("rustup").output().is_ok() {
300 let output = Command::new("rustup")
301 .args(["component", "list", "--toolchain", TOOLCHAIN, "--installed"])
302 .output()
303 .unwrap_or_else(|_| {
304 panic!(
305 "Failed to execute `rustup component list --toolchain {TOOLCHAIN} --installed`.\n\
306 Please install `rustup` to continue.",
307 )
308 });
309
310 if !output.status.success() ||
311 !output.stdout.split(|x| *x == b'\n').any(|x| x[..] == b"rust-src"[..])
312 {
313 if install_rustc {
314 println!("Installing rustc dependencies...");
315 let mut child = Command::new("rustup")
316 .args(["toolchain", "install", TOOLCHAIN, "-c", "rust-src"])
317 .stdout(std::process::Stdio::inherit())
318 .stderr(std::process::Stdio::inherit())
319 .spawn()
320 .unwrap_or_else(|_| {
321 panic!(
322 "Failed to execute `rustup toolchain install {TOOLCHAIN} -c rust-src`.\n\
323 Please install `rustup` to continue."
324 )
325 });
326 if !child.wait().expect("Failed to execute rustup process").success() {
327 panic!("Failed to install `rust-src` component of {TOOLCHAIN}.");
328 }
329 } else {
330 panic!("`rust-src` component of {TOOLCHAIN} is required to build the PVM binary.",);
331 }
332 }
333 println!("ℹ️ `rustup` and toolchain installed. Continuing build process...");
334
335 true
336 } else {
337 println!("ℹ️ `rustup` not installed, here be dragons. Continuing build process...");
338
339 false
340 };
341
342 let info = get_crate_info(crate_dir);
343 println!("📦 Crate name: {}", info.name);
344 println!("🏷️ Build profile: {}", profile.as_str());
345
346 let mut child = Command::new("cargo");
347
348 child
349 .current_dir(crate_dir)
350 .env_clear()
351 .env("PATH", std::env::var("PATH").unwrap())
352 .env("CARGO_ENCODED_RUSTFLAGS", build_encoded_rustflags(crate_dir))
353 .env("CARGO_TARGET_DIR", out_dir)
354 .env("RUSTC_BOOTSTRAP", "1");
356
357 if let Some(w) = std::env::var_os("RUSTC_WRAPPER") {
363 child.env("RUSTC_WRAPPER", w);
364 }
365
366 if rustup_installed {
367 child.arg(format!("+{TOOLCHAIN}"));
368 }
369
370 child.args(["rustc", "--lib", "--crate-type=cdylib", "-Z", "build-std=core,alloc"]);
371 if profile.is_release_like() {
372 child.args(["-Z", "build-std-features=panic_immediate_abort"]);
375 }
376 child.arg(profile.to_arg()).arg("--target").arg(target_json_path);
377
378 if let Some(client) = get_job_server_client() {
381 client.configure(&mut child);
382 }
383
384 let mut child = child.spawn().expect("Failed to execute cargo process");
385 let status = child.wait().expect("Failed to execute cargo process");
386
387 if !status.success() {
388 eprintln!("Failed to build RISC-V ELF due to cargo execution error");
389 std::process::exit(1);
390 }
391
392 println!("Converting RISC-V ELF to PVM blob...");
394 let mut config = polkavm_linker::Config::default();
395 config.set_strip(std::env::var("PVM_BUILDER_STRIP").map(|value| value == "1").unwrap_or(true));
396 config.set_dispatch_table(blob_type.dispatch_table());
397
398 let input_root = &out_dir.join(target_name).join(profile.as_str());
399 let input_path_bin = input_root.join(&info.name);
400 let input_path_cdylib = input_root.join(format!("{}.elf", info.name.replace("-", "_")));
401
402 let input_path = if input_path_cdylib.exists() {
403 if input_path_bin.exists() {
404 eprintln!(
405 "Both {} and {} exist; run 'cargo clean' to get rid of old artifacts!",
406 input_path_cdylib.display(),
407 input_path_bin.display()
408 );
409 std::process::exit(1);
410 }
411 input_path_cdylib
412 } else if input_path_bin.exists() {
413 input_path_bin
414 } else {
415 eprintln!(
416 "Failed to build: neither {} nor {} exist",
417 input_path_cdylib.display(),
418 input_path_bin.display()
419 );
420 std::process::exit(1);
421 };
422
423 let orig =
424 fs::read(&input_path).unwrap_or_else(|e| panic!("Failed to read {input_path:?} :{e:?}"));
425 let linked = polkavm_linker::program_from_elf(
426 config,
427 polkavm_linker::TargetInstructionSet::JamV1,
428 orig.as_ref(),
429 )
430 .expect("Failed to link pvm program:");
431
432 let output_path_pvm = out_dir.join(format!("{}.polkavm", info.name));
434 let hash_output_file = out_dir.join(format!("{}.hash", info.name));
435 fs::write(&output_path_pvm, &linked).expect("Error writing resulting binary");
436 let name = info.name.clone();
437 let metadata = ConventionalMetadata::Info(info).encode().into();
438 let output_file = blob_type.output_file(out_dir, &name);
439 let blob = if !matches!(blob_type, BlobType::CoreVmGuest) {
440 let parts = polkavm_linker::ProgramParts::from_bytes(linked.into())
441 .expect("failed to deserialize linked PolkaVM program");
442 let blob = ProgramBlob::from_pvm(&parts, metadata)
443 .to_vec()
444 .expect("error serializing the .jam blob");
445 fs::write(&output_file, &blob).expect("error writing the .jam blob");
446 blob
447 } else {
448 let blob = CoreVmProgramBlob { metadata, pvm_blob: linked.into() }
449 .to_vec()
450 .expect("error serializing the CoreVM blob");
451 fs::write(&output_file, &blob).expect("error writing the CoreVM blob");
452 blob
453 };
454 let hash = code_hash(&blob);
455 fs::write(&hash_output_file, hash).expect("error writing blob hash");
456
457 (name, output_file, hash_output_file)
458}
459
460pub fn code_hash(data: &[u8]) -> [u8; 32] {
464 let h = blake2b_simd::Params::new().hash_length(32).hash(data);
465 h.as_bytes().try_into().expect("Hash length set to 32")
466}
467
468fn get_job_server_client() -> Option<&'static jobserver::Client> {
469 static CLIENT: OnceLock<Option<jobserver::Client>> = OnceLock::new();
470 CLIENT.get_or_init(|| unsafe { jobserver::Client::from_env() }).as_ref()
471}
472
473#[macro_export]
475macro_rules! pvm_binary {
476 ($name:literal) => {
477 include_bytes!(env!(concat!("PVM_BINARY_", $name)))
478 };
479}
480
481#[macro_export]
486macro_rules! pvm_binary_hash {
487 ($name:literal) => {
488 include_bytes!(env!(concat!("PVM_BINARY_HASH_", $name)))
489 };
490}