Skip to main content

sp1_cli/commands/
build_toolchain.rs

1use anyhow::{Context, Result};
2use clap::Parser;
3use std::{
4    path::PathBuf,
5    process::{Command, Stdio},
6};
7
8use crate::{get_target, LATEST_SUPPORTED_TOOLCHAIN_VERSION_TAG, RUSTUP_TOOLCHAIN_NAME};
9
10// There is a lot of Commands in this module, having this trait back can
11// help us simplify the code a bit.
12trait CommandExecutor {
13    /// Run the command to completion, returning an error if it could not be
14    /// spawned or if it exited with a non-zero status.
15    fn run(&mut self) -> Result<()>;
16}
17
18impl CommandExecutor for Command {
19    fn run(&mut self) -> Result<()> {
20        let status = self
21            .stderr(Stdio::inherit())
22            .stdout(Stdio::inherit())
23            .stdin(Stdio::inherit())
24            .status()
25            .with_context(|| format!("while executing {:?}", self.get_program()))?;
26
27        status
28            .success()
29            .then_some(())
30            .with_context(|| format!("{:?} failed: {status}", self.get_program()))
31    }
32}
33
34#[derive(Parser)]
35#[command(name = "build-toolchain", about = "Build the cargo-prove toolchain.")]
36pub struct BuildToolchainCmd {}
37
38impl BuildToolchainCmd {
39    pub fn run(&self) -> Result<()> {
40        // Get environment variables.
41        let github_access_token = std::env::var("GITHUB_ACCESS_TOKEN");
42        let build_dir = std::env::var("SP1_BUILD_DIR");
43
44        // Clone our rust fork, if necessary.
45        let rust_dir = match build_dir {
46            Ok(build_dir) => {
47                println!("Detected SP1_BUILD_DIR, skipping cloning rust.");
48                PathBuf::from(build_dir).join("rust")
49            }
50            Err(_) => {
51                let temp_dir = std::env::temp_dir();
52                let dir = temp_dir.join("sp1-rust");
53                if dir.exists() {
54                    std::fs::remove_dir_all(&dir)?;
55                }
56
57                println!("No SP1_BUILD_DIR detected, cloning rust.");
58                let repo_url = match github_access_token {
59                    Ok(github_access_token) => {
60                        println!("Detected GITHUB_ACCESS_TOKEN, using it to clone rust.");
61                        format!("https://{github_access_token}@github.com/succinctlabs/rust")
62                    }
63                    Err(_) => {
64                        println!("No GITHUB_ACCESS_TOKEN detected. If you get throttled by Github, set it to bypass the rate limit.");
65                        "ssh://git@github.com/succinctlabs/rust".to_string()
66                    }
67                };
68                Command::new("git")
69                    .args([
70                        "clone",
71                        &repo_url,
72                        "--depth=1",
73                        "--single-branch",
74                        &format!("--branch={LATEST_SUPPORTED_TOOLCHAIN_VERSION_TAG}"),
75                        "sp1-rust",
76                    ])
77                    .current_dir(&temp_dir)
78                    .run()?;
79                Command::new("git").args(["reset", "--hard"]).current_dir(&dir).run()?;
80                Command::new("git")
81                    .args(["submodule", "update", "--init", "--recursive", "--progress"])
82                    .current_dir(&dir)
83                    .run()?;
84                dir
85            }
86        };
87
88        // Install our bootstrap.toml.
89        let bootstrap_toml = include_str!("bootstrap.toml");
90        let bootstrap_file = rust_dir.join("bootstrap.toml");
91        std::fs::write(&bootstrap_file, bootstrap_toml)
92            .with_context(|| format!("while writing configuration to {bootstrap_file:?}"))?;
93
94        // Work around target sanity check added in
95        // rust-lang/rust@09c076810cb7649e5817f316215010d49e78e8d7.
96        let temp_dir = std::env::temp_dir().join("rustc-targets");
97        if !temp_dir.exists() {
98            std::fs::create_dir_all(&temp_dir)?;
99        }
100        std::fs::File::create(temp_dir.join("riscv32im-succinct-zkvm-elf.json"))?;
101
102        // Build the toolchain.
103        Command::new("python3")
104            .env("RUST_TARGET_PATH", &temp_dir)
105            .env("CARGO_TARGET_RISCV32IM_SUCCINCT_ZKVM_ELF_RUSTFLAGS", "-Cpasses=lower-atomic")
106            .env("CARGO_TARGET_RISCV64IM_SUCCINCT_ZKVM_ELF_RUSTFLAGS", "-Cpasses=lower-atomic")
107            .args([
108                "x.py",
109                "build",
110                "--stage",
111                "2",
112                "compiler/rustc",
113                "library",
114                "--target",
115                &format!(
116                    "riscv32im-succinct-zkvm-elf,riscv64im-succinct-zkvm-elf,{}",
117                    get_target()
118                ),
119            ])
120            .current_dir(&rust_dir)
121            .run()?;
122
123        // Remove the existing toolchain from rustup, if it exists.
124        match Command::new("rustup").args(["toolchain", "remove", RUSTUP_TOOLCHAIN_NAME]).run() {
125            Ok(_) => println!("Successfully removed existing toolchain."),
126            Err(_) => println!("No existing toolchain to remove."),
127        }
128
129        // Find the toolchain directory.
130        let mut toolchain_dir = None;
131        for wentry in std::fs::read_dir(rust_dir.join("build"))? {
132            let entry = wentry?;
133            let toolchain_dir_candidate = entry.path().join("stage2");
134            if toolchain_dir_candidate.is_dir() {
135                toolchain_dir = Some(toolchain_dir_candidate);
136                break;
137            }
138        }
139        let toolchain_dir = toolchain_dir.unwrap();
140        println!(
141            "Found built toolchain directory at {}.",
142            toolchain_dir.as_path().to_str().unwrap()
143        );
144
145        // Link the toolchain to rustup.
146        Command::new("rustup")
147            .args(["toolchain", "link", RUSTUP_TOOLCHAIN_NAME])
148            .arg(&toolchain_dir)
149            .run()?;
150        println!("Successfully linked the toolchain to rustup.");
151
152        // Compressing toolchain directory to tar.gz.
153        let target = get_target();
154        let tar_gz_path = format!("rust-toolchain-{target}.tar.gz");
155        Command::new("tar")
156            .args([
157                "--exclude",
158                "lib/rustlib/src",
159                "--exclude",
160                "lib/rustlib/rustc-src",
161                "-hczvf",
162                &tar_gz_path,
163                "-C",
164                toolchain_dir.to_str().unwrap(),
165                ".",
166            ])
167            .run()?;
168        println!("Successfully compressed the toolchain to {tar_gz_path}.");
169
170        Ok(())
171    }
172}
173
174#[cfg(all(test, unix))]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn run_errors_on_nonzero_exit() {
180        let err = Command::new("sh").args(["-c", "exit 42"]).run().unwrap_err();
181        assert!(err.to_string().contains("failed"), "unexpected error: {err}");
182    }
183
184    #[test]
185    fn run_succeeds_on_zero_exit() {
186        Command::new("sh").args(["-c", "exit 0"]).run().unwrap();
187    }
188
189    #[test]
190    fn run_errors_do_not_expose_command_credentials() {
191        let mut nonzero_exit = Command::new("sh");
192        nonzero_exit.args(["-c", "exit 42"]);
193        let mut spawn_failure = Command::new("sh");
194        spawn_failure.current_dir(std::env::current_exe().unwrap());
195
196        for mut command in [nonzero_exit, spawn_failure] {
197            let error = command
198                .arg("https://test-url-token@github.com/succinctlabs/rust")
199                .env("GITHUB_ACCESS_TOKEN", "test-env-token")
200                .run()
201                .unwrap_err();
202            let message = format!("{error:#}");
203            assert!(message.contains("\"sh\""), "missing program name: {message}");
204            assert!(!message.contains("test-url-token"), "exposed argument: {message}");
205            assert!(!message.contains("test-env-token"), "exposed environment: {message}");
206        }
207    }
208}