haskellcabalarchlinuxxmonadghcup

How to manage XMonad (and xmobar and anything else related to it) via GHCup+cabal?


It turns out it was enough to execute

cabal install --package-env=$HOME/.config/xmonad --lib base xmonad xmonad-contrib

but what I don't understand is the following:

I think they all boil down to the some point: my misunderstanding of GHCup and the Haskell ecosystem in general.


Orignal question

When I started using XMonad, I had a hard time just running it, the reason being that I had installed via pacman like any other program, as I'm on ArchLinux, but I also had ghc installed in my home, via ghcup, and available via the PATH. Eventually I asked on ArchLinux forum, and manage to fix the problem.

My choice, at that time, was to install xmonad via cabal (installed via ghcup). The reason for my choice is that I like GHCup and I find ghcup tui a very convenient way to have the most recent recommended version of HLS, the most recent version of Cabal, the most recent recommended and hls-powered version of GHC.

And I thought I had understood what the implications of using XMonad installed via cabal was.

But apparently I'm wrong: I've just updated via GHCup 0.1.20.0 to GHC 9.4.8, Cabal 3.10.2.1, and HLS 2.5.0.0, ...

... and now I can't recompile XMonad, the error being along the lines of "I can't find a single thing!", as you can see:

Errors detected while compiling xmonad config: /home/enlico/.config/xmonad/xmonad.hs
$ ghc --make xmonad.hs -i -ilib -fforce-recomp -main-is main -v0 -outputdir /home/enlico/.cache/xmonad/build-x86_64-linux -o /home/enlico/.cache/xmonad/xmonad-x86_64-linux

xmonad.hs:2:1: error:
    Could not find module ‘XMonad’
    Use -v (or `:set -v` in ghci) to see a list of the files searched for.
  |
2 | import XMonad
  | ^^^^^^^^^^^^^

xmonad.hs:3:1: error:
    Could not find module ‘XMonad.Hooks.DynamicLog’
    Use -v (or `:set -v` in ghci) to see a list of the files searched for.
  |
3 | import XMonad.Hooks.DynamicLog
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

xmonad.hs:4:1: error:
    Could not find module ‘XMonad.Hooks.EwmhDesktops’
    Use -v (or `:set -v` in ghci) to see a list of the files searched for.
  |
4 | import XMonad.Hooks.EwmhDesktops
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

xmonad.hs:5:1: error:
    Could not find module ‘XMonad.Hooks.StatusBar’
    Use -v (or `:set -v` in ghci) to see a list of the files searched for.
  |
5 | import XMonad.Hooks.StatusBar
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

xmonad.hs:6:1: error:
    Could not find module ‘XMonad.Hooks.StatusBar.PP’
    Use -v (or `:set -v` in ghci) to see a list of the files searched for.
  |
6 | import XMonad.Hooks.StatusBar.PP
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

xmonad.hs:7:1: error:
    Could not find module ‘XMonad.Layout.NoBorders’
    Use -v (or `:set -v` in ghci) to see a list of the files searched for.
  |
7 | import XMonad.Layout.NoBorders
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

xmonad.hs:8:1: error:
    Could not find module ‘XMonad.Layout.Spacing’
    Use -v (or `:set -v` in ghci) to see a list of the files searched for.
  |
8 | import XMonad.Layout.Spacing
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

xmonad.hs:9:1: error:
    Could not find module ‘XMonad.Util.EZConfig’
    Use -v (or `:set -v` in ghci) to see a list of the files searched for.
  |
9 | import XMonad.Util.EZConfig (additionalKeysP)
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

xmonad.hs:10:1: error:
    Could not find module ‘XMonad.Util.Loggers’
    Use -v (or `:set -v` in ghci) to see a list of the files searched for.
   |
10 | import XMonad.Util.Loggers
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^

xmonad.hs:11:1: error:
    Could not find module ‘XMonad.Util.Ungrab’
    Use -v (or `:set -v` in ghci) to see a list of the files searched for.
   |
