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 : }
|