@@ -206,7 +206,7 @@ var simulateOverlayInstall = func(info *BaselineInfo, baseline []BaselinePackage
206206 if err != nil {
207207 return nil , err
208208 }
209- return classifyConflicts (info .PackageManager , baselineVersionIndex (baseline ), conflicts ), nil
209+ return classifyConflicts (info .PackageManager , baselineVersionIndex (baseline ), plan . ToInstall , conflicts ), nil
210210}
211211
212212// Preflight runs the two-slice dependency/conflict preflight for an overlay
@@ -429,15 +429,7 @@ func classifyUnsatisfiedDeps(family PackageManager, sliceA map[string]BaselinePa
429429 // Post-install version index: the baseline overlaid with what to-install adds.
430430 // A dependency is checked against the state that will exist after install, so a
431431 // pin satisfied by a co-installed to-install package is correctly not flagged.
432- postInstall := make (map [string ]string , len (sliceA )+ len (resolved ))
433- for name , bp := range sliceA {
434- postInstall [name ] = bp .Version
435- }
436- for _ , rp := range resolved {
437- if name := strings .TrimSpace (rp .Name ); name != "" {
438- postInstall [name ] = rp .Version
439- }
440- }
432+ postInstall := postInstallVersionIndex (sliceA , resolved )
441433
442434 var actions []PlannedAction
443435 for _ , dep := range deps {
@@ -499,11 +491,20 @@ func classifyObsoletions(family PackageManager, sliceA map[string]BaselinePackag
499491// (rpm) on a present baseline package into an ActionConflict, so conflictPolicy
500492// gates a conflict that the package manager would otherwise only reveal by
501493// aborting at unpack time. A conflict whose target is absent from the baseline is
502- // a no-op (nothing to clash with) and is skipped; a versioned conflict only fires
503- // when the baseline version falls within the declared range, and an uncomparable
504- // version is treated conservatively as a potential conflict (better to gate than
505- // to miss it).
506- func classifyConflicts (family PackageManager , sliceA map [string ]BaselinePackage , conflicts []ArtifactConflict ) []PlannedAction {
494+ // a no-op (nothing to clash with) and is skipped.
495+ //
496+ // A versioned conflict is evaluated against the POST-INSTALL version of the
497+ // target, not the baseline version: a Breaks:/Conflicts: bound to a version range
498+ // (e.g. vim-runtime's "Breaks: vim-tiny (<< 9.1.0016-1ubuntu7.17)") is a lockstep
499+ // upgrade marker, and when the overlay upgrades that target to a satisfying
500+ // version in the SAME batch the range no longer covers it, so there is no real
501+ // conflict — dpkg's --auto-deconfigure resolves the transient break at unpack
502+ // time. Checking the baseline version alone would spuriously block that upgrade.
503+ // An uncomparable version is treated conservatively as a potential conflict
504+ // (better to gate than to miss it).
505+ func classifyConflicts (family PackageManager , sliceA map [string ]BaselinePackage , resolved []ResolvedPackage , conflicts []ArtifactConflict ) []PlannedAction {
506+ postInstall := postInstallVersionIndex (sliceA , resolved )
507+
507508 var actions []PlannedAction
508509 for _ , c := range conflicts {
509510 target := strings .TrimSpace (c .Conflicts .Name )
@@ -514,10 +515,12 @@ func classifyConflicts(family PackageManager, sliceA map[string]BaselinePackage,
514515 if ! present {
515516 continue // nothing installed under this name to conflict with
516517 }
517- // A versioned conflict only clashes when the baseline copy's version
518- // satisfies the constraint; a version outside the range is not a conflict.
518+ // A versioned conflict only clashes when the version that will be present
519+ // after install falls within the declared range; a version outside it — most
520+ // commonly because the overlay upgrades the target in the same batch — is not
521+ // a conflict.
519522 if vc := c .Conflicts .Constraint ; vc != nil {
520- if cmp , err := comparePkgVersions (family , base . Version , vc .Ver ); err == nil && ! constraintSatisfied (vc .Op , cmp ) {
523+ if cmp , err := comparePkgVersions (family , postInstall [ target ] , vc .Ver ); err == nil && ! constraintSatisfied (vc .Op , cmp ) {
521524 continue
522525 }
523526 }
@@ -533,6 +536,23 @@ func classifyConflicts(family PackageManager, sliceA map[string]BaselinePackage,
533536 return actions
534537}
535538
539+ // postInstallVersionIndex builds a name→version map of the state that will exist
540+ // after the overlay install: the baseline overlaid with the versions the overlay
541+ // will install (which supersede the baseline copy for any upgraded package). It
542+ // backs the post-install checks in classifyConflicts and classifyUnsatisfiedDeps.
543+ func postInstallVersionIndex (sliceA map [string ]BaselinePackage , resolved []ResolvedPackage ) map [string ]string {
544+ postInstall := make (map [string ]string , len (sliceA )+ len (resolved ))
545+ for name , bp := range sliceA {
546+ postInstall [name ] = bp .Version
547+ }
548+ for _ , rp := range resolved {
549+ if name := strings .TrimSpace (rp .Name ); name != "" {
550+ postInstall [name ] = rp .Version
551+ }
552+ }
553+ return postInstall
554+ }
555+
536556// unsatisfiedVersionedAlternative reports whether a dependency edge is blocked by
537557// the present-but-wrong-version case, returning the offending alternative. An
538558// edge holds if ANY alternative is satisfied, so it is unsatisfied only when
0 commit comments