use std::path::PathBuf;
use rucc_base::Interner;
use rucc_lex::PpTokenKind;
use rucc_pp::{Context, Predef, Preprocessor, Tok};
use rucc_session::{FileSystem, Options, Session};
use rucc_target::Triple;
use rucc_tuple::{Env, Os, TargetTuple, Version};
use crate::preprocess::OsFileSystem;
const PROBE: &str = "<glibc version probe>";
const MARKER: &str = "__rucc_glibc_release";
const SOURCE: &[u8] = b"#include <features.h>\n\
#if defined __GLIBC__ && defined __GLIBC_MINOR__\n\
__rucc_glibc_release __GLIBC__ __GLIBC_MINOR__\n\
#endif\n";
pub(crate) fn skew(
target: Triple,
pinned: Option<TargetTuple>,
dirs: &[PathBuf],
) -> Option<String> {
skew_in(target, pinned, dirs, &OsFileSystem::new())
}
fn skew_in(
target: Triple,
pinned: Option<TargetTuple>,
dirs: &[PathBuf],
fs: &dyn FileSystem,
) -> Option<String> {
let tuple = pinned.unwrap_or_else(|| target.tuple());
if tuple.os() != Os::Linux || tuple.env() != Env::Gnu {
return None;
}
let asked = tuple.env_version()?;
let asked = Version::new(asked.major_part(), asked.minor_part()?);
let tree = release_in(target, dirs, fs)?;
if tree == asked {
return None;
}
Some(format!(
"the target asks for glibc {asked} and the tree named with --sysroot is glibc {tree}, and \
the tree is what is compiled and linked against, so the release in the target decides \
nothing here. Drop it from --target, or name a tree that is the release it asks for"
))
}
fn release_in(target: Triple, dirs: &[PathBuf], fs: &dyn FileSystem) -> Option<Version> {
if dirs.is_empty() {
return None;
}
let mut opts = Options::new(target);
for dir in dirs {
opts.search.push_system(dir.clone());
}
let mut sess = Session::new(opts.clone());
let main = sess.sources.add(PROBE, SOURCE.to_vec()).ok()?;
let mut pp = Preprocessor::new();
let predef = Predef::for_options(&opts);
let tokens = {
let mut cx = Context::new(&mut sess.interner, &mut sess.sources, fs, &opts.search);
cx.lex = rucc_lex::Options::for_dialect(opts.std, opts.gnu_extensions);
pp.predefine(&sess.target, &predef, &mut cx).ok()?;
pp.run(main, &mut cx)
};
read_release(&tokens, &sess.interner)
}
fn read_release(tokens: &[Tok], interner: &Interner) -> Option<Version> {
let spelled = |tok: &Tok| tok.value.map(|sym| interner.resolve(sym));
let mut rest = tokens
.iter()
.skip_while(|tok| tok.kind != PpTokenKind::Ident || spelled(tok) != Some(MARKER));
rest.next()?;
let mut number = || {
let tok = rest.next()?;
if tok.kind != PpTokenKind::Number {
return None;
}
spelled(tok)?.parse::<u32>().ok()
};
let major = number()?;
let minor = number()?;
Some(Version::new(major, minor))
}
#[cfg(test)]
mod tests {
use rucc_session::MemoryFileSystem;
use super::*;
fn gnu() -> Triple {
Triple::new(rucc_target::Arch::X86_64, rucc_target::Os::Linux, rucc_target::Env::Gnu)
}
fn musl() -> Triple {
Triple::new(rucc_target::Arch::X86_64, rucc_target::Os::Linux, rucc_target::Env::Musl)
}
fn pin(spelling: &str) -> TargetTuple {
spelling.parse::<TargetTuple>().expect("a tuple this compiler knows")
}
fn tree(major: u32, minor: u32) -> (MemoryFileSystem, Vec<PathBuf>) {
let mut fs = MemoryFileSystem::new();
let text = format!(
"#ifndef _FEATURES_H\n#define _FEATURES_H 1\n#define __GLIBC__ {major}\n#define \
__GLIBC_MINOR__ {minor}\n#define __GLIBC_PREREQ(maj, min) ((__GLIBC__ << 16) + \
__GLIBC_MINOR__ >= ((maj) << 16) + (min))\n#endif\n"
);
fs.insert("/tree/usr/include/features.h", text.into_bytes());
(fs, vec![PathBuf::from("/tree/usr/include")])
}
#[test]
fn the_release_is_read_out_of_the_headers_rather_than_off_the_directory() {
let (fs, dirs) = tree(2, 28);
assert_eq!(release_in(gnu(), &dirs, &fs), Some(Version::new(2, 28)));
}
#[test]
fn a_tree_that_is_the_release_the_target_asked_for_says_nothing() {
let (fs, dirs) = tree(2, 28);
let pinned = pin("x86_64-linux-gnu.2.28");
assert_eq!(skew_in(gnu(), Some(pinned), &dirs, &fs), None);
}
#[test]
fn a_tree_that_is_a_different_release_names_both_of_them() {
let (fs, dirs) = tree(2, 39);
let pinned = pin("x86_64-linux-gnu.2.28");
let said = skew_in(gnu(), Some(pinned), &dirs, &fs).expect("a warning");
assert!(said.contains("glibc 2.28"), "{said}");
assert!(said.contains("glibc 2.39"), "{said}");
}
#[test]
fn a_target_that_pinned_no_release_has_nothing_to_compare() {
let (fs, dirs) = tree(2, 39);
assert_eq!(skew_in(gnu(), None, &dirs, &fs), None);
}
#[test]
fn a_musl_target_is_not_probed_because_there_is_no_macro_to_read() {
let (fs, dirs) = tree(2, 39);
let pinned = pin("x86_64-linux-musl");
assert_eq!(skew_in(musl(), Some(pinned), &dirs, &fs), None);
}
#[test]
fn a_tree_with_no_libc_headers_in_it_answers_nothing_and_is_not_an_error() {
let fs = MemoryFileSystem::new();
let dirs = vec![PathBuf::from("/deps/include")];
assert_eq!(release_in(gnu(), &dirs, &fs), None);
let pinned = pin("x86_64-linux-gnu.2.28");
assert_eq!(skew_in(gnu(), Some(pinned), &dirs, &fs), None);
}
#[test]
fn a_features_header_that_defines_only_half_the_pair_is_not_a_version() {
let mut fs = MemoryFileSystem::new();
fs.insert("/tree/usr/include/features.h", b"#define __GLIBC__ 2\n".to_vec());
let dirs = vec![PathBuf::from("/tree/usr/include")];
assert_eq!(release_in(gnu(), &dirs, &fs), None);
}
#[test]
fn a_header_the_tree_does_not_have_does_not_stop_the_release_being_read() {
let mut fs = MemoryFileSystem::new();
fs.insert(
"/tree/usr/include/features.h",
b"#define __GLIBC__ 2\n#define __GLIBC_MINOR__ 34\n#include <gnu/stubs.h>\n".to_vec(),
);
let dirs = vec![PathBuf::from("/tree/usr/include")];
assert_eq!(release_in(gnu(), &dirs, &fs), Some(Version::new(2, 34)));
}
#[test]
fn nothing_on_the_search_path_is_nothing_to_probe() {
let fs = MemoryFileSystem::new();
assert_eq!(release_in(gnu(), &[], &fs), None);
}
}