External Publication
Visit Post

Why We Built a Haskell Package Manager in Rust

Haskell Community [Unofficial] April 15, 2026
Source

BurningWitness:

Oh, also there’s a “try it” curl at the end of the article that could well be a malicious link.

I read the shell code. It’s not malicious, just a bit sloppy:

    # Create temp directory
    TMPDIR=$(mktemp -d)
    trap "rm -rf $TMPDIR" EXIT

$TMPDIR is unquoted. In this case that cannot lead to arbitrary file deletion, but it’s still bad form.

get_install_dir() {
    if [ -n "$HX_INSTALL_DIR" ]; then
        echo "$HX_INSTALL_DIR"
    elif [ -d "$HOME/.local/bin" ]; then
        echo "$HOME/.local/bin"
    elif [ -w "/usr/local/bin" ]; then
        echo "/usr/local/bin"
    else
        echo "$HOME/.local/bin"
    fi
}

It may install into /usr/local/bin if it’s writable. Weirdly enough, this code actually prefers a global installation, unless "$HOME/.local/bin" already exists… it doesn’t even try to create it.

And then it also asks for sudo:

    if [ -w "$INSTALL_DIR" ]; then
        cp "$BINARY" "$INSTALL_DIR/"
        chmod +x "$INSTALL_DIR/hx"
    else
        info "Requesting sudo access..."
        sudo mkdir -p "$INSTALL_DIR"
        sudo cp "$BINARY" "$INSTALL_DIR/"
        sudo chmod +x "$INSTALL_DIR/hx"
    fi

Despite that, I tried it in a docker container and I found the UX lacking:

$ hx new my-app
error: unrecognized subcommand 'my-app'

Usage: hx new [OPTIONS] <COMMAND>

For more information, try '--help'.

Well… ok. This is what the documentation told me to run. I guess I can do it myself:

$ mkdir foo
$ cd foo
$ hx init
✓ Created bin project: foo
Next steps:
  cd /root/foo
  hx build
  hx run
$ hx build
Unable to create steel home directory "/root/.local/share/steel": No such file or directory (os error 2)
    Building foo

error: toolchain not found: cabal

fix: Run `hx toolchain install`
      Install cabal
$ hx toolchain install
warning: No version specified
Example: hx toolchain install 9.8.2
Or: hx toolchain install --ghc 9.8.2
Or: hx toolchain install --bhc latest
$ hx toolchain install --ghc latest
  Installing GHC latest
 WARN Direct GHC installation failed, falling back to ghcup: configuration error: Invalid GHC version format: latest. Expected format like 9.8.2

error: toolchain not found: ghcup

fix: Run `hx toolchain install`
      Install ghcup
$ hx toolchain install --ghc 9.12.4
  Installing GHC 9.12.4
✗ Failed to download GHC 9.12.4                                                                                                                     WARN Direct GHC installation failed, falling back to ghcup: configuration error: GHC 9.12.4 download failed: HTTP 404 Not Found. This version may not be available for your platform.

error: toolchain not found: ghcup

fix: Run `hx toolchain install`
      Install ghcup

I don’t know. I ran like 4 commands and they all failed. It tells me to run hx toolchain install, but that isn’t a valid command even (lacks a version). Then the examples indicate I can use latest as version, but that seems to fail as well. Then it seems downloading GHC is broken (it has its own installation logic), then it tries to fall back to ghcup, but that isn’t installed either and hx can’t install ghcup.

Luckily, I know a thing or two about ghcup, so I manage to install it after all.

After installing the toolchain, I run:

$ hx build
Unable to create steel home directory "/root/.local/share/steel": No such file or directory (os error 2)
    Building my-app
✗ Build failed                                                                                                                                     Warning: The package list for 'hackage.haskell.org' does not exist. Run 'cabal
update' to download it.
Error: [Cabal-7107]
Could not resolve dependencies:
[__0] trying: my-app-0.1.0.0 (user goal)
[__1] next goal: base (dependency of my-app)
[__1] rejecting: base-4.22.0.0/installed-fde1 (conflict: my-app => base^>=4.17 || ^>=4.18 || ^>=4.19 || ^>=4.20)
[__1] fail (backjumping, conflict set: base, my-app)
After searching the rest of the dependency tree exhaustively, these were the goals I've had most trouble fulfilling: my-app, base



