Skip to content

Conversation

@MangoIV
Copy link
Collaborator

@MangoIV MangoIV commented Oct 1, 2025

Previously the executable name of the external command was passed to external commands as the first argument.

This behaviour was adapated from cargo which does this because of reasons that are internal to rust that do not affect GHC Haskell, and are even orthogonal to patterns that see common use in Haskell.

Additionally, it complicates the 'simple' case which is what we should optimize for when building such a feature.

The previous use case (one executable that serves multiple external subcommands) is still possible by the following means:

  • using a wrapper around the executable
  • using a symlink and check argv[0] in the executable

Survey of tools that can be affected by this change: #11232 (comment)


Include the following checklist in your PR:

@MangoIV MangoIV force-pushed the mangoiv/change-subcommands branch 2 times, most recently from 47ea2c6 to 50e1bfa Compare October 1, 2025 16:32
Copy link
Collaborator

@geekosaur geekosaur left a comment

Choose a reason for hiding this comment

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

There's an issue here which hasn't been brought up yet, which is one of the reasons the program name is passed as the first parameter.

See the caveat on getProgName. On Windows in particular, there is no way to retrieve argv[0], so anything relying on it will have issues.

This also plays into a potential future change where invocation via cabal would be marked. (Likely by something like how login on Unix marks login shells: leading '-'.) The reason we would want this is the potential for future versions of the external command mechanism to receive bits of project or cabal state via the environment. (We already pass $CABAL / %CABAL%, but tooling should already use that to ensure they are running the right cabal.) External commands would need some "key" indicating that the additional information is available. We don't do this yet because we're waiting for feedback from users of the external command mechanism about what information they would need from cabal.

All of which means I'm not sure we can get away with not passing the name. At minimum, we might need to replace it with a --run-from-cabal parameter or other mechanism to indicate that additional information is available, and potentially parameterized by the actual run name to work around getProgName issues on non-POSIX (which would put us right back where we are now).

@MangoIV
Copy link
Collaborator Author

MangoIV commented Oct 1, 2025

There are two possible workarounds listed, the other one (just using multiple executables) even aligns very nicely with how Haskell projects are usually structured and doesn't incur a lot of overhead. Does that one not work on windows either?

On the topic of getting more information: if you're setting an environment variable anyway, then why is it not possible to just check for the presence of that variable?

@geekosaur
Copy link
Collaborator

$CABAL itself isn't enough, as other tools already use it for this purpose. A separate envar might be.

