cargo_hackerman/
source.rs

1use crate::{
2    feat_graph::{FeatTarget, Pid},
3    hack::{FeatChange, Ty},
4};
5use cargo_metadata::camino::Utf8PathBuf;
6use semver::Version;
7use std::collections::{BTreeMap, BTreeSet};
8use tracing::debug;
9
10fn optimize_feats(declared: &BTreeMap<String, Vec<String>>, requested: &mut BTreeSet<String>) {
11    let mut implicit = BTreeSet::new();
12    for req in requested.iter() {
13        for dep in declared.get(req).iter().flat_map(|x| x.iter()) {
14            if let FeatTarget::Named { name } = FeatTarget::from(dep.as_str()) {
15                implicit.insert(name);
16            }
17        }
18    }
19    for imp in &implicit {
20        requested.remove(*imp);
21    }
22}
23
24#[cfg(test)]
25mod tests {
26    use super::{optimize_feats, PackageSource};
27    use std::collections::{BTreeMap, BTreeSet};
28
29    fn check(req: &[&str], decl: &[(&str, &[&str])], exp: &[&str]) {
30        let mut requested = req
31            .iter()
32            .copied()
33            .map(String::from)
34            .collect::<BTreeSet<_>>();
35
36        let mut declared = BTreeMap::new();
37        for (key, vals) in decl.iter() {
38            declared.insert(
39                key.to_string(),
40                vals.iter().copied().map(String::from).collect::<Vec<_>>(),
41            );
42        }
43        optimize_feats(&declared, &mut requested);
44        let expected = exp
45            .iter()
46            .copied()
47            .map(String::from)
48            .collect::<BTreeSet<_>>();
49        assert_eq!(requested, expected);
50    }
51
52    #[test]
53    fn optimize_feats_1() {
54        check(&["one", "default"], &[("default", &["one"])], &["default"]);
55    }
56
57    #[test]
58    fn optimize_feats_2() {
59        check(
60            &["one", "default"],
61            &[("default", &["two"])],
62            &["default", "one"],
63        );
64    }
65
66    #[test]
67    fn optimize_feats_3() {
68        check(
69            &["one", "two", "default"],
70            &[("default", &["one", "two"])],
71            &["default"],
72        );
73    }
74
75    const CRATES_IO: &str = "registry+https://github.com/rust-lang/crates.io-index";
76    const GIT_0: &str = "git+https://github.com/rust-lang/cargo.git?branch=main#0227f048";
77    const GIT_1: &str = "git+https://github.com/rust-lang/cargo.git?tag=v0.46.0#0227f048";
78    const GIT_2: &str = "git+https://github.com/rust-lang/cargo.git?rev=0227f048#0227f048";
79    const GIT_3: &str = "git+https://github.com/gyscos/zstd-rs.git#bc874a57";
80
81    #[test]
82    fn parse_sources() -> anyhow::Result<()> {
83        PackageSource::try_from(CRATES_IO)?;
84        PackageSource::try_from(GIT_0)?;
85        PackageSource::try_from(GIT_1)?;
86        PackageSource::try_from(GIT_2)?;
87        PackageSource::try_from(GIT_3)?;
88        Ok(())
89    }
90}
91
92impl<'a> TryFrom<&'a str> for PackageSource<'a> {
93    type Error = anyhow::Error;
94    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
95        if let Some(registry) = value.strip_prefix("registry+") {
96            Ok(PackageSource::Registry(registry))
97        } else if let Some(repo) = value.strip_prefix("git+") {
98            if let Some((url, _)) = repo.split_once('#') {
99                Ok(PackageSource::Git(url))
100            } else {
101                Ok(PackageSource::Git(repo))
102            }
103        } else {
104            anyhow::bail!("Not sure what package source is {value}");
105        }
106    }
107}
108
109impl<'a> ChangePackage<'a> {
110    #[allow(clippy::similar_names)]
111    pub fn make(importer: Pid<'a>, importee: FeatChange<'a>) -> anyhow::Result<Self> {
112        let FeatChange {
113            pid: importee,
114            ty,
115            rename,
116            features: mut feats,
117        } = importee;
118        let package = importee.package();
119        optimize_feats(&package.features, &mut feats);
120        // we care if package we are importing comes with the default key, not
121        // the package that imports
122        let has_default = importee.package().features.contains_key("default");
123
124        if let Some(src) = &package.source {
125            let source = PackageSource::try_from(src.repr.as_str())?;
126            Ok(ChangePackage {
127                name: package.name.clone(),
128                ty,
129                version: package.version.clone(),
130                source,
131                feats,
132                rename,
133                has_default,
134            })
135        } else {
136            let source = match relative_import_dir(importer, importee) {
137                Some(path) => PackageSource::File { path },
138                None => {
139                    let manifest = &importee.package().manifest_path;
140                    debug!(
141                        "Using absolute manifest path for {:?}: {}",
142                        importee, manifest
143                    );
144                    PackageSource::File {
145                        path: manifest
146                            .parent()
147                            .expect("Very strange manifest path")
148                            .to_path_buf(),
149                    }
150                }
151            };
152            Ok(ChangePackage {
153                name: package.name.clone(),
154                ty,
155                version: package.version.clone(),
156                source,
157                feats,
158                rename,
159                has_default,
160            })
161        }
162    }
163}
164
165#[allow(clippy::similar_names)]
166fn relative_import_dir(importer: Pid, importee: Pid) -> Option<Utf8PathBuf> {
167    let importer_dir = &importer.package().manifest_path.parent()?;
168    let importee_dir = &importee.package().manifest_path.parent()?;
169    pathdiff::diff_utf8_paths(importee_dir, importer_dir)
170}
171
172#[derive(Debug)]
173pub struct ChangePackage<'a> {
174    pub name: String,
175    pub ty: Ty,
176    pub version: Version,
177    pub source: PackageSource<'a>,
178    pub feats: BTreeSet<String>,
179    pub rename: bool,
180    pub has_default: bool,
181}
182
183impl PackageSource<'_> {
184    pub fn insert_into(&self, ver: &Version, table: &mut toml_edit::InlineTable) {
185        match self {
186            PackageSource::Registry(_) => {
187                table.insert("version", toml_edit::Value::from(ver.to_string()));
188            }
189            PackageSource::Git(url) => {
190                table.insert("git", toml_edit::Value::from(*url));
191            }
192            PackageSource::File { path } => {
193                table.insert("path", toml_edit::Value::from(path.to_string()));
194            }
195        }
196    }
197}
198
199#[derive(Debug, Hash)]
200#[allow(clippy::module_name_repetitions)]
201pub enum PackageSource<'a> {
202    Registry(&'a str),
203    Git(&'a str),
204    File { path: Utf8PathBuf },
205}
206
207impl PackageSource<'_> {
208    pub const CRATES_IO: Self =
209        PackageSource::Registry("https://github.com/rust-lang/crates.io-index");
210}
211
212impl std::fmt::Display for PackageSource<'_> {
213    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214        match self {
215            PackageSource::Registry(_reg) => f.write_str("registry"),
216            PackageSource::Git(url) => write!(f, "{url}"),
217            PackageSource::File { path } => path.fmt(f),
218        }
219    }
220}