about summary refs log tree commit diff
diff options
context:
space:
mode:
authorJakub Beránek <berykubik@gmail.com>2025-01-06 17:23:28 +0100
committerJakub Beránek <berykubik@gmail.com>2025-01-21 10:20:24 +0100
commitef9349db86dcbbe705aaf40a2f5dcb1dd9c9063e (patch)
tree657d7333da396b35cb4d05c49ca2442b3b6062da
parentb605c65b6eb5fa71783f8e26df69975f9f1680ee (diff)
downloadrust-ef9349db86dcbbe705aaf40a2f5dcb1dd9c9063e.tar.gz
rust-ef9349db86dcbbe705aaf40a2f5dcb1dd9c9063e.zip
Add test for checking used glibc symbols
-rw-r--r--src/doc/rustc-dev-guide/src/tests/directives.md2
-rw-r--r--src/tools/compiletest/src/directive-list.rs1
-rw-r--r--src/tools/compiletest/src/header.rs2
-rw-r--r--src/tools/compiletest/src/header/cfg.rs6
-rw-r--r--src/tools/opt-dist/src/tests.rs7
-rw-r--r--tests/run-make/glibc-symbols-x86_64-unknown-linux-gnu/rmake.rs108
6 files changed, 124 insertions, 2 deletions
diff --git a/src/doc/rustc-dev-guide/src/tests/directives.md b/src/doc/rustc-dev-guide/src/tests/directives.md
index e80857b7afa..426a6ff1da5 100644
--- a/src/doc/rustc-dev-guide/src/tests/directives.md
+++ b/src/doc/rustc-dev-guide/src/tests/directives.md
@@ -152,6 +152,8 @@ Some examples of `X` in `ignore-X` or `only-X`:
   `compare-mode-split-dwarf`, `compare-mode-split-dwarf-single`
 - The two different test modes used by coverage tests:
   `ignore-coverage-map`, `ignore-coverage-run`
+- When testing a dist toolchain: `dist`
+  - This needs to be enabled with `COMPILETEST_ENABLE_DIST_TESTS=1`
 
 The following directives will check rustc build settings and target
 settings:
