Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/feature-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Feature Publish

on:
push:
tags:
- 'feature-v*'

jobs:
publish:
runs-on: ubuntu-24.04
permissions:
packages: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install -g @devcontainers/cli@0.88.0
- name: Render install.sh from template
working-directory: devcontainer-feature/src/haskell-toolchain
run: |
nickel export --format text <<'Nickel' > install.sh
let tpl = import "install.sh.tpl.ncl" in
tpl.install_sh
Nickel
chmod +x install.sh
- name: Package and publish feature
working-directory: devcontainer-feature
run: |
devcontainer features package src --output-dir /tmp/feature-output
devcontainer features publish /tmp/feature-output/haskell-toolchain
91 changes: 91 additions & 0 deletions .github/workflows/feature-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Feature Test

on:
pull_request:
branches:
- master
paths:
- 'devcontainer-feature/**'
- 'devcontainer-template/**'
- '9.14/_globals.ncl'
- '9.14/bookworm.ncl'
- '.github/workflows/feature-test.yml'
push:
branches:
- master
paths:
- 'devcontainer-feature/**'
- 'devcontainer-template/**'
- '9.14/_globals.ncl'
- '9.14/bookworm.ncl'

jobs:
test:
name: Test feature (${{ matrix.distro }}/${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- distro: debian:bookworm
tag: debian-bookworm
arch: amd64
runner: ubuntu-24.04
- distro: debian:bookworm
tag: debian-bookworm
arch: arm64
runner: ubuntu-24.04-arm
- distro: ubuntu:24.04
tag: ubuntu-2404
arch: amd64
runner: ubuntu-24.04
- distro: ubuntu:24.04
tag: ubuntu-2404
arch: arm64
runner: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install -g @devcontainers/cli@0.88.0

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build toolchain layer (cached)
uses: docker/build-push-action@v6
with:
context: devcontainer-feature
file: devcontainer-feature/src/haskell-toolchain/Dockerfile.test
target: toolchain
load: true
tags: haskell-feature-toolchain:${{ matrix.tag }}-${{ matrix.arch }}
cache-from: type=gha,scope=toolchain-${{ matrix.tag }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=toolchain-${{ matrix.tag }}-${{ matrix.arch }}
build-args: |
BASE_IMAGE=${{ matrix.distro }}
_REMOTE_USER=
_REMOTE_USER_HOME=

- name: Run test.sh against cached toolchain
run: |
docker run --rm \
-e _REMOTE_USER= \
-e _REMOTE_USER_HOME= \
-v "$(pwd)/devcontainer-feature/test/haskell-toolchain/test.sh:/tmp/test.sh" \
haskell-feature-toolchain:${{ matrix.tag }}-${{ matrix.arch }} \
bash -c 'chmod +x /tmp/test.sh && /tmp/test.sh'

shellcheck:
name: Shellcheck
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Run shellcheck on all shell scripts
run: |
shellcheck -x \
devcontainer-feature/src/haskell-toolchain/install.sh \
devcontainer-feature/test/haskell-toolchain/test.sh \
devcontainer-feature/test/haskell-toolchain/*/scenario.sh \
devcontainer-template/test/haskell/test.sh
39 changes: 39 additions & 0 deletions .github/workflows/feature-versions-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Feature Versions Sync

on:
schedule:
- cron: '0 6 * * 1'
workflow_dispatch:

jobs:
check-drift:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Check for version drift
run: |
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
source ~/.ghcup/env

GHC_PINNED=$(grep 'ghc_version' devcontainer-feature/src/haskell-toolchain/versions.ncl | grep -oP '"[^"]+"' | tr -d '"')
HLS_PINNED=$(grep 'hls_version' devcontainer-feature/src/haskell-toolchain/versions.ncl | grep -oP '"[^"]+"' | tr -d '"')
GHCUP_PINNED=$(grep 'ghcup_version' devcontainer-feature/src/haskell-toolchain/versions.ncl | grep -oP '"[^"]+"' | tr -d '"')

GHC_LATEST=$(ghcup list -t available 2>&1 | grep '^ ghc' | grep 'recommended' | awk '{print $2}')
HLS_LATEST=$(ghcup list -t available 2>&1 | grep '^ hls' | grep 'recommended' | awk '{print $2}')
GHCUP_LATEST=$(ghcup list -t available 2>&1 | grep '^ ghcup' | grep 'recommended' | awk '{print $2}')

echo "GHC: pinned=$GHC_PINNED latest=$GHC_LATEST"
echo "HLS: pinned=$HLS_PINNED latest=$HLS_LATEST"
echo "ghcup: pinned=$GHCUP_PINNED latest=$GHCUP_LATEST"

DRIFT=0
[ "$GHC_PINNED" != "$GHC_LATEST" ] && DRIFT=1 && echo "GHC drifted: $GHC_PINNED -> $GHC_LATEST"
[ "$HLS_PINNED" != "$HLS_LATEST" ] && DRIFT=1 && echo "HLS drifted: $HLS_PINNED -> $HLS_LATEST"
[ "$GHCUP_PINNED" != "$GHCUP_LATEST" ] && DRIFT=1 && echo "ghcup drifted: $GHCUP_PINNED -> $GHCUP_LATEST"

if [ "$DRIFT" = "1" ]; then
echo "::warning::Version drift detected — update versions.ncl"
exit 1
fi
echo "All versions in sync."
27 changes: 27 additions & 0 deletions .github/workflows/template-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Template Lint

on:
pull_request:
branches:
- master
paths:
- 'devcontainer-template/**'
- '.github/workflows/template-lint.yml'

jobs:
lint:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Validate template metadata
run: |
# Validate that devcontainer-template.json is valid JSON and has required fields.
for f in devcontainer-template/src/*/devcontainer-template.json; do
echo "Validating $f..."
jq -e '.id, .name, .description' "$f" > /dev/null
done
# Validate devcontainer.json files.
for f in devcontainer-template/src/*/.devcontainer/devcontainer.json; do
echo "Validating $f..."
jq -e '.image or .build.dockerfile, .features' "$f" > /dev/null
done
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,25 @@ Only the latest patch release in each actively maintained minor line receives ro

For actively supported GHC versions, Cabal and Stack should be updated when new versions are released.

## Devcontainers

An official [devcontainer feature](devcontainer-feature/src/haskell-toolchain) and
[template](devcontainer-template/src/haskell) live alongside this image.

- **Feature** — `ghcr.io/haskell/docker-haskell/devcontainer-features/haskell-toolchain:1.0.0`
installs GHC, cabal, Stack, HLS, and formatters (`ormolu`, `fourmolu`) via `ghcup`
on any Debian/Ubuntu base image. Tool versions are pinned in
[`versions.ncl`](devcontainer-feature/src/haskell-toolchain/versions.ncl), which
imports from the same Nickel data files as the Docker images — bumping a version
in one place updates both.
- **Template** — "Haskell (cabal)" in the devcontainer registry; a clone-and-go
starter with a `Hello, Haskell!` cabal project, VS Code extensions, and
format-on-save configured.

See [`devcontainer-feature/src/haskell-toolchain/README.md`](devcontainer-feature/src/haskell-toolchain/README.md)
and [`devcontainer-template/src/haskell/README.md`](devcontainer-template/src/haskell/README.md)
for details.

## Maintenance

### Generating Dockerfiles
Expand Down
27 changes: 27 additions & 0 deletions devcontainer-feature/src/haskell-toolchain/Dockerfile.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Multistage Dockerfile for local testing of the haskell-toolchain feature.
# Stage 1 ("toolchain"): runs install.sh — expensive, cached by buildx/podman.
# Stage 2 ("test"): copies test scripts, runs them — cheap, re-runs on change.
#
# Usage (from repo root):
# podman build -t haskell-feature-test -f devcontainer-feature/src/haskell-toolchain/Dockerfile.test devcontainer-feature
# podman build --build-arg BASE_IMAGE=ubuntu:24.04 -t haskell-feature-test-ubuntu -f devcontainer-feature/src/haskell-toolchain/Dockerfile.test devcontainer-feature

# --- Stage 1: install the toolchain (cached unless install.sh changes) ---
ARG BASE_IMAGE=mcr.microsoft.com/devcontainers/base:debian-12
FROM ${BASE_IMAGE} AS toolchain

ARG _REMOTE_USER=vscode
ARG _REMOTE_USER_HOME=/home/vscode
ENV _REMOTE_USER=${_REMOTE_USER}
ENV _REMOTE_USER_HOME=${_REMOTE_USER_HOME}

COPY src/haskell-toolchain/install.sh /tmp/dev-container-features/haskell-toolchain/install.sh
RUN chmod +x /tmp/dev-container-features/haskell-toolchain/install.sh && \
/tmp/dev-container-features/haskell-toolchain/install.sh && \
rm -rf /tmp/dev-container-features

# --- Stage 2: run the test scripts (fast, re-runs on every test.sh change) ---
FROM toolchain AS test

COPY test/haskell-toolchain/test.sh /tmp/test.sh
RUN chmod +x /tmp/test.sh && /tmp/test.sh
21 changes: 21 additions & 0 deletions devcontainer-feature/src/haskell-toolchain/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2014-2015 Darin Morrison, Christopher Biscardi, Peter Salvatore

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
103 changes: 103 additions & 0 deletions devcontainer-feature/src/haskell-toolchain/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Haskell Toolchain (ghcup)

Installs the Haskell toolchain via [ghcup](https://www.haskell.org/ghcup/):
GHC, cabal-install, Stack, the Haskell Language Server, and a curated set of
formatters (`ormolu`, `fourmolu`).

Tool versions are **pinned** in [`versions.ncl`](versions.ncl), which imports
from the `9.14/bookworm.ncl` data file in this same repository. This keeps the
feature in lockstep with the official `haskell` Docker images — bumping a
version in one place updates both.

## Example usage

```jsonc
"features": {
"ghcr.io/haskell/docker-haskell/devcontainer-features/haskell-toolchain:1.0.0": {
"version": "9.14"
}
}
```

## Options

| Option | Type | Default | Description |
|---|---|---|---|
| `version` | string | `9.14` | GHC version (ghcup shorthand: `9.14`, `recommended`, `latest`, …). |
| `installStack` | boolean | `true` | Install Stack. |
| `installHLS` | boolean | `true` | Install the Haskell Language Server. |
| `installStackGHCupHook` | boolean | `true` | Force Stack to use ghcup-installed GHC (no separate Stack GHC downloads). |
| `adjustBash` | boolean | `true` | Prepend ghcup to `PATH` in `~/.bashrc`. |
| `installLinters` | boolean | `true` | Install curated formatters (`ormolu`, `fourmolu`). |
| `globalPackages` | string | `""` | Extra `cabal install` packages (space-separated, unpinned). Escape hatch. |
| `globalLibraries` | string | `""` | Extra `cabal install --lib` libraries (space-separated, unpinned). Escape hatch. |

## Pinned versions (v1.0.0)

| Tool | Version | Source |
|---|---|---|
| GHC | 9.14.1 | `9.14/bookworm.ncl` |
| cabal-install | 3.16.1.0 | `9.14/_globals.ncl` |
| Stack | 3.11.1 | `9.14/_globals.ncl` |
| HLS | 2.14.0.0 | `versions.ncl` (LTS pair with GHC 9.14.1) |
| ghcup | 0.2.6.2 | `versions.ncl` |
| ormolu | 0.8.2.0 | `versions.ncl` |
| fourmolu | 0.20.0.0 | `versions.ncl` |

## Why only ormolu and fourmolu?

`hlint`, `stylish-haskell`, and `cabal-fmt` are **not** included in the curated
set because their latest Hackage releases constrain `ghc < 9.13` or
`base < 4.22`, making them incompatible with GHC 9.14.1 (which ships
`base-4.22.0.0`). They depend on `ghc-lib-parser 9.12.x`, which has not been
updated for GHC 9.14's `base` at the time of writing.

When upstream releases versions compatible with GHC 9.14, they will be added
back to the curated set. Until then, users can attempt to install them via the
`globalPackages` escape hatch with `--allow-newer`:

```jsonc
"features": {
"ghcr.io/haskell/docker-haskell/devcontainer-features/haskell-toolchain:1.0.0": {
"installLinters": false,
"globalPackages": "hlint stylish-haskell cabal-fmt"
}
}
```

(Note: `globalPackages` does not pass `--allow-newer`; users needing it should
install manually in a `postCreateCommand`.)

## Supported base images

Debian and Ubuntu only in v1.0.0. Alpine support is tracked by
[issue #22](https://github.com/haskell/docker-haskell/issues/22) and will land
in lockstep with the Alpine base images.

## Regenerating `install.sh`

`install.sh` is **generated** from `install.sh.tpl.ncl` via Nickel. Do not edit
the `.sh` file directly; edit the template and re-render:

```bash
cd devcontainer-feature/src/haskell-toolchain
nickel export --format text <<'Nickel' > install.sh
let tpl = import "install.sh.tpl.ncl" in
tpl.install_sh
Nickel
chmod +x install.sh
```

The `feature-publish.yml` workflow re-renders automatically on tag.

## Install-user behavior

Mirrors [`devcontainers-extra/features/src/haskell`](https://github.com/devcontainers-extra/features/tree/main/src/haskell):

- When `_REMOTE_USER` is set (devcontainer / Codespaces build context),
ghcup is installed as that user via `sudo -iu "$_REMOTE_USER"`.
- When `_REMOTE_USER` is unset and the script runs as root (plain Dockerfile
`RUN`), ghcup is installed under `/root`.
- Non-root invocation without `_REMOTE_USER` hard-fails.

`GHCUP_USE_XDG_DIRS=1` is set in both paths.
Loading
Loading