Skip to content

Switch to Cabal-syntax; widen compatibility range#112

Closed
ulidtko wants to merge 7 commits into
kowainik:mainfrom
ulidtko:feat/cabal-compat
Closed

Switch to Cabal-syntax; widen compatibility range#112
ulidtko wants to merge 7 commits into
kowainik:mainfrom
ulidtko:feat/cabal-compat

Conversation

@ulidtko

@ulidtko ulidtko commented Jun 17, 2025

Copy link
Copy Markdown

Resolves #111.

Dependency footprint reduced by switching CabalCabal-syntax.

Compatibility widened from 3.14 (only) to 3.8, 3.10, 3.12, 3.14.

CI matrix extended accordingly.

The new cabal version guards — cross-checked by hand with the Cabal-syntax API(s).

Tested in this CI run — I'm leaving the branch exactly as was in CI; the workflow yaml will obviously have to revert a few tweaks before merging.

Maintainer edits open.

Comment thread .github/workflows/ci.yml
Comment on lines -30 to +35
if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/main'
# if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/main'

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be reverted before merging

Comment thread src/Extensions/Cabal.hs Outdated
Comment thread .github/workflows/ci.yml
Comment on lines +51 to +54
cabal v2-configure \
--constraint 'Cabal-syntax installed' \
--enable-tests --enable-benchmarks

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haskell-actions/setup already installs Cabal from the matrix.

the added --constraint 'Cabal-syntax installed' prevents the build from satisfying the dependency via compiling another (newest) version.

Comment thread .github/workflows/ci.yml
@ulidtko ulidtko marked this pull request as ready for review June 17, 2025 19:24
@ulidtko ulidtko requested a review from vrom911 as a code owner June 17, 2025 19:24
@tomjaguarpaw

Copy link
Copy Markdown
Collaborator

Thanks. In principle this looks like the right thing to do, but I don't understand who benefits from this. For each recent version of GHC there is a compatible extensions version which was released shortly after it, which is identical in functionality to extensions-1.0.0.0. So what in addition does this PR achieve?

@ulidtko

ulidtko commented Jun 17, 2025

Copy link
Copy Markdown
Author

In general: any consumer of the library might benefit from wider compatibility in its dependencies.

In particular: I'm packaging up stan for Arch Linux repos, and this change would:

  1. Spare me from backporting a literally 10th dependency — just because extensions claims that the one from boot libs isn't "fresh enough" or something.
  2. Spare users of the package from installing yet another copy of Cabal/Cabal-syntax along with Stan — despite having a perfectly suitable version necessarily already installed along with ghc boot-libs.

@ulidtko

ulidtko commented Jun 18, 2025

Copy link
Copy Markdown
Author

... Case in point: here's a benchmark.

