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