1use std::ops::Deref;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use async_recursion::async_recursion;
6use async_trait::async_trait;
7use dashmap::DashSet;
8use globset::{Glob, GlobSet, GlobSetBuilder};
9use hyper::{Client, Method, Request};
10use hyper_tls::HttpsConnector;
11use serde::{Deserialize, Serialize};
12use tokio::{fs, process::Command};
13use toml_edit::{value, Document};
14
15use mol_core::prelude::*;
16
17fn remove_start_dot(dir: PathBuf) -> PathBuf {
18 if dir.starts_with("./") {
19 dir.iter().skip(1).collect()
20 } else {
21 dir
22 }
23}
24
25#[derive(Debug, Serialize, Deserialize)]
26struct CratesError {
27 detail: String,
28}
29
30#[derive(Debug, Serialize, Deserialize)]
31struct CratesVersion {
32 version: CratesVersionMetadata,
33}
34
35#[derive(Debug, Serialize, Deserialize)]
36struct CratesVersionMetadata {
37 #[serde(alias = "crate")]
38 name: String,
39 num: String,
40 yanked: bool,
41}
42
43#[derive(Debug, Serialize, Deserialize)]
44#[serde(untagged)]
45enum CratesResult<T, E = CratesError> {
46 Ok(T),
47 Err { errors: Vec<E> },
48}
49
50#[derive(Default)]
51pub struct Cargo;
52
53impl Cargo {
54 #[async_recursion]
55 async fn check_dir<V: Versioned + Send + Sync + 'static>(
56 exists: Arc<DashSet<PathBuf>>,
57 globs: GlobSet,
58 entry: fs::DirEntry,
59 ) -> anyhow::Result<Vec<Package<V>>> {
60 let mut result = Vec::new();
61 let entry_path = remove_start_dot(entry.path());
62
63 if exists.contains(&entry_path) {
64 return Ok(result);
65 } else {
66 exists.insert(entry_path.clone());
67 }
68
69 if entry_path.starts_with("target") || entry_path.starts_with(".git") {
70 return Ok(result);
71 }
72
73 if let Ok(file_type) = entry.file_type().await {
74 if file_type.is_dir() {
75 return Cargo::check_read_dir(exists, globs, fs::read_dir(entry.path()).await?).await;
76 }
77
78 if file_type.is_symlink() {
79 let link_value = fs::read_link(entry.path()).await?;
80 return Cargo::check_read_dir(exists, globs, fs::read_dir(&link_value).await?).await;
81 }
82
83 if globs.is_match(entry_path) && file_type.is_file() && entry.file_name() == "Cargo.toml" {
84 result.extend(Cargo.read_package(entry.path()).await?);
85 }
86 }
87
88 Ok(result)
89 }
90
91 #[async_recursion]
92 async fn check_read_dir<V: Versioned + Send + Sync + 'static>(
93 exists: Arc<DashSet<PathBuf>>,
94 globs: GlobSet,
95 mut current_dir: fs::ReadDir,
96 ) -> anyhow::Result<Vec<Package<V>>> {
97 let mut handles = Vec::new();
98
99 while let Some(entry) = current_dir.next_entry().await? {
100 let globs = globs.clone();
101 handles.push(tokio::spawn(Cargo::check_dir(exists.clone(), globs, entry)));
102 }
103
104 let mut result = Vec::new();
105
106 for task in futures::future::join_all(handles)
107 .await
108 .into_iter()
109 .flatten()
110 {
111 result.extend(task?);
112 }
113
114 Ok(result)
115 }
116
117 async fn run_command<T: AsRef<Path> + Send + Sync>(
118 &self,
119 command: &str,
120 crate_path: T,
121 args: Vec<&str>,
122 ) -> anyhow::Result<()> {
123 if let Ok(canon_path) = dunce::canonicalize(crate_path) {
124 Command::new("cargo")
125 .current_dir(canon_path)
126 .arg(command)
127 .args(args)
128 .spawn()
129 .expect("Cargo command failed to start")
130 .wait()
131 .await?;
132 }
133
134 Ok(())
135 }
136
137 async fn load_document<T: AsRef<Path>>(
138 &self,
139 crate_path: T,
140 ) -> anyhow::Result<(PathBuf, Document)> {
141 let document = fs::read_to_string(&crate_path)
142 .await?
143 .parse::<Document>()
144 .expect("Invalid Cargo.toml");
145
146 Ok((crate_path.as_ref().to_path_buf(), document))
147 }
148}
149
150#[async_trait]
151impl PackageManager for Cargo {
152 fn default_path() -> &'static str {
153 "Cargo.toml"
154 }
155
156 async fn read_package<T: AsRef<Path> + Send + Sync, V: Versioned + Send + Sync + 'static>(
157 &self,
158 crate_path: T,
159 ) -> anyhow::Result<Vec<Package<V>>> {
160 let mut result = Vec::new();
161 let (crate_path, document) = self.load_document(crate_path).await?;
162
163 let (package_name, version) = if document.contains_key("package") {
164 (
165 document["package"]["name"].as_str(),
166 document["package"]["version"].as_str(),
167 )
168 } else {
169 (None, None)
170 };
171
172 let mut dependencies = Vec::new();
173
174 if document.contains_key("dependencies") {
175 if let Some(deps) = document["dependencies"].as_table() {
177 for (key, value) in deps.iter() {
178 if value.is_str() {
179 dependencies.push((
180 key.to_owned(),
181 value.as_str().unwrap_or_default().to_owned(),
182 ));
183 } else if value.is_table() || value.is_inline_table() {
184 dependencies.push((
185 key.to_owned(),
186 value["version"].as_str().unwrap_or_default().to_owned(),
187 ));
188 }
189 }
190 }
191 }
192
193 if let (Some(package_name), Some(version)) = (package_name, version) {
194 result.push(Package {
195 path: crate_path.clone(),
196 name: package_name.to_owned(),
197 version: version.into(),
198 dependencies,
199 });
200 }
201
202 let workspace = if document.contains_key("workspace") {
203 document["workspace"]["members"].as_array().map(|val| {
204 val
205 .iter()
206 .filter_map(|v| v.as_str())
207 .filter_map(|glob| Glob::new(glob).ok())
208 .collect::<Vec<Glob>>()
209 })
210 } else {
211 None
212 };
213
214 if let Some(ref workspace) = workspace {
215 let mut builder = GlobSetBuilder::new();
216
217 for glob in workspace {
218 builder.add(glob.clone());
219 }
220
221 let exists = Arc::new(DashSet::new());
222
223 result.extend(
224 Cargo::check_read_dir(
225 exists,
226 builder.build().expect("Globs did not set together"),
227 fs::read_dir(crate_path.parent().unwrap_or(&crate_path)).await?,
228 )
229 .await?,
230 );
231 }
232
233 Ok(result)
234 }
235
236 async fn check_version<V: Versioned + Send + Sync + 'static>(
237 &self,
238 package: &Package<V>,
239 ) -> anyhow::Result<bool> {
240 let https = HttpsConnector::new();
241 let client = Client::builder().build::<_, hyper::Body>(https);
242
243 let request = Request::builder()
244 .method(Method::GET)
245 .uri(format!(
246 "https://crates.io/api/v1/crates/{}/{}",
247 package.name, package.version.value
248 ))
249 .header(
250 hyper::header::USER_AGENT,
251 format!(
252 "mol-cargo/{} (https://github.com/DmitryDodzin/mol)",
253 env!("CARGO_PKG_VERSION")
254 ),
255 )
256 .body(hyper::Body::empty())?;
257
258 let response = client.request(request).await?;
259
260 let bytes = hyper::body::to_bytes(response.into_body()).await?;
261
262 let crates_result = serde_json::from_slice::<CratesResult<CratesVersion>>(&bytes)?;
263
264 match crates_result {
265 CratesResult::Ok(val) => Ok(val.version.name == package.name),
266 CratesResult::Err { errors } => {
267 for error in errors {
268 println!("crates-error:\n{}", error.detail);
269 }
270 Ok(false)
271 }
272 }
273 }
274
275 async fn run_build<T: AsRef<Path> + Send + Sync>(
276 &self,
277 crate_path: T,
278 build_args: Vec<String>,
279 ) -> anyhow::Result<()> {
280 self
281 .run_command(
282 "build",
283 crate_path,
284 build_args.iter().map(Deref::deref).collect(),
285 )
286 .await
287 }
288
289 async fn run_publish<T: AsRef<Path> + Send + Sync>(
290 &self,
291 crate_path: T,
292 publish_args: Vec<String>,
293 dry_run: bool,
294 ) -> anyhow::Result<()> {
295 let args = if dry_run {
296 vec!["--dry-run"]
297 .into_iter()
298 .chain(publish_args.iter().map(Deref::deref))
299 .collect()
300 } else {
301 publish_args.iter().map(Deref::deref).collect()
302 };
303
304 self.run_command("publish", crate_path, args).await
305 }
306
307 async fn apply_version<T: AsRef<Path> + Send + Sync>(
308 &self,
309 crate_path: T,
310 version: &str,
311 ) -> anyhow::Result<()> {
312 let (crate_path, mut document) = self.load_document(crate_path).await?;
313
314 if document.contains_key("package") {
315 document["package"]["version"] = value(version);
316 }
317
318 fs::write(&crate_path, document.to_string()).await?;
319
320 Ok(())
321 }
322
323 async fn apply_dependency_version<T: AsRef<Path> + Send + Sync>(
324 &self,
325 crate_path: T,
326 name: &str,
327 version: &str,
328 ) -> anyhow::Result<()> {
329 let (crate_path, mut document) = self.load_document(crate_path).await?;
330
331 if document.contains_key("dependencies") {
332 let dep = &document["dependencies"][name];
333
334 if dep.is_inline_table() {
335 document["dependencies"][name]["version"] = value(version);
336 } else if dep.is_str() {
337 document["dependencies"][name] = value(version);
338 }
339 }
340
341 fs::write(&crate_path, document.to_string()).await?;
342
343 Ok(())
344 }
345}