Skip to main content

rustc_build_sysroot/
lib.rs

1//! Offers an easy way to build a rustc sysroot from source.
2#![warn(missing_docs)]
3// We prefer to always borrow rather than having to figure out whether we can move or borrow (which
4// depends on whether the variable is used again later).
5#![allow(clippy::needless_borrows_for_generic_args)]
6
7use std::collections::hash_map::DefaultHasher;
8use std::env;
9use std::ffi::{OsStr, OsString};
10use std::fs;
11use std::hash::{Hash, Hasher};
12use std::path::{Path, PathBuf};
13use std::process::Command;
14
15use anyhow::{bail, Context, Result};
16use tempfile::TempDir;
17use toml::{Table, Value};
18use walkdir::WalkDir;
19
20/// The name of the profile used for buliding the sysroot.
21const DEFAULT_SYSROOT_PROFILE: &str = "custom_sysroot";
22
23fn rustc_sysroot_dir(mut rustc: Command) -> Result<PathBuf> {
24    let output = rustc
25        .args(["--print", "sysroot"])
26        .output()
27        .context("failed to determine sysroot")?;
28    if !output.status.success() {
29        bail!(
30            "failed to determine sysroot; rustc said:\n{}",
31            String::from_utf8_lossy(&output.stderr).trim_end()
32        );
33    }
34    let sysroot =
35        std::str::from_utf8(&output.stdout).context("sysroot folder is not valid UTF-8")?;
36    let sysroot = PathBuf::from(sysroot.trim_end_matches('\n'));
37    if !sysroot.is_dir() {
38        bail!(
39            "sysroot directory `{}` is not a directory",
40            sysroot.display()
41        );
42    }
43    Ok(sysroot)
44}
45
46/// Returns where the given rustc stores its sysroot source code.
47pub fn rustc_sysroot_src(rustc: Command) -> Result<PathBuf> {
48    let sysroot = rustc_sysroot_dir(rustc)?;
49    let rustc_src = sysroot
50        .join("lib")
51        .join("rustlib")
52        .join("src")
53        .join("rust")
54        .join("library");
55    // There could be symlinks here, so better canonicalize to avoid busting the cache due to path
56    // changes.
57    let rustc_src = rustc_src.canonicalize().unwrap_or(rustc_src);
58    Ok(rustc_src)
59}
60
61/// Encode a list of rustflags for use in CARGO_ENCODED_RUSTFLAGS.
62pub fn encode_rustflags(flags: &[OsString]) -> OsString {
63    let mut res = OsString::new();
64    for flag in flags {
65        if !res.is_empty() {
66            res.push(OsStr::new("\x1f"));
67        }
68        // Cargo ignores this env var if it's not UTF-8.
69        let flag = flag.to_str().expect("rustflags must be valid UTF-8");
70        if flag.contains('\x1f') {
71            panic!("rustflags must not contain `\\x1f` separator");
72        }
73        res.push(flag);
74    }
75    res
76}
77
78/// Make a file writeable.
79#[cfg(unix)]
80fn make_writeable(p: &Path) -> Result<()> {
81    // On Unix we avoid `set_readonly(false)`, see
82    // <https://rust-lang.github.io/rust-clippy/master/index.html#permissions_set_readonly_false>.
83    use std::fs::Permissions;
84    use std::os::unix::fs::PermissionsExt;
85
86    let perms = fs::metadata(p)?.permissions();
87    let perms = Permissions::from_mode(perms.mode() | 0o600); // read/write for owner
88    fs::set_permissions(p, perms).context("cannot set permissions")?;
89    Ok(())
90}
91
92/// Make a file writeable.
93#[cfg(not(unix))]
94fn make_writeable(p: &Path) -> Result<()> {
95    let mut perms = fs::metadata(p)?.permissions();
96    perms.set_readonly(false);
97    fs::set_permissions(p, perms).context("cannot set permissions")?;
98    Ok(())
99}
100
101/// Hash the metadata and size of every file in a directory, recursively.
102fn hash_recursive(path: &Path, hasher: &mut DefaultHasher) -> Result<()> {
103    // We sort the entries to ensure a stable hash.
104    for entry in WalkDir::new(path)
105        .follow_links(true)
106        .sort_by_file_name()
107        .into_iter()
108    {
109        let entry = entry?;
110        // WalkDir yields the directories as well, and File::open will succeed on them. The
111        // reliable way to distinguish directories here is to check explicitly.
112        if entry.file_type().is_dir() {
113            continue;
114        }
115        let meta = entry.metadata()?;
116        // Hashing the mtime and file size should catch basically all mutations,
117        // and is faster than hashing the file contents.
118        meta.modified()?.hash(hasher);
119        meta.len().hash(hasher);
120    }
121    Ok(())
122}
123
124/// The build mode to use for this sysroot.
125#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
126pub enum BuildMode {
127    /// Do a full sysroot build. Suited for all purposes (like the regular sysroot), but only works
128    /// for the host or for targets that have suitable development tools installed.
129    Build,
130    /// Do a check-only sysroot build. This is only suited for check-only builds of crates, but on
131    /// the plus side it works for *arbitrary* targets without having any special tools installed.
132    Check,
133}
134
135impl BuildMode {
136    /// Returns a string with the cargo command matching this build mode.
137    pub fn as_str(&self) -> &str {
138        use BuildMode::*;
139        match self {
140            Build => "build",
141            Check => "check",
142        }
143    }
144}
145
146/// Settings controlling how the sysroot will be built.
147#[derive(Clone, Debug, PartialEq, Eq, Hash)]
148pub enum SysrootConfig {
149    /// Build a no-std (only core and alloc) sysroot.
150    NoStd,
151    /// Build a full sysroot with the `std` and `test` crates.
152    WithStd {
153        /// Features to enable for the `std` crate.
154        std_features: Vec<String>,
155    },
156}
157
158/// Information about a to-be-created sysroot.
159pub struct SysrootBuilder<'a> {
160    sysroot_dir: PathBuf,
161    target: OsString,
162    config: SysrootConfig,
163    mode: BuildMode,
164    rustflags: Vec<OsString>,
165    cargo: Option<Command>,
166    rustc_version: Option<rustc_version::VersionMeta>,
167    when_build_required: Option<Box<dyn FnOnce() + 'a>>,
168    keep_build_dir: bool,
169}
170
171/// Whether a successful [`SysrootBuilder::build_from_source`] call found a cached sysroot or
172/// built a fresh one.
173#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
174pub enum SysrootStatus {
175    /// The required sysroot is already cached.
176    AlreadyCached,
177    /// A fresh sysroot was just compiled.
178    SysrootBuilt,
179}
180
181/// Hash file name (in target/lib directory).
182const HASH_FILE_NAME: &str = ".rustc-build-sysroot-hash";
183
184impl<'a> SysrootBuilder<'a> {
185    /// Prepare to create a new sysroot in the given folder (that folder should later be passed to
186    /// rustc via `--sysroot`), for the given target.
187    pub fn new(sysroot_dir: &Path, target: impl Into<OsString>) -> Self {
188        let default_flags = &[
189            // This is usually set by bootstrap via `RUSTC_FORCE_UNSTABLE`.
190            "-Zforce-unstable-if-unmarked",
191            // We allow `unexpected_cfgs` as the sysroot has tons of custom `cfg` that rustc does not know about.
192            "-Aunexpected_cfgs",
193        ];
194        SysrootBuilder {
195            sysroot_dir: sysroot_dir.to_owned(),
196            target: target.into(),
197            config: SysrootConfig::WithStd {
198                std_features: vec![],
199            },
200            mode: BuildMode::Build,
201            rustflags: default_flags.iter().map(Into::into).collect(),
202            cargo: None,
203            rustc_version: None,
204            when_build_required: None,
205            keep_build_dir: false,
206        }
207    }
208
209    /// Sets the build mode (regular build vs check-only build).
210    pub fn build_mode(mut self, build_mode: BuildMode) -> Self {
211        self.mode = build_mode;
212        self
213    }
214
215    /// Sets the sysroot configuration (which parts of the sysroot to build and with which features).
216    pub fn sysroot_config(mut self, sysroot_config: SysrootConfig) -> Self {
217        self.config = sysroot_config;
218        self
219    }
220
221    /// Appends the given flag.
222    pub fn rustflag(mut self, rustflag: impl Into<OsString>) -> Self {
223        self.rustflags.push(rustflag.into());
224        self
225    }
226
227    /// Appends the given flags.
228    pub fn rustflags(mut self, rustflags: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
229        self.rustflags.extend(rustflags.into_iter().map(Into::into));
230        self
231    }
232
233    /// Sets the cargo command to call.
234    ///
235    /// This will be invoked with `output()`, so if stdout/stderr should be inherited
236    /// then that needs to be set explicitly.
237    pub fn cargo(mut self, cargo: Command) -> Self {
238        self.cargo = Some(cargo);
239        self
240    }
241
242    /// Sets the rustc version information (in case the user has that available).
243    pub fn rustc_version(mut self, rustc_version: rustc_version::VersionMeta) -> Self {
244        self.rustc_version = Some(rustc_version);
245        self
246    }
247
248    /// Sets the hook that will be called if we don't have a cached sysroot available and a new one
249    /// will be compiled.
250    pub fn when_build_required(mut self, when_build_required: impl FnOnce() + 'a) -> Self {
251        self.when_build_required = Some(Box::new(when_build_required));
252        self
253    }
254
255    /// Sets whether the temporary build directory should be kept around for debugging.
256    pub fn keep_build_dir(mut self, keep: bool) -> Self {
257        self.keep_build_dir = keep;
258        self
259    }
260
261    /// Our configured target can be either a built-in target name, or a path to a target file.
262    /// We use the same logic as rustc to tell which is which:
263    /// https://github.com/rust-lang/rust/blob/8d39ec1825024f3014e1f847942ac5bbfcf055b0/compiler/rustc_session/src/config.rs#L2252-L2263
264    fn target_name(&self) -> &OsStr {
265        let path = Path::new(&self.target);
266        if path.extension().and_then(OsStr::to_str) == Some("json") {
267            // Path::file_stem and Path::extension are the last component of the path split on the
268            // rightmost '.' so if we have an extension we must have a file_stem.
269            path.file_stem().unwrap()
270        } else {
271            // The configured target doesn't end in ".json", so we assume that this is a builtin
272            // target.
273            &self.target
274        }
275    }
276
277    fn sysroot_target_dir(&self) -> PathBuf {
278        self.sysroot_dir
279            .join("lib")
280            .join("rustlib")
281            .join(self.target_name())
282    }
283
284    /// Computes the hash for the sysroot, so that we know whether we have to rebuild.
285    fn sysroot_compute_hash(
286        &self,
287        src_dir: &Path,
288        rustc_version: &rustc_version::VersionMeta,
289    ) -> Result<u64> {
290        let mut hasher = DefaultHasher::new();
291
292        src_dir.hash(&mut hasher);
293        hash_recursive(src_dir, &mut hasher)?;
294        self.config.hash(&mut hasher);
295        self.mode.hash(&mut hasher);
296        self.rustflags.hash(&mut hasher);
297        rustc_version.hash(&mut hasher);
298
299        Ok(hasher.finish())
300    }
301
302    fn sysroot_read_hash(&self) -> Option<u64> {
303        let hash_file = self.sysroot_target_dir().join(HASH_FILE_NAME);
304        let hash = fs::read_to_string(&hash_file).ok()?;
305        hash.parse().ok()
306    }
307
308    /// Generate the contents of the manifest file for the sysroot build.
309    fn gen_manifest(&self, src_dir: &Path) -> String {
310        let crates = match &self.config {
311            SysrootConfig::NoStd => format!(
312                r#"
313                [dependencies.core]
314                path = {src_dir_core:?}
315                [dependencies.alloc]
316                path = {src_dir_alloc:?}
317                [dependencies.compiler_builtins]
318                path = {src_dir_builtins:?}
319                features = ["compiler-builtins", "mem"]
320                "#,
321                src_dir_core = src_dir.join("core"),
322                src_dir_alloc = src_dir.join("alloc"),
323                src_dir_builtins = src_dir.join("compiler-builtins").join("compiler-builtins"),
324            ),
325            SysrootConfig::WithStd { std_features } => format!(
326                r#"
327                [dependencies.std]
328                features = {std_features:?}
329                path = {src_dir_std:?}
330                [dependencies.sysroot]
331                path = {src_dir_sysroot:?}
332                "#,
333                std_features = std_features,
334                src_dir_std = src_dir.join("std"),
335                src_dir_sysroot = src_dir.join("sysroot"),
336            ),
337        };
338
339        // If we include a patch for rustc-std-workspace-std for no_std sysroot builds, we get a
340        // warning from Cargo that the patch is unused. If this patching ever breaks that lint will
341        // probably be very helpful, so it would be best to not disable it.
342        // Currently the only user of rustc-std-workspace-alloc is std_detect, which is only used
343        // by std. So we only need to patch rustc-std-workspace-core in no_std sysroot builds, or
344        // that patch also produces a warning.
345        let unneeded_patches = match &self.config {
346            SysrootConfig::NoStd => &["rustc-std-workspace-alloc", "rustc-std-workspace-std"][..],
347            SysrootConfig::WithStd { .. } => &[][..],
348        };
349
350        let mut patches = extract_patches(src_dir);
351        for (repo, repo_patches) in &mut patches {
352            let repo_patches = repo_patches
353                .as_table_mut()
354                .unwrap_or_else(|| panic!("source `{}` is not a table", repo));
355
356            // Remove any patches that we don't need
357            for krate in unneeded_patches {
358                repo_patches.remove(*krate);
359            }
360
361            // Remap paths to be relative to the source directory.
362            for (krate, patch) in repo_patches {
363                if let Some(path) = patch.get_mut("path") {
364                    let curr_path = path
365                        .as_str()
366                        .unwrap_or_else(|| panic!("`{}.path` is not a string", krate));
367
368                    *path = Value::String(src_dir.join(curr_path).display().to_string());
369                }
370            }
371        }
372
373        let mut table: Table = toml::from_str(&format!(
374            r#"
375            [package]
376            authors = ["rustc-build-sysroot"]
377            name = "custom-local-sysroot"
378            version = "0.0.0"
379            edition = "2018"
380
381            [lib]
382            # empty dummy, just so that things are being built
383            path = "lib.rs"
384
385            [profile.{DEFAULT_SYSROOT_PROFILE}]
386            # We inherit from the local release profile, but then overwrite some
387            # settings to ensure we still get a working sysroot.
388            inherits = "release"
389            panic = 'unwind'
390
391            [lints.cargo]
392            # The dependencies are not actually used.
393            unused_dependencies = "allow"
394
395            {crates}
396            "#
397        ))
398        .expect("failed to parse toml");
399
400        table.insert("patch".to_owned(), patches.into());
401        toml::to_string(&table).expect("failed to serialize to toml")
402    }
403
404    /// Build the `self` sysroot from the given sources.
405    ///
406    /// `src_dir` must be the `library` source folder, i.e., the one that contains `std/Cargo.toml`.
407    pub fn build_from_source(mut self, src_dir: &Path) -> Result<SysrootStatus> {
408        // A bit of preparation.
409        if !src_dir.join("std").join("Cargo.toml").exists() {
410            bail!(
411                "{:?} does not seem to be a rust library source folder: `std/Cargo.toml` not found",
412                src_dir
413            );
414        }
415        let sysroot_target_dir = self.sysroot_target_dir();
416        let target_name = self.target_name().to_owned();
417        let cargo = self.cargo.take().unwrap_or_else(|| {
418            Command::new(env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo")))
419        });
420        let rustc_version = match self.rustc_version.take() {
421            Some(v) => v,
422            None => rustc_version::version_meta()?,
423        };
424
425        // Check if we even need to do anything.
426        let cur_hash = self.sysroot_compute_hash(src_dir, &rustc_version)?;
427        if self.sysroot_read_hash() == Some(cur_hash) {
428            // Already done!
429            return Ok(SysrootStatus::AlreadyCached);
430        }
431
432        // A build is required, so we run the when-build-required function if one was set.
433        if let Some(when_build_required) = self.when_build_required.take() {
434            when_build_required();
435        }
436
437        // Create the *parent* directroy of what we are going to create, so that we can later move
438        // into it.
439        fs::create_dir_all(&sysroot_target_dir.parent().unwrap())
440            .context("failed to create target directory")?;
441        // Remove potentially outdated files. Do this via rename to make it atomic.
442        // We do this *before* the step that takes all the time. That means if a bunch of
443        // these builds happen concurrently, then almost certainly this cleanup will happen before
444        // any of them is done, i.e., we only delete outdated files, not new files.
445        // (The delete will happen automatically when `unstaging_dir` gets dropped.)
446        let unstaging_dir =
447            TempDir::new_in(&self.sysroot_dir).context("failed to create un-staging dir")?;
448        let _ = fs::rename(&sysroot_target_dir, &unstaging_dir); // rename may fail if the dir does not exist yet
449
450        // Prepare a workspace for cargo
451        let build_dir = tempfile::Builder::new()
452            .prefix("rustc-build-sysroot-")
453            .disable_cleanup(self.keep_build_dir)
454            .tempdir()
455            .context("failed to create tempdir")?;
456        // Cargo.lock
457        let lock_file = build_dir.path().join("Cargo.lock");
458        let lock_file_src = {
459            // Since <https://github.com/rust-lang/rust/pull/128534>, the lock file
460            // lives inside the src_dir.
461            let new_lock_file_name = src_dir.join("Cargo.lock");
462            if new_lock_file_name.exists() {
463                new_lock_file_name
464            } else {
465                // Previously, the lock file lived one folder up.
466                src_dir
467                    .parent()
468                    .expect("src_dir must have a parent")
469                    .join("Cargo.lock")
470            }
471        };
472        fs::copy(lock_file_src, &lock_file)
473            .context("failed to copy lockfile from sysroot source")?;
474        make_writeable(&lock_file).context("failed to make lockfile writeable")?;
475        // Cargo.toml
476        let manifest_file = build_dir.path().join("Cargo.toml");
477        let manifest = self.gen_manifest(src_dir);
478        fs::write(&manifest_file, manifest.as_bytes()).context("failed to write manifest file")?;
479        // lib.rs
480        let lib_file = build_dir.path().join("lib.rs");
481        let lib = match self.config {
482            SysrootConfig::NoStd => r#"#![no_std]"#,
483            SysrootConfig::WithStd { .. } => "",
484        };
485        fs::write(&lib_file, lib.as_bytes()).context("failed to write lib file")?;
486
487        // Run cargo.
488        let mut cmd = cargo;
489        cmd.arg(self.mode.as_str());
490        cmd.arg("--profile");
491        cmd.arg(DEFAULT_SYSROOT_PROFILE);
492        cmd.arg("--manifest-path");
493        cmd.arg(&manifest_file);
494        cmd.arg("--target");
495        cmd.arg(&self.target);
496        // Set rustflags.
497        cmd.env("CARGO_ENCODED_RUSTFLAGS", encode_rustflags(&self.rustflags));
498        // Make sure the results end up where we expect them.
499        // Cargo provides multiple ways to adjust this and we need to overwrite all of them.
500        let build_target_dir = build_dir.path().join("target");
501        cmd.env("CARGO_TARGET_DIR", &build_target_dir);
502        cmd.env("CARGO_BUILD_BUILD_DIR", &build_target_dir);
503        // To avoid metadata conflicts, we need to inject some custom data into the crate hash.
504        // bootstrap does the same at
505        // <https://github.com/rust-lang/rust/blob/c8e12cc8bf0de646234524924f39c85d9f3c7c37/src/bootstrap/builder.rs#L1613>.
506        cmd.env("__CARGO_DEFAULT_LIB_METADATA", "rustc-build-sysroot");
507
508        let output = cmd
509            .output()
510            .context("failed to execute cargo for sysroot build")?;
511        if !output.status.success() {
512            let stderr = String::from_utf8_lossy(&output.stderr);
513            if stderr.is_empty() {
514                bail!("sysroot build failed\ncommand: {cmd:?}");
515            } else {
516                bail!("sysroot build failed\ncommand: {cmd:?}\nstderr:\n{stderr}");
517            }
518        }
519
520        // Create a staging dir that will become the target sysroot dir (so that we can do the final
521        // installation atomically). By creating this directory inside `sysroot_dir`, we ensure that
522        // it is on the same file system (so `fs::rename`) works. This also means that the mtime of
523        // `sysroot_dir` gets updated, which rustc bootstrap relies on as a signal that a rebuild
524        // happened.
525        fs::create_dir_all(&self.sysroot_dir).context("failed to create sysroot dir")?; // TempDir expects the parent to already exist
526        let staging_dir =
527            TempDir::new_in(&self.sysroot_dir).context("failed to create staging dir")?;
528        // Copy the output to `$staging/lib`.
529        let staging_lib_dir = staging_dir.path().join("lib");
530        fs::create_dir(&staging_lib_dir).context("failed to create staging/lib dir")?;
531        let out_dir = build_target_dir
532            .join(&target_name)
533            .join(DEFAULT_SYSROOT_PROFILE);
534        if out_dir.join("deps").exists() {
535            // Old build dir layout: $out/deps
536            copy_files(&out_dir.join("deps"), &staging_lib_dir)
537                .context("failed to copy cargo out dir (old layout)")?;
538        } else {
539            // New build dir layout: $out/build/*/*/out.
540            for_each_dir(&out_dir.join("build"), |dir| {
541                for_each_dir(dir, |dir| copy_files(&dir.join("out"), &staging_lib_dir))
542            })
543            .context("failed to copy cargo out dir (new layout)")?;
544        }
545
546        // Write the hash file (into the staging dir).
547        fs::write(
548            staging_dir.path().join(HASH_FILE_NAME),
549            cur_hash.to_string().as_bytes(),
550        )
551        .context("failed to write hash file")?;
552
553        // Atomic copy to final destination via rename.
554        // The rename can fail if there was a concurrent build.
555        if fs::rename(staging_dir.path(), sysroot_target_dir).is_err() {
556            // Ensure that that build is identical to what we were going to do.
557            if self.sysroot_read_hash() != Some(cur_hash) {
558                bail!("detected a concurrent sysroot build with different settings");
559            }
560        }
561
562        Ok(SysrootStatus::SysrootBuilt)
563    }
564}
565
566/// Copy all files in `from` to `to`.
567fn copy_files(from: &Path, to: &Path) -> Result<()> {
568    for entry in fs::read_dir(from)? {
569        let entry = entry?;
570        let file_type = entry.file_type()?;
571        assert!(
572            file_type.is_file(),
573            "cargo out dir must only contain regular files, found {:?} which has file type {:?}",
574            entry.path(),
575            file_type
576        );
577        fs::copy(&entry.path(), to.join(entry.file_name()))?;
578    }
579    Ok(())
580}
581
582/// Invoke the closure for each directory inside `path`.
583fn for_each_dir(path: &Path, f: impl Fn(&Path) -> Result<()>) -> Result<()> {
584    for entry in fs::read_dir(path)? {
585        let entry = entry?;
586        if !entry.file_type()?.is_dir() {
587            continue;
588        }
589        f(&entry.path())?;
590    }
591    Ok(())
592}
593
594/// Collect the patches from the sysroot's workspace `Cargo.toml`.
595fn extract_patches(src_dir: &Path) -> Table {
596    // Assume no patch is needed if the workspace Cargo.toml doesn't exist
597    let workspace_manifest = src_dir.join("Cargo.toml");
598    let f = fs::read_to_string(&workspace_manifest).unwrap_or_else(|e| {
599        panic!(
600            "unable to read workspace manifest at `{}`: {}",
601            workspace_manifest.display(),
602            e
603        )
604    });
605    let mut t: Table = toml::from_str(&f).expect("invalid sysroot workspace Cargo.toml");
606    // We only care about the patch table.
607    t.remove("patch")
608        .map(|v| match v {
609            Value::Table(map) => map,
610            _ => panic!("`patch` is not a table"),
611        })
612        .unwrap_or_default()
613}
614
615#[cfg(test)]
616mod tests {
617    use super::*;
618
619    /// Build a manifest for inspecting, returning the manifest and a source directory.
620    fn setup_manifest_test(config: SysrootConfig) -> (Table, TempDir) {
621        let workspace_toml = r#"
622            [workspace]
623            foo = "bar"
624
625            [patch.crates-io]
626            foo = { path = "bar" }
627            rustc-std-workspace-core = { path = "core" }
628            rustc-std-workspace-alloc = { path = "alloc" }
629            rustc-std-workspace-std = { path = "std" }
630        "#;
631
632        let sysroot = tempfile::tempdir().unwrap(); // dummy sysroot dir, shouldn't be written
633        let src_dir = tempfile::tempdir().unwrap();
634        let f = src_dir.path().join("Cargo.toml");
635        fs::write(&f, workspace_toml).unwrap();
636
637        let builder = SysrootBuilder::new(sysroot.path(), "sometarget").sysroot_config(config);
638        let manifest: Table = toml::from_str(&builder.gen_manifest(src_dir.path())).unwrap();
639        (manifest, src_dir)
640    }
641
642    /// Check that a crate's patch is set to the given path, or that it doesn't exist if `None`.
643    #[track_caller]
644    fn check_patch_path(manifest: &Table, krate: &str, path: Option<&Path>) {
645        let patches = &manifest["patch"]["crates-io"];
646        match path {
647            Some(path) => assert_eq!(
648                &patches[krate]["path"].as_str().unwrap(),
649                &path.to_str().unwrap()
650            ),
651            None => assert!(patches.get(krate).is_none()),
652        }
653    }
654
655    #[test]
656    fn check_patches_no_std() {
657        let (manifest, src_dir) = setup_manifest_test(SysrootConfig::NoStd);
658
659        // Ensure that we use the manifest's paths but mapped to the source directory
660        check_patch_path(&manifest, "foo", Some(&src_dir.path().join("bar")));
661        check_patch_path(
662            &manifest,
663            "rustc-std-workspace-core",
664            Some(&src_dir.path().join("core")),
665        );
666
667        // For `NoStd`, we shouldn't get the alloc and std patches.
668        check_patch_path(&manifest, "rustc-std-workspace-alloc", None);
669        check_patch_path(&manifest, "rustc-std-workspace-std", None);
670    }
671
672    #[test]
673    fn check_patches_with_std() {
674        let (manifest, src_dir) = setup_manifest_test(SysrootConfig::WithStd {
675            std_features: Vec::new(),
676        });
677
678        // For `WithStd`, we should get all patches.
679        check_patch_path(&manifest, "foo", Some(&src_dir.path().join("bar")));
680        check_patch_path(
681            &manifest,
682            "rustc-std-workspace-core",
683            Some(&src_dir.path().join("core")),
684        );
685        check_patch_path(
686            &manifest,
687            "rustc-std-workspace-alloc",
688            Some(&src_dir.path().join("alloc")),
689        );
690        check_patch_path(
691            &manifest,
692            "rustc-std-workspace-std",
693            Some(&src_dir.path().join("std")),
694        );
695    }
696}