cjam/
builder.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 = "1.90.0";
8
9use crate::manifest;
10use jam_codec::Encode;
11use jam_program_blob::{ConventionalMetadata, CoreVmProgramBlob, ProgramBlob};
12use polkavm_linker::ProgramParts;
13use std::{
14    borrow::Cow,
15    fmt::Display,
16    fs,
17    path::{Path, PathBuf},
18    process::Command,
19};
20
21/// Type of blob to build
22pub enum BlobType {
23    /// Service blob
24    Service,
25    /// Authorizer blob
26    Authorizer,
27    /// CoreVM guest blob
28    CoreVmGuest,
29}
30impl Display for BlobType {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        match self {
33            Self::Service => write!(f, "Service"),
34            Self::Authorizer => write!(f, "Authorizer"),
35            Self::CoreVmGuest => write!(f, "CoreVmGuest"),
36        }
37    }
38}
39
40impl BlobType {
41    /// Get the dispatch table for the blob type
42    pub fn dispatch_table(&self) -> Vec<Vec<u8>> {
43        match self {
44            Self::Service => vec![b"jade_refine".into(), b"jade_accumulate".into()],
45            Self::Authorizer => vec![b"jade_is_authorized".into()],
46            Self::CoreVmGuest => vec![b"main".into()],
47        }
48    }
49
50    fn output_file(&self, out_dir: &Path, crate_name: &str) -> PathBuf {
51        let suffix = match self {
52            Self::Service | Self::Authorizer => "jam",
53            Self::CoreVmGuest => "corevm",
54        };
55        out_dir.join(format!("{crate_name}.{suffix}"))
56    }
57}
58
59/// Type of profile to build
60pub enum ProfileType {
61    /// Debug profile
62    Debug,
63    /// Release profile
64    Release,
65    /// Other profile
66    Other(&'static str),
67}
68impl ProfileType {
69    fn as_str(&self) -> &'static str {
70        match self {
71            ProfileType::Debug => "debug",
72            ProfileType::Release => "release",
73            ProfileType::Other(s) => s,
74        }
75    }
76    fn to_arg(&self) -> String {
77        match self {
78            ProfileType::Debug => "--debug".into(),
79            ProfileType::Release => "--release".into(),
80            ProfileType::Other(s) => format!("--profile={s}"),
81        }
82    }
83}
84
85/// Build the PVM crate in `crate_dir` called `crate_name` for the RISCV target, convert to PVM
86/// code and finish by creating a `.pvm` blob file of path `output_file`. `out_dir` is used to store
87/// any intermediate build files.
88pub fn build_pvm_blob(
89    crate_dir: &Path,
90    blob_type: BlobType,
91    out_dir: &Path,
92    install_rustc: bool,
93    profile: ProfileType,
94) -> (String, PathBuf) {
95    let (target_name, target_json_path) = (
96        "riscv64emac-unknown-none-polkavm",
97        polkavm_linker::target_json_64_path().unwrap(),
98    );
99
100    println!("🪤 PVM module type: {blob_type}");
101    println!("🎯 Target name: {target_name}");
102
103    let rustup_installed = if Command::new("rustup").output().is_ok() {
104        let output = Command::new("rustup")
105            .args(["component", "list", "--toolchain", TOOLCHAIN, "--installed"])
106            .output()
107            .unwrap_or_else(|_| {
108                panic!(
109				"Failed to execute `rustup component list --toolchain {TOOLCHAIN} --installed`.\n\
110		Please install `rustup` to continue.",
111			)
112            });
113
114        if !output.status.success()
115            || !output
116                .stdout
117                .split(|x| *x == b'\n')
118                .any(|x| x[..] == b"rust-src"[..])
119        {
120            if install_rustc {
121                println!("Installing rustc dependencies...");
122                let mut child = Command::new("rustup")
123                    .args(["toolchain", "install", TOOLCHAIN, "-c", "rust-src"])
124                    .stdout(std::process::Stdio::inherit())
125                    .stderr(std::process::Stdio::inherit())
126                    .spawn()
127                    .unwrap_or_else(|_| {
128                        panic!(
129						"Failed to execute `rustup toolchain install {TOOLCHAIN} -c rust-src`.\n\
130				Please install `rustup` to continue."
131					)
132                    });
133                if !child
134                    .wait()
135                    .expect("Failed to execute rustup process")
136                    .success()
137                {
138                    panic!("Failed to install `rust-src` component of {TOOLCHAIN}.");
139                }
140            } else {
141                panic!("`rust-src` component of {TOOLCHAIN} is required to build the PVM binary.",);
142            }
143        }
144        println!("ℹ️ `rustup` and toolchain installed. Continuing build process...");
145
146        true
147    } else {
148        println!("ℹ️ `rustup` not installed, here be dragons. Continuing build process...");
149
150        false
151    };
152
153    let info = manifest::crate_info(crate_dir).expect("failed to get crate info");
154    println!("📦 Crate name: {}", info.name);
155    println!("🏷️ Build profile: {}", profile.as_str());
156
157    let mut child = Command::new("cargo");
158
159    child
160        .current_dir(crate_dir)
161        .env_clear()
162        .env("PATH", std::env::var("PATH").unwrap())
163        .env("RUSTFLAGS", "-C panic=abort")
164        .env("CARGO_TARGET_DIR", out_dir)
165        // Support building on stable. (required for `-Zbuild-std`)
166        .env("RUSTC_BOOTSTRAP", "1");
167
168    if rustup_installed {
169        child.arg(format!("+{TOOLCHAIN}"));
170    }
171
172    child
173        .args(["rustc", "-Z", "build-std=core,alloc"])
174        .arg(profile.to_arg())
175        .arg("--target")
176        .arg(target_json_path)
177        .arg("--features")
178        .arg(if !matches!(blob_type, BlobType::CoreVmGuest) {
179            "tiny"
180        } else {
181            ""
182        })
183        .arg("--lib")
184        .arg("--crate-type=cdylib")
185        .env(
186            "RUSTFLAGS",
187            format!(
188                "{} -C link-arg=--strip-debug",
189                std::env::var("RUSTFLAGS").unwrap_or_default()
190            ),
191        );
192
193    let mut child = child.spawn().expect("Failed to execute cargo process");
194    let status = child.wait().expect("Failed to execute cargo process");
195
196    if !status.success() {
197        eprintln!("Failed to build RISC-V ELF due to cargo execution error");
198        std::process::exit(1);
199    }
200
201    // Post processing
202    println!("Converting RISC-V ELF to PVM blob...");
203    let mut config = polkavm_linker::Config::default();
204    config.set_strip(true);
205    config.set_dispatch_table(blob_type.dispatch_table());
206
207    let input_root = &out_dir.join(target_name).join(profile.as_str());
208    let input_path_bin = input_root.join(&info.name);
209    let input_path_cdylib = input_root.join(format!("{}.elf", info.name.replace("-", "_")));
210    let input_path = if input_path_cdylib.exists() {
211        if input_path_bin.exists() {
212            eprintln!(
213                "Both {} and {} exist; run 'cargo clean' to get rid of old artifacts!",
214                input_path_cdylib.display(),
215                input_path_bin.display()
216            );
217            std::process::exit(1);
218        }
219        input_path_cdylib
220    } else if input_path_bin.exists() {
221        input_path_bin
222    } else {
223        eprintln!(
224            "Failed to build: neither {} nor {} exist",
225            input_path_cdylib.display(),
226            input_path_bin.display()
227        );
228        std::process::exit(1);
229    };
230
231    let orig =
232        fs::read(&input_path).unwrap_or_else(|e| panic!("Failed to read {input_path:?} :{e:?}"));
233    let linked = polkavm_linker::program_from_elf(config, orig.as_ref())
234        .expect("Failed to link pvm program:");
235
236    // Write out a full `.pvm` blob for debugging/inspection.
237    let jam_out = out_dir.join("jam");
238    fs::create_dir_all(&jam_out).expect("Failed to create jam directory");
239    let output_path_pvm = jam_out.join(format!("{}.pvm", &info.name));
240    fs::write(output_path_pvm, &linked).expect("Error writing resulting binary");
241    let name = info.name.clone();
242    let metadata = ConventionalMetadata::Info(info).encode().into();
243    let output_file = blob_type.output_file(&jam_out, &name);
244    if !matches!(blob_type, BlobType::CoreVmGuest) {
245        let parts = polkavm_linker::ProgramParts::from_bytes(linked.into())
246            .expect("failed to deserialize linked PolkaVM program");
247        let blob = self::to_blob(&parts, metadata)
248            .to_vec()
249            .expect("error serializing the .jam blob");
250        fs::write(&output_file, blob).expect("error writing the .jam blob");
251    } else {
252        let blob = CoreVmProgramBlob {
253            metadata,
254            pvm_blob: linked.into(),
255        }
256        .to_vec()
257        .expect("error serializing the CoreVM blob");
258        fs::write(&output_file, blob).expect("error writing the CoreVM blob");
259    }
260
261    (name, output_file)
262}
263
264/// Macro to include the PVM binary
265#[macro_export]
266macro_rules! pvm_binary {
267    ($name:literal) => {
268        include_bytes!(env!("PVM_BINARY"))
269    };
270}
271
272fn to_blob<'a>(parts: &'a ProgramParts, metadata: Cow<'a, [u8]>) -> ProgramBlob<'a> {
273    // Pad RO section with zeroes.
274    let mut ro_data = parts.ro_data.to_vec();
275    ro_data.resize(parts.ro_data_size as usize, 0);
276    // Calculate the padding for RW section.
277    let padding = (parts.rw_data_size as usize).next_multiple_of(4096)
278        - parts.rw_data.len().next_multiple_of(4096);
279    let rw_data_padding_pages = padding / 4096;
280    let rw_data_padding_pages = rw_data_padding_pages
281        .try_into()
282        .expect("The RW data section is too big");
283    ProgramBlob {
284        metadata,
285        ro_data: ro_data.into(),
286        rw_data: (&parts.rw_data[..]).into(),
287        code_blob: (&parts.code_and_jump_table[..]).into(),
288        rw_data_padding_pages,
289        stack_size: parts.stack_size,
290    }
291}