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

Per-cabal-file constraints #8912

Open
michaelpj opened this issue Apr 20, 2023 · 12 comments
Open

Per-cabal-file constraints #8912

michaelpj opened this issue Apr 20, 2023 · 12 comments

Comments

@michaelpj
Copy link
Collaborator

It is possible to have a lot of components in a package. This is especially true once you start to make use of support for multiple public libraries, since this allows merging what have in the past been forced to be separate packages.

However, you then end up in this situation:

library foo
   build-depends: base ^>= 4.17
   ...

library bar
   build-depends: base ^>= 4.17
   ...

library baz
   build-depends: base ^>= 4.17
   ...

etc.

If you stare hard at your dependencies, you can sometimes cut this down by finding a single component that the others depend on, and then only putting the constraint there. But this is manual and error-prone. And duplicating the constraints everywhere is also manual and tedious.

One workaround is to use lots of common stanzas:

common base                           { build-depends: base                           >= 4.7        && < 5      }

and include it everywhere. See cabal-cache for an example in the wild. This is... okay-ish, but still a bit clumsy.

What I would like to be able to say is:

  • foo, bar, and baz depend on base
  • All things that depend on base in this cabal file have the following constraint on it, i.e. something similar to the constraints stanza in a cabal.project file

That seems to me to be a somewhat viable proposal: allow a constraints stanza in .cabal files, behaving just the same as in cabal.project files. I think that would be fairly intuitive and pretty useful.

@michaelpj michaelpj changed the title Per-cabal file constraints Per-cabal-file constraints Apr 20, 2023
@newhoggy
Copy link

All things that depend on base in this cabal file have the following constraint on it, i.e. something similar to the constraints stanza in a cabal.project file

Should this also include transitive dependencies? Like if component foo has a direct dependecy on bar and bar depends on baz the constraint should be able to apply to baz without making baz a direct dependency of foo.

@michaelpj
Copy link
Collaborator Author

Should this also include transitive dependencies?

I was thinking it would be a top-level stanza, so:

constraints: base ^>= 4.18

library foo
   build-depends: base

library bar
   build-depends: base

so it applies to everything in the cabal file. I think that does what you want?

@dcoutts
Copy link
Contributor

dcoutts commented Apr 21, 2023

It could only apply to dependencies that are listed for components in the package, so that it's still equivalent to the existing build-depends. So they're not constraints in the same way as they are in cabal.project files (which can be global).

IIRC, internally it's the difference between a constraint of the form "foo >= x.y" vs "bar => (foo >= x.y)". The latter says you only get the constraint if you pick bar.

@michaelpj
Copy link
Collaborator Author

It could only apply to dependencies that are listed for components in the package, so that it's still equivalent to the existing build-depends. So they're not constraints in the same way as they are in cabal.project files (which can be global).

Yes, that's what I'd want. Tricky.

Example:

constraints: x > 5

library foo
   build-depends: x

library bar
   build-depends: y

If we only depend on bar that shouldn't incur the constraint on x!

So I guess they're a bit less like the constraints in a project file, which really are globally applied. I think the behaviour suggested is still reasonable, but perhaps a different name would be better? 🤔

@jasagredo
Copy link
Collaborator

jasagredo commented Nov 20, 2023

Wouldn't it be reasonable to have the following?

default-constraints:
  base ^>= 4.18

library
  build-depends: base

library a
  build-depends: base 

library b
  build-depends: base ^>=4.19

With the intended meaning that "any base bounds not specified are defaulted by the default-constraints stanza". So in this case this would be equivalent to:

library
  build-depends: base ^>=4.18

library a
  build-depends: base ^>=4.18

library b
  build-depends: base ^>=4.19

That would be a "simple" sed-like. If no bounds are specified and there are default bounds, apply them.

It would also have no effect in those "transitivity" problems you mention. Just pure syntactic sugar

@michaelpj
Copy link
Collaborator Author

I note that if we had a per-stanza version of "constraint this package, but only if it appears in the build-depends" then we don't need the per-file version, because at that point common stanzas do the job, i.e.

common global-constraints { conditional-constraints: x < 2 }

library a
   import global-constraints
   build-depends: x

library b
   import global-constraints
   build-depends: y 

IIRC, internally it's the difference between a constraint of the form "foo >= x.y" vs "bar => (foo >= x.y)". The latter says you only get the constraint if you pick bar.

Is it possible to have a constraint of the form x => x <2, i.e. "if you pick x, then here is a constraint on it"? If so, that seems like it would actually fit what we want here.

@michaelpj
Copy link
Collaborator Author

Actually I take it back: x => x < 2 is no better than x < 2:

  • If you were not going to pick x then the former does nothing whereas the latter is trivially satisfiable.
  • If you were going to pick x, then both apply anyway.

@Mikolaj
Copy link
Member

Mikolaj commented Nov 21, 2023

I'm too lazy to check --- what happens if I add x < 2 to a component that doesn't have x? Why don't we do that in the common options and import them everywhere? Is the subtle difference that the x < 2 may still apply to transitive deps? Is that important for your use case?

@jasagredo
Copy link
Collaborator

If a component doesn't depend on x but you provide x in a common stanza, then it will complain about unused packages:

❯ cat blah.cabal
cabal-version:      3.0
name:               blah
version:            0.1.0.0

common depends
  build-depends:
    , base ^>=4.18.1.0
    , generics-sop >=0.5

library
    import:           depends
    exposed-modules:  MyLib
    build-depends:    base
    hs-source-dirs:   src
    default-language: Haskell2010

❯ cabal build --ghc-options=-Wall --ghc-options=-Wunused-packages
Resolving dependencies...
Build profile: -w ghc-9.6.3 -O1
In order, the following will be built (use -v for more details):
 - blah-0.1.0.0 (lib) (configuration changed)
Configuring library for blah-0.1.0.0..
Preprocessing library for blah-0.1.0.0..
Building library for blah-0.1.0.0..

<no location info>: warning: [GHC-42258] [-Wunused-packages]
    The following packages were specified via -package or -package-id flags,
    but were not needed for compilation:
      - generics-sop-0.5.1.4 (exposed by flag -package-id generics-sop-0.5.1.4-aa29b3088ce1b1ad5c8d3cfca442dcad66c43dba5292d0316027a653eb18954a)

@jasagredo
Copy link
Collaborator

jasagredo commented Nov 22, 2023

For example, cabal-cache takes the common stanzas approach to its ultimate consequences, see the cabal file.

This could be rewritten as:

cabal-version:          3.11

name:                   cabal-cache
version:                1.1.0.0
synopsis:               CI Assistant for Haskell projects
description:            CI Assistant for Haskell projects.  Implements package caching.
homepage:               https://github.com/haskell-works/cabal-cache
license:                BSD-3-Clause
license-file:           LICENSE
author:                 John Ky
maintainer:             newhoggy@gmail.com
copyright:              John Ky 2019-2023
category:               Development
tested-with:            GHC == 9.4.6, GHC == 9.2.8, GHC == 8.10.7
extra-source-files:     README.md

source-repository head
  type: git
  location: https://github.com/haskell-works/cabal-cache

default-constraints:
  , base                  >=4.7      && <5
  , aeson                 >=1.4.2.0  && <2.2
  , amazonka              >=2        && <3
  , amazonka-core         >=2        && <3
  , amazonka-s3           >=2        && <3
  , attoparsec            >=0.14     && <0.15
  , bytestring            >=0.10.8.2 && <0.12
  , cabal-install-parsers >=0.6.1    && <0.7
  , conduit-extra         >=1.3.1.1  && <1.4
  , containers            >=0.6.0.1  && <0.7
  , cryptonite            >=0.25     && <1
  , deepseq               >=1.4.4.0  && <1.5
  , directory             >=1.3.3.0  && <1.4
  , exceptions            >=0.10.1   && <0.11
  , filepath              >=1.3      && <1.5
  , generic-lens          >=1.1.0.0  && <2.3
  , Glob                  >=0.10.2   && <0.11
  , hedgehog              >=1.0      && <1.3
  , hedgehog-extras       >=0.4      && <0.5
  , hspec                 >=2.4      && <3
  , http-client           >=0.5.14   && <0.8
  , http-client-tls       >=0.3      && <0.4
  , http-types            >=0.12.3   && <0.13
  , hw-hedgehog           >=0.1.0.3  && <0.2
  , hw-hspec-hedgehog     >=0.1.0.4  && <0.2
  , lens                  >=4.17     && <6
  , mtl                   >=2.2.2    && <2.4
  , network-uri           >=2.6.4.1  && <2.8
  , oops                  >=0.2      && <0.3
  , optparse-applicative  >=0.14     && <0.18
  , process               >=1.6.5.0  && <1.7
  , raw-strings-qq        >=1.1      && <2
  , relation              >=0.5      && <0.6
  , resourcet             >=1.2.2    && <1.4
  , selective             >=0.1.0    && <0.6
  , stm                   >=2.5.0.0  && <3
  , stringsearch          >=0.3.6.6  && <0.4
  , tar                   >=0.5.1.0  && <0.6
  , temporary             >=1.3      && <1.4
  , text                  >=1.2.3.1  && <2.1
  , time                  >=1.4      && <1.13
  , topograph             >=1        && <2
  , transformers          >=0.5.6.2  && <0.7
  , unliftio              >=0.2.10   && <0.3
  , zlib                  >=0.6.2    && <0.7

common project-config
  default-language:     Haskell2010
  default-extensions:   BlockArguments
                        DataKinds
                        FlexibleContexts
                        LambdaCase
                        MonoLocalBinds
                        TypeOperators
  ghc-options:          -Wall
                        -Wincomplete-record-updates
                        -Wincomplete-uni-patterns
                        -Wtabs

  if impl(ghc >= 8.10.1)
    ghc-options:        -Wunused-packages

library
  import:               project-config
  build-depends:
     , base
     , project-config
     , aeson
     , amazonka
     , amazonka-core
     , amazonka-s3
     , attoparsec
     , bytestring
     , conduit-extra
     , containers
     , cryptonite
     , deepseq
     , directory
     , exceptions
     , filepath
     , generic-lens
     , http-client
     , http-client-tls
     , http-types
     , lens
     , mtl
     , network-uri
     , oops
     , optparse-applicative
     , process
     , relation
     , resourcet
     , stm
     , text
     , topograph
     , transformers
  other-modules:        Paths_cabal_cache
  autogen-modules:      Paths_cabal_cache
  hs-source-dirs:       src
  exposed-modules:      HaskellWorks.CabalCache.AppError
                        HaskellWorks.CabalCache.AWS.Env
                        HaskellWorks.CabalCache.AWS.Error
                        HaskellWorks.CabalCache.AWS.S3
                        HaskellWorks.CabalCache.AWS.S3.URI
                        HaskellWorks.CabalCache.Concurrent.DownloadQueue
                        HaskellWorks.CabalCache.Concurrent.Fork
                        HaskellWorks.CabalCache.Concurrent.Type
                        HaskellWorks.CabalCache.Core
                        HaskellWorks.CabalCache.Data.List
                        HaskellWorks.CabalCache.Error
                        HaskellWorks.CabalCache.GhcPkg
                        HaskellWorks.CabalCache.Hash
                        HaskellWorks.CabalCache.IO.Console
                        HaskellWorks.CabalCache.IO.File
                        HaskellWorks.CabalCache.IO.Lazy
                        HaskellWorks.CabalCache.IO.Tar
                        HaskellWorks.CabalCache.Location
                        HaskellWorks.CabalCache.Metadata
                        HaskellWorks.CabalCache.Options
                        HaskellWorks.CabalCache.Show
                        HaskellWorks.CabalCache.Store
                        HaskellWorks.CabalCache.Text
                        HaskellWorks.CabalCache.Topology
                        HaskellWorks.CabalCache.Types
                        HaskellWorks.CabalCache.URI
                        HaskellWorks.CabalCache.Version

executable cabal-cache
  import:               project-config
  build-depends:
    , base
    , aeson
    , amazonka
    , amazonka-core
    , bytestring
    , cabal-cache
    , cabal-install-parsers
    , containers
    , directory
    , exceptions
    , filepath
    , generic-lens
    , lens
    , mtl
    , network-uri
    , oops
    , optparse-applicative
    , resourcet
    , stm
    , stringsearch
    , temporary
    , text
    , unliftio
  main-is:              Main.hs
  hs-source-dirs:       app
  other-modules:        App.Commands
                        App.Commands.Options.Parser
                        App.Commands.Debug
                        App.Commands.Debug.S3
                        App.Commands.Debug.S3.Cp
                        App.Commands.Options.Types
                        App.Commands.Plan
                        App.Commands.SyncFromArchive
                        App.Commands.SyncToArchive
                        App.Commands.Version
                        App.Static
                        App.Static.Base
                        App.Static.Posix
                        App.Static.Windows
                        Paths_cabal_cache
  autogen-modules:      Paths_cabal_cache
  ghc-options:          -threaded -rtsopts -with-rtsopts=-N

test-suite cabal-cache-test
  import:               project-config
  build-depends:
    , base
    , aeson
    , amazonka
    , bytestring
    , cabal-cache
    , directory
    , exceptions
    , filepath
    , Glob
    , hedgehog-extras
    , hedgehog
    , hspec
    , http-types
    , hw-hspec-hedgehog
    , lens
    , mtl
    , network-uri
    , oops
    , raw-strings-qq
    , text
    , time

  type:                 exitcode-stdio-1.0
  main-is:              Spec.hs
  hs-source-dirs:       test
  ghc-options:          -threaded -rtsopts -with-rtsopts=-N
  build-tool-depends:   hspec-discover:hspec-discover
  other-modules:        HaskellWorks.CabalCache.AwsSpec
                        HaskellWorks.CabalCache.IntegrationSpec
                        HaskellWorks.CabalCache.LocationSpec
                        HaskellWorks.CabalCache.QuerySpec
                        Test.Base

Which I think is much more informative, namely the dependencies still appear in the build-depends: field and not in import, mixed with project configurations.

@michaelpj
Copy link
Collaborator Author

Adding a package to build-depends is semantically quite different. It'll get passed to GHC for compilation, which means we get the unused packages warning and also I think it'll get linked in to the build products! All of which is unnecessary - what we want here is really an instruction to the cabal solver only.

@Mikolaj
Copy link
Member

Mikolaj commented Nov 22, 2023

I got convinced. It also solves the problem "I have many components with the same deps. In which component do I specify the bounds? Or do I copy paste to all?".

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

No branches or pull requests

6 participants