1#![allow(clippy::unwrap_used)]
5
6const 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 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
118pub fn build_service(crate_name: &str, crate_dir: &Path) {
124 build_pvm_blob_in_build_script(crate_name, crate_dir, BlobType::Service);
125}
126
127pub fn build_authorizer(crate_name: &str, crate_dir: &Path) {
133 build_pvm_blob_in_build_script(crate_name, crate_dir, BlobType::Authorizer);
134}
135
136pub 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
167pub 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 .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 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 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 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}