From 8aa782de83889daa80b22af529b20a5256ec7d16 Mon Sep 17 00:00:00 2001 From: philippedev101 Date: Wed, 22 Jul 2026 22:11:12 +0200 Subject: [PATCH 1/2] Fix Backpack CInst planning for installed signatures Backpack packages can be indefinite. An indefinite package contains signature modules, such as Str.hsig, that describe required modules but do not provide their implementations. When another package uses a Backpack mixin, Stack must build a concrete instantiation, a CInst, where those holes are filled by real modules from implementation packages. Stack represents dependency resolution with AddDepRes. ADRFound means the package is already installed and can be used as-is. ADRToInstall means Stack must build a Task for that package before dependents can use it. That distinction matters because only ADRToInstall entries become normal build-plan tasks. The previous implementation needed Package metadata for already-installed indefinite packages so it could create CInst tasks. It got that metadata by upgrading selected ADRFound entries to ADRToInstall. That made CInst creation possible, but it also changed the meaning of the plan: clean installed signature libraries started looking like normal packages that needed to be built. That was too broad and could trigger incorrect rebuild and unregister behavior. Replace that upgrade step with a separate Backpack template-loading pass. Installed indefinite packages now remain ADRFound in the normal build plan. When Backpack instantiation needs metadata, Stack loads a source-backed Task template for the signature package and keeps it in a side map. These templates are blueprints, not ordinary build-plan tasks. They are only used to construct CInst tasks with the right package description, configure options, dependency information, and source location. This keeps normal CLib planning unchanged. Clean installed signature packages remain installed dependencies, metadata lookup no longer implies a rebuild, and non-Backpack builds stay outside the new path. Also keep CInst-only tasks out of local unregister selection. A CInst registers an instantiated unit for the same package name, but its configure step still needs the indefinite CLib registration to exist. If Stack treats a CInst-only package as the representative local rebuild task, it can unregister the indefinite unit immediately before the CInst configure step needs it. Filtering CInst keys there preserves the installed indefinite unit while still allowing the CInst action itself to run. The normal ConstructPlan dependency resolver and the Backpack template resolver serve different purposes. The normal resolver recursively decides the real build plan: which packages are found, which packages must be installed, and which dependencies are missing or present. The Backpack resolver does not decide the main build plan. It only reconstructs enough dependency information for template tasks, because clean installed indefinite packages intentionally remain ADRFound and therefore do not have normal Tasks in the plan. While loading templates, follow only Backpack mixin dependencies. This discovers inherited holes without walking ordinary dependencies as Backpack targets. That keeps the extra work scoped to Backpack plans and avoids unnecessary lookups for packages like base. Also support source-backed snapshot packages in the template loader. Some already-installed signature packages can come from a snapshot while still having source metadata available. Loading that metadata is needed when inherited holes come through a snapshot-installed signature package. Share the AddDepRes-to-MissingPresentDeps conversion between the normal ConstructPlan dependency path and the Backpack template path. This avoids parallel logic and keeps ADRFound and ADRToInstall dependencies represented consistently in both places. Add focused integration coverage for stale CInst planning when the implementation package changes, the deepest signature package is dirty, an intermediate signature changes, an intermediate signature package source file changes, the final consumer changes, a required hole is inherited through another signature package, and an inherited signature package comes from a snapshot archive. Keep the existing transitive Backpack test as the clean rebuild baseline. --- src/Stack/Build/Backpack.hs | 314 +++++++++++------- src/Stack/Build/ConstructPlan.hs | 69 ++-- src/Stack/Types/Build/ConstructPlan.hs | 36 +- .../Main.hs | 63 ++++ .../files/.gitignore | 4 + .../files/consumer-pkg/app/Main.hs | 6 + .../files/consumer-pkg/package.yaml | 24 ++ .../files/consumer-pkg/src/Consumer.hs | 6 + .../files/impl-pkg/package.yaml | 9 + .../files/impl-pkg/src/Logger.hs | 4 + .../files/impl-pkg/src/Str.hs | 4 + .../files/logger-sig/package.yaml | 14 + .../files/logger-sig/src/LogHelper.hs | 7 + .../files/logger-sig/src/Logger.hsig | 3 + .../files/stack.yaml | 7 + .../files/str-sig/package.yaml | 11 + .../files/str-sig/src/Str.hsig | 3 + .../Main.hs | 72 ++++ .../files/.gitignore | 4 + .../files/consumer-pkg/app/Main.hs | 6 + .../files/consumer-pkg/package.yaml | 24 ++ .../files/consumer-pkg/src/Consumer.hs | 6 + .../files/impl-pkg/package.yaml | 9 + .../files/impl-pkg/src/Logger.hs | 4 + .../files/impl-pkg/src/Str.hs | 4 + .../files/logger-sig/package.yaml | 14 + .../files/logger-sig/src/LogHelper.hs | 7 + .../files/logger-sig/src/Logger.hsig | 3 + .../files/stack.yaml | 7 + .../files/str-sig/package.yaml | 11 + .../files/str-sig/src/Str.hsig | 3 + .../backpack-stale-cinst-hsig-change/Main.hs | 72 ++++ .../files/.gitignore | 4 + .../files/consumer-pkg/app/Main.hs | 6 + .../files/consumer-pkg/package.yaml | 24 ++ .../files/consumer-pkg/src/Consumer.hs | 6 + .../files/impl-pkg/package.yaml | 9 + .../files/impl-pkg/src/Logger.hs | 7 + .../files/impl-pkg/src/Str.hs | 4 + .../files/logger-sig/package.yaml | 14 + .../files/logger-sig/src/LogHelper.hs | 7 + .../files/logger-sig/src/Logger.hsig | 3 + .../files/stack.yaml | 7 + .../files/str-sig/package.yaml | 11 + .../files/str-sig/src/Str.hsig | 3 + .../backpack-stale-cinst-impl-change/Main.hs | 61 ++++ .../files/.gitignore | 4 + .../files/consumer-pkg/app/Main.hs | 6 + .../files/consumer-pkg/package.yaml | 24 ++ .../files/consumer-pkg/src/Consumer.hs | 6 + .../files/impl-pkg/package.yaml | 9 + .../files/impl-pkg/src/Logger.hs | 4 + .../files/impl-pkg/src/Str.hs | 4 + .../files/logger-sig/package.yaml | 14 + .../files/logger-sig/src/LogHelper.hs | 7 + .../files/logger-sig/src/Logger.hsig | 3 + .../files/stack.yaml | 7 + .../files/str-sig/package.yaml | 11 + .../files/str-sig/src/Str.hsig | 3 + .../Main.hs | 66 ++++ .../files/.gitignore | 4 + .../files/consumer-pkg/app/Main.hs | 6 + .../files/consumer-pkg/package.yaml | 21 ++ .../files/consumer-pkg/src/Consumer.hs | 6 + .../files/impl-pkg/package.yaml | 9 + .../files/impl-pkg/src/Logger.hs | 4 + .../files/impl-pkg/src/Str.hs | 4 + .../files/logger-sig/package.yaml | 14 + .../files/logger-sig/src/LogHelper.hs | 7 + .../files/logger-sig/src/Logger.hsig | 3 + .../files/stack.yaml | 7 + .../files/str-sig/package.yaml | 11 + .../files/str-sig/src/Str.hsig | 3 + .../Main.hs | 64 ++++ .../files/.gitignore | 4 + .../files/consumer-pkg/app/Main.hs | 6 + .../files/consumer-pkg/package.yaml | 24 ++ .../files/consumer-pkg/src/Consumer.hs | 6 + .../files/impl-pkg/package.yaml | 9 + .../files/impl-pkg/src/Logger.hs | 4 + .../files/impl-pkg/src/Str.hs | 4 + .../files/logger-sig/package.yaml | 14 + .../files/logger-sig/src/LogHelper.hs | 7 + .../files/logger-sig/src/Logger.hsig | 3 + .../files/stack.yaml | 7 + .../files/str-sig/package.yaml | 11 + .../files/str-sig/src/Str.hsig | 3 + .../Main.hs | 75 +++++ .../files/.gitignore | 7 + .../files/consumer-pkg/app/Main.hs | 6 + .../files/consumer-pkg/package.yaml | 21 ++ .../files/consumer-pkg/src/Consumer.hs | 6 + .../files/impl-pkg/package.yaml | 9 + .../files/impl-pkg/src/Logger.hs | 4 + .../files/impl-pkg/src/Str.hs | 4 + .../files/logger-sig/package.yaml | 14 + .../files/logger-sig/src/LogHelper.hs | 7 + .../files/logger-sig/src/Logger.hsig | 3 + .../files/snapshot.yaml | 6 + .../files/stack-bootstrap.yaml | 4 + .../files/stack.yaml | 6 + .../files/str-sig/package.yaml | 11 + .../files/str-sig/src/Str.hsig | 3 + tests/unit/Stack/Build/ConstructPlanSpec.hs | 10 +- 104 files changed, 1469 insertions(+), 156 deletions(-) create mode 100644 tests/integration/tests/backpack-stale-cinst-consumer-change/Main.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-consumer-change/files/.gitignore create mode 100644 tests/integration/tests/backpack-stale-cinst-consumer-change/files/consumer-pkg/app/Main.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-consumer-change/files/consumer-pkg/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-consumer-change/files/consumer-pkg/src/Consumer.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-consumer-change/files/impl-pkg/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-consumer-change/files/impl-pkg/src/Logger.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-consumer-change/files/impl-pkg/src/Str.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-consumer-change/files/logger-sig/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-consumer-change/files/logger-sig/src/LogHelper.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-consumer-change/files/logger-sig/src/Logger.hsig create mode 100644 tests/integration/tests/backpack-stale-cinst-consumer-change/files/stack.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-consumer-change/files/str-sig/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-consumer-change/files/str-sig/src/Str.hsig create mode 100644 tests/integration/tests/backpack-stale-cinst-dirty-str-sig/Main.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/.gitignore create mode 100644 tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/consumer-pkg/app/Main.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/consumer-pkg/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/consumer-pkg/src/Consumer.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/impl-pkg/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/impl-pkg/src/Logger.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/impl-pkg/src/Str.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/logger-sig/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/logger-sig/src/LogHelper.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/logger-sig/src/Logger.hsig create mode 100644 tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/stack.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/str-sig/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/str-sig/src/Str.hsig create mode 100644 tests/integration/tests/backpack-stale-cinst-hsig-change/Main.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-hsig-change/files/.gitignore create mode 100644 tests/integration/tests/backpack-stale-cinst-hsig-change/files/consumer-pkg/app/Main.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-hsig-change/files/consumer-pkg/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-hsig-change/files/consumer-pkg/src/Consumer.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-hsig-change/files/impl-pkg/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-hsig-change/files/impl-pkg/src/Logger.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-hsig-change/files/impl-pkg/src/Str.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-hsig-change/files/logger-sig/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-hsig-change/files/logger-sig/src/LogHelper.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-hsig-change/files/logger-sig/src/Logger.hsig create mode 100644 tests/integration/tests/backpack-stale-cinst-hsig-change/files/stack.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-hsig-change/files/str-sig/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-hsig-change/files/str-sig/src/Str.hsig create mode 100644 tests/integration/tests/backpack-stale-cinst-impl-change/Main.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-impl-change/files/.gitignore create mode 100644 tests/integration/tests/backpack-stale-cinst-impl-change/files/consumer-pkg/app/Main.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-impl-change/files/consumer-pkg/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-impl-change/files/consumer-pkg/src/Consumer.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-impl-change/files/impl-pkg/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-impl-change/files/impl-pkg/src/Logger.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-impl-change/files/impl-pkg/src/Str.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-impl-change/files/logger-sig/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-impl-change/files/logger-sig/src/LogHelper.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-impl-change/files/logger-sig/src/Logger.hsig create mode 100644 tests/integration/tests/backpack-stale-cinst-impl-change/files/stack.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-impl-change/files/str-sig/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-impl-change/files/str-sig/src/Str.hsig create mode 100644 tests/integration/tests/backpack-stale-cinst-inherited-hole/Main.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-inherited-hole/files/.gitignore create mode 100644 tests/integration/tests/backpack-stale-cinst-inherited-hole/files/consumer-pkg/app/Main.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-inherited-hole/files/consumer-pkg/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-inherited-hole/files/consumer-pkg/src/Consumer.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-inherited-hole/files/impl-pkg/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-inherited-hole/files/impl-pkg/src/Logger.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-inherited-hole/files/impl-pkg/src/Str.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-inherited-hole/files/logger-sig/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-inherited-hole/files/logger-sig/src/LogHelper.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-inherited-hole/files/logger-sig/src/Logger.hsig create mode 100644 tests/integration/tests/backpack-stale-cinst-inherited-hole/files/stack.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-inherited-hole/files/str-sig/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-inherited-hole/files/str-sig/src/Str.hsig create mode 100644 tests/integration/tests/backpack-stale-cinst-sig-source-change/Main.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-sig-source-change/files/.gitignore create mode 100644 tests/integration/tests/backpack-stale-cinst-sig-source-change/files/consumer-pkg/app/Main.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-sig-source-change/files/consumer-pkg/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-sig-source-change/files/consumer-pkg/src/Consumer.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-sig-source-change/files/impl-pkg/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-sig-source-change/files/impl-pkg/src/Logger.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-sig-source-change/files/impl-pkg/src/Str.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-sig-source-change/files/logger-sig/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-sig-source-change/files/logger-sig/src/LogHelper.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-sig-source-change/files/logger-sig/src/Logger.hsig create mode 100644 tests/integration/tests/backpack-stale-cinst-sig-source-change/files/stack.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-sig-source-change/files/str-sig/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-sig-source-change/files/str-sig/src/Str.hsig create mode 100644 tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/Main.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/.gitignore create mode 100644 tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/consumer-pkg/app/Main.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/consumer-pkg/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/consumer-pkg/src/Consumer.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/impl-pkg/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/impl-pkg/src/Logger.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/impl-pkg/src/Str.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/logger-sig/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/logger-sig/src/LogHelper.hs create mode 100644 tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/logger-sig/src/Logger.hsig create mode 100644 tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/snapshot.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/stack-bootstrap.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/stack.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/str-sig/package.yaml create mode 100644 tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/str-sig/src/Str.hsig diff --git a/src/Stack/Build/Backpack.hs b/src/Stack/Build/Backpack.hs index 28e11f7b4b..5a99e30aba 100644 --- a/src/Stack/Build/Backpack.hs +++ b/src/Stack/Build/Backpack.hs @@ -16,7 +16,7 @@ tasks that instantiate those packages with concrete implementations. module Stack.Build.Backpack ( addInstantiationTasks - , upgradeFoundIndefinites + , loadFoundIndefiniteTasks ) where import Crypto.Hash ( hashWith, SHA256 (..) ) @@ -37,10 +37,12 @@ import Distribution.Types.ModuleRenaming import Path ( parent ) import Stack.ConfigureOpts ( packageConfigureOptsFromPackage ) import Stack.Package - ( packageIsIndefinite ) + ( packageIsIndefinite, processPackageDepsEither ) import Stack.Prelude import Stack.Types.Build.ConstructPlan - ( AddDepRes (..), CombinedMap, PackageInfo (..) ) + ( AddDepRes (..), CombinedMap, MissingPresentDeps (..) + , PackageInfo (..), processAdr + ) import Stack.Types.SourceMap ( CommonPackage (..) ) import Distribution.Types.BuildType ( BuildType (Configure) ) import Stack.Types.Cache ( CachePkgSrc (..) ) @@ -56,22 +58,29 @@ import Stack.Types.IsMutable ( IsMutable (..) ) import Stack.Types.NamedComponent ( NamedComponent (..) ) import Stack.Types.Package ( LocalPackage (..), Package (..), PackageSource (..) - , installedMapGhcPkgId, packageIdentifier - , toCabalMungedPackageId + , packageIdentifier, psVersion, toCabalMungedPackageId ) import Stack.Types.Plan ( ComponentKey (..), Task (..), TaskConfigOpts (..) , TaskType (..), installLocationIsMutable ) --- | Upgrade ADRFound entries to ADRToInstall for indefinite (Backpack --- signature) packages whose source is available. This is needed because --- addInstantiationTasks clones the sig-pkg's Task to create CInst tasks, --- and ADRFound entries don't carry a Task. +-- | Extract the package metadata from a build task. +taskPackage :: Task -> Package +taskPackage t = case t.taskType of + TTLocalMutable lp -> lp.package + TTRemotePackage _ p _ -> p + +-- | Load source-backed template tasks for ADRFound indefinite (Backpack +-- signature) packages. These tasks are used only as sources for CInst tasks. +-- The original ADRFound entries must remain ADRFound: a clean installed +-- indefinite package should not be added to the build plan just because a +-- dependent package needs a CInst task. -- --- Only packages actually referenced by a consumer's mixin are loaded, --- to avoid unnecessary Pantry lookups for the common case (no Backpack). -upgradeFoundIndefinites :: +-- Only packages actually referenced by a consumer's mixin, plus the indefinite +-- dependencies discovered while loading those package descriptions, are loaded. +-- This avoids unnecessary Pantry lookups for the common case (no Backpack). +loadFoundIndefiniteTasks :: forall env. HasEnvConfig env => ( PackageLocationImmutable -> Map FlagName Bool @@ -81,83 +90,103 @@ upgradeFoundIndefinites :: ) -- ^ Load package from source (loadPackage0) -> EnvConfig + -> Map PackageName PackageSource -> CombinedMap -> BaseConfigOpts -> [(PackageName, AddDepRes)] - -> RIO env [(PackageName, AddDepRes)] -upgradeFoundIndefinites loadPkg econfig combinedMap bco adrs = do - let adrMap = Map.fromList adrs - -- Collect package names referenced by any consumer's mixin. - mixinTargets :: Set PackageName - mixinTargets = Set.fromList - [ mixinPackageName mixin - | (_, ADRToInstall task) <- adrs - , let pkg = case task.taskType of - TTLocalMutable lp -> lp.package - TTRemotePackage _ p _ -> p - , mixin <- concatMap (\lib -> lib.buildInfo.mixins) - (maybeToList pkg.library ++ toList pkg.subLibraries) - ] - -- Filter to mixin targets that are ADRFound. - foundTargets = - [ name - | name <- Set.toList mixinTargets - , Just (ADRFound _ _) <- [Map.lookup name adrMap] - ] - if null foundTargets - then pure adrs - else do - -- For each ADRFound mixin target, try to load its Package and - -- upgrade to ADRToInstall if it is indefinite. - upgrades <- fmap Map.fromList $ forM foundTargets $ \name -> + -> RIO env (Map PackageName Task) +loadFoundIndefiniteTasks loadPkg econfig sources combinedMap bco adrs = + go Set.empty Map.empty foundTargets + where + adrMap :: Map PackageName AddDepRes + adrMap = Map.fromList adrs + + -- Collect package names referenced by any consumer's mixin. + mixinTargets :: Set PackageName + mixinTargets = Set.fromList + [ depName + | (_, ADRToInstall task) <- adrs + , let pkg = taskPackage task + , depName <- mixinTargetsFor pkg + ] + + -- Filter to mixin targets that are ADRFound. + foundTargets :: [PackageName] + foundTargets = + [ name + | name <- Set.toList mixinTargets + , Just (ADRFound _ _) <- [Map.lookup name adrMap] + ] + + go :: + Set PackageName + -> Map PackageName Task + -> [PackageName] + -> RIO env (Map PackageName Task) + go _seen templates [] = pure templates + go seen templates (name:rest) + | name `Set.member` seen = go seen templates rest + | otherwise = do + mTemplate <- loadTemplate name + case mTemplate of + Nothing -> + go (Set.insert name seen) templates rest + Just (_, template) -> + let pkg = taskPackage template + in go + (Set.insert name seen) + (Map.insert name template templates) + (foundDependencyTargets pkg ++ rest) + + loadTemplate :: + PackageName + -> RIO env (Maybe (PackageName, Task)) + loadTemplate name = + case Map.lookup name sources of + Just ps -> + loadFromSourceTemplate name ps + Nothing -> case Map.lookup name combinedMap of Just (PIOnlyInstalled _ installed) -> - -- Source not available (GHC boot library or similar). Look up - -- Hackage as a fallback. - upgradeFromHackage name installed - Just (PIBoth ps installed) -> - upgradeFromSource name ps (Just installed) + -- Source not available from the source map (GHC boot library, + -- external package DB, or similar). Look up Hackage as a fallback; + -- packageIsIndefinite filters out ordinary dependencies. + loadFromHackage name installed + Just (PIBoth ps _) -> + loadFromSourceTemplate name ps Just (PIOnlySource ps) -> -- Should not happen (ADRFound implies installed), but handle -- gracefully by loading the package to check. - upgradeFromSource name ps Nothing + loadFromSourceTemplate name ps Nothing -> -- Package not in combined map — shouldn't happen. Skip. - pure (name, Nothing) - -- Apply upgrades. - pure - [ case Map.lookup name upgrades of - Just (Just adr') -> (name, adr') - _ -> (name, adr) - | (name, adr) <- adrs - ] - where + pure Nothing + loadFromSource :: PackageSource -> RIO env Package loadFromSource = \case PSRemote pkgLoc _version _fromSnapshot cp -> runRIO econfig $ loadPkg pkgLoc cp.flags cp.ghcOptions cp.cabalConfigOpts PSFilePath lp -> pure lp.package - mkTask :: Package -> PackageSource -> Maybe Installed -> Task - mkTask package ps mInstalled = + mkTask :: Package -> PackageSource -> Task + mkTask package ps = let loc = psLocation ps isMutable = installLocationIsMutable loc - presentMap = case mInstalled of - Just (Library pid ili) -> installedMapGhcPkgId pid ili - _ -> Map.empty + MissingPresentDeps missing present minMutable = + templateDeps package in Task { configOpts = TaskConfigOpts - { missing = Set.empty + { missing , envConfig = econfig , baseConfigOpts = bco , isLocalNonExtraDep = psLocal ps - , isMutable + , isMutable = isMutable <> minMutable , pkgConfigOpts = packageConfigureOptsFromPackage package , instantiationDeps = [] } , buildHaddocks = False , allInOne = True - , present = presentMap + , present , taskType = case ps of PSFilePath lp -> TTLocalMutable lp PSRemote pkgLoc _version _fromSnapshot _cp -> @@ -167,22 +196,21 @@ upgradeFoundIndefinites loadPkg econfig combinedMap bco adrs = do , backpackInstEntries = [] } - upgradeFromSource :: + loadFromSourceTemplate :: PackageName -> PackageSource - -> Maybe Installed - -> RIO env (PackageName, Maybe AddDepRes) - upgradeFromSource name ps mInstalled = do + -> RIO env (Maybe (PackageName, Task)) + loadFromSourceTemplate name ps = do package <- loadFromSource ps if packageIsIndefinite package - then pure (name, Just $ ADRToInstall $ mkTask package ps mInstalled) - else pure (name, Nothing) + then pure $ Just (name, mkTask package ps) + else pure Nothing - upgradeFromHackage :: + loadFromHackage :: PackageName -> Installed - -> RIO env (PackageName, Maybe AddDepRes) - upgradeFromHackage name installed = do + -> RIO env (Maybe (PackageName, Task)) + loadFromHackage name installed = do let version = installedVersion installed mPkgLoc <- runRIO econfig $ getLatestHackageRevision YesRequireHackageIndex name version >>= \case @@ -190,34 +218,84 @@ upgradeFoundIndefinites loadPkg econfig combinedMap bco adrs = do Just (_rev, cfKey, treeKey) -> pure $ Just $ PLIHackage (PackageIdentifier name version) cfKey treeKey case mPkgLoc of - Nothing -> pure (name, Nothing) + Nothing -> pure Nothing Just pkgLoc -> do package <- runRIO econfig $ loadPkg pkgLoc Map.empty [] [] if packageIsIndefinite package then do - let presentMap = case installed of - Library pid ili -> installedMapGhcPkgId pid ili - _ -> Map.empty + let MissingPresentDeps missing present minMutable = + templateDeps package task = Task { configOpts = TaskConfigOpts - { missing = Set.empty + { missing , envConfig = econfig , baseConfigOpts = bco , isLocalNonExtraDep = False - , isMutable = Immutable + , isMutable = Immutable <> minMutable , pkgConfigOpts = packageConfigureOptsFromPackage package , instantiationDeps = [] } , buildHaddocks = False , allInOne = True - , present = presentMap + , present , taskType = TTRemotePackage Immutable package pkgLoc , cachePkgSrc = CacheSrcUpstream , buildTypeConfig = packageBuildTypeConfig package , backpackInstEntries = [] } - pure (name, Just $ ADRToInstall task) - else pure (name, Nothing) + pure $ Just (name, task) + else pure Nothing + + foundDependencyTargets :: Package -> [PackageName] + foundDependencyTargets package = + [ depName + | depName <- mixinTargetsFor package + , Just ADRFound{} <- [Map.lookup depName adrMap] + ] + + mixinTargetsFor :: Package -> [PackageName] + mixinTargetsFor package = + [ mixinPackageName mixin + | lib <- maybeToList package.library ++ toList package.subLibraries + , mixin <- lib.buildInfo.mixins + ] + + templateDeps :: + Package + -> MissingPresentDeps + templateDeps package = + either (const mempty) id $ + runIdentity $ + processPackageDepsEither package $ \depName _depValue -> + pure (Right (case Map.lookup depName adrMap of + Just adr -> + processAdr adr + Nothing -> + dependencyFromCombinedMap depName) :: Either () MissingPresentDeps) + + dependencyFromCombinedMap :: PackageName -> MissingPresentDeps + dependencyFromCombinedMap depName = + case Map.lookup depName combinedMap of + Just (PIOnlyInstalled loc installed) -> + dependencyFromInstalled loc installed + Just (PIBoth _ installed) -> + dependencyFromInstalled Local installed + Just (PIOnlySource ps) -> + MissingPresentDeps + { missingPackages = Set.singleton $ + PackageIdentifier depName (psVersion ps) + , presentPackages = mempty + , isMutable = installLocationIsMutable $ psLocation ps + } + Nothing -> + mempty + + dependencyFromInstalled :: + InstallLocation + -> Installed + -> MissingPresentDeps + dependencyFromInstalled loc installed = + processAdr $ ADRFound loc installed -- | Post-pass: scan consumer tasks for Backpack mixins referencing indefinite -- packages and create CInst instantiation tasks. Returns the augmented list @@ -227,11 +305,13 @@ addInstantiationTasks :: -- ^ Installed package modules (from ghc-pkg dump). Used for module -- resolution when the implementing package is ADRFound (already -- installed, no Task/Package metadata available). + -> Map PackageName Task + -- ^ Source-backed templates for clean ADRFound indefinite packages. -> [(PackageName, AddDepRes)] -- ^ Original per-package ADRs -> [(ComponentKey, AddDepRes)] -- ^ Expanded component-keyed ADRs -> ([(ComponentKey, AddDepRes)], [StyleDoc]) -- ^ (Augmented with CInst tasks, warnings) -addInstantiationTasks installedModules origAdrs expandedAdrs = +addInstantiationTasks installedModules foundIndefiniteTasks origAdrs expandedAdrs = let adrMap = Map.fromList origAdrs -- Process each entry, collecting new CInst tasks and modified consumers. (newInstTasks, modifiedEntries, warns) = @@ -281,12 +361,6 @@ addInstantiationTasks installedModules origAdrs expandedAdrs = processEntry _ entry (instAcc, entryAcc, warnAcc) = (instAcc, entry : entryAcc, warnAcc) - -- Extract the Package from a Task's TaskType. - taskPackage :: Task -> Package - taskPackage t = case t.taskType of - TTLocalMutable lp -> lp.package - TTRemotePackage _ p _ -> p - -- Process all mixins for a consumer, returning new CInst entries, their -- ComponentKeys (to add as deps on the consumer), and any warnings. processAllMixins :: @@ -307,10 +381,10 @@ addInstantiationTasks installedModules origAdrs expandedAdrs = -> ([(ComponentKey, AddDepRes)], [ComponentKey], [StyleDoc]) processMixin adrMap' consumerDeps mixin (instAcc, keyAcc, warnAcc) = let depPkgName = mixinPackageName mixin - in case Map.lookup depPkgName adrMap' of - Just (ADRToInstall sigTask) - | let sigPkg = taskPackage sigTask - , packageIsIndefinite sigPkg -> + in case lookupIndefiniteTask depPkgName adrMap' of + Just sigTask -> + let sigPkg = taskPackage sigTask + in let -- Get signatures from the sig-pkg's main library. ownSigs :: [ModuleName] ownSigs = case sigPkg.library of @@ -384,19 +458,21 @@ addInstantiationTasks installedModules origAdrs expandedAdrs = , instCk : keyAcc , resolveWarns ++ warnAcc ) - Just (ADRFound _ _) -> - -- Sig-pkg is installed and its source could not be resolved - -- (upgradeFoundIndefinites already tried). This typically means - -- a GHC boot library or a package missing from Hackage. - let w = fillSep - [ flow "Backpack: mixin referencing" - , style Current (fromPackageName depPkgName) - , flow "is skipped because that package is installed and its" - , flow "source could not be found for instantiation." - , flow "Consider adding it as a local package in stack.yaml." - ] - in (instAcc, keyAcc, w : warnAcc) - _ -> (instAcc, keyAcc, warnAcc) + Nothing -> + case Map.lookup depPkgName adrMap' of + Just ADRFound{} -> + -- Sig-pkg is installed and its source could not be resolved + -- (loadFoundIndefiniteTasks already tried). This typically means + -- a GHC boot library or a package missing from Hackage. + let w = fillSep + [ flow "Backpack: mixin referencing" + , style Current (fromPackageName depPkgName) + , flow "is skipped because that package is installed and its" + , flow "source could not be found for instantiation." + , flow "Consider adding it as a local package in stack.yaml." + ] + in (instAcc, keyAcc, w : warnAcc) + _ -> (instAcc, keyAcc, warnAcc) -- Collect signatures inherited from a package's transitive indefinite deps. -- When pkg-A depends on indefinite pkg-B, pkg-B's signatures propagate up @@ -418,17 +494,27 @@ addInstantiationTasks installedModules origAdrs expandedAdrs = collectFromDep depName | depName `Set.member` visited = [] | otherwise = - case Map.lookup depName adrMap' of - Just (ADRToInstall depTask) - | let depPkg = taskPackage depTask - , packageIsIndefinite depPkg -> - let depSigs = case depPkg.library of - Just lib -> lib.signatures - Nothing -> [] - transitive = collectInheritedSigs depPkg adrMap' - (Set.insert depName visited) - in depSigs ++ transitive - _ -> [] + case lookupIndefiniteTask depName adrMap' of + Just depTask -> + let depPkg = taskPackage depTask + depSigs = case depPkg.library of + Just lib -> lib.signatures + Nothing -> [] + transitive = collectInheritedSigs depPkg adrMap' + (Set.insert depName visited) + in depSigs ++ transitive + Nothing -> [] + + lookupIndefiniteTask :: + PackageName + -> Map PackageName AddDepRes + -> Maybe Task + lookupIndefiniteTask depPkgName adrMap' = + case Map.lookup depPkgName adrMap' of + Just (ADRToInstall task) + | packageIsIndefinite (taskPackage task) -> Just task + Just ADRFound{} -> Map.lookup depPkgName foundIndefiniteTasks + _ -> Nothing -- Resolve all signature entries for a mixin, collecting both successful -- resolutions and warnings for signatures that could not be resolved. diff --git a/src/Stack/Build/ConstructPlan.hs b/src/Stack/Build/ConstructPlan.hs index 5d38d4b31b..4cf4669213 100644 --- a/src/Stack/Build/ConstructPlan.hs +++ b/src/Stack/Build/ConstructPlan.hs @@ -34,7 +34,7 @@ import RIO.State ( State, StateT (..), execState, get, modify, modify', put ) import RIO.Writer ( WriterT (..), pass, tell ) import Stack.Build.Backpack - ( addInstantiationTasks, upgradeFoundIndefinites ) + ( addInstantiationTasks, loadFoundIndefiniteTasks ) import Stack.Build.Cache ( tryGetFlagCache ) import Stack.Build.Haddock ( shouldHaddockDeps ) import Stack.Build.Source ( loadLocalPackage ) @@ -56,7 +56,8 @@ import Stack.Types.Build.ConstructPlan ( AddDepRes (..), CombinedMap, Ctx (..), LibraryMap, M , MissingPresentDeps (..), PackageInfo (..), PackageLoader , ToolWarning(..), UnregisterState (..), W (..) - , adrHasLibrary, adrVersion, isAdrToInstall, toTask + , adrHasLibrary, adrVersion, isAdrToInstall, processAdr + , toTask ) import Stack.Types.Build.Exception ( BadDependency (..), BuildException (..) @@ -95,14 +96,14 @@ import Stack.Types.NamedComponent ) import Stack.Types.Package ( ExeName (..), LocalPackage (..), Package (..) - , PackageSource (..), installedMapGhcPkgId - , packageIdentifier, psVersion, runMemoizedWith + , PackageSource (..), packageIdentifier, psVersion + , runMemoizedWith ) import Stack.Types.Plan ( ComponentKey (..), Plan (..), Task (..) , TaskConfigOpts (..), TaskType (..), componentKeyPkgName , fromComponentKey, installLocationIsMutable, taskIsTarget - , taskLocation, taskProvides, taskTargetIsMutable + , taskLocation, taskProvides ) import Stack.Types.ProjectConfig ( isPCGlobalProject ) import Stack.Types.Runner ( HasRunner (..), globalOptsL ) @@ -226,9 +227,9 @@ constructPlan errs = errlibs ++ errfinals if null errs then do - -- Upgrade ADRFound entries to ADRToInstall for indefinite packages - -- whose source is available. This ensures addInstantiationTasks can - -- clone their Task to create CInst instantiation tasks. + -- Load source-backed templates for clean installed indefinite packages. + -- This lets addInstantiationTasks clone them for CInst tasks without + -- turning their ADRFound CLib entries into build tasks. let loadPkg w x y z = applyForceCustomBuild globalCabalVersion <$> loadPackage0 w x y z -- Build a module lookup map from installed packages (dump data). @@ -239,11 +240,15 @@ constructPlan | dp <- allDumpPkgs , isNothing dp.sublib -- Only main libraries ] - adrs' <- upgradeFoundIndefinites - loadPkg econfig ctx.combinedMap ctx.baseConfigOpts adrs - let expandedAdrs = concatMap (uncurry expandToComponentKeys) adrs' + foundIndefiniteTasks <- loadFoundIndefiniteTasks + loadPkg econfig sources ctx.combinedMap ctx.baseConfigOpts adrs + let expandedAdrs = concatMap (uncurry expandToComponentKeys) adrs (withInstantiations, bpWarnings) = - addInstantiationTasks installedModules adrs' expandedAdrs + addInstantiationTasks + installedModules + foundIndefiniteTasks + adrs + expandedAdrs tasks = Map.fromList $ mapMaybe (toMaybe . second toTask) withInstantiations @@ -416,10 +421,17 @@ mkUnregisterLocal componentTasks dirtyReason localDumpPkgs initialBuildSteps = -- directly or transitively depending on it. loop Map.empty localDumpPkgs where - -- Derive a per-package task map: pick one representative task per package. + -- Derive a per-package task map: pick one representative non-CInst task per + -- package. A CInst task registers a Backpack instantiation, but it relies on + -- the existing indefinite library registration for the same package name. + -- Treating CInst-only packages as normal rebuilds unregisters that + -- indefinite unit before the CInst configure step can use it. tasks :: Map PackageName Task tasks = Map.fromList - [ (componentKeyPkgName ck, t) | (ck, t) <- Map.toList componentTasks ] + [ (componentKeyPkgName ck, t) + | (ck, t) <- Map.toList componentTasks + , not (componentKeyIsCInst ck) + ] loop :: Map GhcPkgId (PackageIdentifier, Text) @@ -505,6 +517,10 @@ mkUnregisterLocal componentTasks dirtyReason localDumpPkgs initialBuildSteps = relevantPkgName :: PackageName relevantPkgName = maybe (pkgName ident) pkgName mParentLibId + componentKeyIsCInst :: ComponentKey -> Bool + componentKeyIsCInst (ComponentKey _ CInst{}) = True + componentKeyIsCInst _ = False + -- | Given a t'LocalPackage' and its test\/benchmark 'Package', adds a -- t'Task' for running its tests and benchmarks. Resolves the package's deps -- via 'addPackageDeps' (in the common case these have already been resolved @@ -1154,31 +1170,6 @@ adrInRange pkgId name range adr = if adrVersion adr `withinRange` range , reason <> "." ] --- | Given a result of 'addDep', yields a triple indicating: (1) if the --- dependency is to be installed, its package identifier; (2) if the dependency --- is installed and a library, its package identifier and 'GhcPkgId'; and (3) if --- the dependency is, or will be when installed, mutable or immutable. -processAdr :: - AddDepRes - -> MissingPresentDeps -processAdr adr = case adr of - ADRToInstall task -> - MissingPresentDeps - { missingPackages = Set.singleton $ taskProvides task - , presentPackages = mempty - , isMutable = taskTargetIsMutable task - } - ADRFound loc installed -> - MissingPresentDeps - { missingPackages = mempty - , presentPackages = presentPackagesV - , isMutable = installLocationIsMutable loc - } - where - presentPackagesV = case installed of - Library ident installedInfo -> installedMapGhcPkgId ident installedInfo - _ -> Map.empty - checkDirtiness :: PackageSource -> Installed diff --git a/src/Stack/Types/Build/ConstructPlan.hs b/src/Stack/Types/Build/ConstructPlan.hs index 9e7dee3d93..fa0069c6ed 100644 --- a/src/Stack/Types/Build/ConstructPlan.hs +++ b/src/Stack/Types/Build/ConstructPlan.hs @@ -20,6 +20,7 @@ module Stack.Types.Build.ConstructPlan , toTask , adrVersion , adrHasLibrary + , processAdr , isAdrToInstall , Ctx (..) , PackageLoader @@ -29,6 +30,8 @@ module Stack.Types.Build.ConstructPlan ) where import Generics.Deriving.Monoid ( mappenddefault, memptydefault ) +import qualified Data.Map.Strict as Map +import qualified Data.Set as Set import RIO.Process ( HasProcessContext (..) ) import RIO.State ( StateT ) import RIO.Writer ( WriterT (..) ) @@ -52,11 +55,14 @@ import Stack.Types.Installed import Stack.Types.IsMutable ( IsMutable ) import Stack.Types.Package ( ExeName (..), LocalPackage (..), Package (..) - , PackageSource (..) + , PackageSource (..), installedMapGhcPkgId ) import Stack.Types.ParentMap ( ParentMap ) import Stack.Types.Plan - ( ComponentKey, Task (..), TaskType (..), taskProvides ) + ( ComponentKey, Task (..), TaskType (..) + , installLocationIsMutable, taskProvides + , taskTargetIsMutable + ) import Stack.Types.Platform ( HasPlatform (..) ) import Stack.Types.Runner ( HasRunner (..) ) @@ -152,6 +158,32 @@ adrHasLibrary (ADRToInstall task) = case task.taskType of adrHasLibrary (ADRFound _ Library{}) = True adrHasLibrary (ADRFound _ Executable{}) = False +-- | Given a result of 'Stack.Build.ConstructPlan.addDep', yields a triple +-- indicating: (1) if the dependency is to be installed, its package identifier; +-- (2) if the dependency is installed and a library, its package identifier and +-- 'GhcPkgId'; and (3) if the dependency is, or will be when installed, mutable +-- or immutable. +processAdr :: + AddDepRes + -> MissingPresentDeps +processAdr adr = case adr of + ADRToInstall task -> + MissingPresentDeps + { missingPackages = Set.singleton $ taskProvides task + , presentPackages = mempty + , isMutable = taskTargetIsMutable task + } + ADRFound loc installed -> + MissingPresentDeps + { missingPackages = mempty + , presentPackages = presentPackagesV + , isMutable = installLocationIsMutable loc + } + where + presentPackagesV = case installed of + Library ident installedInfo -> installedMapGhcPkgId ident installedInfo + _ -> Map.empty + data MissingPresentDeps = MissingPresentDeps { missingPackages :: !(Set PackageIdentifier) , presentPackages :: !(Map MungedPackageId GhcPkgId) diff --git a/tests/integration/tests/backpack-stale-cinst-consumer-change/Main.hs b/tests/integration/tests/backpack-stale-cinst-consumer-change/Main.hs new file mode 100644 index 0000000000..e7d0e89554 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-consumer-change/Main.hs @@ -0,0 +1,63 @@ +-- Regression test for stale Backpack instantiation state when the final +-- Backpack consumer changes. consumer-pkg is dirty, but both indefinite +-- signature packages are clean and already installed. + +import Control.Monad ( unless ) +import Data.List ( isInfixOf ) +import System.Directory ( removeFile ) +import StackTest + +main :: IO () +main = do + -- Build all four packages. This exercises: + -- 1. str-sig CLib (indefinite, typecheck-only) + -- 2. impl-pkg CLib (concrete Str + Logger) + -- 3. str-sig CInst (instantiation with impl-pkg's Str) + -- 4. logger-sig CLib (indefinite, typecheck-only, inherits Str hole) + -- 5. logger-sig CInst (fills BOTH Logger and Str holes) + -- 6. consumer-pkg CLib + CExe + stack ["build"] + + -- Verify the consumer executable calls through the transitive chain + stackCheckStdout ["exec", "consumer-demo"] $ \out -> + unless ("[LOG] Hello from transitive chain" `isInfixOf` out) $ + error $ "Expected '[LOG] Hello from transitive chain' in output, got: " + ++ show out + + replaceFile "consumer-pkg/src/Consumer.hs" $ unlines + [ "module Consumer where" + , "" + , "import LogHelper (greetWithLog)" + , "" + , "hello :: String" + , "hello = greetWithLog ++ \" after consumer edit\"" + ] + + -- Rebuild should succeed because only the final consumer changed. + stackCheckStderr ["build"] $ \err -> + expectBuildLine "consumer-pkg" "Compiling Consumer" err + + -- Verify output still correct after rebuild + stackCheckStdout ["exec", "consumer-demo"] $ \out -> + unless ("[LOG] Hello from transitive chain after consumer edit" `isInfixOf` out) $ + error $ + "Expected edited consumer-pkg output after rebuild, got: " + ++ show out + +replaceFile :: FilePath -> String -> IO () +replaceFile file contents = do + removeFile file + writeFile file contents + +expectBuildLine :: String -> String -> String -> IO () +expectBuildLine package marker err = + unless (any matches $ lines err) $ + error $ + "Expected build output line containing " + ++ show package + ++ " and " + ++ show marker + ++ ", got stderr: " + ++ show err + where + matches line = package `isInfixOf` line && marker `isInfixOf` line diff --git a/tests/integration/tests/backpack-stale-cinst-consumer-change/files/.gitignore b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/.gitignore new file mode 100644 index 0000000000..1c91379fd4 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/.gitignore @@ -0,0 +1,4 @@ +consumer-pkg.cabal +impl-pkg.cabal +logger-sig.cabal +str-sig.cabal diff --git a/tests/integration/tests/backpack-stale-cinst-consumer-change/files/consumer-pkg/app/Main.hs b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/consumer-pkg/app/Main.hs new file mode 100644 index 0000000000..4c26592390 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/consumer-pkg/app/Main.hs @@ -0,0 +1,6 @@ +module Main where + +import Consumer (hello) + +main :: IO () +main = putStrLn hello diff --git a/tests/integration/tests/backpack-stale-cinst-consumer-change/files/consumer-pkg/package.yaml b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/consumer-pkg/package.yaml new file mode 100644 index 0000000000..ba65db2645 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/consumer-pkg/package.yaml @@ -0,0 +1,24 @@ +spec-version: 0.36.0 + +name: consumer-pkg + +dependencies: +- base + +library: + source-dirs: src + dependencies: + - name: str-sig + mixin: + - requires (Str as Str) + - name: logger-sig + mixin: + - requires (Logger as Logger) + - impl-pkg + +executables: + consumer-demo: + source-dirs: app + main: Main.hs + dependencies: + - consumer-pkg diff --git a/tests/integration/tests/backpack-stale-cinst-consumer-change/files/consumer-pkg/src/Consumer.hs b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/consumer-pkg/src/Consumer.hs new file mode 100644 index 0000000000..e46c8658a6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/consumer-pkg/src/Consumer.hs @@ -0,0 +1,6 @@ +module Consumer where + +import LogHelper (greetWithLog) + +hello :: String +hello = greetWithLog diff --git a/tests/integration/tests/backpack-stale-cinst-consumer-change/files/impl-pkg/package.yaml b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/impl-pkg/package.yaml new file mode 100644 index 0000000000..bd4497b3d7 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/impl-pkg/package.yaml @@ -0,0 +1,9 @@ +spec-version: 0.36.0 + +name: impl-pkg + +dependencies: +- base + +library: + source-dirs: src diff --git a/tests/integration/tests/backpack-stale-cinst-consumer-change/files/impl-pkg/src/Logger.hs b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/impl-pkg/src/Logger.hs new file mode 100644 index 0000000000..ddc7981a34 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/impl-pkg/src/Logger.hs @@ -0,0 +1,4 @@ +module Logger where + +logMessage :: String -> String +logMessage msg = "[LOG] " ++ msg diff --git a/tests/integration/tests/backpack-stale-cinst-consumer-change/files/impl-pkg/src/Str.hs b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/impl-pkg/src/Str.hs new file mode 100644 index 0000000000..3b6d306620 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/impl-pkg/src/Str.hs @@ -0,0 +1,4 @@ +module Str where + +greeting :: String +greeting = "Hello from transitive chain" diff --git a/tests/integration/tests/backpack-stale-cinst-consumer-change/files/logger-sig/package.yaml b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/logger-sig/package.yaml new file mode 100644 index 0000000000..b9db28324f --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/logger-sig/package.yaml @@ -0,0 +1,14 @@ +spec-version: 0.36.0 + +name: logger-sig + +dependencies: +- base + +library: + source-dirs: src + signatures: Logger + dependencies: + - name: str-sig + mixin: + - requires (Str as Str) diff --git a/tests/integration/tests/backpack-stale-cinst-consumer-change/files/logger-sig/src/LogHelper.hs b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/logger-sig/src/LogHelper.hs new file mode 100644 index 0000000000..2eecba92d6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/logger-sig/src/LogHelper.hs @@ -0,0 +1,7 @@ +module LogHelper where + +import Str (greeting) +import Logger (logMessage) + +greetWithLog :: String +greetWithLog = logMessage greeting diff --git a/tests/integration/tests/backpack-stale-cinst-consumer-change/files/logger-sig/src/Logger.hsig b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/logger-sig/src/Logger.hsig new file mode 100644 index 0000000000..446efe6fe6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/logger-sig/src/Logger.hsig @@ -0,0 +1,3 @@ +signature Logger where + +logMessage :: String -> String diff --git a/tests/integration/tests/backpack-stale-cinst-consumer-change/files/stack.yaml b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/stack.yaml new file mode 100644 index 0000000000..f74ff881c8 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/stack.yaml @@ -0,0 +1,7 @@ +snapshot: ghc-9.10.3 + +packages: +- consumer-pkg +- impl-pkg +- logger-sig +- str-sig diff --git a/tests/integration/tests/backpack-stale-cinst-consumer-change/files/str-sig/package.yaml b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/str-sig/package.yaml new file mode 100644 index 0000000000..a6b1796ff1 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/str-sig/package.yaml @@ -0,0 +1,11 @@ +spec-version: 0.36.0 + +name: str-sig + +dependencies: +- base + +library: + source-dirs: src + signatures: + - Str diff --git a/tests/integration/tests/backpack-stale-cinst-consumer-change/files/str-sig/src/Str.hsig b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/str-sig/src/Str.hsig new file mode 100644 index 0000000000..edb56a4a7d --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-consumer-change/files/str-sig/src/Str.hsig @@ -0,0 +1,3 @@ +signature Str where + +greeting :: String diff --git a/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/Main.hs b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/Main.hs new file mode 100644 index 0000000000..fcbf648a77 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/Main.hs @@ -0,0 +1,72 @@ +-- Regression test for stale Backpack instantiation state when the deepest +-- indefinite signature package is dirty. str-sig must rebuild its indefinite +-- CLib, and the str-sig CInst must use that rebuilt unit. + +import Control.Monad ( unless ) +import Data.List ( isInfixOf ) +import System.Directory ( removeFile ) +import StackTest + +main :: IO () +main = do + -- Build all four packages. This exercises: + -- 1. str-sig CLib (indefinite, typecheck-only) + -- 2. impl-pkg CLib (concrete Str + Logger) + -- 3. str-sig CInst (instantiation with impl-pkg's Str) + -- 4. logger-sig CLib (indefinite, typecheck-only, inherits Str hole) + -- 5. logger-sig CInst (fills BOTH Logger and Str holes) + -- 6. consumer-pkg CLib + CExe + stack ["build"] + + -- Verify the consumer executable calls through the transitive chain + stackCheckStdout ["exec", "consumer-demo"] $ \out -> + unless ("[LOG] Hello from transitive chain" `isInfixOf` out) $ + error $ "Expected '[LOG] Hello from transitive chain' in output, got: " + ++ show out + + replaceFile "str-sig/package.yaml" $ unlines + [ "spec-version: 0.36.0" + , "" + , "name: str-sig" + , "" + , "dependencies:" + , "- base" + , "" + , "library:" + , " source-dirs: src" + , " ghc-options:" + , " - -Wall" + , " signatures:" + , " - Str" + ] + + -- Rebuild should succeed because the CInst waits for the rebuilt indefinite + -- str-sig CLib instead of trying to use the stale installed one. + stackCheckStderr ["build"] $ \err -> do + expectBuildLine "str-sig" "Compiling Str[sig]" err + expectBuildLine "str-sig" "Str = impl-pkg" err + + -- Verify output still correct after rebuilding str-sig and its CInst. + stackCheckStdout ["exec", "consumer-demo"] $ \out -> + unless ("[LOG] Hello from transitive chain" `isInfixOf` out) $ + error $ + "Expected original output after rebuilding str-sig, got: " + ++ show out + +replaceFile :: FilePath -> String -> IO () +replaceFile file contents = do + removeFile file + writeFile file contents + +expectBuildLine :: String -> String -> String -> IO () +expectBuildLine package marker err = + unless (any matches $ lines err) $ + error $ + "Expected build output line containing " + ++ show package + ++ " and " + ++ show marker + ++ ", got stderr: " + ++ show err + where + matches line = package `isInfixOf` line && marker `isInfixOf` line diff --git a/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/.gitignore b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/.gitignore new file mode 100644 index 0000000000..1c91379fd4 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/.gitignore @@ -0,0 +1,4 @@ +consumer-pkg.cabal +impl-pkg.cabal +logger-sig.cabal +str-sig.cabal diff --git a/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/consumer-pkg/app/Main.hs b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/consumer-pkg/app/Main.hs new file mode 100644 index 0000000000..4c26592390 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/consumer-pkg/app/Main.hs @@ -0,0 +1,6 @@ +module Main where + +import Consumer (hello) + +main :: IO () +main = putStrLn hello diff --git a/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/consumer-pkg/package.yaml b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/consumer-pkg/package.yaml new file mode 100644 index 0000000000..ba65db2645 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/consumer-pkg/package.yaml @@ -0,0 +1,24 @@ +spec-version: 0.36.0 + +name: consumer-pkg + +dependencies: +- base + +library: + source-dirs: src + dependencies: + - name: str-sig + mixin: + - requires (Str as Str) + - name: logger-sig + mixin: + - requires (Logger as Logger) + - impl-pkg + +executables: + consumer-demo: + source-dirs: app + main: Main.hs + dependencies: + - consumer-pkg diff --git a/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/consumer-pkg/src/Consumer.hs b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/consumer-pkg/src/Consumer.hs new file mode 100644 index 0000000000..e46c8658a6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/consumer-pkg/src/Consumer.hs @@ -0,0 +1,6 @@ +module Consumer where + +import LogHelper (greetWithLog) + +hello :: String +hello = greetWithLog diff --git a/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/impl-pkg/package.yaml b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/impl-pkg/package.yaml new file mode 100644 index 0000000000..bd4497b3d7 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/impl-pkg/package.yaml @@ -0,0 +1,9 @@ +spec-version: 0.36.0 + +name: impl-pkg + +dependencies: +- base + +library: + source-dirs: src diff --git a/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/impl-pkg/src/Logger.hs b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/impl-pkg/src/Logger.hs new file mode 100644 index 0000000000..ddc7981a34 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/impl-pkg/src/Logger.hs @@ -0,0 +1,4 @@ +module Logger where + +logMessage :: String -> String +logMessage msg = "[LOG] " ++ msg diff --git a/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/impl-pkg/src/Str.hs b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/impl-pkg/src/Str.hs new file mode 100644 index 0000000000..3b6d306620 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/impl-pkg/src/Str.hs @@ -0,0 +1,4 @@ +module Str where + +greeting :: String +greeting = "Hello from transitive chain" diff --git a/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/logger-sig/package.yaml b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/logger-sig/package.yaml new file mode 100644 index 0000000000..b9db28324f --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/logger-sig/package.yaml @@ -0,0 +1,14 @@ +spec-version: 0.36.0 + +name: logger-sig + +dependencies: +- base + +library: + source-dirs: src + signatures: Logger + dependencies: + - name: str-sig + mixin: + - requires (Str as Str) diff --git a/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/logger-sig/src/LogHelper.hs b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/logger-sig/src/LogHelper.hs new file mode 100644 index 0000000000..2eecba92d6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/logger-sig/src/LogHelper.hs @@ -0,0 +1,7 @@ +module LogHelper where + +import Str (greeting) +import Logger (logMessage) + +greetWithLog :: String +greetWithLog = logMessage greeting diff --git a/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/logger-sig/src/Logger.hsig b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/logger-sig/src/Logger.hsig new file mode 100644 index 0000000000..446efe6fe6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/logger-sig/src/Logger.hsig @@ -0,0 +1,3 @@ +signature Logger where + +logMessage :: String -> String diff --git a/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/stack.yaml b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/stack.yaml new file mode 100644 index 0000000000..f74ff881c8 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/stack.yaml @@ -0,0 +1,7 @@ +snapshot: ghc-9.10.3 + +packages: +- consumer-pkg +- impl-pkg +- logger-sig +- str-sig diff --git a/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/str-sig/package.yaml b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/str-sig/package.yaml new file mode 100644 index 0000000000..a6b1796ff1 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/str-sig/package.yaml @@ -0,0 +1,11 @@ +spec-version: 0.36.0 + +name: str-sig + +dependencies: +- base + +library: + source-dirs: src + signatures: + - Str diff --git a/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/str-sig/src/Str.hsig b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/str-sig/src/Str.hsig new file mode 100644 index 0000000000..edb56a4a7d --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-dirty-str-sig/files/str-sig/src/Str.hsig @@ -0,0 +1,3 @@ +signature Str where + +greeting :: String diff --git a/tests/integration/tests/backpack-stale-cinst-hsig-change/Main.hs b/tests/integration/tests/backpack-stale-cinst-hsig-change/Main.hs new file mode 100644 index 0000000000..ca4258469c --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-hsig-change/Main.hs @@ -0,0 +1,72 @@ +-- Regression test for stale Backpack instantiation state when an intermediate +-- indefinite package's signature changes. The source touch keeps this focused +-- on stale CInst planning even before .hsig file tracking is fixed: +-- logger-sig is dirty, but its indefinite dependency str-sig is still clean +-- and already installed. + +import Control.Monad ( unless ) +import Data.List ( isInfixOf ) +import System.Directory ( removeFile ) +import StackTest + +main :: IO () +main = do + -- Build all four packages. This exercises: + -- 1. str-sig CLib (indefinite, typecheck-only) + -- 2. impl-pkg CLib (concrete Str + Logger) + -- 3. str-sig CInst (instantiation with impl-pkg's Str) + -- 4. logger-sig CLib (indefinite, typecheck-only, inherits Str hole) + -- 5. logger-sig CInst (fills BOTH Logger and Str holes) + -- 6. consumer-pkg CLib + CExe + stack ["build"] + + -- Verify the consumer executable calls through the transitive chain + stackCheckStdout ["exec", "consumer-demo"] $ \out -> + unless ("[LOG] Hello from transitive chain" `isInfixOf` out) $ + error $ "Expected '[LOG] Hello from transitive chain' in output, got: " + ++ show out + + replaceFile "logger-sig/src/Logger.hsig" $ unlines + [ "signature Logger where" + , "" + , "logMessage :: String -> String" + , "loggerName :: String" + ] + replaceFile "logger-sig/src/LogHelper.hs" $ unlines + [ "module LogHelper where" + , "" + , "import Str (greeting)" + , "import Logger (logMessage)" + , "" + , "-- Force logger-sig dirty independently of .hsig tracking." + , "greetWithLog :: String" + , "greetWithLog = logMessage greeting" + ] + + -- Rebuild should succeed because impl-pkg already exports loggerName. + stackCheckStderr ["build"] $ \err -> + expectBuildLine "logger-sig" "Compiling Logger[sig]" err + + -- Verify output still correct after rebuild + stackCheckStdout ["exec", "consumer-demo"] $ \out -> + unless ("[LOG] Hello from transitive chain" `isInfixOf` out) $ + error $ "Expected '[LOG] Hello from transitive chain' after rebuild, got: " + ++ show out + +replaceFile :: FilePath -> String -> IO () +replaceFile file contents = do + removeFile file + writeFile file contents + +expectBuildLine :: String -> String -> String -> IO () +expectBuildLine package marker err = + unless (any matches $ lines err) $ + error $ + "Expected build output line containing " + ++ show package + ++ " and " + ++ show marker + ++ ", got stderr: " + ++ show err + where + matches line = package `isInfixOf` line && marker `isInfixOf` line diff --git a/tests/integration/tests/backpack-stale-cinst-hsig-change/files/.gitignore b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/.gitignore new file mode 100644 index 0000000000..1c91379fd4 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/.gitignore @@ -0,0 +1,4 @@ +consumer-pkg.cabal +impl-pkg.cabal +logger-sig.cabal +str-sig.cabal diff --git a/tests/integration/tests/backpack-stale-cinst-hsig-change/files/consumer-pkg/app/Main.hs b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/consumer-pkg/app/Main.hs new file mode 100644 index 0000000000..4c26592390 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/consumer-pkg/app/Main.hs @@ -0,0 +1,6 @@ +module Main where + +import Consumer (hello) + +main :: IO () +main = putStrLn hello diff --git a/tests/integration/tests/backpack-stale-cinst-hsig-change/files/consumer-pkg/package.yaml b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/consumer-pkg/package.yaml new file mode 100644 index 0000000000..ba65db2645 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/consumer-pkg/package.yaml @@ -0,0 +1,24 @@ +spec-version: 0.36.0 + +name: consumer-pkg + +dependencies: +- base + +library: + source-dirs: src + dependencies: + - name: str-sig + mixin: + - requires (Str as Str) + - name: logger-sig + mixin: + - requires (Logger as Logger) + - impl-pkg + +executables: + consumer-demo: + source-dirs: app + main: Main.hs + dependencies: + - consumer-pkg diff --git a/tests/integration/tests/backpack-stale-cinst-hsig-change/files/consumer-pkg/src/Consumer.hs b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/consumer-pkg/src/Consumer.hs new file mode 100644 index 0000000000..e46c8658a6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/consumer-pkg/src/Consumer.hs @@ -0,0 +1,6 @@ +module Consumer where + +import LogHelper (greetWithLog) + +hello :: String +hello = greetWithLog diff --git a/tests/integration/tests/backpack-stale-cinst-hsig-change/files/impl-pkg/package.yaml b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/impl-pkg/package.yaml new file mode 100644 index 0000000000..bd4497b3d7 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/impl-pkg/package.yaml @@ -0,0 +1,9 @@ +spec-version: 0.36.0 + +name: impl-pkg + +dependencies: +- base + +library: + source-dirs: src diff --git a/tests/integration/tests/backpack-stale-cinst-hsig-change/files/impl-pkg/src/Logger.hs b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/impl-pkg/src/Logger.hs new file mode 100644 index 0000000000..62c1318517 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/impl-pkg/src/Logger.hs @@ -0,0 +1,7 @@ +module Logger where + +logMessage :: String -> String +logMessage msg = "[LOG] " ++ msg + +loggerName :: String +loggerName = "transitive logger" diff --git a/tests/integration/tests/backpack-stale-cinst-hsig-change/files/impl-pkg/src/Str.hs b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/impl-pkg/src/Str.hs new file mode 100644 index 0000000000..3b6d306620 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/impl-pkg/src/Str.hs @@ -0,0 +1,4 @@ +module Str where + +greeting :: String +greeting = "Hello from transitive chain" diff --git a/tests/integration/tests/backpack-stale-cinst-hsig-change/files/logger-sig/package.yaml b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/logger-sig/package.yaml new file mode 100644 index 0000000000..b9db28324f --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/logger-sig/package.yaml @@ -0,0 +1,14 @@ +spec-version: 0.36.0 + +name: logger-sig + +dependencies: +- base + +library: + source-dirs: src + signatures: Logger + dependencies: + - name: str-sig + mixin: + - requires (Str as Str) diff --git a/tests/integration/tests/backpack-stale-cinst-hsig-change/files/logger-sig/src/LogHelper.hs b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/logger-sig/src/LogHelper.hs new file mode 100644 index 0000000000..2eecba92d6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/logger-sig/src/LogHelper.hs @@ -0,0 +1,7 @@ +module LogHelper where + +import Str (greeting) +import Logger (logMessage) + +greetWithLog :: String +greetWithLog = logMessage greeting diff --git a/tests/integration/tests/backpack-stale-cinst-hsig-change/files/logger-sig/src/Logger.hsig b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/logger-sig/src/Logger.hsig new file mode 100644 index 0000000000..446efe6fe6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/logger-sig/src/Logger.hsig @@ -0,0 +1,3 @@ +signature Logger where + +logMessage :: String -> String diff --git a/tests/integration/tests/backpack-stale-cinst-hsig-change/files/stack.yaml b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/stack.yaml new file mode 100644 index 0000000000..f74ff881c8 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/stack.yaml @@ -0,0 +1,7 @@ +snapshot: ghc-9.10.3 + +packages: +- consumer-pkg +- impl-pkg +- logger-sig +- str-sig diff --git a/tests/integration/tests/backpack-stale-cinst-hsig-change/files/str-sig/package.yaml b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/str-sig/package.yaml new file mode 100644 index 0000000000..a6b1796ff1 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/str-sig/package.yaml @@ -0,0 +1,11 @@ +spec-version: 0.36.0 + +name: str-sig + +dependencies: +- base + +library: + source-dirs: src + signatures: + - Str diff --git a/tests/integration/tests/backpack-stale-cinst-hsig-change/files/str-sig/src/Str.hsig b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/str-sig/src/Str.hsig new file mode 100644 index 0000000000..edb56a4a7d --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-hsig-change/files/str-sig/src/Str.hsig @@ -0,0 +1,3 @@ +signature Str where + +greeting :: String diff --git a/tests/integration/tests/backpack-stale-cinst-impl-change/Main.hs b/tests/integration/tests/backpack-stale-cinst-impl-change/Main.hs new file mode 100644 index 0000000000..634f84606f --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-impl-change/Main.hs @@ -0,0 +1,61 @@ +-- Regression test for stale Backpack instantiation state when the concrete +-- implementation package changes. The signature packages are clean, but their +-- concrete instantiations depend on impl-pkg's modules. + +import Control.Monad ( unless ) +import Data.List ( isInfixOf ) +import System.Directory ( removeFile ) +import StackTest + +main :: IO () +main = do + -- Build all four packages. This exercises: + -- 1. str-sig CLib (indefinite, typecheck-only) + -- 2. impl-pkg CLib (concrete Str + Logger) + -- 3. str-sig CInst (instantiation with impl-pkg's Str) + -- 4. logger-sig CLib (indefinite, typecheck-only, inherits Str hole) + -- 5. logger-sig CInst (fills BOTH Logger and Str holes) + -- 6. consumer-pkg CLib + CExe + stack ["build"] + + -- Verify the consumer executable calls through the transitive chain + stackCheckStdout ["exec", "consumer-demo"] $ \out -> + unless ("[LOG] Hello from transitive chain" `isInfixOf` out) $ + error $ "Expected '[LOG] Hello from transitive chain' in output, got: " + ++ show out + + replaceFile "impl-pkg/src/Str.hs" $ unlines + [ "module Str where" + , "" + , "greeting :: String" + , "greeting = \"Hello from changed implementation\"" + ] + + -- Rebuild should succeed because only the concrete implementation changed. + stackCheckStderr ["build"] $ \err -> + expectBuildLine "impl-pkg" "Compiling Str" err + + -- Verify output reflects the changed implementation after rebuild + stackCheckStdout ["exec", "consumer-demo"] $ \out -> + unless ("[LOG] Hello from changed implementation" `isInfixOf` out) $ + error $ + "Expected changed impl-pkg output after rebuild, got: " + ++ show out + +replaceFile :: FilePath -> String -> IO () +replaceFile file contents = do + removeFile file + writeFile file contents + +expectBuildLine :: String -> String -> String -> IO () +expectBuildLine package marker err = + unless (any matches $ lines err) $ + error $ + "Expected build output line containing " + ++ show package + ++ " and " + ++ show marker + ++ ", got stderr: " + ++ show err + where + matches line = package `isInfixOf` line && marker `isInfixOf` line diff --git a/tests/integration/tests/backpack-stale-cinst-impl-change/files/.gitignore b/tests/integration/tests/backpack-stale-cinst-impl-change/files/.gitignore new file mode 100644 index 0000000000..1c91379fd4 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-impl-change/files/.gitignore @@ -0,0 +1,4 @@ +consumer-pkg.cabal +impl-pkg.cabal +logger-sig.cabal +str-sig.cabal diff --git a/tests/integration/tests/backpack-stale-cinst-impl-change/files/consumer-pkg/app/Main.hs b/tests/integration/tests/backpack-stale-cinst-impl-change/files/consumer-pkg/app/Main.hs new file mode 100644 index 0000000000..4c26592390 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-impl-change/files/consumer-pkg/app/Main.hs @@ -0,0 +1,6 @@ +module Main where + +import Consumer (hello) + +main :: IO () +main = putStrLn hello diff --git a/tests/integration/tests/backpack-stale-cinst-impl-change/files/consumer-pkg/package.yaml b/tests/integration/tests/backpack-stale-cinst-impl-change/files/consumer-pkg/package.yaml new file mode 100644 index 0000000000..ba65db2645 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-impl-change/files/consumer-pkg/package.yaml @@ -0,0 +1,24 @@ +spec-version: 0.36.0 + +name: consumer-pkg + +dependencies: +- base + +library: + source-dirs: src + dependencies: + - name: str-sig + mixin: + - requires (Str as Str) + - name: logger-sig + mixin: + - requires (Logger as Logger) + - impl-pkg + +executables: + consumer-demo: + source-dirs: app + main: Main.hs + dependencies: + - consumer-pkg diff --git a/tests/integration/tests/backpack-stale-cinst-impl-change/files/consumer-pkg/src/Consumer.hs b/tests/integration/tests/backpack-stale-cinst-impl-change/files/consumer-pkg/src/Consumer.hs new file mode 100644 index 0000000000..e46c8658a6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-impl-change/files/consumer-pkg/src/Consumer.hs @@ -0,0 +1,6 @@ +module Consumer where + +import LogHelper (greetWithLog) + +hello :: String +hello = greetWithLog diff --git a/tests/integration/tests/backpack-stale-cinst-impl-change/files/impl-pkg/package.yaml b/tests/integration/tests/backpack-stale-cinst-impl-change/files/impl-pkg/package.yaml new file mode 100644 index 0000000000..bd4497b3d7 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-impl-change/files/impl-pkg/package.yaml @@ -0,0 +1,9 @@ +spec-version: 0.36.0 + +name: impl-pkg + +dependencies: +- base + +library: + source-dirs: src diff --git a/tests/integration/tests/backpack-stale-cinst-impl-change/files/impl-pkg/src/Logger.hs b/tests/integration/tests/backpack-stale-cinst-impl-change/files/impl-pkg/src/Logger.hs new file mode 100644 index 0000000000..ddc7981a34 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-impl-change/files/impl-pkg/src/Logger.hs @@ -0,0 +1,4 @@ +module Logger where + +logMessage :: String -> String +logMessage msg = "[LOG] " ++ msg diff --git a/tests/integration/tests/backpack-stale-cinst-impl-change/files/impl-pkg/src/Str.hs b/tests/integration/tests/backpack-stale-cinst-impl-change/files/impl-pkg/src/Str.hs new file mode 100644 index 0000000000..3b6d306620 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-impl-change/files/impl-pkg/src/Str.hs @@ -0,0 +1,4 @@ +module Str where + +greeting :: String +greeting = "Hello from transitive chain" diff --git a/tests/integration/tests/backpack-stale-cinst-impl-change/files/logger-sig/package.yaml b/tests/integration/tests/backpack-stale-cinst-impl-change/files/logger-sig/package.yaml new file mode 100644 index 0000000000..b9db28324f --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-impl-change/files/logger-sig/package.yaml @@ -0,0 +1,14 @@ +spec-version: 0.36.0 + +name: logger-sig + +dependencies: +- base + +library: + source-dirs: src + signatures: Logger + dependencies: + - name: str-sig + mixin: + - requires (Str as Str) diff --git a/tests/integration/tests/backpack-stale-cinst-impl-change/files/logger-sig/src/LogHelper.hs b/tests/integration/tests/backpack-stale-cinst-impl-change/files/logger-sig/src/LogHelper.hs new file mode 100644 index 0000000000..2eecba92d6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-impl-change/files/logger-sig/src/LogHelper.hs @@ -0,0 +1,7 @@ +module LogHelper where + +import Str (greeting) +import Logger (logMessage) + +greetWithLog :: String +greetWithLog = logMessage greeting diff --git a/tests/integration/tests/backpack-stale-cinst-impl-change/files/logger-sig/src/Logger.hsig b/tests/integration/tests/backpack-stale-cinst-impl-change/files/logger-sig/src/Logger.hsig new file mode 100644 index 0000000000..446efe6fe6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-impl-change/files/logger-sig/src/Logger.hsig @@ -0,0 +1,3 @@ +signature Logger where + +logMessage :: String -> String diff --git a/tests/integration/tests/backpack-stale-cinst-impl-change/files/stack.yaml b/tests/integration/tests/backpack-stale-cinst-impl-change/files/stack.yaml new file mode 100644 index 0000000000..f74ff881c8 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-impl-change/files/stack.yaml @@ -0,0 +1,7 @@ +snapshot: ghc-9.10.3 + +packages: +- consumer-pkg +- impl-pkg +- logger-sig +- str-sig diff --git a/tests/integration/tests/backpack-stale-cinst-impl-change/files/str-sig/package.yaml b/tests/integration/tests/backpack-stale-cinst-impl-change/files/str-sig/package.yaml new file mode 100644 index 0000000000..a6b1796ff1 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-impl-change/files/str-sig/package.yaml @@ -0,0 +1,11 @@ +spec-version: 0.36.0 + +name: str-sig + +dependencies: +- base + +library: + source-dirs: src + signatures: + - Str diff --git a/tests/integration/tests/backpack-stale-cinst-impl-change/files/str-sig/src/Str.hsig b/tests/integration/tests/backpack-stale-cinst-impl-change/files/str-sig/src/Str.hsig new file mode 100644 index 0000000000..edb56a4a7d --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-impl-change/files/str-sig/src/Str.hsig @@ -0,0 +1,3 @@ +signature Str where + +greeting :: String diff --git a/tests/integration/tests/backpack-stale-cinst-inherited-hole/Main.hs b/tests/integration/tests/backpack-stale-cinst-inherited-hole/Main.hs new file mode 100644 index 0000000000..366b7e21ea --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-inherited-hole/Main.hs @@ -0,0 +1,66 @@ +-- Regression test for stale Backpack instantiation state when the final +-- Backpack consumer changes and one required hole is inherited. consumer-pkg +-- mixes in only logger-sig; logger-sig depends on str-sig, so Stack must load +-- both clean installed signature packages to instantiate logger-sig with +-- Logger and Str. + +import Control.Monad ( unless ) +import Data.List ( isInfixOf ) +import System.Directory ( removeFile ) +import StackTest + +main :: IO () +main = do + -- Build all four packages. This exercises: + -- 1. str-sig CLib (indefinite, typecheck-only) + -- 2. impl-pkg CLib (concrete Str + Logger) + -- 3. logger-sig CLib (indefinite, typecheck-only, inherits Str hole) + -- 4. logger-sig CInst (fills BOTH Logger and Str holes) + -- 5. consumer-pkg CLib + CExe + stack ["build"] + + -- Verify the consumer executable calls through the transitive chain + stackCheckStdout ["exec", "consumer-demo"] $ \out -> + unless ("[LOG] Hello from transitive chain" `isInfixOf` out) $ + error $ "Expected '[LOG] Hello from transitive chain' in output, got: " + ++ show out + + replaceFile "consumer-pkg/src/Consumer.hs" $ unlines + [ "module Consumer where" + , "" + , "import LogHelper (greetWithLog)" + , "" + , "hello :: String" + , "hello = greetWithLog ++ \" after consumer edit\"" + ] + + -- Rebuild should succeed because Stack loads logger-sig and its transitive + -- indefinite dependency str-sig as templates for the CInst task. + stackCheckStderr ["build"] $ \err -> do + expectBuildLine "logger-sig" "Str = impl-pkg" err + expectBuildLine "consumer-pkg" "Compiling Consumer" err + + -- Verify output still correct after rebuild + stackCheckStdout ["exec", "consumer-demo"] $ \out -> + unless ("[LOG] Hello from transitive chain after consumer edit" `isInfixOf` out) $ + error $ + "Expected edited consumer-pkg output after rebuild, got: " + ++ show out + +replaceFile :: FilePath -> String -> IO () +replaceFile file contents = do + removeFile file + writeFile file contents + +expectBuildLine :: String -> String -> String -> IO () +expectBuildLine package marker err = + unless (any matches $ lines err) $ + error $ + "Expected build output line containing " + ++ show package + ++ " and " + ++ show marker + ++ ", got stderr: " + ++ show err + where + matches line = package `isInfixOf` line && marker `isInfixOf` line diff --git a/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/.gitignore b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/.gitignore new file mode 100644 index 0000000000..1c91379fd4 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/.gitignore @@ -0,0 +1,4 @@ +consumer-pkg.cabal +impl-pkg.cabal +logger-sig.cabal +str-sig.cabal diff --git a/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/consumer-pkg/app/Main.hs b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/consumer-pkg/app/Main.hs new file mode 100644 index 0000000000..4c26592390 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/consumer-pkg/app/Main.hs @@ -0,0 +1,6 @@ +module Main where + +import Consumer (hello) + +main :: IO () +main = putStrLn hello diff --git a/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/consumer-pkg/package.yaml b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/consumer-pkg/package.yaml new file mode 100644 index 0000000000..c556c7e413 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/consumer-pkg/package.yaml @@ -0,0 +1,21 @@ +spec-version: 0.36.0 + +name: consumer-pkg + +dependencies: +- base + +library: + source-dirs: src + dependencies: + - name: logger-sig + mixin: + - requires (Logger as Logger) + - impl-pkg + +executables: + consumer-demo: + source-dirs: app + main: Main.hs + dependencies: + - consumer-pkg diff --git a/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/consumer-pkg/src/Consumer.hs b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/consumer-pkg/src/Consumer.hs new file mode 100644 index 0000000000..e46c8658a6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/consumer-pkg/src/Consumer.hs @@ -0,0 +1,6 @@ +module Consumer where + +import LogHelper (greetWithLog) + +hello :: String +hello = greetWithLog diff --git a/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/impl-pkg/package.yaml b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/impl-pkg/package.yaml new file mode 100644 index 0000000000..bd4497b3d7 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/impl-pkg/package.yaml @@ -0,0 +1,9 @@ +spec-version: 0.36.0 + +name: impl-pkg + +dependencies: +- base + +library: + source-dirs: src diff --git a/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/impl-pkg/src/Logger.hs b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/impl-pkg/src/Logger.hs new file mode 100644 index 0000000000..ddc7981a34 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/impl-pkg/src/Logger.hs @@ -0,0 +1,4 @@ +module Logger where + +logMessage :: String -> String +logMessage msg = "[LOG] " ++ msg diff --git a/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/impl-pkg/src/Str.hs b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/impl-pkg/src/Str.hs new file mode 100644 index 0000000000..3b6d306620 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/impl-pkg/src/Str.hs @@ -0,0 +1,4 @@ +module Str where + +greeting :: String +greeting = "Hello from transitive chain" diff --git a/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/logger-sig/package.yaml b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/logger-sig/package.yaml new file mode 100644 index 0000000000..b9db28324f --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/logger-sig/package.yaml @@ -0,0 +1,14 @@ +spec-version: 0.36.0 + +name: logger-sig + +dependencies: +- base + +library: + source-dirs: src + signatures: Logger + dependencies: + - name: str-sig + mixin: + - requires (Str as Str) diff --git a/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/logger-sig/src/LogHelper.hs b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/logger-sig/src/LogHelper.hs new file mode 100644 index 0000000000..2eecba92d6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/logger-sig/src/LogHelper.hs @@ -0,0 +1,7 @@ +module LogHelper where + +import Str (greeting) +import Logger (logMessage) + +greetWithLog :: String +greetWithLog = logMessage greeting diff --git a/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/logger-sig/src/Logger.hsig b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/logger-sig/src/Logger.hsig new file mode 100644 index 0000000000..446efe6fe6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/logger-sig/src/Logger.hsig @@ -0,0 +1,3 @@ +signature Logger where + +logMessage :: String -> String diff --git a/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/stack.yaml b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/stack.yaml new file mode 100644 index 0000000000..f74ff881c8 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/stack.yaml @@ -0,0 +1,7 @@ +snapshot: ghc-9.10.3 + +packages: +- consumer-pkg +- impl-pkg +- logger-sig +- str-sig diff --git a/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/str-sig/package.yaml b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/str-sig/package.yaml new file mode 100644 index 0000000000..a6b1796ff1 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/str-sig/package.yaml @@ -0,0 +1,11 @@ +spec-version: 0.36.0 + +name: str-sig + +dependencies: +- base + +library: + source-dirs: src + signatures: + - Str diff --git a/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/str-sig/src/Str.hsig b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/str-sig/src/Str.hsig new file mode 100644 index 0000000000..edb56a4a7d --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-inherited-hole/files/str-sig/src/Str.hsig @@ -0,0 +1,3 @@ +signature Str where + +greeting :: String diff --git a/tests/integration/tests/backpack-stale-cinst-sig-source-change/Main.hs b/tests/integration/tests/backpack-stale-cinst-sig-source-change/Main.hs new file mode 100644 index 0000000000..d678f167f4 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-sig-source-change/Main.hs @@ -0,0 +1,64 @@ +-- Regression test for stale Backpack instantiation state when an intermediate +-- indefinite package's normal source changes. logger-sig is dirty, but its +-- indefinite dependency str-sig is still clean and already installed. + +import Control.Monad ( unless ) +import Data.List ( isInfixOf ) +import System.Directory ( removeFile ) +import StackTest + +main :: IO () +main = do + -- Build all four packages. This exercises: + -- 1. str-sig CLib (indefinite, typecheck-only) + -- 2. impl-pkg CLib (concrete Str + Logger) + -- 3. str-sig CInst (instantiation with impl-pkg's Str) + -- 4. logger-sig CLib (indefinite, typecheck-only, inherits Str hole) + -- 5. logger-sig CInst (fills BOTH Logger and Str holes) + -- 6. consumer-pkg CLib + CExe + stack ["build"] + + -- Verify the consumer executable calls through the transitive chain + stackCheckStdout ["exec", "consumer-demo"] $ \out -> + unless ("[LOG] Hello from transitive chain" `isInfixOf` out) $ + error $ "Expected '[LOG] Hello from transitive chain' in output, got: " + ++ show out + + replaceFile "logger-sig/src/LogHelper.hs" $ unlines + [ "module LogHelper where" + , "" + , "import Str (greeting)" + , "import Logger (logMessage)" + , "" + , "greetWithLog :: String" + , "greetWithLog = logMessage (greeting ++ \" after source edit\")" + ] + + -- Rebuild should succeed because only logger-sig's own source changed. + stackCheckStderr ["build"] $ \err -> + expectBuildLine "logger-sig" "Compiling LogHelper" err + + -- Verify output still correct after rebuild + stackCheckStdout ["exec", "consumer-demo"] $ \out -> + unless ("[LOG] Hello from transitive chain after source edit" `isInfixOf` out) $ + error $ + "Expected edited logger-sig output after rebuild, got: " + ++ show out + +replaceFile :: FilePath -> String -> IO () +replaceFile file contents = do + removeFile file + writeFile file contents + +expectBuildLine :: String -> String -> String -> IO () +expectBuildLine package marker err = + unless (any matches $ lines err) $ + error $ + "Expected build output line containing " + ++ show package + ++ " and " + ++ show marker + ++ ", got stderr: " + ++ show err + where + matches line = package `isInfixOf` line && marker `isInfixOf` line diff --git a/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/.gitignore b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/.gitignore new file mode 100644 index 0000000000..1c91379fd4 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/.gitignore @@ -0,0 +1,4 @@ +consumer-pkg.cabal +impl-pkg.cabal +logger-sig.cabal +str-sig.cabal diff --git a/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/consumer-pkg/app/Main.hs b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/consumer-pkg/app/Main.hs new file mode 100644 index 0000000000..4c26592390 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/consumer-pkg/app/Main.hs @@ -0,0 +1,6 @@ +module Main where + +import Consumer (hello) + +main :: IO () +main = putStrLn hello diff --git a/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/consumer-pkg/package.yaml b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/consumer-pkg/package.yaml new file mode 100644 index 0000000000..ba65db2645 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/consumer-pkg/package.yaml @@ -0,0 +1,24 @@ +spec-version: 0.36.0 + +name: consumer-pkg + +dependencies: +- base + +library: + source-dirs: src + dependencies: + - name: str-sig + mixin: + - requires (Str as Str) + - name: logger-sig + mixin: + - requires (Logger as Logger) + - impl-pkg + +executables: + consumer-demo: + source-dirs: app + main: Main.hs + dependencies: + - consumer-pkg diff --git a/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/consumer-pkg/src/Consumer.hs b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/consumer-pkg/src/Consumer.hs new file mode 100644 index 0000000000..e46c8658a6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/consumer-pkg/src/Consumer.hs @@ -0,0 +1,6 @@ +module Consumer where + +import LogHelper (greetWithLog) + +hello :: String +hello = greetWithLog diff --git a/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/impl-pkg/package.yaml b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/impl-pkg/package.yaml new file mode 100644 index 0000000000..bd4497b3d7 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/impl-pkg/package.yaml @@ -0,0 +1,9 @@ +spec-version: 0.36.0 + +name: impl-pkg + +dependencies: +- base + +library: + source-dirs: src diff --git a/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/impl-pkg/src/Logger.hs b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/impl-pkg/src/Logger.hs new file mode 100644 index 0000000000..ddc7981a34 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/impl-pkg/src/Logger.hs @@ -0,0 +1,4 @@ +module Logger where + +logMessage :: String -> String +logMessage msg = "[LOG] " ++ msg diff --git a/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/impl-pkg/src/Str.hs b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/impl-pkg/src/Str.hs new file mode 100644 index 0000000000..3b6d306620 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/impl-pkg/src/Str.hs @@ -0,0 +1,4 @@ +module Str where + +greeting :: String +greeting = "Hello from transitive chain" diff --git a/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/logger-sig/package.yaml b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/logger-sig/package.yaml new file mode 100644 index 0000000000..b9db28324f --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/logger-sig/package.yaml @@ -0,0 +1,14 @@ +spec-version: 0.36.0 + +name: logger-sig + +dependencies: +- base + +library: + source-dirs: src + signatures: Logger + dependencies: + - name: str-sig + mixin: + - requires (Str as Str) diff --git a/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/logger-sig/src/LogHelper.hs b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/logger-sig/src/LogHelper.hs new file mode 100644 index 0000000000..2eecba92d6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/logger-sig/src/LogHelper.hs @@ -0,0 +1,7 @@ +module LogHelper where + +import Str (greeting) +import Logger (logMessage) + +greetWithLog :: String +greetWithLog = logMessage greeting diff --git a/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/logger-sig/src/Logger.hsig b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/logger-sig/src/Logger.hsig new file mode 100644 index 0000000000..446efe6fe6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/logger-sig/src/Logger.hsig @@ -0,0 +1,3 @@ +signature Logger where + +logMessage :: String -> String diff --git a/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/stack.yaml b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/stack.yaml new file mode 100644 index 0000000000..f74ff881c8 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/stack.yaml @@ -0,0 +1,7 @@ +snapshot: ghc-9.10.3 + +packages: +- consumer-pkg +- impl-pkg +- logger-sig +- str-sig diff --git a/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/str-sig/package.yaml b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/str-sig/package.yaml new file mode 100644 index 0000000000..a6b1796ff1 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/str-sig/package.yaml @@ -0,0 +1,11 @@ +spec-version: 0.36.0 + +name: str-sig + +dependencies: +- base + +library: + source-dirs: src + signatures: + - Str diff --git a/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/str-sig/src/Str.hsig b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/str-sig/src/Str.hsig new file mode 100644 index 0000000000..edb56a4a7d --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-sig-source-change/files/str-sig/src/Str.hsig @@ -0,0 +1,3 @@ +signature Str where + +greeting :: String diff --git a/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/Main.hs b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/Main.hs new file mode 100644 index 0000000000..71919a71ba --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/Main.hs @@ -0,0 +1,75 @@ +-- Regression test for stale Backpack instantiation state when the final +-- Backpack consumer changes and one required hole is inherited from a snapshot +-- package. consumer-pkg mixes in only logger-sig; logger-sig depends on +-- str-sig, which is installed from a local snapshot archive. Stack must use the +-- source map, not just CombinedMap, to load str-sig's signature metadata. + +import Control.Monad ( unless ) +import Data.List ( isInfixOf ) +import System.Directory ( createDirectoryIfMissing, removeFile ) +import StackTest + +main :: IO () +main = do + createDirectoryIfMissing True "snapshots" + stack + [ "--stack-yaml", "stack-bootstrap.yaml" + , "sdist", "str-sig" + , "--ignore-check" + , "--tar-dir", "snapshots" + ] + + -- Build all four packages. This exercises: + -- 1. str-sig CLib (indefinite, typecheck-only) + -- 2. impl-pkg CLib (concrete Str + Logger) + -- 3. logger-sig CLib (indefinite, typecheck-only, inherits Str hole) + -- 4. logger-sig CInst (fills BOTH Logger and Str holes) + -- 5. consumer-pkg CLib + CExe + stack ["build"] + + -- Verify the consumer executable calls through the transitive chain + stackCheckStdout ["exec", "consumer-demo"] $ \out -> + unless ("[LOG] Hello from transitive chain" `isInfixOf` out) $ + error $ "Expected '[LOG] Hello from transitive chain' in output, got: " + ++ show out + + replaceFile "consumer-pkg/src/Consumer.hs" $ unlines + [ "module Consumer where" + , "" + , "import LogHelper (greetWithLog)" + , "" + , "hello :: String" + , "hello = greetWithLog ++ \" after consumer edit\"" + ] + + -- Rebuild should succeed because Stack loads logger-sig and its transitive + -- snapshot-installed indefinite dependency str-sig as templates for the + -- CInst task. + stackCheckStderr ["build"] $ \err -> do + expectBuildLine "logger-sig" "Str = impl-pkg" err + expectBuildLine "consumer-pkg" "Compiling Consumer" err + + -- Verify output still correct after rebuild + stackCheckStdout ["exec", "consumer-demo"] $ \out -> + unless ("[LOG] Hello from transitive chain after consumer edit" `isInfixOf` out) $ + error $ + "Expected edited consumer-pkg output after rebuild, got: " + ++ show out + +replaceFile :: FilePath -> String -> IO () +replaceFile file contents = do + removeFile file + writeFile file contents + +expectBuildLine :: String -> String -> String -> IO () +expectBuildLine package marker err = + unless (any matches $ lines err) $ + error $ + "Expected build output line containing " + ++ show package + ++ " and " + ++ show marker + ++ ", got stderr: " + ++ show err + where + matches line = package `isInfixOf` line && marker `isInfixOf` line diff --git a/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/.gitignore b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/.gitignore new file mode 100644 index 0000000000..83939e06e4 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/.gitignore @@ -0,0 +1,7 @@ +consumer-pkg.cabal +impl-pkg.cabal +logger-sig.cabal +str-sig.cabal +snapshots/*.tar.gz +stack.yaml.lock +snapshot.yaml.lock diff --git a/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/consumer-pkg/app/Main.hs b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/consumer-pkg/app/Main.hs new file mode 100644 index 0000000000..4c26592390 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/consumer-pkg/app/Main.hs @@ -0,0 +1,6 @@ +module Main where + +import Consumer (hello) + +main :: IO () +main = putStrLn hello diff --git a/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/consumer-pkg/package.yaml b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/consumer-pkg/package.yaml new file mode 100644 index 0000000000..c556c7e413 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/consumer-pkg/package.yaml @@ -0,0 +1,21 @@ +spec-version: 0.36.0 + +name: consumer-pkg + +dependencies: +- base + +library: + source-dirs: src + dependencies: + - name: logger-sig + mixin: + - requires (Logger as Logger) + - impl-pkg + +executables: + consumer-demo: + source-dirs: app + main: Main.hs + dependencies: + - consumer-pkg diff --git a/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/consumer-pkg/src/Consumer.hs b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/consumer-pkg/src/Consumer.hs new file mode 100644 index 0000000000..e46c8658a6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/consumer-pkg/src/Consumer.hs @@ -0,0 +1,6 @@ +module Consumer where + +import LogHelper (greetWithLog) + +hello :: String +hello = greetWithLog diff --git a/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/impl-pkg/package.yaml b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/impl-pkg/package.yaml new file mode 100644 index 0000000000..bd4497b3d7 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/impl-pkg/package.yaml @@ -0,0 +1,9 @@ +spec-version: 0.36.0 + +name: impl-pkg + +dependencies: +- base + +library: + source-dirs: src diff --git a/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/impl-pkg/src/Logger.hs b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/impl-pkg/src/Logger.hs new file mode 100644 index 0000000000..ddc7981a34 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/impl-pkg/src/Logger.hs @@ -0,0 +1,4 @@ +module Logger where + +logMessage :: String -> String +logMessage msg = "[LOG] " ++ msg diff --git a/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/impl-pkg/src/Str.hs b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/impl-pkg/src/Str.hs new file mode 100644 index 0000000000..3b6d306620 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/impl-pkg/src/Str.hs @@ -0,0 +1,4 @@ +module Str where + +greeting :: String +greeting = "Hello from transitive chain" diff --git a/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/logger-sig/package.yaml b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/logger-sig/package.yaml new file mode 100644 index 0000000000..b9db28324f --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/logger-sig/package.yaml @@ -0,0 +1,14 @@ +spec-version: 0.36.0 + +name: logger-sig + +dependencies: +- base + +library: + source-dirs: src + signatures: Logger + dependencies: + - name: str-sig + mixin: + - requires (Str as Str) diff --git a/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/logger-sig/src/LogHelper.hs b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/logger-sig/src/LogHelper.hs new file mode 100644 index 0000000000..2eecba92d6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/logger-sig/src/LogHelper.hs @@ -0,0 +1,7 @@ +module LogHelper where + +import Str (greeting) +import Logger (logMessage) + +greetWithLog :: String +greetWithLog = logMessage greeting diff --git a/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/logger-sig/src/Logger.hsig b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/logger-sig/src/Logger.hsig new file mode 100644 index 0000000000..446efe6fe6 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/logger-sig/src/Logger.hsig @@ -0,0 +1,3 @@ +signature Logger where + +logMessage :: String -> String diff --git a/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/snapshot.yaml b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/snapshot.yaml new file mode 100644 index 0000000000..54532d2e55 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/snapshot.yaml @@ -0,0 +1,6 @@ +name: backpack-stale-cinst-snapshot + +snapshot: ghc-9.10.3 + +packages: +- archive: snapshots/str-sig-0.0.0.tar.gz diff --git a/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/stack-bootstrap.yaml b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/stack-bootstrap.yaml new file mode 100644 index 0000000000..7fb56effb0 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/stack-bootstrap.yaml @@ -0,0 +1,4 @@ +snapshot: ghc-9.10.3 + +packages: +- str-sig diff --git a/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/stack.yaml b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/stack.yaml new file mode 100644 index 0000000000..6d3992aabe --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/stack.yaml @@ -0,0 +1,6 @@ +snapshot: snapshot.yaml + +packages: +- consumer-pkg +- impl-pkg +- logger-sig diff --git a/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/str-sig/package.yaml b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/str-sig/package.yaml new file mode 100644 index 0000000000..a6b1796ff1 --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/str-sig/package.yaml @@ -0,0 +1,11 @@ +spec-version: 0.36.0 + +name: str-sig + +dependencies: +- base + +library: + source-dirs: src + signatures: + - Str diff --git a/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/str-sig/src/Str.hsig b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/str-sig/src/Str.hsig new file mode 100644 index 0000000000..edb56a4a7d --- /dev/null +++ b/tests/integration/tests/backpack-stale-cinst-snapshot-inherited-hole/files/str-sig/src/Str.hsig @@ -0,0 +1,3 @@ +signature Str where + +greeting :: String diff --git a/tests/unit/Stack/Build/ConstructPlanSpec.hs b/tests/unit/Stack/Build/ConstructPlanSpec.hs index c57d34cc90..b19513bf2d 100644 --- a/tests/unit/Stack/Build/ConstructPlanSpec.hs +++ b/tests/unit/Stack/Build/ConstructPlanSpec.hs @@ -28,7 +28,7 @@ import Distribution.Types.PackageName ( mkPackageName ) import Distribution.Types.Version ( mkVersion ) import Distribution.Types.VersionRange ( anyVersion ) import Distribution.ModuleName ( ModuleName ) -import Stack.Build.Backpack ( addInstantiationTasks ) +import qualified Stack.Build.Backpack as Backpack import Stack.Build.ConstructPlan ( shouldSplitComponents ) import Stack.Build.ExecutePackage ( findGhcPkgId, mkInstantiateWithOpts ) @@ -71,6 +71,14 @@ import Test.Hspec , shouldBe, shouldSatisfy ) +addInstantiationTasks :: + Map PackageName (Set ModuleName) + -> [(PackageName, AddDepRes)] + -> [(ComponentKey, AddDepRes)] + -> ([(ComponentKey, AddDepRes)], [StyleDoc]) +addInstantiationTasks installedModules = + Backpack.addInstantiationTasks installedModules Map.empty + main :: IO () main = hspec spec From 9f987a6c93a3636149332d5bac13b9d070af6bd0 Mon Sep 17 00:00:00 2001 From: philippedev101 Date: Thu, 23 Jul 2026 22:01:14 +0200 Subject: [PATCH 2/2] Fix Backpack lint findings --- src/Stack/Build/Backpack.hs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Stack/Build/Backpack.hs b/src/Stack/Build/Backpack.hs index 5a99e30aba..c19fdcbf9a 100644 --- a/src/Stack/Build/Backpack.hs +++ b/src/Stack/Build/Backpack.hs @@ -22,7 +22,6 @@ module Stack.Build.Backpack import Crypto.Hash ( hashWith, SHA256 (..) ) import qualified Data.ByteArray.Encoding as Mem ( Base (Base16), convertToBase ) -import qualified Data.ByteString.Char8 as S8 import qualified Data.List as L import qualified Data.Map.Strict as Map import qualified Data.Set as Set @@ -264,7 +263,7 @@ loadFoundIndefiniteTasks loadPkg econfig sources combinedMap bco adrs = Package -> MissingPresentDeps templateDeps package = - either (const mempty) id $ + fromRight mempty $ runIdentity $ processPackageDepsEither package $ \depName _depValue -> pure (Right (case Map.lookup depName adrMap of @@ -399,7 +398,7 @@ addInstantiationTasks installedModules foundIndefiniteTasks origAdrs expandedAdr inheritedSigs = -- Deduplicate: diamond deps can yield the same sig -- from multiple paths. - L.nub $ collectInheritedSigs sigPkg adrMap' Set.empty + nubOrd $ collectInheritedSigs sigPkg adrMap' Set.empty -- Determine the module mapping from the mixin's requiresRn. renaming = includeRequiresRn (mixinIncludeRenaming mixin) -- Resolve the sig-pkg's own signatures using the consumer's @@ -656,7 +655,7 @@ addInstantiationTasks installedModules foundIndefiniteTasks origAdrs expandedAdr ++ ":" ++ CabalText.display implMod | (sig, implPkg, implMod) <- entries ] - input = S8.pack (L.intercalate "," sorted) + input = encodeUtf8 $ T.pack $ L.intercalate "," sorted digest = hashWith SHA256 input -- Take first 16 hex chars for a short but unique-enough suffix. hexStr = Mem.convertToBase Mem.Base16 digest :: ByteString