Skip to content

Commit 05e1a0c

Browse files
committed
ci: cache stage0 cabal binary across CI runs
Cache the built cabal binary at _build/cabal/bin using actions/cache, keyed on hashFiles('cabal.project.stage0'). Makefile: replace the $(CABAL) target with a .PHONY stable-cabal target that conditionally builds cabal based on USE_SYSTEM_CABAL. When USE_SYSTEM_CABAL is non-empty, the build is skipped entirely. CABAL is defined with ?= so it can be overridden to point at a pre-built binary (e.g., make USE_SYSTEM_CABAL=1 CABAL=/usr/bin/cabal). clean-cabal / clean-stage0 become no-ops when USE_SYSTEM_CABAL is set, so CI can run the full clean sequence without wiping a cached binary. CI workflows detect cached binaries and set USE_SYSTEM_CABAL=1 to skip rebuilding on cache hits.
1 parent e2c00fd commit 05e1a0c

3 files changed

Lines changed: 81 additions & 17 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ on:
77
branches: [stable-ghc-9.14, stable-master]
88
workflow_dispatch:
99

10+
env:
11+
CABAL_CACHE_PATH: _build/cabal/bin
12+
1013
jobs:
1114
cabal:
1215
strategy:
@@ -53,6 +56,13 @@ jobs:
5356
minimal: true
5457
ghc: true
5558

59+
- name: Cache stage0 cabal binary
60+
id: cabal-cache
61+
uses: actions/cache@v5
62+
with:
63+
path: ${{ env.CABAL_CACHE_PATH }}
64+
key: cabal-${{ vars.CACHE_VERSION || 'v1' }}-${{ matrix.plat }}-${{ hashFiles('cabal.project.stage0') }}
65+
5666
- name: Update hackage
5767
shell: devx {0}
5868
run: cabal update
@@ -71,7 +81,7 @@ jobs:
7181

7282
- name: Build (dynamic=${{ matrix.dynamic }})
7383
shell: devx {0}
74-
run: make QUIET=1 DYNAMIC=${{ matrix.dynamic }}
84+
run: make QUIET=1 DYNAMIC=${{ matrix.dynamic }} ${{ steps.cabal-cache.outputs.cache-hit == 'true' && 'USE_SYSTEM_CABAL=1' || '' }}
7585

7686
- name: Display build timings
7787
if: ${{ !cancelled() }}
@@ -95,7 +105,7 @@ jobs:
95105
96106
- name: Run the testsuite (dynamic=${{ matrix.dynamic }})
97107
shell: devx {0}
98-
run: make test QUIET=1 DYNAMIC=${{ matrix.dynamic }}
108+
run: make test QUIET=1 DYNAMIC=${{ matrix.dynamic }} ${{ steps.cabal-cache.outputs.cache-hit == 'true' && 'USE_SYSTEM_CABAL=1' || '' }}
99109

100110
- name: Stop metrics collection
101111
if: ${{ !cancelled() }}

.github/workflows/reusable-release.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ env:
4444
CHERE_INVOKING: 1
4545
# dynamic
4646
DYNAMIC: ${{ inputs.dynamic }}
47+
# stage0 cabal cache
48+
CABAL_CACHE_PATH: _build/cabal/bin
4749

4850
jobs:
4951
tool-output:
@@ -149,10 +151,21 @@ jobs:
149151
./emsdk install ${{ env.EMSDK_VERSION }}
150152
./emsdk activate ${{ env.EMSDK_VERSION }}
151153

154+
- name: Cache stage0 cabal binary
155+
uses: actions/cache@v5
156+
with:
157+
path: ${{ env.CABAL_CACHE_PATH }}
158+
key: cabal-${{ vars.CACHE_VERSION || 'v1' }}-${{ matrix.platform.ARTIFACT }}-${{ hashFiles('cabal.project.stage0') }}
159+
152160
- name: Run build
153161
run: &build |
154162
set -eux
155163

164+
# Skip building cabal if a cached binary already exists
165+
if [ -x "${{ env.CABAL_CACHE_PATH }}/cabal" ]; then
166+
export USE_SYSTEM_CABAL=1
167+
fi
168+
156169
# On windows, we use msys2 shell, which does not by default inherit the windows PATHs.
157170
# So although the ghcup action sets the PATH, it's not visible. We don't want to use
158171
# 'pathtype: inherit', because it pollutes the msys2 paths, possibly leading to linking
@@ -214,6 +227,12 @@ jobs:
214227
ref: ${{ matrix.branch }}
215228
submodules: recursive
216229

230+
- name: Cache stage0 cabal binary
231+
uses: actions/cache@v5
232+
with:
233+
path: ${{ env.CABAL_CACHE_PATH }}
234+
key: cabal-${{ vars.CACHE_VERSION || 'v1' }}-${{ matrix.platform.ARTIFACT }}-${{ hashFiles('cabal.project.stage0') }}
235+
217236
# debian 11 ships with make 4.3, but we need 4.4
218237
- if: matrix.platform.ARTIFACT == 'aarch64-linux-deb11'
219238
uses: docker://arm64v8/debian:11
@@ -235,6 +254,7 @@ jobs:
235254
ghcup --no-verbose install ghc --set --install-targets '${{ env.GHC_TARGETS }}' ${{ env.GHC_VERSION }} &&
236255
ghcup --no-verbose install cabal --set ${{ env.CABAL_VERSION }} &&
237256
cabal update &&
257+
if [ -x ${{ env.CABAL_CACHE_PATH }}/cabal ]; then export USE_SYSTEM_CABAL=1; fi &&
238258
make _build/dist/ghc.tar.gz _build/dist/cabal.tar.gz _build/dist/tests.tar.gz &&
239259
cd _build/dist &&
240260
mv ghc.tar.gz ghc-$(bin/ghc --numeric-version)-${{ matrix.platform.ARTIFACT }}.tar.gz &&
@@ -255,6 +275,7 @@ jobs:
255275
ghcup --no-verbose install ghc --set --install-targets '${{ env.GHC_TARGETS }}' ${{ env.GHC_VERSION }} &&
256276
ghcup --no-verbose install cabal --set ${{ env.CABAL_VERSION }} &&
257277
cabal update &&
278+
if [ -x ${{ env.CABAL_CACHE_PATH }}/cabal ]; then export USE_SYSTEM_CABAL=1; fi &&
258279
make _build/dist/ghc.tar.gz _build/dist/cabal.tar.gz _build/dist/tests.tar.gz &&
259280
cd _build/dist &&
260281
mv ghc.tar.gz ghc-$(bin/ghc --numeric-version)-${{ matrix.platform.ARTIFACT }}.tar.gz &&
@@ -304,6 +325,12 @@ jobs:
304325
echo "/usr/local/opt/make/libexec/gnubin" >> $GITHUB_PATH
305326
echo "/usr/local/opt/libtool/libexec/gnubin" >> $GITHUB_PATH
306327
328+
- name: Cache stage0 cabal binary
329+
uses: actions/cache@v5
330+
with:
331+
path: ${{ env.CABAL_CACHE_PATH }}
332+
key: cabal-${{ vars.CACHE_VERSION || 'v1' }}-${{ env.ARTIFACT }}-${{ hashFiles('cabal.project.stage0') }}
333+
307334
- name: Install emscripten
308335
run : *emscripten
309336

@@ -353,6 +380,12 @@ jobs:
353380
echo "/opt/homebrew/opt/make/libexec/gnubin" >> $GITHUB_PATH
354381
echo "/opt/homebrew/opt/libtool/libexec/gnubin" >> $GITHUB_PATH
355382
383+
- name: Cache stage0 cabal binary
384+
uses: actions/cache@v5
385+
with:
386+
path: ${{ env.CABAL_CACHE_PATH }}
387+
key: cabal-${{ vars.CACHE_VERSION || 'v1' }}-${{ env.ARTIFACT }}-${{ hashFiles('cabal.project.stage0') }}
388+
356389
- name: Run build
357390
run: *build
358391

@@ -401,6 +434,12 @@ jobs:
401434
run: ghcup --no-verbose install ghc --set --install-targets "${{ env.GHC_TARGETS }}" "${{ env.GHC_VERSION }}"
402435
shell: pwsh
403436

437+
- name: Cache stage0 cabal binary
438+
uses: actions/cache@v5
439+
with:
440+
path: ${{ env.CABAL_CACHE_PATH }}
441+
key: cabal-${{ vars.CACHE_VERSION || 'v1' }}-${{ env.ARTIFACT }}-${{ hashFiles('cabal.project.stage0') }}
442+
404443
- name: Run build
405444
run: *build
406445
env:
@@ -473,8 +512,18 @@ jobs:
473512
run : |
474513
sudo pkg install -y emscripten
475514
515+
- name: Cache stage0 cabal binary
516+
uses: actions/cache@v5
517+
with:
518+
path: ${{ env.CABAL_CACHE_PATH }}
519+
key: cabal-${{ vars.CACHE_VERSION || 'v1' }}-${{ env.ARTIFACT }}-${{ hashFiles('cabal.project.stage0') }}
520+
476521
- name: Run build
477522
run: |
523+
set -eux
524+
if [ -x "${{ env.CABAL_CACHE_PATH }}/cabal" ]; then
525+
export USE_SYSTEM_CABAL=1
526+
fi
478527
which ghc
479528
ghc --info
480529
cabal update

Makefile

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -185,14 +185,14 @@ STAGE2_STAMP := $(BUILD_DIR)/.stamp-stage2
185185
# Because there are no prerequisites, Make won't re-run these when the stamp
186186
# file already exists — which is the whole point: `test: $(STAGE2_STAMP)` will
187187
# skip the build if stage2 already completed.
188-
$(STAGE0_STAMP): ; @$(MAKE) stage0
188+
$(STAGE0_STAMP): ; @$(MAKE) stable-cabal
189189
$(STAGE1_STAMP): ; @$(MAKE) stage1
190190
$(STAGE2_STAMP): ; @$(MAKE) stage2
191191

192192
# HOST_PLATFROM is always from the bootstrap compiler
193193
HOST_PLATFORM := $(shell $(GHC0) --print-host-platform)
194194

195-
CABAL := $(BUILD_DIR)/cabal/bin/cabal$(EXE_EXT)
195+
CABAL ?= $(BUILD_DIR)/cabal/bin/cabal$(EXE_EXT)
196196

197197
STAGE1_PATH := $(let STAGE,stage1,$(STORE_DIR)/host/$(HOST_PLATFORM))
198198
STAGE2_PATH := $(let STAGE,stage2,$(STORE_DIR)/host/$(HOST_PLATFORM))
@@ -323,7 +323,7 @@ define CABAL_INSTALL_STAGE0
323323
--store-dir $(call NORMALIZE_FP,$(CURDIR)/$(STORE_DIR)) \
324324
--logs-dir $(call NORMALIZE_FP,$(CURDIR)/$(LOGS_DIR)) \
325325
install \
326-
--installdir $(dir $@) \
326+
--installdir $(dir $(CABAL)) \
327327
--builddir $(call NORMALIZE_FP,$(CURDIR)/$(STAGE_DIR)) \
328328
--project-file cabal.project.$(STAGE) \
329329
--overwrite-policy=always --install-method=copy \
@@ -540,16 +540,19 @@ all: stage2
540540
# | (_| (_| | |_) | (_| | |_____| | | | \__ \ || (_| | | |
541541
# \___\__,_|_.__/ \__,_|_| |_|_| |_|___/\__\__,_|_|_|
542542

543-
.PHONY: $(CABAL)
544-
$(CABAL): STAGE=stage0
545-
$(CABAL):
543+
# TODO: Building cabal-install from source as part of the Makefile is a
544+
# temporary workaround. We should eventually require cabal to be provided
545+
# externally (e.g. via ghcup) and drop this target entirely.
546+
.PHONY: stable-cabal
547+
stable-cabal: STAGE=stage0
548+
stable-cabal:
549+
ifeq (,$(USE_SYSTEM_CABAL))
546550
$(call PHASE_START,cabal)
547-
$(call LOG,Building $@)
551+
$(call LOG,Building $(CABAL))
548552
$(CABAL_INSTALL_STAGE0) --with-compiler $(GHC0) cabal-install:exe:cabal
549553
$(call PHASE_END_OK,cabal)
550554
@touch $(STAGE0_STAMP)
551-
552-
stage0 : $(CABAL)
555+
endif
553556

554557
# ____ _ _
555558
# / ___|| |_ __ _ __ _ ___ / |
@@ -587,7 +590,7 @@ STAGE1_CABAL_BUILD = \
587590
--ghc-options "-ghcversion-file=$(call NORMALIZE_FP,$(CURDIR)/rts/include/ghcversion.h)"
588591

589592
stage1: STAGE=stage1
590-
stage1: $(CABAL) $(CONFIGURE_SCRIPTS) $(CONFIGURED_FILES) cabal.project.stage1 cabal.project.common libraries/ghc-boot-th-next | hackage
593+
stage1: stable-cabal $(CONFIGURE_SCRIPTS) $(CONFIGURED_FILES) cabal.project.stage1 cabal.project.common libraries/ghc-boot-th-next | hackage
591594
$(call PHASE_START,stage1)
592595
$(call LOG,Starting build of $(STAGE))
593596

@@ -708,7 +711,7 @@ STAGE2_CABAL_BUILD = \
708711

709712
stage2: STAGE=stage2
710713
stage2: TARGET_PLATFORM:=$(HOST_PLATFORM)
711-
stage2: $(GHC1) $(CABAL) $(CONFIGURE_SCRIPTS) $(CONFIGURED_FILES) cabal.project.stage2 cabal.project.stage2.settings cabal.project.common libraries/ghc-boot-th-next | stage1
714+
stage2: $(GHC1) stable-cabal $(CONFIGURE_SCRIPTS) $(CONFIGURED_FILES) cabal.project.stage2 cabal.project.stage2.settings cabal.project.common libraries/ghc-boot-th-next | stage1
712715
$(call PHASE_START,stage2)
713716
$(call LOG,Starting build of $(STAGE))
714717

@@ -1029,19 +1032,19 @@ $(DIST_DIR)/ghc.tar.gz: stage2
10291032
lib/$(HOST_PLATFORM)
10301033
@echo "::endgroup::"
10311034

1032-
$(DIST_DIR)/cabal.tar.gz: $(CABAL)
1035+
$(DIST_DIR)/cabal.tar.gz: stable-cabal
10331036
@echo "::group::Creating cabal.tar.gz..."
10341037
@mkdir -p $(DIST_DIR)/bin
1035-
@cp $< $(DIST_DIR)/bin/
1038+
@cp $(CABAL) $(DIST_DIR)/bin/
10361039
@tar czf $@ \
10371040
--directory=$(DIST_DIR) \
10381041
bin/cabal
10391042
@echo "::endgroup::"
10401043

1041-
$(DIST_DIR)/haskell-toolchain.tar.gz: $(CABAL) stage2 stage3-javascript-unknown-ghcjs
1044+
$(DIST_DIR)/haskell-toolchain.tar.gz: stable-cabal stage2 stage3-javascript-unknown-ghcjs
10421045
@echo "::group::Creating haskell-toolchain.tar.gz..."
10431046
@mkdir -p $(DIST_DIR)/bin
1044-
@cp $< $(DIST_DIR)/bin/
1047+
@cp $(CABAL) $(DIST_DIR)/bin/
10451048
@tar czf $@ \
10461049
--directory=$(DIST_DIR) \
10471050
$(foreach exe,$(STAGE2_EXECUTABLES),bin/$(exe)$(EXE_EXT)) \
@@ -1123,7 +1126,9 @@ libraries/ghc-boot-th-next: \
11231126
clean-cabal: clean-stage0
11241127
clean-stage0:
11251128
@echo "::group::Cleaning build artifacts..."
1129+
ifeq (,$(USE_SYSTEM_CABAL))
11261130
rm -rf $(BUILD_DIR)/cabal
1131+
endif
11271132
rm -rf $(BUILD_DIR)/stage0
11281133
rm -f $(STAGE0_STAMP)
11291134
@echo "::endgroup::"

0 commit comments

Comments
 (0)