jocker_lib/command/
cargo.rs

1use std::{
2    collections::{HashMap, HashSet},
3    ffi::OsStr,
4    fmt::Display,
5    hash::Hash,
6    path::Path,
7    process::Stdio,
8};
9
10use dotenvy::dotenv_iter;
11use serde::{Deserialize, Serialize};
12use tokio::process::{Child, Command};
13use url::Url;
14
15use crate::error::{Error, InnerError, Result};
16
17pub struct Cargo;
18
19impl Cargo {
20    /// Start a `cargo` subprocess that builds given binaries. Returns a handle to it.
21    pub async fn build<S>(target_dir: &Path, binaries: &[S], cargo_args: &[S]) -> Result<Child>
22    where
23        S: AsRef<OsStr> + Display + Eq + Hash,
24    {
25        let mut env: HashMap<String, String> = HashMap::new();
26        if let Ok(dotenv) = dotenv_iter() {
27            for (key, val) in dotenv.flatten() {
28                env.insert(key, val);
29            }
30        }
31        let env = env;
32
33        let mut build = Command::new("cargo");
34        build.stdout(Stdio::piped()).stderr(Stdio::piped());
35        build.arg("build");
36        for arg in HashSet::<&S>::from_iter(cargo_args) {
37            build.arg(arg);
38        }
39        for binary in HashSet::<&S>::from_iter(binaries) {
40            build.arg(format!("--bin={binary}"));
41        }
42        for (key, val) in env.iter() {
43            build.env(key, val);
44        }
45        build.current_dir(target_dir);
46        let build = build
47            .spawn()
48            .map_err(Error::with_context(InnerError::Start(
49                "Unable to start `cargo build` command".to_string(),
50            )))?;
51        Ok(build)
52    }
53
54    pub async fn metadata(target_dir: &Path) -> Result<Vec<SerializedPackage>> {
55        let metadata = Command::new("cargo")
56            .arg("metadata")
57            .arg("--format-version=1")
58            .current_dir(target_dir)
59            .output()
60            .await
61            .map_err(Error::with_context(InnerError::Cargo))?;
62        let info: ExportInfoMinimal = serde_json::from_slice(&metadata.stdout).unwrap();
63        let ret = info
64            .packages
65            .into_iter()
66            .filter(|package| {
67                package
68                    .targets
69                    .iter()
70                    .filter(|target| {
71                        target
72                            .kind
73                            .iter()
74                            .filter(|kind| matches!(kind, TargetKind::Bin))
75                            .count()
76                            >= 1
77                    })
78                    .count()
79                    >= 1
80                    && package.id.scheme().eq("path+file")
81            })
82            .collect();
83        Ok(ret)
84    }
85}
86
87#[derive(Debug, Deserialize)]
88pub struct ExportInfoMinimal {
89    pub packages: Vec<SerializedPackage>,
90}
91
92#[derive(Debug, Deserialize, Serialize)]
93pub struct SerializedPackage {
94    pub name: String,
95    pub id: Url,
96    pub targets: Vec<TargetInner>,
97}
98
99#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
100pub struct TargetInner {
101    pub kind: Vec<TargetKind>,
102    pub name: String,
103    pub bin_name: Option<String>,
104}
105
106#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
107#[serde(rename_all = "kebab-case")]
108pub enum TargetKind {
109    Lib,
110    Bin,
111    Test,
112    Bench,
113    ExampleLib,
114    ExampleBin,
115    CustomBuild,
116    #[serde(untagged)]
117    Other(String),
118}
119
120pub struct BinaryPackage {
121    pub name: String,
122    pub id: Url,
123}
124
125impl BinaryPackage {
126    pub fn name(&self) -> &str {
127        &self.name
128    }
129}
130
131impl From<SerializedPackage> for BinaryPackage {
132    fn from(value: SerializedPackage) -> Self {
133        Self {
134            name: value.name,
135            id: value.id,
136        }
137    }
138}