mod tempdir;
#[cfg(test)]
mod tests;
use sealed::CompilerAtomic;
use cc::Build;
use std::borrow::Cow;
use std::ffi::{OsStr, OsString};
use std::io::prelude::*;
use std::path::PathBuf;
use std::process::{Command, Output};
use std::sync::atomic::{AtomicI32, Ordering};
use tempdir::TempDir;
static FILE_COUNTER: AtomicI32 = AtomicI32::new(0);
type BoxedError = Box<dyn std::error::Error + Send + Sync + 'static>;
pub struct Target {
is_cl: bool,
temp: TempDir,
toolchain: Build,
verbose: bool,
}
macro_rules! snippet {
($name:expr) => {
include_str!(concat!("../snippets/", $name))
};
}
#[derive(Debug)]
struct CompilationError {
output: Output,
}
impl std::fmt::Display for CompilationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"Compilation error: {}",
String::from_utf8_lossy(&self.output.stderr)
))
}
}
impl std::error::Error for CompilationError {}
fn output_or_err(output: Output) -> Result<(String, String), BoxedError> {
if output.status.success() {
Ok((
String::from_utf8(output.stdout)?,
String::from_utf8(output.stderr)?,
))
} else {
Err(Box::new(CompilationError { output }))
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum BuildMode {
Executable,
ObjectFile,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum LinkType {
#[default]
Default,
Dynamic,
Static,
}
impl LinkType {
fn emit_link_line(&self, lib: &str) {
match self {
LinkType::Static => println!("cargo:rustc-link-lib=static={lib}"),
LinkType::Dynamic => println!("cargo:rustc-link-lib=dylib={lib}"),
LinkType::Default => {
let name = format!("{}_STATIC", lib.to_ascii_uppercase());
println!("cargo:rerun-if-env-changed={name}");
match std::env::var(name).as_deref() {
Err(_) | Ok("0") => println!("cargo:rustc-link-lib={lib}"),
_ => LinkType::Static.emit_link_line(lib),
}
}
}
}
}
pub fn link_library(library: &str, how: LinkType) {
how.emit_link_line(library)
}
pub fn link_libraries(libraries: &[&str], how: LinkType) {
for lib in libraries {
how.emit_link_line(lib)
}
}
pub fn rebuild_if_path_changed(path: &str) {
println!("cargo:rerun-if-changed={path}");
}
pub fn rebuild_if_paths_changed(paths: &[&str]) {
for path in paths {
rebuild_if_path_changed(path)
}
}
pub fn rebuild_if_env_changed(var: &str) {
println!("cargo:rerun-if-env-changed={var}");
}
pub fn rebuild_if_envs_changed(vars: &[&str]) {
for var in vars {
rebuild_if_env_changed(var);
}
}
#[macro_export]
macro_rules! warn {
($msg:tt $(, $($arg:tt)*)?) => {{
println!(concat!("cargo:warning=", $msg) $(, $($arg)*)?)
}};
}
pub fn enable_feature(name: &str) {
declare_feature(name, true)
}
pub fn declare_feature(name: &str, enabled: bool) {
if name.chars().any(|c| c == '"') {
panic!("Invalid feature name: {name}");
}
declare_cfg_values("feature", &[name]);
if enabled {
println!("cargo:rustc-cfg=feature=\"{name}\"");
}
}
pub fn declare_cfg(name: &str, enabled: bool) {
if name.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_') {
panic!("Invalid cfg name {name}");
}
if rustc_version()
.map(|v| v.cmp(&(1, 80, 0)).is_ge())
.unwrap_or(true)
{
println!("cargo:rustc-check-cfg=cfg({name})");
}
if enabled {
println!("cargo:rustc-cfg={name}");
}
}
pub fn enable_cfg(name: &str) {
declare_cfg(name, true);
}
pub fn declare_cfg_values(name: &str, values: &[&str]) {
if name.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_') {
panic!("Invalid cfg name {name}");
}
if rustc_version()
.map(|v| v.cmp(&(1, 80, 0)).is_ge())
.unwrap_or(true)
{
let payload = values
.iter()
.inspect(|value| {
if value.chars().any(|c| c == '"') {
panic!("Invalid value {value} for cfg {name}");
}
})
.map(|v| format!("\"{v}\""))
.collect::<Vec<_>>()
.join(",");
println!("cargo:rustc-check-cfg=cfg({name}, values({payload}))");
}
}
pub fn set_cfg_value(name: &str, value: &str) {
if value.chars().any(|c| c == '"') {
panic!("Invalid value {value} for cfg {name}");
}
println!("cargo:rustc-cfg={name}={value}\"");
}
pub fn set_env_value(name: &str, value: &str) {
if value.chars().any(|c| c == '"') {
panic!("Invalid value {value} for env var {name}");
}
println!("cargo:rustc-env={name}={value}");
}
pub fn add_library_search_path(dir: &str) {
println!("cargo:rustc-link-search={dir}");
}
pub fn out_dir() -> std::path::PathBuf {
let path = std::env::var_os("OUT_DIR").expect("OUT_DIR is missing!");
std::path::PathBuf::from(path)
}
pub fn target_triple() -> String {
std::env::var("TARGET").expect("Missing or invalid TARGET env variable!")
}
pub fn host_triple() -> String {
std::env::var("HOST").expect("Missing or invalid HOST env variable!")
}
pub fn opt_level() -> String {
std::env::var("OPT_LEVEL").expect("Missing or invalid OPT_LEVEL env variable!")
}
pub fn debug() -> bool {
match std::env::var("DEBUG").as_ref().map(|s| s.as_str()) {
Ok("true") => true,
_ => false,
}
}
pub fn profile() -> String {
std::env::var("PROFILE").expect("Missing or invalid PROFILE env variable!")
}
pub fn rustc() -> PathBuf {
let rustc = std::env::var_os("RUSTC").expect("Missing RUSTC environment variable!");
PathBuf::from(rustc)
}
pub fn rustdoc() -> PathBuf {
let rustdoc = std::env::var_os("RUSTDOC").expect("Missing RUSTDOC environment variable!");
PathBuf::from(rustdoc)
}
pub fn rustc_wrapper() -> Option<PathBuf> {
std::env::var_os("RUSTC_WRAPPER").map(PathBuf::from)
}
pub fn rustc_workspace_wrapper() -> Option<PathBuf> {
std::env::var_os("RUSTC_WORKSPACE_WRAPPER").map(PathBuf::from)
}
pub fn rustc_linker() -> Option<PathBuf> {
std::env::var_os("RUSTC_LINKER").map(PathBuf::from)
}
pub fn rustflags() -> Vec<String> {
if let Ok(flags) = std::env::var("CARGO_ENCODED_RUSTFLAGS") {
flags.split('\x1f').map(From::from).collect()
} else if let Ok(flags) = std::env::var("RUSTFLAGS") {
shell_split(&flags)
} else {
Vec::new()
}
}
fn shell_split(s: &str) -> Vec<String> {
let mut args = Vec::new();
let mut current = String::new();
let mut in_single = false;
let mut in_double = false;
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\'' if !in_double => in_single = !in_single,
'"' if !in_single => in_double = !in_double,
' ' if !in_single && !in_double => {
if !current.is_empty() {
args.push(current.clone());
current.clear();
}
}
'\\' if !in_single => {
if let Some(&next) = chars.peek() {
current.push(next);
chars.next();
}
}
_ => current.push(c),
}
}
if !current.is_empty() {
args.push(current);
}
args
}
pub fn num_jobs() -> Option<usize> {
std::env::var("NUM_JOBS")
.ok()
.map(|n| n.parse::<usize>().expect(&format!("Invalid NUM_JOBS: {n}")))
}
impl Target {
const NONE: &'static [&'static str] = &[];
#[inline(always)]
#[allow(non_snake_case)]
fn NULL_CB(_: &str, _: &str) {}
pub fn new() -> std::io::Result<Target> {
let toolchain = cc::Build::new();
Target::new_from(toolchain)
}
pub fn new_from(mut toolchain: cc::Build) -> std::io::Result<Target> {
let temp = if let Some(out_dir) = std::env::var_os("OUT_DIR") {
TempDir::new_in(out_dir)?
} else {
let temp = TempDir::new()?;
toolchain.out_dir(&temp);
temp
};
let is_cl = cfg!(windows) && toolchain.get_compiler().is_like_msvc();
Ok(Self {
is_cl,
temp,
toolchain,
verbose: false,
})
}
pub fn set_verbose(&mut self, verbose: bool) {
self.verbose = verbose;
}
fn new_temp<S: AsRef<str>>(&self, stub: S, ext: &str) -> PathBuf {
let file_num = FILE_COUNTER.fetch_add(1, Ordering::Release);
let stub = stub.as_ref();
let mut path = self.temp.to_owned();
path.push(format!("{stub}-test-{file_num}{ext}"));
path
}
fn build<S: AsRef<str>, C>(
&self,
stub: &str,
mode: BuildMode,
code: &str,
libraries: &[S],
callback: C,
) -> Result<PathBuf, BoxedError>
where
C: FnOnce(&str, &str),
{
let stub = fs_sanitize(stub);
let in_path = self.new_temp(&stub, ".c");
std::fs::File::create(&in_path)?.write_all(code.as_bytes())?;
let exe_ext = if cfg!(unix) { ".out" } else { ".exe" };
let obj_ext = if cfg!(unix) { ".o" } else { ".obj" };
let out_path = match mode {
BuildMode::Executable => self.new_temp(&stub, exe_ext),
BuildMode::ObjectFile => self.new_temp(&stub, obj_ext),
};
let mut cmd = self.toolchain.try_get_compiler()?.to_command();
cmd.current_dir(&self.temp);
let exe = mode == BuildMode::Executable;
let link = exe || !libraries.is_empty();
let mut cmd = if cfg!(unix) || !self.is_cl {
cmd.args([in_path.as_os_str(), OsStr::new("-o"), out_path.as_os_str()]);
if !link {
cmd.arg("-c");
} else if !libraries.is_empty() {
for library in libraries {
cmd.arg(format!("-l{}", library.as_ref()));
}
}
cmd
} else {
cmd.arg(in_path);
let mut output = OsString::from(if exe { "/Fe:" } else { "/Fo:" });
output.push(&out_path);
cmd.arg(output);
if !link {
cmd.arg("/c");
} else if !libraries.is_empty() {
cmd.arg("/link");
for library in libraries {
let mut library = Cow::from(library.as_ref());
if !library.contains('.') {
let owned = library + ".lib";
library = owned;
}
cmd.arg(library.as_ref());
}
}
cmd
};
if self.verbose {
eprintln!("{:?}", cmd);
eprintln!("{}", code);
}
let output = cmd.output()?;
#[cfg(test)]
if self.verbose {
println!("{}", String::from_utf8_lossy(&output.stdout));
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
}
#[cfg(not(test))]
if self.verbose {
std::io::stdout().lock().write_all(&output.stdout).ok();
std::io::stderr().lock().write_all(&output.stderr).ok();
}
let output = output_or_err(output)?;
callback(&output.0, &output.1);
assert!(out_path.exists());
Ok(out_path)
}
pub fn has_type(&self, name: &str) -> bool {
let snippet = format!(snippet!("has_type.c"), "", name);
self.build(
name,
BuildMode::ObjectFile,
&snippet,
Self::NONE,
Self::NULL_CB,
)
.is_ok()
}
pub fn has_type_in(&self, name: &str, headers: &[&str]) -> bool {
let stub = format!("{}_multi", headers.first().unwrap_or(&"has_type_in"));
let snippet = format!(snippet!("has_type.c"), to_includes(headers), name);
self.build(
&stub,
BuildMode::ObjectFile,
&snippet,
Self::NONE,
Self::NULL_CB,
)
.is_ok()
}
pub fn has_symbol(&self, symbol: &str) -> bool {
let snippet = format!(snippet!("has_symbol.c"), symbol);
let libs: &'static [&'static str] = &[];
self.build(symbol, BuildMode::Executable, &snippet, libs, Self::NULL_CB)
.is_ok()
}
pub fn has_symbol_in(&self, symbol: &str, libraries: &[&str]) -> bool {
let snippet = format!(snippet!("has_symbol.c"), symbol);
self.build(
symbol,
BuildMode::Executable,
&snippet,
libraries,
Self::NULL_CB,
)
.is_ok()
}
pub fn has_symbols_in(&self, symbols: &[&str], libraries: &[&str]) -> bool {
symbols
.iter()
.copied()
.all(|symbol| self.has_symbol_in(symbol, libraries))
}
pub fn has_library(&self, library: &str) -> bool {
let snippet = snippet!("empty.c");
self.build(
library,
BuildMode::ObjectFile,
snippet,
&[library],
Self::NULL_CB,
)
.is_ok()
}
pub fn has_libraries(&self, libraries: &[&str]) -> bool {
let stub = libraries.first().copied().unwrap_or("has_libraries");
let snippet = snippet!("empty.c");
self.build(
stub,
BuildMode::ObjectFile,
snippet,
libraries,
Self::NULL_CB,
)
.is_ok()
}
pub fn find_first_library<'a>(&self, libraries: &'a [&str]) -> Option<&'a str> {
for lib in libraries {
if self.has_library(lib) {
return Some(*lib);
}
}
None
}
pub fn find_first_library_with<'a>(
&self,
libraries: &'a [&str],
symbols: &[&str],
) -> Option<&'a str> {
for lib in libraries {
if !self.has_library(lib) {
continue;
}
if self.has_symbols_in(symbols, &[lib]) {
return Some(lib);
}
}
None
}
pub fn has_header(&self, header: &str) -> bool {
let snippet = format!(snippet!("has_header.c"), to_include(header));
self.build(
header,
BuildMode::ObjectFile,
&snippet,
Self::NONE,
Self::NULL_CB,
)
.is_ok()
}
pub fn has_headers(&self, headers: &[&str]) -> bool {
let stub = headers.first().copied().unwrap_or("has_headers");
let snippet = format!(snippet!("has_header.c"), to_includes(headers));
self.build(
stub,
BuildMode::ObjectFile,
&snippet,
Self::NONE,
Self::NULL_CB,
)
.is_ok()
}
pub fn try_link_library(&self, library: &str, how: LinkType) -> bool {
if self.has_library(library) {
link_library(library, how);
return true;
}
false
}
pub fn try_link_libraries(&self, libraries: &[&str], how: LinkType) -> bool {
if self.has_libraries(libraries) {
link_libraries(libraries, how);
return true;
}
false
}
pub fn ifdef(&self, define: &str, headers: &[&str]) -> bool {
let snippet = format!(snippet!("ifdef.c"), to_includes(headers), define);
self.build(
define,
BuildMode::ObjectFile,
&snippet,
Self::NONE,
Self::NULL_CB,
)
.is_ok()
}
pub fn r#if(&self, condition: &str, headers: &[&str]) -> bool {
let snippet = format!(snippet!("if.c"), to_includes(headers), condition);
self.build(
condition,
BuildMode::ObjectFile,
&snippet,
Self::NONE,
Self::NULL_CB,
)
.is_ok()
}
pub fn get_i32_value(&self, ident: &str, headers: &[&str]) -> Result<i32, BoxedError> {
let snippet = format!(snippet!("get_i32_value.c"), to_includes(headers), ident);
let exe = self.build(
ident,
BuildMode::Executable,
&snippet,
Self::NONE,
Self::NULL_CB,
)?;
let output = Command::new(exe).output().map_err(|err| {
format!(
"Failed to run the test executable: {err}!\n{}",
"Note that get_i32_value() does not support cross-compilation!"
)
})?;
Ok(std::str::from_utf8(&output.stdout)?.parse()?)
}
pub fn get_u32_value(&self, ident: &str, headers: &[&str]) -> Result<u32, BoxedError> {
let snippet = format!(snippet!("get_u32_value.c"), to_includes(headers), ident);
let exe = self.build(
ident,
BuildMode::Executable,
&snippet,
Self::NONE,
Self::NULL_CB,
)?;
let output = Command::new(exe).output().map_err(|err| {
format!(
"Failed to run the test executable: {err}!\n{}",
"Note that get_u32_value() does not support cross-compilation!"
)
})?;
Ok(std::str::from_utf8(&output.stdout)?.parse()?)
}
pub fn get_i64_value(&self, ident: &str, headers: &[&str]) -> Result<i64, BoxedError> {
let snippet = format!(snippet!("get_i64_value.c"), to_includes(headers), ident);
let exe = self.build(
ident,
BuildMode::Executable,
&snippet,
Self::NONE,
Self::NULL_CB,
)?;
let output = Command::new(exe).output().map_err(|err| {
format!(
"Failed to run the test executable: {err}!\n{}",
"Note that get_i64_value() does not support cross-compilation!"
)
})?;
Ok(std::str::from_utf8(&output.stdout)?.parse()?)
}
pub fn get_u64_value(&self, ident: &str, headers: &[&str]) -> Result<u64, BoxedError> {
let snippet = format!(snippet!("get_u64_value.c"), to_includes(headers), ident);
let exe = self.build(
ident,
BuildMode::Executable,
&snippet,
Self::NONE,
Self::NULL_CB,
)?;
let output = Command::new(exe).output().map_err(|err| {
format!(
"Failed to run the test executable: {err}!\n{}",
"Note that get_u64_value() does not support cross-compilation!"
)
})?;
Ok(std::str::from_utf8(&output.stdout)?.parse()?)
}
pub fn get_macro_value(
&self,
ident: &str,
headers: &[&str],
) -> Result<Option<String>, BoxedError> {
let bare_name = if let Some(idx) = ident.find('(') {
std::str::from_utf8(&ident.as_bytes()[..idx]).unwrap()
} else {
ident
};
let snippet = format!(
snippet!("get_macro_value.c"),
to_includes(headers),
bare_name,
ident
);
let mut result = None;
let callback = |stdout: &str, stderr: &str| {
let buffer = if self.is_cl { stdout } else { stderr };
if let Some(start) = buffer.find("EXFIL:::").map(|i| i + "EXFIL:::".len()) {
let start = std::str::from_utf8(&buffer.as_bytes()[start..]).unwrap();
let end = start
.find(":::EXFIL")
.expect("Did not find terminating :::EXFIL sequence!");
result = Some(
std::str::from_utf8(&start.as_bytes()[..end])
.unwrap()
.to_string(),
);
}
};
self.build(ident, BuildMode::ObjectFile, &snippet, Self::NONE, callback)
.map_err(|err| {
format!(
"Test compilation failure. Is ident `{}` valid?\n{}",
bare_name, err
)
})?;
Ok(result)
}
pub fn get_macro_value_recursive(
&self,
ident: &str,
headers: &[&str],
) -> Result<Option<String>, BoxedError> {
let mut result = self.get_macro_value(ident, headers)?;
while result.is_some() {
match self.get_macro_value(result.as_ref().unwrap(), headers) {
Ok(Some(r)) => result = Some(r),
_ => break,
};
}
Ok(result)
}
}
impl Target {
pub fn is_unix() -> bool {
std::env::var_os("CARGO_CFG_UNIX").is_some()
}
pub fn is_windows() -> bool {
std::env::var_os("CARGO_CFG_WINDOWS").is_some()
}
pub fn cpu_features() -> Vec<String> {
let features = std::env::var("CARGO_CFG_TARGET_FEATURE").unwrap_or(String::new());
features.split(',').map(From::from).collect()
}
pub fn family() -> Vec<String> {
let families =
std::env::var("CARGO_CFG_TARGET_FAMILY").expect("CARGO_CFG_TARGET_FAMILY not found!");
if families.contains(',') {
families.split(',').map(From::from).collect()
} else {
vec![families]
}
}
pub fn os() -> String {
std::env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not found!")
}
pub fn arch() -> String {
std::env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH not found!")
}
pub fn vendor() -> String {
std::env::var("CARGO_CFG_TARGET_VENDOR").expect("CARGO_CFG_TARGET_VENDOR not found!")
}
pub fn env() -> String {
std::env::var("CARGO_CFG_TARGET_ENV").expect("CARGO_CFG_TARGET_ENV not found!")
}
pub fn abi() -> String {
std::env::var("CARGO_CFG_TARGET_ABI").expect("CARGO_CFG_TARGET_ABI not found!")
}
#[inline(always)]
pub fn triple() -> String {
crate::target_triple()
}
pub fn pointer_width() -> usize {
std::env::var("CARGO_CFG_TARGET_POINTER_WIDTH")
.expect("CARGO_CFG_TARGET_POINTER_WIDTH not found!")
.parse()
.expect("Invalid CARGO_CFG_TARGET_POINTER_WIDTH!")
}
pub fn endian() -> Endian {
match std::env::var("CARGO_CFG_TARGET_ENDIAN")
.expect("CARGO_CFG_TARGET_ENDIAN not found!")
.as_str()
{
"little" => Endian::Little,
"big" => Endian::Big,
other => panic!("Unexpected CARGO_CFG_TARGET_ENDIAN value {other}"),
}
}
}
fn parse_atomic<T>(var: &'static str) -> bool {
let value = std::env::var(var).unwrap_or_default();
let mut atomics = value.split(',');
let tname = std::any::type_name::<T>();
if tname.ends_with("size") {
atomics.find(|&x| x == "ptr").is_some()
} else {
let num = tname.strip_prefix(['u', 'i']).unwrap();
atomics.find(|&x| x == num).is_some()
}
}
impl Target {
pub fn has_atomic<T: CompilerAtomic>() -> bool {
T::has_atomic()
}
#[cfg(feature = "nightly")]
pub fn has_atomic_equal_alignment<T: CompilerAtomic>() -> bool {
T::has_atomic_equal_alignment()
}
#[cfg(feature = "nightly")]
pub fn has_atomic_load_store<T: CompilerAtomic>() -> bool {
T::has_atomic_load_store()
}
}
mod sealed {
use super::parse_atomic;
pub trait CompilerAtomic: Sized {
fn has_atomic() -> bool {
parse_atomic::<Self>("CARGO_CFG_TARGET_HAS_ATOMIC")
}
#[cfg(feature = "nightly")]
fn has_atomic_equal_alignment() -> bool {
parse_atomic::<Self>("CARGO_CFG_TARGET_HAS_ATOMIC_EQUAL_ALIGNMENT")
}
#[cfg(feature = "nightly")]
fn has_atomic_load_store() -> bool {
parse_atomic::<Self>("CARGO_CFG_TARGET_HAS_ATOMIC_LOAD_STORE")
}
}
}
impl CompilerAtomic for u8 {}
impl CompilerAtomic for u16 {}
impl CompilerAtomic for u32 {}
impl CompilerAtomic for u64 {}
impl CompilerAtomic for u128 {}
impl CompilerAtomic for usize {}
impl CompilerAtomic for i8 {}
impl CompilerAtomic for i16 {}
impl CompilerAtomic for i32 {}
impl CompilerAtomic for i64 {}
impl CompilerAtomic for i128 {}
impl CompilerAtomic for isize {}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Endian {
Little,
Big,
}
impl Endian {
pub fn is_little(&self) -> bool {
matches!(self, Self::Little)
}
pub fn is_big(&self) -> bool {
matches!(self, Self::Big)
}
}
impl From<cc::Build> for Target {
fn from(build: cc::Build) -> Self {
Self::new_from(build).unwrap()
}
}
fn fs_sanitize(s: &str) -> Cow<'_, str> {
if s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
return Cow::Borrowed(s);
}
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if !c.is_ascii_alphanumeric() {
out.push('_');
} else {
out.push(c);
}
}
Cow::Owned(out)
}
fn to_include(header: &str) -> String {
format!("#include <{}>", header)
}
fn to_includes(headers: &[&str]) -> String {
let mut vec = Vec::with_capacity(headers.len());
vec.extend(headers.iter().copied().map(to_include));
vec.join("\n")
}
fn rustc_version() -> Option<(u8, u8, u8)> {
use std::env;
use std::sync::OnceLock;
static RUSTC_VERSION: OnceLock<Option<(u8, u8, u8)>> = OnceLock::new();
*RUSTC_VERSION.get_or_init(|| -> Option<(u8, u8, u8)> {
let rustc = env::var_os("RUSTC").unwrap_or_else(|| OsString::from("rustc"));
let mut cmd = match env::var_os("RUSTC_WRAPPER").filter(|w| !w.is_empty()) {
Some(wrapper) => {
let mut cmd = Command::new(wrapper);
cmd.arg(rustc);
cmd
}
None => Command::new(rustc),
};
let cmd = cmd.arg("--version");
let Ok(output) = cmd.output() else {
warn!("Failed to execute rustc!");
return None;
};
let Ok(parts) = std::str::from_utf8(&output.stdout) else {
warn!("Failed to parse `rustc --version` output as UTF-8!");
return None;
};
let mut parts = parts
.strip_prefix("rustc ")
.and_then(|output| output.split([' ', '-']).next())?
.split('.')
.map_while(|v| v.parse::<u8>().ok());
Some((parts.next()?, parts.next()?, parts.next()?))
})
}
#[test]
fn rustc_version_test() {
assert!(matches!(rustc_version(), Some((_major, _minor, _patch))));
}