1use crate::{
7 HakariBuilder, WorkspaceHackLineStyle,
8 cli_ops::{WorkspaceOp, WorkspaceOps},
9 hakari::DepFormatVersion,
10};
11use guppy::{
12 VersionReq,
13 graph::{DependencyDirection, PackageLink, PackageMetadata, PackageSet},
14 platform::{EnabledTernary, PlatformSpec},
15};
16
17impl<'g> HakariBuilder<'g> {
18 pub fn manage_dep_ops(&self, workspace_set: &PackageSet<'g>) -> Option<WorkspaceOps<'g, '_>> {
53 let graph = self.graph();
54 let hakari_package = self.hakari_package()?;
55
56 let (add_to, remove_from) =
57 workspace_set.filter_partition(DependencyDirection::Reverse, |package| {
58 if !package.in_workspace() {
61 return None;
62 }
63 let link_opt = package
64 .link_to(hakari_package.id())
65 .expect("valid package ID");
66 let should_be_included = self.is_managed_member(&package);
67 match (link_opt, should_be_included) {
68 (None, true) => Some(true),
69 (Some(_), false) => Some(false),
70 (Some(link), true) => {
71 if !link.normal().is_present() {
75 return Some(true);
76 }
77 if link.normal().status().required_on(&PlatformSpec::Always)
83 != EnabledTernary::Enabled
84 {
85 return None;
86 }
87 match self.dep_format_version {
88 DepFormatVersion::V1 => None,
89 DepFormatVersion::V2 | DepFormatVersion::V3 | DepFormatVersion::V4 => {
90 needs_update_v2(
91 hakari_package,
92 link,
93 self.workspace_hack_line_style,
94 )
95 .then_some(true)
96 }
97 }
98 }
99 (None, false) => None,
100 }
101 });
102
103 let mut ops = Vec::with_capacity(2);
104 if !add_to.is_empty() {
105 ops.push(WorkspaceOp::AddDependency {
106 name: hakari_package.name(),
107 crate_path: hakari_package
108 .source()
109 .workspace_path()
110 .expect("hakari package is in workspace"),
111 version: hakari_package.version(),
112 dep_format: self.dep_format_version,
113 line_style: self.workspace_hack_line_style,
114 add_to,
115 });
116 }
117 if !remove_from.is_empty() {
118 ops.push(WorkspaceOp::RemoveDependency {
119 name: hakari_package.name(),
120 remove_from,
121 });
122 }
123 Some(WorkspaceOps::new(graph, ops))
124 }
125
126 pub fn add_dep_ops(
133 &self,
134 workspace_set: &PackageSet<'g>,
135 force: bool,
136 ) -> Option<WorkspaceOps<'g, '_>> {
137 let graph = self.graph();
138 let hakari_package = self.hakari_package()?;
139
140 let add_to = if force {
141 workspace_set.clone()
142 } else {
143 workspace_set.filter(DependencyDirection::Reverse, |package| {
144 let link_opt = package
145 .link_to(hakari_package.id())
146 .expect("valid package ID");
147 match link_opt {
148 Some(link) => {
149 needs_update_v2(hakari_package, link, self.workspace_hack_line_style)
150 }
151 None => true,
152 }
153 })
154 };
155
156 let op = if !add_to.is_empty() {
157 Some(WorkspaceOp::AddDependency {
158 name: hakari_package.name(),
159 version: hakari_package.version(),
160 crate_path: hakari_package
161 .source()
162 .workspace_path()
163 .expect("hakari package is in workspace"),
164 dep_format: self.dep_format_version,
165 line_style: self.workspace_hack_line_style,
166 add_to,
167 })
168 } else {
169 None
170 };
171 Some(WorkspaceOps::new(graph, op))
172 }
173
174 pub fn remove_dep_ops(
181 &self,
182 workspace_set: &PackageSet<'g>,
183 force: bool,
184 ) -> Option<WorkspaceOps<'g, '_>> {
185 let graph = self.graph();
186 let hakari_package = self.hakari_package()?;
187
188 let remove_from = if force {
189 workspace_set.clone()
190 } else {
191 workspace_set.filter(DependencyDirection::Reverse, |package| {
192 graph
193 .directly_depends_on(package.id(), hakari_package.id())
194 .expect("valid package ID")
195 })
196 };
197
198 let op = if !remove_from.is_empty() {
199 Some(WorkspaceOp::RemoveDependency {
200 name: hakari_package.name(),
201 remove_from,
202 })
203 } else {
204 None
205 };
206 Some(WorkspaceOps::new(graph, op))
207 }
208}
209
210#[allow(clippy::if_same_then_else, clippy::needless_bool)]
211fn needs_update_v2(
212 hakari_package: &PackageMetadata<'_>,
213 link: PackageLink<'_>,
214 line_style: WorkspaceHackLineStyle,
215) -> bool {
216 if !link.version_req().matches(hakari_package.version()) {
217 true
219 } else if link.version_req() == &VersionReq::STAR {
220 match line_style {
222 WorkspaceHackLineStyle::Full | WorkspaceHackLineStyle::VersionOnly => true,
223 WorkspaceHackLineStyle::WorkspaceDotted => false,
224 }
225 } else {
226 false
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use crate::cli_ops::WorkspaceOp;
234 use fixtures::{
235 json::{
236 JsonFixture, METADATA_HAKARI_REVERSE_DEP_MEMBER_BUILD,
237 METADATA_HAKARI_REVERSE_DEP_MEMBER_CFG, METADATA_HAKARI_REVERSE_DEP_MEMBER_DEV,
238 METADATA_HAKARI_REVERSE_DEP_MEMBER_NORMAL,
239 METADATA_HAKARI_REVERSE_DEP_MEMBER_PUBLISHED,
240 METADATA_HAKARI_REVERSE_DEP_MEMBER_UNLINKED,
241 METADATA_HAKARI_REVERSE_DEP_NORMAL_ON_HACK,
242 },
243 package_id,
244 };
245 use guppy::PackageId;
246 use std::collections::BTreeSet;
247
248 #[test]
249 fn manage_dep_ops_skips_non_workspace_packages() {
250 let fixture = JsonFixture::metadata_hakari_reverse_dep();
251 let graph = fixture.graph();
252 let hakari_id = fixture
253 .details()
254 .hakari_package()
255 .expect("hakari-reverse-dep fixture names a hakari package");
256 let mut builder =
257 HakariBuilder::new(graph, Some(hakari_id)).expect("hakari builder is created");
258 builder.set_dep_format_version(DepFormatVersion::V4);
262
263 let member_unlinked_id = package_id(METADATA_HAKARI_REVERSE_DEP_MEMBER_UNLINKED);
272 let member_published_id = package_id(METADATA_HAKARI_REVERSE_DEP_MEMBER_PUBLISHED);
273 let member_dev_id = package_id(METADATA_HAKARI_REVERSE_DEP_MEMBER_DEV);
274 let normal_on_hack_id = package_id(METADATA_HAKARI_REVERSE_DEP_NORMAL_ON_HACK);
275 let package_set = graph
276 .resolve_ids([
277 &member_unlinked_id,
278 &member_published_id,
279 &member_dev_id,
280 &normal_on_hack_id,
281 ])
282 .expect("all package IDs are known to the graph");
283
284 let ops = builder
285 .manage_dep_ops(&package_set)
286 .expect("hakari package was specified, so ops are returned");
287 let mut add_to = None;
288 for op in ops.ops() {
289 match op {
290 WorkspaceOp::AddDependency { add_to: set, .. } => {
291 assert!(add_to.is_none(), "at most one add op is generated");
292 add_to = Some(set);
293 }
294 WorkspaceOp::RemoveDependency { remove_from, .. } => {
295 let remove_ids: Vec<_> = remove_from
296 .package_ids(DependencyDirection::Forward)
297 .collect();
298 panic!(
299 "hrd-normal-on-hack is outside the workspace and the other two \
300 packages are managed members, so nothing in the set \
301 should have the hack removed, but got a remove op for \
302 {remove_ids:?}"
303 );
304 }
305 WorkspaceOp::NewCrate { .. } => {
306 panic!("manage-deps never creates crates");
307 }
308 }
309 }
310
311 let add_to = add_to.expect("an add op is generated");
312 let add_ids: BTreeSet<_> = add_to.package_ids(DependencyDirection::Forward).collect();
313 let expected_ids: BTreeSet<_> = [&member_unlinked_id, &member_published_id, &member_dev_id]
314 .into_iter()
315 .collect();
316 assert_eq!(
317 add_ids, expected_ids,
318 "hrd-member-unlinked has no dependency on the hack, hrd-member-published's \
319 `req = \"*\"` needs updating under dep format V4, and \
320 hrd-member-dev's only dependency on the hack is dev-only, so all \
321 three are added to; hrd-normal-on-hack is outside the workspace, so it \
322 is ignored"
323 );
324 }
325
326 #[test]
329 fn manage_dep_ops_never_updates_platform_specific_lines() {
330 let fixture = JsonFixture::metadata_hakari_reverse_dep();
331 let graph = fixture.graph();
332 let hakari_id = fixture
333 .details()
334 .hakari_package()
335 .expect("hakari-reverse-dep fixture names a hakari package");
336 let mut builder =
337 HakariBuilder::new(graph, Some(hakari_id)).expect("hakari builder is created");
338 builder.set_dep_format_version(DepFormatVersion::V4);
339 assert_eq!(
340 builder.workspace_hack_line_style(),
341 WorkspaceHackLineStyle::Full,
342 "the full line style requires a version, so a `*` requirement needs updating"
343 );
344
345 let member_published_id = package_id(METADATA_HAKARI_REVERSE_DEP_MEMBER_PUBLISHED);
348 let member_cfg_id = package_id(METADATA_HAKARI_REVERSE_DEP_MEMBER_CFG);
349 let package_set = graph
350 .resolve_ids([&member_published_id, &member_cfg_id])
351 .expect("all package IDs are known to the graph");
352
353 let ops = builder
354 .manage_dep_ops(&package_set)
355 .expect("hakari package was specified, so ops are returned");
356 let mut add_ids: BTreeSet<PackageId> = BTreeSet::new();
357 for op in ops.ops() {
358 match op {
359 WorkspaceOp::AddDependency { add_to, .. } => {
360 add_ids.extend(add_to.package_ids(DependencyDirection::Forward).cloned());
361 }
362 WorkspaceOp::RemoveDependency { .. } => {
363 panic!("both members are managed, so nothing should be removed");
364 }
365 WorkspaceOp::NewCrate { .. } => {
366 panic!("manage-deps never creates crates");
367 }
368 }
369 }
370 assert_eq!(
371 add_ids,
372 [member_published_id].into_iter().collect(),
373 "hrd-member-published's unconditional `*` line is updated; hrd-member-cfg's \
374 cfg(windows)-only line is left alone"
375 );
376 }
377
378 #[test]
379 fn manage_dep_ops_requires_normal_dep() {
380 let fixture = JsonFixture::metadata_hakari_reverse_dep();
381 let graph = fixture.graph();
382 let hakari_id = fixture
383 .details()
384 .hakari_package()
385 .expect("hakari-reverse-dep fixture names a hakari package");
386 let builder =
387 HakariBuilder::new(graph, Some(hakari_id)).expect("hakari builder is created");
388 assert_eq!(
391 builder.dep_format_version(),
392 DepFormatVersion::V1,
393 "dep format version defaults to V1"
394 );
395
396 let cases = [
402 (METADATA_HAKARI_REVERSE_DEP_MEMBER_DEV, "dev-only", true),
403 (METADATA_HAKARI_REVERSE_DEP_MEMBER_BUILD, "build-only", true),
404 (
405 METADATA_HAKARI_REVERSE_DEP_MEMBER_CFG,
406 "cfg(windows)-only",
407 false,
408 ),
409 (
410 METADATA_HAKARI_REVERSE_DEP_MEMBER_NORMAL,
411 "unconditional normal",
412 false,
413 ),
414 ];
415 let ids: Vec<PackageId> = cases.iter().map(|(id, _, _)| package_id(*id)).collect();
416 let package_set = graph
417 .resolve_ids(&ids)
418 .expect("all package IDs are known to the graph");
419
420 let ops = builder
421 .manage_dep_ops(&package_set)
422 .expect("hakari package was specified, so ops are returned");
423 let mut add_ids: BTreeSet<PackageId> = BTreeSet::new();
424 for op in ops.ops() {
425 match op {
426 WorkspaceOp::AddDependency { add_to, .. } => {
427 add_ids.extend(add_to.package_ids(DependencyDirection::Forward).cloned());
428 }
429 WorkspaceOp::RemoveDependency { remove_from, .. } => {
430 let remove_ids: Vec<_> = remove_from
431 .package_ids(DependencyDirection::Forward)
432 .collect();
433 panic!(
434 "all of these members are managed, so nothing should have \
435 the hack removed, but got a remove op for {remove_ids:?}"
436 );
437 }
438 WorkspaceOp::NewCrate { .. } => {
439 panic!("manage-deps never creates crates");
440 }
441 }
442 }
443
444 for (id, description, expect_add) in cases {
445 let id = package_id(id);
446 assert_eq!(
447 add_ids.contains(&id),
448 expect_add,
449 "{id} has a {description} link on the hakari package, so an add op is \
450 {}generated",
451 if expect_add { "" } else { "not " }
452 );
453 }
454 }
455}