Skip to content

Commit e995918

Browse files
committed
Update snapshot to 'nightly'
Instead of pinning the snapshot to a specific date, we set it to 'nightly', which should allow more flexibility i.e. users can build whichever ghc corresponds to the latest snapshot. This should hopefully decrease the chance of users running into issues with ghc minor version upgrades e.g. bounds errors. Snapshot updates that upgrade the ghc major version will almost certainly require manual intervention here, but failures will likely be caught by the CI job that builds everything with --dry-run, so there should be some warning.
1 parent bba2580 commit e995918

33 files changed

Lines changed: 585 additions & 137 deletions

.github/workflows/ci.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ jobs:
2929
- uses: actions/checkout@v4
3030
- uses: haskell-actions/setup@v2
3131
with:
32-
ghc-version: "9.10.3"
32+
# Should be the current stackage nightly, though this will likely go
33+
# out-of-date eventually, until a problem is reported.
34+
ghc-version: "9.12"
3335
- name: Configure
3436
run: |
3537
cabal configure --enable-tests --ghc-options -Werror

README.md

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ An impact assessment is due when
1414

1515
The procedure is as follows:
1616

17-
1. Rebase changes, mandated by your proposal, atop of `ghc-9.10` branch.
17+
1. Rebase changes, mandated by your proposal, atop the ghc branch (or tag) that corresponds to the current [stackage nightly](https://www.stackage.org/nightly). For example, if the latest snapshot ghc is `ghc-9.12.3`, we would want to rebase our changes on the `ghc-9.12.3-release` tag.
1818

1919
2. Compile a patched GHC, say, `~/ghc/_build/stage1/bin/ghc`.
2020

@@ -36,6 +36,7 @@ The procedure is as follows:
3636
* You can interrupt `cabal` at any time and rerun again later.
3737
* Consider setting `--jobs` to retain free CPU cores for other tasks.
3838
* Full build requires roughly 7 Gb of free disk space.
39+
* If the build fails with an error about max amount of arguments in `gcc`, run again, but with smaller batch size. 250 worked well for me.
3940
4041
To get an idea of the current progress, we can run the following commands
4142
on the log file:
@@ -59,6 +60,61 @@ The procedure is as follows:
5960
6061
8. When everything finally builds, get back to CLC with a list of packages affected and patches required.
6162
63+
### Troubleshooting
64+
65+
Because we build with `nightly` and are at the mercy of cabal's constraint solver, it is possible to run into solver / build issues that have nothing to do with our custom GHC. Some of the most common problems include:
66+
67+
- Nightly adds a new, problematic package `p` e.g.
68+
69+
- `p` requires a new system dependency (e.g. a C library).
70+
- `p` is an executable.
71+
- `p` depends on a package in [./excluded_pkgs.jsonc](excluded_pkgs.jsonc).
72+
73+
- A cabal flag is set in a way that breaks the build. For example, our snapshot requires that the `bson` library does *not* have its `_old-network` flag set, as this will cause a build error with our version of `network`. This flag is automatic, so we have to force it in `generated/cabal.project` with `constraints: bson -_old-network`.
74+
75+
- Nightly has many packages drop out for some reason, increasing the chance for solver non-determinism.
76+
77+
We attempt to mitigate such issues by:
78+
79+
- Writing most of the snapshot's exact package versions as cabal constraints to the generated `./generated/cabal.project.local`, which ensures we (transitively) build the same package version every time. Note that boot packages like `text` are deliberately excluded so that we can build a snapshot with multiple GHCs. Otherwise even a GHC minor version difference would fail because `ghc` is in the build plan.
80+
81+
- Ignoring bounds in `generated/cabal.project`:
82+
83+
```
84+
allow-newer: *:*
85+
allow-older: *:*
86+
```
87+
88+
Nevertheless, it is still possible for issues to slip through. When a package `p` fails to build for some reason, we should first:
89+
90+
- Verify that `p` is not in `excluded_pkgs.jsonc`. If it is, nightly probably pulled in some new reverse-dependency `q` that should be added to `excluded_pkgs.jsonc`.
91+
92+
- Verify that `p` does not have cabal flags that can affect dependencies / API.
93+
94+
- Verify that `p`'s version matches what it is in the current snapshot (e.g. `https://www.stackage.org/nightly`). If it does not, either a package needs to be excluded or constraints need to be added.
95+
96+
In general, user mitigations for solver / build problems include:
97+
98+
- Adding `p` to `excluded_pkgs.jsonc`. Note that `p` will still be built if it is a (transitive) dependency of some other package in the snapshot, but will not have its exact bounds written to `cabal.project.local`.
99+
100+
- Manually downloading a snapshot (e.g. `https://www.stackage.org/nightly/cabal.config`), changing / removing the offending package(s), and supplying the file with the `--snapshot-path` param. Like `excluded_pkgs.jsonc`, take care that the problematic package is not a (transitive) dependency of something in the snapshot.
101+
102+
- Adding constraints to `generated/cabal.project` e.g. flags or version constraints like `constraints: filepath > 1.5`.
103+
104+
#### Misc
105+
106+
- Note that while a GHC minor version difference is usually okay, a GHC *major* difference will very likely lead to errors.
107+
108+
- The `flake.nix` line:
109+
110+
```nix
111+
compiler = pkgs.haskell.packages.ghc<vers>;
112+
```
113+
114+
can be a useful guide as to which GHC was last tested, as CI uses this ghc to build everything with `--dry-run`, which should report solver errors (e.g. bounds) at the very least.
115+
116+
- If you encounter an error that you think indicates a problem with the configuration here (e.g. new package needs to be excluded, new constraint added), please open an issue. While that is being resolved, the mitigations from the [previous section](#troubleshooting) may be useful.
117+
62118
### The clc-stackage exe
63119
64120
`clc-stackage` is an executable that will:
@@ -94,13 +150,21 @@ By default (`--write-logs save-failures`), the build logs are saved to the `./ou
94150

95151
#### Group batching
96152

97-
The `clc-stackage` exe allows for splitting the entire package set into subset groups of size `N` with the `--batch N` option. Each group is then built sequentially. Not only can this be useful for situations where building the entire package set in one go is infeasible, but it also provides a "cache" functionality, that allows us to interrupt the program at any point (e.g. `CTRL-C`), and pick up where we left off. For example:
153+
By default, the `clc-stackage` exe tries to build all packages at once i.e. every package is written to `generated/generated.cabal`. This can cause problems e.g. we do not have enough memory to build everything simultaneously, or we receive an error that `gcc` has been given too many arguments. Hence we provide the `--batch N` option, which will split the package set into disjoint groups of size `N`. Each group is then built sequentially.
98154

99-
```sh
100-
$ clc-stackage --batch 100
101-
```
155+
The default behavior is:
156+
157+
1. `clc-stackage` will try to build everything in the same group, even if some package fails (equivalent to cabal's `--keep-going` flag.). If instead `--package-fail-fast` is enabled, the first failure will cause the entire group to immediately fail, and we will move onto the next group.
102158

103-
This will split the entire downloaded package set into groups of size 100. Each time a group finishes (success or failure), stdout/err will be updated, and then the next group will start. If the group failed to build and we have `--write-logs save-failures` (the default), then the logs and error output will be in `./output/logs/<pkg>/`, where `<pkg>` is the name of the first package in the group.
159+
2. `clc-stackage` will try every group, even if some prior group fails. The `--group-fail-fast` option changes this so that the first failure will cause `clc-stackage` to exit.
160+
161+
Each time a group finishes (success or failure), stdout/err will be updated, and then the next group will start. If the group failed to build and we have `--write-logs save-failures` (the default), then the logs and error output will be in `./output/logs/<pkg>/`, where `<pkg>` is the name of the first package in the group.
162+
163+
When `clc-stackage` itself finishes (either on its own or via an interrupt like `CTRL-C`), the results are saved to a cache which records all successes, failures, and untested packages. This allows us to pick up where we left off with untested packages (including failures if the `--retry-failures` flag is active).
164+
165+
> [!IMPORTANT]
166+
>
167+
> The cache operates at the *batch group* level, so only packages that have been part of a successful group will be considered successes. Conversely, a package will be considered a failure if it is part of a failing group, even if it was built successfully. Therefore, to see what packages actually failed, we will want to check the logs directory. Alternatively, we can first run `clc-stackage` initially with a large `--batch` group (for maximum performance), then run it again with, say, `--batch 1`.
104168
105169
See `clc-stackage --help` for more info.
106170

cabal.project

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
index-state: 2026-03-19T21:41:35Z
2-
31
packages: .
42

53
program-options

dev.md

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ This project is organized into several libraries and a single executable. Roughl
88
2. Prune `s` based on packages we know we do not want (e.g. system deps).
99
3. Generate a custom `generated.cabal` file for the given package set, and try to build it.
1010

11-
Futhermore, we allow for building subsets of the entire stackage package set with the `--batch` feature. This will split the package set into disjoint groups, and build each group sequentially. The process can be interrupted at any time (e.g. `CTRL-C`), and progress will be saved in a "cache" (json file), so we can pick up where we left off.
11+
Furthermore, we allow for building subsets of the entire stackage package set with the `--batch` feature. This will split the package set into disjoint groups, and build each group sequentially. The process can be interrupted at any time (e.g. `CTRL-C`), and progress will be saved in a "cache" (json file), so we can pick up where we left off.
1212

1313
## Components
1414

1515
The `clc-stackage` library is namespaced by functionality:
1616

1717
### utils
1818

19-
`CLC.Stackage.Utils` ontains common utilities e.g. logging and hardcoded file paths.
19+
`CLC.Stackage.Utils` contains common utilities e.g. logging and hardcoded file paths.
2020

2121
### parser
2222

@@ -73,30 +73,25 @@ The reason this logic is a library function and not the executable itself is for
7373

7474
The executable that actually runs. This is a very thin wrapper over `runner`, which merely sets up the logging handler.
7575

76-
## Updating to a new shapshot
76+
## Updating to a new snapshot
7777

78-
1. Update to the desired snapshot:
78+
`clc-stackage` is based on `nightly` -- which changes automatically -- meaning we do not necessarily have to do anything when a new (minor) snapshot is released. On the other hand, *major* snapshot updates will almost certainly bring in new packages that need to be excluded, so there are some general "update steps" we will want to take:
7979

80-
```haskell
81-
-- CLC.Stackage.Parser.API
82-
stackageSnapshot :: String
83-
stackageSnapshot = "nightly-yyyy-mm-dd"
84-
```
80+
1. Modify [excluded_pkgs.json](excluded_pkgs.json) as needed. That is, updating the snapshot major version will probably bring in some new packages that we do not want. The update process is essentially trial-and-error i.e. run `clc-stackage` as normal, and later add any failing packages that should be excluded.
8581

86-
2. Update the `index-state` in [cabal.project](cabal.project) and [generated/cabal.project](generated/cabal.project).
82+
2. Update `ghc-version` in [.github/workflows/ci.yaml](.github/workflows/ci.yaml).
8783

88-
3. Modify [excluded_pkgs.json](excluded_pkgs.json) as needed. That is, updating the snapshot will probably bring in some new packages that we do not want. The update process is essentially trial-and-error i.e. run `clc-stackage` as normal, and later add any failing packages that should be excluded.
84+
3. Update functional tests as needed i.e. exact package versions in `*golden` and `test/functional/snapshot.txt`.
8985

90-
4. Update references to the current ghc e.g.
86+
4. Optional: Update nix:
9187

92-
1. `ghc-version` in [.github/workflows/ci.yaml](.github/workflows/ci.yaml).
93-
2. [README.md](README.md).
88+
- Inputs (`nix flake update`).
89+
- GHC: Update the `compiler = pkgs.haskell.packages.ghc<vers>;` line.
90+
- Add to the `flake.nix`'s `ldDeps` and `deps` as needed to have the `nix` CI job pass. System libs available on nix can be found here: https://search.nixos.org/packages?channel=unstable.
9491

95-
5. Update functional tests as needed i.e. exact package versions in `*golden` and `test/functional/snapshot.txt`.
92+
This job builds everything with `--dry-run`, so its success is a useful proxy for `clc-stackage`'s health. In other words, if the nix job fails, there is almost certainly a general issue (i.e. either a package should be excluded or new system dep is required), but if it succeeds, the package set is in pretty good shape (there may still be sporadic issues e.g. a package does not properly declare its system dependencies at config time).
9693

97-
6. Optional: Update `clc-stackage.cabal`'s dependencies (i.e. `cabal outdated`).
98-
99-
7. Optional: Update nix inputs (`nix flake update`).
94+
5. Optional: Update `clc-stackage.cabal`'s dependencies (i.e. `cabal outdated`).
10095

10196
### Verifying snapshot
10297

excluded_pkgs.jsonc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@
270270
"midi-music-box",
271271
"misfortune",
272272
"mmark-cli",
273+
"mmark-ext", // ghc-syntax-highlighter
273274
"moffy-samples-gtk3",
274275
"moffy-samples-gtk3-run",
275276
"moffy-samples-gtk4",
@@ -359,6 +360,7 @@
359360
"sqlcli",
360361
"sqlcli-odbc",
361362
"sqlite-simple",
363+
"stakhanov", // hasql
362364
"stack-all",
363365
"stack-clean-old",
364366
"stack-templatizer",

generated/cabal.project

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1-
index-state: 2026-03-19T21:41:35Z
2-
31
-- should be an absolute path to your custom compiler
42
--with-compiler: /home/ghc/_build/stage1/bin/ghc
53

64
packages: .
75

86
optimization: False
97

10-
allow-newer:
11-
hoogle:crypton-connection,
12-
web-rep:transformers,
8+
-- The generated cabal.project.local has exact constraints for every (non-boot)
9+
-- package in the snapshot to ensure we build the same packages every time.
10+
-- We still want this to override individual package constraints e.g.
11+
-- web-rep has had an overly tight bound that excludes the transformers from
12+
-- the snapshot. The allow-newer ignores the error.
13+
allow-newer: *:*
14+
allow-older: *:*
1315

16+
constraints: bson -_old-network
1417
constraints: hlint +ghc-lib
1518
constraints: ghc-lib-parser-ex -auto
19+
constraints: path +os-string
1620
constraints: stylish-haskell +ghc-lib

src/CLC/Stackage/Builder.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module CLC.Stackage.Builder
55

66
-- * Misc
77
Batch.batchPackages,
8+
Process.cabalUpdate,
89
)
910
where
1011

src/CLC/Stackage/Builder/Process.hs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
module CLC.Stackage.Builder.Process
44
( buildProject,
5+
cabalUpdate,
56
)
67
where
78

@@ -122,6 +123,26 @@ buildProject env idx pkgs = do
122123

123124
addPackages = Set.union pkgsSet
124125

126+
-- | Runs "cabal update".
127+
cabalUpdate :: BuildEnv -> IO ()
128+
cabalUpdate env = do
129+
Logging.putTimeInfoStr env.hLogger "Running 'cabal update'"
130+
P.readProcessWithExitCode env.cabalPath ["update"] "" >>= \(ec, out, err) ->
131+
case ec of
132+
ExitSuccess -> pure ()
133+
ExitFailure _ -> do
134+
let msg =
135+
mconcat
136+
[ "Failed running 'cabal update': ",
137+
"Out: '",
138+
T.pack out,
139+
"', Err: '",
140+
T.pack err,
141+
"'"
142+
]
143+
Logging.putTimeErrStr env.hLogger msg
144+
throwIO ec
145+
125146
createCurrentLogsDir :: IO (OsPath, OsPath, OsPath)
126147
createCurrentLogsDir = do
127148
let dirPath = Paths.logsDir </> [osp|current-build|]

0 commit comments

Comments
 (0)