11 | import XMonad.Util.Ungrab
   | ^^^^^^^^^^^^^^^^^^^^^^^^^

xmonad.hs:12:1: error:
    Could not find module ‘XMonad.StackSet’
    Use -v (or `:set -v` in ghci) to see a list of the files searched for.
   |
12 | import qualified XMonad.StackSet as W
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The unsuccessful attempt to fix things was:

cabal install --overwrite-policy=always --package-env=$HOME/.config/xmonad xmonad xmonad-contrib

The error above is upon recompiling XMonad, but beware, I've re-tried rempiling in order to get the error to paste here in the question, so I'm not 100% sure the error was identical upon recompiling XMonad right after I run the cabal command above.


Incidentally, I've got used to the fact that when I update stuff via GHCup than something goes wrong with some modules.

For instance, right now I've opened a haskell file I used for a hackerrank challenge or something, and I notice that one line is marked as erroneous by HLS:

import Flow ((|>))

Executing cabal install --lib flow fixes that.

But upon reloading HLS, another line shows an error:

import qualified Data.Map.Strict as M

and the error, now that I read, is this:

Could not load module ‘Data.Map.Strict’
It is a member of the hidden package ‘containers-0.6.7’.
You can run ‘:set -package containers’ to expose it.
(Note: this unloads all the modules in the current scope.)

and executing cabal install --lib containers fixes is, but was that the right step?


