Skip to content

Commit 6189268

Browse files
committed
refactor(dist): stage fresh toolchain installs before publication
1 parent fa28f12 commit 6189268

2 files changed

Lines changed: 117 additions & 10 deletions

File tree

src/dist/mod.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -939,16 +939,17 @@ impl<'cfg, 'a> DistOptions<'cfg, 'a> {
939939
pub(crate) async fn install_into(
940940
&self,
941941
prefix: &InstallPrefix,
942+
update_hash: &Path,
942943
manifest: Option<ManifestWithHash>,
943944
) -> Result<Option<String>> {
944945
let fresh_install = !prefix.path().exists();
945946
// fresh_install means the toolchain isn't present, but hash_exists means there is a stray hash file
946-
if fresh_install && self.update_hash.exists() {
947+
if fresh_install && update_hash.exists() {
947948
warn!(
948949
"removing stray hash file in order to continue: {}",
949-
self.update_hash.display()
950+
update_hash.display()
950951
);
951-
std::fs::remove_file(&self.update_hash)?;
952+
std::fs::remove_file(update_hash)?;
952953
}
953954

954955
let mut fetched = String::new();
@@ -1002,7 +1003,7 @@ impl<'cfg, 'a> DistOptions<'cfg, 'a> {
10021003
let res = loop {
10031004
let result = try_update_from_dist_(
10041005
&self.dl_cfg,
1005-
&self.update_hash,
1006+
update_hash,
10061007
&toolchain,
10071008
match self.exists {
10081009
false => Some(self.profile),

src/install.rs

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
//! Installation and upgrade of both distribution-managed and local
22
//! toolchains
3-
use std::path::{Path, PathBuf};
3+
use std::{
4+
fs,
5+
io::ErrorKind,
6+
path::{Path, PathBuf},
7+
};
48

5-
use anyhow::Result;
6-
use tracing::debug;
9+
use anyhow::{Context, Result};
10+
use tracing::{debug, warn};
711

812
use crate::{
913
config::Cfg,
@@ -13,6 +17,61 @@ use crate::{
1317
utils,
1418
};
1519

20+
#[cfg(feature = "test")]
21+
use crate::test::checkpoint;
22+
23+
impl StagedToolchain {
24+
fn new(destination: &Path) -> Result<Self> {
25+
let parent = destination
26+
.parent()
27+
.expect("toolchain destination must have a parent");
28+
utils::ensure_dir_exists("toolchains", parent)?;
29+
30+
loop {
31+
let root = parent.join(format!(
32+
"{STAGING_DIR_PREFIX}{}",
33+
utils::raw::random_string(16)
34+
));
35+
match fs::create_dir(&root) {
36+
Ok(()) => {
37+
let prefix = root.join("toolchain");
38+
return Ok(Self { root, prefix });
39+
}
40+
Err(error) if error.kind() == ErrorKind::AlreadyExists => continue,
41+
Err(error) => {
42+
return Err(error).with_context(|| RustupError::CreatingDirectory {
43+
name: "staging toolchain",
44+
path: root,
45+
});
46+
}
47+
}
48+
}
49+
}
50+
51+
fn prefix(&self) -> &Path {
52+
&self.prefix
53+
}
54+
55+
fn publish(self, destination: &Path) -> Result<()> {
56+
// Staging is a child of the destination directory, so publication must
57+
// be a same-filesystem rename. Never permit the copy-and-delete fallback.
58+
utils::rename("toolchain", &self.prefix, destination, false)
59+
}
60+
}
61+
62+
impl Drop for StagedToolchain {
63+
fn drop(&mut self) {
64+
if utils::path_exists(&self.root)
65+
&& let Err(error) = utils::remove_dir("staging toolchain", &self.root)
66+
{
67+
warn!(
68+
path = %self.root.display(),
69+
"could not remove staging toolchain: {error}"
70+
);
71+
}
72+
}
73+
}
74+
1675
#[derive(Clone, Debug)]
1776
pub(crate) enum UpdateStatus {
1877
Installed,
@@ -53,8 +112,14 @@ impl InstallMethod<'_, '_> {
53112
_ => debug!("updating existing install for '{}'", self.dest_basename()),
54113
}
55114

56-
debug!("toolchain directory: {}", self.dest_path().display());
57-
let updated = self.run(&self.dest_path(), manifest).await?;
115+
let destination = self.dest_path();
116+
debug!("toolchain directory: {}", destination.display());
117+
let updated = match &self {
118+
InstallMethod::Dist(DistOptions { exists: false, .. }) => {
119+
self.run_staged_dist(&destination, manifest).await?
120+
}
121+
_ => self.run(&destination, manifest).await?,
122+
};
58123

59124
let status = match updated {
60125
false => {
@@ -104,7 +169,9 @@ impl InstallMethod<'_, '_> {
104169
}
105170
InstallMethod::Dist(opts) => {
106171
let prefix = &InstallPrefix::from(path.to_owned());
107-
let maybe_new_hash = opts.install_into(prefix, manifest).await?;
172+
let maybe_new_hash = opts
173+
.install_into(prefix, &opts.update_hash, manifest)
174+
.await?;
108175

109176
if let Some(hash) = maybe_new_hash {
110177
utils::write_file("update hash", &opts.update_hash, &hash)?;
@@ -116,6 +183,36 @@ impl InstallMethod<'_, '_> {
116183
}
117184
}
118185

186+
async fn run_staged_dist(
187+
&self,
188+
destination: &Path,
189+
manifest: Option<ManifestWithHash>,
190+
) -> Result<bool> {
191+
let InstallMethod::Dist(opts) = self else {
192+
unreachable!("only distribution installs can be staged");
193+
};
194+
195+
let staging = StagedToolchain::new(destination)?;
196+
let prefix = InstallPrefix::from(staging.prefix().to_owned());
197+
// Installation must not touch alias-scoped metadata before the object
198+
// is published, including a stale update hash from an earlier attempt.
199+
let staging_hash = staging.root.join("update-hash");
200+
let Some(hash) = opts.install_into(&prefix, &staging_hash, manifest).await? else {
201+
return Ok(false);
202+
};
203+
204+
#[cfg(feature = "test")]
205+
checkpoint(opts.cfg.process, "install-before-publish");
206+
207+
staging.publish(destination)?;
208+
209+
#[cfg(feature = "test")]
210+
checkpoint(opts.cfg.process, "install-after-publish");
211+
212+
utils::write_file("update hash", &opts.update_hash, &hash)?;
213+
Ok(true)
214+
}
215+
119216
fn cfg(&self) -> &Cfg<'_> {
120217
match self {
121218
InstallMethod::Copy { cfg, .. } => cfg,
@@ -154,3 +251,12 @@ impl InstallMethod<'_, '_> {
154251
pub(crate) fn uninstall(path: &Path) -> Result<()> {
155252
utils::remove_dir("install", path)
156253
}
254+
255+
// `+` is not valid at the start of a toolchain name, so abandoned stages are
256+
// identifiable and excluded by the existing toolchain enumeration.
257+
const STAGING_DIR_PREFIX: &str = "+rustup-staging-";
258+
259+
struct StagedToolchain {
260+
root: PathBuf,
261+
prefix: PathBuf,
262+
}

0 commit comments

Comments
 (0)