diff --git a/src/tools/compiletest/src/directive-list.rs b/src/tools/compiletest/src/directive-list.rs
index 01068af3e8c..5784cd83119 100644
--- a/src/tools/compiletest/src/directive-list.rs
+++ b/src/tools/compiletest/src/directive-list.rs
@@ -175,6 +175,7 @@ const KNOWN_DIRECTIVE_NAMES: &[&str] = &[
     "only-beta",
     "only-bpf",
     "only-cdb",
+    "only-dist",
     "only-gnu",
     "only-i686-pc-windows-gnu",
     "only-i686-pc-windows-msvc",
diff --git a/src/tools/compiletest/src/header.rs b/src/tools/compiletest/src/header.rs
index 8c96554738e..82925f54a16 100644
--- a/src/tools/compiletest/src/header.rs
+++ b/src/tools/compiletest/src/header.rs
@@ -1280,7 +1280,7 @@ pub fn llvm_has_libzstd(config: &Config) -> bool {
             let stderr = String::from_utf8(output.stderr).ok()?;
             let zstd_available = !stderr.contains("LLVM was not built with LLVM_ENABLE_ZSTD");
 
-            // We don't particularly need to clean the link up (so the previous commands could fail
+            // We don't partiCOMPILETEST_ENABLE_OPT_DIST_TESTScularly need to clean the link up (so the previous commands could fail
             // in theory but won't in practice), but we can try.
             std::fs::remove_file(lld_symlink_path).ok()?;
 
diff --git a/src/tools/compiletest/src/header/cfg.rs b/src/tools/compiletest/src/header/cfg.rs
index 3f7225195ce..6e5ced17c20 100644
--- a/src/tools/compiletest/src/header/cfg.rs
+++ b/src/tools/compiletest/src/header/cfg.rs
@@ -235,6 +235,12 @@ fn parse_cfg_name_directive<'a>(
         message: "when the test mode is {name}",
     }
 
+    condition! {
+        name: "dist",
+        condition: std::env::var("COMPILETEST_ENABLE_DIST_TESTS") == Ok("1".to_string()),
+        message: "when performing tests on dist toolchain"
+    }
+
     if prefix == "ignore" && outcome == MatchOutcome::Invalid {
         // Don't error out for ignore-tidy-* diretives, as those are not handled by compiletest.
         if name.starts_with("tidy-") {
diff --git a/src/tools/opt-dist/src/tests.rs b/src/tools/opt-dist/src/tests.rs
index 06ed076a864..2dd116a5f86 100644
--- a/src/tools/opt-dist/src/tests.rs
+++ b/src/tools/opt-dist/src/tests.rs
@@ -108,7 +108,12 @@ llvm-config = "{llvm_config}"
     for test_path in env.skipped_tests() {
         args.extend(["--skip", test_path]);
     }
-    cmd(&args).env("COMPILETEST_FORCE_STAGE0", "1").run().context("Cannot execute tests")
+    cmd(&args)
+        .env("COMPILETEST_FORCE_STAGE0", "1")
+        // Also run dist-only tests
+        .env("COMPILETEST_ENABLE_DIST_TESTS", "1")
+        .run()
+        .context("Cannot execute tests")
 }
 
 /// Tries to find the version of the dist artifacts (either nightly, beta, or 1.XY.Z).
diff --git a/tests/run-make/glibc-symbols-x86_64-unknown-linux-gnu/rmake.rs b/tests/run-make/glibc-symbols-x86_64-unknown-linux-gnu/rmake.rs
new file mode 100644
index 00000000000..ec693ca793c
--- /dev/null
+++ b/tests/run-make/glibc-symbols-x86_64-unknown-linux-gnu/rmake.rs
@@ -0,0 +1,108 @@
+// Check that the compiler toolchain (rustc) that we distribute is not using newer glibc
+// symbols than a specified minimum.
+// This test should only be executed on an extracted dist archive or in a dist-* CI job.
+
+//@ only-dist
+//@ only-x86_64-unknown-linux-gnu
+//@ ignore-cross-compile
+
+use std::path::{Path, PathBuf};
+
+use run_make_support::{cmd, llvm_objdump, regex, rustc_path};
+
+fn main() {
+    // This is the maximum glibc version *supported* by the x86_64-unknown-linux-gnu target.
+    // All glibc symbols used in the compiler must be lower or equal than this version.
+    let max_supported = (2, 17, 99);
+
+    let rustc = PathBuf::from(rustc_path());
+    // Check symbols directly in rustc
+    check_symbols(&rustc, max_supported);
+
+    // Find dynamic libraries referenced by rustc that come from our lib directory
+    let lib_path = rustc.parent().unwrap().parent().unwrap().join("lib");
+    let dynamic_libs = find_dynamic_libs(&rustc)
+        .into_iter()
+        .filter_map(|path| path.canonicalize().ok())
+        .filter(|lib| lib.starts_with(&lib_path))
+        .collect::<Vec<_>>();
+    for lib in dynamic_libs {
+        check_symbols(&lib, max_supported);
+    }
+}
+
+#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)]
+struct GlibcSymbol {
+    name: String,
+    version: (u32, u32, u32),
+}
+
+fn find_dynamic_libs(path: &Path) -> Vec<PathBuf> {
+    cmd("ldd")
+        .arg(path)
+        .run()
+        .stdout_utf8()
+        .lines()
+        .filter_map(|line| {
+            let line = line.trim();
+            let Some((_, line)) = line.split_once(" => ") else {
+                return None;
+            };
+            line.split_ascii_whitespace().next().map(|path| PathBuf::from(path))
+        })
+        .collect()
+}
+
+fn check_symbols(file: &Path, max_supported: (u32, u32, u32)) {
+    println!("Checking {}", file.display());
+    let mut invalid: Vec<GlibcSymbol> = get_glibc_symbols(file)
+        .into_iter()
+        .filter(|symbol| symbol.version > max_supported)
+        .collect();
+    if !invalid.is_empty() {
+        invalid.sort();
+        panic!(
+            "Found invalid glibc symbols in {}:\n{}",
+            file.display(),
+            invalid
+                .into_iter()
+                .map(|symbol| format!(
+                    "{} ({:?} higher than max allowed {:?})",
+                    symbol.name, symbol.version, max_supported
+                ))
+                .collect::<Vec<_>>()
+                .join("\n")
+        )
+    }
+}
+
+fn get_glibc_symbols(file: &Path) -> Vec<GlibcSymbol> {
+    let regex = regex::Regex::new(r#"GLIBC_(\d)+\.(\d+)(:?\.(\d+))?"#).unwrap();
+
+    // Uses llvm-objdump, because implementing this using the `object` crate is quite complicated.
+    llvm_objdump()
+        .arg("-T")
+        .arg(file)
+        .run()
+        .stdout_utf8()
+        .lines()
+        .filter_map(|line| {
+            // Example line
+            // 0000000000000000 DF *UND* 0000000000000000 (GLIBC_2.2.5) sbrk
+            let mut parts = line.split(" ").collect::<Vec<_>>().into_iter().rev();
+            let Some(name) = parts.next() else {
+                return None;
+            };
+            let Some(lib) = parts.next() else {
+                return None;
+            };
+            let Some(version) = regex.captures(lib) else {
+                return None;
+            };
+            let major = version.get(1).and_then(|m| m.as_str().parse().ok()).unwrap_or(0);
+            let minor = version.get(2).and_then(|m| m.as_str().parse().ok()).unwrap_or(0);
+            let patch = version.get(3).and_then(|m| m.as_str().parse().ok()).unwrap_or(0);
+            Some(GlibcSymbol { version: (major, minor, patch), name: name.to_string() })
+        })
+        .collect()
+}