1#![allow(clippy::unwrap_used)]
5
6const 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub enum BlobType {
22 Service,
24 Authorizer,
26 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 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
94pub fn build_service(crate_dir: &Path) {
102 build_pvm_blob_in_build_script(crate_dir, BlobType::Service);
103}
104
105pub fn build_authorizer(crate_dir: &Path) {
113 build_pvm_blob_in_build_script(crate_dir, BlobType::Authorizer);
114}
115
116pub 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 let name = man.get("name").unwrap().as_str().unwrap().to_string();
145 let version = man.get("version").unwrap().as_str().unwrap().to_string();
146 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 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
167pub 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 .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 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 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 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#[macro_export]
338macro_rules! pvm_binary {
339 ($name:literal) => {
340 include_bytes!(env!(concat!("PVM_BINARY_", $name)))
341 };
342}