1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
use cargo_metadata::{Edition, Message};
use std::process::{Command, Stdio};

/// Returns the rustc version `rustc-tools` is currently using.
pub fn get_supported_rustc_version() -> &'static str {
    const TOOLCHAIN_FILE: &str = include_str!("../rust-toolchain");
    TOOLCHAIN_FILE
        .lines()
        .find(|line| line.starts_with("channel ") || line.starts_with("channel="))
        .and_then(|line| {
            line.rsplit('=')
                .next()
                .unwrap()
                .trim()
                .strip_prefix('"')
                .unwrap()
                .strip_suffix('"')
        })
        .unwrap()
}

/// Allows your rustc plugin to run directly with cargo integration. Quite useful in case you are
/// writing a cargo tool.
///
/// `cargo_args` is the list of extra arguments you want to pass to `cargo` such as `--all-targets`
/// or `-p [PACKAGE]`, etc. DO NOT PASS cargo commands like `check`, `build` or equivalent!
pub fn cargo_integration<T, F: Fn(&[String]) -> T>(
    cargo_args: &[String],
    f: F,
) -> Result<T, String> {
    let supported_rustc_version = get_supported_rustc_version();

    let version = format!("+{supported_rustc_version}");

    let mut args = vec![
        &version,
        "check",
        "--message-format=json-render-diagnostics",
    ];
    for arg in cargo_args {
        args.push(arg.as_str());
    }

    let mut command = Command::new("cargo")
        .args(&args)
        // We silence warnings for the initial compilation.
        .env("RUSTFLAGS", "-Awarnings")
        .stdout(Stdio::piped())
        .spawn()
        .unwrap();

    let reader = std::io::BufReader::new(command.stdout.take().unwrap());

    let mut artifacts = Vec::new();

    for message in cargo_metadata::Message::parse_stream(reader) {
        match message.unwrap() {
            Message::CompilerArtifact(artifact) => {
                if artifact.target.is_bin() || artifact.target.is_lib() {
                    artifacts.push(artifact);
                }
            }
            _ => {
                // not handled.
            }
        }
    }

    let artifact = match artifacts.pop() {
        Some(artifact) => artifact,
        None => return Err("Nothing to check?".to_string()),
    };

    let mut rustc_args = vec![
        String::new(), // rustc skip the first argument (which is supposed to be itself).
        artifact.target.src_path.into_string(),
    ];

    for crate_type in &artifact.target.crate_types {
        rustc_args.push("--crate-type".to_string());
        rustc_args.push(crate_type.to_string());
    }
    if artifact.target.edition != Edition::E2015 {
        rustc_args.push(format!("--edition={}", artifact.target.edition.as_str()));
    }
    for feature in artifact.features {
        rustc_args.push("--cfg".to_string());
        rustc_args.push(format!("feature=\"{feature}\""));
    }
    rustc_args.push("-L".to_string());
    match artifact.filenames.first().and_then(|path| path.parent()) {
        Some(path) => {
            rustc_args.push(format!("dependency={}", path.as_str()));
        }
        None => return Err("Cannot figure out `-L` option".to_string()),
    }
    for artifact in artifacts {
        rustc_args.push("--extern".to_string());
        match artifact.filenames.first() {
            Some(path) => rustc_args.push(format!(
                "{}={}",
                artifact.target.name.replace('-', "_"),
                path.as_str()
            )),
            None => {
                return Err(format!(
                    "Cannot get path for `{}` dependency",
                    artifact.target.name
                ))
            }
        }
    }

    Ok(f(&rustc_args))
}