1#![allow(clippy::unwrap_used)]
5
6const 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 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
134pub fn build_service(crate_name: &str, crate_dir: &Path) {
140 build_pvm_blob_in_build_script(crate_name, crate_dir, BlobType::Service);
141}
142
143pub fn build_authorizer(crate_name: &str, crate_dir: &Path) {
149 build_pvm_blob_in_build_script(crate_name, crate_dir, BlobType::Authorizer);
150}
151
152pub 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
183pub 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 .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 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 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 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}