GHC Stan static exe size (extensions main) Stan static exe size (extensions w/ PR #112)
9.4.8 40 MB (39681888) 27 MB (27407088)
9.6.7 36 MB (36456336) 24 MB (24412928)
9.8.4 37 MB (36906328) 25 MB (24847776)
9.10.1 37 MB (37464264) 25 MB (25462784)
Measurement details

All builds made in a simple cabal.project with just stan and extensions repos, using the command:

cabal build --disable-documentation --enable-optimization --disable-shared --enable-library-vanilla --disable-executable-dynamic

On GHC 9.12.2, dependency resolution fails without --allow-newer: rejecting: base-4.21.0.0/installed-ae91 (conflict: extensions => base>=4.13.0.0 && <4.21)

You can see that without this patch, builds of Stan are consistently 50% larger.

This happens because both Cabal and Cabal-syntax get linked into the executable twice (each), at different versions, due to the rigid pin to 3.14 in extensions.

@tomjaguarpaw

Copy link
Copy Markdown
Collaborator

I still don't understand. Arch is on GHC 9.4, right? So you should be using extensions-1.0.0.0, right? And therefore Cabal-3.8 right? So how do Cabal and Cabal-syntax 3.14 come into it? What am I missing?

@ulidtko

ulidtko commented Jun 18, 2025

Copy link
Copy Markdown
Author

I still don't understand. Arch is on GHC 9.4, right? So you should be using extensions-1.0.0.0, right? And therefore Cabal-3.8 right? So how do Cabal and Cabal-syntax 3.14 come into it? What am I missing?

For the benchmark, I built stan with the latest main of extensions (2nd column), and once more with extensions from feat/cabal-compat branch.

@tomjaguarpaw

tomjaguarpaw commented Jun 18, 2025

Copy link
Copy Markdown
Collaborator

Oh, well I'm definitely in favour of depending on only Cabal-syntax. Let's do that here: #113

I'm skeptical whether the numbers you post have anything to do with different versions of Cabal linked. I wasn't even aware that Cabal/GHC could do that! Can you post some additional evidence, like the output of ldd or similar?

[EDIT: Oh I see, it's static, so ldd won't work. But I'd like to find some way to be sure that the stan static exe actually does have two different versions of each library linked.]

My current guess is that the difference in size is due to depending on Cabal-syntax but let's see after my PR above is merged. If I'm wrong then that's good evidence that we should merge your PR. I'm happy to run the benchmark myself if you show me full details of how to reproduce it, including the repo contents.

@ulidtko

ulidtko commented Jun 18, 2025

Copy link
Copy Markdown
Author
  1. git clone https://github.com/kowainik/stan && cd stan
  2. git clone https://github.com/kowainik/extensions #-- to subdir of stan
  3. echo packages: . extensions > cabal.project
  4. cabal build --disable-documentation --enable-optimization --disable-shared --enable-library-vanilla --disable-executable-dynamic
  5. ls -l dist/<long path that cabal tells>/stan

Then cd extensions; git switch feat/cabal-compat; cd .. and rebuild.

You might be right that the size reduction is due to reduced dependency footprint — I haven't checked symbol tables, only guessed a conjecture.

@tomjaguarpaw

Copy link
Copy Markdown
Collaborator

Thanks. If you can reproduce the same behaviour on top of current main (which only depends on Cabal-syntax, not Cabal) then I'll investigate further. If not then I'm still looking for a good justification for adding complexity to the CPP.

@ulidtko

ulidtko commented Jun 18, 2025

Copy link
Copy Markdown
Author

Got it; thanks for clarity.

I must switch to something else now; will postpone this for a day or two. After rechecking, will either close the PR or send a ping.

ulidtko added 6 commits June 21, 2025 10:13
These are, obviously, needed for Cabal < 3.6 which is already outside
the supported version range.
* Cabal-syntax is a smaller package than Cabal; reduce dependency footprint.

* Using Cabal-syntax requires Cabal >= 3.8 (and 3.6, with workarounds).

* In addition to GHC version checks (which guard case arms' RHS's), also guard
  the LHS patterns, which may not exist depending on Cabal-syntax version.

In all configurations, the entire case is exhaustive, and -Werror=incomplete-patterns
(in extensions.cabal) guards that.
The library interface stay exactly the same -- thus, sub-patch-level
version bump.
@ulidtko ulidtko force-pushed the feat/cabal-compat branch from cf90f04 to 9b23e9f Compare June 21, 2025 13:32
@ulidtko

ulidtko commented Jun 21, 2025

Copy link
Copy Markdown
Author

I do still reproduce the issue.

REPOSITORY                                      TAG                 IMAGE ID       CREATED          SIZE
stan-extensions-repro111                        9.6.7-fix           80fdc2faa9d3   5 minutes ago    3.51GB
stan-extensions-repro111                        9.6.7-main          bec22fa42eec   17 minutes ago   3.6GB

The build with my fix is 90 MB smaller.

(repro steps, isolated in Docker)

Same repository setup as before:

git clone https://github.com/kowainik/stan && cd stan
git clone https://github.com/kowainik/extensions #-- to subdir of stan
#-- add PR #112 as branch
( cd extensions; git fetch origin pull/112/head:feat/cabal-compat; )

Add cabal.project.local:

packages: . extensions

shared: True
static: True

executable-dynamic: True
executable-static: False

optimization: 2
documentation: False

Add repro#111.dockerfile:

ARG GHC=9.6.7
FROM haskell:$GHC-slim

RUN cabal v2-update hackage.haskell.org,2025-06-21T11:05:47Z

COPY . /BUILD
WORKDIR /BUILD
RUN cabal install stan:exe:stan

Build the 2 images:

( cd extensions; git switch main; )
docker build -f 'repro#111.dockerfile' -t stan-extensions-repro111:9.6.7-main .
( cd extensions; git switch feat/cabal-compat; )
docker build -f 'repro#111.dockerfile' -t stan-extensions-repro111:9.6.7-fix .

I've switched to builds with dynamic linking — because they're more transparent for analysis than static builds.
(And because of changing my workstation machine; and because of cabal bugs.)
(The huge total size of 3.5 GB can be optimized by more complex dockerfile, I chose to keep it simple.)

The size difference is fully explained by an additional build of Cabal-syntax (which was required via the tight bounds):

$ docker run --rm --entrypoint=/bin/bash stan-extensions-repro111:9.6.7-main -c \
   'du -sh /root/.local/state/cabal/store/ghc-9.6.7'
193M	/root/.local/state/cabal/store/ghc-9.6.7

$ docker run --rm --entrypoint=/bin/bash stan-extensions-repro111:9.6.7-fix -c \
  'du -sh /root/.local/state/cabal/store/ghc-9.6.7'
103M	/root/.local/state/cabal/store/ghc-9.6.7

$ docker run --rm --entrypoint=/bin/bash stan-extensions-repro111:9.6.7-main -c \
  'du -sh /root/.local/state/cabal/store/ghc-9.6.7/Cabal-syntax-*'
90M	/root/.local/state/cabal/store/ghc-9.6.7/Cabal-syntax-3.14.2.0-be475844da27bcbf1c35aec7937d0c63838b3ad1bd46f667d32703da3106665e

The -fix build did not need the additional copy of Cabal-syntax (v3.14) — because it instead linked with the version 3.10.3.0 already installed with GHC boot-libs:

$ docker run --rm --entrypoint=/bin/bash stan-extensions-repro111:9.6.7-fix -c \
  'ldd /root/.local/bin/stan' | grep Cabal-syntax
	libHSCabal-syntax-3.10.3.0-ghc9.6.7.so => /opt/ghc/9.6.7/lib/ghc-9.6.7/lib/x86_64-linux-ghc-9.6.7/libHSCabal-syntax-3.10.3.0-ghc9.6.7.so (0x000078eb963a1000)

@tomjaguarpaw ping; additional discussion in #111.

@tomjaguarpaw

tomjaguarpaw commented Jun 21, 2025

Copy link
Copy Markdown
Collaborator

The -fix build did not need the additional copy of Cabal-syntax (v3.14) — because it instead linked with the version 3.10.3.0 already installed with GHC boot-libs:

Firstly I'm very surprised to hear this because I thought that cabal-install always chose the latest versions of libraries available to it, in this case extensions-0.1.0.3 and Cabal-syntax-3.14.2.0. I don't understand why about the -fix setup causes the latest version of Cabal-syntax not to be chosen.

But secondly, you could have achieved the same size reduction by depending on extensions-0.1.0.1, couldn't you?

@tomjaguarpaw

Copy link
Copy Markdown
Collaborator

By the way, your build matrix is not actually building against different versions of Cabal-syntax, it's just using different versions of cabal-install.

@ulidtko

ulidtko commented Jun 21, 2025

Copy link
Copy Markdown
Author

By the way, your build matrix is not actually building against different versions of Cabal-syntax, it's just using different versions of cabal-install.

I'm pretty sure that's not the case.

cabal-install 3.14.2.0 depends on Cabal (>=3.14.2 && <3.15) depends on Cabal-syntax (>=3.14.2 && <3.15), likewise other versions. So, transitively, the available Cabal-syntax for satisfying --constraint 'Cabal-syntax installed' is closely tied to the cabal-install version from matrix.

@tomjaguarpaw

Copy link
Copy Markdown
Collaborator

The "cabal install" installed by the CI script is an executable not a library, and therefore has no bearing on what library versions extensions is built with. By way of further evidence, your branch simply does not build under Cabal-syntax^>=3.14:

% git rev-parse HEAD && cabal repl -w ghc-9.6 --build-depends 'Cabal-syntax^>=3.14'
9b23e9f08cc464cb2afde72de7b7844eab7a1967
Configuration is affected by the following files:
- cabal.project.local
Resolving dependencies...
Build profile: -w ghc-9.6.7 -O1
In order, the following will be built (use -v for more details):
 - extensions-0.1.0.3 (interactive) (lib) (first run)
Preprocessing library for extensions-0.1.0.3...
GHCi, version 9.6.7: https://www.haskell.org/ghc/  :? for help
[1 of 5] Compiling Extensions.Types
[2 of 5] Compiling Extensions.Module
[3 of 5] Compiling Extensions.Cabal

src/Extensions/Cabal.hs:118:65: error: [GHC-83865]
    • Couldn't match type: Distribution.Utils.Path.SymbolicPathX
                             Distribution.Utils.Path.OnlyRelative
                             Distribution.Utils.Path.Source
                             Distribution.Utils.Path.File
                     with: [Char]
      Expected: FilePath
        Actual: Distribution.Utils.Path.RelativePath
                  Distribution.Utils.Path.Source Distribution.Utils.Path.File
    • In the expression: modulePath
      In the expression: [modulePath]
      In the first argument of ‘condTreeToExtensions’, namely
        ‘(\ Executable {..} -> [modulePath])’
    |
118 |     exeToExtensions = condTreeToExtensions (\Executable{..} -> [modulePath]) buildInfo
    |                                                                 ^^^^^^^^^^

src/Extensions/Cabal.hs:125:40: error: [GHC-83865]
    • Couldn't match type: Distribution.Utils.Path.SymbolicPathX
                             Distribution.Utils.Path.OnlyRelative
                             Distribution.Utils.Path.Source
                             Distribution.Utils.Path.File
                     with: [Char]
      Expected: FilePath
        Actual: Distribution.Utils.Path.RelativePath
                  Distribution.Utils.Path.Source Distribution.Utils.Path.File
    • In the expression: path
      In the expression: [path]
      In a case alternative: TestSuiteExeV10 _ path -> [path]
    |
125 |             TestSuiteExeV10 _ path -> [path]
    |                                        ^^^^

src/Extensions/Cabal.hs:134:40: error: [GHC-83865]
    • Couldn't match type: Distribution.Utils.Path.SymbolicPathX
                             Distribution.Utils.Path.OnlyRelative
                             Distribution.Utils.Path.Source
                             Distribution.Utils.Path.File
                     with: [Char]
      Expected: FilePath
        Actual: Distribution.Utils.Path.RelativePath
                  Distribution.Utils.Path.Source Distribution.Utils.Path.File
    • In the expression: path
      In the expression: [path]
      In a case alternative: BenchmarkExeV10 _ path -> [path]
    |
134 |             BenchmarkExeV10 _ path -> [path]
    |                                        ^^^^

@ulidtko

ulidtko commented Jun 21, 2025

Copy link
Copy Markdown
Author

you could have achieved the same size reduction by depending on extensions-0.1.0.1, couldn't you?

Correct.

Yet, I think this approach asks for vastly more maintenance overhead: next time a release (or revision) must be made — the release process now must be replicated three times.

So, I guess it's desirable to avoid having 3 versions 0.1.0.1, 0.1.0.2, 0.1.0.3 (each aiming to support different GHC version) — and instead, reconcile compatibility into one single version, "the latest".

@ulidtko

ulidtko commented Jun 21, 2025

Copy link
Copy Markdown
Author

your branch simply does not build under Cabal-syntax^>=3.14:

That is simply because you'd added 9.12 to the matrix — and I rebased, resolving conflicts.

Watch if I comment out 9.12...

@tomjaguarpaw

Copy link
Copy Markdown
Collaborator

I'm very surprised to hear this because I thought that cabal-install always chose the latest versions of libraries

I asked about this here: https://discourse.haskell.org/t/how-does-cabal-install-choose-boot-library-versions/12352

@ulidtko

ulidtko commented Jun 21, 2025

Copy link
Copy Markdown
Author

The "cabal install" installed by the CI script is an executable not a library, and therefore has no bearing on what library versions extensions is built with.

It has direct bearing. cabal-install depends on Cabal depends on Cabal-syntax.

I asked about this here: https://discourse.haskell.org/t/how-does-cabal-install-choose-boot-library-versions/12352

I think Cabal just gives preference to already-installed versions (if they satisfy bounds, of course) — exactly to mitigate this multiple-versions-of-a-dependency bloat.

@tomjaguarpaw

Copy link
Copy Markdown
Collaborator

you could have achieved the same size reduction by depending on extensions-0.1.0.1, couldn't you?

Correct.

OK, good. Let's say that's the supported workflow for now.

next time a release (or revision) must be made — the release process now must be replicated three times

Or users of older compilers could just stick to the older version? It's not like extensions is some critical ecosystem package that needs wide compatibility.

In principle I agree with you: wider versions bounds are better. I just don't see that it's worth the effort in this case.


your branch simply does not build under Cabal-syntax^>=3.14:

That is simply because you'd added 9.12 to the matrix — and I rebased, resolving conflicts.

It has nothing to do with the matrix or CI. Try

cabal repl -w ghc-9.6 --build-depends 'Cabal-syntax^>=3.14'

in your local checkout of your branch. It will fail.

@ulidtko

ulidtko commented Jun 21, 2025

Copy link
Copy Markdown
Author

It will fail.

Yes; because of breaking changes in Cabal 3.14 (which 9.12 ships in boot-libs by default).

See here haskell/cabal#10559

@tomjaguarpaw

Copy link
Copy Markdown
Collaborator

It has direct bearing. cabal-install depends on Cabal depends on Cabal-syntax.

But having a particular version of cabal-install installed in the CI system doesn't force packages built by that CI system to use that same version of the Cabal library.

I think Cabal just gives preference to already-installed versions (if they satisfy bounds, of course) — exactly to mitigate this multiple-versions-of-a-dependency bloat.

It doesn't. It installs newer versions of packages even if earlier versions are already installed, at least for non-boot packages. This is trivial to confirm: give it a go!

@tomjaguarpaw

tomjaguarpaw commented Jun 21, 2025

Copy link
Copy Markdown
Collaborator

It will fail.

Yes; because of breaking changes in Cabal 3.14 (which 9.12 ships in boot-libs by default).

OK, but that means that not only is your PR broken because it allows failing build configurations, it's also broken because the matrix doesn't pick that up. The matrix claims that 9.6.3 works with 3.14, but it doesn't: https://github.com/kowainik/extensions/actions/runs/15797159640/job/44530750661

@ulidtko

ulidtko commented Jun 21, 2025

Copy link
Copy Markdown
Author

Yes, the PR will need additional work to support 3.14, you're right.

@ulidtko

ulidtko commented Jun 21, 2025

Copy link
Copy Markdown
Author

But having a particular version of cabal-install installed in the CI system doesn't force packages built by that CI system to use that same version of the Cabal library.

This does:

--constraint 'Cabal-syntax installed' \

@tomjaguarpaw

Copy link
Copy Markdown
Collaborator

This does

It clearly doesn't because it doesn't pick up the build failure!


Since there's a simple workaround and the proposed solution introduces additional complexity into the source and the CI system which seems difficult to get correct I'm not interested in tackling this issue now. Thank you for bringing it to my attention and your work so far. I am willing to revisit the issue if a situation arises where there is no longer a simple workaround.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

extensions >= 0.1.0.1 not compatible with GHC 9.4.8

2 participants