Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c9e9521

Browse files
committedApr 18, 2025
Auto merge of #140017 - matthiaskrgr:rollup-w8kunky, r=matthiaskrgr
Rollup of 7 pull requests Successful merges: - #137454 (not lint break with label and unsafe block) - #138934 (support config extensions) - #139297 (Deduplicate & clean up Nix shell) - #139834 (Don't canonicalize crate paths) - #139978 (Add citool command for generating a test dashboard) - #140000 (skip llvm-config in autodiff check builds, when its unavailable) - #140007 (Disable has_thread_local on i686-win7-windows-msvc) r? `@ghost` `@rustbot` modify labels: rollup
2 parents 191df20 + b7b3dde commit c9e9521

File tree

28 files changed

+1098
-105
lines changed

28 files changed

+1098
-105
lines changed
 

‎bootstrap.example.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@
1919
# Note that this has no default value (x.py uses the defaults in `bootstrap.example.toml`).
2020
#profile = <none>
2121

22+
# Inherits configuration values from different configuration files (a.k.a. config extensions).
23+
# Supports absolute paths, and uses the current directory (where the bootstrap was invoked)
24+
# as the base if the given path is not absolute.
25+
#
26+
# The overriding logic follows a right-to-left order. For example, in `include = ["a.toml", "b.toml"]`,
27+
# extension `b.toml` overrides `a.toml`. Also, parent extensions always overrides the inner ones.
28+
#include = []
29+
2230
# Keeps track of major changes made to this configuration.
2331
#
2432
# This value also represents ID of the PR that caused major changes. Meaning,

