Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] default-package-bounds #9569

Open
jasagredo opened this issue Dec 27, 2023 · 12 comments
Open

[RFC] default-package-bounds #9569

jasagredo opened this issue Dec 27, 2023 · 12 comments
Labels

Comments

@jasagredo
Copy link
Collaborator

jasagredo commented Dec 27, 2023

Obsoletes #9477

Problem this solves

When describing a package in a .cabal file, multiple components can declare the same dependency in their build-depends sections. There can be two flavors of this:

  1. Components actually depend on different versions of the dependency, for example if we are using one version of aeson in one test-suite and a different version in another, in order to test backwards compatibility always.
  2. Components depend on the same version of the dependency.

It is to case (2) that we turn our attention in this RFC. On the one hand it feels redundant and error-prone to repeat the version bound in all components. On the other hand, in order to make sure you are building with the same version regardless of what subset of those components is enabled one has to perform a topological-sort of the components and assign the version bound in the bottom-most component.

A different solution is to use common stanzas as done by some packages but this leads to a weird syntax, where the import section of a component is importing both configurations and build-depends sections.

Proposed solution

Implement a new section in the .cabal package file named default-package-bounds which contains a list of dependencies in the same format as a build-depends.

For each of the unqualified dependencies in the default-package-bounds, whenever a dependency on the same package appears in a build-depends section of a component without a specified version bound, the version bound from default-package-bounds will be used. For each qualified dependencies in the default-package-bounds, whenever a dependency on the same package appears in a build-tool-depends section of a component without a specified version bound, the version bound from default-package-bounds will be used.

In particular default-package-bounds: foo >2 will be applied to both build-depends: foo, foo:bar, and default-package-bounds: foo:baz >2 will be applied to build-tool-depends: foo:baz.

For specific cases like (1) above, the user should not include such a special dependency in these default-package-bounds section.

This will be just a syntactic sugar and will have no influence in:

  • transitive dependencies (as opposed to constraints in cabal.project)
  • pkgconfig-depends

Backwards compatibility

This change would be fully backwards-compatible as there existed no field with this name before and omitting it would work the same way as before this change, i.e. it is an optional field.

@hasufell
Copy link
Member

hasufell commented Sep 1, 2024

What is wrong with common stanza exactly? Do we really need another feature doing the same thing?

@jasagredo
Copy link
Collaborator Author

jasagredo commented Sep 2, 2024

I find it quite confusing if the dependencies are declared in common stanzas.

Also in that way of doing things, the naive action of adding another dependency to build-depends will result in a "common bound" not being used (as one should have used a common stanza instead), but with the default-package-bounds the naive action will imply the "common bound" already.

It can be seen as as wrong-by-naive vs right-by-naive decision.

For example tools that add new dependencies from the cli (I think it was named cabal-add?) would do the right thing in this case.

@hasufell
Copy link
Member

hasufell commented Sep 2, 2024

Cabal is a spec.

I find that we are increasingly mudding the boundaries between good spec and good user interface.

Common stanzas are the pattern for code reuse.

If we're thinking purely in terms of user experience, we have to try harder first:

  • provide cli interfaces that do the right thing, so users don't hand-edit the cabal file all the time
  • provide some sort of syntax sugar and then figure out how/when we desugar (that was also discussed regarding the module wildcards)

I think adding more ad-hoc features is going to degrade the spec.

@Bodigrim
Copy link
Collaborator

Bodigrim commented Sep 2, 2024

For each of the unqualified dependencies in the default-package-bounds, whenever a dependency on the same package appears in a build-depends section of a component without a specified version bound, the version bound from default-package-bounds will be used. For each qualified dependencies in the default-package-bounds, whenever a dependency on the same package appears in a build-tool-depends section of a component without a specified version bound, the version bound from default-package-bounds will be used.

I would not have guessed that

default-package-bounds
  build-depends:
    text > 5

library d 
  build-depends: text >4.5

is equivalent to

library d 
  build-depends: text >4.5

and not to

library d 
  build-depends: text >4.5 && >5

Cabal usually treats build-depends as a semigroup under (&&); deviating from this pattern feels very confusing to me.

On the other hand, in order to make sure you are building with the same version regardless of what subset of those components is enabled one has to perform a topological-sort of the components and assign the version bound in the bottom-most component.

There is already an error when a build plan for test executable differs from a build plant for a library. You need to override it manually (with --enable-tests) to proceed, it is very explicit.

