Skip to content

Commit f3d4bb5

Browse files
authored
Merge pull request #364 from OffchainLabs/braga/fix-docker-image-entrypoint
Fix reproducible docker image build for stylus verify
2 parents 955070b + ba65ec7 commit f3d4bb5

File tree

7 files changed

+136
-45
lines changed

7 files changed

+136
-45
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

stylus-tools/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ serde = { version = "1.0", features = ["derive"] }
4343
serde_json = "1.0"
4444
sneks = "0.1"
4545
sys-info = "0.9.1"
46+
tempfile = "3.8"
4647
thiserror = "2.0"
4748
tiny-keccak = { version = "2.0", features = ["keccak"] }
4849
toml = "0.8"

stylus-tools/src/core/build/reproducible.rs

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
use std::{cmp::Ordering, io::Write};
55

66
use cargo_metadata::{semver::Version, Package};
7+
use tempfile::NamedTempFile;
78

89
use crate::utils::{
9-
docker::{self, image_exists, validate_host, DockerError},
10+
docker::{self, validate_host, DockerError},
1011
toolchain::{get_toolchain_channel, ToolchainError},
1112
};
1213

@@ -25,11 +26,24 @@ pub fn run_reproducible(
2526
let selected_cargo_stylus_version = select_stylus_version(cargo_stylus_version)?;
2627
let image_name = create_image(&selected_cargo_stylus_version, &toolchain_channel)?;
2728

29+
/// Currently only calling cargo stylus is supported (not cargo stylus-beta for instance)
2830
let mut args = vec!["cargo".to_string(), "stylus".to_string()];
2931
for arg in command_line.into_iter() {
3032
args.push(arg);
3133
}
32-
docker::run_in_container(&image_name, package.source.clone().unwrap().repr, args)?;
34+
// Use package source if available, otherwise use current working directory
35+
let source = package
36+
.source
37+
.as_ref()
38+
.map(|s| s.repr.to_owned())
39+
.unwrap_or_else(|| {
40+
std::env::current_dir()
41+
.unwrap_or_else(|_| std::path::PathBuf::from("."))
42+
.to_string_lossy()
43+
.to_string()
44+
});
45+
46+
docker::run_in_container(&image_name, &source, args)?;
3347
Ok(())
3448
}
3549

