sp1_build/
build.rs

1use std::path::PathBuf;
2
3use anyhow::Result;
4use cargo_metadata::camino::Utf8PathBuf;
5
6use crate::{
7    command::{docker::create_docker_command, local::create_local_command, utils::execute_command},
8    utils::{cargo_rerun_if_changed, current_datetime},
9    BuildArgs, WarningLevel, BUILD_TARGET, HELPER_TARGET_SUBDIR,
10};
11
12/// Build a program with the specified [`BuildArgs`]. The `program_dir` is specified as an argument
13/// when the program is built via `build_program`.
14///
15/// # Arguments
16///
17/// * `args` - A reference to a `BuildArgs` struct that holds various arguments used for building
18///   the program.
19/// * `program_dir` - An optional `PathBuf` specifying the directory of the program to be built.
20///
21/// # Returns
22///
23/// * `Result<Vec<(String, Utf8PathBuf)>>` - A list of mapping from bin target names to the paths to
24///   the built program as a `Utf8PathBuf` on success, or an error on failure.
25pub fn execute_build_program(
26    args: &BuildArgs,
27    program_dir: Option<PathBuf>,
28) -> Result<Vec<(String, Utf8PathBuf)>> {
29    // If the program directory is not specified, use the current directory.
30    let program_dir = program_dir
31        .unwrap_or_else(|| std::env::current_dir().expect("Failed to get current directory."));
32    let program_dir: Utf8PathBuf =
33        program_dir.try_into().expect("Failed to convert PathBuf to Utf8PathBuf");
34
35    // Get the program metadata.
36    let program_metadata_file = program_dir.join("Cargo.toml");
37    let mut program_metadata_cmd = cargo_metadata::MetadataCommand::new();
38    let program_metadata = program_metadata_cmd.manifest_path(program_metadata_file).exec()?;
39
40    // Get the command corresponding to Docker or local build.
41    let cmd = if args.docker {
42        create_docker_command(args, &program_dir, &program_metadata)?
43    } else {
44        create_local_command(args, &program_dir, &program_metadata)
45    };
46
47    let target_elf_paths = generate_elf_paths(&program_metadata, Some(args))?;
48
49    if target_elf_paths.len() > 1 && args.elf_name.is_some() {
50        anyhow::bail!("--elf-name is not supported when --output-directory is used and multiple ELFs are built.");
51    }
52
53    execute_command(cmd, args.docker)?;
54
55    if let Some(output_directory) = &args.output_directory {
56        // The path to the output directory, maybe relative or absolute.
57        let output_directory = PathBuf::from(output_directory);
58
59        // Ensure the output directory is a directory. If it doesnt exist, this is false.
60        if output_directory.is_file() {
61            anyhow::bail!("--output-directory is a file.");
62        }
63
64        // Ensure the output directory exists.
65        std::fs::create_dir_all(&output_directory)?;
66
67        // Copy the ELF files to the output directory.
68        for (_, elf_path) in target_elf_paths.iter() {
69            let elf_path = elf_path.to_path_buf();
70            let elf_name = elf_path.file_name().expect("ELF path has a file name");
71            let output_path = output_directory.join(args.elf_name.as_deref().unwrap_or(elf_name));
72
73            std::fs::copy(&elf_path, &output_path)?;
74        }
75    }
76
77    print_elf_paths_cargo_directives(&target_elf_paths);
78
79    Ok(target_elf_paths)
80}
81
82/// Internal helper function to build the program with or without arguments.
83pub(crate) fn build_program_internal(path: &str, args: Option<BuildArgs>) {
84    // Get the root package name and metadata.
85    let program_dir = std::path::Path::new(path);
86    let metadata_file = program_dir.join("Cargo.toml");
87    let mut metadata_cmd = cargo_metadata::MetadataCommand::new();
88    let metadata = metadata_cmd.manifest_path(metadata_file).exec().unwrap();
89    let root_package = metadata.root_package();
90    let root_package_name = root_package.as_ref().map(|p| p.name.as_str()).unwrap_or("Program");
91
92    // Skip the program build if the SP1_SKIP_PROGRAM_BUILD environment variable is set to true.
93    let skip_program_build = std::env::var("SP1_SKIP_PROGRAM_BUILD")
94        .map(|v| v.eq_ignore_ascii_case("true"))
95        .unwrap_or(false);
96    if skip_program_build {
97        // Still need to set ELF env vars even if build is skipped.
98        let target_elf_paths = generate_elf_paths(&metadata, args.as_ref())
99            .expect("failed to collect target ELF paths");
100
101        print_elf_paths_cargo_directives(&target_elf_paths);
102
103        println!(
104            "cargo:warning=Build skipped for {} at {} due to SP1_SKIP_PROGRAM_BUILD flag",
105            root_package_name,
106            current_datetime()
107        );
108        return;
109    }
110
111    // Activate the build command if the dependencies change.
112    cargo_rerun_if_changed(&metadata, program_dir);
113
114    // Check if RUSTC_WORKSPACE_WRAPPER is set to clippy-driver (i.e. if `cargo clippy` is the
115    // current compiler). If so, don't execute `cargo prove build` because it breaks
116    // rust-analyzer's `cargo clippy` feature.
117    let is_clippy_driver = std::env::var("RUSTC_WORKSPACE_WRAPPER")
118        .map(|val| val.contains("clippy-driver"))
119        .unwrap_or(false);
120    if is_clippy_driver {
121        // Still need to set ELF env vars even if build is skipped.
122        let target_elf_paths = generate_elf_paths(&metadata, args.as_ref())
123            .expect("failed to collect target ELF paths");
124
125        print_elf_paths_cargo_directives(&target_elf_paths);
126
127        println!("cargo:warning=Skipping build due to clippy invocation.");
128        return;
129    }
130
131    // Build the program with the given arguments.
132    let path_output = if let Some(args) = &args {
133        execute_build_program(args, Some(program_dir.to_path_buf()))
134    } else {
135        execute_build_program(&BuildArgs::default(), Some(program_dir.to_path_buf()))
136    };
137    if let Err(err) = path_output {
138        panic!("Failed to build SP1 program: {err}.");
139    }
140
141    if args.map(|args| matches!(args.warning_level, WarningLevel::All)).unwrap_or(true) {
142        println!("cargo:warning={} built at {}", root_package_name, current_datetime());
143    }
144}
145
146/// Collects the list of targets that would be built and their output ELF file paths.
147pub fn generate_elf_paths(
148    metadata: &cargo_metadata::Metadata,
149    args: Option<&BuildArgs>,
150) -> Result<Vec<(String, Utf8PathBuf)>> {
151    let mut target_elf_paths = vec![];
152    let packages_to_iterate = if let Some(args) = args {
153        if !args.packages.is_empty() {
154            args.packages
155                .iter()
156                .map(|wanted_package| {
157                    metadata
158                        .packages
159                        .iter()
160                        .find(|p| p.name == *wanted_package)
161                        .ok_or_else(|| {
162                            anyhow::anyhow!("cannot find package named {}", wanted_package)
163                        })
164                        .map(|p| p.id.clone())
165                })
166                .collect::<anyhow::Result<Vec<_>>>()?
167        } else {
168            metadata.workspace_default_members.to_vec()
169        }
170    } else {
171        metadata.workspace_default_members.to_vec()
172    };
173
174    for program_crate in packages_to_iterate {
175        let program = metadata
176            .packages
177            .iter()
178            .find(|p| p.id == program_crate)
179            .ok_or_else(|| anyhow::anyhow!("cannot find package for {}", program_crate))?;
180
181        for bin_target in program.targets.iter().filter(|t| {
182            t.kind.contains(&"bin".to_owned()) && t.crate_types.contains(&"bin".to_owned())
183        }) {
184            // Filter out irrelevant targets if `--bin` is used.
185            if let Some(args) = args {
186                if !args.binaries.is_empty() && !args.binaries.contains(&bin_target.name) {
187                    continue;
188                }
189            }
190
191            let elf_path = metadata.target_directory.join(HELPER_TARGET_SUBDIR);
192            let elf_path = match args {
193                Some(args) if args.docker => elf_path.join("docker"),
194                _ => elf_path,
195            };
196            let elf_path = elf_path.join(BUILD_TARGET).join("release").join(&bin_target.name);
197
198            target_elf_paths.push((bin_target.name.to_owned(), elf_path));
199        }
200    }
201
202    Ok(target_elf_paths)
203}
204
205/// Prints cargo directives setting relevant `SP1_ELF_` environment variables.
206fn print_elf_paths_cargo_directives(target_elf_paths: &[(String, Utf8PathBuf)]) {
207    for (target_name, elf_path) in target_elf_paths.iter() {
208        println!("cargo:rustc-env=SP1_ELF_{target_name}={elf_path}");
209    }
210}