cargo_hackerman/
source.rs1use 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 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}