Add Backpack support to Stack#6865
Add Backpack support to Stack#6865philippedev101 wants to merge 1 commit intocommercialhaskell:masterfrom
Conversation
Implement full support for GHC's Backpack module system, addressing the long-standing request in issue commercialhaskell#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.
|
@philippedev101, thanks! There is a lot to read but, in advance of that, in implementing component-based builds, did you encounter, and overcome, the performance issue that was blocking @theobat making progress at: |
|
@mpilgrem Thanks for the pointer to #6356 and @theobat's work. After going through the discussion there and the branches on their fork in some detail: short answer, this PR takes a different architectural approach that sidesteps the performance problem, at the cost of not solving the broader set of issues that full per-component builds would address. How the two approaches differ@theobat's approach was to make Stack build each component of a package as a separate The approach in this PR is narrower. The build plan is keyed per-component using a What this does not solveFull per-component execution would address several long-standing issues that this approach leaves untouched: Unnecessary recompilation when switching between Rebuilding components that aren't dependencies of the target (#6569). If you ask for Parallel builds within a package (#4391). A package with five independent executables currently builds them sequentially (Cabal handles the ordering internally). Per-component execution would let Stack schedule them in parallel, the same way it parallelizes across packages. Component-level cycle resolution (#2583). When package A's library depends on package B's library, and B's test suite depends on A's library, that looks like a cycle at the package level. At the component level it's not: build A:lib, then B:lib, then both test suites. Per-component execution could handle this. Where this PR sitsThere are roughly three points on the spectrum:
This PR lands at point 2. It gets cross-package Backpack working today without regressing anything for existing users. Transitioning to full per-component builds laterThe
None of these changes require rethinking the plan-level architecture. They're about threading component information through the execution layer. And they're independent of the subprocess performance question, which could be tackled separately (the in-process |
Addresses #2540.
This PR adds support for GHC's Backpack module system, the feature request that has been open since August 2016. After this change, Stack can build projects that use
signatures,mixins, and cross-package mixin linking, which previously only worked undercabal-install.Background
Backpack lets you write a library against an abstract interface (a signature) and have the consumer decide which concrete implementation to plug in. The compiler recompiles the library for each implementation, so there's no runtime overhead. When the signature and the implementation live in the same package (using sub-libraries), this is "private Backpack" and has always worked in Stack. The hard part is cross-package Backpack, where the signature lives in one package and the implementation in another. This requires the build tool to create extra instantiation build steps, which Stack didn't do before.
What changed
The work breaks down into three layers:
Per-component build plan. The build plan is now keyed by
ComponentKey(package name + component) instead of just package name. This was a prerequisite: Backpack instantiation tasks need their own entries in the plan alongside the regular library task for the same package. This also lets Stack detect intra-package sub-library dependencies (the Backpack pattern) and avoid splitting those packages into independent component tasks, which would break the build order that Cabal expects.Cross-package instantiation. When a consumer package uses
mixinsto depend on an indefinite package, Stack now scans the consumer's dependencies, resolves which modules fill which signatures, and creates CInst (component instantiation) tasks. Each CInst task runsSetup configure --instantiate-with=Sig=impl-pkg:Modulefollowed bySetup buildin its owninst-<hash>build directory. The hash is derived from the signature-to-implementation mapping, so different instantiations of the same package get different build artifacts.This handles the full range of Backpack patterns: default renaming (name identity), explicit
ModuleRenaming,HidingRenaming(propagating unfilled holes), multiple instantiations of the same indefinite package with different implementations, transitive chains where an indefinite package depends on another indefinite package, sub-library signatures and implementations, and indefinite packages from Hackage or snapshots (not just local packages). CInst tasks also get their own config cache entries, precompiled cache support, and haddock generation.Documentation and changelog. A new topic page at
doc/topics/backpack.mdintroduces what Backpack is, walks through its features (signatures, mixin linking, renaming, multiple instantiations, sub-libraries, reexported modules, Template Haskell restrictions), and then explains how Stack supports it, including what the build output looks like and what the limitations are. The page is linked frommkdocs.ymlunder Topics, and cross-referenced from the package description tutorial (mentioning thesignaturesfield) and the multi-package projects tutorial (mentioning Backpack as a use case for multi-package setups). A major changes entry has been added toChangeLog.mdunder the unreleased section.Testing
103 unit tests in
ConstructPlanSpeccover the instantiation logic: signature resolution, renaming, hiding, deduplication, transitive chains, multiple instantiations, sub-library mixins, ADRFound (installed) packages, config cache round-trips, and various warning/error paths.8 integration tests build real multi-package Backpack projects end-to-end: private Backpack, sub-library dependencies, cross-package with default renaming, explicit renaming, sub-library mixins, transitive chains, and multiple instantiations.
The full existing test suite (814 tests) continues to pass. The two pre-existing
Stack.Config/loadConfigfailures are environmental (TLS certificates) and unrelated.What's not covered
requires hidingthat actually hides signatures does not create a partial instantiation. Cabal requires a closing substitution, so the hidden signatures propagate as holes to the consumer. Template Haskell in indefinite packages doesn't work, but that's a GHC limitation that affectscabal-installequally.