nexus_sdk/legacy/
compile.rs

1use std::fmt::Display;
2use std::fs;
3use std::io;
4use std::io::Write;
5use std::path::PathBuf;
6use std::process::Command;
7use std::str::FromStr;
8use uuid::Uuid;
9
10pub use crate::error::BuildError;
11
12#[doc(hidden)]
13#[derive(Default)]
14pub enum ForProver {
15    #[default]
16    Default,
17    Jolt,
18}
19
20impl Display for ForProver {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        match self {
23            Self::Default => write!(f, "default"),
24            Self::Jolt => write!(f, "jolt"),
25        }
26    }
27}
28
29/// Options for dynamic compilation of guest programs.
30///
31/// By default, compilation occurs within `/tmp`. However, the implementation does respect the [`OUT_DIR`](https://doc.rust-lang.org/cargo/reference/environment-variables.html) environment variable.
32#[derive(Clone)]
33pub struct CompileOpts {
34    /// The (in-workspace) package to build.
35    pub package: String,
36    /// The binary produced by the build that should be loaded into the zkVM after successful compilation.
37    pub binary: String,
38    debug: bool,
39    //native: bool,
40    unique: bool,
41    pub(crate) memlimit: Option<usize>, // in mb
42}
43
44impl CompileOpts {
45    /// Setup options for dynamic compilation.
46    pub fn new(package: &str) -> Self {
47        Self {
48            package: package.to_string(),
49            binary: package.to_string(),
50            debug: false,
51            //native: false,
52            unique: false,
53            memlimit: None,
54        }
55    }
56
57    /// Setup options for dynamic compilation, using non-default binary name.
58    pub fn new_with_custom_binary(package: &str, binary: &str) -> Self {
59        Self {
60            package: package.to_string(),
61            binary: binary.to_string(),
62            debug: false,
63            //native: false,
64            unique: false,
65            memlimit: None,
66        }
67    }
68
69    /// Set dynamic compilation to build the guest program in a debug profile.
70    pub fn set_debug_build(&mut self, debug: bool) {
71        self.debug = debug;
72    }
73
74    // NOTE: SDK should be enhanced to support native building once feature parity is achieved for the runtime.
75    //   (see, https://github.com/nexus-xyz/nexus-zkvm/pull/195#discussion_r1646697743)
76    //
77    // /// Set dynamic compilation to build for the native (host machine) target, rather than for the zkVM.
78    // pub fn set_native_build(&mut self, native: bool) {
79    //     self.native = native;
80    // }
81
82    /// Set dynamic compilation to run a unique build that neither overwrites prior builds nor will be overwritten by future builds. May be used to concurrently build different versions of the same binary.
83    ///
84    /// Note: the SDK does not automatically clean or otherwise manage the resultant builds in the output directory.
85    pub fn set_unique_build(&mut self, unique: bool) {
86        self.unique = unique;
87    }
88
89    /// Set the amount of memory available to the guest program in mb. For certain provers increasing the memory limit can lead to corresponding increases in the required proving time.
90    ///
91    /// Compilation will fail if this option is set when compiling for use with [`Jolt`](crate::legacy::jolt::Jolt), which uses a fixed memory size.
92    ///
93    /// The memory limit can also be set using an argument to the `nexus_rt::main` macro (e.g., `#[nexus_rt::main(memlimit = 16)]`). The SDK _will not_ overwrite such a hardcoded value.
94    pub fn set_memlimit(&mut self, memlimit: usize) {
95        self.memlimit = Some(memlimit);
96    }
97
98    fn set_linker(&mut self, prover: &ForProver) -> Result<PathBuf, BuildError> {
99        let linker_script = match prover {
100            ForProver::Jolt => {
101                if self.memlimit.is_some() {
102                    return Err(BuildError::InvalidMemoryConfiguration);
103                }
104
105                include_str!("./linker-scripts/jolt.x").into()
106            }
107            ForProver::Default => {
108                if self.memlimit.is_none() {
109                    return Err(BuildError::InvalidMemoryConfiguration);
110                }
111
112                include_str!("./linker-scripts/default.x").replace(
113                    "{MEMORY_LIMIT}",
114                    &format!(
115                        "0x{:X}",
116                        &(self.memlimit.unwrap() as u32).saturating_mul(0x100000)
117                    ),
118                )
119            }
120        };
121
122        let linker_path =
123            PathBuf::from_str(&format!("/tmp/nexus-guest-linkers/{}.ld", prover)).unwrap();
124
125        if let Some(parent) = linker_path.parent() {
126            fs::create_dir_all(parent)?;
127        }
128
129        let mut file = fs::File::create(linker_path.clone())?;
130        file.write_all(linker_script.as_bytes())?;
131
132        Ok(linker_path)
133    }
134
135    pub(crate) fn build(&mut self, prover: &ForProver) -> Result<PathBuf, BuildError> {
136        let linker_path = self.set_linker(prover)?;
137
138        let rust_flags = [
139            "-C",
140            &format!("link-arg=-T{}", linker_path.display()),
141            "-C",
142            "panic=abort",
143        ];
144
145        // (see comment above on `set_native_build`)
146        //
147        // let target = if self.native {
148        //     "native"
149        // } else {
150        //     "riscv32i-unknown-none-elf"
151        // };
152        let target = "riscv32i-unknown-none-elf";
153
154        let profile = if self.debug { "debug" } else { "release" };
155
156        let envs = vec![("CARGO_ENCODED_RUSTFLAGS", rust_flags.join("\x1f"))];
157        let prog = self.binary.as_str();
158
159        let mut dest = match std::env::var_os("OUT_DIR") {
160            Some(path) => path.into_string().unwrap(),
161            None => "/tmp/nexus-target".into(),
162        };
163
164        if self.unique {
165            let uuid = Uuid::new_v4();
166            dest = format!("{}-{}", dest, uuid);
167        }
168
169        let cargo_bin = std::env::var("CARGO").unwrap_or_else(|_err| "cargo".into());
170        let mut cmd = Command::new(cargo_bin);
171
172        cmd.envs(envs).args([
173            "build",
174            "--package",
175            self.package.as_str(),
176            "--bin",
177            prog,
178            "--target-dir",
179            &dest,
180            "--target",
181            target,
182            "--profile",
183            profile,
184        ]);
185
186        let res = cmd.output()?;
187
188        if !res.status.success() {
189            io::stderr().write_all(&res.stderr)?;
190            return Err(BuildError::CompilerError);
191        }
192
193        let elf_path =
194            PathBuf::from_str(&format!("{}/{}/{}/{}", dest, target, profile, prog)).unwrap();
195
196        Ok(elf_path)
197    }
198}