@@ -39,23 +53,49 @@ fn create_image(
3953
toolchain_version: &str,
4054
) -> Result<String, DockerError> {
4155
let name = image_name(cargo_stylus_version, toolchain_version);
42-
if image_exists(&name)? {
56+
57+
// First, check if image exists locally
58+
if docker::image_exists_locally(&name)? {
59+
info!(@grey, "Using local image {name}");
4360
return Ok(name);
4461
}
45-
println!("Building Docker image for Rust toolchain {toolchain_version}");
46-
let mut build = docker::cmd::build(&name)?;
47-
write!(
48-
build.file(),
49-
"\
62+
info!(@grey, "Building Docker image for Rust toolchain {toolchain_version}");
63+
64+
// Second, check if base image exists on Docker Hub. If not, we fail early since
65+
// docker build will fail trying to pull such image
66+
let base_image = format!("offchainlabs/cargo-stylus-base:{cargo_stylus_version}");
67+
info!(@grey, "Checking if base image exists on Docker Hub: {base_image}");
68+
69+
if !docker::image_exists_on_hub(&base_image)? {
70+
return Err(DockerError::ImageNotFound(
71+
base_image,
72+
cargo_stylus_version.to_string(),
73+
));
74+
}
75+
76+
info!(@grey, "Image exists, building container with base image: {base_image}");
77+
78+
// Create temporary Dockerfile
79+
let dockerfile_content = format!(
80+
r#"\
5081
ARG BUILD_PLATFORM=linux/amd64
51-
FROM --platform=${{BUILD_PLATFORM}} offchainlabs/cargo-stylus-base:{cargo_stylus_version} AS base
82+
FROM --platform=${{BUILD_PLATFORM}} {base_image} AS base
5283
RUN rustup toolchain install {toolchain_version}-x86_64-unknown-linux-gnu
5384
RUN rustup default {toolchain_version}-x86_64-unknown-linux-gnu
5485
RUN rustup target add wasm32-unknown-unknown
5586
RUN rustup component add rust-src --toolchain {toolchain_version}-x86_64-unknown-linux-gnu
56-
",
57-
)?;
58-
build.wait()?;
87+
"#
88+
);
89+
90+
// Write to temporary file (automatically cleaned up when dropped)
91+
let temp_file = NamedTempFile::new().map_err(DockerError::Io)?;
92+
temp_file
93+
.as_file()
94+
.write_all(dockerfile_content.as_bytes())
95+
.map_err(DockerError::Io)?;
96+
97+
// Build using the temporary file
98+
docker::cmd::build_with_file(&name, temp_file.path())?;
5999
Ok(name)
60100
}
61101

stylus-tools/src/utils/docker/cmd.rs

Lines changed: 59 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
66
use std::{
77
ffi::OsStr,
8+
path::Path,
89
process::{Command, Stdio},
910
str,
1011
};
@@ -13,55 +14,83 @@ use super::{error::DockerError, json};
1314

1415
const DOCKER_PROGRAM: &str = "docker";
1516

16-
#[derive(Debug)]
17-
pub struct DockerBuild {
18-
pub child: std::process::Child,
19-
}
20-
21-
impl DockerBuild {
22-
pub fn file(&mut self) -> impl std::io::Write + '_ {
23-
self.child.stdin.as_mut().unwrap()
24-
}
25-
26-
pub fn wait(mut self) -> Result<(), DockerError> {
27-
self.child.wait().map_err(DockerError::WaitFailure)?;
28-
Ok(())
29-
}
30-
}
17+
/// Build a Docker image using a Dockerfile from an existing file.
18+
pub fn build_with_file(tag: &str, dockerfile_path: &Path) -> Result<(), DockerError> {
19+
info!(@grey, "Building Docker image: {} (using Dockerfile: {})", tag, dockerfile_path.display());
3120

32-
/// Start a Docker build.
33-
pub fn build(tag: &str) -> Result<DockerBuild, DockerError> {
34-
let child = Command::new(DOCKER_PROGRAM)
21+
let mut child = Command::new(DOCKER_PROGRAM)
3522
.arg("build")
3623
.args(["--tag", tag])
24+
.args(["--file", dockerfile_path.to_str().unwrap()])
3725
.arg(".")
38-
.args(["--file", "-"])
39-
.stdin(Stdio::piped())
26+
.stdout(Stdio::inherit())
27+
.stderr(Stdio::inherit())
4028
.spawn()
4129
.map_err(DockerError::CommandExecution)?;
42-
Ok(DockerBuild { child })
30+
31+
let status = child.wait().map_err(DockerError::WaitFailure)?;
32+
33+
if !status.success() {
34+
return Err(DockerError::Docker(format!(
35+
"Docker build failed with exit code: {}",
36+
status.code().unwrap_or(-1)
37+
)));
38+
}
39+
40+
info!(@grey, "Docker image built successfully: {}", tag);
41+
Ok(())
42+
}
43+
44+
/// Check if a Docker image exists on Docker Hub.
45+
pub fn image_exists_on_hub(image_name: &str) -> Result<bool, DockerError> {
46+
// Use docker manifest inspect to check if the image exists on the registry
47+
let output = Command::new(DOCKER_PROGRAM)
48+
.arg("manifest")
49+
.arg("inspect")
50+
.arg(image_name)
51+
.output()
52+
.map_err(DockerError::CommandExecution)?;
53+
54+
// If the command succeeds, the image exists
55+
let exists = output.status.success();
56+
57+
Ok(exists)
4358
}
4459

45-
/// List local Docker images.
46-
///
47-
/// We currently only use this with a repository specified.
48-
pub fn images(repository: &str) -> Result<Vec<json::Image>, DockerError> {
60+
/// Check if a specific Docker image exists locally.
61+
/// Returns true if the exact image:tag combination exists locally.
62+
pub fn image_exists_locally(image_name: &str) -> Result<bool, DockerError> {
4963
let output = Command::new(DOCKER_PROGRAM)
5064
.arg("images")
5165
.args(["--format", "json"])
52-
.arg(repository)
66+
.arg(image_name)
5367
.output()
5468
.map_err(DockerError::CommandExecution)?;
5569

56-
if !output.status.success() {
70+
let success = output.status.success();
71+
if !success {
5772
return Err(DockerError::from_stderr(output.stderr));
5873
}
5974

60-
output
75+
// Parse the JSON output to check if any images match the exact image:tag
76+
let images: Vec<json::Image> = output
6177
.stdout
6278
.split(|b| *b == b'\n')
63-
.map(|slice| serde_json::from_slice(slice).map_err(Into::into))
64-
.collect()
79+
.filter(|slice| !slice.is_empty()) // Filter out empty lines
80+
.map(|slice| serde_json::from_slice(slice).map_err(DockerError::Json))
81+
.collect::<Result<Vec<_>, _>>()?;
82+
83+
// Check if any image matches the exact repository:tag combination
84+
let exists = images.iter().any(|image| {
85+
let full_name = if image.tag == "<none>" {
86+
image.repository.clone()
87+
} else {
88+
format!("{}:{}", image.repository, image.tag)
89+
};
90+
full_name == image_name
91+
});
92+
93+
Ok(exists)
6594
}
6695

6796
/// Run a command in a Docker container.

stylus-tools/src/utils/docker/error.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ pub enum DockerError {
2424
CannotConnect(String),
2525
#[error("Docker error: {0}")]
2626
Docker(String),
27+
#[error(
28+
"Base Docker image '{0}' not found locally nor on Docker Hub.
29+
This usually means the version '{1}' is not available on Docker Hub.
30+
Available options:
31+
1. Visit https://hub.docker.com/r/offchainlabs/cargo-stylus-base/tags for all available versions
32+
2. Try using a stable version: cargo stylus --version <stable-version>
33+
3. Pull the image manually: docker pull {0}
34+
Common stable versions: 0.6.3, 0.6.2"
35+
)]
36+
ImageNotFound(String, String),
2737

2838
#[error("unable to determine host OS type")]
2939
UnableToDetermineOsType,

stylus-tools/src/utils/docker/json.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use serde::Deserialize;
1010
#[serde(rename_all = "PascalCase")]
1111
#[allow(dead_code)]
1212
pub struct Image {
13+
pub id: Option<String>,
1314
pub repository: String,
15+
pub created_at: String,
1416
pub tag: String,
1517
}

stylus-tools/src/utils/docker/mod.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,27 @@ pub fn validate_host() -> Result<(), DockerError> {
2727
Ok(())
2828
}
2929

30-
pub fn image_exists(image_name: &str) -> Result<bool, DockerError> {
31-
Ok(!cmd::images(image_name)?.is_empty())
30+
pub fn image_exists_locally(image_name: &str) -> Result<bool, DockerError> {
31+
cmd::image_exists_locally(image_name)
32+
}
33+
34+
/// Check if a Docker image exists on Docker Hub.
35+
pub fn image_exists_on_hub(image_name: &str) -> Result<bool, DockerError> {
36+
cmd::image_exists_on_hub(image_name)
3237
}
3338

3439
pub fn run_in_container(
3540
image_name: &str,
3641
dir: impl AsRef<Path>,
3742
args: impl IntoIterator<Item = impl AsRef<OsStr>>,
3843
) -> Result<(), DockerError> {
44+
let dir_str = dir.as_ref().to_str().unwrap();
45+
info!(@grey, "Using directory as entry point {dir_str}");
46+
3947
cmd::run(
4048
image_name,
4149
Some("host"),
42-
&[(dir.as_ref().to_str().unwrap(), "/source")],
50+
&[(dir_str, "/source")],
4351
Some("/source"),
4452
args,
4553
)?;

0 commit comments

Comments
 (0)