1use 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
18pub(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
136macro_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
147pub 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 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 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 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}