use crate::error::warn;
use anyhow::{anyhow, ensure, Context, Result};
use cargo_metadata::MetadataCommand;
use dylint_internal::{
env::{self, var},
rustup::{toolchain_path, SanitizeEnvironment},
};
use semver::Version;
use std::{
env::consts,
fs::{copy, create_dir_all, write},
path::{Path, PathBuf},
};
use tempfile::tempdir;
const README_TXT: &str = r#"
This directory contains Rust compiler drivers used by Dylint
(https://github.com/trailofbits/dylint).
Deleting this directory will cause Dylint to rebuild the drivers
the next time it needs them, but will have no ill effects.
"#;
fn cargo_toml(toolchain: &str, dylint_driver_spec: &str) -> String {
format!(
r#"
[package]
name = "dylint_driver-{}"
version = "0.1.0"
edition = "2018"
[dependencies]
anyhow = "1.0.38"
env_logger = "0.8.3"
dylint_driver = {{ {} }}
"#,
toolchain, dylint_driver_spec,
)
}
fn rust_toolchain(toolchain: &str) -> String {
format!(
r#"
[toolchain]
channel = "{}"
components = ["llvm-tools-preview", "rustc-dev"]
"#,
toolchain,
)
}
const MAIN_RS: &str = r#"
use anyhow::Result;
use std::env;
use std::ffi::OsString;
pub fn main() -> Result<()> {
env_logger::init();
let args: Vec<_> = env::args().map(OsString::from).collect();
dylint_driver::dylint_driver(&args)
}
"#;
#[allow(unknown_lints)]
#[allow(question_mark_in_expression)]
pub fn get(opts: &crate::Dylint, toolchain: &str) -> Result<PathBuf> {
let dylint_drivers = dylint_drivers()?;
let driver_dir = dylint_drivers.join(&toolchain);
if !driver_dir.is_dir() {
create_dir_all(&driver_dir).with_context(|| {
format!(
"`create_dir_all` failed for `{}`",
driver_dir.to_string_lossy()
)
})?;
}
let driver = driver_dir.join("dylint-driver");
if !driver.exists() || is_outdated(opts, toolchain, &driver)? {
build(opts, toolchain, &driver)?;
}
Ok(driver)
}
fn dylint_drivers() -> Result<PathBuf> {
if let Ok(dylint_driver_path) = var(env::DYLINT_DRIVER_PATH) {
let dylint_drivers = Path::new(&dylint_driver_path);
ensure!(dylint_drivers.is_dir());
Ok(dylint_drivers.to_path_buf())
} else {
let home = dirs::home_dir().ok_or_else(|| anyhow!("Could not find HOME directory"))?;
let dylint_drivers = Path::new(&home).join(".dylint_drivers");
if !dylint_drivers.is_dir() {
create_dir_all(&dylint_drivers).with_context(|| {
format!(
"`create_dir_all` failed for `{}`",
dylint_drivers.to_string_lossy()
)
})?;
let readme_txt = dylint_drivers.join("README.txt");
write(&readme_txt, README_TXT).with_context(|| {
format!("`write` failed for `{}`", readme_txt.to_string_lossy())
})?;
}
Ok(dylint_drivers)
}
}
fn is_outdated(opts: &crate::Dylint, toolchain: &str, driver: &Path) -> Result<bool> {
let mut command = dylint_internal::driver(toolchain, driver)?;
(|| -> Result<bool> {
let output = command.args(&["-V"]).output()?;
let stdout = std::str::from_utf8(&output.stdout)?;
let theirs = stdout
.trim_end()
.rsplit_once(' ')
.map(|pair| pair.1)
.ok_or_else(|| anyhow!("Could not determine driver version"))?;
let their_version = Version::parse(theirs)
.with_context(|| format!("Could not parse driver version `{}`", theirs))?;
let our_version = Version::parse(env!("CARGO_PKG_VERSION"))?;
Ok(their_version < our_version)
})()
.or_else(|error| {
warn(opts, &error.to_string());
Ok(true)
})
}
#[allow(clippy::assertions_on_constants)]
#[allow(clippy::expect_used)]
fn build(opts: &crate::Dylint, toolchain: &str, driver: &Path) -> Result<()> {
let tempdir = tempdir().with_context(|| "`tempdir` failed")?;
let package = tempdir.path();
initialize(toolchain, package)?;
let metadata = MetadataCommand::new()
.current_dir(package)
.no_deps()
.exec()?;
let toolchain_path = toolchain_path(package)?;
let rustflags = format!(
"-C link-args=-Wl,-rpath,{}/lib",
toolchain_path.to_string_lossy()
);
dylint_internal::build(&format!("driver for toolchain `{}`", toolchain), opts.quiet)
.sanitize_environment()
.envs(vec![(env::RUSTFLAGS, rustflags)])
.current_dir(&package)
.success()?;
let binary = metadata.target_directory.join("debug").join(format!(
"dylint_driver-{}{}",
toolchain,
consts::EXE_SUFFIX
));
copy(&binary, driver).with_context(|| {
format!(
"Could not copy `{}` to `{}`",
binary,
driver.to_string_lossy()
)
})?;
Ok(())
}
fn initialize(toolchain: &str, package: &Path) -> Result<()> {
let version_spec = format!("version = \"={}\"", env!("CARGO_PKG_VERSION"));
#[cfg(any(not(debug_assertions), not(feature = "dylint_driver_local")))]
let path_spec = "";
#[cfg(all(debug_assertions, feature = "dylint_driver_local"))]
#[allow(clippy::expect_used)]
let path_spec = format!(
", path = \"{}\"",
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("Could not get parent directory")
.join("driver")
.to_string_lossy()
.replace('\\', "\\\\")
);
let dylint_driver_spec = format!("{}{}", version_spec, path_spec);
let cargo_toml_path = package.join("Cargo.toml");
write(&cargo_toml_path, cargo_toml(toolchain, &dylint_driver_spec))
.with_context(|| format!("`write` failed for `{}`", cargo_toml_path.to_string_lossy()))?;
let rust_toolchain_path = package.join("rust-toolchain");
write(&rust_toolchain_path, rust_toolchain(toolchain)).with_context(|| {
format!(
"`write` failed for `{}`",
rust_toolchain_path.to_string_lossy()
)
})?;
let src = package.join("src");
create_dir_all(&src)
.with_context(|| format!("`create_dir_all` failed for `{}`", src.to_string_lossy()))?;
let main_rs = src.join("main.rs");
write(&main_rs, MAIN_RS)
.with_context(|| format!("`write` failed for `{}`", main_rs.to_string_lossy()))?;
Ok(())
}