use std::ffi::{OsStr, OsString};
use std::fs::{DirBuilder, File};
use std::io::prelude::*;
use std::env::var_os as env;
use std::path::{Path, PathBuf};
use std::str;
use cargo_metadata::{Edition, Message, MetadataCommand};
use std::process::{Command, Stdio};
use std::collections::{BTreeMap, BTreeSet};
pub enum ConfTest {}
impl ConfTest {
#[allow(dead_code)]
pub fn run() {
if let Some(inhibit) = env("CONF_TEST_INHIBIT") {
if inhibit == "skip" {
println!("cargo:warning=Skipping ConfTest via CONF_TEST_INHIBIT");
return;
} else if inhibit == "stop" {
std::process::exit(0);
} else if inhibit == "fail" {
println!("cargo:warning=Requested ConfTest failure via CONF_TEST_INHIBIT");
std::process::exit(1)
} else {
panic!("Unknown CONF_TEST_INHIBIT value: {:?}", inhibit)
}
}
let mut outputs = Vec::new();
outputs.push(format!(
"# OUT_DIR is '{:?}'\n",
env("OUT_DIR").expect("env var OUT_DIR is not set")
));
let mut out_dir = PathBuf::new();
out_dir.push(env("OUT_DIR").unwrap());
out_dir.push("conf_test");
DirBuilder::new()
.recursive(true)
.create(out_dir)
.expect("Failed to create output directory");
let mut logfile = PathBuf::new();
logfile.push(env("OUT_DIR").unwrap());
logfile.push("conf_test");
logfile.push("conf_test.log");
let mut logfile = File::create(logfile).expect("Failed to create logfile");
let metadata = MetadataCommand::new()
.other_options(["--frozen".to_string()])
.no_deps()
.exec()
.expect("Querying cargo metadata failed");
let mut features = BTreeSet::new();
let mut dependencies = BTreeSet::new();
let mut edition: Option<Edition> = None;
for package in metadata.packages {
if edition == None {
edition = Some(package.edition);
}
for (feature, _) in package.features {
features.insert(feature);
}
for dep in package.dependencies {
dependencies.insert(dep.name);
}
}
if env("DOCS_RS").is_some() {
outputs.push("# running on DOCS.RS\n".to_string());
if features.contains("docs_rs") {
outputs.push("cargo:rustc-cfg=feature=\"docs_rs\"\n".to_string());
}
} else {
let edition = edition.unwrap_or_else(|| Edition::E2021);
let mut lockfile = PathBuf::new();
lockfile
.push(env("CARGO_MANIFEST_DIR").expect("env var CARGO_MANIFEST_DIR is not set"));
lockfile.push("Cargo.lock");
let lockfile_exists = lockfile.exists();
outputs.push(format!(
"# Lockfile '{:?}' present: {}\n",
lockfile, lockfile_exists
));
let extern_libs = Self::get_extern_libs(&dependencies);
if !lockfile_exists {
outputs.push(format!(
"# Delete Lockfile: '{:?}', {}\n",
&lockfile,
std::fs::remove_file(&lockfile).is_ok()
));
}
let mut test_features = Vec::new();
for feature in features {
if env(format!("CARGO_FEATURE_{}", feature.to_uppercase())).is_none() {
outputs.push(format!("# checking for {}\n", &feature));
let mut test_src = PathBuf::from("conf_tests");
test_src.push(&feature);
test_src.set_extension("rs");
if test_src.exists() {
outputs.push(format!("# {} exists\n", test_src.display()));
outputs.push(format!("cargo:rerun-if-changed={}\n", test_src.display()));
if let Some(binary) =
Self::compile_test(&test_src, &edition, &extern_libs, &test_features)
{
outputs
.push(format!("# compiling ConfTest for {} success\n", &feature));
if let Some(stdout) = Self::run_test(&binary) {
outputs.push(format!(
"# executing ConfTest for {} success\n",
&feature
));
outputs.push(format!("cargo:rustc-cfg=feature=\"{}\"\n", &feature));
outputs.push(stdout);
test_features.push(feature.clone());
} else {
outputs.push(format!(
"# executing ConfTest for {} failed\n",
&feature
));
}
} else {
outputs.push(format!("# compiling ConfTest for {} failed\n", &feature));
}
} else {
outputs.push(format!("# test for '{}' does not exist\n", &feature));
}
} else {
outputs.push(format!("# test for '{}' manually overridden\n", &feature));
}
outputs.push(String::from("\n"));
test_features.push(feature.clone());
}
}
for output in outputs {
logfile.write_all(output.as_bytes()).unwrap();
print!("{}", output);
}
}
fn run_test(test_binary: &Path) -> Option<String> {
let command = Command::new(test_binary).output().ok()?;
if command.status.success() {
Some(String::from_utf8_lossy(&command.stdout).to_string())
} else {
None
}
}
fn compile_test(
src: &Path,
edition: &Edition,
extern_libs: &BTreeMap<OsString, (String, PathBuf)>,
features: &[String],
) -> Option<PathBuf> {
let mut out_file = PathBuf::new();
out_file.push(env("OUT_DIR").expect("env var OUT_DIR is not set"));
out_file.push("conf_test");
out_file.push(src.file_stem().unwrap());
let mut rust_cmd = Command::new(env("RUSTC").unwrap_or_else(|| OsString::from("rustc")));
let rust_cmd = rust_cmd
.arg("--crate-type")
.arg("bin")
.arg("--edition")
.arg(edition_to_str(edition))
.arg("-o")
.arg(&out_file)
.arg("-v")
.arg(src);
for (name, filename) in extern_libs.values() {
rust_cmd.arg("--extern").arg(format!(
"{}={}", name,
filename.to_str().expect("invalid file name")
));
}
for feature in features {
rust_cmd
.arg("--cfg")
.arg(format!("feature=\"{}\"", feature));
}
let rust_output = rust_cmd.output().ok()?;
if rust_output.status.success() {
Some(out_file)
} else {
None
}
}
fn get_extern_libs(dependencies: &BTreeSet<String>) -> BTreeMap<OsString, (String, PathBuf)> {
let mut extern_libs = BTreeMap::new();
let mut target_dir = PathBuf::new();
target_dir.push(env("OUT_DIR").expect("env var OUT_DIR is not set"));
target_dir.push("conf_test");
let mut cargo = Command::new(env("CARGO").unwrap_or_else(|| OsString::from("cargo")))
.arg("--offline")
.arg("rustc")
.arg("--message-format")
.arg("json")
.arg("--target-dir")
.arg(target_dir)
.arg("--")
.arg("--emit")
.arg("metadata")
.env("CONF_TEST_INHIBIT", "stop")
.stdout(Stdio::piped())
.spawn()
.unwrap();
let reader = std::io::BufReader::new(cargo.stdout.take().unwrap());
for message in cargo_metadata::Message::parse_stream(reader) {
if let Message::CompilerArtifact(artifact) = message.unwrap() {
if dependencies.contains(&artifact.target.name) {
for filename in artifact.filenames {
let filename = PathBuf::from(filename);
let id = OsString::from(filename.file_stem().expect("invalid file name"));
let extension = filename.extension();
let name = String::from(&artifact.target.name);
match extension.and_then(OsStr::to_str) {
Some("rlib") => {
extern_libs.insert(id, (name, filename));
}
Some("rmeta") => {
if extern_libs.contains_key(&id) {
let stored_extension = extern_libs[&id]
.1
.extension()
.and_then(OsStr::to_str)
.unwrap();
if stored_extension == "rlib" {
continue;
}
extern_libs.insert(id, (name, filename));
}
}
Some(_other) => {
if extern_libs.contains_key(&id) {
let stored_extension = extern_libs[&id]
.1
.extension()
.and_then(OsStr::to_str)
.unwrap();
if stored_extension == "rmeta" || stored_extension == "rlib" {
continue;
}
extern_libs.insert(id, (name, filename));
}
}
None => {
panic!("extension is not utf8 {:?}", extension);
}
}
}
}
}
}
cargo.wait().expect("Couldn't get cargo's exit status");
extern_libs
}
}
fn edition_to_str(edition: &Edition) -> &str {
match edition {
Edition::E2015 => "2015",
Edition::E2018 => "2018",
Edition::E2021 => "2021",
_ => todo!("send PR for new editions"),
}
}