@jasagredo
Copy link
Collaborator Author

@Bodigrim said:

There is already an error when a build plan for test executable differs from a build plant for a library. You need to override it manually (with --enable-tests) to proceed, it is very explicit.

I don't think it is unreasonable to have for example two benchmarks, one with some version of say aeson, and a different benchmark with some other version. In general I think there is freedom in using different bounds for different components.

Although you say that "there is already an error if the build plan differs from the build plan of the library", cabal does not prevent such an error in any way.

This feature (default-package-bounds) would be unnecessary if for example Cabal warned the user if bounds changed from enabling tests or not, but that is not the case.

@jasagredo
Copy link
Collaborator Author

jasagredo commented Sep 2, 2024

Cabal usually treats build-depends as a semigroup under (&&); deviating from this pattern feels very confusing to me.

That is a very valid concern and the matter is perfectly up for debate. I could change the implementation to work this way and I also don't think it is unreasonable.

@Bodigrim
Copy link
Collaborator

Bodigrim commented Sep 2, 2024

This feature (default-package-bounds) would be unnecessary if for example Cabal warned the user if bounds changed from enabling tests or not, but that is not the case.

if cabal build and cabal test entail different build plans, Cabal warns you and asks to configure the package explicitly with --enable-tests. Once you pass --enable-tests (or put tests: True in cabal.project), Cabal uses dependency bounds from tests even if you execute only cabal build. Or am I dreaming?..

@jasagredo
Copy link
Collaborator Author

if cabal build and cabal test entail different build plans, Cabal warns you and asks to configure the package explicitly with --enable-tests. Once you pass --enable-tests (or put tests: True in cabal.project), Cabal uses dependency bounds from tests even if you execute only cabal build. Or am I dreaming?..

That is not my understanding. Cabal says the following:

➜ cabal test --dry-run
Error: [Cabal-7043]
Cannot test the package aa-0.1.0.0 because none of the components are available to
build: the test suite 'aa-test' is not available because the solver picked a plan 
that does not include the test suites, perhaps because no such plan exists. To see 
the error message explaining the problems with such plans, force the solver to 
include the test suites for all packages, by adding the line 'tests: True' to 
the 'cabal.project.local' file.

But if I provide --enable-tests then it will accept cabal test. The error message says that the test-suite was not in the build plan, it doesn't mention or allude to the fact that versions of dependencies in the build plan might be different depending on whether the tests are enabled or not.

I recreated de above with cabal init --lib --simple with a test-suite, and then adding aeson (which will pick 2.2.3.0) to the library and aeson <= 2.2.0.0 to the test-suite.

@geekosaur
Copy link
Collaborator

geekosaur commented Sep 2, 2024

That's just a lousy error message: --enable-tests is the same as tests: True in the project file, as @Bodigrim said (in the part you quoted, even!), and without enabling the test suites they are not considered by the solver when constructing a build plan. You can find a bunch of issues from people who've been confused by this in the past.

@Bodigrim
Copy link
Collaborator

Bodigrim commented Sep 2, 2024

The error message is not very clear, but the key point is "force the solver to include the test suites for all packages" = "use package bounds from test suites even when building the main library". See #5079 (comment) and #7883 (comment)

@jasagredo
Copy link
Collaborator Author

I think the nature of my issue is slightly different to those you are linking.

My concern is that there is no warning that enabling the tests changes versions of the dependencies in the build plan. If I upgrade my library to use a newer dependency but not my test-suite, I will always test the older version when I enable the tests.

Yes, there is a warning that the build plan doesn't include the tests and that you have to explicitly enable them, but that doesn't warn you of the scenario above.

In my example on the other comment I might think "ah my library supports aeson 2.2.3.0" and then a user comes and says "but it fails for me with aeson 2.2.3.0!" and my immediate thought is "that's weird, all my tests pass", but inadvertently my tests were forcing me to use 2.2.0.0. There is no current way to prevent this.

Default-package-bounds makes this an opt-in. Unless you specify a bound manually, the same bound as the library will be picked if you enable the tests, always.

@andreasabel
Copy link
Member

I agree that default-package-bounds would be slightly more convenient to use than implementing the sharing of bounds with common and include.

However, I wonder whether it is worth adding and maintaining a new feature for that bit of extra convenience.
The cabal specification language is already a bit bloated, imo.

If one wants more convenience, one could also use a wizard like hpack (although it does not have the exact feature described here---it has top-level dependencies sections, which are somewhat similar.).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants