Skip to content

Commit 519a184

Browse files
committed
Add real build to CI
Adds the build-batch job to CI which actually builds the entire snapshot. It does this by dividing the snapshot into batch groups per --batch, then having a different job build each group via github action's matrix strategy and new clc-stackage --batch-index param. This allows us to build the entire snapshot such that overall CI time is reasonable i.e. ~ 1 hour.
1 parent 8a57a85 commit 519a184

9 files changed

Lines changed: 224 additions & 20 deletions

File tree

.github/scripts/batch_index.sh

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
set -e
2+
3+
# Similar to dry_run.sh, except actually builds exactly one batch group.
4+
# This way CI can spread the full build across multiple jobs, keeping the
5+
# total time reasonable.
6+
batch_index=$1
7+
8+
# -f not -x since downloaded exe may not have executable permissions.
9+
if [[ -f ./bin/clc-stackage ]]; then
10+
echo "*** ./bin/clc-stackage exists, not re-installing ***"
11+
12+
# May need to add permissions, if this exe was downloaded
13+
chmod a+x ./bin/clc-stackage
14+
else
15+
echo "*** Updating cabal ***"
16+
cabal update
17+
18+
echo "*** Installing clc-stackage ***"
19+
cabal install exe:clc-stackage --installdir=./bin --overwrite-policy=always
20+
fi
21+
22+
if [[ -d output ]]; then
23+
rm -r output
24+
fi
25+
26+
echo "*** Building with --batch-index $batch_index ***"
27+
28+
set +e
29+
30+
./bin/clc-stackage \
31+
--batch 200 \
32+
--batch-index $batch_index \
33+
--cabal-options="--semaphore" \
34+
--cleanup off
35+
36+
ec=$?
37+
38+
if [[ $ec != 0 ]]; then
39+
echo "*** clc-stackage failed ***"
40+
else
41+
echo "*** clc-stackage succeeded ***"
42+
fi
43+
44+
# Print out the logs + the packages we built, in case it is useful e.g.
45+
# what did CI actually do.
46+
if [[ -f generated/generated.cabal ]]; then
47+
echo "*** Printing generated cabal file ***"
48+
cat generated/generated.cabal
49+
else
50+
echo "*** No generated/generated.cabal ***"
51+
fi
52+
53+
if [[ -f generated/cabal.project.local ]]; then
54+
echo "*** Printing generated cabal.project.local file ***"
55+
cat generated/cabal.project.local
56+
else
57+
echo "*** No generated/cabal.project.local ***"
58+
fi
59+
60+
.github/scripts/print_logs.sh
61+
62+
exit $ec

.github/scripts/dry_run.sh

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
set -e
22

3-
echo "*** Updating cabal ***"
4-
5-
cabal update
3+
if [[ -f ./bin/clc-stackage ]]; then
4+
echo "*** ./bin/clc-stackage exists, not re-installing ***"
5+
chmod a+x ./bin/clc-stackage
6+
else
7+
echo "*** Updating cabal ***"
8+
cabal update
69

7-
echo "*** Installing clc-stackage ***"
10+
# --overwrite-policy=always and deleting output/ are unnecessary for CI since
11+
# this script will only be run one time, but it's helpful when we are
12+
# testing the script locally.
813

9-
# --overwrite-policy=always and deleting output/ are unnecessary for CI since
10-
# this script will only be run one time, but it's helpful when we are
11-
# testing the script locally.
12-
cabal install exe:clc-stackage --installdir=./bin --overwrite-policy=always
14+
echo "*** Installing clc-stackage ***"
15+
cabal install exe:clc-stackage --installdir=./bin --overwrite-policy=always
16+
fi
1317

1418
if [[ -d output ]]; then
1519
rm -r output
@@ -18,7 +22,7 @@ fi
1822
echo "*** Building all with --dry-run ***"
1923

2024
set +e
21-
./bin/clc-stackage --batch 100 --cabal-options="--dry-run"
25+
./bin/clc-stackage --batch 200 --cabal-options="--dry-run"
2226

2327
ec=$?
2428

.github/workflows/ci.yaml

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
- "windows-latest"
2727
runs-on: ${{ matrix.os }}
2828
steps:
29-
- uses: actions/checkout@v4
29+
- uses: actions/checkout@v6
3030
- uses: haskell-actions/setup@v2
3131
with:
3232
# Should be the current stackage nightly, though this will likely go
@@ -57,21 +57,94 @@ jobs:
5757
if: ${{ failure() && steps.functional.conclusion == 'failure' }}
5858
shell: bash
5959
run: .github/scripts/print_logs.sh
60-
nix:
60+
dry-run:
6161
strategy:
6262
fail-fast: false
6363
matrix:
6464
os:
6565
- "ubuntu-latest"
6666
runs-on: ${{ matrix.os }}
6767
steps:
68-
- uses: actions/checkout@v4
68+
- uses: actions/checkout@v6
6969

7070
- name: Setup nix
71-
uses: cachix/install-nix-action@v30
71+
uses: cachix/install-nix-action@v31
7272
with:
7373
github_access_token: ${{ secrets.GITHUB_TOKEN }}
7474
nix_path: nixpkgs=channel:nixos-unstable
7575

7676
- name: Dry run
7777
run: nix develop .#ci -Lv -c bash -c '.github/scripts/dry_run.sh'
78+
79+
# Upload installed binary so that build-batch does not need to re-install
80+
# it.
81+
- name: Upload clc-stackage binary
82+
uses: actions/upload-artifact@v7
83+
with:
84+
name: clc-stackage-binary
85+
path: ./bin/clc-stackage
86+
retention-days: 1
87+
88+
# Uses jq's 'range(m; n)' operator to create list of indexes from [m, n)
89+
# for the build-batch job. Slightly nicer than manually listing all of them.
90+
build-batch-indexes:
91+
runs-on: "ubuntu-latest"
92+
outputs:
93+
indexes: ${{ steps.set-batch-indexes.outputs.indexes }}
94+
steps:
95+
- id: set-batch-indexes
96+
run: echo "indexes=$(jq -cn '[range(1; 19)]')" >> $GITHUB_OUTPUT
97+
98+
# Ideally CI would run a job that actually builds all packages, but this
99+
# can take a very long time, potentially longer than github's free CI limits
100+
# (last time checked: 5.5 hrs).
101+
#
102+
# What we can do instead, is perform the usual batch process of dividing the
103+
# package set into groups, then have a different job build each group.
104+
# This does /not/ run up against github's free CI limits.
105+
#
106+
# To do this, we have the script batch_index.sh divide the package set into
107+
# groups, per --batch. Then, using github's matrix strategy, have each
108+
# job build only a specific group by passing its index as --batch-index.
109+
#
110+
# In other words, each job runs
111+
#
112+
# clc-stackage --batch N --batch-index k
113+
#
114+
# where k is matrix.index, hence each building a different group.
115+
# The only other consideration we have, then, is to make sure we have enough
116+
# indices to cover the whole package set.
117+
#
118+
# Currently, we choose --batch to be 200, and the total package set is
119+
# around 3400, which is filtered to about 3100 packages to build. We thus
120+
# need at least ceiling(3100 / 200) = 16 indexes to cover this.
121+
#
122+
# There is no harm in going overboard e.g. if we have an index that is out of
123+
# range, that job will simply end with a warning message. We should
124+
# therefore err on the side of adding too many indices, rather than too few.
125+
build-batch:
126+
needs: [build-batch-indexes, dry-run]
127+
strategy:
128+
fail-fast: false
129+
matrix:
130+
index: ${{ fromJSON(needs.build-batch-indexes.outputs.indexes) }}
131+
name: Batch group ${{ matrix.index }}
132+
runs-on: "ubuntu-latest"
133+
steps:
134+
- uses: actions/checkout@v6
135+
136+
- name: Setup nix
137+
uses: cachix/install-nix-action@v31
138+
with:
139+
github_access_token: ${{ secrets.GITHUB_TOKEN }}
140+
nix_path: nixpkgs=channel:nixos-unstable
141+
142+
# Download clc-stackage binary from dry-run job.
143+
- name: Download binary
144+
uses: actions/download-artifact@v7
145+
with:
146+
name: clc-stackage-binary
147+
path: ./bin
148+
149+
- name: Build
150+
run: nix develop .#ci -Lv -c bash -c '.github/scripts/batch_index.sh ${{ matrix.index }}'

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ In general, user mitigations for solver / build problems include:
119119
compiler = pkgs.haskell.packages.ghc<vers>;
120120
```
121121
122-
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.
122+
can be a useful guide as to which GHC was last tested, as CI uses this ghc to build everything.
123123
124124
- 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.
125125

src/CLC/Stackage/Builder/Env.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ data BuildEnv = MkBuildEnv
3434
{ -- | If we have @Just n@, 'packagesToBuild' will be split into groups of at most
3535
-- size @n@. If @Nothing@, the entire set will be built in one go.
3636
batch :: Maybe Int,
37+
-- | 1-based index for building the Nth package group only, according to
38+
-- --batch. Intended for CI use, where building all groups takes too much
39+
-- time.
40+
batchIndex :: Maybe Int,
3741
-- | Build arguments for cabal.
3842
buildArgs :: [String],
3943
-- | Optional path to cabal executable.

src/CLC/Stackage/Runner.hs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ where
88

99
import CLC.Stackage.Builder qualified as Builder
1010
import CLC.Stackage.Builder.Env
11-
( BuildEnv (hLogger, progress),
11+
( BuildEnv (batchIndex, hLogger, progress),
1212
Progress (failuresRef),
1313
)
1414
import CLC.Stackage.Builder.Writer qualified as Writer
@@ -20,6 +20,10 @@ import Control.Exception (bracket, throwIO)
2020
import Control.Monad (when)
2121
import Data.Foldable (for_)
2222
import Data.IORef (readIORef)
23+
import Data.List.NonEmpty (NonEmpty)
24+
import Data.List.NonEmpty qualified as NE
25+
import Data.Text (Text)
26+
import Data.Text qualified as T
2327
import System.Exit (ExitCode (ExitFailure))
2428
import System.IO qualified as IO
2529

@@ -52,7 +56,24 @@ runModifyPackages hLogger modifyPackages = withHiddenInput $ do
5256

5357
Logging.putTimeInfoStr buildEnv.hLogger "Starting build(s)"
5458

55-
for_ pkgGroupsIdx $ \(pkgGroup, idx) -> Builder.buildProject buildEnv idx pkgGroup
59+
case buildEnv.batchIndex of
60+
-- No batch index: normal, build all groups sequentially.
61+
Nothing -> for_ pkgGroupsIdx $ \(pkgGroup, idx) ->
62+
Builder.buildProject buildEnv idx pkgGroup
63+
Just batchIndex ->
64+
-- Some batch index: if it is in range, build that group.
65+
case index batchIndex pkgGroupsIdx of
66+
Just (pkgGroup, idx) -> Builder.buildProject buildEnv idx pkgGroup
67+
Nothing -> do
68+
let msg =
69+
mconcat
70+
[ "Nothing to build. Index '",
71+
showt batchIndex,
72+
"' is out of range for ",
73+
showt $ NE.length pkgGroupsIdx,
74+
" group(s)."
75+
]
76+
Logging.putTimeWarnStr buildEnv.hLogger msg
5677

5778
numErrors <- length <$> readIORef buildEnv.progress.failuresRef
5879
when (numErrors > 0) $ throwIO $ ExitFailure 1
@@ -75,3 +96,16 @@ withHiddenInput m = bracket hideInput unhideInput (const m)
7596
unhideInput (buffMode, echoMode) = do
7697
IO.hSetBuffering IO.stdin buffMode
7798
IO.hSetEcho IO.stdin echoMode
99+
100+
index :: Int -> NonEmpty a -> Maybe a
101+
index idx = go idx' . NE.toList
102+
where
103+
go _ [] = Nothing
104+
go 0 (x : _) = Just x
105+
go !n (_ : xs) = go (n - 1) xs
106+
107+
-- Subtract one since the index is one-based, not zero.
108+
idx' = idx - 1
109+
110+
showt :: (Show a) => a -> Text
111+
showt = T.pack . show

src/CLC/Stackage/Runner/Args.hs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ data Args = MkArgs
5858
{ -- | If given, batches packages together so we build more than one.
5959
-- Defaults to batching everything together in the same group.
6060
batch :: Maybe Int,
61+
-- | 1-based index for building the Nth package group only, according to
62+
-- --batch. Intended for CI use, where building all groups takes too much
63+
-- time.
64+
batchIndex :: Maybe Int,
6165
-- | Global options to pass to cabal e.g. --store-dir.
6266
cabalGlobalOpts :: [String],
6367
-- | Options to pass to cabal e.g. --semaphore.
@@ -191,12 +195,13 @@ parseCliArgs =
191195
~(cabalGlobalOpts, cabalOpts, cabalPath, cabalUpdate) <- parseCabalGroup
192196
~(cache, retryFailures) <- parseCacheGroup
193197
~(groupFailFast, packageFailFast) <- parseFailuresGroup
194-
~(batch, printPackageSet, snapshotPath) <- parseMiscGroup
198+
~(batch, batchIndex, printPackageSet, snapshotPath) <- parseMiscGroup
195199
~(cleanup, colorLogs, writeLogs) <- parseOutputGroup
196200

197201
pure $
198202
MkArgs
199203
{ batch,
204+
batchIndex,
200205
cabalGlobalOpts,
201206
cabalOpts,
202207
cabalPath,
@@ -236,8 +241,9 @@ parseCliArgs =
236241

237242
parseMiscGroup =
238243
OA.parserOptionGroup "Misc options:" $
239-
(,,)
244+
(,,,)
240245
<$> parseBatch
246+
<*> parseBatchIndex
241247
<*> parsePrintPackageSet
242248
<*> parseSnapshotPath
243249

@@ -267,6 +273,22 @@ parseBatch =
267273
]
268274
)
269275

276+
-- Determines which --batch group to build. Normally we want to build all
277+
-- groups, so this arg is intended only for CI, where a single CI job cannot
278+
-- build everything, or it will time out. Hence this is marked 'internal'
279+
-- to hide it from the --help page, as its presence would only confuse.
280+
parseBatchIndex :: Parser (Maybe Int)
281+
parseBatchIndex =
282+
OA.optional $
283+
OA.option
284+
OA.auto
285+
( mconcat
286+
[ OA.long "batch-index",
287+
OA.internal,
288+
OA.metavar "NAT"
289+
]
290+
)
291+
270292
parseCabalGlobalOpts :: Parser [String]
271293
parseCabalGlobalOpts =
272294
OA.option

src/CLC/Stackage/Runner/Env.hs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ setup hLoggerRaw modifyPackages = do
181181
buildEnv =
182182
MkBuildEnv
183183
{ batch = cliArgs.batch,
184+
batchIndex = cliArgs.batchIndex,
184185
buildArgs,
185186
cabalPath,
186187
groupFailFast = cliArgs.groupFailFast,
@@ -217,8 +218,10 @@ teardown :: RunnerEnv -> IO ()
217218
teardown env = do
218219
endTime <- env.buildEnv.hLogger.getLocalTime
219220
when env.cleanup $ do
220-
Dir.removeFile Paths.generatedCabalPath
221-
Dir.removeFile Paths.generatedCabalProjectLocalPath
221+
-- removePathForcibly as these might not exist e.g. if the we did not
222+
-- build anything.
223+
Dir.removePathForcibly Paths.generatedCabalPath
224+
Dir.removePathForcibly Paths.generatedCabalProjectLocalPath
222225

223226
results <- getResults env.buildEnv
224227
let report =

test/unit/Unit/Prelude.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import CLC.Stackage.Builder.Env
99
( BuildEnv
1010
( MkBuildEnv,
1111
batch,
12+
batchIndex,
1213
buildArgs,
1314
cabalPath,
1415
groupFailFast,
@@ -66,6 +67,7 @@ mkBuildEnv = do
6667
pure $
6768
MkBuildEnv
6869
{ batch = Nothing,
70+
batchIndex = Nothing,
6971
buildArgs = [],
7072
cabalPath = "cabal",
7173
groupFailFast = False,

0 commit comments

Comments
 (0)