‎compiler/rustc_metadata/src/locator.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -427,12 +427,21 @@ impl<'a> CrateLocator<'a> {
427427

428428
let (rlibs, rmetas, dylibs) =
429429
candidates.entry(hash.to_string()).or_default();
430-
let path =
431-
try_canonicalize(&spf.path).unwrap_or_else(|_| spf.path.to_path_buf());
432-
if seen_paths.contains(&path) {
433-
continue;
434-
};
435-
seen_paths.insert(path.clone());
430+
{
431+
// As a perforamnce optimisation we canonicalize the path and skip
432+
// ones we've already seeen. This allows us to ignore crates
433+
// we know are exactual equal to ones we've already found.
434+
// Going to the same crate through different symlinks does not change the result.
435+
let path = try_canonicalize(&spf.path)
436+
.unwrap_or_else(|_| spf.path.to_path_buf());
437+
if seen_paths.contains(&path) {
438+
continue;
439+
};
440+
seen_paths.insert(path);
441+
}
442+
// Use the original path (potentially with unresolved symlinks),
443+
// filesystem code should not care, but this is nicer for diagnostics.
444+
let path = spf.path.to_path_buf();
436445
match kind {
437446
CrateFlavor::Rlib => rlibs.insert(path, search_path.kind),
438447
CrateFlavor::Rmeta => rmetas.insert(path, search_path.kind),

‎compiler/rustc_parse/src/parser/expr.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1884,13 +1884,15 @@ impl<'a> Parser<'a> {
18841884
let mut expr = self.parse_expr_opt()?;
18851885
if let Some(expr) = &mut expr {
18861886
if label.is_some()
1887-
&& matches!(
1888-
expr.kind,
1887+
&& match &expr.kind {
18891888
ExprKind::While(_, _, None)
1890-
| ExprKind::ForLoop { label: None, .. }
1891-
| ExprKind::Loop(_, None, _)
1892-
| ExprKind::Block(_, None)
1893-
)
1889+
| ExprKind::ForLoop { label: None, .. }
1890+
| ExprKind::Loop(_, None, _) => true,
1891+
ExprKind::Block(block, None) => {
1892+
matches!(block.rules, BlockCheckMode::Default)
1893+
}
1894+
_ => false,
1895+
}
18941896
{
18951897
self.psess.buffer_lint(
18961898
BREAK_WITH_LABEL_AND_LOOP,

‎compiler/rustc_target/src/spec/targets/i686_win7_windows_msvc.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ pub(crate) fn target() -> Target {
77
base.cpu = "pentium4".into();
88
base.max_atomic_width = Some(64);
99
base.supported_sanitizers = SanitizerSet::ADDRESS;
10+
// On Windows 7 32-bit, the alignment characteristic of the TLS Directory
11+
// don't appear to be respected by the PE Loader, leading to crashes. As
12+
// a result, let's disable has_thread_local to make sure TLS goes through
13+
// the emulation layer.
14+
// See https://github.com/rust-lang/rust/issues/138903
15+
base.has_thread_local = false;
1016

1117
base.add_pre_link_args(
1218
LinkerFlavor::Msvc(Lld::No),

‎src/bootstrap/src/core/build_steps/compile.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,8 +1194,7 @@ pub fn rustc_cargo(
11941194
let enzyme_dir = builder.build.out.join(arch).join("enzyme").join("lib");
11951195
cargo.rustflag("-L").rustflag(enzyme_dir.to_str().expect("Invalid path"));
11961196

1197-
if !builder.config.dry_run() {
1198-
let llvm_config = builder.llvm_config(builder.config.build).unwrap();
1197+
if let Some(llvm_config) = builder.llvm_config(builder.config.build) {
11991198
let llvm_version_major = llvm::get_llvm_version_major(builder, &llvm_config);
12001199
cargo.rustflag("-l").rustflag(&format!("Enzyme-{llvm_version_major}"));
12011200
}

‎src/bootstrap/src/core/config/config.rs

Lines changed: 115 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use std::cell::{Cell, RefCell};
77
use std::collections::{BTreeSet, HashMap, HashSet};
88
use std::fmt::{self, Display};
9+
use std::hash::Hash;
910
use std::io::IsTerminal;
1011
use std::path::{Path, PathBuf, absolute};
1112
use std::process::Command;
@@ -701,6 +702,7 @@ pub(crate) struct TomlConfig {
701702
target: Option<HashMap<String, TomlTarget>>,
702703
dist: Option<Dist>,
703704
profile: Option<String>,
705+
include: Option<Vec<PathBuf>>,
704706
}
705707

706708
/// This enum is used for deserializing change IDs from TOML, allowing both numeric values and the string `"ignore"`.
@@ -747,27 +749,35 @@ enum ReplaceOpt {
747749
}
748750

749751
trait Merge {
750-
fn merge(&mut self, other: Self, replace: ReplaceOpt);
752+
fn merge(
753+
&mut self,
754+
parent_config_path: Option<PathBuf>,
755+
included_extensions: &mut HashSet<PathBuf>,
756+
other: Self,
757+
replace: ReplaceOpt,
758+
);
751759
}
752760

753761
impl Merge for TomlConfig {
754762
fn merge(
755763
&mut self,
756-
TomlConfig { build, install, llvm, gcc, rust, dist, target, profile, change_id }: Self,
764+
parent_config_path: Option<PathBuf>,
765+
included_extensions: &mut HashSet<PathBuf>,
766+
TomlConfig { build, install, llvm, gcc, rust, dist, target, profile, change_id, include }: Self,
757767
replace: ReplaceOpt,
758768
) {
759769
fn do_merge<T: Merge>(x: &mut Option<T>, y: Option<T>, replace: ReplaceOpt) {
760770
if let Some(new) = y {
761771
if let Some(original) = x {
762-
original.merge(new, replace);
772+
original.merge(None, &mut Default::default(), new, replace);
763773
} else {
764774
*x = Some(new);
765775
}
766776
}
767777
}
768778

769-
self.change_id.inner.merge(change_id.inner, replace);
770-
self.profile.merge(profile, replace);
779+
self.change_id.inner.merge(None, &mut Default::default(), change_id.inner, replace);
780+
self.profile.merge(None, &mut Default::default(), profile, replace);
771781

772782
do_merge(&mut self.build, build, replace);
773783
do_merge(&mut self.install, install, replace);
@@ -782,13 +792,50 @@ impl Merge for TomlConfig {
782792
(Some(original_target), Some(new_target)) => {
783793
for (triple, new) in new_target {
784794
if let Some(original) = original_target.get_mut(&triple) {
785-
original.merge(new, replace);
795+
original.merge(None, &mut Default::default(), new, replace);
786796
} else {
787797
original_target.insert(triple, new);
788798
}
789799
}
790800
}
791801
}
802+
803+
let parent_dir = parent_config_path
804+
.as_ref()
805+
.and_then(|p| p.parent().map(ToOwned::to_owned))
806+
.unwrap_or_default();
807+
808+
// `include` handled later since we ignore duplicates using `ReplaceOpt::IgnoreDuplicate` to
809+
// keep the upper-level configuration to take precedence.
810+
for include_path in include.clone().unwrap_or_default().iter().rev() {
811+
let include_path = parent_dir.join(include_path);
812+
let include_path = include_path.canonicalize().unwrap_or_else(|e| {
813+
eprintln!("ERROR: Failed to canonicalize '{}' path: {e}", include_path.display());
814+
exit!(2);
815+
});
816+
817+
let included_toml = Config::get_toml_inner(&include_path).unwrap_or_else(|e| {
818+
eprintln!("ERROR: Failed to parse '{}': {e}", include_path.display());
819+
exit!(2);
820+
});
821+
822+
assert!(
823+
included_extensions.insert(include_path.clone()),
824+
"Cyclic inclusion detected: '{}' is being included again before its previous inclusion was fully processed.",
825+
include_path.display()
826+
);
827+
828+
self.merge(
829+
Some(include_path.clone()),
830+
included_extensions,
831+
included_toml,
832+
// Ensures that parent configuration always takes precedence
833+
// over child configurations.
834+
ReplaceOpt::IgnoreDuplicate,
835+
);
836+
837+
included_extensions.remove(&include_path);
838+
}
792839
}
793840
}
794841

@@ -803,7 +850,13 @@ macro_rules! define_config {
803850
}
804851

805852
impl Merge for $name {
806-
fn merge(&mut self, other: Self, replace: ReplaceOpt) {
853+
fn merge(
854+
&mut self,
855+
_parent_config_path: Option<PathBuf>,
856+
_included_extensions: &mut HashSet<PathBuf>,
857+
other: Self,
858+
replace: ReplaceOpt
859+
) {
807860
$(
808861
match replace {
809862
ReplaceOpt::IgnoreDuplicate => {
@@ -903,7 +956,13 @@ macro_rules! define_config {
903956
}
904957

905958
impl<T> Merge for Option<T> {
906-
fn merge(&mut self, other: Self, replace: ReplaceOpt) {
959+
fn merge(
960+
&mut self,
961+
_parent_config_path: Option<PathBuf>,
962+
_included_extensions: &mut HashSet<PathBuf>,
963+
other: Self,
964+
replace: ReplaceOpt,
965+
) {
907966
match replace {
908967
ReplaceOpt::IgnoreDuplicate => {
909968
if self.is_none() {
@@ -1363,13 +1422,15 @@ impl Config {
13631422
Self::get_toml(&builder_config_path)
13641423
}
13651424

1366-
#[cfg(test)]
1367-
pub(crate) fn get_toml(_: &Path) -> Result<TomlConfig, toml::de::Error> {
1368-
Ok(TomlConfig::default())
1425+
pub(crate) fn get_toml(file: &Path) -> Result<TomlConfig, toml::de::Error> {
1426+
#[cfg(test)]
1427+
return Ok(TomlConfig::default());
1428+
1429+
#[cfg(not(test))]
1430+
Self::get_toml_inner(file)
13691431
}
13701432

1371-
#[cfg(not(test))]
1372-
pub(crate) fn get_toml(file: &Path) -> Result<TomlConfig, toml::de::Error> {
1433+
fn get_toml_inner(file: &Path) -> Result<TomlConfig, toml::de::Error> {
13731434
let contents =
13741435
t!(fs::read_to_string(file), format!("config file {} not found", file.display()));
13751436
// Deserialize to Value and then TomlConfig to prevent the Deserialize impl of
@@ -1548,7 +1609,8 @@ impl Config {
15481609
// but not if `bootstrap.toml` hasn't been created.
15491610
let mut toml = if !using_default_path || toml_path.exists() {
15501611
config.config = Some(if cfg!(not(test)) {
1551-
toml_path.canonicalize().unwrap()
1612+
toml_path = toml_path.canonicalize().unwrap();
1613+
toml_path.clone()
15521614
} else {
15531615
toml_path.clone()
15541616
});
@@ -1576,6 +1638,26 @@ impl Config {
15761638
toml.profile = Some("dist".into());
15771639
}
15781640

1641+
// Reverse the list to ensure the last added config extension remains the most dominant.
1642+
// For example, given ["a.toml", "b.toml"], "b.toml" should take precedence over "a.toml".
1643+
//
1644+
// This must be handled before applying the `profile` since `include`s should always take
1645+
// precedence over `profile`s.
1646+
for include_path in toml.include.clone().unwrap_or_default().iter().rev() {
1647+
let include_path = toml_path.parent().unwrap().join(include_path);
1648+
1649+
let included_toml = get_toml(&include_path).unwrap_or_else(|e| {
1650+
eprintln!("ERROR: Failed to parse '{}': {e}", include_path.display());
1651+
exit!(2);
1652+
});
1653+
toml.merge(
1654+
Some(include_path),
1655+
&mut Default::default(),
1656+
included_toml,
1657+
ReplaceOpt::IgnoreDuplicate,
1658+
);
1659+
}
1660+
15791661
if let Some(include) = &toml.profile {
15801662
// Allows creating alias for profile names, allowing
15811663
// profiles to be renamed while maintaining back compatibility
@@ -1597,7 +1679,12 @@ impl Config {
15971679
);
15981680
exit!(2);
15991681
});
1600-
toml.merge(included_toml, ReplaceOpt::IgnoreDuplicate);
1682+
toml.merge(
1683+
Some(include_path),
1684+
&mut Default::default(),
1685+
included_toml,
1686+
ReplaceOpt::IgnoreDuplicate,
1687+
);
16011688
}
16021689

16031690
let mut override_toml = TomlConfig::default();
@@ -1608,7 +1695,12 @@ impl Config {
16081695

16091696
let mut err = match get_table(option) {
16101697
Ok(v) => {
1611-
override_toml.merge(v, ReplaceOpt::ErrorOnDuplicate);
1698+
override_toml.merge(
1699+
None,
1700+
&mut Default::default(),
1701+
v,
1702+
ReplaceOpt::ErrorOnDuplicate,
1703+
);
16121704
continue;
16131705
}
16141706
Err(e) => e,
@@ -1619,7 +1711,12 @@ impl Config {
16191711
if !value.contains('"') {
16201712
match get_table(&format!(r#"{key}="{value}""#)) {
16211713
Ok(v) => {
1622-
override_toml.merge(v, ReplaceOpt::ErrorOnDuplicate);
1714+
override_toml.merge(
1715+
None,
1716+
&mut Default::default(),
1717+
v,
1718+
ReplaceOpt::ErrorOnDuplicate,
1719+
);
16231720
continue;
16241721
}
16251722
Err(e) => err = e,
@@ -1629,7 +1726,7 @@ impl Config {
16291726
eprintln!("failed to parse override `{option}`: `{err}");
16301727
exit!(2)
16311728
}
1632-
toml.merge(override_toml, ReplaceOpt::Override);
1729+
toml.merge(None, &mut Default::default(), override_toml, ReplaceOpt::Override);
16331730

16341731
config.change_id = toml.change_id.inner;
16351732

‎src/bootstrap/src/core/config/tests.rs

Lines changed: 209 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use std::collections::BTreeSet;
2-
use std::env;
32
use std::fs::{File, remove_file};
43
use std::io::Write;
5-
use std::path::Path;
4+
use std::path::{Path, PathBuf};
5+
use std::{env, fs};
66

77
use build_helper::ci::CiEnv;
88
use clap::CommandFactory;
@@ -23,6 +23,27 @@ pub(crate) fn parse(config: &str) -> Config {
2323
)
2424
}
2525

26+
fn get_toml(file: &Path) -> Result<TomlConfig, toml::de::Error> {
27+
let contents = std::fs::read_to_string(file).unwrap();
28+
toml::from_str(&contents).and_then(|table: toml::Value| TomlConfig::deserialize(table))
29+
}
30+
31+
/// Helps with debugging by using consistent test-specific directories instead of
32+
/// random temporary directories.
33+
fn prepare_test_specific_dir() -> PathBuf {
34+
let current = std::thread::current();
35+
// Replace "::" with "_" to make it safe for directory names on Windows systems
36+
let test_path = current.name().unwrap().replace("::", "_");
37+
38+
let testdir = parse("").tempdir().join(test_path);
39+
40+
// clean up any old test files
41+
let _ = fs::remove_dir_all(&testdir);
42+
let _ = fs::create_dir_all(&testdir);
43+
44+
testdir
45+
}
46+
2647
#[test]
2748
fn download_ci_llvm() {
2849
let config = parse("llvm.download-ci-llvm = false");
@@ -539,3 +560,189 @@ fn test_ci_flag() {
539560
let config = Config::parse_inner(Flags::parse(&["check".into()]), |&_| toml::from_str(""));
540561
assert_eq!(config.is_running_on_ci, CiEnv::is_ci());
541562
}
563+
564+
#[test]
565+
fn test_precedence_of_includes() {
566+
let testdir = prepare_test_specific_dir();
567+
568+
let root_config = testdir.join("config.toml");
569+
let root_config_content = br#"
570+
include = ["./extension.toml"]
571+
572+
[llvm]
573+
link-jobs = 2
574+
"#;
575+
File::create(&root_config).unwrap().write_all(root_config_content).unwrap();
576+
577+
let extension = testdir.join("extension.toml");
578+
let extension_content = br#"
579+
change-id=543
580+
include = ["./extension2.toml"]
581+
"#;
582+
File::create(extension).unwrap().write_all(extension_content).unwrap();
583+
584+
let extension = testdir.join("extension2.toml");
585+
let extension_content = br#"
586+
change-id=742
587+
588+
[llvm]
589+
link-jobs = 10
590+
591+
[build]
592+
description = "Some creative description"
593+
"#;
594+
File::create(extension).unwrap().write_all(extension_content).unwrap();
595+
596+
let config = Config::parse_inner(
597+
Flags::parse(&["check".to_owned(), format!("--config={}", root_config.to_str().unwrap())]),
598+
get_toml,
599+
);
600+
601+
assert_eq!(config.change_id.unwrap(), ChangeId::Id(543));
602+
assert_eq!(config.llvm_link_jobs.unwrap(), 2);
603+
assert_eq!(config.description.unwrap(), "Some creative description");
604+
}
605+
606+
#[test]
607+
#[should_panic(expected = "Cyclic inclusion detected")]
608+
fn test_cyclic_include_direct() {
609+
let testdir = prepare_test_specific_dir();
610+
611+
let root_config = testdir.join("config.toml");
612+
let root_config_content = br#"
613+
include = ["./extension.toml"]
614+
"#;
615+
File::create(&root_config).unwrap().write_all(root_config_content).unwrap();
616+
617+
let extension = testdir.join("extension.toml");
618+
let extension_content = br#"
619+
include = ["./config.toml"]
620+
"#;
621+
File::create(extension).unwrap().write_all(extension_content).unwrap();
622+
623+
let config = Config::parse_inner(
624+
Flags::parse(&["check".to_owned(), format!("--config={}", root_config.to_str().unwrap())]),
625+
get_toml,
626+
);
627+
}
628+
629+
#[test]
630+
#[should_panic(expected = "Cyclic inclusion detected")]
631+
fn test_cyclic_include_indirect() {
632+
let testdir = prepare_test_specific_dir();
633+
634+
let root_config = testdir.join("config.toml");
635+
let root_config_content = br#"
636+
include = ["./extension.toml"]
637+
"#;
638+
File::create(&root_config).unwrap().write_all(root_config_content).unwrap();
639+
640+
let extension = testdir.join("extension.toml");
641+
let extension_content = br#"
642+
include = ["./extension2.toml"]
643+
"#;
644+
File::create(extension).unwrap().write_all(extension_content).unwrap();
645+
646+
let extension = testdir.join("extension2.toml");
647+
let extension_content = br#"
648+
include = ["./extension3.toml"]
649+
"#;
650+
File::create(extension).unwrap().write_all(extension_content).unwrap();
651+
652+
let extension = testdir.join("extension3.toml");
653+
let extension_content = br#"
654+
include = ["./extension.toml"]
655+
"#;
656+
File::create(extension).unwrap().write_all(extension_content).unwrap();
657+
658+
let config = Config::parse_inner(
659+
Flags::parse(&["check".to_owned(), format!("--config={}", root_config.to_str().unwrap())]),
660+
get_toml,
661+
);
662+
}
663+
664+
#[test]
665+
fn test_include_absolute_paths() {
666+
let testdir = prepare_test_specific_dir();
667+
668+
let extension = testdir.join("extension.toml");
669+
File::create(&extension).unwrap().write_all(&[]).unwrap();
670+
671+
let root_config = testdir.join("config.toml");
672+
let extension_absolute_path =
673+
extension.canonicalize().unwrap().to_str().unwrap().replace('\\', r"\\");
674+
let root_config_content = format!(r#"include = ["{}"]"#, extension_absolute_path);
675+
File::create(&root_config).unwrap().write_all(root_config_content.as_bytes()).unwrap();
676+
677+
let config = Config::parse_inner(
678+
Flags::parse(&["check".to_owned(), format!("--config={}", root_config.to_str().unwrap())]),
679+
get_toml,
680+
);
681+
}
682+
683+
#[test]
684+
fn test_include_relative_paths() {
685+
let testdir = prepare_test_specific_dir();
686+
687+
let _ = fs::create_dir_all(&testdir.join("subdir/another_subdir"));
688+
689+
let root_config = testdir.join("config.toml");
690+
let root_config_content = br#"
691+
include = ["./subdir/extension.toml"]
692+
"#;
693+
File::create(&root_config).unwrap().write_all(root_config_content).unwrap();
694+
695+
let extension = testdir.join("subdir/extension.toml");
696+
let extension_content = br#"
697+
include = ["../extension2.toml"]
698+
"#;
699+
File::create(extension).unwrap().write_all(extension_content).unwrap();
700+
701+
let extension = testdir.join("extension2.toml");
702+
let extension_content = br#"
703+
include = ["./subdir/another_subdir/extension3.toml"]
704+
"#;
705+
File::create(extension).unwrap().write_all(extension_content).unwrap();
706+
707+
let extension = testdir.join("subdir/another_subdir/extension3.toml");
708+
let extension_content = br#"
709+
include = ["../../extension4.toml"]
710+
"#;
711+
File::create(extension).unwrap().write_all(extension_content).unwrap();
712+
713+
let extension = testdir.join("extension4.toml");
714+
File::create(extension).unwrap().write_all(&[]).unwrap();
715+
716+
let config = Config::parse_inner(
717+
Flags::parse(&["check".to_owned(), format!("--config={}", root_config.to_str().unwrap())]),
718+
get_toml,
719+
);
720+
}
721+
722+
#[test]
723+
fn test_include_precedence_over_profile() {
724+
let testdir = prepare_test_specific_dir();
725+
726+
let root_config = testdir.join("config.toml");
727+
let root_config_content = br#"
728+
profile = "dist"
729+
include = ["./extension.toml"]
730+
"#;
731+
File::create(&root_config).unwrap().write_all(root_config_content).unwrap();
732+
733+
let extension = testdir.join("extension.toml");
734+
let extension_content = br#"
735+
[rust]
736+
channel = "dev"
737+
"#;
738+
File::create(extension).unwrap().write_all(extension_content).unwrap();
739+
740+
let config = Config::parse_inner(
741+
Flags::parse(&["check".to_owned(), format!("--config={}", root_config.to_str().unwrap())]),
742+
get_toml,
743+
);
744+
745+
// "dist" profile would normally set the channel to "auto-detect", but includes should
746+
// override profile settings, so we expect this to be "dev" here.
747+
assert_eq!(config.channel, "dev");
748+
}

‎src/bootstrap/src/utils/change_tracker.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,4 +396,9 @@ pub const CONFIG_CHANGE_HISTORY: &[ChangeInfo] = &[
396396
severity: ChangeSeverity::Info,
397397
summary: "Added a new option `build.compiletest-use-stage0-libtest` to force `compiletest` to use the stage 0 libtest.",
398398
},
399+
ChangeInfo {
400+
change_id: 138934,
401+
severity: ChangeSeverity::Info,
402+
summary: "Added new option `include` to create config extensions.",
403+
},
399404
];

‎src/ci/citool/Cargo.lock

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change

‎src/ci/citool/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ edition = "2021"
55

66
[dependencies]
77
anyhow = "1"
8+
askama = "0.13"
89
clap = { version = "4.5", features = ["derive"] }
910
csv = "1"
1011
diff = "0.1"

‎src/ci/citool/src/analysis.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ use build_helper::metrics::{
88
};
99

1010
use crate::github::JobInfoResolver;
11-
use crate::metrics;
1211
use crate::metrics::{JobMetrics, JobName, get_test_suites};
1312
use crate::utils::{output_details, pluralize};
13+
use crate::{metrics, utils};
1414

1515
/// Outputs durations of individual bootstrap steps from the gathered bootstrap invocations,
1616
/// and also a table with summarized information about executed tests.
@@ -394,18 +394,17 @@ fn aggregate_tests(metrics: &JsonRoot) -> TestSuiteData {
394394
// Poor man's detection of doctests based on the "(line XYZ)" suffix
395395
let is_doctest = matches!(suite.metadata, TestSuiteMetadata::CargoPackage { .. })
396396
&& test.name.contains("(line");
397-
let test_entry = Test { name: generate_test_name(&test.name), stage, is_doctest };
397+
let test_entry = Test {
398+
name: utils::normalize_path_delimiters(&test.name).to_string(),
399+
stage,
400+
is_doctest,
401+
};
398402
tests.insert(test_entry, test.outcome.clone());
399403
}
400404
}
401405
TestSuiteData { tests }
402406
}
403407

404-
/// Normalizes Windows-style path delimiters to Unix-style paths.
405-
fn generate_test_name(name: &str) -> String {
406-
name.replace('\\', "/")
407-
}
408-
409408
/// Prints test changes in Markdown format to stdout.
410409
fn report_test_diffs(
411410
diff: AggregatedTestDiffs,

‎src/ci/citool/src/main.rs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod datadog;
44
mod github;
55
mod jobs;
66
mod metrics;
7+
mod test_dashboard;
78
mod utils;
89

910
use std::collections::{BTreeMap, HashMap};
@@ -22,7 +23,8 @@ use crate::datadog::upload_datadog_metric;
2223
use crate::github::JobInfoResolver;
2324
use crate::jobs::RunType;
2425
use crate::metrics::{JobMetrics, download_auto_job_metrics, download_job_metrics, load_metrics};
25-
use crate::utils::load_env_var;
26+
use crate::test_dashboard::generate_test_dashboard;
27+
use crate::utils::{load_env_var, output_details};
2628

2729
const CI_DIRECTORY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/..");
2830
const DOCKER_DIRECTORY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../docker");
@@ -180,12 +182,26 @@ fn postprocess_metrics(
180182
}
181183

182184
fn post_merge_report(db: JobDatabase, current: String, parent: String) -> anyhow::Result<()> {
183-
let metrics = download_auto_job_metrics(&db, &parent, &current)?;
185+
let metrics = download_auto_job_metrics(&db, Some(&parent), &current)?;
184186

185187
println!("\nComparing {parent} (parent) -> {current} (this PR)\n");
186188

187189
let mut job_info_resolver = JobInfoResolver::new();
188190
output_test_diffs(&metrics, &mut job_info_resolver);
191+
192+
output_details("Test dashboard", || {
193+
println!(
194+
r#"\nRun
195+
196+
```bash
197+
cargo run --manifest-path src/ci/citool/Cargo.toml -- \
198+
test-dashboard {current} --output-dir test-dashboard
199+
```
200+
And then open `test-dashboard/index.html` in your browser to see an overview of all executed tests.
201+
"#
202+
);
203+
});
204+
189205
output_largest_duration_changes(&metrics, &mut job_info_resolver);
190206

191207
Ok(())
@@ -234,6 +250,14 @@ enum Args {
234250
/// Current commit that will be compared to `parent`.
235251
current: String,
236252
},
253+
/// Generate a directory containing a HTML dashboard of test results from a CI run.
254+
TestDashboard {
255+
/// Commit SHA that was tested on CI to analyze.
256+
current: String,
257+
/// Output path for the HTML directory.
258+
#[clap(long)]
259+
output_dir: PathBuf,
260+
},
237261
}
238262

239263
#[derive(clap::ValueEnum, Clone)]
@@ -275,7 +299,11 @@ fn main() -> anyhow::Result<()> {
275299
postprocess_metrics(metrics_path, parent, job_name)?;
276300
}
277301
Args::PostMergeReport { current, parent } => {
278-
post_merge_report(load_db(default_jobs_file)?, current, parent)?;
302+
post_merge_report(load_db(&default_jobs_file)?, current, parent)?;
303+
}
304+
Args::TestDashboard { current, output_dir } => {
305+
let db = load_db(&default_jobs_file)?;
306+
generate_test_dashboard(db, &current, &output_dir)?;
279307
}
280308
}
281309

‎src/ci/citool/src/metrics.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,24 +46,25 @@ pub struct JobMetrics {
4646
/// `parent` and `current` should be commit SHAs.
4747
pub fn download_auto_job_metrics(
4848
job_db: &JobDatabase,
49-
parent: &str,
49+
parent: Option<&str>,
5050
current: &str,
5151
) -> anyhow::Result<HashMap<JobName, JobMetrics>> {
5252
let mut jobs = HashMap::default();
5353

5454
for job in &job_db.auto_jobs {
5555
eprintln!("Downloading metrics of job {}", job.name);
56-
let metrics_parent = match download_job_metrics(&job.name, parent) {
57-
Ok(metrics) => Some(metrics),
58-
Err(error) => {
59-
eprintln!(
60-
r#"Did not find metrics for job `{}` at `{parent}`: {error:?}.
56+
let metrics_parent =
57+
parent.and_then(|parent| match download_job_metrics(&job.name, parent) {
58+
Ok(metrics) => Some(metrics),
59+
Err(error) => {
60+
eprintln!(
61+
r#"Did not find metrics for job `{}` at `{parent}`: {error:?}.
6162
Maybe it was newly added?"#,
62-
job.name
63-
);
64-
None
65-
}
66-
};
63+
job.name
64+
);
65+
None
66+
}
67+
});
6768
let metrics_current = download_job_metrics(&job.name, current)?;
6869
jobs.insert(
6970
job.name.clone(),

‎src/ci/citool/src/test_dashboard.rs

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
use std::collections::{BTreeMap, HashMap};
2+
use std::fs::File;
3+
use std::io::BufWriter;
4+
use std::path::{Path, PathBuf};
5+
6+
use askama::Template;
7+
use build_helper::metrics::{TestOutcome, TestSuiteMetadata};
8+
9+
use crate::jobs::JobDatabase;
10+
use crate::metrics::{JobMetrics, JobName, download_auto_job_metrics, get_test_suites};
11+
use crate::utils::normalize_path_delimiters;
12+
13+
/// Generate a set of HTML files into a directory that contain a dashboard of test results.
14+
pub fn generate_test_dashboard(
15+
db: JobDatabase,
16+
current: &str,
17+
output_dir: &Path,
18+
) -> anyhow::Result<()> {
19+
let metrics = download_auto_job_metrics(&db, None, current)?;
20+
let suites = gather_test_suites(&metrics);
21+
22+
std::fs::create_dir_all(output_dir)?;
23+
24+
let test_count = suites.test_count();
25+
write_page(output_dir, "index.html", &TestSuitesPage { suites, test_count })?;
26+
27+
Ok(())
28+
}
29+
30+
fn write_page<T: Template>(dir: &Path, name: &str, template: &T) -> anyhow::Result<()> {
31+
let mut file = BufWriter::new(File::create(dir.join(name))?);
32+
Template::write_into(template, &mut file)?;
33+
Ok(())
34+
}
35+
36+
fn gather_test_suites(job_metrics: &HashMap<JobName, JobMetrics>) -> TestSuites {
37+
struct CoarseTestSuite<'a> {
38+
tests: BTreeMap<String, Test<'a>>,
39+
}
40+
41+
let mut suites: HashMap<String, CoarseTestSuite> = HashMap::new();
42+
43+
// First, gather tests from all jobs, stages and targets, and aggregate them per suite
44+
// Only work with compiletest suites.
45+
for (job, metrics) in job_metrics {
46+
let test_suites = get_test_suites(&metrics.current);
47+
for suite in test_suites {
48+
let (suite_name, stage, target) = match &suite.metadata {
49+
TestSuiteMetadata::CargoPackage { .. } => {
50+
continue;
51+
}
52+
TestSuiteMetadata::Compiletest { suite, stage, target, .. } => {
53+
(suite.clone(), *stage, target)
54+
}
55+
};
56+
let suite_entry = suites
57+
.entry(suite_name.clone())
58+
.or_insert_with(|| CoarseTestSuite { tests: Default::default() });
59+
let test_metadata = TestMetadata { job, stage, target };
60+
61+
for test in &suite.tests {
62+
let test_name = normalize_test_name(&test.name, &suite_name);
63+
let (test_name, variant_name) = match test_name.rsplit_once('#') {
64+
Some((name, variant)) => (name.to_string(), variant.to_string()),
65+
None => (test_name, "".to_string()),
66+
};
67+
let test_entry = suite_entry
68+
.tests
69+
.entry(test_name.clone())
70+
.or_insert_with(|| Test { revisions: Default::default() });
71+
let variant_entry = test_entry
72+
.revisions
73+
.entry(variant_name)
74+
.or_insert_with(|| TestResults { passed: vec![], ignored: vec![] });
75+
76+
match test.outcome {
77+
TestOutcome::Passed => {
78+
variant_entry.passed.push(test_metadata);
79+
}
80+
TestOutcome::Ignored { ignore_reason: _ } => {
81+
variant_entry.ignored.push(test_metadata);
82+
}
83+
TestOutcome::Failed => {
84+
eprintln!("Warning: failed test {test_name}");
85+
}
86+
}
87+
}
88+
}
89+
}
90+
91+
// Then, split the suites per directory
92+
let mut suites = suites.into_iter().collect::<Vec<_>>();
93+
suites.sort_by(|a, b| a.0.cmp(&b.0));
94+
95+
let suites = suites
96+
.into_iter()
97+
.map(|(suite_name, suite)| TestSuite { group: build_test_group(&suite_name, suite.tests) })
98+
.collect();
99+
100+
TestSuites { suites }
101+
}
102+
103+
/// Recursively expand a test group based on filesystem hierarchy.
104+
fn build_test_group<'a>(name: &str, tests: BTreeMap<String, Test<'a>>) -> TestGroup<'a> {
105+
let mut root_tests = vec![];
106+
let mut subdirs: BTreeMap<String, BTreeMap<String, Test<'a>>> = Default::default();
107+
108+
// Split tests into root tests and tests located in subdirectories
109+
for (name, test) in tests {
110+
let mut components = Path::new(&name).components().peekable();
111+
let subdir = components.next().unwrap();
112+
113+
if components.peek().is_none() {
114+
// This is a root test
115+
root_tests.push((name, test));
116+
} else {
117+
// This is a test in a nested directory
118+
let subdir_tests =
119+
subdirs.entry(subdir.as_os_str().to_str().unwrap().to_string()).or_default();
120+
let test_name =
121+
components.into_iter().collect::<PathBuf>().to_str().unwrap().to_string();
122+
subdir_tests.insert(test_name, test);
123+
}
124+
}
125+
let dirs = subdirs
126+
.into_iter()
127+
.map(|(name, tests)| {
128+
let group = build_test_group(&name, tests);
129+
(name, group)
130+
})
131+
.collect();
132+
133+
TestGroup { name: name.to_string(), root_tests, groups: dirs }
134+
}
135+
136+
/// Compiletest tests start with `[suite] tests/[suite]/a/b/c...`.
137+
/// Remove the `[suite] tests/[suite]/` prefix so that we can find the filesystem path.
138+
/// Also normalizes path delimiters.
139+
fn normalize_test_name(name: &str, suite_name: &str) -> String {
140+
let name = normalize_path_delimiters(name);
141+
let name = name.as_ref();
142+
let name = name.strip_prefix(&format!("[{suite_name}]")).unwrap_or(name).trim();
143+
let name = name.strip_prefix("tests/").unwrap_or(name);
144+
let name = name.strip_prefix(suite_name).unwrap_or(name);
145+
name.trim_start_matches("/").to_string()
146+
}
147+
148+
struct TestSuites<'a> {
149+
suites: Vec<TestSuite<'a>>,
150+
}
151+
152+
impl<'a> TestSuites<'a> {
153+
fn test_count(&self) -> u64 {
154+
self.suites.iter().map(|suite| suite.group.test_count()).sum::<u64>()
155+
}
156+
}
157+
158+
struct TestSuite<'a> {
159+
group: TestGroup<'a>,
160+
}
161+
162+
struct TestResults<'a> {
163+
passed: Vec<TestMetadata<'a>>,
164+
ignored: Vec<TestMetadata<'a>>,
165+
}
166+
167+
struct Test<'a> {
168+
revisions: BTreeMap<String, TestResults<'a>>,
169+
}
170+
171+
impl<'a> Test<'a> {
172+
/// If this is a test without revisions, it will have a single entry in `revisions` with
173+
/// an empty string as the revision name.
174+
fn single_test(&self) -> Option<&TestResults<'a>> {
175+
if self.revisions.len() == 1 {
176+
self.revisions.iter().next().take_if(|e| e.0.is_empty()).map(|e| e.1)
177+
} else {
178+
None
179+
}
180+
}
181+
}
182+
183+
#[derive(Clone, Copy)]
184+
#[allow(dead_code)]
185+
struct TestMetadata<'a> {
186+
job: &'a str,
187+
stage: u32,
188+
target: &'a str,
189+
}
190+
191+
// We have to use a template for the TestGroup instead of a macro, because
192+
// macros cannot be recursive in askama at the moment.
193+
#[derive(Template)]
194+
#[template(path = "test_group.askama")]
195+
/// Represents a group of tests
196+
struct TestGroup<'a> {
197+
name: String,
198+
/// Tests located directly in this directory
199+
root_tests: Vec<(String, Test<'a>)>,
200+
/// Nested directories with additional tests
201+
groups: Vec<(String, TestGroup<'a>)>,
202+
}
203+
204+
impl<'a> TestGroup<'a> {
205+
fn test_count(&self) -> u64 {
206+
let root = self.root_tests.len() as u64;
207+
self.groups.iter().map(|(_, group)| group.test_count()).sum::<u64>() + root
208+
}
209+
}
210+
211+
#[derive(Template)]
212+
#[template(path = "test_suites.askama")]
213+
struct TestSuitesPage<'a> {
214+
suites: TestSuites<'a>,
215+
test_count: u64,
216+
}

‎src/ci/citool/src/utils.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::borrow::Cow;
12
use std::path::Path;
23

34
use anyhow::Context;
@@ -28,3 +29,8 @@ where
2829
func();
2930
println!("</details>\n");
3031
}
32+
33+
/// Normalizes Windows-style path delimiters to Unix-style paths.
34+
pub fn normalize_path_delimiters(name: &str) -> Cow<str> {
35+
if name.contains("\\") { name.replace('\\', "/").into() } else { name.into() }
36+
}

‎src/ci/citool/templates/layout.askama

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<html>
2+
<head>
3+
<meta charset="UTF-8">
4+
<title>Rust CI Test Dashboard</title>
5+
<style>
6+
body {
7+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
8+
line-height: 1.6;
9+
max-width: 1500px;
10+
margin: 0 auto;
11+
padding: 20px;
12+
background: #F5F5F5;
13+
}
14+
{% block styles %}{% endblock %}
15+
</style>
16+
</head>
17+
18+
<body>
19+
{% block content %}{% endblock %}
20+
{% block scripts %}{% endblock %}
21+
</body>
22+
</html>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{% macro test_result(r) -%}
2+
passed: {{ r.passed.len() }}, ignored: {{ r.ignored.len() }}
3+
{%- endmacro %}
4+
5+
<li>
6+
<details>
7+
<summary>{{ name }} ({{ test_count() }} test{{ test_count() | pluralize }}{% if !root_tests.is_empty() && root_tests.len() as u64 != test_count() -%}
8+
, {{ root_tests.len() }} root test{{ root_tests.len() | pluralize }}
9+
{%- endif %}{% if !groups.is_empty() -%}
10+
, {{ groups.len() }} subdir{{ groups.len() | pluralize }}
11+
{%- endif %})
12+
</summary>
13+
14+
{% if !groups.is_empty() %}
15+
<ul>
16+
{% for (dir_name, subgroup) in groups %}
17+
{{ subgroup|safe }}
18+
{% endfor %}
19+
</ul>
20+
{% endif %}
21+
22+
{% if !root_tests.is_empty() %}
23+
<ul>
24+
{% for (name, test) in root_tests %}
25+
<li>
26+
{% if let Some(result) = test.single_test() %}
27+
<b>{{ name }}</b> ({% call test_result(result) %})
28+
{% else %}
29+
<b>{{ name }}</b> ({{ test.revisions.len() }} revision{{ test.revisions.len() | pluralize }})
30+
<ul>
31+
{% for (revision, result) in test.revisions %}
32+
<li>#<i>{{ revision }}</i> ({% call test_result(result) %})</li>
33+
{% endfor %}
34+
</ul>
35+
{% endif %}
36+
</li>
37+
{% endfor %}
38+
</ul>
39+
{% endif %}
40+
41+
</details>
42+
</li>
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
{% extends "layout.askama" %}
2+
3+
{% block content %}
4+
<h1>Rust CI test dashboard</h1>
5+
<div>
6+
Here's how to interpret the "passed" and "ignored" counts:
7+
the count includes all combinations of "stage" x "target" x "CI job where the test was executed or ignored".
8+
</div>
9+
<div class="test-suites">
10+
<div class="summary">
11+
<div>
12+
<div class="test-count">Total tests: {{ test_count }}</div>
13+
<div>
14+
To find tests that haven't been executed anywhere, click on "Open all" and search for "passed: 0".
15+
</div>
16+
</div>
17+
<div>
18+
<button onclick="openAll()">Open all</button>
19+
<button onclick="closeAll()">Close all</button>
20+
</div>
21+
</div>
22+
23+
<ul>
24+
{% for suite in suites.suites %}
25+
{{ suite.group|safe }}
26+
{% endfor %}
27+
</ul>
28+
</div>
29+
{% endblock %}
30+
31+
{% block styles %}
32+
h1 {
33+
text-align: center;
34+
color: #333333;
35+
margin-bottom: 30px;
36+
}
37+
38+
.summary {
39+
display: flex;
40+
justify-content: space-between;
41+
}
42+
43+
.test-count {
44+
font-size: 1.2em;
45+
}
46+
47+
.test-suites {
48+
background: white;
49+
border-radius: 8px;
50+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
51+
padding: 20px;
52+
}
53+
54+
ul {
55+
padding-left: 0;
56+
}
57+
58+
li {
59+
list-style: none;
60+
padding-left: 20px;
61+
}
62+
summary {
63+
margin-bottom: 5px;
64+
padding: 6px;
65+
background-color: #F4F4F4;
66+
border: 1px solid #ddd;
67+
border-radius: 4px;
68+
cursor: pointer;
69+
}
70+
summary:hover {
71+
background-color: #CFCFCF;
72+
}
73+
74+
/* Style the disclosure triangles */
75+
details > summary {
76+
list-style: none;
77+
position: relative;
78+
}
79+
80+
details > summary::before {
81+
content: "▶";
82+
position: absolute;
83+
left: -15px;
84+
transform: rotate(0);
85+
transition: transform 0.2s;
86+
}
87+
88+
details[open] > summary::before {
89+
transform: rotate(90deg);
90+
}
91+
{% endblock %}
92+
93+
{% block scripts %}
94+
<script type="text/javascript">
95+
function openAll() {
96+
const details = document.getElementsByTagName("details");
97+
for (const elem of details) {
98+
elem.open = true;
99+
}
100+
}
101+
function closeAll() {
102+
const details = document.getElementsByTagName("details");
103+
for (const elem of details) {
104+
elem.open = false;
105+
}
106+
}
107+
</script>
108+
{% endblock %}

‎src/doc/rustc-dev-guide/src/building/suggested.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,43 @@ your `.git/hooks` folder as `pre-push` (without the `.sh` extension!).
2020

2121
You can also install the hook as a step of running `./x setup`!
2222

23+
## Config extensions
24+
25+
When working on different tasks, you might need to switch between different bootstrap configurations.
26+
Sometimes you may want to keep an old configuration for future use. But saving raw config values in
27+
random files and manually copying and pasting them can quickly become messy, especially if you have a
28+
long history of different configurations.
29+
30+
To simplify managing multiple configurations, you can create config extensions.
31+
32+
For example, you can create a simple config file named `cross.toml`:
33+
34+
```toml
35+
[build]
36+
build = "x86_64-unknown-linux-gnu"
37+
host = ["i686-unknown-linux-gnu"]
38+
target = ["i686-unknown-linux-gnu"]
39+
40+
41+
[llvm]
42+
download-ci-llvm = false
43+
44+
[target.x86_64-unknown-linux-gnu]
45+
llvm-config = "/path/to/llvm-19/bin/llvm-config"
46+
```
47+
48+
Then, include this in your `bootstrap.toml`:
49+
50+
```toml
51+
include = ["cross.toml"]
52+
```
53+
54+
You can also include extensions within extensions recursively.
55+
56+
**Note:** In the `include` field, the overriding logic follows a right-to-left order. For example,
57+
in `include = ["a.toml", "b.toml"]`, extension `b.toml` overrides `a.toml`. Also, parent extensions
58+
always overrides the inner ones.
59+
2360
## Configuring `rust-analyzer` for `rustc`
2461

2562
### Project-local rust-analyzer setup

‎src/tools/nix-dev-shell/flake.nix

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,24 @@
11
{
22
description = "rustc dev shell";
33

4-
inputs = {
5-
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6-
flake-utils.url = "github:numtide/flake-utils";
7-
};
4+
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
85

9-
outputs = { self, nixpkgs, flake-utils, ... }:
10-
flake-utils.lib.eachDefaultSystem (system:
11-
let
12-
pkgs = import nixpkgs { inherit system; };
13-
x = import ./x { inherit pkgs; };
14-
in
15-
{
16-
devShells.default = with pkgs; mkShell {
17-
name = "rustc-dev-shell";
18-
nativeBuildInputs = with pkgs; [
19-
binutils cmake ninja pkg-config python3 git curl cacert patchelf nix
20-
];
21-
buildInputs = with pkgs; [
22-
openssl glibc.out glibc.static x
23-
];
24-
# Avoid creating text files for ICEs.
25-
RUSTC_ICE = "0";
26-
# Provide `libstdc++.so.6` for the self-contained lld.
27-
# Provide `libz.so.1`.
28-
LD_LIBRARY_PATH = "${with pkgs; lib.makeLibraryPath [stdenv.cc.cc.lib zlib]}";
29-
};
30-
}
31-
);
6+
outputs =
7+
{
8+
self,
9+
nixpkgs,
10+
}:
11+
let
12+
inherit (nixpkgs) lib;
13+
forEachSystem = lib.genAttrs lib.systems.flakeExposed;
14+
in
15+
{
16+
devShells = forEachSystem (system: {
17+
default = nixpkgs.legacyPackages.${system}.callPackage ./shell.nix { };
18+
});
19+
20+
packages = forEachSystem (system: {
21+
default = nixpkgs.legacyPackages.${system}.callPackage ./x { };
22+
});
23+
};
3224
}

‎src/tools/nix-dev-shell/shell.nix

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
1-
{ pkgs ? import <nixpkgs> {} }:
2-
let
3-
x = import ./x { inherit pkgs; };
1+
{
2+
pkgs ? import <nixpkgs> { },
3+
}:
4+
let
5+
inherit (pkgs.lib) lists attrsets;
6+
7+
x = pkgs.callPackage ./x { };
8+
inherit (x.passthru) cacert env;
49
in
510
pkgs.mkShell {
6-
name = "rustc";
7-
nativeBuildInputs = with pkgs; [
8-
binutils cmake ninja pkg-config python3 git curl cacert patchelf nix
9-
];
10-
buildInputs = with pkgs; [
11-
openssl glibc.out glibc.static x
12-
];
13-
# Avoid creating text files for ICEs.
14-
RUSTC_ICE = "0";
15-
# Provide `libstdc++.so.6` for the self-contained lld.
16-
# Provide `libz.so.1`
17-
LD_LIBRARY_PATH = "${with pkgs; lib.makeLibraryPath [stdenv.cc.cc.lib zlib]}";
11+
name = "rustc-shell";
12+
13+
inputsFrom = [ x ];
14+
packages = [
15+
pkgs.git
16+
pkgs.nix
17+
x
18+
# Get the runtime deps of the x wrapper
19+
] ++ lists.flatten (attrsets.attrValues env);
20+
21+
env = {
22+
# Avoid creating text files for ICEs.
23+
RUSTC_ICE = 0;
24+
SSL_CERT_FILE = cacert;
25+
};
1826
}

‎src/tools/nix-dev-shell/x/default.nix

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,83 @@
11
{
2-
pkgs ? import <nixpkgs> { },
2+
pkgs,
3+
lib,
4+
stdenv,
5+
rustc,
6+
python3,
7+
makeBinaryWrapper,
8+
# Bootstrap
9+
curl,
10+
pkg-config,
11+
libiconv,
12+
openssl,
13+
patchelf,
14+
cacert,
15+
zlib,
16+
# LLVM Deps
17+
ninja,
18+
cmake,
19+
glibc,
320
}:
4-
pkgs.stdenv.mkDerivation {
5-
name = "x";
21+
stdenv.mkDerivation (self: {
22+
strictDeps = true;
23+
name = "x-none";
24+
25+
outputs = [
26+
"out"
27+
"unwrapped"
28+
];
629

730
src = ./x.rs;
831
dontUnpack = true;
932

10-
nativeBuildInputs = with pkgs; [ rustc ];
33+
nativeBuildInputs = [
34+
rustc
35+
makeBinaryWrapper
36+
];
1137

38+
env.PYTHON = python3.interpreter;
1239
buildPhase = ''
13-
PYTHON=${pkgs.lib.getExe pkgs.python3} rustc -Copt-level=3 --crate-name x $src --out-dir $out/bin
40+
rustc -Copt-level=3 --crate-name x $src --out-dir $unwrapped/bin
1441
'';
1542

16-
meta = with pkgs.lib; {
43+
installPhase =
44+
let
45+
inherit (self.passthru) cacert env;
46+
in
47+
''
48+
makeWrapper $unwrapped/bin/x $out/bin/x \
49+
--set-default SSL_CERT_FILE ${cacert} \
50+
--prefix CPATH ";" "${lib.makeSearchPath "include" env.cpath}" \
51+
--prefix PATH : ${lib.makeBinPath env.path} \
52+
--prefix LD_LIBRARY_PATH : ${lib.makeLibraryPath env.ldLib}
53+
'';
54+
55+
# For accessing them in the devshell
56+
passthru = {
57+
env = {
58+
cpath = [ libiconv ];
59+
path = [
60+
python3
61+
patchelf
62+
curl
63+
pkg-config
64+
cmake
65+
ninja
66+
stdenv.cc
67+
];
68+
ldLib = [
69+
openssl
70+
zlib
71+
stdenv.cc.cc.lib
72+
];
73+
};
74+
cacert = "${cacert}/etc/ssl/certs/ca-bundle.crt";
75+
};
76+
77+
meta = {
1778
description = "Helper for rust-lang/rust x.py";
1879
homepage = "https://github.com/rust-lang/rust/blob/master/src/tools/x";
19-
license = licenses.mit;
80+
license = lib.licenses.mit;
2081
mainProgram = "x";
2182
};
22-
}
83+
})
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#![crate_name = "crateresolve1"]
2+
#![crate_type = "lib"]
3+
4+
pub fn f() -> isize {
5+
10
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#![crate_name = "crateresolve1"]
2+
#![crate_type = "lib"]
3+
4+
pub fn f() -> isize {
5+
20
6+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
extern crate crateresolve1;
2+
3+
fn main() {}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
error[E0464]: multiple candidates for `rlib` dependency `crateresolve1` found
2+
--> multiple-candidates.rs:1:1
3+
|
4+
LL | extern crate crateresolve1;
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
6+
|
7+
= note: candidate #1: ./mylibs/libcrateresolve1-1.rlib
8+
= note: candidate #2: ./mylibs/libcrateresolve1-2.rlib
9+
10+
error: aborting due to 1 previous error
11+
12+
For more information about this error, try `rustc --explain E0464`.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//@ needs-symlink
2+
//@ ignore-cross-compile
3+
4+
// Tests that the multiple candidate dependencies diagnostic prints relative
5+
// paths if a relative library path was passed in.
6+
7+
use run_make_support::{bare_rustc, diff, rfs, rustc};
8+
9+
fn main() {
10+
// Check that relative paths are preserved in the diagnostic
11+
rfs::create_dir("mylibs");
12+
rustc().input("crateresolve1-1.rs").out_dir("mylibs").extra_filename("-1").run();
13+
rustc().input("crateresolve1-2.rs").out_dir("mylibs").extra_filename("-2").run();
14+
check("./mylibs");
15+
16+
// Check that symlinks aren't followed when printing the diagnostic
17+
rfs::rename("mylibs", "original");
18+
rfs::symlink_dir("original", "mylibs");
19+
check("./mylibs");
20+
}
21+
22+
fn check(library_path: &str) {
23+
let out = rustc()
24+
.input("multiple-candidates.rs")
25+
.library_search_path(library_path)
26+
.ui_testing()
27+
.run_fail()
28+
.stderr_utf8();
29+
diff()
30+
.expected_file("multiple-candidates.stderr")
31+
.normalize(r"\\", "/")
32+
.actual_text("(rustc)", &out)
33+
.run();
34+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//@ check-pass
2+
3+
#![deny(break_with_label_and_loop)]
4+
5+
unsafe fn foo() -> i32 { 42 }
6+
7+
fn main () {
8+
'label: loop {
9+
break 'label unsafe { foo() }
10+
};
11+
}

0 commit comments

Comments
 (0)
Please sign in to comment.