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_name: &str, crate_dir: &Path, blob_type: BlobType) {
68	let out_dir: PathBuf = std::env::var("OUT_DIR").expect("No OUT_DIR").into();
69	let crate_dir = if !crate_dir.exists() {
70		// This provided path doesn't exist - this probably means we're building one crate at a
71		// time. It should still be available, but in the dependencies folder.
72		println!("Provided source path invalid. Presume building from crates.io");
73		let cd = std::env::current_dir().unwrap();
74		println!("Current path: {}", cd.display());
75
76		let lock = cd.join("Cargo.lock");
77		if !lock.exists() {
78			panic!("Cargo.lock not found in current directory. Presume building from crates.io");
79		}
80		let lock = fs::read_to_string(lock).expect("Failed to read Cargo.lock").parse::<toml::Value>().unwrap();
81		let package = lock["package"].as_array().unwrap()
82			.iter()
83			.filter_map(|x| x.as_table().map(|x| x.to_owned()))
84			.find(|x| x.get("name").unwrap().as_str().unwrap() == crate_name)
85			.expect("Dependency not found in Cargo.lock. Cannot continue.");
86		let version = package.get("version").unwrap().as_str().unwrap();
87
88		println!("Found dependency {crate_name} in manifest of version {version}");
89		let mut source_path = cd.clone();
90		source_path.pop();
91		source_path.push(&format!("{crate_name}-{version}"));
92		if source_path.exists() {
93			println!("Found source path: {}", source_path.display());
94			source_path
95		} else {
96			println!("Dependency source not found at {}. Packages found:", source_path.display());
97			for entry in std::fs::read_dir(cd.parent().unwrap()).unwrap() {
98				let entry = entry.unwrap();
99				if entry.file_type().unwrap().is_dir() {
100					println!("  - {}", entry.file_name().to_string_lossy());
101				}
102			}
103			panic!("Cannot continue.");
104		}
105	} else {
106		crate_dir.to_owned()
107	};
108	println!("cargo:rerun-if-env-changed=SKIP_PVM_BUILDS");
109	if std::env::var_os("SKIP_PVM_BUILDS").is_some() {
110		let output_file = out_dir.join(format!("{}.jam", &crate_name));
111		fs::write(&output_file, []).expect("error creating dummy .jam blob");
112	} else {
113		println!("cargo:rerun-if-changed={}", crate_dir.to_str().unwrap());
114		build_pvm_blob(&crate_dir, blob_type, &out_dir, false, ProfileType::Release);
115	}
116}
117
118/// Build the service crate in `crate_dir` for the RISCV target, convert to PVM code and finish
119/// by creating a `<crate_name>.pvm` blob file.
120///
121/// This is intended to be called from a crate's `build.rs`. The generated blob may be included in
122/// the crate by using the `pvm_binary` macro.
123pub fn build_service(crate_name: &str, crate_dir: &Path) {
124	build_pvm_blob_in_build_script(crate_name, crate_dir, BlobType::Service);
125}
126
127/// Build the authorizer crate in `crate_dir` for the RISCV target, convert to PVM code and finish
128/// by creating a `<crate_name>.pvm` blob file.
129///
130/// This is intended to be called from a crate's `build.rs`. The generated blob may be included in
131/// the crate by using the `pvm_binary` macro.
132pub fn build_authorizer(crate_name: &str, crate_dir: &Path) {
133	build_pvm_blob_in_build_script(crate_name, crate_dir, BlobType::Authorizer);
134}
135
136/// Build the CoreVM guest program crate in `crate_dir` for the RISCV target, convert to PVM code
137/// and finish by creating a `<crate_name>.pvm` blob file.
138///
139/// If used in `build.rs`, then this may be included in the relevant crate by using the `pvm_binary`
140/// macro.
141pub fn build_core_vm(crate_name: &str, crate_dir: &Path) {
142	build_pvm_blob_in_build_script(crate_name, crate_dir, BlobType::CoreVm);
143}
144
145fn get_crate_info(crate_dir: &Path) -> CrateInfo {
146	let manifest = Command::new("cargo")
147		.current_dir(crate_dir)
148		.arg("read-manifest")
149		.output()
150		.unwrap()
151		.stdout;
152	let man = serde_json::from_slice::<serde_json::Value>(&manifest).unwrap();
153	let name = man.get("name").unwrap().as_str().unwrap().to_string();
154	let version = man.get("version").unwrap().as_str().unwrap().to_string();
155	let license = man.get("license").unwrap().as_str().unwrap().to_string();
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` called `crate_name` for the RISCV target, convert to PVM
168/// code and finish by creating a `.pvm` blob file of path `output_file`. `out_dir` is used to store
169/// any intermediate build files.
170pub fn build_pvm_blob(
171	crate_dir: &Path,
172	blob_type: BlobType,
173	out_dir: &Path,
174	install_rustc: bool,
175	profile: ProfileType,
176) -> (String, PathBuf) {
177	let (target_name, target_json_path) =
178		("riscv64emac-unknown-none-polkavm", polkavm_linker::target_json_64_path().unwrap());
179
180	println!("🪤 PVM module type: {}", blob_type);
181	println!("🎯 Target name: {}", target_name);
182
183	let rustup_installed = if Command::new("rustup").output().is_ok() {
184		let output = Command::new("rustup")
185			.args(["component", "list", "--toolchain", TOOLCHAIN, "--installed"])
186			.output()
187			.unwrap_or_else(|_| {
188				panic!(
189				"Failed to execute `rustup component list --toolchain {TOOLCHAIN} --installed`.\n\
190		Please install `rustup` to continue.",
191			)
192			});
193
194		if !output.status.success() ||
195			!output.stdout.split(|x| *x == b'\n').any(|x| x[..] == b"rust-src"[..])
196		{
197			if install_rustc {
198				println!("Installing rustc dependencies...");
199				let mut child = Command::new("rustup")
200					.args(["toolchain", "install", TOOLCHAIN, "-c", "rust-src"])
201					.stdout(std::process::Stdio::inherit())
202					.stderr(std::process::Stdio::inherit())
203					.spawn()
204					.unwrap_or_else(|_| {
205						panic!(
206						"Failed to execute `rustup toolchain install {TOOLCHAIN} -c rust-src`.\n\
207				Please install `rustup` to continue."
208					)
209					});
210				if !child.wait().expect("Failed to execute rustup process").success() {
211					panic!("Failed to install `rust-src` component of {TOOLCHAIN}.");
212				}
213			} else {
214				panic!("`rust-src` component of {TOOLCHAIN} is required to build the PVM binary.",);
215			}
216		}
217		println!("ℹ️ `rustup` and toolchain installed. Continuing build process...");
218
219		true
220	} else {
221		println!("ℹ️ `rustup` not installed, here be dragons. Continuing build process...");
222
223		false
224	};
225
226	let info = get_crate_info(crate_dir);
227	println!("📦 Crate name: {}", info.name);
228	println!("🏷️ Build profile: {}", profile.as_str());
229
230	let mut child = Command::new("cargo");
231
232	child
233		.current_dir(crate_dir)
234		.env_clear()
235		.env("PATH", std::env::var("PATH").unwrap())
236		.env("RUSTFLAGS", "-C panic=abort")
237		.env("CARGO_TARGET_DIR", out_dir)
238		// Support building on stable. (required for `-Zbuild-std`)
239		.env("RUSTC_BOOTSTRAP", "1");
240
241	if rustup_installed {
242		child.arg(format!("+{TOOLCHAIN}"));
243	}
244
245	child
246		.args(["build", "-Z", "build-std=core,alloc"])
247		.arg(profile.to_arg())
248		.arg("--target")
249		.arg(target_json_path)
250		.arg("--features")
251		.arg(if cfg!(feature = "tiny") { "tiny" } else { "" });
252
253	// Use job server to not oversubscribe CPU cores when compiling multiple PVM binaries in
254	// parallel.
255	if let Some(client) = get_job_server_client() {
256		client.configure(&mut child);
257	}
258
259	let mut child = child.spawn().expect("Failed to execute cargo process");
260	let status = child.wait().expect("Failed to execute cargo process");
261
262	if !status.success() {
263		eprintln!("Failed to build RISC-V ELF due to cargo execution error");
264		std::process::exit(1);
265	}
266
267	// Post processing
268	println!("Converting RISC-V ELF to PVM blob...");
269	let mut config = polkavm_linker::Config::default();
270	config.set_strip(true);
271	config.set_dispatch_table(blob_type.dispatch_table());
272	let input_path = &out_dir.join(target_name).join(profile.as_str()).join(&info.name);
273	let orig =
274		fs::read(input_path).unwrap_or_else(|e| panic!("Failed to read {:?} :{:?}", input_path, e));
275	let linked = polkavm_linker::program_from_elf(config, orig.as_ref())
276		.expect("Failed to link polkavm program:");
277
278	// Write out a full `.polkavm` blob for debugging/inspection.
279	let output_path_polkavm = &out_dir.join(format!("{}.polkavm", &info.name));
280	fs::write(output_path_polkavm, &linked).expect("Error writing resulting binary");
281
282	let parts = polkavm_linker::ProgramParts::from_bytes(linked.into())
283		.expect("failed to deserialize linked PolkaVM program");
284
285	let rw_data_padding = parts.rw_data_size as usize - parts.rw_data.len();
286	let rw_data_padding_pages = rw_data_padding / 4096;
287
288	let mut ro_data = parts.ro_data.to_vec();
289	let mut rw_data = parts.rw_data.to_vec();
290
291	ro_data.resize(parts.ro_data_size as usize, 0);
292	rw_data.resize(ro_data.len() + parts.rw_data_size as usize - rw_data_padding_pages * 4096, 0);
293
294	let name = info.name.clone();
295
296	let rw_data_padding_pages: u16 =
297		rw_data_padding_pages.try_into().expect("the RW data section is too big");
298	let blob_jam = ProgramBlob {
299		metadata: ConventionalMetadata::Info(info).encode().into(),
300		ro_data: ro_data.into(),
301		rw_data: (&parts.rw_data[..]).into(),
302		code_blob: (&parts.code_and_jump_table[..]).into(),
303		rw_data_padding_pages,
304		stack_size: parts.stack_size,
305	};
306
307	let output_file = out_dir.join(format!("{}.jam", &name));
308	fs::write(&output_file, blob_jam.to_vec().expect("error serializing the .jam blob"))
309		.expect("error writing the .jam blob");
310
311	(name, output_file)
312}
313
314fn get_job_server_client() -> Option<&'static jobserver::Client> {
315	static CLIENT: OnceLock<Option<jobserver::Client>> = OnceLock::new();
316	CLIENT.get_or_init(|| unsafe { jobserver::Client::from_env() }).as_ref()
317}
318
319#[macro_export]
320macro_rules! pvm_binary {
321	($name:literal) => {
322		include_bytes!(concat!(env!("OUT_DIR"), "/", $name, ".jam"))
323	};
324}