mol_cargo/
lib.rs

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      // TODO: support [dependencies.xyz] patterns
176      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}