evcxr/
cargo_metadata.rs

1// Copyright 2020 The Evcxr Authors.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE
5// or https://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8use crate::eval_context::Config;
9use anyhow::Context;
10use anyhow::Result;
11use anyhow::bail;
12use json::JsonValue;
13use json::{self};
14use once_cell::sync::Lazy;
15use regex::Regex;
16use std::collections::HashMap;
17
18/// Returns the library names for the direct dependencies of the crate rooted at
19/// the specified path.
20pub(crate) fn get_library_names(config: &Config) -> Result<Vec<String>> {
21    let output = config
22        .cargo_command("metadata")
23        .arg("--format-version")
24        .arg("1")
25        .output()
26        .with_context(|| "Error running cargo metadata")?;
27    if output.status.success() {
28        library_names_from_metadata(std::str::from_utf8(&output.stdout)?)
29    } else {
30        bail!(
31            "cargo metadata failed with output:\n{}{}",
32            std::str::from_utf8(&output.stdout)?,
33            std::str::from_utf8(&output.stderr)?,
34        )
35    }
36}
37
38pub(crate) fn validate_dep(dep: &str, dep_config: &str, config: &Config) -> Result<()> {
39    std::fs::write(
40        config.crate_dir().join("Cargo.toml"),
41        format!(
42            r#"
43    [package]
44    name = "evcxr_dummy_validate_dep"
45    version = "0.0.1"
46    edition = "2024"
47
48    [lib]
49    path = "lib.rs"
50
51    [dependencies]
52    {dep} = {dep_config}
53    "#
54        ),
55    )?;
56    let mut cmd = config.cargo_command("metadata");
57    let output = cmd.arg("--format-version=1").output()?;
58    if output.status.success() {
59        static NO_LIB_PATTERN: Lazy<Regex> = Lazy::new(|| {
60            Regex::new("ignoring invalid dependency `(.*)` which is missing a lib target").unwrap()
61        });
62        let stderr = String::from_utf8_lossy(&output.stderr);
63        if let Some(captures) = NO_LIB_PATTERN.captures(&stderr) {
64            bail!("Dependency `{}` is missing a lib target", &captures[1]);
65        }
66        Ok(())
67    } else {
68        static IGNORED_LINES_PATTERN: Lazy<Regex> =
69            Lazy::new(|| Regex::new("required by package `evcxr_dummy_validate_dep.*").unwrap());
70        static PRIMARY_ERROR_PATTERN: Lazy<Regex> =
71            Lazy::new(|| Regex::new("(.*) as a dependency of package `[^`]*`").unwrap());
72        let mut message = Vec::new();
73        let mut suggest_offline_mode = false;
74        for line in String::from_utf8_lossy(&output.stderr).lines() {
75            if let Some(captures) = PRIMARY_ERROR_PATTERN.captures(line) {
76                message.push(captures[1].to_string());
77            } else if !IGNORED_LINES_PATTERN.is_match(line) {
78                message.push(line.to_owned());
79            }
80
81            if line.contains("failed to fetch `https://github.com/rust-lang/crates.io-index`") {
82                suggest_offline_mode = true;
83            }
84        }
85
86        if suggest_offline_mode {
87            message.push("\nTip: Enable offline mode with `:offline 1`".to_owned());
88        }
89        bail!(message.join("\n"));
90    }
91}
92
93fn library_names_from_metadata(metadata: &str) -> Result<Vec<String>> {
94    let metadata = json::parse(metadata)?;
95    let mut direct_dependencies = Vec::new();
96    let mut crate_to_library_names = HashMap::new();
97    if let (JsonValue::Array(packages), Some(main_crate_id)) = (
98        &metadata["packages"],
99        metadata["workspace_members"][0].as_str(),
100    ) {
101        for package in packages {
102            if let (Some(package_name), Some(id)) =
103                (package["name"].as_str(), package["id"].as_str())
104            {
105                if id == main_crate_id
106                    && let JsonValue::Array(dependencies) = &package["dependencies"]
107                {
108                    for dependency in dependencies {
109                        if let Some(dependency_name) = dependency["name"].as_str() {
110                            direct_dependencies.push(dependency_name);
111                        }
112                    }
113                }
114                if let JsonValue::Array(targets) = &package["targets"] {
115                    for target in targets {
116                        if let JsonValue::Array(kinds) = &target["kind"]
117                            && kinds.iter().any(|kind| kind == "lib")
118                            && let Some(target_name) = target["name"].as_str()
119                        {
120                            crate_to_library_names.insert(package_name, target_name.to_owned());
121                        }
122                    }
123                }
124            }
125        }
126    }
127    let mut library_names = Vec::new();
128    for dep_name in direct_dependencies {
129        if let Some(lib_name) = crate_to_library_names.get(dep_name) {
130            library_names.push(lib_name.replace('-', "_"));
131        }
132    }
133    Ok(library_names)
134}
135
136/// Match the pattern or return an error.
137macro_rules! prism {
138    ($pattern:path, $rhs:expr, $msg:literal) => {
139        if let $pattern(x) = $rhs {
140            x
141        } else {
142            bail!("Parse error in Cargo.toml: {}", $msg)
143        }
144    };
145}
146
147/// Parse the crate at given path, producing crate name
148pub fn parse_crate_name(path: &str) -> Result<String> {
149    use toml::Value;
150
151    let config_path = std::path::Path::new(path).join("Cargo.toml");
152    let content = std::fs::read_to_string(config_path)?;
153    let package = content
154        .parse::<toml::Table>()
155        .context("Can't parse Cargo.toml")?;
156
157    // https://doc.rust-lang.org/cargo/reference/manifest.html
158    // The fields to define a package are 'package' and 'workspace'
159    if let Some(package) = package.get("package") {
160        let package = prism!(Value::Table, package, "expected 'package' to be a table");
161        let name = prism!(Some, package.get("name"), "no 'name' in package");
162        let name = prism!(Value::String, name, "expected 'name' to be a string");
163        Ok(name.clone())
164    } else if let Some(_workspace) = package.get("workspace") {
165        bail!("Workspaces are not supported");
166    } else {
167        bail!("Unexpected Cargo.toml format: not package or workspace")
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use crate::eval_context::Config;
175    use anyhow::Result;
176    use std::path::Path;
177    use std::path::PathBuf;
178
179    #[test]
180    fn test_library_names_from_metadata() {
181        assert_eq!(
182            library_names_from_metadata(include_str!("testdata/sample_metadata.json")).unwrap(),
183            vec!["crate1"]
184        );
185    }
186
187    fn create_crate(path: &Path, name: &str, deps: &str) -> Result<()> {
188        let src_dir = path.join("src");
189        std::fs::create_dir_all(&src_dir)?;
190        std::fs::write(
191            path.join("Cargo.toml"),
192            format!(
193                r#"
194            [package]
195            name = "{name}"
196            version = "0.0.1"
197            edition = "2024"
198
199            [dependencies]
200            {deps}
201        "#
202            ),
203        )?;
204        std::fs::write(src_dir.join("lib.rs"), "")?;
205        Ok(())
206    }
207
208    fn path_to_string(path: &Path) -> String {
209        path.to_string_lossy().replace('\\', "\\\\")
210    }
211
212    #[test]
213    fn valid_dependency() -> Result<()> {
214        let tempdir = tempfile::tempdir()?;
215        let crate1 = tempdir.path().join("crate1");
216        let crate2 = tempdir.path().join("crate2");
217        create_crate(&crate1, "crate1", "")?;
218        create_crate(
219            &crate2,
220            "crate2",
221            &format!(r#"crate1 = {{ path = "{}" }}"#, path_to_string(&crate1)),
222        )?;
223        let mut config = Config::new(crate2, PathBuf::from("/dummy_evcxr_bin"))?;
224        // We allow static linking so that we don't need a valid RUSTC_WRAPPER, since we just passed
225        // /dummy_evcxr_bin.
226        config.allow_static_linking = true;
227        assert_eq!(get_library_names(&config)?, vec!["crate1".to_owned()]);
228        Ok(())
229    }
230
231    #[test]
232    fn invalid_feature() -> Result<()> {
233        let tempdir = tempfile::tempdir()?;
234        let crate1 = tempdir.path().join("crate1");
235        let crate2 = tempdir.path().join("crate2");
236        create_crate(&crate1, "crate1", "")?;
237        create_crate(
238            &crate2,
239            "crate2",
240            &format!(
241                r#"crate1 = {{ path = "{}", features = ["no_such_feature"] }}"#,
242                path_to_string(&crate1)
243            ),
244        )?;
245        // Make sure that the problematic feature "no_such_feature" is mentioned
246        // somewhere in the error message.
247        let mut config = Config::new(crate2, PathBuf::from("/dummy_evcxr_bin"))?;
248        config.allow_static_linking = true;
249        assert!(
250            get_library_names(&config)
251                .unwrap_err()
252                .to_string()
253                .contains("no_such_feature")
254        );
255        Ok(())
256    }
257}