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
812use 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 ) ]
1776pub ( 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<'_, '_> {
154251pub ( 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