Solution

  • Quoting assorted comments:

    Also, with "Is executing cabal install --lib the-stuff-that-hls-claims-hidden wrong?" I was not referring to xmonad only, but in general to any package.

    Opinions may vary, and I'm open to be corrected on this take, but I understand cabal install --lib as a way to approximate how things used to work in the old days of cabal-install v1 (that is, making things available in a shared environment). With that, however, the old cabal-install v1 problems tend to return (namely, running into version conflicts once you install or update enough stuff). Furthermore, as you have already noticed, cabal install --lib relies on hidden per-directory state (the environment files) that can be easy to forget about.

    My current practice, instead, is to give anything its own subdirectory and .cabal file, even quick experiments. That might look like a lot of ceremony, but it really doesn't have to be. Once you remove all the metadata you don't care about for something you won't publish, a .cabal file can be very minimal. Besides keeping your builds from interfering with each other, by having a project for your code you gain confidence in being able to successfully build it at a later point. Declaring dependencies explicitly in the .cabal file is a good first step in that direction; depending on how much you care about reproducibility for some particular piece of code, you can go further and add extra information of various kinds.

    I suspect the answer would still be "make you cabal config file". I tried cabal init in xmonad.hs's dir, but I'm unsure how to proceed

    This is the .cabal file for my XMonad configuration once I remove all the optional fields:

    cabal-version:       3.4
    name:                xmonad-duplode
    version:             0.1.0.0
    
    executable xmonad
      main-is:             xmonad.hs
      build-depends:       base >= 4.13
                         , xmonad >= 0.16
                         , xmonad-contrib >= 0.16
                         , directory
                         , filepath
    

    In particular, if your xmonad.hs uses flow, add it to the build-depends and it will just work next time you compile your code.

    For this kind of code meant for personal use, it's up to you whether to have version bounds on the dependencies, and how tight to make them. You might e.g. find it of some use to add a lower bound once you begin using a feature that's only available from a certain version onward.

    (A different approach that also can make sense for some kinds of personal use code is not putting any version bounds, but using cabal freeze to generate a freeze file, which pins exact versions of everything. Then, when you feel like updating GHC or any of the dependencies, delete the freeze file and generate it again once you're done updating.)

    Package name? Why should it be not "xmonad"? The question The name xmonad is already in use by another package on Hackage. Do you want to choose a different name (y/n)? makes me think I'm doing something wrong.

    In the .cabal file quoted above, the package name is xmonad-duplode, while the executable name is xmonad. Your project will depend on the xmonad package, so avoiding the clash does make sense.

    but if my intention was to always update to the most recent "recommended+hls-powered" GHC version (fundamentally because I don't have any Haskell project that could suffer from being too bleeding edge; plus "recommended" doesn't feel bleeding edge anyway, so what am I risking at all..) and to keep xmonad in sync with that, would updating .cabal and run cabal build... after every GHC update fundamentally be the same thing as the approach I'd adopted so far?

    If your only project configuration is a minimal .cabal file like the one above, your XMonad will be built with the default GHC set by GHCup. While that may well be the most convenient option for your purposes, it has a few minor downsides -- for instance, if some change in base in a brand new GHC you got from GHCup ever requires you to tweak your xmonad.hs, you'll have to do it before being able to recompile it.

    One easy way to pin down the compiler version, so that you don't have to think about it until you want to, is to have, alongside the .cabal file, a cabal.project file, which might look like this:

    packages: .
    
    with-compiler: ghc-9.4.8
    

    (In a public project, the with-compiler setting would be better placed in a separate cabal.project.local file that you wouldn't commit to the public repository, but here it probably doesn't matter.)

    With the cabal.project file in place, you can manage your GHC versions as usual with GHCup, and your XMonad will keep being built with the specified GHC version regardless of whatever else you're doing. When you feel like trying out a different GHC with your XMonad config, just change the with-compiler field.

    The cabal.project file is also useful if you want to try bleeding edge versions of xmonad and xmonad-contrib, as it can be used to pull dependencies from the development repositories rather than Hackage.


    Here goes one way of setting up XMonad with a cabal project in ~/.xmonad along the lines of what we've been discussing. Begin by creating your xmonad.hs, the .cabal file and, if you wish so, the cabal.project file in ~/.xmonad. Then, as suggested by Daniel Wagner, from your .xmonad directory run (replacing ~/.local/bin by a destination of your preference in your PATH):

    cabal build && cp "$(cabal list-bin xmonad)" ~/.local/bin
    

    The above, which only needs to be done a single time, will place a "bootstrap" xmonad executable (the one that will be called by .xinitrc) in ~/.local/bin. It corresponds to the second cabal install (the one without --lib) in the relevant section of the official installation guide; I prefer doing it this way because for our current purposes there's no need or advantage in involving the global cabal store.

    Next, create (and make executable) the build script in .xmonad. The script below is adapted from this comment by Tony Zorman on XMonad GitHub issue #403:

    #!/bin/sh
    cd "$HOME/.xmonad" && cabal build || exit
    ln -sfT "$(cabal list-bin xmonad)" "$1"
    

    With that in place, both the "bootstrap" binary (on startup) and the running binary (when you use the recompile command) will build the executable and symlink the binary thus produced by cabal-install to the destination expected by XMonad (which gets passed as $1).


    The setup described just above for XMonad can also be used for xmobar, if you're compiling your configuration by using it as a library. In the xmobar configuration directory (the default is ~/.config/xmobar), put your xmobar.hs, create a .cabal file...

    cabal-version:       3.4
    name:                xmobar-duplode
    version:             0.1.0.0
    
    executable xmobar
      main-is:             xmobar.hs
      build-depends:       base
                         , xmobar
    

    ... optionally, a cabal.project file (it can be used to enable optional features of the xmobar library)...

    packages: .
    
    -- Leave out if you don't care about the compiler version.
    with-compiler: ghc-9.4.8
    
    -- Note the package in this stanza is xmobar, the library dependency,
    -- and not xmobar-duplode, the executable with the configuration.
    -- See the xmobar Hackage docs for the list of flags.
    package xmobar
      flags: +all_extensions
    

    ... and a build script...

    #!/bin/sh
    cd "$HOME/.config/xmobar" && cabal build || exit
    ln -sfT "$(cabal list-bin xmobar)" "$1"
    

    ... then create the "bootstrap" binary by running cabal install xmobar outside of ~/.config/xmobar, so that cabal will install Hackage xmobar rather than your custom one -- installing the "bootstrap" binary from Hackage is the easier option in the case of xmobar.