use std::{
path::{Path, PathBuf},
process::{exit, Command, Stdio},
};
use anyhow::{Context, Result};
use cargo_metadata::{camino::Utf8PathBuf, semver};
use crate::{BuildArgs, WarningLevel};
use super::utils::{get_program_build_args, get_rust_compiler_flags};
#[allow(clippy::uninlined_format_args)]
fn get_docker_image(tag: &str) -> String {
std::env::var("SP1_DOCKER_IMAGE").unwrap_or_else(|_| {
let image_base = "ghcr.io/succinctlabs/sp1";
format!("{image_base}:{tag}")
})
}
#[allow(clippy::uninlined_format_args)]
pub(crate) fn create_docker_command(
args: &BuildArgs,
program_dir: &Utf8PathBuf,
program_metadata: &cargo_metadata::Metadata,
) -> Result<Command> {
let image = get_docker_image(&args.tag);
let canonicalized_program_dir: Utf8PathBuf = program_dir
.canonicalize()
.expect("Failed to canonicalize program directory")
.try_into()
.unwrap();
let workspace_root: &Utf8PathBuf = &args
.workspace_directory
.as_deref()
.map(|workspace_path| {
std::path::Path::new(workspace_path)
.to_path_buf()
.canonicalize()
.expect("Failed to canonicalize workspace directory")
.try_into()
.unwrap()
})
.unwrap_or_else(|| program_metadata.workspace_root.clone());
if !program_metadata.workspace_root.starts_with(workspace_root) {
eprintln!(
"Workspace root ({}) must be a parent of the program directory ({}).",
workspace_root, program_metadata.workspace_root
);
exit(1);
}
let docker_check = Command::new("docker")
.args(["info"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.context("failed to run docker command")?;
if !docker_check.success() {
eprintln!("docker is not installed or not running: https://docs.docker.com/get-docker/");
exit(1);
}
let workspace_root_path = format!("{workspace_root}:/root/program");
let program_dir_path = format!(
"/root/program/{}",
canonicalized_program_dir.strip_prefix(workspace_root).unwrap()
);
let relative_target_dir =
program_metadata.target_directory.strip_prefix(workspace_root).with_context(|| {
format!(
"Cargo target directory ({}) must be a child of the workspace directory ({}).\n\
This can happen if CARGO_TARGET_DIR is set to a location outside the workspace.",
program_metadata.target_directory, workspace_root
)
})?;
let target_dir = format!(
"/root/program/{}/{}/{}",
relative_target_dir,
crate::HELPER_TARGET_SUBDIR,
"docker"
);
let parsed_version = {
let mut cmd = run_command_in_docker(&image);
cmd.args(["rustc", "--version"]);
let output = cmd.output().expect("rustc --version should succeed in docker image");
if !output.status.success() {
return Err(anyhow::anyhow!(
"Failed to run rustc --version in docker image stdout: {} \n stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
));
}
let stdout_string =
String::from_utf8(output.stdout).expect("Can't parse rustc --version stdout");
if matches!(args.warning_level, WarningLevel::All) {
println!("cargo:warning=docker: rustc +succinct --version: {stdout_string:?}");
}
super::utils::parse_rustc_version(&stdout_string)
};
std::thread::sleep(std::time::Duration::from_secs(2));
let rustc_bin = {
let mut cmd = run_command_in_docker(&image);
cmd.args(["rustc", "--print", "sysroot"]);
let output = cmd.output().expect("rustc --bin rustc should succeed in docker image");
let stdout_string =
String::from_utf8(output.stdout).expect("Can't parse rustc --bin rustc stdout");
PathBuf::from(stdout_string.trim()).join("bin/rustc")
};
println!("cargo:warning=docker: rustc_bin: {rustc_bin:?}");
let docker_args = build_docker_args(
args,
workspace_root_path,
program_dir_path,
target_dir,
&rustc_bin,
&parsed_version,
image,
);
let mut command = Command::new("docker");
command.current_dir(canonicalized_program_dir.clone()).args(&docker_args);
Ok(command)
}
fn build_docker_args(
args: &BuildArgs,
workspace_root_path: String,
program_dir_path: String,
target_dir: String,
rustc_bin: &Path,
parsed_version: &semver::Version,
image: String,
) -> Vec<String> {
let mut docker_args = vec![
"run".to_string(),
"--rm".to_string(),
"--platform".to_string(),
"linux/amd64".to_string(),
"-v".to_string(),
workspace_root_path,
];
if !args.no_docker_cache {
docker_args.extend_from_slice(&[
"-v".to_string(),
"sp1-cargo-registry:/root/.cargo/registry".to_string(),
"-v".to_string(),
"sp1-cargo-git:/root/.cargo/git".to_string(),
]);
}
docker_args.extend_from_slice(&[
"-w".to_string(),
program_dir_path,
"-e".to_string(),
format!("CARGO_TARGET_DIR={target_dir}"),
"-e".to_string(),
format!("RUSTUP_TOOLCHAIN={}", super::TOOLCHAIN_NAME),
"-e".to_string(),
"RUSTC_BOOTSTRAP=1".to_string(), "-e".to_string(),
format!("CARGO_ENCODED_RUSTFLAGS={}", get_rust_compiler_flags(args, parsed_version)),
"-e".to_string(),
format!("RUSTC={}", rustc_bin.display()),
"-e".to_string(),
"CFLAGS_riscv32im_succinct_zkvm_elf=-D__ILP32__".to_string(),
"--entrypoint".to_string(),
"".to_string(),
image,
"cargo".to_string(),
]);
docker_args.extend_from_slice(&get_program_build_args(args));
docker_args
}
fn run_command_in_docker(image: &str) -> Command {
let mut cmd = Command::new("docker");
cmd.args(["run", "--rm"]);
cmd.args(["-e", &format!("RUSTUP_TOOLCHAIN={}", super::TOOLCHAIN_NAME)]);
cmd.args(["--platform", "linux/amd64", "--entrypoint", "", "-i", image]);
cmd
}
#[cfg(test)]
mod tests {
use super::*;
use crate::BuildArgs;
use std::path::PathBuf;
fn test_docker_args(args: &BuildArgs) -> Vec<String> {
let version = semver::Version::new(1, 93, 0);
build_docker_args(
args,
"/workspace:/root/program".to_string(),
"/root/program/my-program".to_string(),
"/root/program/target/elf-compilation/docker".to_string(),
&PathBuf::from("/root/.sp1/toolchains/abc/bin/rustc"),
&version,
"ghcr.io/succinctlabs/sp1:v6.0.2".to_string(),
)
}
#[test]
fn test_docker_args_include_cargo_cache_volumes() {
let args = BuildArgs { docker: true, ..Default::default() };
let docker_args = test_docker_args(&args);
assert!(docker_args.contains(&"sp1-cargo-registry:/root/.cargo/registry".to_string()));
assert!(docker_args.contains(&"sp1-cargo-git:/root/.cargo/git".to_string()));
}
#[test]
fn test_docker_args_exclude_cache_volumes_when_disabled() {
let args = BuildArgs { docker: true, no_docker_cache: true, ..Default::default() };
let docker_args = test_docker_args(&args);
assert!(!docker_args.contains(&"sp1-cargo-registry:/root/.cargo/registry".to_string()));
assert!(!docker_args.contains(&"sp1-cargo-git:/root/.cargo/git".to_string()));
}
#[test]
fn test_docker_args_include_rustup_toolchain() {
let args = BuildArgs { docker: true, ..Default::default() };
let docker_args = test_docker_args(&args);
let toolchain_env = format!("RUSTUP_TOOLCHAIN={}", super::super::TOOLCHAIN_NAME);
let has_toolchain = docker_args.windows(2).any(|w| w[0] == "-e" && w[1] == toolchain_env);
assert!(has_toolchain, "Expected RUSTUP_TOOLCHAIN env var in docker args");
}
}