Skip to content

Commit 1780110

Browse files
committed
Pin the rust toolchain in verifiable builds.
1 parent d0831dc commit 1780110

1 file changed

Lines changed: 88 additions & 18 deletions

File tree

cmd/soroban-cli/src/commands/contract/build/verifiable.rs

Lines changed: 88 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,61 @@ async fn probe_cli_version(image_ref: &str, docker: &Docker) -> Result<Version,
777777
.map_err(|e| Error::TagListUnavailable(format!("unparseable version {stdout:?}: {e}")))
778778
}
779779

780+
/// Probe the image for the toolchain rustup uses by default, so it can be
781+
/// pinned via `RUSTUP_TOOLCHAIN` (see `run_in_container`). Overrides the
782+
/// entrypoint to run `rustup show active-toolchain` and returns the toolchain
783+
/// name — the first whitespace-delimited token, dropping any trailing
784+
/// `(default)` marker (e.g. `1.93.0-x86_64-unknown-linux-gnu`). Returns `None`
785+
/// on any failure (e.g. an image without rustup), so the build proceeds without
786+
/// the pin rather than failing.
787+
async fn probe_active_toolchain(image_ref: &str, docker: &Docker) -> Option<String> {
788+
let config = ContainerCreateBody {
789+
image: Some(image_ref.to_string()),
790+
entrypoint: Some(vec!["rustup".to_string()]),
791+
cmd: Some(vec!["show".to_string(), "active-toolchain".to_string()]),
792+
attach_stdout: Some(true),
793+
attach_stderr: Some(true),
794+
host_config: Some(HostConfig {
795+
auto_remove: Some(true),
796+
..Default::default()
797+
}),
798+
..Default::default()
799+
};
800+
let created = docker
801+
.create_container(None::<CreateContainerOptions>, config)
802+
.await
803+
.ok()?;
804+
let attached = docker
805+
.attach_container(
806+
&created.id,
807+
Some(AttachContainerOptions {
808+
stdout: true,
809+
stderr: true,
810+
stream: true,
811+
..Default::default()
812+
}),
813+
)
814+
.await
815+
.ok()?;
816+
docker
817+
.start_container(&created.id, None::<StartContainerOptions>)
818+
.await
819+
.ok()?;
820+
821+
let mut stdout = String::new();
822+
let mut output = attached.output;
823+
while let Some(chunk) = output.next().await {
824+
if let Ok(bollard::container::LogOutput::StdOut { message }) = chunk {
825+
stdout.push_str(&String::from_utf8_lossy(&message));
826+
}
827+
}
828+
829+
let mut wait = docker.wait_container(&created.id, None::<WaitContainerOptions>);
830+
while wait.next().await.is_some() {}
831+
832+
stdout.split_whitespace().next().map(str::to_string)
833+
}
834+
780835
/// Render the per-package `stellar contract build …` commands into a single
781836
/// `sh -c` script (`stellar … && stellar …`), shell-escaping every token so meta
782837
/// values with spaces survive. Used when more than one package is built so they
@@ -817,10 +872,22 @@ async fn run_in_container(
817872
) -> Result<(), Error> {
818873
let bind = format!("{}:/source", workspace_root.display());
819874

875+
// Pin rustup to the image's own toolchain (per SEP-58): without this, a
876+
// `rust-toolchain.toml` in the source could make rustup switch toolchains
877+
// mid-build, defeating the digest-pinned image. Probe the image for its
878+
// active toolchain and pass it through with `-e`, unless the caller already
879+
// set RUSTUP_TOOLCHAIN. Skipped silently when the image has no rustup.
880+
let mut env = env.to_vec();
881+
if !env.iter().any(|e| e.starts_with("RUSTUP_TOOLCHAIN=")) {
882+
if let Some(toolchain) = probe_active_toolchain(image_ref, docker).await {
883+
env.push(format!("RUSTUP_TOOLCHAIN={toolchain}"));
884+
}
885+
}
886+
820887
// `-e KEY=VALUE` flags for the reproduce command, mirroring the env passed
821888
// to the container below.
822889
let mut env_flags = String::new();
823-
for e in env {
890+
for e in &env {
824891
env_flags.push_str(" -e ");
825892
env_flags.push_str(&shell_escape::escape(e.as_str().into()));
826893
}
@@ -852,7 +919,7 @@ async fn run_in_container(
852919
image: Some(image_ref.to_string()),
853920
entrypoint,
854921
cmd: Some(cmd),
855-
env: (!env.is_empty()).then(|| env.to_vec()),
922+
env: (!env.is_empty()).then(|| env.clone()),
856923
working_dir: Some("/source".to_string()),
857924
attach_stdout: Some(true),
858925
attach_stderr: Some(true),
@@ -908,24 +975,27 @@ async fn run_in_container(
908975
}
909976
}
910977

911-
let mut wait = docker.wait_container(&created.id, None::<WaitContainerOptions>);
978+
wait_for_container_exit(docker, &created.id, &reproduce).await
979+
}
980+
981+
/// Block until the container exits, mapping a non-zero exit code (whether
982+
/// reported as a successful wait or as a `DockerContainerWaitError`) to
983+
/// `ContainerExit` carrying the reproduce command.
984+
async fn wait_for_container_exit(docker: &Docker, id: &str, reproduce: &str) -> Result<(), Error> {
985+
let mut wait = docker.wait_container(id, None::<WaitContainerOptions>);
912986
while let Some(item) = wait.next().await {
913-
match item {
914-
Ok(r) if r.status_code == 0 => {}
915-
Ok(r) => {
916-
return Err(Error::ContainerExit {
917-
status: r.status_code,
918-
command: reproduce.clone(),
919-
});
920-
}
921-
Err(bollard::errors::Error::DockerContainerWaitError { code: 0, .. }) => {}
922-
Err(bollard::errors::Error::DockerContainerWaitError { code, .. }) => {
923-
return Err(Error::ContainerExit {
924-
status: code,
925-
command: reproduce.clone(),
926-
});
927-
}
987+
// Both a successful wait and a `DockerContainerWaitError` carry an exit
988+
// code; normalize to it (other errors are genuine failures).
989+
let status = match item {
990+
Ok(r) => r.status_code,
991+
Err(bollard::errors::Error::DockerContainerWaitError { code, .. }) => code,
928992
Err(e) => return Err(e.into()),
993+
};
994+
if status != 0 {
995+
return Err(Error::ContainerExit {
996+
status,
997+
command: reproduce.to_string(),
998+
});
929999
}
9301000
}
9311001

0 commit comments

Comments
 (0)