If two tools share most of their implementation, multiple driver programs are possible but may lead to complex library entry points. This is why Unix likes to use (sym)links in that case, but that doesn't fly on Windows and probably other OSes that aren't Unix-like (there's ongoing work to port ghc and build tools to Haiku, for example).

@geekosaur
Copy link
Collaborator

geekosaur commented Oct 1, 2025

You haven't updated the external command tests (PackageTests/ExternalCommandExitCode/cabal.test.hs, PackageTests/ExternalCommand/cabal.test.hs, PackageTests/ExternalCommandHelp/cabal.test.hs).

@MangoIV
Copy link
Collaborator Author

MangoIV commented Oct 1, 2025

You haven't updated the external command test

I didn't see it failing locally, must've slipped through the one million lines of output

If two tools share most of their implementation, multiple driver programs are possible but may lead to complex library entry points.

That's why I said in Haskell, it is super common to make the executable just a shim that invokes the library. In my opinion we don't have too look too closely what other people are doing there because they have other backgrounds. The default case is that it's very simple to add new executables to Haskell projects and even if that's not a case, for that hypothetical project (I know of none that has this problem) it's not a hurdle that big that should force other users to take the hurdles that they have to take now. (Or making anything impossible)

$CABAL itself isn't enough, as other tools already use it for this purpose. A separate envar might be.

Yes, I propose not setting that variable and instead use a longer, more rarely used one like $CABAL_EXTERNAL_COMMAND_CONTEXT which at the same time would be good to avoid executables that currently long for $CABAL to break.

@geekosaur
Copy link
Collaborator

Yes, I propose not setting that variable and instead use a longer, more rarely used one like $CABAL_EXTERNAL_COMMAND_CONTEXT which at the same time would be good to avoid executables that currently long for $CABAL to break.

We still need to set it, because if a user runs a different cabal than the default on on $PATH (I do this somewhat regularly, for example) we need to inform the external command which one to use to e.g. query project information. $CABAL is the standard for this.

@Bodigrim
Copy link
Collaborator

Bodigrim commented Oct 1, 2025

We still need to set it, because if a user runs a different cabal than the default on on $PATH (I do this somewhat regularly, for example) we need to inform the external command which one to use to e.g. query project information. $CABAL is the standard for this.

I doubt it's ever wise to use $CABAL to query project information or similar. Why would anyone do it? Cabal CLI interface isn't stable, and certainly running a new process, checking its version and parsing the output is too much work, when you can simply use cabal-install as a library instead.

@geekosaur
Copy link
Collaborator

That may tie your program to a specific version. In the opposite direction, this is why I think we'll be exporting more information in the future. Also, future versions of cabal will have more stable interfaces for this, whose design will come from this and other users (e.g. HLS); we already have a ticket and some early design work for a proposed cabal status, and some work on extending cabal path.

@MangoIV
Copy link
Collaborator Author

MangoIV commented Oct 1, 2025

Do I understand this incorrectly though: I think this is orthogonal to the issue at hand - the additional argument wouldn't carry any information about the status of cabal, it would be the environment variable.

@geekosaur
Copy link
Collaborator

If the additional parameter were to stay, it would be the same as the argv[0] which isn't always accessible on non-Unix. It might instead be shifted into the environment.

@geekosaur
Copy link
Collaborator

Some care is needed with the environment, though; it's already pretty sizable on some systems, and there is a limit on argv+envp on anything other than FreeBSD.

@Bodigrim
Copy link
Collaborator

Bodigrim commented Oct 1, 2025

That may tie your program to a specific version.

Which is something under mine control as a developer, so it's good. While with $CABAL I have no idea what I'm dealing with: what is the version? was it patched somehow? can I execute it as a child process without hitting a lock of some sort? As a third-party developer, I'm very certain about my preference here. But we digress.

@mniip
Copy link

mniip commented Oct 1, 2025

There's an issue here which hasn't been brought up yet, which is one of the reasons the program name is passed as the first parameter.

See the caveat on getProgName. On Windows in particular, there is no way to retrieve argv[0], so anything relying on it will have issues.

I feel like you're severily misunderstanding the docs here. getProgName works on windows like a charm, in the sense that it outputs the filename of the program as it appeared in the first word in the CreateProcess invocation (i.e. pre symlink resolution).

Here's what it looks like:

PS C:\Users\mniip\Desktop\prog> cat .\GetProgName.hs
import System.Environment

main :: IO ()
main = getProgName >>= putStrLn
PS C:\Users\mniip\Desktop\prog> ghc .\GetProgName.hs -o cabal-getprogname.exe
[2 of 2] Linking cabal-getprogname.exe
PS C:\Users\mniip\Desktop\prog> New-Item -Path cabal-getprogname-symlink.exe -ItemType SymbolicLink -Value cabal-getprogname.exe


    Directory: C:\Users\mniip\Desktop\prog


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---l         10/1/2025  11:05 PM              0 cabal-getprogname-symlink.exe


PS C:\Users\mniip\Desktop\prog> fsutil hardlink create cabal-getprogname-hardlink.exe cabal-getprogname.exe
Hard link created for C:\Users\mniip\Desktop\prog\cabal-getprogname-hardlink.exe <<===>> C:\Users\mniip\Desktop\prog\cabal-getprogname.exe
PS C:\Users\mniip\Desktop\prog> cabal getprogname
cabal-getprogname.exe
PS C:\Users\mniip\Desktop\prog> cabal getprogname-symlink
cabal-getprogname-symlink.exe
PS C:\Users\mniip\Desktop\prog> cabal getprogname-hardlink
cabal-getprogname-hardlink.exe

@MangoIV MangoIV force-pushed the mangoiv/change-subcommands branch from 50e1bfa to 2fbabfe Compare October 1, 2025 21:19
@geekosaur
Copy link
Collaborator

I feel like you're severily misunderstanding the docs here. getProgName works on windows like a charm, in the sense that it outputs the filename of the program as it appeared in the first word in the CreateProcess invocation (i.e. pre symlink resolution).

It still leaves out cabal-specific changes to it to indicate that it has been run from cabal and extra information may be available (#11232 (review)); it only provides the executable name that was run.

@mniip
Copy link

mniip commented Oct 1, 2025

It still leaves out cabal-specific changes to it to indicate that it has been run from cabal and extra information may be available (#11232 (review)); it only provides the executable name that was run.

Yes. This is a completely orthogonal issue though to whether cabal copies the command name in argv[1] or not. This PR isn't about making the perfect cabal external commands system, it's about changing that one specific aspect.

@MangoIV MangoIV force-pushed the mangoiv/change-subcommands branch from 2fbabfe to cdd2f7a Compare October 1, 2025 21:40
@ulysses4ever
Copy link
Collaborator

Thank you for submitting the patch @MangoIV! I agree with your arguments and I wasn't able to discern serious counter-arguments from the discussion above. So I'm in support of the change.

Why is it a draft?

@MangoIV
Copy link
Collaborator Author

MangoIV commented Oct 1, 2025

Why is it a draft?

There's two missing checkboxes that I haven't gone through yet (I want to adjust missing docs)

@geekosaur
Copy link
Collaborator

You still also have a typo in your changelog entry and two tests that still need to be fixed (I saw only one fixed in the last push).

@MangoIV
Copy link
Collaborator Author

MangoIV commented Oct 1, 2025

yeah that's also why it's still draft, sorry. I will of course go through this. :)

@ulysses4ever
Copy link
Collaborator

No worries, I was just making sure it's not an oversight.

@MangoIV
Copy link
Collaborator Author

MangoIV commented Oct 2, 2025

As per request of @mpickering I have announced this change on discourse

https://discourse.haskell.org/t/cabal-install-external-command-feature-without-passing-the-subcommands-name-as-argv-1/13062

in order for users to be able to chime in if they expect breakage.

@MangoIV MangoIV force-pushed the mangoiv/change-subcommands branch from cdd2f7a to 9280ae0 Compare October 22, 2025 09:36
@MangoIV
Copy link
Collaborator Author

MangoIV commented Oct 23, 2025

I would like to note that validate.sh does not reproduce the error, which is really weird.

@MangoIV MangoIV force-pushed the mangoiv/change-subcommands branch 3 times, most recently from 041ad3a to 017a12e Compare October 23, 2025 10:47
Previously the executable name of the external command was passed to external commands as the
first argument.

This behaviour was adapated from cargo which does this because of reasons that are internal
to rust that do not affect GHC Haskell, and are even orthogonal to patterns that see common use in
Haskell.

Additionally, it complicates the 'simple' case which is what we should optimize for when building
such a feature.

The previous use case (one executable that serves multiple external subcommands) is still possible
by the following means:
- using a wrapper around the executable
- using a symlink and check argv[0] in the executable

Additionally, the variable `$CABAL` that was set by `cabal-install` was renamed to `CABAL_EXTERNAL_CABAL_PATH`. This has two reasons:
1. it makes migration easier for users of the external command feature that were previously expecting the name of the executable
   to appear in `argv[1]`
2. it does not unnecessarily pollute the environment variable namespace as it turns out some other tools have been and are already
   using this name, historically

Resolves haskell#10275
@mergify
Copy link
Contributor

mergify bot commented Nov 9, 2025

rebase

✅ Branch has been successfully rebased

@ulysses4ever ulysses4ever force-pushed the mangoiv/change-subcommands branch from fb6aa24 to d8ac2bf Compare November 9, 2025 14:33
@ulysses4ever
Copy link
Collaborator

given the extensive discussion, it doesn't look like we're reaching consensus in time before 3.16.1.0 release.

@geekosaur can I ask you to state your proposal for a deprecation strategy? With as much details as possible, since I wasn't quite able to follow before and it seems I'm not the only one.

@Mikolaj I think the answer about the change in the variable name is reasonable: as I understand it, this change allows tools to handle both old cabals (before th change in the argument passing) and new ones (after the change).

@geekosaur
Copy link
Collaborator

geekosaur commented Nov 9, 2025

That's the idea, yes.

Unfortunately, I've realized there's a kink that makes my plan unworkable: since ghc 9.14 is intended to be an LTS release, we'll want cabal 3.16 to be an LTS release as well — but that means we'll be stuck with using $CABAL until the next ghc LTS release (I don't think changing it in a minor release will fly, even though it's strictly speaking not a PVP change since the Haskell API won't change).

For the record, my plan had been:

  1. Use both $CABAL and $CABAL_EXTERNAL_CABAL_CMD (or whatever it's called) for 3.16, then remove use of $CABAL in 3.18.
  2. Document this and the deprecation in the user manual and release notes.

(This could have been implemented as changing it for #1 in this PR, then a second PR that wouldn't be backported on master implementing #2.)

Unless someone thinks changing it in a minor LTS release after 6 months or something is viable, that means I have to concede and hope it doesn't trip anyone up. I still don't like it much, but we're running pretty short on alternatives.

@MangoIV
Copy link
Collaborator Author

MangoIV commented Nov 9, 2025

Maybe I am misunderstanding but I think this can’t work.

If an old binary is looking for $CABAL, it mustn’t be set because otherwise the binary would assume old behaviour. Since the new behaviour is a no op for old binaries that only serve a single executable name, not setting $CABAL it is strictly better than setting both.

@MangoIV
Copy link
Collaborator Author

MangoIV commented Nov 9, 2025

The inherent problem is that cabal-install cannot detect (without horrible, horrible hacks, yes it could work but i wouldn’t even consider it an option) which behaviour the callee expects - that is the source of problems for a good migration path.

@geekosaur
Copy link
Collaborator

The problem you're flatly ignoring is the possibility that someone is currently relying on the current behavior.

@MangoIV
Copy link
Collaborator Author

MangoIV commented Nov 9, 2025

you're flatly ignoring

Im not sure what gives you the impression I’m ignoring it. If i did this wouldn’t even be a breaking change.

The only situation where this could be problematic is if someone would actually be detecting the exe name via argv[1] (and not just use it as a marker that it was called from cabal-install) in which case

  • I have made an announcement
  • Made sure that popular, open source tools, are not affected by it.

@geekosaur
Copy link
Collaborator

I'm not, and have never been, debating that part: apps already have to deal with that if they're run standalone instead of via cabal. Just the environment variable change.

@geekosaur
Copy link
Collaborator

Also I suspect they'll need to deal with it for a couple years even after this goes in, because we have no control over Linux distributions that package cabal, nor over the many users who insist on only using things packaged by their distro.

@MangoIV
Copy link
Collaborator Author

MangoIV commented Nov 9, 2025

exe (ordered by usefulness, most useful top, least useful at the bottom) old behavior new behaviour without variable name change new behaviour with variable name change (implemented)
old exe, no detection doesn't work works works
one old exe, detection of whether it has been called by cabal-install via $CABAL works doesn't work (assumes old behaviour because $CABAL is set) works (no $CABAL, no problems, just works, like above
multiple old exe, detection of exe name via $CABAL works doesn't work doesn't work (this is the breaking change)
new exe works doesn't work (no way to distinguish new and old behaviour) works
exe that abuses the $CABAL env var to get the cabal-install executable name for other usecases doesn't work across all cabal versions anyway, might lead to misdetection because the variable name is common, etc. doesn't work (good) doesn't work (technically a breaking change, but a good one if you ask me)

Mind that the really important bits are that the first two rows work across the board, the rest are just corner cases, which are broken but

  1. that's been announced
  2. this can't be prevented without messing with the first two rows which, again, are the really important bits to get right.

@ulysses4ever
Copy link
Collaborator

@sol as one of the early adopters of the external-commands interface, do you have an opinion about this change and a deprecation strategy?

@sol
Copy link
Member

sol commented Nov 10, 2025

@ulysses4ever thanks for looping me in.

My main issue here is that this PR does not state a clear motivation for this change.

So I am left guessing.

@MangoIV is it so that you can run e.g. cabal-doctest instead of cabal doctest? Can you please update the PR description accordingly?

It dazzles me why I would put so much discussion into a proposed breaking change without stating a convincing user benefit.

@geekosaur
Copy link
Collaborator

This doesn't benefit users at all; it benefits authors of external tools intended to work with cabal's external command interface, who won't need to deal with a weird requirement of the original version that was taken from cargo when their reason for doing it isn't applicable to us.

@sol
Copy link
Member

sol commented Nov 10, 2025

it benefits authors of external tools

If this change means that I as a tool author have to replace

  lookupEnv "CABAL" >>= \ case
    Nothing -> run "cabal" args
    Just cabal -> run cabal (drop 1 args)

with

  lookupEnv "CABAL_EXTERNAL_CABAL_PATH" >>= \ case
    Just cabal -> run cabal args
    Nothing -> lookupEnv "CABAL" >>= \ case
      Just cabal -> run cabal (drop 1 args)
      Nothing -> run "cabal" args

then I don't see how I as a tool author benefit from this change.

If there is anything else to it, then again, I think this PR lacks proper motivation.

@geekosaur
Copy link
Collaborator

The problem with $CABAL is that it's already in use by a number of other tools, sometimes incompatibly from how the current external commands interface uses it. So we want to migrate away from using it ourselves.

@geekosaur
Copy link
Collaborator

(In fact, I thought that would have been clear from the discussion earlier in this PR, because that's central to why I'm worried about backward compatibility issues.)

@MangoIV
Copy link
Collaborator Author

MangoIV commented Nov 10, 2025

@sol the motivation is to remove the currently complicated "simple" case. As you have already showcased, a tool must currently look up the variable and then drop the first argument. With this change, this is not required anymore. Any tool that provides only one subcommand per executable will work without further changes, as is the case with e.g. git subcommands.

More information can be found in the commit message, in the discourse announcement or the original issue.

Copy link
Member

@Mikolaj Mikolaj left a comment

Choose a reason for hiding this comment

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

I think it makes sense to provide rationale in the issue ticket that a PR ticket implements and indeed the issue ticket mentioned in the description, and the ticket it links to, do provide it IMHO. Thank you again for the PR, the discussion and for the table detailing the compatibility situation. It's now clear to me why this PR changes the variable name and why no more backward compatibility measures are attempted.

@Mikolaj
Copy link
Member

Mikolaj commented Nov 10, 2025

@MangoIV: please kindly set the merge label. This still gives us two days to propose amendments to this PR.

@sol: would you have any advice on how to make this PR better?

@geekosaur: from you PR approval I take it you are fine (if not completely happy) to merge this PR as is? Even for release 3.16.1.0?

@MangoIV MangoIV added the merge me Tell Mergify Bot to merge label Nov 10, 2025
@mergify mergify bot added the ready and waiting Mergify is waiting out the cooldown period label Nov 10, 2025
@geekosaur
Copy link
Collaborator

I'm kinda treating 3.16.1.0 as what 3.16.0.0 should have been. (I still think that should have been treated as a prerelease.)

I'm still worried about removing $CABAL without deprecating it (and "technically a breaking change, but a good one if you ask me" is a dangerous attitude for a major build tool, IMO). But it seems everyone else is fine with it, so I'm basically stepping away from the whole thing.

@MangoIV
Copy link
Collaborator Author

MangoIV commented Nov 10, 2025

I don’t wanna be too snarky but I wonder where the caution was when the original behaviour was approved with no possibility of migration to a different behaviour without creating a breaking change like this.

I don’t want anyone to „give in“. If the maintainers think this is not workable and they want to keep the behaviour because major tools cannot create breaking changes like this, then so be it.

It is hard for me to navigate this situation. It feels like the narrative is a bit like „let him do the change and let him see how it’ll blow up in their face“, and I am not here for that. I want to make an improvement to the actual user experience. If this change does not satisfy this expectation, or the costs are considered too high, I can very well accept a rejection of the change.

Mind though since there is no and will not be a proper way for deprecation and migration, that the longer this behaviour stays as is, the bigger the problems with a future change will be.

@geekosaur
Copy link
Collaborator

What exactly would you have recommended as an alternative? There aren't many useful possibilities, and use of environment variables can't reasonably be tracked.

@geekosaur
Copy link
Collaborator

Also, "I don’t wanna be too snarky but I wonder where the caution was when the original behavior was approved" is exactly snarky, and honestly not thinking. A new feature was added, a mistake was made in its design, "caution" here would have meant "time travel to the future to see the outcome".

@MangoIV
Copy link
Collaborator Author

MangoIV commented Nov 10, 2025

is exactly snarky

yes, this was inadequate. My point is that when a feature is added that will be cemented with no migration path forever, an attempt to fix it is easily rejected because the cost is high, but the cost is not caused by the fix but by the addition of the feature.

@Bodigrim
Copy link
Collaborator

@MangoIV: please kindly set the merge label. This still gives us two days to propose amendments to this PR.

There are unresolved comments by @ulysses4ever above, so Mergify will not pick it up.
(Is this explained in CONTRIBUTING.md? I cannot find it there)


On a meta level this PR highlights why people are reluctant to contribute to Cabal: the procedure is vague, protracted and hindered by obscure Mergify bureaucracy. It's clear that @MangoIV struggles to navigate it, and anyone else will too.

Could the Cabal team possibly reflect on this at their next meeting and extend CONTRIBUTING.md with a precise algorithm to get things merged? Are two approvals a sufficient condition? Whose approvals count? Because there are loads of people with green approval tick here, me included. If I got two approvals, can I resolve all remaining comments from other participants with "won't do"?

(To be clear: I'm not suggesting that the Cabal team has to apply more or less scrutiny when reviewing PRs, I'd just prefer their desired level of scrutiny to be written down somewhere)

@MangoIV
Copy link
Collaborator Author

MangoIV commented Nov 10, 2025

Fwiw I was going to apply the suggested fixes by @ulysses4ever tomorrow, sorry for not acting on them sooner.

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

Labels

merge me Tell Mergify Bot to merge ready and waiting Mergify is waiting out the cooldown period

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants