Add PackageMeta_ autogenerated module for build-time metadata
- Dominant language
- Haskell
- Stars
- 1.7k
- Forks
- 750
- Avg merge
- 4d 3h
- Merged PRs (30d)
- 28
Description
## Summary
Add a new autogenerated module `PackageMeta_` that exposes build-environment metadata known at configure time: compiler identity, platform, cabal flag values, and (optionally) VCS revision. This complements the existing `PackageInfo_` (static `.cabal` metadata) and `Paths_` (install paths) modules, filling a long-standing gap in Cabal's build-time information story.
## Motivation
Haskell packages frequently need access to build-environment metadata at runtime -- particularly the git revision for `--version` output. Today, the ecosystem relies on a patchwork of fragile workarounds:
**Template Haskell** (`githash`, `gitrev`, `cardano-git-rev`): The most common approach. TH splices call `git rev-parse HEAD` at compile time and use `addDependentFile` to track `.git/HEAD` for recompilation. This works at the GHC level, but `cabal-install`'s outer "Up to date" check does not read `.hi` files for `addDependentFile` dependencies (#4746), so the embedded revision goes stale after commits that don't change source files. Additionally, TH is unavailable on some platforms (unregisterised GHC builds for s390x, ppc64le, WASM) and breaks with `cabal install` which copies sources without `.git/` (#7355).
**Custom Setup.hs**: A `buildHook` can generate a module with the git hash. This requires `build-type: Custom`, which is on the deprecation path (HF RFC 060), causes cross-compilation issues (#1493), degrades HLS support, and adds setup-dependency solving overhead. Furthermore, cabal's "Up to date" check can still skip invoking the build hook entirely.
**`build-type: Configure`**: A `./configure` script writes a `.buildinfo` file with `-D__GIT_REV__`. This is the worst option: `configure` only re-runs when the configuration itself changes, not when source files change or the git HEAD moves. The revision is stale in almost all incremental build scenarios. Also requires MSYS2 on Windows.
**CLI `--ghc-options`**: Passing `-D__GIT_REV__="$(git rev-parse HEAD)"` on the command line. Cabal correctly tracks this as a configuration change and triggers recompilation -- but it recompiles *every module in the component*, not just the one using the define. Scoping via a sublibrary's `cpp-options` field in the `.cabal` file achieves minimal recompilation, but requires an external script to rewrite the `.cabal` file before each build.
**`-pgmF` custom preprocessor**: GHC can run an external preprocessor per-module via `{-# OPTIONS_GHC -F -pgmF ./script.sh #-}`. The preprocessor can call `git`, but GHC fingerprints the *original* source file for recompilation, not the preprocessor output. The revision goes stale even when GHC runs.
All of these approaches share a fundamental problem: the information lives *below* cabal-install, so cabal's own change detection can short-circuit before the mechanism ever fires. The natural solution is for cabal itself to provide this metadata, just as it already provides `PackageInfo_` and `Paths_`.
### Prior art and existing requests
This need has been expressed repeatedly over 18 years:
- #392 (2008): Duncan Coutts requested "revision control context (git hash, darcs context)" in a generated module alongside OS, arch, compiler, and flag values. Still open.
- #826 (2011): Request for cabal flag values in an autogenerated module. Still open.
- #1506 (2013): Request to include VCS revision number in `Paths_`. Still open.
- #7355 (2021): `gitrev`/`githash` TH packages break with `cabal install`. Still open.
- #7488 (2021): Proposes distinguishing "early generated" files (packaging-time, e.g. git hash) vs "late generated" files (compile-time, e.g. `Paths_`). Still open.
- #11258 (2025): Modern re-request for flag values in a generated module. Cabal maintainer @ulysses4ever commented: "the fact that Cabal twists user's elbow into using CPP where a mere Bool could do is horrible." Still open.
- #4746 (2017): `addDependentFile` not tracked by `v2-build`. Blocked on GHC upstream. Still open.
Cabal already generates three autogenerated artifacts per component: `Paths_`, `PackageInfo_`, and `cabal_macros.h`. Adding `PackageMeta_` follows the established pattern.
## Design
### Taxonomy of autogenerated information
| Module | Changes when... | Content |
|--------|----------------|---------|
| `PackageInfo_` | `.cabal` file edited | name, version, synopsis, copyright, homepage |
| **`PackageMeta_`** (new) | **Build environment or VCS state changes** | **compiler, os, arch, flags, gitRevision** |
| `Paths_` | Package installed to different prefix | bindir, libdir, datadir, ... |
`PackageInfo_` is static metadata from the `.cabal` file.
`PackageMeta_` is dynamic metadata from the build environment.
`Paths_` is deployment metadata from the install location.
This separation ensures that importing `PackageInfo_` never causes spurious recompilation, and importing `PackageMeta_` only recompiles when build-environment facts actually change.
### Generated module contents
```haskell
{-# LANGUAGE NoRebindableSyntax #-}
module PackageMeta_mypackage
( compiler
, compilerVersion
, os
, arch
, gitRevision
, gitDirty
-- one Bool per cabal flag:
, flagDebug
, flagBenchmarks
) where
import Data.Version (Version (..))
import Prelude
compiler :: String
compiler = "ghc"
compilerVersion :: Version
compilerVersion = Version [9,6,7] []
os :: String
os = "darwin"
arch :: String
arch = "aarch64"
gitRevision :: String
gitRevision = "abc123def456..."
gitDirty :: Bool
gitDirty = False
-- Cabal flag values:
flagDebug :: Bool
flagDebug = True
flagBenchmarks :: Bool
flagBenchmarks = False
```
### Where the data comes from
All of this information is already available to cabal at configure time:
- **compiler, compilerVersion**: `compilerId` from `LocalBuildInfo.compiler`
- **os, arch**: `hostPlatform` from `LocalBuildInfo`
- **flag values**: `flagAssignment` from `LocalBuildInfo`
- **gitRevision, gitDirty**: Requires running `git rev-parse HEAD` and `git diff --quiet` during configure. Cabal already runs external programs during configure (e.g. `pkg-config`). Graceful fallback to `""` / `False` when git is unavailable or `.git/` doesn't exist (Nix builds, sdist installs, `cabal install` from Hackage).
### Recompilation behavior
The module is generated by `writeBuiltinAutogenFiles` using `rewriteFileEx`, which only writes when content changes. This means:
- If the git revision hasn't changed: file content is identical, no write, no recompilation.
- If the git revision changed: file content differs, `rewriteFileEx` writes, cabal detects the source change, GHC recompiles `PackageMeta_` and its transitive dependents. Other modules in the package are unaffected.
This gives **correct, minimal recompilation** by construction -- the same behavior we get from any autogenerated source file.
The remaining question is when cabal re-evaluates the git state. The configure phase already re-runs when configuration changes. For VCS info specifically, cabal would need to check `.git/HEAD` freshness as part of its file monitoring, or re-run the VCS query on every build (cheap -- `git rev-parse HEAD` takes ~2ms).
### `cabal-version` gating
Following the lesson from `PackageInfo_` (#9331), `PackageMeta_` should be gated behind a `cabal-version` minimum (e.g., `>= 3.16` or whatever the next release is).
### Implementation approach
Following PR #8534 (which added `PackageInfo_`):
1. Add `Distribution.Simple.Build.PackageMetaModule` with `generatePackageMetaModule`
2. Add `Distribution.Simple.Build.PackageMetaModule.Z` with the Zinza template renderer
3. Add `autogenPackageMetaModuleName` to `Distribution.Simple.BuildPaths`
4. Wire into `builtinAutogenFiles` in `Distribution.Simple.Build`
5. Add `cabal check` warnings in `Distribution.PackageDescription.Check`
6. Add tests following the `PackageTests/PackageInfoModule/` pattern
7. Gate behind appropriate `cabal-version`
A prototype implementation (validated end-to-end: renderer produces valid Haskell, generated module compiles and runs correctly) is available and will be submitted as a PR.
## Open questions
1. **Should VCS info be optional?** A cabal flag or `.cabal` field (e.g., `package-meta-vcs: True`) could control whether `git` is invoked. Packages that only want compiler/flags metadata shouldn't need git in their build environment.
2. **What VCS systems to support?** Starting with git-only is pragmatic (covers >99% of Haskell packages). The field names (`gitRevision`, `gitDirty`) are git-specific. An alternative is generic `vcsRevision :: String` with VCS-agnostic semantics.
3. **Should flag values be per-component?** Cabal flags are package-level, but their effects can differ per component. The generated module could live in the per-component autogen directory (like `Paths_` does) while exposing the same flag values for all components.
4. **Interaction with `cabal install` and Hackage**: When installing from Hackage (no `.git/`), `gitRevision` would be `""`. This is correct behavior -- the same graceful fallback that `githash` uses today. Optionally, `cabal sdist` could snapshot the VCS revision into the sdist metadata (related to #7488).
Contributor guide
Assessment
This issue has not been assessed yet.