From 7ade5c641f3a36d1a9a5da8d1e07ef0842f9e323 Mon Sep 17 00:00:00 2001 From: philippedev101 Date: Sat, 21 Mar 2026 14:21:16 +0100 Subject: [PATCH] Add Backpack (cross-package) support to Stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement full support for GHC's Backpack module system, addressing the long-standing request in issue #2540 (open since 2016). Phase 1 — Intra-package component ordering: Detect sub-library self-dependencies (the Backpack pattern) and skip per-component splitting for those packages, preserving Cabal's own component ordering. Phase 2 — Component-keyed build plan: Replace the per-package build plan with a per-component plan using ComponentKey (PackageName, UnqualCompName). Each library, executable, test, and benchmark gets its own entry in the plan, enabling fine-grained dependency tracking between components across packages. Phase 3 — Cross-package Backpack instantiation: When a consumer package uses mixins/signatures to depend on an indefinite (signature-only) package, Stack now automatically creates CInst instantiation tasks that compile the indefinite package against the concrete implementation. This includes: - Preserving Backpack metadata (signatures, mixins) through the plan - Detecting indefinite packages and creating CInst build tasks - Configuring CInst tasks with --instantiate-with flags - Module resolution scoped to consumer's build-depends - Transitive chain support (inherited signatures from indefinite deps) - Multiple instantiations with different implementations (deduplicated) - Sub-library mixin and module resolution - Remote/snapshot indefinite packages (loaded from Hackage/Pantry) - HidingRenaming support (no partial instantiation; hides propagate) - Per-CInst config cache (ConfigCacheTypeInstantiation) - Precompiled cache support for CInst tasks - Haddock generation for instantiated packages - Build output showing instantiation details Test coverage: 103 unit tests (ConstructPlanSpec), 8 integration tests (backpack-private, backpack-sublib-deps, backpack-cross-package-sublib, backpack-cross-package-sig, backpack-cross-package-rename, backpack-cross-package-transitive, backpack-cross-package-multi-inst, per-component-build). Documentation: new Backpack topic page, ChangeLog entry, and cross-references from tutorial pages. --- ChangeLog.md | 11 + doc/topics/backpack.md | 246 ++ doc/tutorial/multi-package_projects.md | 5 + doc/tutorial/package_description.md | 4 + mkdocs.yml | 1 + src/Control/Concurrent/Execute.hs | 14 +- src/Stack/Build.hs | 18 +- src/Stack/Build/Backpack.hs | 589 ++++ src/Stack/Build/Cache.hs | 16 +- src/Stack/Build/ConstructPlan.hs | 181 +- src/Stack/Build/Execute.hs | 188 +- src/Stack/Build/ExecuteEnv.hs | 18 +- src/Stack/Build/ExecutePackage.hs | 343 ++- src/Stack/Build/Target.hs | 1 + src/Stack/Component.hs | 2 + src/Stack/ComponentFile.hs | 4 +- src/Stack/Package.hs | 34 + src/Stack/SDist.hs | 2 +- src/Stack/Types/Build/ConstructPlan.hs | 8 +- src/Stack/Types/Build/Exception.hs | 3 - src/Stack/Types/Cache.hs | 8 +- src/Stack/Types/Component.hs | 5 + src/Stack/Types/GhcPkgId.hs | 3 +- src/Stack/Types/NamedComponent.hs | 13 +- src/Stack/Types/Plan.hs | 30 +- stack.cabal | 3 + .../backpack-cross-package-multi-inst/Main.hs | 24 + .../files/app/app.cabal | 13 + .../files/app/app/Main.hs | 9 + .../files/consumer-a/consumer-a.cabal | 15 + .../files/consumer-a/src/ConsumerA.hs | 6 + .../files/consumer-b/consumer-b.cabal | 15 + .../files/consumer-b/src/ConsumerB.hs | 6 + .../files/impl-a/impl-a.cabal | 10 + .../files/impl-a/src/Str.hs | 4 + .../files/impl-b/impl-b.cabal | 10 + .../files/impl-b/src/Str.hs | 4 + .../files/sig-pkg/sig-pkg.cabal | 10 + .../files/sig-pkg/src/Str.hsig | 3 + .../files/stack.yaml | 8 + .../backpack-cross-package-rename/Main.hs | 14 + .../files/consumer-pkg/app/Main.hs | 6 + .../files/consumer-pkg/consumer-pkg.cabal | 23 + .../files/consumer-pkg/src/Consumer.hs | 6 + .../files/impl-pkg/impl-pkg.cabal | 10 + .../files/impl-pkg/src/Impl.hs | 4 + .../files/sig-pkg/sig-pkg.cabal | 10 + .../files/sig-pkg/src/Sig.hsig | 3 + .../files/stack.yaml | 5 + .../tests/backpack-cross-package-sig/Main.hs | 27 + .../files/consumer-pkg/app/Main.hs | 6 + .../files/consumer-pkg/consumer-pkg.cabal | 23 + .../files/consumer-pkg/src/Consumer.hs | 6 + .../files/impl-pkg/impl-pkg.cabal | 10 + .../files/impl-pkg/src/Str.hs | 4 + .../files/sig-pkg/sig-pkg.cabal | 10 + .../files/sig-pkg/src/Str.hsig | 3 + .../files/stack.yaml | 5 + .../backpack-cross-package-sublib/Main.hs | 7 + .../files/consumer/consumer.cabal | 22 + .../files/consumer/src/exe/Main.hs | 6 + .../files/consumer/src/main/Consumer.hs | 10 + .../files/provider/provider.cabal | 21 + .../files/provider/src/main/Provider.hs | 6 + .../provider/src/utils/Provider/Utils.hs | 7 + .../files/stack.yaml | 4 + .../backpack-cross-package-transitive/Main.hs | 32 + .../files/consumer-pkg/app/Main.hs | 6 + .../files/consumer-pkg/consumer-pkg.cabal | 25 + .../files/consumer-pkg/src/Consumer.hs | 6 + .../files/impl-pkg/impl-pkg.cabal | 10 + .../files/impl-pkg/src/Logger.hs | 4 + .../files/impl-pkg/src/Str.hs | 4 + .../files/logger-sig/logger-sig.cabal | 15 + .../files/logger-sig/src/LogHelper.hs | 7 + .../files/logger-sig/src/Logger.hsig | 3 + .../files/stack.yaml | 6 + .../files/str-sig/src/Str.hsig | 3 + .../files/str-sig/str-sig.cabal | 10 + .../tests/backpack-private/Main.hs | 11 + .../files/private-backpack.cabal | 37 + .../backpack-private/files/src/exe/Main.hs | 6 + .../backpack-private/files/src/impl/Str.hs | 16 + .../files/src/main/PrivateBackpack.hs | 8 + .../backpack-private/files/src/sig/Str.hsig | 7 + .../tests/backpack-private/files/stack.yaml | 3 + .../tests/backpack-sublib-deps/Main.hs | 7 + .../files/src/core/SublibDeps/Core.hs | 4 + .../files/src/exe/Main.hs | 6 + .../files/src/extended/SublibDeps/Extended.hs | 10 + .../files/src/main/SublibDeps.hs | 3 + .../backpack-sublib-deps/files/stack.yaml | 3 + .../files/sublib-deps.cabal | 37 + .../tests/per-component-build/Main.hs | 33 + .../per-component-build/files/app1/Main.hs | 6 + .../per-component-build/files/app2/Main.hs | 6 + .../files/per-component-build.cabal | 30 + .../per-component-build/files/src/Lib.hs | 4 + .../per-component-build/files/stack.yaml | 3 + .../per-component-build/files/test/Main.hs | 9 + tests/unit/Stack/Build/ConstructPlanSpec.hs | 2630 +++++++++++++++++ tests/unit/Stack/Build/ExecuteSpec.hs | 536 +++- tests/unit/Stack/PackageDumpSpec.hs | 22 +- tests/unit/Stack/Types/PlanSpec.hs | 78 + 104 files changed, 5540 insertions(+), 251 deletions(-) create mode 100644 doc/topics/backpack.md create mode 100644 src/Stack/Build/Backpack.hs create mode 100644 tests/integration/tests/backpack-cross-package-multi-inst/Main.hs create mode 100644 tests/integration/tests/backpack-cross-package-multi-inst/files/app/app.cabal create mode 100644 tests/integration/tests/backpack-cross-package-multi-inst/files/app/app/Main.hs create mode 100644 tests/integration/tests/backpack-cross-package-multi-inst/files/consumer-a/consumer-a.cabal create mode 100644 tests/integration/tests/backpack-cross-package-multi-inst/files/consumer-a/src/ConsumerA.hs create mode 100644 tests/integration/tests/backpack-cross-package-multi-inst/files/consumer-b/consumer-b.cabal create mode 100644 tests/integration/tests/backpack-cross-package-multi-inst/files/consumer-b/src/ConsumerB.hs create mode 100644 tests/integration/tests/backpack-cross-package-multi-inst/files/impl-a/impl-a.cabal create mode 100644 tests/integration/tests/backpack-cross-package-multi-inst/files/impl-a/src/Str.hs create mode 100644 tests/integration/tests/backpack-cross-package-multi-inst/files/impl-b/impl-b.cabal create mode 100644 tests/integration/tests/backpack-cross-package-multi-inst/files/impl-b/src/Str.hs create mode 100644 tests/integration/tests/backpack-cross-package-multi-inst/files/sig-pkg/sig-pkg.cabal create mode 100644 tests/integration/tests/backpack-cross-package-multi-inst/files/sig-pkg/src/Str.hsig create mode 100644 tests/integration/tests/backpack-cross-package-multi-inst/files/stack.yaml create mode 100644 tests/integration/tests/backpack-cross-package-rename/Main.hs create mode 100644 tests/integration/tests/backpack-cross-package-rename/files/consumer-pkg/app/Main.hs create mode 100644 tests/integration/tests/backpack-cross-package-rename/files/consumer-pkg/consumer-pkg.cabal create mode 100644 tests/integration/tests/backpack-cross-package-rename/files/consumer-pkg/src/Consumer.hs create mode 100644 tests/integration/tests/backpack-cross-package-rename/files/impl-pkg/impl-pkg.cabal create mode 100644 tests/integration/tests/backpack-cross-package-rename/files/impl-pkg/src/Impl.hs create mode 100644 tests/integration/tests/backpack-cross-package-rename/files/sig-pkg/sig-pkg.cabal create mode 100644 tests/integration/tests/backpack-cross-package-rename/files/sig-pkg/src/Sig.hsig create mode 100644 tests/integration/tests/backpack-cross-package-rename/files/stack.yaml create mode 100644 tests/integration/tests/backpack-cross-package-sig/Main.hs create mode 100644 tests/integration/tests/backpack-cross-package-sig/files/consumer-pkg/app/Main.hs create mode 100644 tests/integration/tests/backpack-cross-package-sig/files/consumer-pkg/consumer-pkg.cabal create mode 100644 tests/integration/tests/backpack-cross-package-sig/files/consumer-pkg/src/Consumer.hs create mode 100644 tests/integration/tests/backpack-cross-package-sig/files/impl-pkg/impl-pkg.cabal create mode 100644 tests/integration/tests/backpack-cross-package-sig/files/impl-pkg/src/Str.hs create mode 100644 tests/integration/tests/backpack-cross-package-sig/files/sig-pkg/sig-pkg.cabal create mode 100644 tests/integration/tests/backpack-cross-package-sig/files/sig-pkg/src/Str.hsig create mode 100644 tests/integration/tests/backpack-cross-package-sig/files/stack.yaml create mode 100644 tests/integration/tests/backpack-cross-package-sublib/Main.hs create mode 100644 tests/integration/tests/backpack-cross-package-sublib/files/consumer/consumer.cabal create mode 100644 tests/integration/tests/backpack-cross-package-sublib/files/consumer/src/exe/Main.hs create mode 100644 tests/integration/tests/backpack-cross-package-sublib/files/consumer/src/main/Consumer.hs create mode 100644 tests/integration/tests/backpack-cross-package-sublib/files/provider/provider.cabal create mode 100644 tests/integration/tests/backpack-cross-package-sublib/files/provider/src/main/Provider.hs create mode 100644 tests/integration/tests/backpack-cross-package-sublib/files/provider/src/utils/Provider/Utils.hs create mode 100644 tests/integration/tests/backpack-cross-package-sublib/files/stack.yaml create mode 100644 tests/integration/tests/backpack-cross-package-transitive/Main.hs create mode 100644 tests/integration/tests/backpack-cross-package-transitive/files/consumer-pkg/app/Main.hs create mode 100644 tests/integration/tests/backpack-cross-package-transitive/files/consumer-pkg/consumer-pkg.cabal create mode 100644 tests/integration/tests/backpack-cross-package-transitive/files/consumer-pkg/src/Consumer.hs create mode 100644 tests/integration/tests/backpack-cross-package-transitive/files/impl-pkg/impl-pkg.cabal create mode 100644 tests/integration/tests/backpack-cross-package-transitive/files/impl-pkg/src/Logger.hs create mode 100644 tests/integration/tests/backpack-cross-package-transitive/files/impl-pkg/src/Str.hs create mode 100644 tests/integration/tests/backpack-cross-package-transitive/files/logger-sig/logger-sig.cabal create mode 100644 tests/integration/tests/backpack-cross-package-transitive/files/logger-sig/src/LogHelper.hs create mode 100644 tests/integration/tests/backpack-cross-package-transitive/files/logger-sig/src/Logger.hsig create mode 100644 tests/integration/tests/backpack-cross-package-transitive/files/stack.yaml create mode 100644 tests/integration/tests/backpack-cross-package-transitive/files/str-sig/src/Str.hsig create mode 100644 tests/integration/tests/backpack-cross-package-transitive/files/str-sig/str-sig.cabal create mode 100644 tests/integration/tests/backpack-private/Main.hs create mode 100644 tests/integration/tests/backpack-private/files/private-backpack.cabal create mode 100644 tests/integration/tests/backpack-private/files/src/exe/Main.hs create mode 100644 tests/integration/tests/backpack-private/files/src/impl/Str.hs create mode 100644 tests/integration/tests/backpack-private/files/src/main/PrivateBackpack.hs create mode 100644 tests/integration/tests/backpack-private/files/src/sig/Str.hsig create mode 100644 tests/integration/tests/backpack-private/files/stack.yaml create mode 100644 tests/integration/tests/backpack-sublib-deps/Main.hs create mode 100644 tests/integration/tests/backpack-sublib-deps/files/src/core/SublibDeps/Core.hs create mode 100644 tests/integration/tests/backpack-sublib-deps/files/src/exe/Main.hs create mode 100644 tests/integration/tests/backpack-sublib-deps/files/src/extended/SublibDeps/Extended.hs create mode 100644 tests/integration/tests/backpack-sublib-deps/files/src/main/SublibDeps.hs create mode 100644 tests/integration/tests/backpack-sublib-deps/files/stack.yaml create mode 100644 tests/integration/tests/backpack-sublib-deps/files/sublib-deps.cabal create mode 100644 tests/integration/tests/per-component-build/Main.hs create mode 100644 tests/integration/tests/per-component-build/files/app1/Main.hs create mode 100644 tests/integration/tests/per-component-build/files/app2/Main.hs create mode 100644 tests/integration/tests/per-component-build/files/per-component-build.cabal create mode 100644 tests/integration/tests/per-component-build/files/src/Lib.hs create mode 100644 tests/integration/tests/per-component-build/files/stack.yaml create mode 100644 tests/integration/tests/per-component-build/files/test/Main.hs create mode 100644 tests/unit/Stack/Build/ConstructPlanSpec.hs create mode 100644 tests/unit/Stack/Types/PlanSpec.hs diff --git a/ChangeLog.md b/ChangeLog.md index 034489bd67..9f9e908edd 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -8,6 +8,17 @@ Release notes: Major changes: +* Stack now supports GHC's Backpack module system for cross-package use. When a + package uses `signatures` and `mixins` to depend on an abstract interface + provided by another package, Stack automatically creates the extra + instantiation build steps that Cabal requires. This includes support for + explicit renaming, multiple instantiations of the same indefinite package, + transitive signature chains, sub-library signatures, and indefinite packages + from Hackage or snapshots. See the + [Backpack topic](https://docs.haskellstack.org/en/stable/topics/backpack/) for + details. (Private Backpack — where signatures and implementations live in the + same package — has always worked without changes.) + Behavior changes: * Stack's default Nix integration now includes the `cacert` Nix package, in diff --git a/doc/topics/backpack.md b/doc/topics/backpack.md new file mode 100644 index 0000000000..07d204f30e --- /dev/null +++ b/doc/topics/backpack.md @@ -0,0 +1,246 @@ +
+ +# Backpack + +Backpack is an extension to Haskell's module system, available since GHC 8.2. +It lets you write a library that depends on an abstract interface (a +*signature*) instead of a concrete implementation. The consumer of that library +then decides which implementation to plug in. The compiler recompiles the +library for each implementation, so there is no runtime cost. + +Backpack is jointly supported by GHC and Cabal. Stack builds on that support to +orchestrate the extra build steps that cross-package Backpack requires. + +## What Backpack gives you + +### Signatures + +A *signature file* (`.hsig`) declares the types and functions that an +implementation must provide, without supplying any code. For example, a +signature for an abstract string type: + +~~~haskell +-- Str.hsig +signature Str where + +data Str + +empty :: Str +append :: Str -> Str -> Str +~~~ + +The package lists its signatures in the `signatures` field of the library +stanza: + +~~~cabal +library + build-depends: base + signatures: Str + exposed-modules: MyModule +~~~ + +Any module inside this library can `import Str` and use it as if it were a +normal module. The compiler will type-check the code against the signature +without needing an actual implementation. + +A package that has at least one unfilled signature is called *indefinite*. + +### Mixin linking + +Backpack fills a signature through *mixin linking*: when a module with the same +name as a signature is brought into scope, the compiler treats that module as +the implementation. There is no special syntax for this — it happens +automatically by name. + +For example, if you depend on both the indefinite package above and a package +that exposes a module named `Str`, mixin linking fills the hole: + +~~~cabal +library + build-depends: + , base + , str-string -- exposes module "Str" + , my-indefinite-pkg -- has signature "Str" +~~~ + +Because `str-string` exposes a module called `Str` and `my-indefinite-pkg` +requires a signature called `Str`, the compiler matches them up and compiles +`my-indefinite-pkg` with `str-string`'s `Str` as the implementation. + +### Renaming + +When the signature name and the implementation module name differ, the +`mixins` field lets you rename one or the other. + +Rename the requirement to match the implementation: + +~~~cabal + mixins: my-indefinite-pkg requires (Str as Data.Text) +~~~ + +Or rename the implementation to match the requirement: + +~~~cabal + mixins: text (Data.Text as Str) +~~~ + +Both achieve the same result: the `Str` signature is filled by `Data.Text`. + +The `mixins` field also supports `hiding` on the `requires` side. This tells +the compiler *not* to fill the listed signatures through mixin linking for this +dependency — they remain as holes and propagate to the consumer. + +### Multiple instantiations + +You can instantiate the same indefinite package more than once with different +implementations. Each instantiation gets its own renaming in `mixins`: + +~~~cabal + mixins: + my-indefinite-pkg + (MyModule as MyModule.Text) + requires (Str as Data.Text), + my-indefinite-pkg + (MyModule as MyModule.BS) + requires (Str as Data.ByteString) +~~~ + +This produces two copies of `MyModule` — one backed by `Data.Text`, the other +by `Data.ByteString` — each with a distinct module name so they do not clash. + +### Sub-libraries + +Backpack projects tend to involve several small packages (a signature package, +one or more implementation packages, and a consumer). Cabal's sub-libraries +(also called internal libraries) let you keep all of these inside a single +`.cabal` file: + +~~~cabal +cabal-version: 2.2 +name: my-project + +library str-sig + signatures: Str + +library str-text + build-depends: base, text + exposed-modules: Str + +library + build-depends: base, str-sig, str-text + exposed-modules: MyModule +~~~ + +This is purely an organizational convenience — the semantics are identical to +having three separate packages. + +Note that implementation modules that fill a signature cannot live in the same +component that has the dependency on the signature package. They must be in a +separate package or sub-library. + +### Reexported modules + +The `reexported-modules` field lets you expose an instantiated module under a +public name. This is useful when you want to use Backpack as an internal +implementation detail while presenting a straightforward API to users who do not +need to know about Backpack: + +~~~cabal +library + build-depends: base, regex-indef, str-bytestring + reexported-modules: Regex as Regex.ByteString +~~~ + +### Template Haskell + +GHC cannot run Template Haskell splices from an indefinite package because +indefinite code is type-checked but not compiled — there is no object code to +execute at splice time. Splicing TH code *from a definite package into* an +indefinite one works fine. This is a GHC limitation, not a Stack limitation. + +## Backpack in Stack + +### Private Backpack + +When all signatures and their implementations live inside the same package (for +example using sub-libraries), no special build orchestration is needed. This has +always worked in Stack without any extra configuration. + +### Cross-package Backpack + +When a signature is defined in one package and filled by a module from a +different package, Stack needs to perform an extra build step: after building the +indefinite package and the implementing package, it creates an *instantiation +task* that compiles the indefinite package against the concrete implementation. + +Stack handles this automatically. There is nothing you need to add to +`stack.yaml` beyond listing the packages as usual: + +~~~yaml +packages: + - sig-pkg + - impl-pkg + - consumer-pkg +~~~ + +As long as the `.cabal` files set up the `signatures`, `build-depends`, and +`mixins` fields correctly, `stack build` does the rest. + +### What happens during a build + +When Stack encounters a cross-package Backpack setup, the build output shows the +extra instantiation step: + +~~~text +sig-pkg > configure (lib) +sig-pkg > build (lib) +impl-pkg > configure (lib) +impl-pkg > build (lib) +sig-pkg > build (inst:941095d7: Str = impl-pkg) +consumer > configure (lib) +consumer > build (lib) +~~~ + +The line marked `inst:` is the instantiation task. The hash identifies the +particular combination of signature-to-implementation mappings. The output also +shows which signatures are filled and by which packages. + +### Supported features + +Stack supports the full set of Backpack features that Cabal exposes: + +* Signature modules and indefinite packages +* Mixin linking (filling signatures by bringing a same-named module into scope) +* Explicit renaming in `mixins` (`requires (Sig as Impl)`) +* Multiple instantiations of the same indefinite package with different + implementations +* Sub-library signatures and implementations +* Transitive Backpack chains (an indefinite package depending on another + indefinite package — all inherited signatures are filled) +* Indefinite packages from Hackage or Stackage snapshots (not just local + packages) +* Haddock generation for instantiated packages +* Precompiled caching of instantiation results + +### Limitations + +**`requires hiding` with partial instantiation.** If a `mixins` entry uses +`requires hiding (SomeSig)` to leave a signature unfilled, Stack will not create +an instantiation task for that mixin entry. Cabal requires all signatures to be +filled in a single instantiation — partial instantiation is not possible. This +means the indefinite package remains indefinite for the hidden signatures and a +higher-level consumer must fill them. When `hiding` hides nothing (i.e. +`requires hiding ()`) it is equivalent to `DefaultRenaming` and works normally. + +**Template Haskell in indefinite packages.** As described above, this is a GHC +restriction, not specific to Stack. + +## Further reading + +* [How to use Backpack modules](https://cabal.readthedocs.io/en/latest/how-to-use-backpack.html) + in the Cabal documentation +* [Backpack: Retrofitting Haskell with Interfaces](https://plv.mpi-sws.org/backpack/) + — the original paper +* [Try Backpack: Cabal packages](http://blog.ezyang.com/2017/01/try-backpack-cabal-packages/) + — a practical walkthrough by Edward Z. Yang +* [GHC wiki: Backpack](https://gitlab.haskell.org/ghc/ghc/-/wikis/backpack) diff --git a/doc/tutorial/multi-package_projects.md b/doc/tutorial/multi-package_projects.md index dc0ddd9735..bcea0a9dfa 100644 --- a/doc/tutorial/multi-package_projects.md +++ b/doc/tutorial/multi-package_projects.md @@ -150,3 +150,8 @@ someFunc of packageA's Lib module [extra-dep](../configure/yaml/project.md#extra-deps). Although both dependencies are local, the former is part of the project and the latter is not. + +Multi-package projects are also the natural setting for GHC's +[Backpack](../topics/backpack.md) module system, where a signature package, an +implementation package, and a consumer package each live in separate directories +under the same `stack.yaml`. diff --git a/doc/tutorial/package_description.md b/doc/tutorial/package_description.md index 5fc75d080e..4a53d8aef8 100644 --- a/doc/tutorial/package_description.md +++ b/doc/tutorial/package_description.md @@ -93,6 +93,10 @@ package's root directory. benchmark components. The description identifies other packages on which those components depend. + A library component can also declare *signatures* — abstract module + interfaces that are filled in by a consumer. This is GHC's + [Backpack](../topics/backpack.md) module system. + Stack is aware of two different formats of package description, and both files may be present in the package's root directory: diff --git a/mkdocs.yml b/mkdocs.yml index aa4c274cb6..8a5c7d2d0f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -125,6 +125,7 @@ nav: - Azure CI: topics/azure_ci.md - Lock files: topics/lock_files.md - Haskell and C code: topics/haskell_and_c_code.md + - Backpack: topics/backpack.md - Get involved: - community/index.md - Contributors: diff --git a/src/Control/Concurrent/Execute.hs b/src/Control/Concurrent/Execute.hs index db8fd9a42c..bdae654536 100644 --- a/src/Control/Concurrent/Execute.hs +++ b/src/Control/Concurrent/Execute.hs @@ -25,6 +25,7 @@ import Control.Concurrent.STM ( check ) import Stack.Prelude import Data.List ( sortBy ) import qualified Data.Set as Set +import Stack.Types.Plan ( ComponentKey ) -- | Type representing exceptions thrown by functions exported by the -- "Control.Concurrent.Execute" module. @@ -40,13 +41,10 @@ instance Exception ExecuteException where -- | Type representing types of Stack build actions. data ActionType = ATBuild - -- ^ Action for building a package's library and executables. If - -- 'Stack.Types.Build.Task.allInOne' is 'True', then this will also build - -- benchmarks and tests. It is 'False' when the library's benchmarks or - -- test-suites have cyclic dependencies. - | ATBuildFinal - -- ^ Task for building the package's benchmarks and test-suites. Requires - -- that the library was already built. + -- ^ Action for configuring and building a single component (library, + -- sub-library, executable, or Backpack instantiation). Also used for + -- final build steps that compile test-suites and benchmarks when no + -- separate library build task exists for the component. | ATRunTests -- ^ Task for running the package's test-suites. | ATRunBenchmarks @@ -55,7 +53,7 @@ data ActionType -- | Types representing the unique ids of Stack build actions. data ActionId - = ActionId !PackageIdentifier !ActionType + = ActionId !ComponentKey !ActionType deriving (Eq, Ord, Show) -- | Type representing Stack build actions. diff --git a/src/Stack/Build.hs b/src/Stack/Build.hs index 06e754eca5..f9dcfe7fde 100644 --- a/src/Stack/Build.hs +++ b/src/Stack/Build.hs @@ -69,8 +69,8 @@ import Stack.Types.Package , PackageConfig (..), lpFiles, lpFilesForComponents ) import Stack.Types.Plan - ( Plan (..), Task (..), TaskType (..), taskLocation - , taskProvides + ( Plan (..), Task (..), TaskType (..), componentKeyPkgName + , taskLocation, taskProvides ) import Stack.Types.Platform ( HasPlatform (..) ) import Stack.Types.Runner ( Runner, globalOptsL ) @@ -187,9 +187,11 @@ build msetLocalFiles = do getInstalled installMap baseConfigOpts <- mkBaseConfigOpts boptsCli + let allDumpPkgs = globalDumpPkgs ++ snapshotDumpPkgs ++ localDumpPkgs plan <- constructPlan baseConfigOpts localDumpPkgs + allDumpPkgs loadPackage sourceMap installedMap @@ -242,8 +244,11 @@ buildLocalTargets :: buildLocalTargets targets = tryAny $ withNewLocalBuildTargets (NE.toList targets) $ build Nothing +-- | Extract the local package identifiers from the plan. Multiple component +-- tasks for the same package are deduplicated. justLocals :: Plan -> [PackageIdentifier] justLocals = + Set.toList . Set.fromList . map taskProvides . filter ((== Local) . taskLocation) . Map.elems . @@ -324,11 +329,16 @@ warnIfExecutablesWithSameNameCouldBeOverwritten locals plan = do exesToBuild :: Map StackUnqualCompName (NonEmpty PackageName) exesToBuild = collect - [ (exe, pkgName') - | (pkgName', task) <- Map.toList plan.tasks + [ (exe, pn) + | (pn, task) <- Map.toList perPkgTasks , TTLocalMutable lp <- [task.taskType] , exe <- (Set.toList . exeComponents . (.components)) lp ] + -- Multiple component tasks for the same package share the same Task data. + -- Use one representative task per package to avoid duplicates. + perPkgTasks :: Map PackageName Task + perPkgTasks = Map.fromList + [ (componentKeyPkgName ck, t) | (ck, t) <- Map.toList plan.tasks ] localExes :: Map StackUnqualCompName (NonEmpty PackageName) localExes = collect diff --git a/src/Stack/Build/Backpack.hs b/src/Stack/Build/Backpack.hs new file mode 100644 index 0000000000..61f293626b --- /dev/null +++ b/src/Stack/Build/Backpack.hs @@ -0,0 +1,589 @@ +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE OverloadedRecordDot #-} +{-# LANGUAGE OverloadedStrings #-} + +{-| +Module : Stack.Build.Backpack +Description : Backpack (cross-package) instantiation support. +License : BSD-3-Clause + +Create CInst instantiation tasks for Backpack packages. Given the build plan's +dependency resolution results, this module scans consumer packages for mixin +references to indefinite (signature-only) packages and creates additional build +tasks that instantiate those packages with concrete implementations. +-} + +module Stack.Build.Backpack + ( addInstantiationTasks + , upgradeFoundIndefinites + ) where + +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 +import qualified Data.Text as T +import Distribution.ModuleName ( ModuleName ) +import qualified Distribution.Text as CabalText +import Distribution.Types.IncludeRenaming ( IncludeRenaming (..) ) +import Distribution.Types.Mixin + ( Mixin (..), mixinIncludeRenaming, mixinPackageName ) +import Distribution.Types.ModuleRenaming + ( ModuleRenaming (..) ) +import Path ( parent ) +import Stack.ConfigureOpts ( packageConfigureOptsFromPackage ) +import Stack.Package + ( packageIsIndefinite ) +import Stack.Prelude +import Stack.Types.Build.ConstructPlan + ( AddDepRes (..), CombinedMap, PackageInfo (..) ) +import Stack.Types.SourceMap ( CommonPackage (..) ) +import Distribution.Types.BuildType ( BuildType (Configure) ) +import Stack.Types.Cache ( CachePkgSrc (..) ) +import Stack.Types.Component + ( StackBuildInfo (..), StackLibrary (..) ) +import Stack.Types.ConfigureOpts ( BaseConfigOpts (..) ) +import Stack.Types.EnvConfig ( EnvConfig (..), HasEnvConfig (..) ) +import Stack.Types.Installed + ( InstallLocation (..), Installed (..) + , InstalledLibraryInfo (..), installedVersion + ) +import Stack.Types.IsMutable ( IsMutable (..) ) +import Stack.Types.NamedComponent ( NamedComponent (..) ) +import Stack.Types.Package + ( LocalPackage (..), Package (..), PackageSource (..) + , installedMapGhcPkgId, packageIdentifier + ) +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. +-- +-- Only packages actually referenced by a consumer's mixin are loaded, +-- to avoid unnecessary Pantry lookups for the common case (no Backpack). +upgradeFoundIndefinites :: + forall env. HasEnvConfig env + => ( PackageLocationImmutable + -> Map FlagName Bool + -> [Text] + -> [Text] + -> RIO EnvConfig Package + ) + -- ^ Load package from source (loadPackage0) + -> EnvConfig + -> 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 -> + 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) + Just (PIOnlySource ps) -> + -- Should not happen (ADRFound implies installed), but handle + -- gracefully by loading the package to check. + upgradeFromSource name ps Nothing + 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 + 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 = + let loc = psLocation ps + isMutable = installLocationIsMutable loc + presentMap = case mInstalled of + Just (Library pid ili) -> installedMapGhcPkgId pid ili + _ -> Map.empty + in Task + { configOpts = TaskConfigOpts + { missing = Set.empty + , envConfig = econfig + , baseConfigOpts = bco + , isLocalNonExtraDep = psLocal ps + , isMutable + , pkgConfigOpts = packageConfigureOptsFromPackage package + , instantiationDeps = [] + } + , buildHaddocks = False + , present = presentMap + , taskType = case ps of + PSFilePath lp -> TTLocalMutable lp + PSRemote pkgLoc _version _fromSnapshot _cp -> + TTRemotePackage isMutable package pkgLoc + , cachePkgSrc = toCachePkgSrc ps + , buildTypeConfig = packageBuildTypeConfig package + , backpackInstEntries = [] + } + + upgradeFromSource :: + PackageName + -> PackageSource + -> Maybe Installed + -> RIO env (PackageName, Maybe AddDepRes) + upgradeFromSource name ps mInstalled = do + package <- loadFromSource ps + if packageIsIndefinite package + then pure (name, Just $ ADRToInstall $ mkTask package ps mInstalled) + else pure (name, Nothing) + + upgradeFromHackage :: + PackageName + -> Installed + -> RIO env (PackageName, Maybe AddDepRes) + upgradeFromHackage name installed = do + let version = installedVersion installed + mPkgLoc <- runRIO econfig $ + getLatestHackageRevision YesRequireHackageIndex name version >>= \case + Nothing -> pure Nothing + Just (_rev, cfKey, treeKey) -> + pure $ Just $ PLIHackage (PackageIdentifier name version) cfKey treeKey + case mPkgLoc of + Nothing -> pure (name, 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 + task = Task + { configOpts = TaskConfigOpts + { missing = Set.empty + , envConfig = econfig + , baseConfigOpts = bco + , isLocalNonExtraDep = False + , isMutable = Immutable + , pkgConfigOpts = packageConfigureOptsFromPackage package + , instantiationDeps = [] + } + , buildHaddocks = False + , present = presentMap + , taskType = TTRemotePackage Immutable package pkgLoc + , cachePkgSrc = CacheSrcUpstream + , buildTypeConfig = packageBuildTypeConfig package + , backpackInstEntries = [] + } + pure (name, Just $ ADRToInstall task) + else pure (name, Nothing) + +-- | Post-pass: scan consumer tasks for Backpack mixins referencing indefinite +-- packages and create CInst instantiation tasks. Returns the augmented list +-- plus any warnings about unsupported Backpack patterns. +addInstantiationTasks :: + Map PackageName (Set ModuleName) + -- ^ Installed package modules (from ghc-pkg dump). Used for module + -- resolution when the implementing package is ADRFound (already + -- installed, no Task/Package metadata available). + -> [(PackageName, AddDepRes)] -- ^ Original per-package ADRs + -> [(ComponentKey, AddDepRes)] -- ^ Expanded component-keyed ADRs + -> ([(ComponentKey, AddDepRes)], [StyleDoc]) + -- ^ (Augmented with CInst tasks, warnings) +addInstantiationTasks installedModules origAdrs expandedAdrs = + let adrMap = Map.fromList origAdrs + -- Process each entry, collecting new CInst tasks and modified consumers. + (newInstTasks, modifiedEntries, warns) = + foldr (processEntry adrMap) ([], [], []) expandedAdrs + -- Deduplicate CInst tasks: multiple components of the same consumer + -- may reference the same mixin, producing identical CInst entries. + -- Map.fromList keeps the last, Map.toList restores the list. + dedupedInstTasks = Map.toList $ Map.fromList newInstTasks + in (modifiedEntries ++ dedupedInstTasks, warns) + where + processEntry :: + Map PackageName AddDepRes + -> (ComponentKey, AddDepRes) + -> ([(ComponentKey, AddDepRes)], [(ComponentKey, AddDepRes)], [StyleDoc]) + -> ([(ComponentKey, AddDepRes)], [(ComponentKey, AddDepRes)], [StyleDoc]) + processEntry adrMap (ck, ADRToInstall task) (instAcc, entryAcc, warnAcc) = + let pkg = taskPackage task + -- Get mixins from the main library and all sub-libraries. + mainMixins = case pkg.library of + Just lib -> lib.buildInfo.mixins + Nothing -> [] + subLibMixins = concatMap (\lib -> lib.buildInfo.mixins) + (toList pkg.subLibraries) + allMixins = mainMixins ++ subLibMixins + -- Collect the consumer's build-depends to scope module resolution. + -- When multiple packages expose the same module, only the consumer's + -- direct dependencies are considered (matching Cabal's behavior). + consumerDeps = Set.fromList $ concatMap + (\lib -> Map.keys lib.buildInfo.dependency) + (maybeToList pkg.library ++ toList pkg.subLibraries) + -- Process each mixin that references an indefinite dep. + (instTasks, instKeys, mixinWarns) = + processAllMixins ck pkg allMixins adrMap consumerDeps + -- Add CInst keys to the consumer's instantiationDeps. + modifiedTask + | null instKeys = task + | otherwise = task + { configOpts = task.configOpts + { instantiationDeps = + task.configOpts.instantiationDeps ++ instKeys + } + } + in ( instTasks ++ instAcc + , (ck, ADRToInstall modifiedTask) : entryAcc + , mixinWarns ++ warnAcc + ) + 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 :: + ComponentKey -- ^ Consumer's key + -> Package -- ^ Consumer's package + -> [Mixin] -- ^ Consumer's library mixins + -> Map PackageName AddDepRes + -> Set PackageName -- ^ Consumer's build-depends + -> ([(ComponentKey, AddDepRes)], [ComponentKey], [StyleDoc]) + processAllMixins _ck _pkg mixins adrMap' consumerDeps = + foldr (processMixin adrMap' consumerDeps) ([], [], []) mixins + + processMixin :: + Map PackageName AddDepRes + -> Set PackageName -- ^ Consumer's build-depends + -> Mixin + -> ([(ComponentKey, AddDepRes)], [ComponentKey], [StyleDoc]) + -> ([(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 -> + let -- Get signatures from the sig-pkg's main library. + ownSigs :: [ModuleName] + ownSigs = case sigPkg.library of + Just lib -> lib.signatures + Nothing -> [] + -- Collect inherited signatures from transitive indefinite + -- deps. When a sig-pkg depends on another indefinite package, + -- its holes propagate upward. The CInst must fill ALL holes + -- (own + inherited) or Cabal reports "non-closing + -- substitution". + inheritedSigs :: [ModuleName] + inheritedSigs = + -- Deduplicate: diamond deps can yield the same sig + -- from multiple paths. + L.nub $ 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 + -- mixin renaming. Module lookup is scoped to the consumer's + -- build-depends so that multiple instantiations with different + -- implementations don't conflict. + (ownEntries, ownWarns) = + resolveAllEntries depPkgName renaming adrMap' + consumerDeps ownSigs + -- Resolve inherited signatures using DefaultRenaming (the + -- consumer's mixin only renames the direct sig-pkg's + -- signatures, not inherited ones). + (inheritedEntries, inheritedWarns) = + resolveAllEntries depPkgName DefaultRenaming adrMap' + consumerDeps inheritedSigs + entries = ownEntries ++ inheritedEntries + resolveWarns = ownWarns ++ inheritedWarns + in if null entries + then (instAcc, keyAcc, resolveWarns ++ warnAcc) + else + let hashSuffix = instHashSuffix entries + instCk = ComponentKey depPkgName (CInst hashSuffix) + -- The CInst task's missing includes the sig-pkg's + -- original missing plus implementing packages that + -- are ADRToInstall. + implPidsMissing = Set.fromList + [ packageIdentifier (taskPackage implTask) + | (_, implPkgName', _) <- entries + , Just (ADRToInstall implTask) <- + [Map.lookup implPkgName' adrMap'] + ] + -- For ADRFound implementing packages, add their + -- PID→GhcPkgId to the CInst task's present map so + -- that mkInstantiateWithOpts can generate + -- --instantiate-with flags. + implPresent = Map.fromList + [ (pid, gid) + | (_, implPkgName', _) <- entries + , Just (ADRFound _ (Library pid (InstalledLibraryInfo gid _ _))) <- + [Map.lookup implPkgName' adrMap'] + ] + instTask = sigTask + { backpackInstEntries = entries + , configOpts = sigTask.configOpts + { missing = + sigTask.configOpts.missing <> implPidsMissing + , instantiationDeps = [] + } + , present = + sigTask.present <> implPresent + } + in ( (instCk, ADRToInstall instTask) : instAcc + , 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) + + -- Collect signatures inherited from a package's transitive indefinite deps. + -- When pkg-A depends on indefinite pkg-B, pkg-B's signatures propagate up + -- as holes in pkg-A. This recursively walks the dep graph, collecting + -- signatures from all reachable indefinite packages. + collectInheritedSigs :: + Package + -> Map PackageName AddDepRes + -> Set PackageName -- ^ Already visited (cycle prevention) + -> [ModuleName] + collectInheritedSigs pkg adrMap' visited = + concatMap collectFromDep depNames + where + depNames :: [PackageName] + depNames = case pkg.library of + Just lib -> Map.keys lib.buildInfo.dependency + Nothing -> [] + collectFromDep :: PackageName -> [ModuleName] + 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 + _ -> [] + + -- Resolve all signature entries for a mixin, collecting both successful + -- resolutions and warnings for signatures that could not be resolved. + resolveAllEntries :: + PackageName -- ^ Sig-pkg name (for warning messages) + -> ModuleRenaming + -> Map PackageName AddDepRes + -> Set PackageName -- ^ Consumer's build-depends (scope) + -> [ModuleName] -- ^ Signatures to resolve + -> ([(ModuleName, PackageName, ModuleName)], [StyleDoc]) + resolveAllEntries sigPkgName renaming adrMap' scope sigs = + case renaming of + HidingRenaming hiddenSigs -> + -- HidingRenaming: hide specified sigs from mixin linking. Hidden + -- sigs remain as unfilled holes (the consumer becomes indefinite for + -- them). Since Cabal requires a closing substitution, we can only + -- create a CInst when nothing is actually hidden — otherwise the + -- partial --instantiate-with would fail. + let visibleSigs = filter (`notElem` hiddenSigs) sigs + in if length visibleSigs == length sigs + -- Nothing actually hidden: equivalent to DefaultRenaming. + then foldr (resolveOne sigPkgName DefaultRenaming adrMap' scope) + ([], []) visibleSigs + -- Some sigs hidden: can't fully instantiate this copy. + else ([], []) + _ -> foldr (resolveOne sigPkgName renaming adrMap' scope) ([], []) sigs + + -- Resolve a single signature entry, producing a warning if it fails. + resolveOne :: + PackageName -- ^ Sig-pkg name (for warning messages) + -> ModuleRenaming + -> Map PackageName AddDepRes + -> Set PackageName -- ^ Consumer's build-depends (scope) + -> ModuleName -- ^ Signature module name + -> ([(ModuleName, PackageName, ModuleName)], [StyleDoc]) + -> ([(ModuleName, PackageName, ModuleName)], [StyleDoc]) + resolveOne sigPkgName renaming adrMap' scope sigName (entryAcc, warnAcc) = + case renaming of + DefaultRenaming -> + -- Identity mapping: sigName is filled by a module with the same name. + case findDepExposingModule sigName adrMap' scope of + Right implPkgName -> + ((sigName, implPkgName, sigName) : entryAcc, warnAcc) + Left w -> + (entryAcc, mkModuleWarn sigPkgName sigName w : warnAcc) + ModuleRenaming mappings -> + -- Explicit: look for (sigName, implModuleName) in the mapping. + case lookup sigName mappings of + Just implModName -> + case findDepExposingModule implModName adrMap' scope of + Right implPkgName -> + ((sigName, implPkgName, implModName) : entryAcc, warnAcc) + Left w -> + (entryAcc, mkModuleWarn sigPkgName implModName w : warnAcc) + Nothing -> + -- Signature not mentioned in the explicit mapping — this is + -- normal (the mixin doesn't remap this sig), skip silently. + (entryAcc, warnAcc) + HidingRenaming _ -> + -- Converted to DefaultRenaming in resolveAllEntries; should not + -- reach here. + (entryAcc, warnAcc) + + -- Build a warning message for a failed module resolution. + mkModuleWarn :: PackageName -> ModuleName -> [PackageName] -> StyleDoc + mkModuleWarn sigPkgName modName candidates = fillSep $ case candidates of + [] -> + [ flow "Backpack: no dependency exposes module" + , style Current (fromString (CabalText.display modName)) + , flow "needed to instantiate" + , style Current (fromPackageName sigPkgName) <> "." + , flow "Ensure the implementing package is listed in build-depends." + ] + _ -> + [ flow "Backpack: multiple dependencies expose module" + , style Current (fromString (CabalText.display modName)) + , flow "needed to instantiate" + , style Current (fromPackageName sigPkgName) <> ":" + ] + ++ L.intersperse "," (map (style Current . fromPackageName) candidates) + ++ ["—", flow "cannot determine which to use. Skipping instantiation."] + + -- Find which dep in the ADR map exposes a given module name. Only packages + -- that are in the consumer's build-depends (scope) are considered, so that + -- multiple consumers can instantiate the same sig-pkg with different + -- implementations without ambiguity. Returns @Right pkgName@ on unique + -- match, or @Left candidates@ (empty = no match, multiple = ambiguous) on + -- failure. + -- + -- Checks both ADRToInstall entries (via Package metadata) and ADRFound + -- entries (via the installed modules map from ghc-pkg dump). This allows + -- module resolution to work when the implementing package is already + -- installed (e.g., from a snapshot or a previous build). + findDepExposingModule :: + ModuleName + -> Map PackageName AddDepRes + -> Set PackageName -- ^ Consumer's build-depends (scope) + -> Either [PackageName] PackageName + findDepExposingModule modName adrMap' scope = + let -- Check ADRToInstall entries (have full Package metadata). + fromTasks = + [ pn + | (pn, ADRToInstall t) <- Map.toList adrMap' + , pn `Set.member` scope + , let p = taskPackage t + , not (packageIsIndefinite p) + , exposesModule modName p + ] + -- Check ADRFound entries (use installed modules map from dump). + fromInstalled = + [ pn + | (pn, ADRFound _ _) <- Map.toList adrMap' + , pn `Set.member` scope + , case Map.lookup pn installedModules of + Just mods -> modName `Set.member` mods + Nothing -> False + ] + candidates = fromTasks ++ fromInstalled + in case candidates of + [pn] -> Right pn + _ -> Left candidates + + exposesModule :: ModuleName -> Package -> Bool + exposesModule modName p = + mainExposes || subLibExposes + where + mainExposes = case p.library of + Just lib -> modName `L.elem` (lib.exposedModules :: [ModuleName]) + Nothing -> False + subLibExposes = any + (\lib -> modName `L.elem` (lib.exposedModules :: [ModuleName])) + (toList p.subLibraries) + + -- Produce a deterministic hash suffix from instantiation entries. + instHashSuffix :: [(ModuleName, PackageName, ModuleName)] -> Text + instHashSuffix entries = + let sorted = L.sort + [ CabalText.display sig ++ "=" ++ packageNameString implPkg + ++ ":" ++ CabalText.display implMod + | (sig, implPkg, implMod) <- entries + ] + input = S8.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 + in T.take 16 (decodeUtf8Lenient hexStr) + +-- Local helpers (duplicated from ConstructPlan to avoid import cycle) + +packageBuildTypeConfig :: Package -> Bool +packageBuildTypeConfig pkg = pkg.buildType == Configure + +psLocal :: PackageSource -> Bool +psLocal (PSFilePath _) = True +psLocal PSRemote{} = False + +psLocation :: PackageSource -> InstallLocation +psLocation (PSFilePath _) = Local +psLocation PSRemote{} = Snap + +toCachePkgSrc :: PackageSource -> CachePkgSrc +toCachePkgSrc (PSFilePath lp) = + CacheSrcLocal (toFilePath (parent lp.cabalFP)) +toCachePkgSrc PSRemote{} = CacheSrcUpstream diff --git a/src/Stack/Build/Cache.hs b/src/Stack/Build/Cache.hs index 2f5aca0d3e..d1184a6fa0 100644 --- a/src/Stack/Build/Cache.hs +++ b/src/Stack/Build/Cache.hs @@ -179,9 +179,11 @@ tryGetConfigCache :: HasEnvConfig env => Path Abs Dir -- ^ Package directory. + -> ConfigCacheType + -- ^ Type of cache. -> RIO env (Maybe ConfigCache) tryGetConfigCache dir = - loadConfigCache $ configCacheKey dir ConfigCacheTypeConfig + loadConfigCache . configCacheKey dir -- | Try to read the modification time of the Cabal file from the last build. tryGetCabalMod :: @@ -241,11 +243,13 @@ writeConfigCache :: HasEnvConfig env => Path Abs Dir -- ^ Package directory. + -> ConfigCacheType + -- ^ Type of cache. -> ConfigCache -- ^ Cabal configuration cache. -> RIO env () -writeConfigCache dir = - saveConfigCache (configCacheKey dir ConfigCacheTypeConfig) +writeConfigCache dir cacheType = + saveConfigCache (configCacheKey dir cacheType) -- | See 'tryGetCabalMod' writeCabalMod :: @@ -288,13 +292,15 @@ deleteCaches :: HasEnvConfig env => Path Abs Dir -- ^ Package directory. + -> ConfigCacheType + -- ^ Type of cache. -> RIO env () -deleteCaches dir = +deleteCaches dir cacheType = {- FIXME confirm that this is acceptable to remove bfp <- buildCacheFile dir removeFileIfExists bfp -} - deactiveConfigCache $ configCacheKey dir ConfigCacheTypeConfig + deactiveConfigCache $ configCacheKey dir cacheType -- | For the given installed item, yields the key used to retrieve a record from -- the library Cabal flag cache or executable Cabal flag cache. diff --git a/src/Stack/Build/ConstructPlan.hs b/src/Stack/Build/ConstructPlan.hs index 8a625945a1..10fbd29375 100644 --- a/src/Stack/Build/ConstructPlan.hs +++ b/src/Stack/Build/ConstructPlan.hs @@ -14,6 +14,8 @@ Construct a @Plan@ for how to build. module Stack.Build.ConstructPlan ( constructPlan + -- * Exported for testing + , shouldSplitComponents ) where import Control.Monad.Trans.Maybe ( MaybeT (..) ) @@ -22,7 +24,7 @@ import qualified Data.Map.Strict as Map import Data.Monoid.Map ( MonoidMap(..) ) import qualified Data.Set as Set import qualified Data.Text as T -import Distribution.Types.BuildType ( BuildType (Configure) ) +import Distribution.Types.BuildType ( BuildType (Configure, Simple) ) import Distribution.Types.PackageName ( mkPackageName ) import Distribution.Version ( mkVersion ) import Path ( parent ) @@ -31,6 +33,8 @@ import RIO.Process ( findExecutable ) import RIO.State ( State, StateT (..), execState, get, modify, modify', put ) import RIO.Writer ( WriterT (..), pass, tell ) +import Stack.Build.Backpack + ( addInstantiationTasks, upgradeFoundIndefinites ) import Stack.Build.Cache ( tryGetFlagCache ) import Stack.Build.Haddock ( shouldHaddockDeps ) import Stack.Build.Source ( loadLocalPackage ) @@ -40,7 +44,10 @@ import Stack.ConfigureOpts ) import Stack.Constants ( compilerOptionsCabalFlag ) import Stack.Package - ( applyForceCustomBuild, buildableExes, packageUnknownTools + ( applyForceCustomBuild, buildableExes + , buildableForeignLibs, buildableSubLibs + , hasBuildableMainLibrary, hasIntraPackageDeps + , packageUnknownTools , processPackageDepsEither ) import Stack.Prelude hiding ( loadPackage ) @@ -49,7 +56,7 @@ import Stack.Types.Build.ConstructPlan ( AddDepRes (..), CombinedMap, Ctx (..), M , MissingPresentDeps (..), PackageInfo (..), ToolWarning(..) , UnregisterState (..), W (..), adrHasLibrary, adrVersion - , isAdrToInstall, toTask + , toTask ) import Stack.Types.Build.Exception ( BadDependency (..), BuildException (..) @@ -68,7 +75,6 @@ import Stack.Types.CompilerPaths import Stack.Types.ComponentUtils ( unqualCompFromText ) import Stack.Types.Config ( Config (..), HasConfig (..), stackRootL ) import Stack.Types.ConfigureOpts ( BaseConfigOpts (..) ) -import Stack.Types.Curator ( Curator (..) ) import Stack.Types.Dependency ( DepValue (..), isDepTypeLibrary ) import Stack.Types.DumpPackage ( DumpPackage (..), sublibParentPkgId ) import Stack.Types.EnvConfig ( EnvConfig (..), HasEnvConfig (..) ) @@ -77,20 +83,26 @@ import Stack.Types.EnvSettings import Stack.Types.GhcPkgId ( GhcPkgId ) import Stack.Types.GlobalOpts ( GlobalOpts (..) ) import Stack.Types.Installed - ( InstallLocation (..), Installed (..), InstalledMap + ( InstallLocation (..), Installed (..) + , InstalledMap , installedVersion ) import Stack.Types.IsMutable ( IsMutable (..) ) -import Stack.Types.NamedComponent ( exeComponents, renderComponent ) +import Stack.Types.NamedComponent + ( NamedComponent (..), exeComponents, isCBench, isCTest + , renderComponent + ) import Stack.Types.Package ( ExeName (..), LocalPackage (..), Package (..) , PackageSource (..), installedMapGhcPkgId , packageIdentifier, psVersion, runMemoizedWith ) import Stack.Types.Plan - ( Plan (..), Task (..), TaskConfigOpts (..), TaskType (..) - , installLocationIsMutable, taskIsTarget, taskLocation - , taskProvides, taskTargetIsMutable + ( ComponentKey (..), Plan (..), Task (..) + , TaskConfigOpts (..), TaskType (..) + , componentKeyPkgName, installLocationIsMutable + , taskIsTarget, taskLocation, taskProvides + , taskTargetIsMutable ) import Stack.Types.ProjectConfig ( isPCGlobalProject ) import Stack.Types.Runner ( HasRunner (..), globalOptsL ) @@ -122,6 +134,7 @@ constructPlan :: forall env. HasEnvConfig env => BaseConfigOpts -> [DumpPackage] -- ^ locally registered + -> [DumpPackage] -- ^ all dump packages (global + snapshot + local) -> ( PackageLocationImmutable -> Map FlagName Bool -> [Text] @@ -139,6 +152,7 @@ constructPlan :: constructPlan baseConfigOpts0 localDumpPkgs + allDumpPkgs loadPackage0 sourceMap installedMap @@ -208,7 +222,28 @@ constructPlan errs = errlibs ++ errfinals if null errs then do - let tasks = Map.fromList $ mapMaybe (toMaybe . second toTask) adrs + -- Upgrade ADRFound entries to ADRToInstall for indefinite packages + -- whose source is available. This ensures addInstantiationTasks can + -- clone their Task to create CInst instantiation tasks. + let loadPkg w x y z = applyForceCustomBuild globalCabalVersion + <$> loadPackage0 w x y z + -- Build a module lookup map from installed packages (dump data). + -- Used by addInstantiationTasks to resolve modules from ADRFound + -- packages that don't have Package metadata. + installedModules = Map.fromListWith Set.union + [ (pkgName dp.packageIdent, dp.exposedModules) + | dp <- allDumpPkgs + , isNothing dp.sublib -- Only main libraries + ] + adrs' <- upgradeFoundIndefinites + loadPkg econfig ctx.combinedMap ctx.baseConfigOpts adrs + let expandedAdrs = concatMap (uncurry expandToComponentKeys) adrs' + (withInstantiations, bpWarnings) = + addInstantiationTasks installedModules adrs' expandedAdrs + tasks = Map.fromList $ mapMaybe + (toMaybe . second toTask) + withInstantiations + mapM_ prettyWarn bpWarnings takeSubset Plan { tasks = tasks , finals = Map.fromList finals @@ -306,7 +341,8 @@ constructPlan -- | Throw an exception if there are any snapshot packages in the plan. errorOnSnapshot :: Plan -> RIO env Plan errorOnSnapshot plan@(Plan tasks _finals _unregister installExes) = do - let snapTasks = Map.keys $ Map.filter (\t -> taskLocation t == Snap) tasks + let snapTasks = Set.toList $ Set.fromList $ map componentKeyPkgName + $ Map.keys $ Map.filter (\t -> taskLocation t == Snap) tasks snapExes = Map.keys $ Map.filter (== Snap) installExes unless (null snapTasks && null snapExes) $ prettyThrowIO $ NotOnlyLocal snapTasks snapExes @@ -348,7 +384,7 @@ constructPlan -- | Determine which packages to unregister based on the given tasks and -- already registered project packages and local extra-deps. mkUnregisterLocal :: - Map PackageName Task + Map ComponentKey Task -- ^ Tasks -> Map PackageName Text -- ^ Reasons why packages are dirty and must be rebuilt @@ -358,12 +394,17 @@ mkUnregisterLocal :: -- ^ If true, we're doing a special initialBuildSteps build - don't -- unregister target packages. -> Map GhcPkgId (PackageIdentifier, Text) -mkUnregisterLocal tasks dirtyReason localDumpPkgs initialBuildSteps = +mkUnregisterLocal componentTasks dirtyReason localDumpPkgs initialBuildSteps = -- We'll take multiple passes through the local packages. This will allow us -- to detect that a package should be unregistered, as well as all packages -- directly or transitively depending on it. loop Map.empty localDumpPkgs where + -- Derive a per-package task map: pick one representative task per package. + tasks :: Map PackageName Task + tasks = Map.fromList + [ (componentKeyPkgName ck, t) | (ck, t) <- Map.toList componentTasks ] + loop :: Map GhcPkgId (PackageIdentifier, Text) -- ^ Current local packages to unregister. @@ -448,25 +489,17 @@ mkUnregisterLocal tasks dirtyReason localDumpPkgs initialBuildSteps = relevantPkgName :: PackageName relevantPkgName = maybe (pkgName ident) pkgName mParentLibId --- | Given a t'LocalPackage' and its 'testBench', adds a t'Task' for running --- its tests and benchmarks. --- --- If @isAllInOne@ is 'True', then this means that the build step will also --- build the tests. Otherwise, this indicates that there's a cyclic dependency --- and an additional build step needs to be done. --- --- This will also add all the deps needed to build the tests / benchmarks. If --- @isAllInOne@ is 'True' (the common case), then all of these should have --- already been taken care of as part of the build step. +-- | 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 +-- for the library build, so the lookups are cache hits). addFinal :: LocalPackage -> Package - -> Bool - -- ^ Will the build step also build the tests? -> Bool -- ^ Should Haddock documentation be built? -> M () -addFinal lp package allInOne buildHaddocks = do +addFinal lp package buildHaddocks = do res <- addPackageDeps package >>= \case Left e -> pure $ Left e Right (MissingPresentDeps missing present _minLoc) -> do @@ -479,17 +512,27 @@ addFinal lp package allInOne buildHaddocks = do , isLocalNonExtraDep = True , isMutable = Mutable , pkgConfigOpts + , instantiationDeps = [] } pure $ Right Task { configOpts , buildHaddocks , present , taskType = TTLocalMutable lp - , allInOne , cachePkgSrc = CacheSrcLocal (toFilePath (parent lp.cabalFP)) , buildTypeConfig = packageBuildTypeConfig package + , backpackInstEntries = [] } - tell mempty { wFinals = Map.singleton package.name res } + let finalKeys + | shouldSplitComponents package = + let testComps = filter isCTest $ Set.toList lp.components + benchComps = filter isCBench $ Set.toList lp.components + in case testComps ++ benchComps of + [] -> [ComponentKey package.name CLib] + comps -> map (ComponentKey package.name) comps + | otherwise = [ComponentKey package.name CLib] + forM_ finalKeys $ \ck -> + tell mempty { wFinals = Map.singleton ck res } -- | Given a 'PackageName', adds all of the build tasks to build the package, if -- needed. First checks if the package name is in the library map. @@ -663,24 +706,24 @@ installPackage name ps minstalled = do case ps of PSRemote pkgLoc _version _fromSnapshot cp -> do logDebugPlanS "installPackage" $ - "Doing all-in-one build for upstream package " + "Building upstream package " <> fromPackageName name <> "." package <- ctx.loadPackage pkgLoc cp.flags cp.ghcOptions cp.cabalConfigOpts - resolveDepsAndInstall True cp.buildHaddocks ps package minstalled + resolveDepsAndInstall cp.buildHaddocks ps package minstalled PSFilePath lp -> do case lp.testBench of Nothing -> do logDebugPlanS "installPackage" $ "No test or bench component for " <> fromPackageName name - <> " so doing an all-in-one build." + <> ", building directly." resolveDepsAndInstall - True lp.buildHaddocks ps lp.package minstalled + lp.buildHaddocks ps lp.package minstalled Just tb -> do - -- Attempt to find a plan which performs an all-in-one build. Ignore - -- the writer action + reset the state if it fails. + -- Attempt to resolve test/bench deps together with the library. + -- Ignore the writer action + reset the state if it fails. libMap <- get res <- pass $ do res <- addPackageDeps tb @@ -694,22 +737,15 @@ installPackage name ps minstalled = do "For " <> fromPackageName name <> ", successfully added package deps." - -- in curator builds we can't do all-in-one build as - -- test/benchmark failure could prevent library from being - -- available to its dependencies but when it's already available - -- it's OK to do that - splitRequired <- expectedTestOrBenchFailures <$> asks (.curator) - let isAllInOne = not splitRequired adr <- installPackageGivenDeps - isAllInOne lp.buildHaddocks ps tb minstalled deps - let finalAllInOne = not (isAdrToInstall adr && splitRequired) + lp.buildHaddocks ps tb minstalled deps -- FIXME: this redundantly adds the deps (but they'll all just -- get looked up in the map) - addFinal lp tb finalAllInOne False + addFinal lp tb False pure $ Right adr Left _ -> do - -- Reset the state to how it was before attempting to find an - -- all-in-one build plan. + -- Reset the state to how it was before attempting to find a + -- combined build plan. logDebugPlanS "installPackage" $ "Before trying cyclic plan, resetting lib result map to: " <> fromString (show libMap) @@ -717,49 +753,40 @@ installPackage name ps minstalled = do -- Otherwise, fall back on building the tests / benchmarks in a -- separate step. res' <- resolveDepsAndInstall - False lp.buildHaddocks ps lp.package minstalled + lp.buildHaddocks ps lp.package minstalled when (isRight res') $ do -- Insert it into the map so that it's available for addFinal. updateLibMap name res' - addFinal lp tb False False + addFinal lp tb False pure res' - where - expectedTestOrBenchFailures maybeCurator = fromMaybe False $ do - curator <- maybeCurator - pure $ Set.member name curator.expectTestFailure - || Set.member name curator.expectBenchmarkFailure resolveDepsAndInstall :: Bool - -- ^ will the build step also build any tests? - -> Bool -- ^ Should Haddock documentation be built? -> PackageSource -> Package -> Maybe Installed -> M (Either ConstructPlanException AddDepRes) -resolveDepsAndInstall isAllInOne buildHaddocks ps package minstalled = +resolveDepsAndInstall buildHaddocks ps package minstalled = addPackageDeps package >>= \case Left err -> pure $ Left err Right deps -> Right <$> installPackageGivenDeps - isAllInOne buildHaddocks ps package minstalled deps + buildHaddocks ps package minstalled deps -- | Checks if we need to install the given t'Package', given the results of -- 'addPackageDeps'. If dependencies are missing, the package is dirty, or it is -- not installed, then it needs to be installed. installPackageGivenDeps :: Bool - -- ^ will the build step also build any tests? - -> Bool -- ^ Should Haddock documentation be built? -> PackageSource -> Package -> Maybe Installed -> MissingPresentDeps -> M AddDepRes -installPackageGivenDeps allInOne buildHaddocks ps package minstalled +installPackageGivenDeps buildHaddocks ps package minstalled (MissingPresentDeps missing present minMutable) = do let name = package.name mRightVersionInstalled <- case minstalled of @@ -788,6 +815,7 @@ installPackageGivenDeps allInOne buildHaddocks ps package minstalled , isLocalNonExtraDep = psLocal ps , isMutable , pkgConfigOpts + , instantiationDeps = [] } pure $ case mRightVersionInstalled of Just installed -> ADRFound loc installed @@ -801,15 +829,48 @@ installPackageGivenDeps allInOne buildHaddocks ps package minstalled TTLocalMutable lp PSRemote pkgLoc _version _fromSnapshot _cp -> TTRemotePackage isMutable package pkgLoc - , allInOne , cachePkgSrc = toCachePkgSrc ps , buildTypeConfig = packageBuildTypeConfig package + , backpackInstEntries = [] } -- | Is the build type of the package Configure packageBuildTypeConfig :: Package -> Bool packageBuildTypeConfig pkg = pkg.buildType == Configure +-- | Whether a package should be split into per-component tasks. Only local +-- packages with Simple build type and no intra-package dependencies (Backpack) +-- are split. Remote packages, Custom/Hooks/Make build types, and Backpack +-- packages get a single task. +shouldSplitComponents :: Package -> Bool +shouldSplitComponents pkg = + pkg.buildType == Simple && not (hasIntraPackageDeps pkg) + +-- | Expand an AddDepRes into per-component (ComponentKey, AddDepRes) pairs. +-- For local Simple packages without intra-package deps, creates one entry per +-- buildable component (lib, sub-libs, foreign libs, exes). Everything else +-- gets a single CLib entry. +expandToComponentKeys :: + PackageName -> AddDepRes -> [(ComponentKey, AddDepRes)] +expandToComponentKeys name adr = case adr of + ADRToInstall task -> case task.taskType of + TTLocalMutable lp + | shouldSplitComponents lp.package -> + let pkg = lp.package + libComps = [CLib | hasBuildableMainLibrary pkg] + subLibComps = + map CSubLib $ Set.toList $ buildableSubLibs pkg + flibComps = + map CFlib $ Set.toList $ buildableForeignLibs pkg + exeComps = + map CExe $ Set.toList $ buildableExes pkg + allComps = libComps ++ subLibComps ++ flibComps ++ exeComps + in case allComps of + [] -> [(ComponentKey name CLib, adr)] + _ -> map (\comp -> (ComponentKey name comp, adr)) allComps + _ -> [(ComponentKey name CLib, adr)] + _ -> [(ComponentKey name CLib, adr)] + -- Update response in the library map. If it is an error, and there's already an -- error about cyclic dependencies, prefer the cyclic error. updateLibMap :: PackageName -> Either ConstructPlanException AddDepRes -> M () diff --git a/src/Stack/Build/Execute.hs b/src/Stack/Build/Execute.hs index a64035fbdf..77b43a303b 100644 --- a/src/Stack/Build/Execute.hs +++ b/src/Stack/Build/Execute.hs @@ -18,6 +18,10 @@ module Stack.Build.Execute -- * Running Setup.hs , ExcludeTHLoading (..) , KeepOutputOpen (..) + -- * Exported for testing + , missingToDeps + , intraPackageDeps + , finalTestsAndBenches ) where import Control.Concurrent.Execute @@ -81,13 +85,14 @@ import Stack.Types.Installed ( InstallLocation (..), InstalledMap , installedPackageIdentifier ) -import Stack.Types.NamedComponent - ( NamedComponent, benchComponents, testComponents ) import Stack.Types.Package ( LocalPackage (..), Package (..), packageIdentifier ) +import Stack.Types.NamedComponent + ( NamedComponent (..), benchComponents, testComponents ) import Stack.Types.Plan - ( Plan (..), Task (..), TaskConfigOpts (..), TaskType (..) - , taskLocation, taskProvides + ( ComponentKey (..), Plan (..), Task (..) + , TaskConfigOpts (..), TaskType (..) + , componentKeyPkgName, taskLocation, taskProvides ) import Stack.Types.Platform ( HasPlatform (..) ) import Stack.Types.Runner ( terminalL, viewExecutablePath ) @@ -286,7 +291,7 @@ executePlan where mlargestPackageName = Set.lookupMax $ - Set.map (length . packageNameString) $ + Set.map (length . packageNameString . componentKeyPkgName) $ Map.keysSet plan.tasks <> Map.keysSet plan.finals copyExecutables :: @@ -406,8 +411,10 @@ executePlan' installedMap0 targets plan ee = do then pure Nothing else Just <$> liftIO (newMVar ()) - let actions = concatMap (toActions installedMap' mtestLock run ee) $ - Map.elems $ Map.merge + let taskKeys = Map.keysSet plan.tasks + actions = concatMap + (uncurry $ toActions installedMap' mtestLock run ee taskKeys) $ + Map.toList $ Map.merge (Map.mapMissing (\_ b -> (Just b, Nothing))) (Map.mapMissing (\_ f -> (Nothing, Just f))) (Map.zipWithMatched (\_ b f -> (Just b, Just f))) @@ -447,7 +454,7 @@ executePlan' installedMap0 targets plan ee = do | otherwise = do inProgress <- readTVarIO actionsVar let packageNames = map - (\(ActionId pkgID _) -> pkgName pkgID) + (\(ActionId ck' _) -> componentKeyPkgName ck') (toList inProgress) nowBuilding :: [PackageName] -> Utf8Builder nowBuilding [] = "" @@ -500,8 +507,10 @@ executePlan' installedMap0 targets plan ee = do when buildOpts.openHaddocks $ do let planPkgs, localPkgs, installedPkgs, availablePkgs :: Map PackageName (PackageIdentifier, InstallLocation) - planPkgs = - Map.map (taskProvides &&& taskLocation) plan.tasks + planPkgs = Map.fromList + [ (componentKeyPkgName ck, (taskProvides t, taskLocation t)) + | (ck, t) <- Map.toList plan.tasks + ] localPkgs = Map.fromList [ (p.name, (packageIdentifier p, Local)) @@ -558,84 +567,88 @@ toActions :: -> Maybe (MVar ()) -> (RIO env () -> IO ()) -> ExecuteEnv + -> Set ComponentKey + -- ^ All component keys that have build tasks (not finals), used to + -- determine intra-package dependencies. + -> ComponentKey -> (Maybe Task, Maybe Task) -- build and final -> [Action] -toActions installedMap mtestLock runInBase ee (mbuild, mfinal) = +toActions installedMap mtestLock runInBase ee taskKeys ck (mbuild, mfinal) = abuild ++ afinal where + intraPackageDep = intraPackageDeps taskKeys ck abuild = case mbuild of Nothing -> [] Just task -> [ Action - { actionId = ActionId (taskProvides task) ATBuild + { actionId = ActionId ck ATBuild , actionDeps = - Set.map (`ActionId` ATBuild) task.configOpts.missing + missingToDeps task.configOpts.missing + <> intraPackageDep + <> Set.fromList + (map (`ActionId` ATBuild) + task.configOpts.instantiationDeps) , action = - \ac -> runInBase $ singleBuild ac ee task installedMap False + \ac -> runInBase $ singleBuild ac ee task installedMap False ck , concurrency = ConcurrencyAllowed } ] afinal = case mfinal of Nothing -> [] - Just task -> - ( if task.allInOne - then id - else (:) Action - { actionId = ActionId pkgId ATBuildFinal - , actionDeps = addBuild - (Set.map (`ActionId` ATBuild) task.configOpts.missing) - , action = - \ac -> runInBase $ singleBuild ac ee task installedMap True - , concurrency = ConcurrencyAllowed - } - ) $ + Just task -> finalBuild ++ finalRun + where + (tests, benches) = finalTestsAndBenches ck (taskComponents task) + buildDep = Set.singleton (ActionId ck ATBuild) + -- When there's no library build task, we need our own build step for + -- tests/benchmarks. When there IS a library build, depend on it instead. + finalBuild = case mbuild of + Just _ -> [] + Nothing -> + [ Action + { actionId = ActionId ck ATBuild + , actionDeps = + missingToDeps task.configOpts.missing + <> intraPackageDep + <> Set.fromList + (map (`ActionId` ATBuild) + task.configOpts.instantiationDeps) + , action = + \ac -> runInBase $ singleBuild ac ee task installedMap True ck + , concurrency = ConcurrencyAllowed + } + ] -- These are the "final" actions - running test suites and benchmarks, -- unless --no-run-tests or --no-run-benchmarks is enabled. - ( if Set.null tests || not runTests - then id - else (:) Action - { actionId = ActionId pkgId ATRunTests - , actionDeps = finalDeps - , action = \ac -> withLock mtestLock $ runInBase $ - singleTest topts (Set.toList tests) ac ee task installedMap - -- Always allow tests tasks to run concurrently with other tasks, - -- particularly build tasks. Note that 'mtestLock' can optionally - -- make it so that only one test is run at a time. - , concurrency = ConcurrencyAllowed - } - ) $ - ( if Set.null benches || not runBenchmarks - then id - else (:) Action - { actionId = ActionId pkgId ATRunBenchmarks - , actionDeps = finalDeps - , action = \ac -> runInBase $ - singleBench - beopts - (Set.toList benches) - ac - ee - task - installedMap - -- Never run benchmarks concurrently with any other task, see - -- #3663 - , concurrency = ConcurrencyDisallowed - } - ) - [] - where - pkgId = taskProvides task - comps = taskComponents task - tests = testComponents comps - benches = benchComponents comps - finalDeps = - if task.allInOne - then addBuild mempty - else Set.singleton (ActionId pkgId ATBuildFinal) - addBuild = - case mbuild of - Nothing -> id - Just _ -> Set.insert $ ActionId pkgId ATBuild + finalRun = + [ Action + { actionId = ActionId ck ATRunTests + , actionDeps = buildDep + , action = \ac -> withLock mtestLock $ runInBase $ + singleTest topts (Set.toList tests) ac ee task installedMap + -- Always allow test tasks to run concurrently with other + -- tasks, particularly build tasks. Note that 'mtestLock' can + -- optionally make it so that only one test is run at a time. + , concurrency = ConcurrencyAllowed + } + | not (Set.null tests) && runTests + ] + ++ [ Action + { actionId = ActionId ck ATRunBenchmarks + , actionDeps = buildDep + , action = \ac -> runInBase $ + singleBench + beopts + (Set.toList benches) + ac + ee + task + installedMap + -- Never run benchmarks concurrently with any other task, see + -- #3663 + , concurrency = ConcurrencyDisallowed + } + | not (Set.null benches) && runBenchmarks + ] withLock Nothing f = f withLock (Just lock) f = withMVar lock $ \() -> f bopts = ee.buildOpts @@ -644,6 +657,39 @@ toActions installedMap mtestLock runInBase ee (mbuild, mfinal) = runTests = topts.runTests runBenchmarks = beopts.runBenchmarks +-- | Map external package dependencies to ActionIds. External dependencies are +-- always library dependencies, so each PackageIdentifier maps to a +-- ComponentKey with CLib. +missingToDeps :: Set PackageIdentifier -> Set ActionId +missingToDeps = Set.map + (\pid -> ActionId (ComponentKey (pkgName pid) CLib) ATBuild) + +-- | Compute intra-package dependencies for a component. Non-CLib components +-- depend on CLib ATBuild of the same package, if a CLib build task exists in +-- the plan. CLib itself has no intra-package deps. +intraPackageDeps :: Set ComponentKey -> ComponentKey -> Set ActionId +intraPackageDeps taskKeys ck = case ck of + ComponentKey _ CLib -> Set.empty + ComponentKey name _ -> + let libKey = ComponentKey name CLib + in if libKey `Set.member` taskKeys + then Set.singleton (ActionId libKey ATBuild) + else Set.empty + +-- | Determine which test suites and benchmarks should be run for a final +-- action. For per-component CTest\/CBench keys, returns exactly that one +-- component. For non-split CLib keys, returns all tests\/benches from the +-- component set. +finalTestsAndBenches :: + ComponentKey + -> Set NamedComponent + -> (Set StackUnqualCompName, Set StackUnqualCompName) + -- ^ (tests, benches) +finalTestsAndBenches ck comps = case ck of + ComponentKey _ (CTest name) -> (Set.singleton name, Set.empty) + ComponentKey _ (CBench name) -> (Set.empty, Set.singleton name) + _ -> (testComponents comps, benchComponents comps) + taskComponents :: Task -> Set NamedComponent taskComponents task = case task.taskType of diff --git a/src/Stack/Build/ExecuteEnv.hs b/src/Stack/Build/ExecuteEnv.hs index dfee2c548e..ccb553d0c3 100644 --- a/src/Stack/Build/ExecuteEnv.hs +++ b/src/Stack/Build/ExecuteEnv.hs @@ -110,7 +110,8 @@ import Stack.Types.Installed ( InstallLocation (..), Installed (..) ) import Stack.Types.Package ( LocalPackage (..), Package (..), packageIdentifier ) import Stack.Types.Plan - ( TaskType (..), taskTypeLocation, taskTypePackageIdentifier + ( TaskType (..), componentKeyPkgName + , taskTypeLocation, taskTypePackageIdentifier ) import Stack.Types.Platform ( HasPlatform (..) ) import Stack.Types.Version ( withinRange ) @@ -599,6 +600,11 @@ withSingleContext :: -> Map PackageIdentifier GhcPkgId -- ^ All dependencies' package ids to provide to Setup.hs. -> Maybe String + -> Maybe Text + -- ^ Optional CInst dist-dir suffix for Backpack instantiation tasks. + -- When 'Just', the @--builddir@ passed to Setup.hs is extended with + -- @inst-\@ so that the instantiated build does not clobber the + -- indefinite package's build artifacts. -> ( Package -- Package info -> Path Abs File -- Cabal file path -> Path Abs Dir -- Package root directory file path @@ -618,6 +624,7 @@ withSingleContext taskType allDeps msuffix + mInstSuffix inner0 = withPackage $ \package cabalFP pkgDir -> withOutputType pkgDir package $ \outputType -> @@ -643,7 +650,7 @@ withSingleContext console = ( wanted && all - (\(ActionId ident _) -> ident == pkgId) + (\(ActionId ck _) -> componentKeyPkgName ck == pkgName pkgId) (Set.toList ac.remaining) && ee.totalWanted == 1 ) @@ -725,6 +732,11 @@ withSingleContext } menv <- liftIO $ config.processContextSettings envSettings distRelativeDir' <- distRelativeDir + buildDir <- case mInstSuffix of + Nothing -> pure distRelativeDir' + Just suffix -> do + instSubDir <- parseRelDir ("inst-" ++ T.unpack suffix) + pure (distRelativeDir' instSubDir) setupexehs <- -- Avoid broken Setup.hs files causing problems for simple build -- types, see: @@ -873,7 +885,7 @@ withSingleContext <> cabalPackageArg setupArgs = - ("--builddir=" ++ toFilePathNoTrailingSep distRelativeDir') : args + ("--builddir=" ++ toFilePathNoTrailingSep buildDir) : args runExe :: Path Abs File -> [String] -> RIO env () runExe exeName fullArgs = do diff --git a/src/Stack/Build/ExecutePackage.hs b/src/Stack/Build/ExecutePackage.hs index b03d1d2b02..a0f16d70d7 100644 --- a/src/Stack/Build/ExecutePackage.hs +++ b/src/Stack/Build/ExecutePackage.hs @@ -16,6 +16,12 @@ module Stack.Build.ExecutePackage ( singleBuild , singleTest , singleBench + , componentTarget + , componentEnableTests + , componentEnableBenchmarks + -- * Backpack helpers (exported for testing) + , findGhcPkgId + , mkInstantiateWithOpts ) where import Control.Concurrent.Execute @@ -33,6 +39,7 @@ import qualified Data.List as L import qualified Data.Map.Strict as Map import qualified Data.Set as Set import qualified Data.Text as T +import Distribution.ModuleName ( ModuleName ) import qualified Distribution.PackageDescription as C import Distribution.System ( OS (..), Platform (..) ) import qualified Distribution.Text as C @@ -55,6 +62,7 @@ import RIO.Process , useHandleOpen, waitExitCode, withModifyEnvVars , withProcessWait, withWorkingDir ) +import Stack.Build.ConstructPlan ( shouldSplitComponents ) import Stack.Build.Cache ( TestStatus (..), deleteCaches, getTestStatus , markExeInstalled, markExeNotInstalled, readPrecompiledCache @@ -82,7 +90,7 @@ import Stack.Coverage ( generateHpcReport, updateTixFile ) import Stack.GhcPkg ( ghcPkg, ghcPkgPathEnvVar, unregisterGhcPkgIds ) import Stack.Package ( buildLogPath, buildableExes, buildableSubLibs - , hasBuildableMainLibrary + , hasBuildableMainLibrary, hasIntraPackageDeps ) import Stack.PackageDump ( conduitDumpPackage, ghcPkgDescribe ) import Stack.Prelude @@ -96,7 +104,9 @@ import Stack.Types.BuildOpts ) import Stack.Types.BuildOptsCLI ( BuildOptsCLI (..) ) import Stack.Types.Cache - ( ConfigCache (..), PrecompiledCache (..) ) + ( ConfigCache (..), ConfigCacheType (..) + , PrecompiledCache (..) + ) import qualified Stack.Types.Cache as ConfigCache ( ConfigCache (..) ) import Stack.Types.CompCollection ( collectionKeyValueList, collectionLookup @@ -123,7 +133,8 @@ import Stack.Types.EnvConfig , appropriateGhcColorFlag ) import Stack.Types.EnvSettings ( EnvSettings (..) ) -import Stack.Types.GhcPkgId ( GhcPkgId, ghcPkgIdToText ) +import Stack.Types.GhcPkgId + ( GhcPkgId, ghcPkgIdString, ghcPkgIdToText ) import Stack.Types.GlobalOpts ( GlobalOpts (..) ) import Stack.Types.Installed ( InstallLocation (..), Installed (..), InstalledMap @@ -131,7 +142,7 @@ import Stack.Types.Installed ) import Stack.Types.IsMutable ( IsMutable (..) ) import Stack.Types.NamedComponent - ( NamedComponent, exeComponents, isCBench, isCTest + ( NamedComponent (..), exeComponents, isCBench, isCTest , renderComponent ) import Stack.Types.Package @@ -141,7 +152,8 @@ import Stack.Types.Package ) import Stack.Types.PackageFile ( PackageWarning (..) ) import Stack.Types.Plan - ( Task (..), TaskConfigOpts (..), TaskType (..), taskIsTarget + ( ComponentKey (..), Task (..), TaskConfigOpts (..) + , TaskType (..), componentKeyPkgName, taskIsTarget , taskLocation, taskProvides, taskTargetIsMutable , taskTypePackageIdentifier ) @@ -204,8 +216,14 @@ getConfigCache ee task installedMap enableTest enableBench = do cOpts.isLocalNonExtraDep cOpts.isMutable pcOpts + instWithOpts = + mkInstantiateWithOpts task.backpackInstEntries allDepsMap configureOpts = configureOpts' - { nonPathRelated = configureOpts'.nonPathRelated ++ map T.unpack extra } + { nonPathRelated = + configureOpts'.nonPathRelated + ++ map T.unpack extra + ++ instWithOpts + } deps = Set.fromList $ Map.elems missing' ++ Map.elems task.present components = case task.taskType of TTLocalMutable lp -> @@ -221,6 +239,40 @@ getConfigCache ee task installedMap enableTest enableBench = do } pure (allDepsMap, cache) +-- | Look up the 'GhcPkgId' for a package by its 'PackageName' in a dependency +-- map keyed by 'PackageIdentifier'. Used to resolve implementing packages for +-- Backpack @--instantiate-with@ flags. +findGhcPkgId :: + Map PackageIdentifier GhcPkgId + -> PackageName + -> Maybe GhcPkgId +findGhcPkgId depsMap pn = + case [gid | (PackageIdentifier n _, gid) <- Map.toList depsMap, n == pn] of + (gid:_) -> Just gid + [] -> Nothing + +-- | Generate @--instantiate-with@ configure flags for CInst (Backpack +-- instantiation) tasks. Each entry maps a signature name to an implementing +-- module identified by its package's 'GhcPkgId'. +-- +-- Format: @--instantiate-with=SigName=\:ImplModuleName@ +mkInstantiateWithOpts :: + [(ModuleName, PackageName, ModuleName)] + -- ^ Backpack instantiation entries: (sigName, implPkgName, implModuleName) + -> Map PackageIdentifier GhcPkgId + -- ^ All dependency GhcPkgIds + -> [String] +mkInstantiateWithOpts entries depsMap = + [ "--instantiate-with=" + ++ C.display sigName + ++ "=" + ++ ghcPkgIdString implGhcPkgId + ++ ":" + ++ C.display implModuleName + | (sigName, implPkgName, implModuleName) <- entries + , Just implGhcPkgId <- [findGhcPkgId depsMap implPkgName] + ] + -- | Ensure that the configuration for the package matches what is given ensureConfig :: HasEnvConfig env @@ -229,6 +281,8 @@ ensureConfig :: -> Path Abs Dir -- ^ package directory -> BuildOpts + -> Maybe Text + -- ^ CInst hash suffix (Nothing for normal builds) -> RIO env () -- ^ announce -> (ExcludeTHLoading -> [String] -> RIO env ()) @@ -237,10 +291,24 @@ ensureConfig :: -- ^ Cabal file -> Task -> RIO env Bool -ensureConfig newConfigCache pkgDir buildOpts announce cabal cabalFP task = do +ensureConfig newConfigCache pkgDir buildOpts mInstSuffix announce cabal cabalFP task = do + -- CInst (Backpack instantiation) tasks share a source directory with the + -- indefinite package but use a separate --builddir. They use a separate + -- config cache entry (ConfigCacheTypeInstantiation) to avoid colliding with + -- the indefinite build's cache. File-based caches (cabalMod, + -- projectRoot) are shared since the .cabal file is the same, but + -- setup-config lives in the inst builddir and must be checked there. + let configCacheType = + maybe ConfigCacheTypeConfig ConfigCacheTypeInstantiation mInstSuffix newCabalMod <- liftIO $ modificationTime <$> getFileStatus (toFilePath cabalFP) - setupConfigfp <- setupConfigFromDir pkgDir + setupConfigfp <- case mInstSuffix of + Nothing -> setupConfigFromDir pkgDir + Just suffix -> do + dist <- distDirFromDir pkgDir + instSubDir <- parseRelDir ("inst-" ++ T.unpack suffix) + setupConfig <- parseRelFile "setup-config" + pure $ dist instSubDir setupConfig let getNewSetupConfigMod = liftIO $ either (const Nothing) (Just . modificationTime) <$> tryJust @@ -267,7 +335,7 @@ ensureConfig newConfigCache pkgDir buildOpts announce cabal cabalFP task = do ignoreComponents cc = cc { ConfigCache.components = Set.empty } -- Determine the old and new Cabal configuration for the package -- directory, to determine if we need to reconfigure. - mOldConfigCache <- tryGetConfigCache pkgDir + mOldConfigCache <- tryGetConfigCache pkgDir configCacheType mOldCabalMod <- tryGetCabalMod pkgDir @@ -290,7 +358,7 @@ ensureConfig newConfigCache pkgDir buildOpts announce cabal cabalFP task = do ensureConfigureScript pkgDir when needConfig $ do - deleteCaches pkgDir + deleteCaches pkgDir configCacheType announce cp <- view compilerPathsL let (GhcPkgExe pkgPath) = cp.pkg @@ -310,18 +378,22 @@ ensureConfig newConfigCache pkgDir buildOpts announce cabal cabalFP task = do -- Configure cabal with arguments determined by -- Stack.Types.Build.configureOpts cabal KeepTHLoading $ "configure" : allOpts - -- Only write the cache for local packages. Remote packages are built in a + -- Only write the cache for local packages. Remote packages are built in a -- temporary directory so the cache would never be used anyway. case task.taskType of - TTLocalMutable{} -> writeConfigCache pkgDir newConfigCache + TTLocalMutable{} -> writeConfigCache pkgDir configCacheType newConfigCache TTRemotePackage{} -> pure () - writeCabalMod pkgDir newCabalMod - -- This file gets updated one more time by the configure step, so get the - -- most recent value. We could instead change our logic above to check if - -- our config mod file is newer than the file above, but this seems - -- reasonable too. - getNewSetupConfigMod >>= writeSetupConfigMod pkgDir - writePackageProjectRoot pkgDir newConfigFileRoot + -- File-based caches are shared with the indefinite build (same .cabal + -- file, same pkgDir). Only write them for normal (non-CInst) tasks to + -- avoid redundant writes. + when (isNothing mInstSuffix) $ do + writeCabalMod pkgDir newCabalMod + -- This file gets updated one more time by the configure step, so get the + -- most recent value. We could instead change our logic above to check if + -- our config mod file is newer than the file above, but this seems + -- reasonable too. + getNewSetupConfigMod >>= writeSetupConfigMod pkgDir + writePackageProjectRoot pkgDir newConfigFileRoot pure needConfig -- | Make a padded prefix for log messages @@ -347,9 +419,7 @@ announceTask ee taskType action = logInfo $ <> action -- | Implements running a package's build, used to implement --- 'Control.Concurrent.Execute.ATBuild' and --- 'Control.Concurrent.Execute.ATBuildFinal' tasks. The latter is a task for --- building a package's benchmarks and test-suites. +-- 'Control.Concurrent.Execute.ATBuild' tasks. -- -- In particular this does the following: -- @@ -374,6 +444,8 @@ singleBuild :: -> InstalledMap -> Bool -- ^ Is this a final build? + -> ComponentKey + -- ^ The component key identifying which component this build is for. -> RIO env () singleBuild ac @@ -381,14 +453,19 @@ singleBuild task installedMap isFinalBuild + ck = do (allDepsMap, cache) <- getConfigCache ee task installedMap enableTests enableBenchmarks let bcoSnapInstallRoot = ee.baseConfigOpts.snapInstallRoot - mprecompiled <- getPrecompiled cache task.taskType bcoSnapInstallRoot + isCInstTask = case ck of + ComponentKey _ (CInst _) -> True + _ -> False + mprecompiled <- getPrecompiled isCInstTask cache task.taskType bcoSnapInstallRoot minstalled <- case mprecompiled of - Just precompiled -> copyPreCompiled ee task pkgId precompiled + Just precompiled -> + copyPreCompiled isCInstTask ee task pkgId precompiled Nothing -> do curator <- view $ buildConfigL . to (.curator) realConfigAndBuild @@ -401,14 +478,24 @@ singleBuild cache curator allDepsMap - whenJust minstalled $ \installed -> do - writeFlagCache installed cache - liftIO $ atomically $ modifyTVar ee.ghcPkgIds $ Map.insert pkgId installed + ck + -- For CInst (Backpack instantiation) tasks, do NOT update the ghcPkgIds + -- TVar. The consumer's --dependency flag must reference the indefinite + -- GhcPkgId (stored by the CLib task). Cabal resolves the instantiation + -- by looking up the package DB using mixin declarations. Writing the + -- flag cache is also skipped to avoid clobbering the indefinite build's + -- cache. + unless isCInstTask $ + whenJust minstalled $ \installed -> do + writeFlagCache installed cache + liftIO $ atomically $ modifyTVar ee.ghcPkgIds $ Map.insert pkgId installed where pkgId = taskProvides task - buildingFinals = isFinalBuild || task.allInOne - enableTests = buildingFinals && any isCTest (taskComponents task) - enableBenchmarks = buildingFinals && any isCBench (taskComponents task) + buildingFinals = isFinalBuild + enableTests = buildingFinals + && componentEnableTests ck (taskComponents task) + enableBenchmarks = buildingFinals + && componentEnableBenchmarks ck (taskComponents task) realConfigAndBuild :: forall env a. HasEnvConfig env @@ -423,6 +510,7 @@ realConfigAndBuild :: -> ConfigCache -> Maybe Curator -> Map PackageIdentifier GhcPkgId + -> ComponentKey -> RIO env (Maybe Installed) realConfigAndBuild ac @@ -434,7 +522,8 @@ realConfigAndBuild cache mcurator0 allDepsMap - = withSingleContext ac ee task.taskType allDepsMap Nothing $ + ck + = withSingleContext ac ee task.taskType allDepsMap Nothing mInstSuffix $ \package cabalFP pkgDir cabal0 announce _outputType -> do let cabal = cabal0 CloseOnException _neededConfig <- @@ -442,6 +531,7 @@ realConfigAndBuild cache pkgDir ee.buildOpts + mInstSuffix (announce ("configure" <> display annSuffix)) cabal cabalFP @@ -474,6 +564,9 @@ realConfigAndBuild where pkgId = taskProvides task PackageIdentifier pname _ = pkgId + mInstSuffix = case ck of + ComponentKey _ (CInst hashSuffix) -> Just hashSuffix + _ -> Nothing doHaddock curator = task.buildHaddocks && not isFinalBuild @@ -481,24 +574,7 @@ realConfigAndBuild -- to fail && maybe True (Set.notMember pname . (.skipHaddock)) curator - annSuffix = if result == "" then "" else " (" <> result <> ")" - where - result = T.intercalate " + " $ concat - [ ["lib" | task.allInOne && hasLib] - , ["sub-lib" | task.allInOne && hasSubLib] - , ["exe" | task.allInOne && hasExe] - , ["test" | enableTests] - , ["bench" | enableBenchmarks] - ] - (hasLib, hasSubLib, hasExe) = case task.taskType of - TTLocalMutable lp -> - let package = lp.package - hasLibrary = hasBuildableMainLibrary package - hasSubLibraries = not $ null package.subLibraries - hasExecutables = not . Set.null $ exesToBuild lp - in (hasLibrary, hasSubLibraries, hasExecutables) - -- This isn't true, but we don't want to have this info for upstream deps. - _ -> (False, False, False) + annSuffix = buildAnnSuffix ck task enableTests enableBenchmarks initialBuildSteps cabal announce = do announce ("initial-build-steps" <> display annSuffix) cabal KeepTHLoading ["repl", "stack-initial-build-steps"] @@ -574,17 +650,8 @@ realConfigAndBuild let stripTHLoading | config.hideTHLoading = ExcludeTHLoading | otherwise = KeepTHLoading - (buildOpts, copyOpts) <- - case (task.taskType, task.allInOne, isFinalBuild) of - (_, True, True) -> throwM AllInOneBuildBug - (TTLocalMutable lp, False, False) -> - let componentOpts = primaryComponentOptions lp - in pure (componentOpts, componentOpts) - (TTLocalMutable lp, False, True) -> pure (finalComponentOptions lp, []) - (TTLocalMutable lp, True, False) -> - let componentOpts = primaryComponentOptions lp - in pure (componentOpts <> finalComponentOptions lp, componentOpts) - (TTRemotePackage{}, _, _) -> pure ([], []) + let (buildOpts, copyOpts) = + buildAndCopyOpts task.taskType ck isFinalBuild cabal stripTHLoading ("build" : buildOpts <> extraOpts) `catch` \ex -> case ex of CabalExitedUnsuccessfully{} -> @@ -697,7 +764,7 @@ postProcessRemotePackage -- in tmp (#3018). let remaining = Set.filter - (\(ActionId x _) -> x == pkgId) + (\(ActionId ck _) -> componentKeyPkgName ck == pkgName pkgId) ac.remaining when (null remaining) $ removeDirRecur pkgDir _ -> pure () @@ -795,19 +862,28 @@ copyDdumpFilesIfNeeded buildingFinals mDdumpPath = when buildingFinals $ getPrecompiled :: HasEnvConfig env - => ConfigCache + => Bool + -- ^ Is this a CInst (Backpack instantiation) task? CInst tasks are + -- always created by addInstantiationTasks even when the instantiated + -- package is already registered. Skip the self-reference check so + -- the precompiled cache can short-circuit the build. + -> ConfigCache -> TaskType -> Path Abs Dir -> RIO env (Maybe (PrecompiledCache Abs)) -getPrecompiled cache taskType bcoSnapInstallRoot = +getPrecompiled isCInst cache taskType bcoSnapInstallRoot = case taskType of TTRemotePackage Immutable _ loc -> readPrecompiledCache loc cache.configureOpts cache.buildHaddocks >>= \case Nothing -> pure Nothing -- Only pay attention to precompiled caches that refer to packages - -- within the snapshot. + -- within the snapshot. For CInst tasks, skip this check: CInst + -- tasks are always created regardless of whether the instantiated + -- package is already installed, and re-registering with --force is + -- harmless. Just pc - | maybe False + | not isCInst + , maybe False (bcoSnapInstallRoot `isProperPrefixOf`) pc.library -> pure Nothing -- If old precompiled cache files are left around but snapshots are @@ -832,19 +908,26 @@ copyPreCompiled :: , HasProcessContext env , HasEnvConfig env ) - => ExecuteEnv + => Bool + -- ^ Is this a CInst (Backpack instantiation) task? If so, skip + -- unregistration — the indefinite package and other instantiations + -- must remain in the DB. + -> ExecuteEnv -> Task -> PackageIdentifier -> PrecompiledCache b0 -> RIO env (Maybe Installed) -copyPreCompiled ee task pkgId (PrecompiledCache mlib subLibs exes) = do +copyPreCompiled isCInst ee task pkgId (PrecompiledCache mlib subLibs exes) = do let PackageIdentifier pname pversion = pkgId announceTask ee task.taskType "using precompiled package" -- We need to copy .conf files for the main library and all sub-libraries -- which exist in the cache, from their old snapshot to the new one. -- However, we must unregister any such library in the new snapshot, in case - -- it was built with different flags. + -- it was built with different flags. For CInst tasks, we skip unregistration + -- because the indefinite package and other instantiations share the same + -- package name and must remain in the DB. The --force flag on register + -- handles any conflicts. let subLibNames = Set.toList $ buildableSubLibs $ case task.taskType of TTLocalMutable lp -> lp.package @@ -870,12 +953,15 @@ copyPreCompiled ee task pkgId (PrecompiledCache mlib subLibs exes) = do let pkgDb = ee.baseConfigOpts.snapDB ghcPkgExe <- getGhcPkgExe -- First unregister, silently, everything that needs to be unregistered. - whenJust (nonEmpty allToUnregister) $ \allToUnregister' -> do - logLevel <- view $ globalOptsL . to (.logLevel) - let isDebug = logLevel == LevelDebug - catchAny - (unregisterGhcPkgIds isDebug ghcPkgExe pkgDb allToUnregister') - (const (pure ())) + -- Skip for CInst tasks to preserve the indefinite and other instantiated + -- entries. + unless isCInst $ + whenJust (nonEmpty allToUnregister) $ \allToUnregister' -> do + logLevel <- view $ globalOptsL . to (.logLevel) + let isDebug = logLevel == LevelDebug + catchAny + (unregisterGhcPkgIds isDebug ghcPkgExe pkgDb allToUnregister') + (const (pure ())) -- There appears to be a bug in the ghc-pkg executable such that, on -- Windows only, it cannot register a package into a package database that -- is also listed in the GHC_PACKAGE_PATH environment variable. See: @@ -937,7 +1023,24 @@ loadInstalledPkg pkgDbs tvar name = do [dp] -> do liftIO $ atomically $ modifyTVar' tvar (Map.insert dp.ghcPkgId dp) pure $ Just dp.ghcPkgId - _ -> throwM $ MultipleResultsBug name dps + -- For Backpack packages, ghc-pkg may return multiple entries for the same + -- component name: an indefinite (signature-only) package and one or more + -- instantiated packages. Pick the last instantiated one (has '+' in its + -- ID) since that contains the compiled code. Multiple instantiations + -- arise when different consumers fill the same signature with different + -- implementations. Register all entries in the dump. + _ -> case filter isInstantiated dps of + [] -> throwM $ MultipleResultsBug name dps + instantiated -> do + forM_ dps $ \d -> + liftIO $ atomically $ modifyTVar' tvar (Map.insert d.ghcPkgId d) + -- Pick the last instantiated entry. With multiple instantiations, the + -- order doesn't matter — the caller (CInst) discards the result anyway. + let dp = L.last instantiated + pure $ Just dp.ghcPkgId + where + isInstantiated dp = + '+' `elem` ghcPkgIdString dp.ghcPkgId fulfillHaddockExpectations :: (MonadUnliftIO m, HasTerm env, MonadReader env m) @@ -996,7 +1099,7 @@ singleTest topts testsToRun ac ee task installedMap = do mcurator <- view $ buildConfigL . to (.curator) let pname = pkgName $ taskProvides task expectFailure = expectTestFailure pname mcurator - withSingleContext ac ee task.taskType allDepsMap (Just "test") $ + withSingleContext ac ee task.taskType allDepsMap (Just "test") Nothing $ \package _cabalfp pkgDir _cabal announce outputType -> do config <- view configL let needHpc = topts.coverage @@ -1275,7 +1378,7 @@ singleBench :: -> RIO env () singleBench beopts benchesToRun ac ee task installedMap = do (allDepsMap, _cache) <- getConfigCache ee task installedMap False True - withSingleContext ac ee task.taskType allDepsMap (Just "bench") $ + withSingleContext ac ee task.taskType allDepsMap (Just "bench") Nothing $ \_package _cabalfp _pkgDir cabal announce _outputType -> do let args = map unqualCompToString benchesToRun <> maybe [] ((:[]) . ("--benchmark-options=" <>)) @@ -1312,6 +1415,92 @@ extraBuildOptions wc bopts semaphore = do else pure $ semaphoreFlag ++ [optsFlag, baseOpts] +-- | Compute the announce suffix for build/configure log messages. For CInst +-- tasks, shows the instantiation details (e.g. @inst:941095d7: Str = impl-pkg@). +-- For other per-component builds, shows the component type. For final builds, +-- shows test/bench. +buildAnnSuffix :: + ComponentKey + -> Task + -> Bool -- ^ enableTests + -> Bool -- ^ enableBenchmarks + -> Text +buildAnnSuffix ck task enableTests enableBenchmarks = case ck of + ComponentKey _ (CInst hash) -> + " (inst:" <> T.take 8 hash <> instDetail <> ")" + ComponentKey _ comp + | not (isCLib comp) -> " (" <> renderComponent comp <> ")" + _ | result /= "" -> " (" <> result <> ")" + _ -> "" + where + instDetail + | null task.backpackInstEntries = "" + | otherwise = ": " <> T.intercalate ", " + [ T.pack (C.display sig) + <> " = " + <> T.pack (packageNameString impl) + <> if sig == implMod then "" else ":" <> T.pack (C.display implMod) + | (sig, impl, implMod) <- task.backpackInstEntries + ] + result = T.intercalate " + " $ + ["test" | enableTests] ++ ["bench" | enableBenchmarks] + isCLib CLib = True + isCLib _ = False + +-- | Compute the Cabal build and copy target options for a component. For +-- packages with intra-package deps (e.g. Backpack), omits targets so Cabal +-- handles ordering. For per-component builds, passes a single target. For +-- remote packages, no targets are needed. +buildAndCopyOpts :: + TaskType + -> ComponentKey + -> Bool -- ^ isFinalBuild + -> ([String], [String]) + -- ^ (buildOpts, copyOpts) +buildAndCopyOpts taskType ck isFinalBuild = case taskType of + TTLocalMutable lp + | hasIntraPackageDeps lp.package -> ([], []) + | shouldSplitComponents lp.package + , ComponentKey pn comp <- ck -> + let target = [componentTarget pn comp] + in if isFinalBuild then (target, []) else (target, target) + -- Legacy: non-splittable local packages use original multi-component + -- target logic (Custom/Configure/Make build types, Backpack). + | isFinalBuild -> (finalComponentOptions lp, []) + | otherwise -> + let componentOpts = primaryComponentOptions lp + in (componentOpts, componentOpts) + TTRemotePackage{} -> ([], []) + +-- | Render a 'NamedComponent' as a Cabal build target string. This uses +-- Cabal's target syntax (e.g. @lib:pkg-name@, @exe:my-exe@, @test:my-test@). +componentTarget :: PackageName -> NamedComponent -> String +componentTarget pn CLib = "lib:" ++ packageNameString pn +componentTarget _ (CSubLib x) = "lib:" ++ unqualCompToString x +componentTarget _ (CFlib x) = "flib:" ++ unqualCompToString x +componentTarget _ (CExe x) = "exe:" ++ unqualCompToString x +componentTarget _ (CTest x) = "test:" ++ unqualCompToString x +componentTarget _ (CBench x) = "bench:" ++ unqualCompToString x +componentTarget _ (CInst _) = "" + +-- | Should tests be enabled (Cabal @--enable-tests@) for a given +-- 'ComponentKey'? For per-component CTest keys, always True. For non-split +-- CLib keys, checks the full component set. For all other keys, False. +componentEnableTests :: ComponentKey -> Set NamedComponent -> Bool +componentEnableTests ck comps = case ck of + ComponentKey _ (CTest _) -> True + ComponentKey _ CLib -> any isCTest comps + _ -> False + +-- | Should benchmarks be enabled (Cabal @--enable-benchmarks@) for a given +-- 'ComponentKey'? For per-component CBench keys, always True. For non-split +-- CLib keys, checks the full component set. For all other keys, False. +componentEnableBenchmarks :: ComponentKey -> Set NamedComponent -> Bool +componentEnableBenchmarks ck comps = case ck of + ComponentKey _ (CBench _) -> True + ComponentKey _ CLib -> any isCBench comps + _ -> False + -- Library, sub-library, foreign library and executable build components. primaryComponentOptions :: LocalPackage -> [String] primaryComponentOptions lp = diff --git a/src/Stack/Build/Target.hs b/src/Stack/Build/Target.hs index 26cf63ee21..72ac490e26 100644 --- a/src/Stack/Build/Target.hs +++ b/src/Stack/Build/Target.hs @@ -270,6 +270,7 @@ resolveRawTarget sma allLocs (rawInput, rt) = -- 'ComponentName' isCompNamed :: ComponentName -> NamedComponent -> Bool isCompNamed _ CLib = False + isCompNamed _ CInst{} = False isCompNamed t1 t2 = case t2 of (CSubLib t2') -> t1' == t2' (CExe t2') -> t1' == t2' diff --git a/src/Stack/Component.hs b/src/Stack/Component.hs index 175ed76e02..135f131294 100644 --- a/src/Stack/Component.hs +++ b/src/Stack/Component.hs @@ -68,6 +68,7 @@ stackLibraryFromCabal cabalLib = StackLibrary LSubLibName v -> fromCabalName v , buildInfo = stackBuildInfoFromCabal cabalLib.libBuildInfo , exposedModules = cabalLib.exposedModules + , signatures = cabalLib.signatures } stackExecutableFromCabal :: Executable -> StackExecutable @@ -122,6 +123,7 @@ stackBuildInfoFromCabal buildInfoV = gatherComponentToolsAndDepsFromCabal , extraLibs = buildInfoV.extraLibs , extraLibDirs = map interpretSymbolicPathCWD buildInfoV.extraLibDirs , frameworks = map interpretSymbolicPathCWD buildInfoV.frameworks + , mixins = buildInfoV.mixins } -- | Iterate on all three dependency list given, and transform and sort them diff --git a/src/Stack/ComponentFile.hs b/src/Stack/ComponentFile.hs index d8bf8ad49a..54a23511e6 100644 --- a/src/Stack/ComponentFile.hs +++ b/src/Stack/ComponentFile.hs @@ -61,7 +61,7 @@ import Stack.Types.Component , StackTestSuite (..), StackUnqualCompName (..) ) import Stack.Types.ComponentUtils - ( emptyCompName, unqualCompToString ) + ( emptyCompName, unqualCompFromText, unqualCompToString ) import Stack.Types.Config ( Config (..), HasConfig (..), prettyStackDevL ) import Stack.Types.NamedComponent ( NamedComponent (..) ) @@ -375,6 +375,7 @@ componentOutputDir namedComponent distDir = CExe name -> makeTmp name CTest name -> makeTmp name CBench name -> makeTmp name + CInst x -> makeTmp (unqualCompFromText x) where makeTmp name = buildDir distDir componentNameToDirNormOrTmp True name @@ -603,6 +604,7 @@ componentBuildDir component distDir = case component of CExe name -> buildDir distDir componentNameToDir name CTest name -> buildDir distDir componentNameToDir name CBench name -> buildDir distDir componentNameToDir name + CInst x -> buildDir distDir componentNameToDir (unqualCompFromText x) -- Internal helper to define resolveFileOrWarn and resolveDirOrWarn resolveOrWarn :: diff --git a/src/Stack/Package.hs b/src/Stack/Package.hs index a30ea47146..2530c01e70 100644 --- a/src/Stack/Package.hs +++ b/src/Stack/Package.hs @@ -34,6 +34,8 @@ module Stack.Package , listOfPackageDeps , setOfPackageDeps , topSortPackageComponent + , hasIntraPackageDeps + , packageIsIndefinite ) where import qualified Data.Map.Strict as M @@ -912,3 +914,35 @@ topProcessPackageComponent package target fn res = do processSubLibDep r = foldr' processSingleSubLib r subLibDeps processSubLibDep (processMainLibDep res') _ -> res' + +-- | Does this package have intra-package component dependencies that indicate +-- Backpack or chained sub-libraries? Specifically, checks whether the main +-- library or any sub-library depends on the same package (indicating the +-- library uses internal sub-libs, or sub-libraries chain among themselves). +-- +-- Executables, tests, and benchmarks commonly depend on their own package's +-- library via @build-depends: pkg-name@; this is normal and does NOT count as +-- an intra-package dep for our purposes, because @cabal build exe:foo@ handles +-- building the library first automatically. +-- +-- When 'True', @Setup.hs build@ should be called without explicit component +-- targets so that Cabal's internal build ordering (which handles Backpack +-- instantiation) is used. +hasIntraPackageDeps :: Package -> Bool +hasIntraPackageDeps package = + packageIsIndefinite package || any hasSelfDep libraryBuildInfos + where + pname = package.name + hasSelfDep sbi = M.member pname sbi.dependency + libraryBuildInfos :: [Component.StackBuildInfo] + libraryBuildInfos = + maybe [] (\l -> [l.buildInfo]) package.library + ++ map (.buildInfo) (toList package.subLibraries) + +-- | Is this package indefinite (has unfilled Backpack signatures)? +packageIsIndefinite :: Package -> Bool +packageIsIndefinite pkg = any hasSignatures allLibs + where + hasSignatures lib = not (null lib.signatures) + allLibs = maybe [] (:[]) pkg.library + ++ toList pkg.subLibraries diff --git a/src/Stack/SDist.hs b/src/Stack/SDist.hs index a27f117ad6..2e85cddcb7 100644 --- a/src/Stack/SDist.hs +++ b/src/Stack/SDist.hs @@ -489,7 +489,7 @@ getSDistFileList lp deps = [] [] [] Nothing -- provide empty list of globals. This is a hack around -- custom Setup.hs files $ \ee -> - withSingleContext ac ee taskType deps (Just "sdist") $ + withSingleContext ac ee taskType deps (Just "sdist") Nothing $ \_package cabalFP _pkgDir cabal _announce _outputType -> do let outFile = toFilePath tmpdir FP. "source-files-list" cabal diff --git a/src/Stack/Types/Build/ConstructPlan.hs b/src/Stack/Types/Build/ConstructPlan.hs index 1f94cfbcbb..9d36ba2ecc 100644 --- a/src/Stack/Types/Build/ConstructPlan.hs +++ b/src/Stack/Types/Build/ConstructPlan.hs @@ -54,7 +54,7 @@ import Stack.Types.Package ) import Stack.Types.ParentMap ( ParentMap ) import Stack.Types.Plan - ( Task (..), TaskType (..), taskProvides ) + ( ComponentKey, Task (..), TaskType (..), taskProvides ) import Stack.Types.Platform ( HasPlatform (..) ) import Stack.Types.Runner ( HasRunner (..) ) @@ -95,9 +95,9 @@ type M = -- | Type representing values used as the output to be collected during the -- construction of a build plan. data W = W - { wFinals :: !(Map PackageName (Either ConstructPlanException Task)) - -- ^ A dictionary of package names, and either a final task to perform when - -- building the package or an exception. + { wFinals :: !(Map ComponentKey (Either ConstructPlanException Task)) + -- ^ A dictionary of component keys, and either a final task to perform when + -- building the component or an exception. , wInstall :: !(Map StackUnqualCompName InstallLocation) -- ^ A dictionary of executables to be installed, and location where the -- executable's binary is placed. diff --git a/src/Stack/Types/Build/Exception.hs b/src/Stack/Types/Build/Exception.hs index e5f375aecf..51fcd8ce8b 100644 --- a/src/Stack/Types/Build/Exception.hs +++ b/src/Stack/Types/Build/Exception.hs @@ -76,7 +76,6 @@ data BuildException | CouldNotLockDistDir !(Path Abs File) | TaskCycleBug PackageIdentifier | PackageIdMissingBug PackageIdentifier - | AllInOneBuildBug | MultipleResultsBug PackageName [DumpPackage] | TemplateHaskellNotFoundBug | HaddockIndexNotFound @@ -212,8 +211,6 @@ instance Exception BuildException where displayException (PackageIdMissingBug ident) = bugReport "[S-8923]" $ "singleBuild: missing package ID missing: " ++ show ident - displayException AllInOneBuildBug = bugReport "[S-7371]" - "Cannot have an all-in-one build that also has a final build step." displayException (MultipleResultsBug name dps) = bugReport "[S-6739]" $ "singleBuild: multiple results when describing installed package " ++ show (name, dps) diff --git a/src/Stack/Types/Cache.hs b/src/Stack/Types/Cache.hs index 154b6499b5..474a2610c5 100644 --- a/src/Stack/Types/Cache.hs +++ b/src/Stack/Types/Cache.hs @@ -40,6 +40,8 @@ data ConfigCacheType -- ^ Library Cabal flag cache. | ConfigCacheTypeFlagExecutable PackageIdentifier -- ^ Executable Cabal flag cache. + | ConfigCacheTypeInstantiation Text + -- ^ Backpack CInst instantiation cache, keyed by hash suffix. deriving (Eq, Show) instance PersistField ConfigCacheType where @@ -48,10 +50,13 @@ instance PersistField ConfigCacheType where PersistText $ "lib:" <> ghcPkgIdToText v toPersistValue (ConfigCacheTypeFlagExecutable v) = PersistText $ "exe:" <> T.pack (packageIdentifierString v) + toPersistValue (ConfigCacheTypeInstantiation v) = + PersistText $ "inst:" <> v fromPersistValue (PersistText t) = fromMaybe (Left $ "Unexpected ConfigCacheType value: " <> t) $ config <|> fmap lib (T.stripPrefix "lib:" t) <|> - fmap exe (T.stripPrefix "exe:" t) + fmap exe (T.stripPrefix "exe:" t) <|> + fmap inst (T.stripPrefix "inst:" t) where config | t == "config" = Just (Right ConfigCacheTypeConfig) @@ -64,6 +69,7 @@ instance PersistField ConfigCacheType where maybe (Left $ "Unexpected ConfigCacheType value: " <> t) Right $ parsePackageIdentifier (T.unpack v) Right $ ConfigCacheTypeFlagExecutable pkgId + inst v = Right $ ConfigCacheTypeInstantiation v fromPersistValue _ = Left "Unexpected ConfigCacheType type" instance PersistFieldSql ConfigCacheType where diff --git a/src/Stack/Types/Component.hs b/src/Stack/Types/Component.hs index 28c9e1b550..c6309a1605 100644 --- a/src/Stack/Types/Component.hs +++ b/src/Stack/Types/Component.hs @@ -30,6 +30,7 @@ import Distribution.Compiler ( PerCompilerFlavor ) import Distribution.ModuleName ( ModuleName ) import Distribution.PackageDescription ( BenchmarkInterface, Dependency, TestSuiteInterface ) +import Distribution.Types.Mixin ( Mixin ) import Distribution.Simple ( Extension, Language ) import Distribution.Utils.Path ( Pkg, Source, SymbolicPath ) import qualified Distribution.Utils.Path as Cabal @@ -55,6 +56,8 @@ data StackLibrary = StackLibrary , buildInfo :: !StackBuildInfo , exposedModules :: [ModuleName] -- |^ This is only used for gathering the files related to this component. + , signatures :: [ModuleName] + -- ^ Backpack signature module names. Non-empty for indefinite packages. } deriving Show @@ -157,6 +160,8 @@ data StackBuildInfo = StackBuildInfo -- ^ Only used in opts gathering. , frameworks :: [String] -- ^ Only used in opts gathering. + , mixins :: [Mixin] + -- ^ Backpack mixin declarations. Used for cross-package Backpack support. } deriving Show diff --git a/src/Stack/Types/GhcPkgId.hs b/src/Stack/Types/GhcPkgId.hs index d66886f05d..5334533064 100644 --- a/src/Stack/Types/GhcPkgId.hs +++ b/src/Stack/Types/GhcPkgId.hs @@ -86,7 +86,8 @@ parseGhcPkgId x = go x -- | A parser for a package-version-hash pair. ghcPkgIdParser :: Parser GhcPkgId ghcPkgIdParser = - let elements = "_.-" :: String + -- '+' is needed for Backpack instantiated unit IDs (e.g. "pkg-id-str-sig+hash") + let elements = "_.-+" :: String in GhcPkgId . mkUnitId <$> many1 (choice [alphaNum, satisfy (`elem` elements)]) diff --git a/src/Stack/Types/NamedComponent.hs b/src/Stack/Types/NamedComponent.hs index dac24005e8..3235f4235e 100644 --- a/src/Stack/Types/NamedComponent.hs +++ b/src/Stack/Types/NamedComponent.hs @@ -25,6 +25,7 @@ module Stack.Types.NamedComponent , isCExe , isCTest , isCBench + , isCInst , isPotentialDependency , splitComponents ) where @@ -50,6 +51,8 @@ data NamedComponent -- A named test-suite component. | CBench !StackUnqualCompName -- A named benchmark component. + | CInst !Text + -- A Backpack instantiation (hash suffix identifying the instantiation). deriving (Eq, Ord, Show) -- | Render a component to anything with an "IsString" instance. For 'Text' @@ -64,6 +67,7 @@ renderComponent (CFlib x) = "flib:" <> unqualCompToText x renderComponent (CExe x) = "exe:" <> unqualCompToText x renderComponent (CTest x) = "test:" <> unqualCompToText x renderComponent (CBench x) = "bench:" <> unqualCompToText x +renderComponent (CInst x) = "inst:" <> x componentCachePath :: NamedComponent -> String componentCachePath CLib = "lib" @@ -72,6 +76,7 @@ componentCachePath (CFlib x) = "flib-" <> unqualCompToString x componentCachePath (CExe x) = "exe-" <> unqualCompToString x componentCachePath (CTest x) = "test-" <> unqualCompToString x componentCachePath (CBench x) = "bench-" <> unqualCompToString x +componentCachePath (CInst x) = "inst-" <> T.unpack x renderPkgComponents :: [(PackageName, NamedComponent)] -> Text renderPkgComponents = T.intercalate " " . map renderPkgComponent @@ -124,8 +129,12 @@ isCBench :: NamedComponent -> Bool isCBench CBench{} = True isCBench _ = False +isCInst :: NamedComponent -> Bool +isCInst CInst{} = True +isCInst _ = False + isPotentialDependency :: NamedComponent -> Bool -isPotentialDependency v = isCLib v || isCSubLib v || isCExe v +isPotentialDependency v = isCLib v || isCSubLib v || isCExe v || isCInst v -- | A function to split the given list of components into sets of the names of -- the named components by the type of component (sub-libraries, executables, @@ -155,3 +164,5 @@ splitComponents = go s e t b (CExe x : xs) = go s (e . (x:)) t b xs go s e t b (CTest x : xs) = go s e (t . (x:)) b xs go s e t b (CBench x : xs) = go s e t (b . (x:)) xs + -- Ignore CInst (Backpack instantiation), like CFlib. + go s e t b (CInst _ : xs) = go s e t b xs diff --git a/src/Stack/Types/Plan.hs b/src/Stack/Types/Plan.hs index 8789f3f0d9..37e851e110 100644 --- a/src/Stack/Types/Plan.hs +++ b/src/Stack/Types/Plan.hs @@ -17,6 +17,8 @@ module Stack.Types.Plan , Task (..) , TaskType (..) , TaskConfigOpts (..) + , ComponentKey (..) + , componentKeyPkgName , taskAnyMissing , taskIsTarget , taskLocation @@ -28,6 +30,7 @@ module Stack.Types.Plan ) where import Data.List as L +import Distribution.ModuleName ( ModuleName ) import qualified RIO.Set as Set import Stack.Prelude import Stack.Types.Cache ( CachePkgSrc ) @@ -37,15 +40,26 @@ import Stack.Types.ConfigureOpts import Stack.Types.EnvConfig ( EnvConfig ) import Stack.Types.GhcPkgId ( GhcPkgId ) import Stack.Types.IsMutable ( IsMutable (..) ) +import Stack.Types.NamedComponent ( NamedComponent ) import Stack.Types.Package ( InstallLocation (..), LocalPackage (..), Package (..) , packageIdentifier ) +-- | A key that identifies a single buildable unit in the plan. For most +-- packages this is @ComponentKey pkgName CLib@. When per-component building +-- is enabled (Phase 2 Backpack support), each component gets its own key. +data ComponentKey = ComponentKey !PackageName !NamedComponent + deriving (Eq, Ord, Show) + +-- | Extract the package name from a 'ComponentKey'. +componentKeyPkgName :: ComponentKey -> PackageName +componentKeyPkgName (ComponentKey n _) = n + -- | A complete plan of what needs to be built and how to do it data Plan = Plan - { tasks :: !(Map PackageName Task) - , finals :: !(Map PackageName Task) + { tasks :: !(Map ComponentKey Task) + , finals :: !(Map ComponentKey Task) -- ^ Final actions to be taken (test, benchmark, etc) , unregisterLocal :: !(Map GhcPkgId (PackageIdentifier, Text)) -- ^ Text is reason we're unregistering, for display only @@ -66,12 +80,13 @@ data Task = Task , present :: !(Map PackageIdentifier GhcPkgId) -- ^ A dictionary of the package identifiers of already-installed -- dependencies, and their 'GhcPkgId'. - , allInOne :: !Bool - -- ^ indicates that the package can be built in one step , cachePkgSrc :: !CachePkgSrc , buildTypeConfig :: !Bool -- ^ Is the build type of this package Configure. Check out -- ensureConfigureScript in Stack.Build.Execute for the motivation + , backpackInstEntries :: ![(ModuleName, PackageName, ModuleName)] + -- ^ For CInst tasks: [(sigName, implPkgName, implModuleName)]. + -- Empty for non-instantiation tasks. } deriving Show @@ -88,11 +103,14 @@ data TaskType data TaskConfigOpts = TaskConfigOpts { missing :: !(Set PackageIdentifier) -- ^ Dependencies for which we don't yet have a 'GhcPkgId' - , envConfig :: !EnvConfig - , baseConfigOpts :: !BaseConfigOpts + , envConfig :: EnvConfig + , baseConfigOpts :: BaseConfigOpts , isLocalNonExtraDep :: !Bool , isMutable :: !IsMutable , pkgConfigOpts :: PackageConfigureOpts + , instantiationDeps :: ![ComponentKey] + -- ^ Additional deps on CInst tasks. Added to action deps at execution. + -- Empty for most tasks. } instance Show TaskConfigOpts where diff --git a/stack.cabal b/stack.cabal index d6671e9a9d..d553f21561 100644 --- a/stack.cabal +++ b/stack.cabal @@ -216,6 +216,7 @@ library Path.Find Stack Stack.Build + Stack.Build.Backpack Stack.Build.Cache Stack.Build.ConstructPlan Stack.Build.Execute @@ -747,6 +748,7 @@ test-suite stack-unit-test main-is: Spec.hs other-modules: Stack.ArgsSpec + Stack.Build.ConstructPlanSpec Stack.Build.ExecuteSpec Stack.Build.TargetSpec Stack.Config.DockerSpec @@ -757,6 +759,7 @@ test-suite stack-unit-test Stack.LockSpec Stack.NixSpec Stack.PackageDumpSpec + Stack.Types.PlanSpec Stack.Types.TemplateNameSpec Stack.UploadSpec Paths_stack diff --git a/tests/integration/tests/backpack-cross-package-multi-inst/Main.hs b/tests/integration/tests/backpack-cross-package-multi-inst/Main.hs new file mode 100644 index 0000000000..8b3196e3b4 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-multi-inst/Main.hs @@ -0,0 +1,24 @@ +import Control.Monad (unless) +import Data.List (isInfixOf) +import StackTest + +-- Test multiple instantiations: two consumers fill the same sig-pkg with +-- different implementations. Each consumer should get its own CInst task. +main :: IO () +main = do + stack ["build"] + + stackCheckStdout ["exec", "multi-inst-demo"] $ \out -> do + unless ("A says: Hello from impl-a" `isInfixOf` out) $ + error $ "Expected 'A says: Hello from impl-a' in output, got: " ++ show out + unless ("B says: Hello from impl-b" `isInfixOf` out) $ + error $ "Expected 'B says: Hello from impl-b' in output, got: " ++ show out + + -- Rebuild should succeed + stack ["build"] + + stackCheckStdout ["exec", "multi-inst-demo"] $ \out -> do + unless ("A says: Hello from impl-a" `isInfixOf` out) $ + error $ "Expected 'A says: Hello from impl-a' after rebuild, got: " ++ show out + unless ("B says: Hello from impl-b" `isInfixOf` out) $ + error $ "Expected 'B says: Hello from impl-b' after rebuild, got: " ++ show out diff --git a/tests/integration/tests/backpack-cross-package-multi-inst/files/app/app.cabal b/tests/integration/tests/backpack-cross-package-multi-inst/files/app/app.cabal new file mode 100644 index 0000000000..be01e3c637 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-multi-inst/files/app/app.cabal @@ -0,0 +1,13 @@ +cabal-version: 2.0 +name: app +version: 0.1.0.0 +build-type: Simple + +executable multi-inst-demo + main-is: Main.hs + build-depends: + base >= 4.7 && < 5, + consumer-a, + consumer-b + hs-source-dirs: app + default-language: Haskell2010 diff --git a/tests/integration/tests/backpack-cross-package-multi-inst/files/app/app/Main.hs b/tests/integration/tests/backpack-cross-package-multi-inst/files/app/app/Main.hs new file mode 100644 index 0000000000..8ac221522a --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-multi-inst/files/app/app/Main.hs @@ -0,0 +1,9 @@ +module Main where + +import ConsumerA (helloA) +import ConsumerB (helloB) + +main :: IO () +main = do + putStrLn helloA + putStrLn helloB diff --git a/tests/integration/tests/backpack-cross-package-multi-inst/files/consumer-a/consumer-a.cabal b/tests/integration/tests/backpack-cross-package-multi-inst/files/consumer-a/consumer-a.cabal new file mode 100644 index 0000000000..70f36bfb47 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-multi-inst/files/consumer-a/consumer-a.cabal @@ -0,0 +1,15 @@ +cabal-version: 2.0 +name: consumer-a +version: 0.1.0.0 +build-type: Simple + +library + exposed-modules: ConsumerA + build-depends: + base >= 4.7 && < 5, + sig-pkg, + impl-a + mixins: + sig-pkg requires (Str as Str) + hs-source-dirs: src + default-language: Haskell2010 diff --git a/tests/integration/tests/backpack-cross-package-multi-inst/files/consumer-a/src/ConsumerA.hs b/tests/integration/tests/backpack-cross-package-multi-inst/files/consumer-a/src/ConsumerA.hs new file mode 100644 index 0000000000..0c0e9ca6c4 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-multi-inst/files/consumer-a/src/ConsumerA.hs @@ -0,0 +1,6 @@ +module ConsumerA where + +import Str (greeting) + +helloA :: String +helloA = "A says: " ++ greeting diff --git a/tests/integration/tests/backpack-cross-package-multi-inst/files/consumer-b/consumer-b.cabal b/tests/integration/tests/backpack-cross-package-multi-inst/files/consumer-b/consumer-b.cabal new file mode 100644 index 0000000000..b76b66258a --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-multi-inst/files/consumer-b/consumer-b.cabal @@ -0,0 +1,15 @@ +cabal-version: 2.0 +name: consumer-b +version: 0.1.0.0 +build-type: Simple + +library + exposed-modules: ConsumerB + build-depends: + base >= 4.7 && < 5, + sig-pkg, + impl-b + mixins: + sig-pkg requires (Str as Str) + hs-source-dirs: src + default-language: Haskell2010 diff --git a/tests/integration/tests/backpack-cross-package-multi-inst/files/consumer-b/src/ConsumerB.hs b/tests/integration/tests/backpack-cross-package-multi-inst/files/consumer-b/src/ConsumerB.hs new file mode 100644 index 0000000000..0ca5225c98 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-multi-inst/files/consumer-b/src/ConsumerB.hs @@ -0,0 +1,6 @@ +module ConsumerB where + +import Str (greeting) + +helloB :: String +helloB = "B says: " ++ greeting diff --git a/tests/integration/tests/backpack-cross-package-multi-inst/files/impl-a/impl-a.cabal b/tests/integration/tests/backpack-cross-package-multi-inst/files/impl-a/impl-a.cabal new file mode 100644 index 0000000000..3e096774ad --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-multi-inst/files/impl-a/impl-a.cabal @@ -0,0 +1,10 @@ +cabal-version: 2.0 +name: impl-a +version: 0.1.0.0 +build-type: Simple + +library + exposed-modules: Str + build-depends: base >= 4.7 && < 5 + hs-source-dirs: src + default-language: Haskell2010 diff --git a/tests/integration/tests/backpack-cross-package-multi-inst/files/impl-a/src/Str.hs b/tests/integration/tests/backpack-cross-package-multi-inst/files/impl-a/src/Str.hs new file mode 100644 index 0000000000..274c2be1c9 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-multi-inst/files/impl-a/src/Str.hs @@ -0,0 +1,4 @@ +module Str where + +greeting :: String +greeting = "Hello from impl-a" diff --git a/tests/integration/tests/backpack-cross-package-multi-inst/files/impl-b/impl-b.cabal b/tests/integration/tests/backpack-cross-package-multi-inst/files/impl-b/impl-b.cabal new file mode 100644 index 0000000000..b4bf61ce67 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-multi-inst/files/impl-b/impl-b.cabal @@ -0,0 +1,10 @@ +cabal-version: 2.0 +name: impl-b +version: 0.1.0.0 +build-type: Simple + +library + exposed-modules: Str + build-depends: base >= 4.7 && < 5 + hs-source-dirs: src + default-language: Haskell2010 diff --git a/tests/integration/tests/backpack-cross-package-multi-inst/files/impl-b/src/Str.hs b/tests/integration/tests/backpack-cross-package-multi-inst/files/impl-b/src/Str.hs new file mode 100644 index 0000000000..addddbb467 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-multi-inst/files/impl-b/src/Str.hs @@ -0,0 +1,4 @@ +module Str where + +greeting :: String +greeting = "Hello from impl-b" diff --git a/tests/integration/tests/backpack-cross-package-multi-inst/files/sig-pkg/sig-pkg.cabal b/tests/integration/tests/backpack-cross-package-multi-inst/files/sig-pkg/sig-pkg.cabal new file mode 100644 index 0000000000..7ce99e3c8f --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-multi-inst/files/sig-pkg/sig-pkg.cabal @@ -0,0 +1,10 @@ +cabal-version: 2.0 +name: sig-pkg +version: 0.1.0.0 +build-type: Simple + +library + signatures: Str + build-depends: base >= 4.7 && < 5 + hs-source-dirs: src + default-language: Haskell2010 diff --git a/tests/integration/tests/backpack-cross-package-multi-inst/files/sig-pkg/src/Str.hsig b/tests/integration/tests/backpack-cross-package-multi-inst/files/sig-pkg/src/Str.hsig new file mode 100644 index 0000000000..edb56a4a7d --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-multi-inst/files/sig-pkg/src/Str.hsig @@ -0,0 +1,3 @@ +signature Str where + +greeting :: String diff --git a/tests/integration/tests/backpack-cross-package-multi-inst/files/stack.yaml b/tests/integration/tests/backpack-cross-package-multi-inst/files/stack.yaml new file mode 100644 index 0000000000..52bc41f10e --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-multi-inst/files/stack.yaml @@ -0,0 +1,8 @@ +snapshot: ghc-9.10.3 +packages: + - sig-pkg + - impl-a + - impl-b + - consumer-a + - consumer-b + - app diff --git a/tests/integration/tests/backpack-cross-package-rename/Main.hs b/tests/integration/tests/backpack-cross-package-rename/Main.hs new file mode 100644 index 0000000000..9fa1a77c97 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-rename/Main.hs @@ -0,0 +1,14 @@ +import Control.Monad (unless) +import Data.List (isInfixOf) +import StackTest + +-- Test cross-package Backpack with module renaming: sig-pkg has a "Sig" +-- signature, impl-pkg exposes "Impl", and consumer-pkg uses +-- mixins: sig-pkg requires (Sig as Impl) to wire them together. +main :: IO () +main = do + stack ["build"] + + stackCheckStdout ["exec", "consumer-demo"] $ \out -> + unless ("Renamed module works" `isInfixOf` out) $ + error $ "Expected 'Renamed module works' in output, got: " ++ show out diff --git a/tests/integration/tests/backpack-cross-package-rename/files/consumer-pkg/app/Main.hs b/tests/integration/tests/backpack-cross-package-rename/files/consumer-pkg/app/Main.hs new file mode 100644 index 0000000000..4c26592390 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-rename/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-cross-package-rename/files/consumer-pkg/consumer-pkg.cabal b/tests/integration/tests/backpack-cross-package-rename/files/consumer-pkg/consumer-pkg.cabal new file mode 100644 index 0000000000..1c7ecd0e8e --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-rename/files/consumer-pkg/consumer-pkg.cabal @@ -0,0 +1,23 @@ +cabal-version: 2.0 +name: consumer-pkg +version: 0.1.0.0 +build-type: Simple + +library + exposed-modules: Consumer + build-depends: + base >= 4.7 && < 5, + sig-pkg, + impl-pkg + mixins: + sig-pkg requires (Sig as Impl) + hs-source-dirs: src + default-language: Haskell2010 + +executable consumer-demo + main-is: Main.hs + build-depends: + base >= 4.7 && < 5, + consumer-pkg + hs-source-dirs: app + default-language: Haskell2010 diff --git a/tests/integration/tests/backpack-cross-package-rename/files/consumer-pkg/src/Consumer.hs b/tests/integration/tests/backpack-cross-package-rename/files/consumer-pkg/src/Consumer.hs new file mode 100644 index 0000000000..0439a8a006 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-rename/files/consumer-pkg/src/Consumer.hs @@ -0,0 +1,6 @@ +module Consumer where + +import Sig (message) + +hello :: String +hello = message diff --git a/tests/integration/tests/backpack-cross-package-rename/files/impl-pkg/impl-pkg.cabal b/tests/integration/tests/backpack-cross-package-rename/files/impl-pkg/impl-pkg.cabal new file mode 100644 index 0000000000..43b2ce5a7b --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-rename/files/impl-pkg/impl-pkg.cabal @@ -0,0 +1,10 @@ +cabal-version: 2.0 +name: impl-pkg +version: 0.1.0.0 +build-type: Simple + +library + exposed-modules: Impl + build-depends: base >= 4.7 && < 5 + hs-source-dirs: src + default-language: Haskell2010 diff --git a/tests/integration/tests/backpack-cross-package-rename/files/impl-pkg/src/Impl.hs b/tests/integration/tests/backpack-cross-package-rename/files/impl-pkg/src/Impl.hs new file mode 100644 index 0000000000..cf8ca1d3e2 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-rename/files/impl-pkg/src/Impl.hs @@ -0,0 +1,4 @@ +module Impl where + +message :: String +message = "Renamed module works" diff --git a/tests/integration/tests/backpack-cross-package-rename/files/sig-pkg/sig-pkg.cabal b/tests/integration/tests/backpack-cross-package-rename/files/sig-pkg/sig-pkg.cabal new file mode 100644 index 0000000000..246f14ab3d --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-rename/files/sig-pkg/sig-pkg.cabal @@ -0,0 +1,10 @@ +cabal-version: 2.0 +name: sig-pkg +version: 0.1.0.0 +build-type: Simple + +library + signatures: Sig + build-depends: base >= 4.7 && < 5 + hs-source-dirs: src + default-language: Haskell2010 diff --git a/tests/integration/tests/backpack-cross-package-rename/files/sig-pkg/src/Sig.hsig b/tests/integration/tests/backpack-cross-package-rename/files/sig-pkg/src/Sig.hsig new file mode 100644 index 0000000000..42f438e05c --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-rename/files/sig-pkg/src/Sig.hsig @@ -0,0 +1,3 @@ +signature Sig where + +message :: String diff --git a/tests/integration/tests/backpack-cross-package-rename/files/stack.yaml b/tests/integration/tests/backpack-cross-package-rename/files/stack.yaml new file mode 100644 index 0000000000..3f5df23831 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-rename/files/stack.yaml @@ -0,0 +1,5 @@ +snapshot: ghc-9.10.3 +packages: + - sig-pkg + - impl-pkg + - consumer-pkg diff --git a/tests/integration/tests/backpack-cross-package-sig/Main.hs b/tests/integration/tests/backpack-cross-package-sig/Main.hs new file mode 100644 index 0000000000..7010a9ae91 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-sig/Main.hs @@ -0,0 +1,27 @@ +import Control.Monad (unless) +import Data.List (isInfixOf) +import StackTest + +-- Test cross-package Backpack: sig-pkg (indefinite, has Str signature) is +-- instantiated with impl-pkg's Str module via consumer-pkg's mixin declaration. +main :: IO () +main = do + -- Build all three packages. This exercises: + -- 1. sig-pkg CLib (indefinite, typecheck-only) + -- 2. impl-pkg CLib (concrete Str implementation) + -- 3. sig-pkg CInst (instantiation with impl-pkg's Str) + -- 4. consumer-pkg CLib + CExe + stack ["build"] + + -- Verify the consumer executable calls through the instantiated signature + stackCheckStdout ["exec", "consumer-demo"] $ \out -> + unless ("Hello from impl-pkg" `isInfixOf` out) $ + error $ "Expected 'Hello from impl-pkg' in output, got: " ++ show out + + -- Rebuild should succeed (no stale CInst state or dist-dir conflicts) + stack ["build"] + + -- Verify output still correct after rebuild + stackCheckStdout ["exec", "consumer-demo"] $ \out -> + unless ("Hello from impl-pkg" `isInfixOf` out) $ + error $ "Expected 'Hello from impl-pkg' after rebuild, got: " ++ show out diff --git a/tests/integration/tests/backpack-cross-package-sig/files/consumer-pkg/app/Main.hs b/tests/integration/tests/backpack-cross-package-sig/files/consumer-pkg/app/Main.hs new file mode 100644 index 0000000000..4c26592390 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-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-cross-package-sig/files/consumer-pkg/consumer-pkg.cabal b/tests/integration/tests/backpack-cross-package-sig/files/consumer-pkg/consumer-pkg.cabal new file mode 100644 index 0000000000..52fd5a3132 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-sig/files/consumer-pkg/consumer-pkg.cabal @@ -0,0 +1,23 @@ +cabal-version: 2.0 +name: consumer-pkg +version: 0.1.0.0 +build-type: Simple + +library + exposed-modules: Consumer + build-depends: + base >= 4.7 && < 5, + sig-pkg, + impl-pkg + mixins: + sig-pkg requires (Str as Str) + hs-source-dirs: src + default-language: Haskell2010 + +executable consumer-demo + main-is: Main.hs + build-depends: + base >= 4.7 && < 5, + consumer-pkg + hs-source-dirs: app + default-language: Haskell2010 diff --git a/tests/integration/tests/backpack-cross-package-sig/files/consumer-pkg/src/Consumer.hs b/tests/integration/tests/backpack-cross-package-sig/files/consumer-pkg/src/Consumer.hs new file mode 100644 index 0000000000..4c44a58a8a --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-sig/files/consumer-pkg/src/Consumer.hs @@ -0,0 +1,6 @@ +module Consumer where + +import Str (greeting) + +hello :: String +hello = greeting diff --git a/tests/integration/tests/backpack-cross-package-sig/files/impl-pkg/impl-pkg.cabal b/tests/integration/tests/backpack-cross-package-sig/files/impl-pkg/impl-pkg.cabal new file mode 100644 index 0000000000..8ee468919a --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-sig/files/impl-pkg/impl-pkg.cabal @@ -0,0 +1,10 @@ +cabal-version: 2.0 +name: impl-pkg +version: 0.1.0.0 +build-type: Simple + +library + exposed-modules: Str + build-depends: base >= 4.7 && < 5 + hs-source-dirs: src + default-language: Haskell2010 diff --git a/tests/integration/tests/backpack-cross-package-sig/files/impl-pkg/src/Str.hs b/tests/integration/tests/backpack-cross-package-sig/files/impl-pkg/src/Str.hs new file mode 100644 index 0000000000..7d546cc5a1 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-sig/files/impl-pkg/src/Str.hs @@ -0,0 +1,4 @@ +module Str where + +greeting :: String +greeting = "Hello from impl-pkg" diff --git a/tests/integration/tests/backpack-cross-package-sig/files/sig-pkg/sig-pkg.cabal b/tests/integration/tests/backpack-cross-package-sig/files/sig-pkg/sig-pkg.cabal new file mode 100644 index 0000000000..7ce99e3c8f --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-sig/files/sig-pkg/sig-pkg.cabal @@ -0,0 +1,10 @@ +cabal-version: 2.0 +name: sig-pkg +version: 0.1.0.0 +build-type: Simple + +library + signatures: Str + build-depends: base >= 4.7 && < 5 + hs-source-dirs: src + default-language: Haskell2010 diff --git a/tests/integration/tests/backpack-cross-package-sig/files/sig-pkg/src/Str.hsig b/tests/integration/tests/backpack-cross-package-sig/files/sig-pkg/src/Str.hsig new file mode 100644 index 0000000000..edb56a4a7d --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-sig/files/sig-pkg/src/Str.hsig @@ -0,0 +1,3 @@ +signature Str where + +greeting :: String diff --git a/tests/integration/tests/backpack-cross-package-sig/files/stack.yaml b/tests/integration/tests/backpack-cross-package-sig/files/stack.yaml new file mode 100644 index 0000000000..3f5df23831 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-sig/files/stack.yaml @@ -0,0 +1,5 @@ +snapshot: ghc-9.10.3 +packages: + - sig-pkg + - impl-pkg + - consumer-pkg diff --git a/tests/integration/tests/backpack-cross-package-sublib/Main.hs b/tests/integration/tests/backpack-cross-package-sublib/Main.hs new file mode 100644 index 0000000000..a3fe8844f0 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-sublib/Main.hs @@ -0,0 +1,7 @@ +import StackTest + +-- Test that a package can depend on another package's public sub-library. +-- consumer depends on provider:utils (not provider's main library). +-- This already works in Stack; this test prevents regressions. +main :: IO () +main = stack ["build"] diff --git a/tests/integration/tests/backpack-cross-package-sublib/files/consumer/consumer.cabal b/tests/integration/tests/backpack-cross-package-sublib/files/consumer/consumer.cabal new file mode 100644 index 0000000000..ffec6d82ac --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-sublib/files/consumer/consumer.cabal @@ -0,0 +1,22 @@ +cabal-version: 3.0 +name: consumer +version: 0.1.0.0 +build-type: Simple + +-- A package that depends on provider's sub-library "utils" directly, +-- NOT on provider's main library. This tests cross-package sub-library +-- dependency resolution. + +library + exposed-modules: Consumer + build-depends: base >=4.14, + provider:utils + hs-source-dirs: src/main + default-language: Haskell2010 + +executable consumer-demo + main-is: Main.hs + build-depends: base >=4.14, + consumer + hs-source-dirs: src/exe + default-language: Haskell2010 diff --git a/tests/integration/tests/backpack-cross-package-sublib/files/consumer/src/exe/Main.hs b/tests/integration/tests/backpack-cross-package-sublib/files/consumer/src/exe/Main.hs new file mode 100644 index 0000000000..e1f66a0061 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-sublib/files/consumer/src/exe/Main.hs @@ -0,0 +1,6 @@ +module Main where + +import Consumer (shout) + +main :: IO () +main = putStrLn (shout "hello world") diff --git a/tests/integration/tests/backpack-cross-package-sublib/files/consumer/src/main/Consumer.hs b/tests/integration/tests/backpack-cross-package-sublib/files/consumer/src/main/Consumer.hs new file mode 100644 index 0000000000..24f96db198 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-sublib/files/consumer/src/main/Consumer.hs @@ -0,0 +1,10 @@ +module Consumer (shout) where + +import Provider.Utils (capitalize) + +shout :: String -> String +shout = map toUpper . capitalize + where + toUpper c + | c >= 'a' && c <= 'z' = toEnum (fromEnum c - 32) + | otherwise = c diff --git a/tests/integration/tests/backpack-cross-package-sublib/files/provider/provider.cabal b/tests/integration/tests/backpack-cross-package-sublib/files/provider/provider.cabal new file mode 100644 index 0000000000..e899ed52d1 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-sublib/files/provider/provider.cabal @@ -0,0 +1,21 @@ +cabal-version: 3.0 +name: provider +version: 0.1.0.0 +build-type: Simple + +-- A package that exposes a sub-library "utils" alongside its main library. +-- The consumer package depends on the sub-library specifically. + +library utils + visibility: public + exposed-modules: Provider.Utils + build-depends: base >=4.14 + hs-source-dirs: src/utils + default-language: Haskell2010 + +library + exposed-modules: Provider + build-depends: base >=4.14, + provider:utils + hs-source-dirs: src/main + default-language: Haskell2010 diff --git a/tests/integration/tests/backpack-cross-package-sublib/files/provider/src/main/Provider.hs b/tests/integration/tests/backpack-cross-package-sublib/files/provider/src/main/Provider.hs new file mode 100644 index 0000000000..429344c0b6 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-sublib/files/provider/src/main/Provider.hs @@ -0,0 +1,6 @@ +module Provider (capitalGreet) where + +import Provider.Utils (capitalize) + +capitalGreet :: String -> String +capitalGreet name = capitalize ("hello, " ++ name ++ "!") diff --git a/tests/integration/tests/backpack-cross-package-sublib/files/provider/src/utils/Provider/Utils.hs b/tests/integration/tests/backpack-cross-package-sublib/files/provider/src/utils/Provider/Utils.hs new file mode 100644 index 0000000000..6a35d58a6c --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-sublib/files/provider/src/utils/Provider/Utils.hs @@ -0,0 +1,7 @@ +module Provider.Utils (capitalize) where + +capitalize :: String -> String +capitalize [] = [] +capitalize (c:cs) + | c >= 'a' && c <= 'z' = toEnum (fromEnum c - 32) : cs + | otherwise = c : cs diff --git a/tests/integration/tests/backpack-cross-package-sublib/files/stack.yaml b/tests/integration/tests/backpack-cross-package-sublib/files/stack.yaml new file mode 100644 index 0000000000..ded94ebe0b --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-sublib/files/stack.yaml @@ -0,0 +1,4 @@ +snapshot: ghc-9.10.3 +packages: + - provider + - consumer diff --git a/tests/integration/tests/backpack-cross-package-transitive/Main.hs b/tests/integration/tests/backpack-cross-package-transitive/Main.hs new file mode 100644 index 0000000000..6bb1a4bb18 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-transitive/Main.hs @@ -0,0 +1,32 @@ +import Control.Monad (unless) +import Data.List (isInfixOf) +import StackTest + +-- Test transitive Backpack chains: logger-sig (indefinite, sig: Logger) depends +-- on str-sig (indefinite, sig: Str). When consumer mixes in logger-sig, both +-- Logger and Str holes must be filled transitively. +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 + + -- Rebuild should succeed (no stale CInst state) + stack ["build"] + + -- 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 diff --git a/tests/integration/tests/backpack-cross-package-transitive/files/consumer-pkg/app/Main.hs b/tests/integration/tests/backpack-cross-package-transitive/files/consumer-pkg/app/Main.hs new file mode 100644 index 0000000000..4c26592390 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-transitive/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-cross-package-transitive/files/consumer-pkg/consumer-pkg.cabal b/tests/integration/tests/backpack-cross-package-transitive/files/consumer-pkg/consumer-pkg.cabal new file mode 100644 index 0000000000..d86d09cec6 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-transitive/files/consumer-pkg/consumer-pkg.cabal @@ -0,0 +1,25 @@ +cabal-version: 2.0 +name: consumer-pkg +version: 0.1.0.0 +build-type: Simple + +library + exposed-modules: Consumer + build-depends: + base >= 4.7 && < 5, + str-sig, + logger-sig, + impl-pkg + mixins: + str-sig requires (Str as Str), + logger-sig requires (Logger as Logger) + hs-source-dirs: src + default-language: Haskell2010 + +executable consumer-demo + main-is: Main.hs + build-depends: + base >= 4.7 && < 5, + consumer-pkg + hs-source-dirs: app + default-language: Haskell2010 diff --git a/tests/integration/tests/backpack-cross-package-transitive/files/consumer-pkg/src/Consumer.hs b/tests/integration/tests/backpack-cross-package-transitive/files/consumer-pkg/src/Consumer.hs new file mode 100644 index 0000000000..e46c8658a6 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-transitive/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-cross-package-transitive/files/impl-pkg/impl-pkg.cabal b/tests/integration/tests/backpack-cross-package-transitive/files/impl-pkg/impl-pkg.cabal new file mode 100644 index 0000000000..521171e7bc --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-transitive/files/impl-pkg/impl-pkg.cabal @@ -0,0 +1,10 @@ +cabal-version: 2.0 +name: impl-pkg +version: 0.1.0.0 +build-type: Simple + +library + exposed-modules: Str, Logger + build-depends: base >= 4.7 && < 5 + hs-source-dirs: src + default-language: Haskell2010 diff --git a/tests/integration/tests/backpack-cross-package-transitive/files/impl-pkg/src/Logger.hs b/tests/integration/tests/backpack-cross-package-transitive/files/impl-pkg/src/Logger.hs new file mode 100644 index 0000000000..ddc7981a34 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-transitive/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-cross-package-transitive/files/impl-pkg/src/Str.hs b/tests/integration/tests/backpack-cross-package-transitive/files/impl-pkg/src/Str.hs new file mode 100644 index 0000000000..3b6d306620 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-transitive/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-cross-package-transitive/files/logger-sig/logger-sig.cabal b/tests/integration/tests/backpack-cross-package-transitive/files/logger-sig/logger-sig.cabal new file mode 100644 index 0000000000..86bdecede0 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-transitive/files/logger-sig/logger-sig.cabal @@ -0,0 +1,15 @@ +cabal-version: 2.0 +name: logger-sig +version: 0.1.0.0 +build-type: Simple + +library + signatures: Logger + exposed-modules: LogHelper + build-depends: + base >= 4.7 && < 5, + str-sig + mixins: + str-sig requires (Str as Str) + hs-source-dirs: src + default-language: Haskell2010 diff --git a/tests/integration/tests/backpack-cross-package-transitive/files/logger-sig/src/LogHelper.hs b/tests/integration/tests/backpack-cross-package-transitive/files/logger-sig/src/LogHelper.hs new file mode 100644 index 0000000000..2eecba92d6 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-transitive/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-cross-package-transitive/files/logger-sig/src/Logger.hsig b/tests/integration/tests/backpack-cross-package-transitive/files/logger-sig/src/Logger.hsig new file mode 100644 index 0000000000..446efe6fe6 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-transitive/files/logger-sig/src/Logger.hsig @@ -0,0 +1,3 @@ +signature Logger where + +logMessage :: String -> String diff --git a/tests/integration/tests/backpack-cross-package-transitive/files/stack.yaml b/tests/integration/tests/backpack-cross-package-transitive/files/stack.yaml new file mode 100644 index 0000000000..72c111f0fc --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-transitive/files/stack.yaml @@ -0,0 +1,6 @@ +snapshot: ghc-9.10.3 +packages: + - str-sig + - logger-sig + - impl-pkg + - consumer-pkg diff --git a/tests/integration/tests/backpack-cross-package-transitive/files/str-sig/src/Str.hsig b/tests/integration/tests/backpack-cross-package-transitive/files/str-sig/src/Str.hsig new file mode 100644 index 0000000000..edb56a4a7d --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-transitive/files/str-sig/src/Str.hsig @@ -0,0 +1,3 @@ +signature Str where + +greeting :: String diff --git a/tests/integration/tests/backpack-cross-package-transitive/files/str-sig/str-sig.cabal b/tests/integration/tests/backpack-cross-package-transitive/files/str-sig/str-sig.cabal new file mode 100644 index 0000000000..e9efd6ab50 --- /dev/null +++ b/tests/integration/tests/backpack-cross-package-transitive/files/str-sig/str-sig.cabal @@ -0,0 +1,10 @@ +cabal-version: 2.0 +name: str-sig +version: 0.1.0.0 +build-type: Simple + +library + signatures: Str + build-depends: base >= 4.7 && < 5 + hs-source-dirs: src + default-language: Haskell2010 diff --git a/tests/integration/tests/backpack-private/Main.hs b/tests/integration/tests/backpack-private/Main.hs new file mode 100644 index 0000000000..b18577c0a6 --- /dev/null +++ b/tests/integration/tests/backpack-private/Main.hs @@ -0,0 +1,11 @@ +import StackTest + +-- Test that a package using Backpack internally (private Backpack) builds. +-- lib:str-sig defines a Str signature (.hsig), lib:str-impl provides +-- a concrete Str module, and the main library depends on both so that +-- mix-in linking fills the signature. +main :: IO () +main = do + stack ["build"] + -- Verify the built executable actually runs (Backpack instantiation worked) + stack ["exec", "private-backpack-demo"] diff --git a/tests/integration/tests/backpack-private/files/private-backpack.cabal b/tests/integration/tests/backpack-private/files/private-backpack.cabal new file mode 100644 index 0000000000..119495823a --- /dev/null +++ b/tests/integration/tests/backpack-private/files/private-backpack.cabal @@ -0,0 +1,37 @@ +cabal-version: 3.4 +name: private-backpack +version: 0.1.0.0 +build-type: Simple + +-- A package using Backpack internally (private Backpack). +-- Sub-library "str-sig" defines an abstract Str signature. +-- Sub-library "str-impl" provides a concrete Str module that fills the sig. +-- Main library uses str-sig (indefinite) and str-impl fills the hole. +-- The public API has no unfilled signatures. + +library str-sig + signatures: Str + build-depends: base >=4.14 + hs-source-dirs: src/sig + default-language: Haskell2010 + +library str-impl + exposed-modules: Str + build-depends: base >=4.14 + hs-source-dirs: src/impl + default-language: Haskell2010 + +library + exposed-modules: PrivateBackpack + build-depends: base >=4.14, + private-backpack:str-sig, + private-backpack:str-impl + hs-source-dirs: src/main + default-language: Haskell2010 + +executable private-backpack-demo + main-is: Main.hs + build-depends: base >=4.14, + private-backpack + hs-source-dirs: src/exe + default-language: Haskell2010 diff --git a/tests/integration/tests/backpack-private/files/src/exe/Main.hs b/tests/integration/tests/backpack-private/files/src/exe/Main.hs new file mode 100644 index 0000000000..23c84ad3f1 --- /dev/null +++ b/tests/integration/tests/backpack-private/files/src/exe/Main.hs @@ -0,0 +1,6 @@ +module Main where + +import PrivateBackpack (greetString) + +main :: IO () +main = putStrLn (greetString "world") diff --git a/tests/integration/tests/backpack-private/files/src/impl/Str.hs b/tests/integration/tests/backpack-private/files/src/impl/Str.hs new file mode 100644 index 0000000000..6c308bdf04 --- /dev/null +++ b/tests/integration/tests/backpack-private/files/src/impl/Str.hs @@ -0,0 +1,16 @@ +module Str (Str, empty, append, toString, fromString) where + +-- Concrete implementation of the Str signature using plain String. +type Str = String + +empty :: Str +empty = "" + +append :: Str -> Str -> Str +append = (++) + +toString :: Str -> String +toString = id + +fromString :: String -> Str +fromString = id diff --git a/tests/integration/tests/backpack-private/files/src/main/PrivateBackpack.hs b/tests/integration/tests/backpack-private/files/src/main/PrivateBackpack.hs new file mode 100644 index 0000000000..c3faa73392 --- /dev/null +++ b/tests/integration/tests/backpack-private/files/src/main/PrivateBackpack.hs @@ -0,0 +1,8 @@ +module PrivateBackpack (greetString) where + +import Str (Str, fromString, append, toString) + +-- Public API: no Backpack types leak. Str is resolved internally +-- by mix-in linking str-sig's requirement with str-impl's provision. +greetString :: String -> String +greetString name = toString (fromString "Hello, " `append` fromString name `append` fromString "!") diff --git a/tests/integration/tests/backpack-private/files/src/sig/Str.hsig b/tests/integration/tests/backpack-private/files/src/sig/Str.hsig new file mode 100644 index 0000000000..6115bc93a1 --- /dev/null +++ b/tests/integration/tests/backpack-private/files/src/sig/Str.hsig @@ -0,0 +1,7 @@ +signature Str where + +data Str +empty :: Str +append :: Str -> Str -> Str +toString :: Str -> String +fromString :: String -> Str diff --git a/tests/integration/tests/backpack-private/files/stack.yaml b/tests/integration/tests/backpack-private/files/stack.yaml new file mode 100644 index 0000000000..2bc06edd65 --- /dev/null +++ b/tests/integration/tests/backpack-private/files/stack.yaml @@ -0,0 +1,3 @@ +snapshot: ghc-9.10.3 +packages: + - . diff --git a/tests/integration/tests/backpack-sublib-deps/Main.hs b/tests/integration/tests/backpack-sublib-deps/Main.hs new file mode 100644 index 0000000000..add9a019b7 --- /dev/null +++ b/tests/integration/tests/backpack-sublib-deps/Main.hs @@ -0,0 +1,7 @@ +import StackTest + +-- Test that a package with chained sub-library dependencies builds correctly. +-- lib:core -> lib:extended -> lib (main) -> exe:sublib-deps-demo +-- This already works in Stack; this test prevents regressions. +main :: IO () +main = stack ["build"] diff --git a/tests/integration/tests/backpack-sublib-deps/files/src/core/SublibDeps/Core.hs b/tests/integration/tests/backpack-sublib-deps/files/src/core/SublibDeps/Core.hs new file mode 100644 index 0000000000..08ae3f1958 --- /dev/null +++ b/tests/integration/tests/backpack-sublib-deps/files/src/core/SublibDeps/Core.hs @@ -0,0 +1,4 @@ +module SublibDeps.Core (greet) where + +greet :: String -> String +greet name = "Hello, " ++ name ++ "!" diff --git a/tests/integration/tests/backpack-sublib-deps/files/src/exe/Main.hs b/tests/integration/tests/backpack-sublib-deps/files/src/exe/Main.hs new file mode 100644 index 0000000000..142d54f503 --- /dev/null +++ b/tests/integration/tests/backpack-sublib-deps/files/src/exe/Main.hs @@ -0,0 +1,6 @@ +module Main where + +import SublibDeps (greetLoud) + +main :: IO () +main = putStrLn (greetLoud "world") diff --git a/tests/integration/tests/backpack-sublib-deps/files/src/extended/SublibDeps/Extended.hs b/tests/integration/tests/backpack-sublib-deps/files/src/extended/SublibDeps/Extended.hs new file mode 100644 index 0000000000..f8c5f06e95 --- /dev/null +++ b/tests/integration/tests/backpack-sublib-deps/files/src/extended/SublibDeps/Extended.hs @@ -0,0 +1,10 @@ +module SublibDeps.Extended (greetLoud) where + +import SublibDeps.Core (greet) + +greetLoud :: String -> String +greetLoud name = map toUpper (greet name) + where + toUpper c + | c >= 'a' && c <= 'z' = toEnum (fromEnum c - 32) + | otherwise = c diff --git a/tests/integration/tests/backpack-sublib-deps/files/src/main/SublibDeps.hs b/tests/integration/tests/backpack-sublib-deps/files/src/main/SublibDeps.hs new file mode 100644 index 0000000000..5fa190f1d8 --- /dev/null +++ b/tests/integration/tests/backpack-sublib-deps/files/src/main/SublibDeps.hs @@ -0,0 +1,3 @@ +module SublibDeps (greetLoud) where + +import SublibDeps.Extended (greetLoud) diff --git a/tests/integration/tests/backpack-sublib-deps/files/stack.yaml b/tests/integration/tests/backpack-sublib-deps/files/stack.yaml new file mode 100644 index 0000000000..2bc06edd65 --- /dev/null +++ b/tests/integration/tests/backpack-sublib-deps/files/stack.yaml @@ -0,0 +1,3 @@ +snapshot: ghc-9.10.3 +packages: + - . diff --git a/tests/integration/tests/backpack-sublib-deps/files/sublib-deps.cabal b/tests/integration/tests/backpack-sublib-deps/files/sublib-deps.cabal new file mode 100644 index 0000000000..a063723ed9 --- /dev/null +++ b/tests/integration/tests/backpack-sublib-deps/files/sublib-deps.cabal @@ -0,0 +1,37 @@ +cabal-version: 3.0 +name: sublib-deps +version: 0.1.0.0 +build-type: Simple + +-- A package with sub-libraries that depend on each other. +-- Sub-library "core" is standalone. +-- Sub-library "extended" depends on "core". +-- Main library depends on "extended". +-- This tests intra-package component ordering. + +library core + exposed-modules: SublibDeps.Core + build-depends: base >=4.14 + hs-source-dirs: src/core + default-language: Haskell2010 + +library extended + exposed-modules: SublibDeps.Extended + build-depends: base >=4.14, + sublib-deps:core + hs-source-dirs: src/extended + default-language: Haskell2010 + +library + exposed-modules: SublibDeps + build-depends: base >=4.14, + sublib-deps:extended + hs-source-dirs: src/main + default-language: Haskell2010 + +executable sublib-deps-demo + main-is: Main.hs + build-depends: base >=4.14, + sublib-deps + hs-source-dirs: src/exe + default-language: Haskell2010 diff --git a/tests/integration/tests/per-component-build/Main.hs b/tests/integration/tests/per-component-build/Main.hs new file mode 100644 index 0000000000..6e200bcda1 --- /dev/null +++ b/tests/integration/tests/per-component-build/Main.hs @@ -0,0 +1,33 @@ +import Control.Monad (unless) +import Data.List (isInfixOf) +import StackTest + +main :: IO () +main = do + -- Clean to ensure a full rebuild so we can inspect build output. + stack ["clean"] + + -- Build all targets and verify per-component build messages appear in + -- stderr (Stack prints build progress there). + stackCheckStderr ["build"] $ \err -> do + -- With per-component builds, each executable should be built separately + -- and announced with its component name. + let expect tag = + unless (tag `isInfixOf` err) $ + error $ "Expected " ++ show tag ++ " in build output:\n" ++ err + expect "exe:app1" + expect "exe:app2" + + -- Verify both executables produce correct output. + stackCheckStdout ["exec", "app1"] $ \out -> + unless ("app1: Hello from Lib" `isInfixOf` out) $ + error $ "Unexpected app1 output: " ++ out + + stackCheckStdout ["exec", "app2"] $ \out -> + unless ("app2: Hello from Lib" `isInfixOf` out) $ + error $ "Unexpected app2 output: " ++ out + + -- Run the test suite and verify it passes. + stackCheckStderr ["test"] $ \err -> + unless ("per-component-build" `isInfixOf` err) $ + error $ "Expected package name in test output:\n" ++ err diff --git a/tests/integration/tests/per-component-build/files/app1/Main.hs b/tests/integration/tests/per-component-build/files/app1/Main.hs new file mode 100644 index 0000000000..c4ed68390c --- /dev/null +++ b/tests/integration/tests/per-component-build/files/app1/Main.hs @@ -0,0 +1,6 @@ +module Main where + +import Lib (greeting) + +main :: IO () +main = putStrLn ("app1: " ++ greeting) diff --git a/tests/integration/tests/per-component-build/files/app2/Main.hs b/tests/integration/tests/per-component-build/files/app2/Main.hs new file mode 100644 index 0000000000..2dfa058723 --- /dev/null +++ b/tests/integration/tests/per-component-build/files/app2/Main.hs @@ -0,0 +1,6 @@ +module Main where + +import Lib (greeting) + +main :: IO () +main = putStrLn ("app2: " ++ greeting) diff --git a/tests/integration/tests/per-component-build/files/per-component-build.cabal b/tests/integration/tests/per-component-build/files/per-component-build.cabal new file mode 100644 index 0000000000..5146198749 --- /dev/null +++ b/tests/integration/tests/per-component-build/files/per-component-build.cabal @@ -0,0 +1,30 @@ +cabal-version: 2.0 +name: per-component-build +version: 0.1.0.0 +build-type: Simple +license: BSD3 + +library + exposed-modules: Lib + hs-source-dirs: src + build-depends: base + default-language: Haskell2010 + +executable app1 + main-is: Main.hs + hs-source-dirs: app1 + build-depends: base, per-component-build + default-language: Haskell2010 + +executable app2 + main-is: Main.hs + hs-source-dirs: app2 + build-depends: base, per-component-build + default-language: Haskell2010 + +test-suite tests + type: exitcode-stdio-1.0 + main-is: Main.hs + hs-source-dirs: test + build-depends: base, per-component-build + default-language: Haskell2010 diff --git a/tests/integration/tests/per-component-build/files/src/Lib.hs b/tests/integration/tests/per-component-build/files/src/Lib.hs new file mode 100644 index 0000000000..579eefd84f --- /dev/null +++ b/tests/integration/tests/per-component-build/files/src/Lib.hs @@ -0,0 +1,4 @@ +module Lib (greeting) where + +greeting :: String +greeting = "Hello from Lib" diff --git a/tests/integration/tests/per-component-build/files/stack.yaml b/tests/integration/tests/per-component-build/files/stack.yaml new file mode 100644 index 0000000000..2bc06edd65 --- /dev/null +++ b/tests/integration/tests/per-component-build/files/stack.yaml @@ -0,0 +1,3 @@ +snapshot: ghc-9.10.3 +packages: + - . diff --git a/tests/integration/tests/per-component-build/files/test/Main.hs b/tests/integration/tests/per-component-build/files/test/Main.hs new file mode 100644 index 0000000000..5935fd731b --- /dev/null +++ b/tests/integration/tests/per-component-build/files/test/Main.hs @@ -0,0 +1,9 @@ +module Main where + +import Lib (greeting) +import System.Exit (exitSuccess) + +main :: IO () +main = do + putStrLn ("test: " ++ greeting) + exitSuccess diff --git a/tests/unit/Stack/Build/ConstructPlanSpec.hs b/tests/unit/Stack/Build/ConstructPlanSpec.hs new file mode 100644 index 0000000000..07a758d7e7 --- /dev/null +++ b/tests/unit/Stack/Build/ConstructPlanSpec.hs @@ -0,0 +1,2630 @@ +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE OverloadedRecordDot #-} + +module Stack.Build.ConstructPlanSpec + ( main + , spec + ) where + +import qualified Data.Map.Strict as Map +import qualified Data.Set as Set +import Distribution.CabalSpecVersion ( CabalSpecVersion (..) ) +import Distribution.Compiler ( PerCompilerFlavor (..) ) +import Distribution.License ( License (BSD3) ) +import qualified Distribution.ModuleName as Cabal +import Distribution.PackageDescription + ( BenchmarkInterface (..), BenchmarkType (..) + , TestSuiteInterface (..), TestType (..) + ) +import Distribution.Types.BuildType + ( BuildType (Configure, Custom, Simple) ) +import Distribution.Types.IncludeRenaming + ( IncludeRenaming (..), defaultIncludeRenaming ) +import Distribution.Types.Mixin ( Mixin (..) ) +import Distribution.Types.ModuleRenaming + ( ModuleRenaming (..) ) +import Distribution.Types.LibraryName ( LibraryName (LMainLibName) ) +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 Stack.Build.ConstructPlan ( shouldSplitComponents ) +import Stack.Build.ExecutePackage + ( findGhcPkgId, mkInstantiateWithOpts ) +import Stack.Package ( hasIntraPackageDeps, packageIsIndefinite ) +import Stack.Prelude +import Stack.Types.Build.ConstructPlan ( AddDepRes (..) ) +import Database.Persist ( PersistField (..), PersistValue (..) ) +import Stack.Types.Cache ( CachePkgSrc (..), ConfigCacheType (..) ) +import Stack.Types.CompCollection + ( foldAndMakeCollection, getBuildableSet ) +import Stack.Types.Component + ( StackBenchmark (..), StackBuildInfo (..) + , StackExecutable (..), StackForeignLibrary (..) + , StackLibrary (..), StackTestSuite (..) + ) +import Stack.Types.ComponentUtils + ( emptyCompName, unqualCompFromString ) +import Stack.Types.Dependency + ( DepLibrary (..), DepType (..), DepValue (..) ) +import Stack.Types.Installed + ( InstallLocation (..), Installed (..) + , installedLibraryInfoFromGhcPkgId + ) +import Stack.Types.GhcPkgId ( GhcPkgId, parseGhcPkgId ) +import Stack.Types.IsMutable ( IsMutable (..) ) +import Stack.Types.NamedComponent ( NamedComponent (..) ) +import Stack.Types.Package ( Package (..), packageIdentifier ) +import Stack.Types.PackageFile ( StackPackageFile (..) ) +import Stack.Types.Plan + ( ComponentKey (..), Task (..), TaskConfigOpts (..) + , TaskType (..) + ) +import Test.Hspec + ( Spec, describe, expectationFailure, hspec, it + , shouldBe, shouldSatisfy + ) + +main :: IO () +main = hspec spec + +-- | A minimal StackBuildInfo suitable for testing. All lazy fields are empty +-- lists/sets; buildable defaults to True, no dependencies. +testBuildInfo :: StackBuildInfo +testBuildInfo = StackBuildInfo + { buildable = True + , dependency = Map.empty + , unknownTools = Set.empty + , otherModules = [] + , jsSources = [] + , hsSourceDirs = [] + , cSources = [] + , cppOptions = [] + , targetBuildDepends = [] + , options = PerCompilerFlavor [] [] + , allLanguages = [] + , usedExtensions = [] + , includeDirs = [] + , extraLibs = [] + , extraLibDirs = [] + , frameworks = [] + , mixins = [] + } + +-- | A build info that declares a dependency on the package itself (Backpack). +selfDepBuildInfo :: PackageName -> StackBuildInfo +selfDepBuildInfo pn = testBuildInfo + { dependency = Map.singleton pn depVal } + where + depVal = DepValue + { versionRange = anyVersion + , depType = AsLibrary DepLibrary { main = True, subLib = Set.empty } + } + +-- | Create a minimal test Package. +testPackage :: PackageName -> BuildType -> Package +testPackage pn bt = Package + { name = pn + , version = mkVersion [1, 0, 0] + , license = Right BSD3 + , ghcOptions = [] + , cabalConfigOpts = [] + , flags = Map.empty + , defaultFlags = Map.empty + , library = Nothing + , subLibraries = mempty + , foreignLibraries = mempty + , testSuites = mempty + , benchmarks = mempty + , executables = mempty + , buildType = bt + , setupDeps = Nothing + , cabalSpec = CabalSpecV2_0 + , file = StackPackageFile + { extraSrcFiles = [], dataDir = "", dataFiles = [] } + , testEnabled = False + , benchmarkEnabled = False + } + +-- | Add a buildable main library to a package. +withLibrary :: Package -> Package +withLibrary pkg = pkg + { library = Just StackLibrary + { name = emptyCompName + , buildInfo = testBuildInfo + , exposedModules = [] + , signatures = [] + } + } + +-- | Add a buildable main library that depends on itself (Backpack pattern: +-- main lib using internal sub-libs). +withSelfDepLibrary :: Package -> Package +withSelfDepLibrary pkg = pkg + { library = Just StackLibrary + { name = emptyCompName + , buildInfo = selfDepBuildInfo pkg.name + , exposedModules = [] + , signatures = [] + } + } + +-- | Add a buildable sub-library that depends on the same package (Backpack +-- pattern: sub-library chains). +withSelfDepSubLib :: Package -> Package +withSelfDepSubLib pkg = pkg + { subLibraries = foldAndMakeCollection id + [ StackLibrary + { name = unqualCompFromString "internal" + , buildInfo = selfDepBuildInfo pkg.name + , exposedModules = [] + , signatures = [] + } + ] + } + +-- | Add a buildable main library with Backpack signatures (makes it indefinite). +withIndefiniteLibrary :: [ModuleName] -> Package -> Package +withIndefiniteLibrary sigs pkg = pkg + { library = Just StackLibrary + { name = emptyCompName + , buildInfo = testBuildInfo + , exposedModules = [] + , signatures = sigs + } + } + +-- | Add buildable executables to a package. +withExes :: [String] -> Package -> Package +withExes names pkg = pkg + { executables = foldAndMakeCollection id + [ StackExecutable + { name = unqualCompFromString n + , buildInfo = testBuildInfo + , modulePath = "Main.hs" + } + | n <- names + ] + } + +-- | Add buildable executables that depend on the package's own library. +-- This is the common Cabal pattern: @build-depends: my-package@. +withSelfDepExes :: [String] -> Package -> Package +withSelfDepExes names pkg = pkg + { executables = foldAndMakeCollection id + [ StackExecutable + { name = unqualCompFromString n + , buildInfo = selfDepBuildInfo pkg.name + , modulePath = "Main.hs" + } + | n <- names + ] + } + +-- | Add buildable test suites that depend on the package's own library. +-- This is the common Cabal pattern: @build-depends: my-package@. +withSelfDepTests :: [String] -> Package -> Package +withSelfDepTests names pkg = pkg + { testSuites = foldAndMakeCollection id + [ StackTestSuite + { name = unqualCompFromString n + , buildInfo = selfDepBuildInfo pkg.name + , interface = TestSuiteUnsupported (TestTypeUnknown "" (mkVersion [0])) + } + | n <- names + ] + } + +-- | Add buildable benchmarks that depend on the package's own library. +withSelfDepBenchmarks :: [String] -> Package -> Package +withSelfDepBenchmarks names pkg = pkg + { benchmarks = foldAndMakeCollection id + [ StackBenchmark + { name = unqualCompFromString n + , buildInfo = selfDepBuildInfo pkg.name + , interface = BenchmarkUnsupported (BenchmarkTypeUnknown "" (mkVersion [0])) + } + | n <- names + ] + } + +-- | Add buildable foreign libraries that depend on the package itself. +withSelfDepForeignLibs :: [String] -> Package -> Package +withSelfDepForeignLibs names pkg = pkg + { foreignLibraries = foldAndMakeCollection id + [ StackForeignLibrary + { name = unqualCompFromString n + , buildInfo = selfDepBuildInfo pkg.name + } + | n <- names + ] + } + +-- | Add buildable sub-libraries to a package. +withSubLibs :: [String] -> Package -> Package +withSubLibs names pkg = pkg + { subLibraries = foldAndMakeCollection id + [ StackLibrary + { name = unqualCompFromString n + , buildInfo = testBuildInfo + , exposedModules = [] + , signatures = [] + } + | n <- names + ] + } + +-- | Add buildable foreign libraries to a package. +withForeignLibs :: [String] -> Package -> Package +withForeignLibs names pkg = pkg + { foreignLibraries = foldAndMakeCollection id + [ StackForeignLibrary + { name = unqualCompFromString n + , buildInfo = testBuildInfo + } + | n <- names + ] + } + +-- | Add buildable test suites to a package. +withTests :: [String] -> Package -> Package +withTests names pkg = pkg + { testSuites = foldAndMakeCollection id + [ StackTestSuite + { name = unqualCompFromString n + , buildInfo = testBuildInfo + , interface = TestSuiteUnsupported (TestTypeUnknown "" (mkVersion [0])) + } + | n <- names + ] + } + +-- | Add buildable benchmarks to a package. +withBenchmarks :: [String] -> Package -> Package +withBenchmarks names pkg = pkg + { benchmarks = foldAndMakeCollection id + [ StackBenchmark + { name = unqualCompFromString n + , buildInfo = testBuildInfo + , interface = BenchmarkUnsupported (BenchmarkTypeUnknown "" (mkVersion [0])) + } + | n <- names + ] + } + +spec :: Spec +spec = do + describe "shouldSplitComponents" $ do + it "returns True for Simple package without intra-package deps" $ do + let pkg = testPackage (mkPackageName "pkg") Simple + shouldSplitComponents pkg `shouldBe` True + + it "returns False for Custom build type" $ do + let pkg = testPackage (mkPackageName "pkg") Custom + shouldSplitComponents pkg `shouldBe` False + + it "returns False for Configure build type" $ do + let pkg = testPackage (mkPackageName "pkg") Configure + shouldSplitComponents pkg `shouldBe` False + + it "returns False when main lib depends on own sub-libs (Backpack)" $ do + let pkg = withSelfDepLibrary $ testPackage (mkPackageName "pkg") Simple + shouldSplitComponents pkg `shouldBe` False + + it "returns False when sub-lib has self-dep (Backpack)" $ do + let pkg = withSelfDepSubLib $ testPackage (mkPackageName "pkg") Simple + shouldSplitComponents pkg `shouldBe` False + + it "returns True when only exes depend on own lib (normal pattern)" $ do + let pkg = withSelfDepExes ["my-exe"] + $ withLibrary + $ testPackage (mkPackageName "pkg") Simple + shouldSplitComponents pkg `shouldBe` True + + it "returns True for Simple package with library but no self-dep" $ do + let pkg = withLibrary $ testPackage (mkPackageName "pkg") Simple + shouldSplitComponents pkg `shouldBe` True + + it "returns True for Simple package with exes and lib" $ do + let pkg = withExes ["exe1", "exe2"] + $ withLibrary + $ testPackage (mkPackageName "pkg") Simple + shouldSplitComponents pkg `shouldBe` True + + it "returns True when tests depend on own lib (normal pattern)" $ do + let pkg = withSelfDepTests ["my-tests"] + $ withLibrary + $ testPackage (mkPackageName "pkg") Simple + shouldSplitComponents pkg `shouldBe` True + + it "returns True when benchmarks depend on own lib (normal pattern)" $ do + let pkg = withSelfDepBenchmarks ["my-bench"] + $ withLibrary + $ testPackage (mkPackageName "pkg") Simple + shouldSplitComponents pkg `shouldBe` True + + it "returns True when foreign lib has self-dep (not Backpack)" $ do + let pkg = withSelfDepForeignLibs ["cbits"] + $ withLibrary + $ testPackage (mkPackageName "pkg") Simple + shouldSplitComponents pkg `shouldBe` True + + it "returns True for all non-library self-deps combined" $ do + let pkg = withSelfDepExes ["app"] + $ withSelfDepTests ["tests"] + $ withSelfDepBenchmarks ["bench"] + $ withSelfDepForeignLibs ["ffi"] + $ withLibrary + $ testPackage (mkPackageName "pkg") Simple + shouldSplitComponents pkg `shouldBe` True + + it "returns False for indefinite package (has signatures)" $ do + let pkg = withIndefiniteLibrary ["Str"] + $ testPackage (mkPackageName "sig-pkg") Simple + shouldSplitComponents pkg `shouldBe` False + + describe "packageIsIndefinite" $ do + it "returns False for package without signatures" $ do + let pkg = withLibrary $ testPackage (mkPackageName "pkg") Simple + packageIsIndefinite pkg `shouldBe` False + + it "returns True for package with signatures" $ do + let pkg = withIndefiniteLibrary ["Str"] + $ testPackage (mkPackageName "sig-pkg") Simple + packageIsIndefinite pkg `shouldBe` True + + it "returns True for package with multiple signatures" $ do + let pkg = withIndefiniteLibrary ["Str", "Num"] + $ testPackage (mkPackageName "sig-pkg") Simple + packageIsIndefinite pkg `shouldBe` True + + it "returns True for sub-library with signatures" $ do + let pkg = (withLibrary $ testPackage (mkPackageName "pkg") Simple) + { subLibraries = foldAndMakeCollection id + [ StackLibrary + { name = unqualCompFromString "indef-sub" + , buildInfo = testBuildInfo + , exposedModules = [] + , signatures = ["Str"] + } + ] + } + packageIsIndefinite pkg `shouldBe` True + + it "returns False for package with no library" $ do + let pkg = withExes ["app"] $ testPackage (mkPackageName "pkg") Simple + packageIsIndefinite pkg `shouldBe` False + + describe "hasIntraPackageDeps" $ do + it "returns False for plain library" $ do + let pkg = withLibrary $ testPackage (mkPackageName "pkg") Simple + hasIntraPackageDeps pkg `shouldBe` False + + it "returns True when main lib depends on own sub-libs" $ do + let pkg = withSelfDepLibrary $ testPackage (mkPackageName "pkg") Simple + hasIntraPackageDeps pkg `shouldBe` True + + it "returns True when sub-lib has self-dep" $ do + let pkg = withSelfDepSubLib $ testPackage (mkPackageName "pkg") Simple + hasIntraPackageDeps pkg `shouldBe` True + + it "returns True for indefinite package (has signatures)" $ do + let pkg = withIndefiniteLibrary ["Str"] + $ testPackage (mkPackageName "sig-pkg") Simple + hasIntraPackageDeps pkg `shouldBe` True + + it "returns False when only exes have self-dep" $ do + let pkg = withSelfDepExes ["app"] + $ withLibrary + $ testPackage (mkPackageName "pkg") Simple + hasIntraPackageDeps pkg `shouldBe` False + + it "returns False when only tests have self-dep" $ do + let pkg = withSelfDepTests ["tests"] + $ withLibrary + $ testPackage (mkPackageName "pkg") Simple + hasIntraPackageDeps pkg `shouldBe` False + + describe "per-component task expansion" $ do + describe "component enumeration" $ do + it "lib-only package produces [CLib]" $ do + let pkg = withLibrary $ testPackage (mkPackageName "pkg") Simple + comps = packageBuildableComponents pkg + comps `shouldBe` [CLib] + + it "lib+exe package produces [CLib, CExe]" $ do + let pkg = withExes ["my-exe"] + $ withLibrary + $ testPackage (mkPackageName "pkg") Simple + comps = packageBuildableComponents pkg + comps `shouldSatisfy` (CLib `elem`) + comps `shouldSatisfy` + (CExe (unqualCompFromString "my-exe") `elem`) + length comps `shouldBe` 2 + + it "exe-only package (no lib) produces [CExe]" $ do + let pkg = withExes ["app"] + $ testPackage (mkPackageName "pkg") Simple + comps = packageBuildableComponents pkg + comps `shouldBe` [CExe (unqualCompFromString "app")] + + it "package with sub-libraries produces CLib + CSubLib entries" $ do + let pkg = withSubLibs ["internal"] + $ withLibrary + $ testPackage (mkPackageName "pkg") Simple + comps = packageBuildableComponents pkg + length comps `shouldBe` 2 + comps `shouldSatisfy` (CLib `elem`) + comps `shouldSatisfy` + (CSubLib (unqualCompFromString "internal") `elem`) + + it "package with foreign libraries produces CFlib entries" $ do + let pkg = withForeignLibs ["cbits"] + $ withLibrary + $ testPackage (mkPackageName "pkg") Simple + comps = packageBuildableComponents pkg + length comps `shouldBe` 2 + comps `shouldSatisfy` (CFlib (unqualCompFromString "cbits") `elem`) + + it "package with multiple exes produces one CExe per exe" $ do + let pkg = withExes ["exe-a", "exe-b", "exe-c"] + $ testPackage (mkPackageName "pkg") Simple + comps = packageBuildableComponents pkg + length comps `shouldBe` 3 + + it "full package produces CLib + CSubLib + CFlib + CExe" $ do + let pkg = withExes ["app"] + $ withSubLibs ["sub"] + $ withForeignLibs ["ffi"] + $ withLibrary + $ testPackage (mkPackageName "pkg") Simple + comps = packageBuildableComponents pkg + length comps `shouldBe` 4 + + it "empty package (no components) produces empty list" $ do + let pkg = testPackage (mkPackageName "pkg") Simple + comps = packageBuildableComponents pkg + -- No library, no exes, no sub-libs, no foreign libs + -- expandToComponentKeys would fall back to CLib for this case + comps `shouldBe` [] + + describe "ComponentKey expansion" $ do + it "lib+exe package produces distinct ComponentKeys" $ do + let pn = mkPackageName "pkg" + pkg = withExes ["my-exe"] + $ withLibrary + $ testPackage pn Simple + keys = packageToComponentKeys pn pkg + Set.size (Set.fromList keys) `shouldBe` 2 + keys `shouldSatisfy` (ComponentKey pn CLib `elem`) + keys `shouldSatisfy` + (ComponentKey pn (CExe (unqualCompFromString "my-exe")) `elem`) + + it "non-splittable package always maps to single CLib key" $ do + let pn = mkPackageName "pkg" + pkg = withExes ["my-exe"] + $ withLibrary + $ testPackage pn Custom + keys = packageToComponentKeys pn pkg + keys `shouldBe` [ComponentKey pn CLib] + + it "Backpack package (sub-lib self-dep) maps to single CLib key" $ do + let pn = mkPackageName "pkg" + pkg = withExes ["my-exe"] + $ withSelfDepSubLib + $ testPackage pn Simple + keys = packageToComponentKeys pn pkg + keys `shouldBe` [ComponentKey pn CLib] + + it "empty splittable package falls back to CLib" $ do + let pn = mkPackageName "pkg" + pkg = testPackage pn Simple + keys = packageToComponentKeys pn pkg + keys `shouldBe` [ComponentKey pn CLib] + + it "tests and benchmarks are NOT in build component keys" $ do + let pn = mkPackageName "pkg" + pkg = withTests ["test-suite"] + $ withBenchmarks ["bench"] + $ withLibrary + $ testPackage pn Simple + comps = packageBuildableComponents pkg + -- Tests and benchmarks go into finals, not build tasks + comps `shouldBe` [CLib] + + describe "finals splitting" $ do + it "test components produce CTest keys" $ do + let pn = mkPackageName "pkg" + pkg = withTests ["suite-a", "suite-b"] + $ withLibrary + $ testPackage pn Simple + finalKeys = packageToFinalKeys pn pkg (Set.fromList + [ CTest (unqualCompFromString "suite-a") + , CTest (unqualCompFromString "suite-b") + ]) + length finalKeys `shouldBe` 2 + finalKeys `shouldSatisfy` + (ComponentKey pn (CTest (unqualCompFromString "suite-a")) `elem`) + finalKeys `shouldSatisfy` + (ComponentKey pn (CTest (unqualCompFromString "suite-b")) `elem`) + + it "bench components produce CBench keys" $ do + let pn = mkPackageName "pkg" + pkg = withBenchmarks ["bench-a"] + $ withLibrary + $ testPackage pn Simple + finalKeys = packageToFinalKeys pn pkg (Set.fromList + [ CBench (unqualCompFromString "bench-a") + ]) + finalKeys `shouldBe` + [ComponentKey pn (CBench (unqualCompFromString "bench-a"))] + + it "non-splittable package uses single CLib key for finals" $ do + let pn = mkPackageName "pkg" + pkg = withTests ["tests"] + $ withLibrary + $ testPackage pn Custom + finalKeys = packageToFinalKeys pn pkg (Set.fromList + [ CTest (unqualCompFromString "tests") + ]) + finalKeys `shouldBe` [ComponentKey pn CLib] + + it "mixed test+bench components produce both keys" $ do + let pn = mkPackageName "pkg" + pkg = withTests ["tests"] + $ withBenchmarks ["bench"] + $ withLibrary + $ testPackage pn Simple + finalKeys = packageToFinalKeys pn pkg (Set.fromList + [ CTest (unqualCompFromString "tests") + , CBench (unqualCompFromString "bench") + ]) + length finalKeys `shouldBe` 2 + + it "empty components for splittable package falls back to CLib" $ do + let pn = mkPackageName "pkg" + pkg = withLibrary $ testPackage pn Simple + finalKeys = packageToFinalKeys pn pkg Set.empty + finalKeys `shouldBe` [ComponentKey pn CLib] + + describe "addInstantiationTasks" $ do + it "no mixins → no instantiation tasks added" $ do + let consumerPn = mkPackageName "consumer" + consumerPkg = withLibrary $ testPackage consumerPn Simple + consumerTask = mockTask consumerPkg + origAdrs = [(consumerPn, ADRToInstall consumerTask)] + expanded = [(ComponentKey consumerPn CLib, ADRToInstall consumerTask)] + (result, _warnings) = addInstantiationTasks Map.empty origAdrs expanded + -- No CInst tasks should be created. + let instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + instKeys `shouldBe` [] + + it "mixin referencing indefinite dep creates CInst task" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + implPkg = withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, _warnings) = addInstantiationTasks Map.empty origAdrs expanded + instEntries = [ (ck, t) + | (ck@(ComponentKey pn (CInst _)), ADRToInstall t) + <- result + , pn == sigPn + ] + -- Exactly one CInst task for sig-pkg. + length instEntries `shouldBe` 1 + + it "CInst task has correct backpackInstEntries" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + implPkg = withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, _warnings) = addInstantiationTasks Map.empty origAdrs expanded + instTasks = [ t + | (ComponentKey pn (CInst _), ADRToInstall t) <- result + , pn == sigPn + ] + case instTasks of + [t] -> do + let entries = t.backpackInstEntries + length entries `shouldBe` 1 + case entries of + [(sig, ipn, imod)] -> do + sig `shouldBe` mn "Str" + ipn `shouldBe` implPn + imod `shouldBe` mn "Str" + _ -> error "unexpected entries length" + _ -> error "expected exactly one CInst task" + + it "CInst task's missing includes impl-pkg dep" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + implPkg = withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, _warnings) = addInstantiationTasks Map.empty origAdrs expanded + instTasks = [ t + | (ComponentKey pn (CInst _), ADRToInstall t) <- result + , pn == sigPn + ] + case instTasks of + [t] -> + -- The CInst task's missing should include impl-pkg's identifier. + Set.member (packageIdentifier implPkg) t.configOpts.missing + `shouldBe` True + _ -> error "expected exactly one CInst task" + + it "consumer's instantiationDeps includes the CInst key" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + implPkg = withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, _warnings) = addInstantiationTasks Map.empty origAdrs expanded + consumerTasks = [ t + | (ComponentKey pn CLib, ADRToInstall t) <- result + , pn == consumerPn + ] + case consumerTasks of + [t] -> do + let deps = t.configOpts.instantiationDeps + length deps `shouldBe` 1 + case deps of + [ComponentKey pn (CInst _)] -> pn `shouldBe` sigPn + _ -> error "expected CInst dep on sig-pkg" + _ -> error "expected exactly one consumer task" + + it "mixin referencing non-indefinite dep creates no CInst task" $ do + let depPn = mkPackageName "dep-pkg" + consumerPn = mkPackageName "consumer" + -- dep-pkg has a library but NO signatures (not indefinite). + depPkg = withExposingLibrary [mn "Foo"] + $ testPackage depPn Simple + consumerPkg = withDeps [depPn] + $ withMixins [defaultMixin depPn] + $ withLibrary + $ testPackage consumerPn Simple + depTask = mockTask depPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (depPn, ADRToInstall depTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey depPn CLib, ADRToInstall depTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, _warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + instKeys `shouldBe` [] + + it "multiple signatures produce single CInst with multiple entries" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str", mn "Num"] + $ testPackage sigPn Simple + implPkg = withExposingLibrary [mn "Str", mn "Num"] + $ testPackage implPn Simple + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, _warnings) = addInstantiationTasks Map.empty origAdrs expanded + instTasks = [ t + | (ComponentKey pn (CInst _), ADRToInstall t) <- result + , pn == sigPn + ] + case instTasks of + [t] -> length t.backpackInstEntries `shouldBe` 2 + _ -> error "expected exactly one CInst task" + + it "DefaultRenaming maps signatures to identity" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + implPkg = withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, _warnings) = addInstantiationTasks Map.empty origAdrs expanded + instTasks = [ t + | (ComponentKey pn (CInst _), ADRToInstall t) <- result + , pn == sigPn + ] + case instTasks of + [t] -> case t.backpackInstEntries of + [(sig, _, implMod)] -> do + -- With DefaultRenaming, sig name == impl module name. + sig `shouldBe` mn "Str" + implMod `shouldBe` mn "Str" + _ -> error "expected one entry" + _ -> error "expected one CInst task" + + it "CInst task's missing preserves sig-pkg's original missing" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + extraDepPn = mkPackageName "extra-dep" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + implPkg = withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + -- Give sigTask a pre-existing missing dep. + extraDepPid = PackageIdentifier extraDepPn (mkVersion [2, 0]) + sigTask = (mockTask sigPkg) + { configOpts = (mockTask sigPkg).configOpts + { missing = Set.singleton extraDepPid } + } + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, _warnings) = addInstantiationTasks Map.empty origAdrs expanded + instTasks = [ t + | (ComponentKey pn (CInst _), ADRToInstall t) <- result + , pn == sigPn + ] + case instTasks of + [t] -> do + -- Should contain both the original extra-dep AND impl-pkg. + Set.member extraDepPid t.configOpts.missing `shouldBe` True + Set.member (packageIdentifier implPkg) t.configOpts.missing + `shouldBe` True + _ -> error "expected exactly one CInst task" + + it "explicit ModuleRenaming maps sig to renamed module" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + -- sig-pkg has signature "Str" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + -- impl-pkg exposes "Data.Text.Impl" (not "Str") + implPkg = withExposingLibrary [mn "Data.Text.Impl"] + $ testPackage implPn Simple + -- consumer uses explicit renaming: Str = Data.Text.Impl + consumerPkg = withDeps [sigPn, implPn] + $ withMixins + [explicitRequiresMixin sigPn [(mn "Str", mn "Data.Text.Impl")]] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, _warnings) = addInstantiationTasks Map.empty origAdrs expanded + instTasks = [ t + | (ComponentKey pn (CInst _), ADRToInstall t) <- result + , pn == sigPn + ] + case instTasks of + [t] -> case t.backpackInstEntries of + [(sig, ipn, implMod)] -> do + sig `shouldBe` mn "Str" + ipn `shouldBe` implPn + implMod `shouldBe` mn "Data.Text.Impl" + _ -> error "expected one entry" + _ -> error "expected one CInst task" + + it "HidingRenaming hiding nothing instantiates all sigs" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + implPkg = withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + hidingMixin = Mixin + { mixinPackageName = sigPn + , mixinLibraryName = LMainLibName + , mixinIncludeRenaming = IncludeRenaming + { includeProvidesRn = DefaultRenaming + , includeRequiresRn = HidingRenaming [] + } + } + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [hidingMixin] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + length instKeys `shouldBe` 1 + length warnings `shouldBe` 0 + + it "HidingRenaming with actual hidden sigs produces no CInst" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str", mn "Num"] + $ testPackage sigPn Simple + implPkg = withExposingLibrary [mn "Str", mn "Num"] + $ testPackage implPn Simple + -- Hide "Str" — can't fully instantiate, so no CInst. + hidingMixin = Mixin + { mixinPackageName = sigPn + , mixinLibraryName = LMainLibName + , mixinIncludeRenaming = IncludeRenaming + { includeProvidesRn = DefaultRenaming + , includeRequiresRn = HidingRenaming [mn "Str"] + } + } + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [hidingMixin] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + -- Partial instantiation not possible — no CInst, no warning. + instKeys `shouldBe` [] + length warnings `shouldBe` 0 + + it "HidingRenaming hiding all sigs produces no CInst" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + implPkg = withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + hidingMixin = Mixin + { mixinPackageName = sigPn + , mixinLibraryName = LMainLibName + , mixinIncludeRenaming = IncludeRenaming + { includeProvidesRn = DefaultRenaming + , includeRequiresRn = HidingRenaming [mn "Str"] + } + } + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [hidingMixin] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + instKeys `shouldBe` [] + length warnings `shouldBe` 0 + + it "no CInst when no dep exposes the required module, with warning" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + -- impl-pkg exposes "Foo" not "Str" + implPkg = withExposingLibrary [mn "Foo"] + $ testPackage implPn Simple + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + instKeys `shouldBe` [] + length warnings `shouldBe` 1 + + it "ADRFound dep for sig-pkg is skipped with warning" $ do + let sigPn = mkPackageName "sig-pkg" + consumerPn = mkPackageName "consumer" + consumerPkg = withDeps [sigPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + consumerTask = mockTask consumerPkg + sigPid = PackageIdentifier sigPn (mkVersion [1, 0]) + sigFound = ADRFound Snap (Library sigPid + (installedLibraryInfoFromGhcPkgId + (error "ghcPkgId not needed"))) + origAdrs = + [ (sigPn, sigFound) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, sigFound) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + instKeys `shouldBe` [] + length warnings `shouldBe` 1 + + it "successful resolution produces no warnings" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + implPkg = withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + length instKeys `shouldBe` 1 + warnings `shouldSatisfy` null + + it "CInst hash is deterministic for same entries" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + implPkg = withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result1, _) = addInstantiationTasks Map.empty origAdrs expanded + (result2, _) = addInstantiationTasks Map.empty origAdrs expanded + getHash rs = + [ h | (ComponentKey _ (CInst h), _) <- rs ] + getHash result1 `shouldBe` getHash result2 + getHash result1 `shouldSatisfy` (not . null) + + it "deduplicates CInst tasks from multiple consumer components" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + implPkg = withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + -- Consumer has both a library and an exe, both with the same mixin + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withExes ["demo"] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + -- Two consumer components in expanded: CLib and CExe + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + , ( ComponentKey consumerPn + (CExe (unqualCompFromString "demo")) + , ADRToInstall consumerTask + ) + ] + (result, _warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + -- Should produce exactly one CInst entry, not two + length instKeys `shouldBe` 1 + + it "multiple deps exposing same module produces warning and no CInst" $ do + let sigPn = mkPackageName "sig-pkg" + implPn1 = mkPackageName "impl-a" + implPn2 = mkPackageName "impl-b" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + -- Both impl packages expose the same module "Str" + implPkg1 = withExposingLibrary [mn "Str"] + $ testPackage implPn1 Simple + implPkg2 = withExposingLibrary [mn "Str"] + $ testPackage implPn2 Simple + consumerPkg = withDeps [sigPn, implPn1, implPn2] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask1 = mockTask implPkg1 + implTask2 = mockTask implPkg2 + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn1, ADRToInstall implTask1) + , (implPn2, ADRToInstall implTask2) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn1 CLib, ADRToInstall implTask1) + , (ComponentKey implPn2 CLib, ADRToInstall implTask2) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + instKeys `shouldBe` [] + length warnings `shouldBe` 1 + + it "partial resolution: one sig resolves, one warns" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + -- sig-pkg has two signatures: Str and Map + sigPkg = withIndefiniteLibrary [mn "Str", mn "Map"] + $ testPackage sigPn Simple + -- impl-pkg only exposes Str, not Map + implPkg = withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instTasks = [ t + | (ComponentKey _ (CInst _), ADRToInstall t) <- result + ] + -- Str resolves so CInst is created, but Map warns + length instTasks `shouldBe` 1 + length (concatMap (.backpackInstEntries) instTasks) `shouldBe` 1 + length warnings `shouldBe` 1 + + it "ModuleRenaming with unresolvable module produces warning" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + -- impl-pkg exposes Foo, not RenamedStr + implPkg = withExposingLibrary [mn "Foo"] + $ testPackage implPn Simple + renameMixin = Mixin + { mixinPackageName = sigPn + , mixinLibraryName = LMainLibName + , mixinIncludeRenaming = IncludeRenaming + { includeProvidesRn = DefaultRenaming + , includeRequiresRn = + ModuleRenaming [(mn "Str", mn "RenamedStr")] + } + } + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [renameMixin] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + instKeys `shouldBe` [] + length warnings `shouldBe` 1 + + it "non-indefinite mixin dep produces no warning" $ do + let regularPn = mkPackageName "regular-pkg" + consumerPn = mkPackageName "consumer" + -- regular-pkg is NOT indefinite (no signatures) + regularPkg = withExposingLibrary [mn "Foo"] + $ testPackage regularPn Simple + consumerPkg = withDeps [regularPn] + $ withMixins [defaultMixin regularPn] + $ withLibrary + $ testPackage consumerPn Simple + regularTask = mockTask regularPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (regularPn, ADRToInstall regularTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey regularPn CLib, ADRToInstall regularTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + instKeys `shouldBe` [] + warnings `shouldSatisfy` null + + it "sub-library mixin creates CInst task" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + implPkg = withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + -- Consumer has the mixin on a sub-library, not the main library + consumerPkg = withDeps [sigPn, implPn] + $ withSubLibMixins "internal" [defaultMixin sigPn] + $ withSubLibs ["internal"] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + length instKeys `shouldBe` 1 + warnings `shouldSatisfy` null + + it "impl module in sub-library is found" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + -- impl-pkg exposes Str only via a sub-library, not the main library + implPkg = withExposingSubLib "internal" [mn "Str"] + $ withLibrary + $ testPackage implPn Simple + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + length instKeys `shouldBe` 1 + warnings `shouldSatisfy` null + + it "sub-library mixin + sub-library impl module resolves" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + -- impl-pkg exposes Str only via sub-library + implPkg = withExposingSubLib "str-impl" [mn "Str"] + $ withLibrary + $ testPackage implPn Simple + -- consumer has mixin on its sub-library + consumerPkg = withDeps [sigPn, implPn] + $ withSubLibMixins "uses-sig" [defaultMixin sigPn] + $ withSubLibs ["uses-sig"] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + length instKeys `shouldBe` 1 + warnings `shouldSatisfy` null + + it "main lib and sub-lib both with same mixin deduplicates CInst" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + implPkg = withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + -- Both main library and sub-library reference the same mixin + consumerPkg = withDeps [sigPn, implPn] + $ withSubLibMixins "internal" [defaultMixin sigPn] + $ withSubLibs ["internal"] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + -- Same mixin → same hash → deduplicated to one CInst + length instKeys `shouldBe` 1 + warnings `shouldSatisfy` null + + it "impl module exposed by both main lib and sub-lib is not ambiguous" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + -- impl-pkg exposes Str in both main library and sub-library + implPkg = withExposingSubLib "extra" [mn "Str"] + $ withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + -- Same package exposes it — should resolve (not count as two candidates) + length instKeys `shouldBe` 1 + warnings `shouldSatisfy` null + + it "sub-lib with no mixins produces no CInst" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + implPkg = withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + -- Sub-library exists but has no mixins; only main lib matters + consumerPkg = withSubLibs ["internal"] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + instKeys `shouldBe` [] + warnings `shouldSatisfy` null + + it "transitive chain: inherited sigs filled in CInst" $ do + -- str-sig (indefinite, sig: Str) + -- logger-sig (indefinite, sig: Logger, depends on str-sig) + -- impl-pkg (concrete, exposes: Str, Logger) + -- consumer (mixins: logger-sig requires (Logger as Logger)) + -- logger-sig CInst must fill BOTH Logger (own) and Str (inherited) + let strSigPn = mkPackageName "str-sig" + loggerSigPn = mkPackageName "logger-sig" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + strSigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage strSigPn Simple + loggerSigPkg = withIndefiniteLibraryDeps [mn "Logger"] [strSigPn] + $ testPackage loggerSigPn Simple + implPkg = withExposingLibrary [mn "Str", mn "Logger"] + $ testPackage implPn Simple + consumerPkg = withDeps [strSigPn, loggerSigPn, implPn] + $ withMixins [defaultMixin loggerSigPn] + $ withLibrary + $ testPackage consumerPn Simple + strSigTask = mockTask strSigPkg + loggerSigTask = mockTask loggerSigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (strSigPn, ADRToInstall strSigTask) + , (loggerSigPn, ADRToInstall loggerSigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey strSigPn CLib, ADRToInstall strSigTask) + , (ComponentKey loggerSigPn CLib, ADRToInstall loggerSigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instEntries = [ (ck, t.backpackInstEntries) + | (ck@(ComponentKey pn (CInst _)), ADRToInstall t) + <- result + , pn == loggerSigPn + ] + -- Exactly one CInst for logger-sig + length instEntries `shouldBe` 1 + -- The CInst must have entries for BOTH Logger and Str + case instEntries of + [(_, entries)] -> do + let sigNames = map (\(s, _, _) -> s) entries + sigNames `shouldSatisfy` (mn "Logger" `elem`) + sigNames `shouldSatisfy` (mn "Str" `elem`) + _ -> error "expected exactly one CInst" + warnings `shouldSatisfy` null + + it "transitive chain: no inherited sigs from concrete dep" $ do + -- sig-pkg (indefinite, sig: Str) + -- concrete-dep (concrete lib, depends on sig-pkg in build-depends) + -- consumer (mixins: sig-pkg) + -- sig-pkg CInst should only fill Str (no inherited sigs from concrete) + let sigPn = mkPackageName "sig-pkg" + concretePn = mkPackageName "concrete-dep" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + -- concrete-dep is NOT indefinite — just a regular package + concretePkg = withLibrary $ testPackage concretePn Simple + implPkg = withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + concreteTask = mockTask concretePkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (concretePn, ADRToInstall concreteTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey concretePn CLib, ADRToInstall concreteTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instEntries = [ (ck, t.backpackInstEntries) + | (ck@(ComponentKey pn (CInst _)), ADRToInstall t) + <- result + , pn == sigPn + ] + length instEntries `shouldBe` 1 + case instEntries of + [(_, entries)] -> do + let sigNames = map (\(s, _, _) -> s) entries + sigNames `shouldBe` [mn "Str"] + _ -> error "expected exactly one CInst" + warnings `shouldSatisfy` null + + it "three-level transitive chain fills all inherited sigs" $ do + -- base-sig (indefinite, sig: Base) + -- mid-sig (indefinite, sig: Mid, depends on base-sig) + -- top-sig (indefinite, sig: Top, depends on mid-sig) + -- impl (concrete, exposes: Base, Mid, Top) + -- consumer (mixins: top-sig) + -- top-sig CInst must fill Top (own) + Mid (from mid-sig) + Base (from + -- base-sig, transitively) + let baseSigPn = mkPackageName "base-sig" + midSigPn = mkPackageName "mid-sig" + topSigPn = mkPackageName "top-sig" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + baseSigPkg = withIndefiniteLibrary [mn "Base"] + $ testPackage baseSigPn Simple + midSigPkg = withIndefiniteLibraryDeps [mn "Mid"] [baseSigPn] + $ testPackage midSigPn Simple + topSigPkg = withIndefiniteLibraryDeps [mn "Top"] [midSigPn] + $ testPackage topSigPn Simple + implPkg = withExposingLibrary [mn "Base", mn "Mid", mn "Top"] + $ testPackage implPn Simple + consumerPkg = withDeps [baseSigPn, midSigPn, topSigPn, implPn] + $ withMixins [defaultMixin topSigPn] + $ withLibrary + $ testPackage consumerPn Simple + baseSigTask = mockTask baseSigPkg + midSigTask = mockTask midSigPkg + topSigTask = mockTask topSigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (baseSigPn, ADRToInstall baseSigTask) + , (midSigPn, ADRToInstall midSigTask) + , (topSigPn, ADRToInstall topSigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey baseSigPn CLib, ADRToInstall baseSigTask) + , (ComponentKey midSigPn CLib, ADRToInstall midSigTask) + , (ComponentKey topSigPn CLib, ADRToInstall topSigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + topInstEntries = [ t.backpackInstEntries + | (ComponentKey pn (CInst _), ADRToInstall t) + <- result + , pn == topSigPn + ] + length topInstEntries `shouldBe` 1 + case topInstEntries of + [entries] -> do + let sigNames = map (\(s, _, _) -> s) entries + length sigNames `shouldBe` 3 + sigNames `shouldSatisfy` (mn "Top" `elem`) + sigNames `shouldSatisfy` (mn "Mid" `elem`) + sigNames `shouldSatisfy` (mn "Base" `elem`) + _ -> error "expected exactly one CInst for top-sig" + warnings `shouldSatisfy` null + + it "diamond deps: shared indefinite dep's sigs not duplicated" $ do + -- common-sig (indefinite, sig: Common) + -- a-sig (indefinite, sig: SigA, depends on common-sig) + -- b-sig (indefinite, sig: SigB, depends on common-sig) + -- top-sig (indefinite, sig: Top, depends on a-sig AND b-sig) + -- impl (concrete, exposes: Common, SigA, SigB, Top) + -- consumer (mixins: top-sig) + -- top-sig CInst must fill Top + SigA + SigB + Common (no duplicates) + let commonSigPn = mkPackageName "common-sig" + aSigPn = mkPackageName "a-sig" + bSigPn = mkPackageName "b-sig" + topSigPn = mkPackageName "top-sig" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + commonSigPkg = withIndefiniteLibrary [mn "Common"] + $ testPackage commonSigPn Simple + aSigPkg = withIndefiniteLibraryDeps [mn "SigA"] [commonSigPn] + $ testPackage aSigPn Simple + bSigPkg = withIndefiniteLibraryDeps [mn "SigB"] [commonSigPn] + $ testPackage bSigPn Simple + topSigPkg = withIndefiniteLibraryDeps [mn "Top"] [aSigPn, bSigPn] + $ testPackage topSigPn Simple + implPkg = withExposingLibrary [mn "Common", mn "SigA", mn "SigB", mn "Top"] + $ testPackage implPn Simple + consumerPkg = withDeps [commonSigPn, aSigPn, bSigPn, topSigPn, implPn] + $ withMixins [defaultMixin topSigPn] + $ withLibrary + $ testPackage consumerPn Simple + commonSigTask = mockTask commonSigPkg + aSigTask = mockTask aSigPkg + bSigTask = mockTask bSigPkg + topSigTask = mockTask topSigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (commonSigPn, ADRToInstall commonSigTask) + , (aSigPn, ADRToInstall aSigTask) + , (bSigPn, ADRToInstall bSigTask) + , (topSigPn, ADRToInstall topSigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey commonSigPn CLib, ADRToInstall commonSigTask) + , (ComponentKey aSigPn CLib, ADRToInstall aSigTask) + , (ComponentKey bSigPn CLib, ADRToInstall bSigTask) + , (ComponentKey topSigPn CLib, ADRToInstall topSigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + topInstEntries = [ t.backpackInstEntries + | (ComponentKey pn (CInst _), ADRToInstall t) + <- result + , pn == topSigPn + ] + length topInstEntries `shouldBe` 1 + case topInstEntries of + [entries] -> do + let sigNames = map (\(s, _, _) -> s) entries + -- Exactly 4 unique sigs: Top (own) + SigA + SigB + Common (inherited) + -- Common should NOT appear twice despite diamond. + length sigNames `shouldBe` 4 + sigNames `shouldSatisfy` (mn "Top" `elem`) + sigNames `shouldSatisfy` (mn "SigA" `elem`) + sigNames `shouldSatisfy` (mn "SigB" `elem`) + sigNames `shouldSatisfy` (mn "Common" `elem`) + _ -> error "expected exactly one CInst for top-sig" + warnings `shouldSatisfy` null + + it "inherited sig not resolvable produces warning" $ do + -- str-sig (indefinite, sig: Str) + -- logger-sig (indefinite, sig: Logger, depends on str-sig) + -- impl-pkg only exposes Logger, NOT Str + -- consumer (mixins: logger-sig) + -- The inherited Str cannot be resolved → warning + let strSigPn = mkPackageName "str-sig" + loggerSigPn = mkPackageName "logger-sig" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + strSigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage strSigPn Simple + loggerSigPkg = withIndefiniteLibraryDeps [mn "Logger"] [strSigPn] + $ testPackage loggerSigPn Simple + -- impl-pkg only has Logger, NOT Str + implPkg = withExposingLibrary [mn "Logger"] + $ testPackage implPn Simple + consumerPkg = withDeps [strSigPn, loggerSigPn, implPn] + $ withMixins [defaultMixin loggerSigPn] + $ withLibrary + $ testPackage consumerPn Simple + strSigTask = mockTask strSigPkg + loggerSigTask = mockTask loggerSigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (strSigPn, ADRToInstall strSigTask) + , (loggerSigPn, ADRToInstall loggerSigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey strSigPn CLib, ADRToInstall strSigTask) + , (ComponentKey loggerSigPn CLib, ADRToInstall loggerSigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instEntries = [ t.backpackInstEntries + | (ComponentKey pn (CInst _), ADRToInstall t) + <- result + , pn == loggerSigPn + ] + -- CInst still created (for the Logger sig that resolves) + length instEntries `shouldBe` 1 + case instEntries of + [entries] -> do + let sigNames = map (\(s, _, _) -> s) entries + -- Only Logger resolved; Str could not be found + sigNames `shouldBe` [mn "Logger"] + _ -> error "expected exactly one CInst" + -- Warning for unresolvable inherited sig + length warnings `shouldBe` 1 + + it "two consumers with different impls produce two CInst tasks" $ do + -- sig-pkg (indefinite, sig: Str) + -- impl-a (concrete, exposes: Str) + -- impl-b (concrete, exposes: Str) + -- consumer-a (mixins: sig-pkg, depends on impl-a) + -- consumer-b (mixins: sig-pkg, depends on impl-b) + -- Should produce TWO CInst tasks for sig-pkg with different hashes. + let sigPn = mkPackageName "sig-pkg" + implAPn = mkPackageName "impl-a" + implBPn = mkPackageName "impl-b" + consumerAPn = mkPackageName "consumer-a" + consumerBPn = mkPackageName "consumer-b" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + implAPkg = withExposingLibrary [mn "Str"] + $ testPackage implAPn Simple + implBPkg = withExposingLibrary [mn "Str"] + $ testPackage implBPn Simple + consumerAPkg = withDeps [sigPn, implAPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerAPn Simple + consumerBPkg = withDeps [sigPn, implBPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerBPn Simple + sigTask = mockTask sigPkg + implATask = mockTask implAPkg + implBTask = mockTask implBPkg + consumerATask = mockTask consumerAPkg + consumerBTask = mockTask consumerBPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implAPn, ADRToInstall implATask) + , (implBPn, ADRToInstall implBTask) + , (consumerAPn, ADRToInstall consumerATask) + , (consumerBPn, ADRToInstall consumerBTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implAPn CLib, ADRToInstall implATask) + , (ComponentKey implBPn CLib, ADRToInstall implBTask) + , (ComponentKey consumerAPn CLib, ADRToInstall consumerATask) + , (ComponentKey consumerBPn CLib, ADRToInstall consumerBTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey pn (CInst _)), _) <- result + , pn == sigPn + ] + -- Two different CInst tasks for sig-pkg (different impl hashes) + length instKeys `shouldBe` 2 + warnings `shouldSatisfy` null + + it "two consumers with same impl produce one CInst task (dedup)" $ do + -- sig-pkg (indefinite, sig: Str) + -- impl-pkg (concrete, exposes: Str) + -- consumer-a (mixins: sig-pkg, depends on impl-pkg) + -- consumer-b (mixins: sig-pkg, depends on impl-pkg) + -- Both consumers use the same impl → same hash → ONE CInst task. + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerAPn = mkPackageName "consumer-a" + consumerBPn = mkPackageName "consumer-b" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + implPkg = withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + consumerAPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerAPn Simple + consumerBPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerBPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerATask = mockTask consumerAPkg + consumerBTask = mockTask consumerBPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerAPn, ADRToInstall consumerATask) + , (consumerBPn, ADRToInstall consumerBTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerAPn CLib, ADRToInstall consumerATask) + , (ComponentKey consumerBPn CLib, ADRToInstall consumerBTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey pn (CInst _)), _) <- result + , pn == sigPn + ] + -- Same impl → same hash → deduplicated to one CInst task + length instKeys `shouldBe` 1 + warnings `shouldSatisfy` null + + it "impl out of consumer scope is not used for resolution" $ do + -- sig-pkg (indefinite, sig: Str) + -- impl-pkg (concrete, exposes: Str) — exists in plan + -- consumer (mixins: sig-pkg, depends on sig-pkg only, NOT impl-pkg) + -- impl-pkg is out of consumer's build-depends scope → no resolution → warning + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + implPkg = withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + consumerPkg = withDeps [sigPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey pn (CInst _)), _) <- result + , pn == sigPn + ] + -- impl-pkg not in consumer's build-depends → can't resolve → no CInst + length instKeys `shouldBe` 0 + length warnings `shouldBe` 1 + + it "three consumers: two same impl, one different → two CInsts" $ do + -- sig-pkg (indefinite, sig: Str) + -- impl-a, impl-b (concrete, expose: Str) + -- consumer-1 depends on impl-a, consumer-2 depends on impl-a, + -- consumer-3 depends on impl-b + -- → two CInst tasks (impl-a deduped, impl-b separate) + let sigPn = mkPackageName "sig-pkg" + implAPn = mkPackageName "impl-a" + implBPn = mkPackageName "impl-b" + c1Pn = mkPackageName "consumer-1" + c2Pn = mkPackageName "consumer-2" + c3Pn = mkPackageName "consumer-3" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + implAPkg = withExposingLibrary [mn "Str"] + $ testPackage implAPn Simple + implBPkg = withExposingLibrary [mn "Str"] + $ testPackage implBPn Simple + c1Pkg = withDeps [sigPn, implAPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage c1Pn Simple + c2Pkg = withDeps [sigPn, implAPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage c2Pn Simple + c3Pkg = withDeps [sigPn, implBPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage c3Pn Simple + sigTask = mockTask sigPkg + implATask = mockTask implAPkg + implBTask = mockTask implBPkg + c1Task = mockTask c1Pkg + c2Task = mockTask c2Pkg + c3Task = mockTask c3Pkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implAPn, ADRToInstall implATask) + , (implBPn, ADRToInstall implBTask) + , (c1Pn, ADRToInstall c1Task) + , (c2Pn, ADRToInstall c2Task) + , (c3Pn, ADRToInstall c3Task) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implAPn CLib, ADRToInstall implATask) + , (ComponentKey implBPn CLib, ADRToInstall implBTask) + , (ComponentKey c1Pn CLib, ADRToInstall c1Task) + , (ComponentKey c2Pn CLib, ADRToInstall c2Task) + , (ComponentKey c3Pn CLib, ADRToInstall c3Task) + ] + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey pn (CInst _)), _) <- result + , pn == sigPn + ] + -- impl-a used twice (deduped) + impl-b once = 2 CInst tasks + length instKeys `shouldBe` 2 + warnings `shouldSatisfy` null + + it "ADRFound impl resolved via installedModules map" $ do + -- sig-pkg is ADRToInstall (indefinite), impl-pkg is ADRFound + -- (already installed). The installed modules map should allow + -- module resolution for the ADRFound impl. + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + consumerTask = mockTask consumerPkg + implPid = PackageIdentifier implPn (mkVersion [1, 0]) + implGid = unsafeParseGhcPkgId "impl-pkg-1.0-abc" + implFound = ADRFound Snap (Library implPid + (installedLibraryInfoFromGhcPkgId implGid)) + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, implFound) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, implFound) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + -- Provide the impl's modules via the installed modules map + instMods = Map.singleton implPn (Set.singleton (mn "Str")) + (result, warnings) = addInstantiationTasks instMods origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey pn (CInst _)), _) <- result + , pn == sigPn + ] + -- CInst task should be created using the installed modules map + length instKeys `shouldBe` 1 + warnings `shouldSatisfy` null + -- Verify the impl's GhcPkgId is in the CInst task's present map + let instPresent = [ t.present + | (ComponentKey pn (CInst _), ADRToInstall t) <- result + , pn == sigPn + ] + case instPresent of + [pm] -> Map.lookup implPid pm `shouldBe` Just implGid + _ -> expectationFailure "Expected exactly one CInst task" + + it "ADRFound impl not in installedModules produces warning" $ do + -- impl-pkg is ADRFound but the installedModules map is empty, + -- so module resolution fails and a warning is produced. + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + consumerTask = mockTask consumerPkg + implPid = PackageIdentifier implPn (mkVersion [1, 0]) + implGid = unsafeParseGhcPkgId "impl-pkg-1.0-abc" + implFound = ADRFound Snap (Library implPid + (installedLibraryInfoFromGhcPkgId implGid)) + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, implFound) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, implFound) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + -- Empty installed modules map — impl can't be resolved + (result, warnings) = addInstantiationTasks Map.empty origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + instKeys `shouldBe` [] + length warnings `shouldBe` 1 + + it "ADRFound impl not in consumer scope is ignored" $ do + -- impl-pkg is ADRFound and in installedModules, but the consumer + -- doesn't list it in build-depends, so it's out of scope. + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + -- Consumer depends on sig-pkg only, NOT impl-pkg + consumerPkg = withDeps [sigPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + consumerTask = mockTask consumerPkg + implPid = PackageIdentifier implPn (mkVersion [1, 0]) + implGid = unsafeParseGhcPkgId "impl-pkg-1.0-abc" + implFound = ADRFound Snap (Library implPid + (installedLibraryInfoFromGhcPkgId implGid)) + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, implFound) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, implFound) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + instMods = Map.singleton implPn (Set.singleton (mn "Str")) + (result, warnings) = addInstantiationTasks instMods origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + instKeys `shouldBe` [] + length warnings `shouldBe` 1 + + it "ADRFound + ADRToInstall both exposing module is ambiguous" $ do + -- Two packages expose the same module: one ADRToInstall, one ADRFound. + -- This should produce an ambiguity warning. + let sigPn = mkPackageName "sig-pkg" + impl1Pn = mkPackageName "impl1" + impl2Pn = mkPackageName "impl2" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + impl1Pkg = withExposingLibrary [mn "Str"] + $ testPackage impl1Pn Simple + consumerPkg = withDeps [sigPn, impl1Pn, impl2Pn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + impl1Task = mockTask impl1Pkg + consumerTask = mockTask consumerPkg + impl2Pid = PackageIdentifier impl2Pn (mkVersion [1, 0]) + impl2Gid = unsafeParseGhcPkgId "impl2-1.0-abc" + impl2Found = ADRFound Snap (Library impl2Pid + (installedLibraryInfoFromGhcPkgId impl2Gid)) + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (impl1Pn, ADRToInstall impl1Task) + , (impl2Pn, impl2Found) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey impl1Pn CLib, ADRToInstall impl1Task) + , (ComponentKey impl2Pn CLib, impl2Found) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + -- impl2 also exposes Str via installedModules + instMods = Map.singleton impl2Pn (Set.singleton (mn "Str")) + (result, warnings) = addInstantiationTasks instMods origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + -- Ambiguous → no CInst, warning produced + instKeys `shouldBe` [] + length warnings `shouldBe` 1 + + it "two ADRFound impls exposing same module is ambiguous" $ do + let sigPn = mkPackageName "sig-pkg" + impl1Pn = mkPackageName "impl1" + impl2Pn = mkPackageName "impl2" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + consumerPkg = withDeps [sigPn, impl1Pn, impl2Pn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + consumerTask = mockTask consumerPkg + impl1Pid = PackageIdentifier impl1Pn (mkVersion [1, 0]) + impl1Gid = unsafeParseGhcPkgId "impl1-1.0-aaa" + impl1Found = ADRFound Snap (Library impl1Pid + (installedLibraryInfoFromGhcPkgId impl1Gid)) + impl2Pid = PackageIdentifier impl2Pn (mkVersion [1, 0]) + impl2Gid = unsafeParseGhcPkgId "impl2-1.0-bbb" + impl2Found = ADRFound Snap (Library impl2Pid + (installedLibraryInfoFromGhcPkgId impl2Gid)) + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (impl1Pn, impl1Found) + , (impl2Pn, impl2Found) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey impl1Pn CLib, impl1Found) + , (ComponentKey impl2Pn CLib, impl2Found) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + instMods = Map.fromList + [ (impl1Pn, Set.singleton (mn "Str")) + , (impl2Pn, Set.singleton (mn "Str")) + ] + (result, warnings) = addInstantiationTasks instMods origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + instKeys `shouldBe` [] + length warnings `shouldBe` 1 + + it "ADRFound impl with wrong module in installedModules not matched" $ do + -- impl-pkg is ADRFound and in installedModules, but exposes "Other" + -- not "Str", so resolution fails. + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = mockTask sigPkg + consumerTask = mockTask consumerPkg + implPid = PackageIdentifier implPn (mkVersion [1, 0]) + implGid = unsafeParseGhcPkgId "impl-pkg-1.0-abc" + implFound = ADRFound Snap (Library implPid + (installedLibraryInfoFromGhcPkgId implGid)) + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, implFound) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, implFound) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + -- impl exposes "Other", not "Str" + instMods = Map.singleton implPn (Set.singleton (mn "Other")) + (result, warnings) = addInstantiationTasks instMods origAdrs expanded + instKeys = [ ck | (ck@(ComponentKey _ (CInst _)), _) <- result ] + instKeys `shouldBe` [] + length warnings `shouldBe` 1 + + it "CInst inherits buildHaddocks = True from sig task" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + implPkg = withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = (mockTask sigPkg) { buildHaddocks = True } + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, _) = addInstantiationTasks Map.empty origAdrs expanded + instHaddocks = [ t.buildHaddocks + | (ComponentKey pn (CInst _), ADRToInstall t) + <- result + , pn == sigPn + ] + instHaddocks `shouldBe` [True] + + it "CInst inherits buildHaddocks = False from sig task" $ do + let sigPn = mkPackageName "sig-pkg" + implPn = mkPackageName "impl-pkg" + consumerPn = mkPackageName "consumer" + sigPkg = withIndefiniteLibrary [mn "Str"] + $ testPackage sigPn Simple + implPkg = withExposingLibrary [mn "Str"] + $ testPackage implPn Simple + consumerPkg = withDeps [sigPn, implPn] + $ withMixins [defaultMixin sigPn] + $ withLibrary + $ testPackage consumerPn Simple + sigTask = (mockTask sigPkg) { buildHaddocks = False } + implTask = mockTask implPkg + consumerTask = mockTask consumerPkg + origAdrs = + [ (sigPn, ADRToInstall sigTask) + , (implPn, ADRToInstall implTask) + , (consumerPn, ADRToInstall consumerTask) + ] + expanded = + [ (ComponentKey sigPn CLib, ADRToInstall sigTask) + , (ComponentKey implPn CLib, ADRToInstall implTask) + , (ComponentKey consumerPn CLib, ADRToInstall consumerTask) + ] + (result, _) = addInstantiationTasks Map.empty origAdrs expanded + instHaddocks = [ t.buildHaddocks + | (ComponentKey pn (CInst _), ADRToInstall t) + <- result + , pn == sigPn + ] + instHaddocks `shouldBe` [False] + + describe "findGhcPkgId" $ do + it "finds matching GhcPkgId by package name" $ do + let implGid = unsafeParseGhcPkgId "impl-pkg-0.1.0.0-abc123" + depsMap = Map.fromList + [ (PackageIdentifier (mkPackageName "impl-pkg") (mkVersion [0,1,0,0]) + , implGid) + ] + findGhcPkgId depsMap (mkPackageName "impl-pkg") `shouldBe` Just implGid + + it "returns Nothing for missing package" $ do + let depsMap = Map.fromList + [ (PackageIdentifier (mkPackageName "other") (mkVersion [1,0]) + , unsafeParseGhcPkgId "other-1.0-xyz") + ] + findGhcPkgId depsMap (mkPackageName "impl-pkg") `shouldBe` Nothing + + it "returns first match when multiple versions exist" $ do + let gid1 = unsafeParseGhcPkgId "pkg-1.0-aaa" + gid2 = unsafeParseGhcPkgId "pkg-2.0-bbb" + depsMap = Map.fromList + [ (PackageIdentifier (mkPackageName "pkg") (mkVersion [1,0]), gid1) + , (PackageIdentifier (mkPackageName "pkg") (mkVersion [2,0]), gid2) + ] + -- Map.toList iterates in key order; (pkg-1.0) < (pkg-2.0) + findGhcPkgId depsMap (mkPackageName "pkg") `shouldBe` Just gid1 + + it "returns Nothing for empty deps map" $ do + findGhcPkgId Map.empty (mkPackageName "anything") `shouldBe` Nothing + + describe "mkInstantiateWithOpts" $ do + it "produces correct --instantiate-with flag" $ do + let implGid = unsafeParseGhcPkgId "impl-pkg-0.1.0.0-abc123" + depsMap = Map.fromList + [ (PackageIdentifier (mkPackageName "impl-pkg") (mkVersion [0,1,0,0]) + , implGid) + ] + entries = [(mn "Str", mkPackageName "impl-pkg", mn "Str")] + result = mkInstantiateWithOpts entries depsMap + result `shouldBe` + ["--instantiate-with=Str=impl-pkg-0.1.0.0-abc123:Str"] + + it "produces empty list when no entries" $ do + mkInstantiateWithOpts [] Map.empty `shouldBe` [] + + it "skips entry when implementing package not in deps map" $ do + let entries = [(mn "Str", mkPackageName "missing-pkg", mn "Str")] + depsMap = Map.fromList + [ (PackageIdentifier (mkPackageName "other") (mkVersion [1,0]) + , unsafeParseGhcPkgId "other-1.0-xyz") + ] + mkInstantiateWithOpts entries depsMap `shouldBe` [] + + it "handles multiple instantiation entries" $ do + let gid1 = unsafeParseGhcPkgId "impl-a-1.0-aaa" + gid2 = unsafeParseGhcPkgId "impl-b-1.0-bbb" + depsMap = Map.fromList + [ (PackageIdentifier (mkPackageName "impl-a") (mkVersion [1,0]), gid1) + , (PackageIdentifier (mkPackageName "impl-b") (mkVersion [1,0]), gid2) + ] + entries = + [ (mn "SigA", mkPackageName "impl-a", mn "ModA") + , (mn "SigB", mkPackageName "impl-b", mn "ModB") + ] + result = mkInstantiateWithOpts entries depsMap + length result `shouldBe` 2 + result `shouldBe` + [ "--instantiate-with=SigA=impl-a-1.0-aaa:ModA" + , "--instantiate-with=SigB=impl-b-1.0-bbb:ModB" + ] + + it "handles renamed module (sigName /= implModuleName)" $ do + let implGid = unsafeParseGhcPkgId "impl-pkg-1.0-abc" + depsMap = Map.fromList + [ (PackageIdentifier (mkPackageName "impl-pkg") (mkVersion [1,0]) + , implGid) + ] + entries = [(mn "Signature", mkPackageName "impl-pkg", mn "ConcreteImpl")] + result = mkInstantiateWithOpts entries depsMap + result `shouldBe` + ["--instantiate-with=Signature=impl-pkg-1.0-abc:ConcreteImpl"] + + it "handles hierarchical module names" $ do + let implGid = unsafeParseGhcPkgId "impl-pkg-1.0-abc" + depsMap = Map.fromList + [ (PackageIdentifier (mkPackageName "impl-pkg") (mkVersion [1,0]) + , implGid) + ] + entries = + [(mn "Data.Map.Sig", mkPackageName "impl-pkg", mn "Data.Map.Strict")] + result = mkInstantiateWithOpts entries depsMap + result `shouldBe` + ["--instantiate-with=Data.Map.Sig=impl-pkg-1.0-abc:Data.Map.Strict"] + + describe "ConfigCacheType persistence" $ do + it "round-trips ConfigCacheTypeConfig" $ do + let v = ConfigCacheTypeConfig + fromPersistValue (toPersistValue v) `shouldBe` Right v + + it "round-trips ConfigCacheTypeFlagLibrary" $ do + let gid = unsafeParseGhcPkgId "foo-1.0-abc123" + v = ConfigCacheTypeFlagLibrary gid + fromPersistValue (toPersistValue v) `shouldBe` Right v + + it "round-trips ConfigCacheTypeFlagExecutable" $ do + let pid = PackageIdentifier (mkPackageName "bar") (mkVersion [2,0]) + v = ConfigCacheTypeFlagExecutable pid + fromPersistValue (toPersistValue v) `shouldBe` Right v + + it "round-trips ConfigCacheTypeInstantiation" $ do + let v = ConfigCacheTypeInstantiation "941095d7fd7eb1e4" + fromPersistValue (toPersistValue v) `shouldBe` Right v + + it "round-trips ConfigCacheTypeInstantiation with empty suffix" $ do + let v = ConfigCacheTypeInstantiation "" + fromPersistValue (toPersistValue v) `shouldBe` Right v + + it "rejects unknown cache type" $ do + fromPersistValue (PersistText "unknown:foo") + `shouldSatisfy` \case + Left _ -> True + Right (_ :: ConfigCacheType) -> False + +-- | Parse a GhcPkgId from text, throwing an error on failure. +unsafeParseGhcPkgId :: Text -> GhcPkgId +unsafeParseGhcPkgId t = case parseGhcPkgId t of + Just gid -> gid + Nothing -> error $ "unsafeParseGhcPkgId: invalid GhcPkgId: " ++ show t + +-- | Helper: compute the list of buildable components for a package (same +-- logic as expandToComponentKeys). +packageBuildableComponents :: Package -> [NamedComponent] +packageBuildableComponents pkg = + let libComps = [CLib | isJust pkg.library && buildableLib] + subLibComps = + map CSubLib $ Set.toList $ getBuildableSet pkg.subLibraries + flibComps = + map CFlib $ Set.toList $ getBuildableSet pkg.foreignLibraries + exeComps = + map CExe $ Set.toList $ getBuildableSet pkg.executables + in libComps ++ subLibComps ++ flibComps ++ exeComps + where + buildableLib = case pkg.library of + Just lib -> lib.buildInfo.buildable + Nothing -> False + +-- | Helper: simulate component key expansion for build tasks. +packageToComponentKeys :: PackageName -> Package -> [ComponentKey] +packageToComponentKeys pn pkg + | shouldSplitComponents pkg = + case packageBuildableComponents pkg of + [] -> [ComponentKey pn CLib] + comps -> map (ComponentKey pn) comps + | otherwise = [ComponentKey pn CLib] + +-- | Helper: simulate final key expansion (mirrors addFinal logic). +packageToFinalKeys :: + PackageName -> Package -> Set NamedComponent -> [ComponentKey] +packageToFinalKeys pn pkg components + | shouldSplitComponents pkg = + let testComps = filter isCTest $ Set.toList components + benchComps = filter isCBench $ Set.toList components + in case testComps ++ benchComps of + [] -> [ComponentKey pn CLib] + comps -> map (ComponentKey pn) comps + | otherwise = [ComponentKey pn CLib] + where + isCTest (CTest _) = True + isCTest _ = False + isCBench (CBench _) = True + isCBench _ = False + +-- | Build a mock Task wrapping a Package. Uses 'error' for TaskConfigOpts +-- fields that addInstantiationTasks never accesses (envConfig, baseConfigOpts, +-- pkgConfigOpts). +mockTask :: Package -> Task +mockTask pkg = Task + { taskType = TTRemotePackage Immutable pkg + (error "mockTask: PackageLocationImmutable not needed") + , configOpts = TaskConfigOpts + { missing = Set.empty + , envConfig = error "mockTask: envConfig not needed" + , baseConfigOpts = error "mockTask: baseConfigOpts not needed" + , isLocalNonExtraDep = False + , isMutable = Immutable + , pkgConfigOpts = error "mockTask: pkgConfigOpts not needed" + , instantiationDeps = [] + } + , buildHaddocks = False + , present = Map.empty + , cachePkgSrc = CacheSrcUpstream + , buildTypeConfig = False + , backpackInstEntries = [] + } + +-- | Add a library with exposed modules (for testing module resolution). +withExposingLibrary :: [ModuleName] -> Package -> Package +withExposingLibrary mods pkg = pkg + { library = Just StackLibrary + { name = emptyCompName + , buildInfo = testBuildInfo + , exposedModules = mods + , signatures = [] + } + } + +-- | Add mixins to a package's main library buildInfo. +withMixins :: [Mixin] -> Package -> Package +withMixins ms pkg = case pkg.library of + Just (StackLibrary n bi em sigs) -> pkg + { library = Just (StackLibrary n bi { mixins = ms } em sigs) + } + Nothing -> pkg + +-- | Helper: create a simple mixin with default renaming. +defaultMixin :: PackageName -> Mixin +defaultMixin pn = Mixin + { mixinPackageName = pn + , mixinLibraryName = LMainLibName + , mixinIncludeRenaming = defaultIncludeRenaming + } + +-- | Helper: create a mixin with explicit requires renaming. +explicitRequiresMixin :: + PackageName + -> [(ModuleName, ModuleName)] + -> Mixin +explicitRequiresMixin pn reqs = Mixin + { mixinPackageName = pn + , mixinLibraryName = LMainLibName + , mixinIncludeRenaming = IncludeRenaming + { includeProvidesRn = DefaultRenaming + , includeRequiresRn = ModuleRenaming reqs + } + } + +-- | Add a sub-library with specific exposed modules. +withExposingSubLib :: String -> [ModuleName] -> Package -> Package +withExposingSubLib name mods pkg = pkg + { subLibraries = foldAndMakeCollection id $ + StackLibrary + { name = unqualCompFromString name + , buildInfo = testBuildInfo + , exposedModules = mods + , signatures = [] + } + : toList pkg.subLibraries + } + +-- | Add mixins to a specific sub-library's buildInfo. +withSubLibMixins :: String -> [Mixin] -> Package -> Package +withSubLibMixins subLibName ms pkg = pkg + { subLibraries = foldAndMakeCollection id + [ if lib.name == unqualCompFromString subLibName + then let bi = lib.buildInfo :: StackBuildInfo + in StackLibrary lib.name (bi { mixins = ms }) + lib.exposedModules lib.signatures + else lib + | lib <- toList pkg.subLibraries + ] + } + +-- | Create an indefinite library package that depends on other packages. +-- Used to test transitive Backpack chains where one indefinite package +-- depends on another. +withIndefiniteLibraryDeps :: [ModuleName] -> [PackageName] -> Package -> Package +withIndefiniteLibraryDeps sigs depPkgs pkg = pkg + { library = Just StackLibrary + { name = emptyCompName + , buildInfo = testBuildInfo + { dependency = Map.fromList + [ (pn, DepValue + { versionRange = anyVersion + , depType = AsLibrary DepLibrary + { main = True, subLib = Set.empty } + }) + | pn <- depPkgs + ] + } + , exposedModules = [] + , signatures = sigs + } + } + +-- | Add build-depends on the given packages to a package's main library. +-- This is needed so that module resolution is scoped to the consumer's deps. +withDeps :: [PackageName] -> Package -> Package +withDeps depPkgs pkg = case pkg.library of + Just (StackLibrary n bi em sigs) -> + let newDeps = Map.fromList + [ (pn, DepValue + { versionRange = anyVersion + , depType = AsLibrary DepLibrary + { main = True, subLib = Set.empty } + }) + | pn <- depPkgs + ] + bi' = bi { dependency = Map.union bi.dependency newDeps } + in pkg { library = Just (StackLibrary n bi' em sigs) } + Nothing -> pkg + +-- | Convenience: make a ModuleName from a String. +mn :: String -> ModuleName +mn = Cabal.fromString diff --git a/tests/unit/Stack/Build/ExecuteSpec.hs b/tests/unit/Stack/Build/ExecuteSpec.hs index 7b6a53b1c2..5bfa5c5bdf 100644 --- a/tests/unit/Stack/Build/ExecuteSpec.hs +++ b/tests/unit/Stack/Build/ExecuteSpec.hs @@ -1,15 +1,545 @@ {-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} module Stack.Build.ExecuteSpec ( main , spec ) where -import Stack.Prelude -import Test.Hspec +import qualified Data.Map.Strict as Map +import qualified Data.Set as Set +import Distribution.Types.PackageName ( mkPackageName ) +import Distribution.Types.Version ( mkVersion ) +import Control.Concurrent.Execute + ( ActionId (..), ActionType (..) ) +import Stack.Build.Execute + ( finalTestsAndBenches, intraPackageDeps, missingToDeps ) +import Stack.Build.ExecutePackage + ( componentEnableBenchmarks, componentEnableTests + , componentTarget + ) +import Stack.Prelude +import Stack.Types.ComponentUtils ( unqualCompFromString ) +import Stack.Types.NamedComponent ( NamedComponent (..) ) +import Stack.Types.Plan ( ComponentKey (..), componentKeyPkgName ) +import Test.Hspec ( Spec, describe, hspec, it, shouldBe, shouldSatisfy ) main :: IO () main = hspec spec spec :: Spec -spec = pure () +spec = do + describe "ActionId" $ do + it "uses ComponentKey instead of PackageIdentifier" $ do + let ck = ComponentKey (mkPackageName "my-pkg") CLib + aid = ActionId ck ATBuild + aid `shouldBe` ActionId ck ATBuild + + it "ActionIds with different ComponentKeys are not equal" $ do + let ck1 = ComponentKey (mkPackageName "pkg") CLib + ck2 = ComponentKey (mkPackageName "pkg") + (CTest (unqualCompFromString "test-suite")) + aid1 = ActionId ck1 ATBuild + aid2 = ActionId ck2 ATBuild + (aid1 == aid2) `shouldBe` False + + it "ActionIds with different ActionTypes are not equal" $ do + let ck = ComponentKey (mkPackageName "pkg") CLib + aid1 = ActionId ck ATBuild + aid2 = ActionId ck ATRunTests + (aid1 == aid2) `shouldBe` False + + it "can be used in Sets (dependency tracking)" $ do + let ckLib = ComponentKey (mkPackageName "dep") CLib + ckTest = ComponentKey (mkPackageName "pkg") + (CTest (unqualCompFromString "tests")) + deps = Set.fromList + [ ActionId ckLib ATBuild + , ActionId ckTest ATBuild + , ActionId ckTest ATRunTests + ] + Set.size deps `shouldBe` 3 + + it "dependency from test ATRunTests to test ATBuild works" $ do + let ck = ComponentKey (mkPackageName "pkg") + (CTest (unqualCompFromString "tests")) + buildAction = ActionId ck ATBuild + runAction = ActionId ck ATRunTests + deps = Set.singleton buildAction + Set.member buildAction deps `shouldBe` True + Set.member runAction deps `shouldBe` False + + it "dependency from component to CLib ATBuild maps correctly" $ do + let pkgName = mkPackageName "base-dep" + depKey = ComponentKey pkgName CLib + depAction = ActionId depKey ATBuild + testKey = ComponentKey (mkPackageName "my-pkg") + (CTest (unqualCompFromString "tests")) + testBuild = ActionId testKey ATBuild + deps = Set.fromList [depAction, testBuild] + Set.member depAction deps `shouldBe` True + + describe "ActionType" $ do + it "ATBuildFinal no longer exists (3 constructors only)" $ do + let types = [ATBuild, ATRunTests, ATRunBenchmarks] + length types `shouldBe` 3 + + it "ATBuild < ATRunTests < ATRunBenchmarks (Ord)" $ do + (ATBuild < ATRunTests) `shouldBe` True + (ATRunTests < ATRunBenchmarks) `shouldBe` True + + describe "missingToDeps" $ do + it "maps empty set to empty set" $ do + missingToDeps Set.empty `shouldBe` Set.empty + + it "maps a single PackageIdentifier to ComponentKey CLib ATBuild" $ do + let pid = PackageIdentifier (mkPackageName "base") (mkVersion [4,20,0,0]) + result = missingToDeps (Set.singleton pid) + expected = Set.singleton $ + ActionId (ComponentKey (mkPackageName "base") CLib) ATBuild + result `shouldBe` expected + + it "maps multiple dependencies preserving each package name" $ do + let pid1 = PackageIdentifier (mkPackageName "text") (mkVersion [2,0]) + pid2 = PackageIdentifier (mkPackageName "bytestring") (mkVersion [0,12]) + pid3 = PackageIdentifier (mkPackageName "containers") (mkVersion [0,7]) + result = missingToDeps (Set.fromList [pid1, pid2, pid3]) + Set.size result `shouldBe` 3 + -- All mapped to CLib + result `shouldSatisfy` all + (\(ActionId (ComponentKey _ comp) at) -> comp == CLib && at == ATBuild) + + it "discards version info — only package name matters" $ do + -- Two different versions of the same package should map to the same + -- ActionId (since ComponentKey uses PackageName, not PackageIdentifier) + let pid1 = PackageIdentifier (mkPackageName "aeson") (mkVersion [2,1]) + pid2 = PackageIdentifier (mkPackageName "aeson") (mkVersion [2,2]) + result = missingToDeps (Set.fromList [pid1, pid2]) + -- Set deduplicates: both map to the same ActionId + Set.size result `shouldBe` 1 + + it "preserves the invariant that external deps are always CLib" $ do + let pids = Set.fromList + [ PackageIdentifier (mkPackageName "foo") (mkVersion [1,0]) + , PackageIdentifier (mkPackageName "bar") (mkVersion [2,0]) + ] + result = missingToDeps pids + -- Every resulting ActionId should have CLib component and ATBuild type + forM_ (Set.toList result) $ \(ActionId (ComponentKey _ comp) at) -> do + comp `shouldBe` CLib + at `shouldBe` ATBuild + + describe "action graph invariants" $ do + it "build-only key produces exactly one ATBuild" $ do + -- Simulating toActions (Just build, Nothing): should produce [ATBuild] + let ck = ComponentKey (mkPackageName "pkg") CLib + buildId = ActionId ck ATBuild + -- In toActions, abuild produces one Action with this id + actionIds = [buildId] + length actionIds `shouldBe` 1 + actionIds `shouldBe` [ActionId ck ATBuild] + + it "final-only key produces ATBuild + run actions" $ do + -- Simulating toActions (Nothing, Just final) with tests: + -- Should produce ATBuild (for final) + ATRunTests + let ck = ComponentKey (mkPackageName "pkg") CLib + buildId = ActionId ck ATBuild + runId = ActionId ck ATRunTests + actionIds = [buildId, runId] + -- ATRunTests depends on ATBuild + runDeps = Set.singleton buildId + length actionIds `shouldBe` 2 + Set.member buildId runDeps `shouldBe` True + + it "build+final key produces one ATBuild (no duplicate)" $ do + -- Simulating toActions (Just build, Just final): + -- abuild produces ATBuild, afinal must NOT produce another ATBuild + -- afinal.finalBuild is [] when mbuild is Just + let ck = ComponentKey (mkPackageName "pkg") CLib + -- From abuild + abuildIds = [ActionId ck ATBuild] + -- From afinal.finalBuild when mbuild is Just: empty + afinalBuildIds = [] :: [ActionId] + -- From afinal.finalRun + afinalRunIds = [ActionId ck ATRunTests] + allIds = abuildIds ++ afinalBuildIds ++ afinalRunIds + -- No duplicate ATBuild + let buildCount = length $ filter + (\(ActionId _ at) -> at == ATBuild) allIds + buildCount `shouldBe` 1 + -- Total: 1 ATBuild + 1 ATRunTests + length allIds `shouldBe` 2 + + it "run actions always depend on ATBuild for same ComponentKey" $ do + let ck = ComponentKey (mkPackageName "pkg") + (CTest (unqualCompFromString "tests")) + buildDep = Set.singleton (ActionId ck ATBuild) + -- ATRunTests and ATRunBenchmarks should both depend on ATBuild + runTestsDeps = buildDep + runBenchDeps = buildDep + Set.member (ActionId ck ATBuild) runTestsDeps `shouldBe` True + Set.member (ActionId ck ATBuild) runBenchDeps `shouldBe` True + + describe "per-package derivation from ComponentKey map" $ do + it "single CLib key maps to one package entry" $ do + let ck = ComponentKey (mkPackageName "pkg") CLib + componentMap = Map.singleton ck ("task-value" :: String) + pkgMap = Map.fromList + [ (componentKeyPkgName k, v) | (k, v) <- Map.toList componentMap ] + Map.size pkgMap `shouldBe` 1 + Map.lookup (mkPackageName "pkg") pkgMap `shouldBe` Just "task-value" + + it "multiple components for same package collapse to one entry" $ do + let pn = mkPackageName "pkg" + ck1 = ComponentKey pn CLib + ck2 = ComponentKey pn (CExe (unqualCompFromString "exe")) + ck3 = ComponentKey pn (CTest (unqualCompFromString "test")) + componentMap :: Map ComponentKey String + componentMap = Map.fromList + [(ck1, "lib-task"), (ck2, "exe-task"), (ck3, "test-task")] + -- Map.fromList with last-wins gives us the last component + pkgMap = Map.fromList + [ (componentKeyPkgName k, v) + | (k, v) <- Map.toList componentMap + ] + -- All three map to the same PackageName, so only one entry + Map.size pkgMap `shouldBe` 1 + -- Map.toList on componentMap is ascending by key; + -- CLib < CExe < CTest, so CTest's value wins + Map.lookup pn pkgMap `shouldBe` Just "test-task" + + it "different packages stay separate after derivation" $ do + let ck1 = ComponentKey (mkPackageName "pkg-a") CLib + ck2 = ComponentKey (mkPackageName "pkg-a") + (CExe (unqualCompFromString "exe")) + ck3 = ComponentKey (mkPackageName "pkg-b") CLib + componentMap :: Map ComponentKey String + componentMap = Map.fromList + [(ck1, "a-lib"), (ck2, "a-exe"), (ck3, "b-lib")] + pkgMap = Map.fromList + [ (componentKeyPkgName k, v) + | (k, v) <- Map.toList componentMap + ] + Map.size pkgMap `shouldBe` 2 + Map.member (mkPackageName "pkg-a") pkgMap `shouldBe` True + Map.member (mkPackageName "pkg-b") pkgMap `shouldBe` True + + it "preserves all package names when components are distinct packages" $ do + let keys = + [ ComponentKey (mkPackageName "aeson") CLib + , ComponentKey (mkPackageName "text") CLib + , ComponentKey (mkPackageName "bytestring") CLib + ] + componentMap :: Map ComponentKey Int + componentMap = Map.fromList $ zip keys [1..] + pkgNames = Set.fromList $ map componentKeyPkgName $ Map.keys componentMap + Set.size pkgNames `shouldBe` 3 + + describe "consumer dedup patterns" $ do + -- These test the deduplication patterns used by justLocals, + -- errorOnSnapshot, and warnIfExecutablesWithSameNameCouldBeOverwritten + -- when operating on component-keyed maps. + + it "justLocals pattern: Set deduplicates split-package identifiers" $ do + -- Simulates justLocals: multiple component tasks for the same package + -- produce the same PackageIdentifier; Set.fromList removes duplicates. + let pid = PackageIdentifier (mkPackageName "pkg") (mkVersion [1,0]) + taskPids = [pid, pid, pid] + result = Set.toList $ Set.fromList taskPids + result `shouldBe` [pid] + + it "justLocals pattern: different packages remain distinct" $ do + let pid1 = PackageIdentifier (mkPackageName "pkg-a") (mkVersion [1,0]) + pid2 = PackageIdentifier (mkPackageName "pkg-b") (mkVersion [2,0]) + taskPids = [pid1, pid1, pid2, pid2] + result = Set.toList $ Set.fromList taskPids + length result `shouldBe` 2 + result `shouldSatisfy` (pid1 `elem`) + result `shouldSatisfy` (pid2 `elem`) + + it "errorOnSnapshot pattern: PackageNames are deduplicated" $ do + -- Simulates errorOnSnapshot: extracting PackageNames from ComponentKeys + -- and deduplicating. + let pn = mkPackageName "pkg" + keys = + [ ComponentKey pn CLib + , ComponentKey pn (CExe (unqualCompFromString "exe")) + , ComponentKey pn (CSubLib (unqualCompFromString "sub")) + ] + result = Set.toList $ Set.fromList $ map componentKeyPkgName keys + result `shouldBe` [pn] + + it "errorOnSnapshot pattern: distinct packages stay separate" $ do + let keys = + [ ComponentKey (mkPackageName "a") CLib + , ComponentKey (mkPackageName "a") + (CExe (unqualCompFromString "exe")) + , ComponentKey (mkPackageName "b") CLib + ] + result = Set.toList $ Set.fromList $ map componentKeyPkgName keys + length result `shouldBe` 2 + + it "perPkgTasks pattern: split components yield one entry per package" $ do + -- Simulates perPkgTasks in warnIfExecutablesWithSameNameCouldBeOverwritten + let pn = mkPackageName "pkg" + componentMap :: Map ComponentKey String + componentMap = Map.fromList + [ (ComponentKey pn CLib, "task") + , (ComponentKey pn (CExe (unqualCompFromString "exe1")), "task") + , (ComponentKey pn (CExe (unqualCompFromString "exe2")), "task") + ] + perPkg = Map.fromList + [ (componentKeyPkgName ck, v) + | (ck, v) <- Map.toList componentMap + ] + Map.size perPkg `shouldBe` 1 + + it "perPkgTasks pattern: mixed packages yield correct count" $ do + let componentMap :: Map ComponentKey String + componentMap = Map.fromList + [ (ComponentKey (mkPackageName "a") CLib, "a-task") + , (ComponentKey (mkPackageName "a") + (CExe (unqualCompFromString "exe")), "a-task") + , (ComponentKey (mkPackageName "b") CLib, "b-task") + , (ComponentKey (mkPackageName "b") + (CExe (unqualCompFromString "app")), "b-task") + ] + perPkg = Map.fromList + [ (componentKeyPkgName ck, v) + | (ck, v) <- Map.toList componentMap + ] + Map.size perPkg `shouldBe` 2 + Map.member (mkPackageName "a") perPkg `shouldBe` True + Map.member (mkPackageName "b") perPkg `shouldBe` True + + describe "intraPackageDeps" $ do + it "CLib has no intra-package deps" $ do + let ck = ComponentKey (mkPackageName "pkg") CLib + taskKeys = Set.singleton ck + intraPackageDeps taskKeys ck `shouldBe` Set.empty + + it "CExe depends on CLib ATBuild when CLib is in taskKeys" $ do + let pn = mkPackageName "pkg" + libKey = ComponentKey pn CLib + exeKey = ComponentKey pn (CExe (unqualCompFromString "my-exe")) + taskKeys = Set.fromList [libKey, exeKey] + result = intraPackageDeps taskKeys exeKey + result `shouldBe` + Set.singleton (ActionId libKey ATBuild) + + it "CExe has no intra-package dep when CLib is not in taskKeys" $ do + let pn = mkPackageName "pkg" + exeKey = ComponentKey pn (CExe (unqualCompFromString "my-exe")) + taskKeys = Set.singleton exeKey + intraPackageDeps taskKeys exeKey `shouldBe` Set.empty + + it "CTest depends on CLib ATBuild when CLib is in taskKeys" $ do + let pn = mkPackageName "pkg" + libKey = ComponentKey pn CLib + testKey = ComponentKey pn (CTest (unqualCompFromString "tests")) + taskKeys = Set.singleton libKey + result = intraPackageDeps taskKeys testKey + result `shouldBe` + Set.singleton (ActionId libKey ATBuild) + + it "CBench depends on CLib ATBuild when CLib is in taskKeys" $ do + let pn = mkPackageName "pkg" + libKey = ComponentKey pn CLib + benchKey = ComponentKey pn (CBench (unqualCompFromString "bench")) + taskKeys = Set.singleton libKey + intraPackageDeps taskKeys benchKey `shouldBe` + Set.singleton (ActionId libKey ATBuild) + + it "CSubLib depends on CLib ATBuild when CLib is in taskKeys" $ do + let pn = mkPackageName "pkg" + libKey = ComponentKey pn CLib + subKey = ComponentKey pn (CSubLib (unqualCompFromString "internal")) + taskKeys = Set.singleton libKey + intraPackageDeps taskKeys subKey `shouldBe` + Set.singleton (ActionId libKey ATBuild) + + it "does not depend on CLib of a different package" $ do + let pn1 = mkPackageName "pkg-a" + pn2 = mkPackageName "pkg-b" + libKeyA = ComponentKey pn1 CLib + exeKeyB = ComponentKey pn2 (CExe (unqualCompFromString "exe")) + taskKeys = Set.fromList [libKeyA, exeKeyB] + -- exeKeyB should NOT depend on libKeyA (different package) + intraPackageDeps taskKeys exeKeyB `shouldBe` Set.empty + + it "CLib never has intra-package deps even when other libs exist" $ do + let pn = mkPackageName "pkg" + libKey = ComponentKey pn CLib + subKey = ComponentKey pn (CSubLib (unqualCompFromString "sub")) + taskKeys = Set.fromList [libKey, subKey] + intraPackageDeps taskKeys libKey `shouldBe` Set.empty + + describe "componentTarget" $ do + it "CLib renders as lib:" $ do + componentTarget (mkPackageName "my-pkg") CLib + `shouldBe` "lib:my-pkg" + + it "CSubLib renders as lib:" $ do + componentTarget (mkPackageName "pkg") (CSubLib (unqualCompFromString "internal")) + `shouldBe` "lib:internal" + + it "CFlib renders as flib:" $ do + componentTarget (mkPackageName "pkg") (CFlib (unqualCompFromString "cbits")) + `shouldBe` "flib:cbits" + + it "CExe renders as exe:" $ do + componentTarget (mkPackageName "pkg") (CExe (unqualCompFromString "my-exe")) + `shouldBe` "exe:my-exe" + + it "CTest renders as test:" $ do + componentTarget (mkPackageName "pkg") (CTest (unqualCompFromString "my-test")) + `shouldBe` "test:my-test" + + it "CBench renders as bench:" $ do + componentTarget (mkPackageName "pkg") (CBench (unqualCompFromString "my-bench")) + `shouldBe` "bench:my-bench" + + it "package name is only used for CLib" $ do + -- For non-CLib components, the package name is ignored + let pn1 = mkPackageName "pkg-a" + pn2 = mkPackageName "pkg-b" + comp = CExe (unqualCompFromString "exe") + componentTarget pn1 comp `shouldBe` componentTarget pn2 comp + + describe "componentEnableTests" $ do + let pn = mkPackageName "pkg" + testName = unqualCompFromString "my-test" + benchName = unqualCompFromString "my-bench" + mixedComps = Set.fromList + [CLib, CTest testName, CBench benchName] + + it "CTest key enables tests regardless of component set" $ do + componentEnableTests (ComponentKey pn (CTest testName)) Set.empty + `shouldBe` True + componentEnableTests (ComponentKey pn (CTest testName)) mixedComps + `shouldBe` True + + it "CBench key does not enable tests" $ do + componentEnableTests (ComponentKey pn (CBench benchName)) mixedComps + `shouldBe` False + + it "CExe key does not enable tests" $ do + componentEnableTests + (ComponentKey pn (CExe (unqualCompFromString "exe"))) + mixedComps + `shouldBe` False + + it "CSubLib key does not enable tests" $ do + componentEnableTests + (ComponentKey pn (CSubLib (unqualCompFromString "sub"))) + mixedComps + `shouldBe` False + + it "CLib key enables tests when component set has tests" $ do + componentEnableTests (ComponentKey pn CLib) mixedComps + `shouldBe` True + + it "CLib key does not enable tests when no tests in component set" $ do + componentEnableTests (ComponentKey pn CLib) (Set.singleton CLib) + `shouldBe` False + + it "CLib key does not enable tests with only benchmarks" $ do + componentEnableTests + (ComponentKey pn CLib) + (Set.singleton (CBench benchName)) + `shouldBe` False + + describe "componentEnableBenchmarks" $ do + let pn = mkPackageName "pkg" + testName = unqualCompFromString "my-test" + benchName = unqualCompFromString "my-bench" + mixedComps = Set.fromList + [CLib, CTest testName, CBench benchName] + + it "CBench key always enables benchmarks" $ do + componentEnableBenchmarks (ComponentKey pn (CBench benchName)) Set.empty + `shouldBe` True + + it "CTest key does not enable benchmarks" $ do + componentEnableBenchmarks (ComponentKey pn (CTest testName)) mixedComps + `shouldBe` False + + it "CExe key does not enable benchmarks" $ do + componentEnableBenchmarks + (ComponentKey pn (CExe (unqualCompFromString "exe"))) + mixedComps + `shouldBe` False + + it "CLib key enables benchmarks when component set has benchmarks" $ do + componentEnableBenchmarks (ComponentKey pn CLib) mixedComps + `shouldBe` True + + it "CLib key does not enable benchmarks when no benchmarks" $ do + componentEnableBenchmarks (ComponentKey pn CLib) (Set.singleton CLib) + `shouldBe` False + + it "CLib key does not enable benchmarks with only tests" $ do + componentEnableBenchmarks + (ComponentKey pn CLib) + (Set.singleton (CTest testName)) + `shouldBe` False + + describe "finalTestsAndBenches" $ do + let pn = mkPackageName "pkg" + testA = unqualCompFromString "test-a" + testB = unqualCompFromString "test-b" + benchA = unqualCompFromString "bench-a" + + it "CTest key returns exactly that one test, no benches" $ do + let (tests, benches) = + finalTestsAndBenches + (ComponentKey pn (CTest testA)) + (Set.fromList [CTest testA, CTest testB, CBench benchA]) + tests `shouldBe` Set.singleton testA + benches `shouldBe` Set.empty + + it "CBench key returns exactly that one bench, no tests" $ do + let (tests, benches) = + finalTestsAndBenches + (ComponentKey pn (CBench benchA)) + (Set.fromList [CTest testA, CBench benchA]) + tests `shouldBe` Set.empty + benches `shouldBe` Set.singleton benchA + + it "CLib key returns all tests and benches from component set" $ do + let comps = Set.fromList [CTest testA, CTest testB, CBench benchA] + (tests, benches) = + finalTestsAndBenches (ComponentKey pn CLib) comps + tests `shouldBe` Set.fromList [testA, testB] + benches `shouldBe` Set.singleton benchA + + it "CLib key with empty set returns empty tests and benches" $ do + let (tests, benches) = + finalTestsAndBenches (ComponentKey pn CLib) Set.empty + tests `shouldBe` Set.empty + benches `shouldBe` Set.empty + + it "CLib key with only CLib returns empty tests and benches" $ do + let (tests, benches) = + finalTestsAndBenches (ComponentKey pn CLib) (Set.singleton CLib) + tests `shouldBe` Set.empty + benches `shouldBe` Set.empty + + it "CExe key falls through to all tests/benches" $ do + let comps = Set.fromList [CTest testA, CBench benchA] + (tests, benches) = + finalTestsAndBenches + (ComponentKey pn (CExe (unqualCompFromString "exe"))) + comps + tests `shouldBe` Set.singleton testA + benches `shouldBe` Set.singleton benchA + + it "CTest key ignores component set entirely" $ do + -- Even though comp set has multiple tests, only the CTest key's test + -- is returned + let (tests, _) = + finalTestsAndBenches + (ComponentKey pn (CTest testA)) + (Set.fromList [CTest testA, CTest testB]) + Set.size tests `shouldBe` 1 + tests `shouldBe` Set.singleton testA diff --git a/tests/unit/Stack/PackageDumpSpec.hs b/tests/unit/Stack/PackageDumpSpec.hs index 80b5c1c386..024e9e6fd1 100644 --- a/tests/unit/Stack/PackageDumpSpec.hs +++ b/tests/unit/Stack/PackageDumpSpec.hs @@ -29,9 +29,11 @@ import Stack.PackageDump ) import Stack.Prelude import Stack.Types.CompilerPaths ( GhcPkgExe (..) ) -import Stack.Types.GhcPkgId ( parseGhcPkgId ) +import Stack.Types.GhcPkgId ( GhcPkgId, parseGhcPkgId ) import Test.Hspec - ( Spec, describe, hspec, it, shouldBe ) + ( Spec, anyException, describe, hspec, it, shouldBe + , shouldThrow + ) import Test.Hspec.QuickCheck ( prop ) main :: IO () @@ -222,6 +224,22 @@ spec = do } + describe "parseGhcPkgId" $ do + it "parses traditional package ID" $ do + gpkgId <- parseGhcPkgId "base-4.7.0.2-bfd89587617e381ae01b8dd7b6c7f1c1" + show gpkgId `shouldBe` show ("base-4.7.0.2-bfd89587617e381ae01b8dd7b6c7f1c1" :: String) + + it "parses Backpack instantiated unit ID with '+'" $ do + gpkgId <- parseGhcPkgId "private-backpack-0.1.0.0-str-sig+8GTPQYg43xPFNOuG93VVSN" + show gpkgId `shouldBe` show ("private-backpack-0.1.0.0-str-sig+8GTPQYg43xPFNOuG93VVSN" :: String) + + it "parses unit ID with only '+'" $ do + gpkgId <- parseGhcPkgId "pkg+hash" + show gpkgId `shouldBe` show ("pkg+hash" :: String) + + it "rejects empty string" $ do + (parseGhcPkgId "" :: IO GhcPkgId) `shouldThrow` anyException + it "sinkMatching" $ runEnvNoLogging $ \pkgexe -> do m <- ghcPkgDump pkgexe [] $ conduitDumpPackage diff --git a/tests/unit/Stack/Types/PlanSpec.hs b/tests/unit/Stack/Types/PlanSpec.hs new file mode 100644 index 0000000000..6e9fd99acb --- /dev/null +++ b/tests/unit/Stack/Types/PlanSpec.hs @@ -0,0 +1,78 @@ +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} + +module Stack.Types.PlanSpec + ( main + , spec + ) where + +import qualified Data.Map.Strict as Map +import qualified Data.Set as Set +import Distribution.Types.PackageName ( mkPackageName ) +import Stack.Prelude +import Stack.Types.ComponentUtils ( unqualCompFromString ) +import Stack.Types.NamedComponent ( NamedComponent (..) ) +import Stack.Types.Plan + ( ComponentKey (..), componentKeyPkgName ) +import Test.Hspec ( Spec, describe, hspec, it, shouldBe ) + +main :: IO () +main = hspec spec + +spec :: Spec +spec = do + describe "ComponentKey" $ do + it "componentKeyPkgName extracts the package name" $ do + let pn = mkPackageName "my-pkg" + ck = ComponentKey pn CLib + componentKeyPkgName ck `shouldBe` pn + + it "componentKeyPkgName works for non-CLib components" $ do + let pn = mkPackageName "my-pkg" + cn = unqualCompFromString "test-suite" + ck = ComponentKey pn (CTest cn) + componentKeyPkgName ck `shouldBe` pn + + it "two ComponentKeys with different components are not equal" $ do + let pn = mkPackageName "my-pkg" + ckLib = ComponentKey pn CLib + ckExe = ComponentKey pn (CExe (unqualCompFromString "my-exe")) + (ckLib == ckExe) `shouldBe` False + + it "two ComponentKeys with different packages are not equal" $ do + let ck1 = ComponentKey (mkPackageName "pkg-a") CLib + ck2 = ComponentKey (mkPackageName "pkg-b") CLib + (ck1 == ck2) `shouldBe` False + + it "two identical ComponentKeys are equal" $ do + let ck1 = ComponentKey (mkPackageName "pkg") CLib + ck2 = ComponentKey (mkPackageName "pkg") CLib + (ck1 == ck2) `shouldBe` True + + it "ComponentKey has a well-defined Ord instance" $ do + let pn = mkPackageName "pkg" + ckLib = ComponentKey pn CLib + ckExe = ComponentKey pn (CExe (unqualCompFromString "exe")) + ckTest = ComponentKey pn (CTest (unqualCompFromString "test")) + -- CLib, CSubLib, CFlib, CExe, CTest, CBench ordering comes from + -- NamedComponent's derived Ord + (ckLib < ckExe) `shouldBe` True + (ckExe < ckTest) `shouldBe` True + + it "can be used as Map keys" $ do + let ck1 = ComponentKey (mkPackageName "pkg-a") CLib + ck2 = ComponentKey (mkPackageName "pkg-a") + (CTest (unqualCompFromString "tests")) + ck3 = ComponentKey (mkPackageName "pkg-b") CLib + m :: Map ComponentKey String + m = Map.fromList [(ck1, "lib"), (ck2, "test"), (ck3, "lib-b")] + Map.size m `shouldBe` 3 + Map.lookup ck1 m `shouldBe` Just "lib" + Map.lookup ck2 m `shouldBe` Just "test" + + it "can be used in Sets" $ do + let ck1 = ComponentKey (mkPackageName "pkg") CLib + ck2 = ComponentKey (mkPackageName "pkg") + (CExe (unqualCompFromString "exe")) + s = Set.fromList [ck1, ck2, ck1] -- duplicate ck1 + Set.size s `shouldBe` 2