error: build failed

Oh… uhm. What is cabal? I thought I only have to use hx. Anyway.

$ hx build
Unable to create steel home directory "/root/.local/share/steel": No such file or directory (os error 2)
    Building my-app
✗ Build failed                                                                                                                                     Error: [Cabal-7107]
Could not resolve dependencies:
[__0] trying: my-app-0.1.0.0 (user goal)
[__1] next goal: base (dependency of my-app)
[__1] rejecting: base-4.22.0.0/installed-fde1 (conflict: my-app => base^>=4.17 || ^>=4.18 || ^>=4.19 || ^>=4.20)
[__1] skipping: base; 4.22.0.0, 4.21.2.0, 4.21.1.0, 4.21.0.0 (has the same characteristics that caused the previous version to fail: excluded by constraint '^>=4.17 || ^>=4.18 || ^>=4.19 || ^>=4.20' from 'my-app')
[__1] rejecting: base; 4.20.2.0, 4.20.1.0, 4.20.0.1, 4.20.0.0, 4.19.2.0, 4.19.1.0, 4.19.0.0, 4.18.3.0, 4.18.2.1, 4.18.2.0, 4.18.1.0, 4.18.0.0, 4.17.2.1, 4.17.2.0, 4.17.1.0, 4.17.0.0, 4.16.4.0, 4.16.3.0, 4.16.2.0, 4.16.1.0, 4.16.0.0, 4.15.1.0, 4.15.0.0, 4.14.3.0, 4.14.2.0, 4.14.1.0, 4.14.0.0, 4.13.0.0, 4.12.0.0, 4.11.1.0, 4.11.0.0, 4.10.1.0, 4.10.0.0, 4.9.1.0, 4.9.0.0, 4.8.2.0, 4.8.1.0, 4.8.0.0, 4.7.0.2, 4.7.0.1, 4.7.0.0, 4.6.0.1, 4.6.0.0, 4.5.1.0, 4.5.0.0, 4.4.1.0, 4.4.0.0, 4.3.1.0, 4.3.0.0, 4.2.0.2, 4.2.0.1, 4.2.0.0, 4.1.0.0, 4.0.0.0, 3.0.3.2, 3.0.3.1 (constraint from non-reinstallable package requires installed instance)
[__1] fail (backjumping, conflict set: base, my-app)
After searching the rest of the dependency tree exhaustively, these were the goals I've had most trouble fulfilling: base, my-app



error: build failed

Aha, so it created the project with some random bounds. I don’t even know what it used.

I looked up the code that generates the project: hx/crates/hx-cli/src/commands/init.rs at b9dcac765e9f500e1bce398e6a395a012cb2dae6 · arcanist-sh/hx · GitHub

fn generate_cabal(name: &str, kind: ProjectKind) -> String {
    match kind {
        ProjectKind::Bin => format!(
            r#"cabal-version:      3.0
name:               {name}
version:            0.1.0.0
synopsis:           A Haskell project
license:            MIT
author:             Author
maintainer:         author@example.com
build-type:         Simple

executable {name}
    main-is:          Main.hs
    hs-source-dirs:   src
    default-language: GHC2021
    build-depends:
        base ^>=4.17 || ^>=4.18 || ^>=4.19 || ^>=4.20
    ghc-options:      -Wall
"#
        ),
        ProjectKind::Lib => format!(
            r#"cabal-version:      3.0
name:               {name}
version:            0.1.0.0
synopsis:           A Haskell library
license:            MIT
author:             Author
maintainer:         author@example.com
build-type:         Simple

library
    exposed-modules:  Lib
    hs-source-dirs:   src
    default-language: GHC2021
    build-depends:
        base ^>=4.17 || ^>=4.18 || ^>=4.19 || ^>=4.20
    ghc-options:      -Wall
"#
        ),
    }
}

And at this point, I’m not quite sure if I want to continue my experimentation.

Discussion in the ATmosphere

Loading comments...