jam_pvm_builder/
lib.rs

1//! Builder logic for creating PVM code blobs for execution on the JAM PVM instances (service code
2//! and authorizer code).
3
4#![allow(clippy::unwrap_used)]
5
6// If you update this, you should also update the toolchain installed by .github/workflows/rust.yml
7const TOOLCHAIN: &str = "nightly-2025-05-10";
8
9use codec::Encode;
10use jam_program_blob_common::{ConventionalMetadata, CoreVmProgramBlob, CrateInfo, ProgramBlob};
11use std::{
12	fmt::Display,
13	fs,
14	path::{Path, PathBuf},
15	process::Command,
16	sync::OnceLock,
17};
18
19/// Program blob type.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub enum BlobType {
22	/// JAM service (`jam_pvm_common::Service`).
23	Service,
24	/// JAM authorizer (`jam_pvm_common::Authorizer`).
25	Authorizer,
26	/// CoreVM guest program (`corevm_guest` crate).
27	CoreVmGuest,
28}
29
30impl Display for BlobType {
31	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32		std::fmt::Debug::fmt(self, f)
33	}
34}
35
36impl BlobType {
37	pub fn dispatch_table(&self) -> Vec<Vec<u8>> {
38		match self {
39			Self::Service => vec![b"refine_ext".into(), b"accumulate_ext".into()],
40			Self::Authorizer => vec![b"is_authorized_ext".into()],
41			Self::CoreVmGuest => Vec::new(),
42		}
43	}
44
45	/// Get output file path for the specified crate name and output directory.
46	pub fn output_file(&self, out_dir: &Path, crate_name: &str) -> PathBuf {
47		let suffix = match self {
48			Self::Service | Self::Authorizer => "jam",
49			Self::CoreVmGuest => "corevm",
50		};
51		out_dir.join(format!("{crate_name}.{suffix}"))
52	}
53}
54
55pub enum ProfileType {
56	Debug,
57	Release,
58	Other(&'static str),
59}
60impl ProfileType {
61	fn as_str(&self) -> &'static str {
62		match self {
63			ProfileType::Debug => "debug",
64			ProfileType::Release => "release",
65			ProfileType::Other(s) => s,
66		}
67	}
68	fn to_arg(&self) -> String {
69		match self {
70			ProfileType::Debug => "--debug".into(),
71			ProfileType::Release => "--release".into(),
72			ProfileType::Other(s) => format!("--profile={s}"),
73		}
74	}
75}
76
77fn build_pvm_blob_in_build_script(crate_dir: &Path, blob_type: BlobType) {
78	let out_dir: PathBuf = std::env::var("OUT_DIR").expect("No OUT_DIR").into();
79	println!("cargo:rerun-if-env-changed=SKIP_PVM_BUILDS");
80	println!("cargo:rerun-if-env-changed=PVM_BUILDER_STRIP");
81	if std::env::var_os("SKIP_PVM_BUILDS").is_some() {
82		let crate_name = get_crate_info(crate_dir).name;
83		let output_file = blob_type.output_file(&out_dir, &crate_name);
84		fs::write(&output_file, []).expect("error creating dummy program blob");
85		println!("cargo:rustc-env=PVM_BINARY_{crate_name}={}", output_file.display());
86	} else {
87		println!("cargo:rerun-if-changed={}", crate_dir.to_str().unwrap());
88		let (crate_name, output_file) =
89			build_pvm_blob(crate_dir, blob_type, &out_dir, false, ProfileType::Other("production"));
90		println!("cargo:rustc-env=PVM_BINARY_{crate_name}={}", output_file.display());
91	}
92}
93
94/// Build the service crate in `crate_dir` for PVM.
95///
96/// Outputs
97/// - `{out_dir}/{crate_name}.jam` - JAM program blob,
98/// - `{out_dir}/{crate_name}.polkavm` - PolkaVM program blob for debugging.
99///
100/// The blob may be included in the relevant crate by using the [`pvm_binary`] macro.
101pub fn build_service(crate_dir: &Path) {
102	build_pvm_blob_in_build_script(crate_dir, BlobType::Service);
103}
104
105/// Build the authorizer crate in `crate_dir` for PVM.
106///
107/// Outputs
108/// - `{out_dir}/{crate_name}.jam` - JAM program blob,
109/// - `{out_dir}/{crate_name}.polkavm` - PolkaVM program blob for debugging.
110///
111/// The blob may be included in the relevant crate by using the [`pvm_binary`] macro.
112pub fn build_authorizer(crate_dir: &Path) {
113	build_pvm_blob_in_build_script(crate_dir, BlobType::Authorizer);
114}
115
116/// Build the CoreVM guest program crate in `crate_dir` for PVM.
117///
118/// Outputs
119/// - `{out_dir}/{crate_name}.corevm` - CoreVM program blob,
120/// - `{out_dir}/{crate_name}.polkavm` - PolkaVM program blob for debugging.
121///
122/// The blob may be included in the relevant crate by using the [`pvm_binary`] macro.
123pub fn build_corevm_guest(crate_dir: &Path) {
124	build_pvm_blob_in_build_script(crate_dir, BlobType::CoreVmGuest);
125}
126
127fn get_crate_info(crate_dir: &Path) -> CrateInfo {
128	let read_manifest_output = Command::new("cargo")
129		.current_dir(crate_dir)
130		.arg("read-manifest")
131		.output()
132		.unwrap_or_else(|err| {
133			panic!("Failed to run `cargo read-manifest` in {}: {err}", crate_dir.display());
134		});
135	if !read_manifest_output.status.success() {
136		panic!(
137			"Failed to read Cargo.toml manifest in {}:\n{}",
138			crate_dir.display(),
139			String::from_utf8_lossy(&read_manifest_output.stderr)
140		);
141	}
142	let man = serde_json::from_slice::<serde_json::Value>(&read_manifest_output.stdout).unwrap();
143	// read-manifest output should always contain a valid name/version
144	let name = man.get("name").unwrap().as_str().unwrap().to_string();
145	let version = man.get("version").unwrap().as_str().unwrap().to_string();
146	// read-manifest output contains "license": null when no license is specified in the Cargo.toml
147	let license = man
148		.get("license")
149		.unwrap()
150		.as_str()
151		.unwrap_or_else(|| {
152			panic!("No license specified in Cargo.toml manifest in {}", crate_dir.display());
153		})
154		.to_string();
155	// read-manifest output should always contain a valid authors list
156	let authors = man
157		.get("authors")
158		.unwrap()
159		.as_array()
160		.unwrap()
161		.iter()
162		.map(|x| x.as_str().unwrap().to_owned())
163		.collect::<Vec<String>>();
164	CrateInfo { name, version, license, authors }
165}
166
167/// Build the PVM crate in `crate_dir` for the RISCV target.
168///
169/// Outputs
170/// - depending on the `blob_type` either [JAM program blob](jam_program_blob_common::ProgramBlob)
171///   or [CoreVM program blob](jam_program_blob_common::CoreVmProgramBlob) as
172///   `{out_dir}/{crate_name}.jam` or `{out_dir}/{crate_name}.corevm` respectively;
173/// - [PolkaVM program blob](polkavm_linker::ProgramBlob) as `{out_dir}/{crate_name}.polkavm` for
174///   debugging.
175///
176/// `out_dir` is used to store any intermediate build files.
177pub fn build_pvm_blob(
178	crate_dir: &Path,
179	blob_type: BlobType,
180	out_dir: &Path,
181	install_rustc: bool,
182	profile: ProfileType,
183) -> (String, PathBuf) {
184	let (target_name, target_json_path) =
185		("riscv64emac-unknown-none-polkavm", polkavm_linker::target_json_64_path().unwrap());
186
187	println!("🪤 PVM module type: {blob_type}");
188	println!("🎯 Target name: {target_name}");
189
190	let rustup_installed = if Command::new("rustup").output().is_ok() {
191		let output = Command::new("rustup")
192			.args(["component", "list", "--toolchain", TOOLCHAIN, "--installed"])
193			.output()
194			.unwrap_or_else(|_| {
195				panic!(
196				"Failed to execute `rustup component list --toolchain {TOOLCHAIN} --installed`.\n\
197		Please install `rustup` to continue.",
198			)
199			});
200
201		if !output.status.success() ||
202			!output.stdout.split(|x| *x == b'\n').any(|x| x[..] == b"rust-src"[..])
203		{
204			if install_rustc {
205				println!("Installing rustc dependencies...");
206				let mut child = Command::new("rustup")
207					.args(["toolchain", "install", TOOLCHAIN, "-c", "rust-src"])
208					.stdout(std::process::Stdio::inherit())
209					.stderr(std::process::Stdio::inherit())
210					.spawn()
211					.unwrap_or_else(|_| {
212						panic!(
213						"Failed to execute `rustup toolchain install {TOOLCHAIN} -c rust-src`.\n\
214				Please install `rustup` to continue."
215					)
216					});
217				if !child.wait().expect("Failed to execute rustup process").success() {
218					panic!("Failed to install `rust-src` component of {TOOLCHAIN}.");
219				}
220			} else {
221				panic!("`rust-src` component of {TOOLCHAIN} is required to build the PVM binary.",);
222			}
223		}
224		println!("ℹ️ `rustup` and toolchain installed. Continuing build process...");
225
226		true
227	} else {
228		println!("ℹ️ `rustup` not installed, here be dragons. Continuing build process...");
229
230		false
231	};
232
233	let info = get_crate_info(crate_dir);
234	println!("📦 Crate name: {}", info.name);
235	println!("🏷️ Build profile: {}", profile.as_str());
236
237	let mut child = Command::new("cargo");
238
239	child
240		.current_dir(crate_dir)
241		.env_clear()
242		.env("PATH", std::env::var("PATH").unwrap())
243		.env("RUSTFLAGS", "-C panic=abort")
244		.env("CARGO_TARGET_DIR", out_dir)
245		// Support building on stable. (required for `-Zbuild-std`)
246		.env("RUSTC_BOOTSTRAP", "1");
247
248	if rustup_installed {
249		child.arg(format!("+{TOOLCHAIN}"));
250	}
251
252	child
253		.args(["rustc", "--lib", "--crate-type=cdylib", "-Z", "build-std=core,alloc"])
254		.arg(profile.to_arg())
255		.arg("--target")
256		.arg(target_json_path);
257
258	// Use job server to not oversubscribe CPU cores when compiling multiple PVM binaries in
259	// parallel.
260	if let Some(client) = get_job_server_client() {
261		client.configure(&mut child);
262	}
263
264	let mut child = child.spawn().expect("Failed to execute cargo process");
265	let status = child.wait().expect("Failed to execute cargo process");
266
267	if !status.success() {
268		eprintln!("Failed to build RISC-V ELF due to cargo execution error");
269		std::process::exit(1);
270	}
271
272	// Post processing
273	println!("Converting RISC-V ELF to PVM blob...");
274	let mut config = polkavm_linker::Config::default();
275	config.set_strip(std::env::var("PVM_BUILDER_STRIP").map(|value| value == "1").unwrap_or(true));
276	config.set_dispatch_table(blob_type.dispatch_table());
277
278	let input_root = &out_dir.join(target_name).join(profile.as_str());
279	let input_path_bin = input_root.join(&info.name);
280	let input_path_cdylib = input_root.join(format!("{}.elf", info.name.replace("-", "_")));
281
282	let input_path = if input_path_cdylib.exists() {
283		if input_path_bin.exists() {
284			eprintln!(
285				"Both {} and {} exist; run 'cargo clean' to get rid of old artifacts!",
286				input_path_cdylib.display(),
287				input_path_bin.display()
288			);
289			std::process::exit(1);
290		}
291		input_path_cdylib
292	} else if input_path_bin.exists() {
293		input_path_bin
294	} else {
295		eprintln!(
296			"Failed to build: neither {} nor {} exist",
297			input_path_cdylib.display(),
298			input_path_bin.display()
299		);
300		std::process::exit(1);
301	};
302
303	let orig =
304		fs::read(&input_path).unwrap_or_else(|e| panic!("Failed to read {input_path:?} :{e:?}"));
305	let linked = polkavm_linker::program_from_elf(config, orig.as_ref())
306		.expect("Failed to link pvm program:");
307
308	// Write out a full `.polkavm` blob for debugging/inspection.
309	let output_path_pvm = &out_dir.join(format!("{}.polkavm", &info.name));
310	fs::write(output_path_pvm, &linked).expect("Error writing resulting binary");
311	let name = info.name.clone();
312	let metadata = ConventionalMetadata::Info(info).encode().into();
313	let output_file = blob_type.output_file(out_dir, &name);
314	if !matches!(blob_type, BlobType::CoreVmGuest) {
315		let parts = polkavm_linker::ProgramParts::from_bytes(linked.into())
316			.expect("failed to deserialize linked PolkaVM program");
317		let blob = ProgramBlob::from_pvm(&parts, metadata)
318			.to_vec()
319			.expect("error serializing the .jam blob");
320		fs::write(&output_file, blob).expect("error writing the .jam blob");
321	} else {
322		let blob = CoreVmProgramBlob { metadata, pvm_blob: linked.into() }
323			.to_vec()
324			.expect("error serializing the CoreVM blob");
325		fs::write(&output_file, blob).expect("error writing the CoreVM blob");
326	}
327
328	(name, output_file)
329}
330
331fn get_job_server_client() -> Option<&'static jobserver::Client> {
332	static CLIENT: OnceLock<Option<jobserver::Client>> = OnceLock::new();
333	CLIENT.get_or_init(|| unsafe { jobserver::Client::from_env() }).as_ref()
334}
335
336/// Returns the resulting blob as a byte slice.
337#[macro_export]
338macro_rules! pvm_binary {
339	($name:literal) => {
340		include_bytes!(env!(concat!("PVM_BINARY_", $name)))
341	};
342}