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 jam_program_blob::{ConventionalMetadata, CrateInfo, ProgramBlob};
10use scale::Encode;
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	CoreVm,
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::CoreVm => write!(f, "CoreVm"),
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::CoreVm => vec![b"main".into()],
41		}
42	}
43}
44
45pub enum ProfileType {
46	Debug,
47	Release,
48	Other(&'static str),
49}
50impl ProfileType {
51	fn as_str(&self) -> &'static str {
52		match self {
53			ProfileType::Debug => "debug",
54			ProfileType::Release => "release",
55			ProfileType::Other(s) => s,
56		}
57	}
58	fn to_arg(&self) -> String {
59		match self {
60			ProfileType::Debug => "--debug".into(),
61			ProfileType::Release => "--release".into(),
62			ProfileType::Other(s) => format!("--profile={s}"),
63		}
64	}
65}
66
67fn build_pvm_blob_in_build_script(crate_dir: &Path, blob_type: BlobType) {
68	let out_dir: PathBuf = std::env::var("OUT_DIR").expect("No OUT_DIR").into();
69	println!("cargo:rerun-if-env-changed=SKIP_PVM_BUILDS");
70	if std::env::var_os("SKIP_PVM_BUILDS").is_some() {
71		let output_file = out_dir.join(format!("{}.jam", &get_crate_info(crate_dir).name));
72		fs::write(&output_file, []).expect("error creating dummy .jam blob");
73	} else {
74		println!("cargo:rerun-if-changed={}", crate_dir.to_str().unwrap());
75		build_pvm_blob(crate_dir, blob_type, &out_dir, false, ProfileType::Release);
76	}
77}
78
79/// Build the service crate in `crate_dir` for the RISCV target, convert to PVM code and finish
80/// by creating a `<crate_name>.pvm` blob file.
81///
82/// This is intended to be called from a crate's `build.rs`. The generated blob may be included in
83/// the crate by using the `pvm_binary` macro.
84pub fn build_service(crate_dir: &Path) {
85	build_pvm_blob_in_build_script(crate_dir, BlobType::Service);
86}
87
88/// Build the authorizer crate in `crate_dir` for the RISCV target, convert to PVM code and finish
89/// by creating a `<crate_name>.pvm` blob file.
90///
91/// This is intended to be called from a crate's `build.rs`. The generated blob may be included in
92/// the crate by using the `pvm_binary` macro.
93pub fn build_authorizer(crate_dir: &Path) {
94	build_pvm_blob_in_build_script(crate_dir, BlobType::Authorizer);
95}
96
97/// Build the CoreVM guest program crate in `crate_dir` for the RISCV target, convert to PVM code
98/// and finish by creating a `<crate_name>.pvm` blob file.
99///
100/// If used in `build.rs`, then this may be included in the relevant crate by using the `pvm_binary`
101/// macro.
102pub fn build_core_vm(crate_dir: &Path) {
103	build_pvm_blob_in_build_script(crate_dir, BlobType::CoreVm);
104}
105
106fn get_crate_info(crate_dir: &Path) -> CrateInfo {
107	let manifest = Command::new("cargo")
108		.current_dir(crate_dir)
109		.arg("read-manifest")
110		.output()
111		.unwrap()
112		.stdout;
113	let man = serde_json::from_slice::<serde_json::Value>(&manifest).unwrap();
114	let name = man.get("name").unwrap().as_str().unwrap().to_string();
115	let version = man.get("version").unwrap().as_str().unwrap().to_string();
116	let license = man.get("license").unwrap().as_str().unwrap().to_string();
117	let authors = man
118		.get("authors")
119		.unwrap()
120		.as_array()
121		.unwrap()
122		.iter()
123		.map(|x| x.as_str().unwrap().to_owned())
124		.collect::<Vec<String>>();
125	CrateInfo { name, version, license, authors }
126}
127
128/// Build the PVM crate in `crate_dir` called `crate_name` for the RISCV target, convert to PVM
129/// code and finish by creating a `.pvm` blob file of path `output_file`. `out_dir` is used to store
130/// any intermediate build files.
131pub fn build_pvm_blob(
132	crate_dir: &Path,
133	blob_type: BlobType,
134	out_dir: &Path,
135	install_rustc: bool,
136	profile: ProfileType,
137) -> (String, PathBuf) {
138	let (target_name, target_json_path) =
139		("riscv64emac-unknown-none-polkavm", polkavm_linker::target_json_64_path().unwrap());
140
141	println!("🪤 PVM module type: {}", blob_type);
142	println!("🎯 Target name: {}", target_name);
143
144	let rustup_installed = if Command::new("rustup").output().is_ok() {
145		let output = Command::new("rustup")
146			.args(["component", "list", "--toolchain", TOOLCHAIN, "--installed"])
147			.output()
148			.unwrap_or_else(|_| {
149				panic!(
150				"Failed to execute `rustup component list --toolchain {TOOLCHAIN} --installed`.\n\
151		Please install `rustup` to continue.",
152			)
153			});
154
155		if !output.status.success() ||
156			!output.stdout.split(|x| *x == b'\n').any(|x| x[..] == b"rust-src"[..])
157		{
158			if install_rustc {
159				println!("Installing rustc dependencies...");
160				let mut child = Command::new("rustup")
161					.args(["toolchain", "install", TOOLCHAIN, "-c", "rust-src"])
162					.stdout(std::process::Stdio::inherit())
163					.stderr(std::process::Stdio::inherit())
164					.spawn()
165					.unwrap_or_else(|_| {
166						panic!(
167						"Failed to execute `rustup toolchain install {TOOLCHAIN} -c rust-src`.\n\
168				Please install `rustup` to continue."
169					)
170					});
171				if !child.wait().expect("Failed to execute rustup process").success() {
172					panic!("Failed to install `rust-src` component of {TOOLCHAIN}.");
173				}
174			} else {
175				panic!("`rust-src` component of {TOOLCHAIN} is required to build the PVM binary.",);
176			}
177		}
178
179		true
180	} else {
181		println!("ℹ️ `rustup` not installed, here be dragons. Continuing build process...");
182
183		false
184	};
185
186	let info = get_crate_info(crate_dir);
187	println!("📦 Crate name: {}", info.name);
188	println!("🏷️ Build profile: {}", profile.as_str());
189
190	let mut child = Command::new("cargo");
191
192	child
193		.current_dir(crate_dir)
194		.env_clear()
195		.env("PATH", std::env::var("PATH").unwrap())
196		.env("RUSTFLAGS", "-C panic=abort")
197		.env("CARGO_TARGET_DIR", out_dir)
198		// Support building on stable. (required for `-Zbuild-std`)
199		.env("RUSTC_BOOTSTRAP", "1");
200
201	if rustup_installed {
202		child.arg(format!("+{TOOLCHAIN}"));
203	}
204
205	child
206		.args(["build", "-Z", "build-std=core,alloc"])
207		.arg(profile.to_arg())
208		.arg("--target")
209		.arg(target_json_path)
210		.arg("--features")
211		.arg(if cfg!(feature = "tiny") { "tiny" } else { "" });
212
213	// Use job server to not oversubscribe CPU cores when compiling multiple PVM binaries in
214	// parallel.
215	if let Some(client) = get_job_server_client() {
216		client.configure(&mut child);
217	}
218
219	let mut child = child.spawn().expect("Failed to execute cargo process");
220	let status = child.wait().expect("Failed to execute cargo process");
221
222	if !status.success() {
223		eprintln!("Failed to build RISC-V ELF due to cargo execution error");
224		std::process::exit(1);
225	}
226
227	// Post processing
228	println!("Converting RISC-V ELF to PVM blob...");
229	let mut config = polkavm_linker::Config::default();
230	config.set_strip(true);
231	config.set_dispatch_table(blob_type.dispatch_table());
232	let input_path = &out_dir.join(target_name).join(profile.as_str()).join(&info.name);
233	let orig =
234		fs::read(input_path).unwrap_or_else(|e| panic!("Failed to read {:?} :{:?}", input_path, e));
235	let linked = polkavm_linker::program_from_elf(config, orig.as_ref())
236		.expect("Failed to link polkavm program:");
237
238	// Write out a full `.polkavm` blob for debugging/inspection.
239	let output_path_polkavm = &out_dir.join(format!("{}.polkavm", &info.name));
240	fs::write(output_path_polkavm, &linked).expect("Error writing resulting binary");
241
242	let parts = polkavm_linker::ProgramParts::from_bytes(linked.into())
243		.expect("failed to deserialize linked PolkaVM program");
244
245	let rw_data_padding = parts.rw_data_size as usize - parts.rw_data.len();
246	let rw_data_padding_pages = rw_data_padding / 4096;
247
248	let mut ro_data = parts.ro_data.to_vec();
249	let mut rw_data = parts.rw_data.to_vec();
250
251	ro_data.resize(parts.ro_data_size as usize, 0);
252	rw_data.resize(ro_data.len() + parts.rw_data_size as usize - rw_data_padding_pages * 4096, 0);
253
254	let name = info.name.clone();
255
256	let rw_data_padding_pages: u16 =
257		rw_data_padding_pages.try_into().expect("the RW data section is too big");
258	let blob_jam = ProgramBlob {
259		metadata: ConventionalMetadata::Info(info).encode().into(),
260		ro_data: ro_data.into(),
261		rw_data: (&parts.rw_data[..]).into(),
262		code_blob: (&parts.code_and_jump_table[..]).into(),
263		rw_data_padding_pages,
264		stack_size: parts.stack_size,
265	};
266
267	let output_file = out_dir.join(format!("{}.jam", &name));
268	fs::write(&output_file, blob_jam.to_vec().expect("error serializing the .jam blob"))
269		.expect("error writing the .jam blob");
270
271	(name, output_file)
272}
273
274fn get_job_server_client() -> Option<&'static jobserver::Client> {
275	static CLIENT: OnceLock<Option<jobserver::Client>> = OnceLock::new();
276	CLIENT.get_or_init(|| unsafe { jobserver::Client::from_env() }).as_ref()
277}
278
279#[macro_export]
280macro_rules! pvm_binary {
281	($name:literal) => {
282		include_bytes!(concat!(env!("OUT_DIR"), "/", $name, ".jam"))
283	};
284}