LCOV - code coverage report
Current view: top level - dylint-link/src - main.rs (source / functions) Hit Total Coverage
Test: unnamed Lines: 84 180 46.7 %
Date: 2024-10-23 19:20:50 Functions: 5 24 20.8 %

          Line data    Source code
       1             : #![deny(clippy::expect_used)]
       2             : #![deny(clippy::unwrap_used)]
       3             : #![deny(clippy::panic)]
       4             : 
       5             : #[cfg(target_os = "windows")]
       6             : use anyhow::ensure;
       7             : use anyhow::{anyhow, Context, Result};
       8             : use dylint_internal::{cargo::cargo_home, env, library_filename, CommandExt};
       9             : use if_chain::if_chain;
      10             : use std::{
      11             :     env::{args, consts},
      12             :     ffi::OsStr,
      13             :     fs::{copy, read_to_string},
      14             :     path::{Path, PathBuf},
      15             :     process::Command,
      16             : };
      17             : #[cfg(target_os = "windows")]
      18             : use std::{fs::File, io::Read};
      19             : use toml_edit::{DocumentMut, Item};
      20             : 
      21           0 : fn main() -> Result<()> {
      22           0 :     env_logger::init();
      23             : 
      24           0 :     let linker = linker()?;
      25           0 :     let args: Vec<String> = args().collect();
      26           0 :     Command::new(linker).args(&args[1..]).success()?;
      27             : 
      28           0 :     if let Some(path) = output_path(args.iter())? {
      29           0 :         copy_library(&path)?;
      30           0 :     }
      31             : 
      32           0 :     Ok(())
      33           0 : }
      34             : 
      35           0 : fn linker() -> Result<PathBuf> {
      36           0 :     let rustup_toolchain = env::var(env::RUSTUP_TOOLCHAIN)?;
      37           0 :     let target = parse_toolchain(&rustup_toolchain)
      38           0 :         .map_or_else(|| env!("TARGET").to_owned(), |(_, target)| target);
      39           0 :     let cargo_home = cargo_home().with_context(|| "Could not determine `CARGO_HOME`")?;
      40           0 :     let config_toml = cargo_home.join("config.toml");
      41           0 :     if config_toml.is_file() {
      42           0 :         let contents = read_to_string(&config_toml).with_context(|| {
      43           0 :             format!(
      44           0 :                 "`read_to_string` failed for `{}`",
      45           0 :                 config_toml.to_string_lossy()
      46           0 :             )
      47           0 :         })?;
      48           0 :         let document = contents.parse::<DocumentMut>()?;
      49           0 :         document
      50           0 :             .as_table()
      51           0 :             .get("target")
      52           0 :             .and_then(Item::as_table)
      53           0 :             .and_then(|table| table.get(&target))
      54           0 :             .and_then(Item::as_table)
      55           0 :             .and_then(|table| table.get("linker"))
      56           0 :             .and_then(Item::as_str)
      57           0 :             .map_or_else(default_linker, |s| Ok(PathBuf::from(s)))
      58             :     } else {
      59           0 :         default_linker()
      60             :     }
      61           0 : }
      62             : 
      63             : #[cfg(target_os = "windows")]
      64             : fn default_linker() -> Result<PathBuf> {
      65             :     let rustup_toolchain = env::var(env::RUSTUP_TOOLCHAIN)?;
      66             :     if rustup_toolchain.split('-').last() == Some("msvc") {
      67             :         // MinerSebas: Removes the Release Information: "nightly-2021-04-08-x86_64-pc-windows-msvc"
      68             :         // -> "x86_64-pc-windows-msvc"
      69             :         // smoelius: The approach has changed slightly.
      70             :         if let Some(tool) = parse_toolchain(&rustup_toolchain)
      71             :             .and_then(|(_, target)| cc::windows_registry::find_tool(&target, "link.exe"))
      72             :         {
      73             :             Ok(tool.path().into())
      74             :         } else {
      75             :             Err(anyhow!("Could not find the MSVC Linker"))
      76             :         }
      77             :     } else {
      78             :         Err(anyhow!("Only the MSVC toolchain is supported on Windows"))
      79             :     }
      80             : }
      81             : 
      82             : #[cfg(not(target_os = "windows"))]
      83             : #[allow(clippy::unnecessary_wraps)]
      84           0 : fn default_linker() -> Result<PathBuf> {
      85           0 :     Ok(PathBuf::from("cc"))
      86           0 : }
      87             : 
      88             : #[cfg(target_os = "windows")]
      89             : fn output_path<'a, I>(iter: I) -> Result<Option<PathBuf>>
      90             : where
      91             :     I: Iterator<Item = &'a String>,
      92             : {
      93             :     for arg in iter {
      94             :         if let Some(path) = arg.strip_prefix("/OUT:") {
      95             :             return Ok(Some(path.into()));
      96             :         }
      97             :         if let Some(path) = arg.strip_prefix('@') {
      98             :             return extract_out_path_from_linker_response_file(path);
      99             :         }
     100             :     }
     101             : 
     102             :     Ok(None)
     103             : }
     104             : 
     105             : #[cfg(not(target_os = "windows"))]
     106             : #[allow(clippy::unnecessary_wraps)]
     107           0 : fn output_path<'a, I>(mut iter: I) -> Result<Option<PathBuf>>
     108           0 : where
     109           0 :     I: Iterator<Item = &'a String>,
     110           0 : {
     111           0 :     while let Some(arg) = iter.next() {
     112           0 :         if arg == "-o" {
     113           0 :             if let Some(path) = iter.next() {
     114           0 :                 return Ok(Some(path.into()));
     115           0 :             }
     116           0 :         }
     117             :     }
     118             : 
     119           0 :     Ok(None)
     120           0 : }
     121             : 
     122             : #[cfg(target_os = "windows")]
     123             : fn extract_out_path_from_linker_response_file(path: impl AsRef<Path>) -> Result<Option<PathBuf>> {
     124             :     // MinerSebas: On Windows the cmd line has a Limit of 8191 Characters.
     125             :     // If your command would exceed this you can instead use a Linker Response File to set
     126             :     // arguments. (https://docs.microsoft.com/en-us/cpp/build/reference/at-specify-a-linker-response-file?view=msvc-160)
     127             : 
     128             :     // MinerSebas: Read the Linker Response File
     129             :     let mut buf: Vec<u8> = Vec::new();
     130             :     File::open(path)?.read_to_end(&mut buf)?;
     131             : 
     132             :     // MinerSebas: Convert the File from UTF-16 to a Rust UTF-8 String
     133             :     // (Only necessary for MSVC, the GNU Linker uses UTF-8 isntead.)
     134             :     // Based on: https://stackoverflow.com/a/57172592
     135             :     let file: Vec<u16> = buf
     136             :         .chunks_exact(2)
     137             :         .into_iter()
     138             :         .map(|a| u16::from_ne_bytes([a[0], a[1]]))
     139             :         .collect();
     140             :     let file = String::from_utf16_lossy(file.as_slice());
     141             : 
     142             :     let paths: Vec<_> = file
     143             :         .lines()
     144             :         .flat_map(|line| line.trim().trim_matches('"').strip_prefix("/OUT:"))
     145             :         .collect();
     146             : 
     147             :     ensure!(paths.len() <= 1, "Found multiple output paths");
     148             : 
     149             :     // smoelius: Do not raise an error if no output path is found.
     150             :     Ok(paths.last().map(Into::into))
     151             : }
     152             : 
     153           0 : fn copy_library(path: &Path) -> Result<()> {
     154           0 :     if_chain! {
     155           0 :         if let Some(lib_name) = parse_path_plain_filename(path);
     156           0 :         let cargo_pkg_name = env::var(env::CARGO_PKG_NAME)?;
     157           0 :         if lib_name == cargo_pkg_name.replace('-', "_");
     158             :         then {
     159           0 :             let rustup_toolchain = env::var(env::RUSTUP_TOOLCHAIN)?;
     160           0 :             let filename_with_toolchain = library_filename(&lib_name, &rustup_toolchain);
     161           0 :             let parent = path
     162           0 :                 .parent()
     163           0 :                 .ok_or_else(|| anyhow!("Could not get parent directory"))?;
     164           0 :             let path_with_toolchain = strip_deps(parent).join(filename_with_toolchain);
     165           0 :             copy(path, &path_with_toolchain).with_context(|| {
     166           0 :                 format!(
     167           0 :                     "Could not copy `{}` to `{}`",
     168           0 :                     path.to_string_lossy(),
     169           0 :                     path_with_toolchain.to_string_lossy()
     170           0 :                 )
     171           0 :             })?;
     172             :         }
     173             :     }
     174             : 
     175           0 :     Ok(())
     176           0 : }
     177             : 
     178             : // smoelius: I do not know what the right/best way to parse a toolchain is. `parse_toolchain` does
     179             : // so by looking for the architecture.
     180           0 : fn parse_toolchain(toolchain: &str) -> Option<(String, String)> {
     181           0 :     let split_toolchain: Vec<_> = toolchain.split('-').collect();
     182           0 :     split_toolchain
     183           0 :         .iter()
     184           0 :         .rposition(|s| ARCHITECTURES.binary_search(s).is_ok())
     185           0 :         .map(|i| {
     186           0 :             (
     187           0 :                 split_toolchain[..i].join("-"),
     188           0 :                 split_toolchain[i..].join("-"),
     189           0 :             )
     190           0 :         })
     191           0 : }
     192             : 
     193           0 : fn parse_path_plain_filename(path: &Path) -> Option<String> {
     194           0 :     let filename = path.file_name()?;
     195           0 :     let s = filename.to_string_lossy();
     196           0 :     let file_stem = s.strip_suffix(consts::DLL_SUFFIX)?;
     197           0 :     let lib_name = file_stem.strip_prefix(consts::DLL_PREFIX)?;
     198           0 :     Some(lib_name.to_owned())
     199           0 : }
     200             : 
     201           0 : fn strip_deps(path: &Path) -> PathBuf {
     202           0 :     if path.file_name() == Some(OsStr::new("deps")) {
     203           0 :         path.parent()
     204             :     } else {
     205           0 :         None
     206             :     }
     207           0 :     .unwrap_or(path)
     208           0 :     .to_path_buf()
     209           0 : }
     210             : 
     211             : // smoelius: `ARCHITECTURES` is based on: https://doc.rust-lang.org/rustc/platform-support.html
     212             : const ARCHITECTURES: &[&str] = &[
     213             :     "aarch64",
     214             :     "aarch64_be",
     215             :     "arm",
     216             :     "arm64_32",
     217             :     "arm64e",
     218             :     "arm64ec",
     219             :     "armeb",
     220             :     "armebv7r",
     221             :     "armv4t",
     222             :     "armv5te",
     223             :     "armv6",
     224             :     "armv6k",
     225             :     "armv7",
     226             :     "armv7a",
     227             :     "armv7k",
     228             :     "armv7r",
     229             :     "armv7s",
     230             :     "armv8r",
     231             :     "avr",
     232             :     "bpfeb",
     233             :     "bpfel",
     234             :     "csky",
     235             :     "hexagon",
     236             :     "i386",
     237             :     "i586",
     238             :     "i686",
     239             :     "loongarch64",
     240             :     "m68k",
     241             :     "mips",
     242             :     "mips64",
     243             :     "mips64el",
     244             :     "mipsel",
     245             :     "mipsisa32r6",
     246             :     "mipsisa32r6el",
     247             :     "mipsisa64r6",
     248             :     "mipsisa64r6el",
     249             :     "msp430",
     250             :     "nvptx64",
     251             :     "powerpc",
     252             :     "powerpc64",
     253             :     "powerpc64le",
     254             :     "riscv32gc",
     255             :     "riscv32i",
     256             :     "riscv32im",
     257             :     "riscv32ima",
     258             :     "riscv32imac",
     259             :     "riscv32imafc",
     260             :     "riscv32imc",
     261             :     "riscv64",
     262             :     "riscv64gc",
     263             :     "riscv64imac",
     264             :     "s390x",
     265             :     "sparc",
     266             :     "sparc64",
     267             :     "sparcv9",
     268             :     "thumbv4t",
     269             :     "thumbv5te",
     270             :     "thumbv6m",
     271             :     "thumbv7a",
     272             :     "thumbv7em",
     273             :     "thumbv7m",
     274             :     "thumbv7neon",
     275             :     "thumbv8m.base",
     276             :     "thumbv8m.main",
     277             :     "wasm32",
     278             :     "wasm64",
     279             :     "x86_64",
     280             :     "x86_64h",
     281             :     "xtensa",
     282             : ];
     283             : 
     284             : #[allow(clippy::unwrap_used)]
     285             : #[cfg(test)]
     286             : mod test {
     287             :     use super::{env, ARCHITECTURES};
     288             :     use assert_cmd::prelude::*;
     289             :     use dylint_internal::{packaging::isolate, CommandExt};
     290             :     use predicates::prelude::*;
     291             :     use std::fs::{create_dir, write};
     292             :     use tempfile::{tempdir, tempdir_in};
     293             : 
     294             :     #[test]
     295           1 :     fn architectures_are_current() {
     296           1 :         let output = std::process::Command::new("rustc")
     297           1 :             .args(["--print", "target-list"])
     298           1 :             .unwrap();
     299           1 :         let mut architectures = std::str::from_utf8(&output.stdout)
     300           1 :             .unwrap()
     301           1 :             .lines()
     302         262 :             .filter_map(|line| line.split_once('-').map(|(architecture, _)| architecture))
     303           1 :             .collect::<Vec<_>>();
     304           1 :         architectures.sort_unstable();
     305           1 :         architectures.dedup();
     306           1 :         assert_eq!(ARCHITECTURES, architectures);
     307           1 :     }
     308             : 
     309             :     #[test]
     310           1 :     fn architectures_are_sorted() {
     311           1 :         let mut architectures = ARCHITECTURES.to_vec();
     312           1 :         architectures.sort_unstable();
     313           1 :         architectures.dedup();
     314           1 :         assert_eq!(ARCHITECTURES, architectures);
     315           1 :     }
     316             : 
     317             :     #[cfg_attr(not(all(target_arch = "x86_64", target_os = "linux")), ignore)]
     318             :     #[cfg_attr(dylint_lib = "general", allow(non_thread_safe_call_in_test))]
     319             :     #[test]
     320           1 :     fn global_config() {
     321           1 :         let cargo_home = tempdir().unwrap();
     322           1 :         let package = tempdir_in(".").unwrap();
     323           1 : 
     324           1 :         dylint_internal::cargo::build("dylint-link")
     325           1 :             .build()
     326           1 :             .current_dir(env!("CARGO_MANIFEST_DIR"))
     327           1 :             .success()
     328           1 :             .unwrap();
     329           1 : 
     330           1 :         dylint_internal::cargo::init("package `global_config_test`")
     331           1 :             .build()
     332           1 :             .current_dir(&package)
     333           1 :             .args(["--name", "global_config_test"])
     334           1 :             .success()
     335           1 :             .unwrap();
     336           1 : 
     337           1 :         isolate(package.path()).unwrap();
     338           1 : 
     339           1 :         let package_cargo = package.path().join(".cargo");
     340           1 :         create_dir(&package_cargo).unwrap();
     341           1 :         write(
     342           1 :             package_cargo.join("config.toml"),
     343           1 :             r#"
     344           1 : [target.x86_64-unknown-linux-gnu]
     345           1 : linker = "../../target/debug/dylint-link"
     346           1 : "#,
     347           1 :         )
     348           1 :         .unwrap();
     349           1 : 
     350           1 :         std::process::Command::new("cargo")
     351           1 :             .current_dir(&package)
     352           1 :             .arg("build")
     353           1 :             .assert()
     354           1 :             .success();
     355           1 : 
     356           1 :         write(
     357           1 :             cargo_home.path().join("config.toml"),
     358           1 :             r#"
     359           1 : [target.x86_64-unknown-linux-gnu]
     360           1 : linker = "false"
     361           1 : "#,
     362           1 :         )
     363           1 :         .unwrap();
     364           1 : 
     365           1 :         std::process::Command::new("cargo")
     366           1 :             .current_dir(&package)
     367           1 :             .arg("clean")
     368           1 :             .assert()
     369           1 :             .success();
     370           1 : 
     371           1 :         std::process::Command::new("cargo")
     372           1 :             .env(env::CARGO_HOME, cargo_home.path())
     373           1 :             .env(env::CARGO_TERM_COLOR, "never")
     374           1 :             .current_dir(&package)
     375           1 :             .arg("build")
     376           1 :             .assert()
     377           1 :             .failure()
     378           1 :             .stderr(
     379           1 :                 predicate::str::is_match(
     380           1 :                     "error: linking with `[^`]*/target/debug/dylint-link` failed",
     381           1 :                 )
     382           1 :                 .unwrap(),
     383           1 :             );
     384           1 :     }
     385             : }

Generated by: LCOV version 1.14