From ebd521ac684a704e85bb3e76ab21a5aeeb902cf4 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Thu, 25 Dec 2025 22:15:32 +0800 Subject: [PATCH 01/19] feat(deps): upgrade nixpkgs to v25.11 (ghc910), bump flake inputs --- flake.lock | 12 ++++++------ flake.nix | 5 +++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/flake.lock b/flake.lock index 21559ff..34d6f67 100644 --- a/flake.lock +++ b/flake.lock @@ -20,16 +20,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1748302896, - "narHash": "sha256-ixMT0a8mM091vSswlTORZj93WQAJsRNmEvqLL+qwTFM=", - "owner": "nixos", + "lastModified": 1766473571, + "narHash": "sha256-5G1NDO2PulBx1RoaA6U1YoUDX0qZslpPxv+n5GX6Qto=", + "owner": "NixOS", "repo": "nixpkgs", - "rev": "7848cd8c982f7740edf76ddb3b43d234cb80fc4d", + "rev": "76701a179d3a98b07653e2b0409847499b2a07d3", "type": "github" }, "original": { - "owner": "nixos", - "ref": "nixos-25.05", + "owner": "NixOS", + "ref": "nixos-25.11", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index b663683..b330000 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ description = "Haskell Project Template"; inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; flake-utils.url = "github:numtide/flake-utils"; }; @@ -43,7 +43,8 @@ ## Build inputs for development shell: buildInputs = [ ## Haskell related build inputs: - thisHaskell.apply-refact + ## TODO: Once we are on ghc > 9.10, enable apply-refact again. + # thisHaskell.apply-refact thisHaskell.cabal-fmt thisHaskell.cabal-install thisHaskell.cabal2nix From b7f62369088a0dbb36b097a0808e704cf564b33a Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Fri, 26 Dec 2025 11:30:43 +0800 Subject: [PATCH 02/19] test: revisit command to verify codebase (lint, test, build, etc...) --- .github/workflows/check.yaml | 2 +- README.md | 12 +-- flake.nix | 10 +- nix/cabal-verify/default.nix | 26 +++++ nix/cabal-verify/script.sh | 167 ++++++++++++++++++++++++++++++ nix/dev-test-build.sh | 192 ----------------------------------- 6 files changed, 203 insertions(+), 206 deletions(-) create mode 100644 nix/cabal-verify/default.nix create mode 100644 nix/cabal-verify/script.sh delete mode 100644 nix/dev-test-build.sh diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index c386ba5..47d7a3d 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -17,7 +17,7 @@ jobs: - name: "Check, Test and Build" run: | - nix develop --command bash -c "cabal update --ignore-project && cabal dev-test-build" + nix develop --command cabal verify - name: "Build Docker Image" run: | diff --git a/README.md b/README.md index 2f9e3e4..5162714 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ hpack && direnv reload && fourmolu -i app/ src/ test/ && prettier --write . && - find . -iname "*.nix" -not -path "*/nix/sources.nix" -print0 | xargs --null nixpkgs-fmt && + find . -iname "*.nix" -print0 | xargs --null nixpkgs-fmt && hlint app/ src/ test/ && cabal build -O0 && cabal run -O0 haskell-template-hebele -- --version && @@ -64,22 +64,22 @@ hpack && cabal haddock -O0 ``` -To run checks, tests and build the codebase in the development environment, run: +To run checks, linters, tests and build the codebase in the development environment, run: ```sh -cabal-dev-test-build +cabal-verify ``` -You can pass `-c` to clean the build artifacts first: +You can pass `-c` (or `--clean`) to clean the build artifacts first: ```sh -cabal-dev-test-build -c +cabal-verify -c ``` As of Cabal 3.12, you can now run the above as an external `cabal` command: ```sh -cabal dev-test-build [-c] +cabal verify [-c|--clean] ``` diff --git a/flake.nix b/flake.nix index b330000..be2c54d 100644 --- a/flake.nix +++ b/flake.nix @@ -25,12 +25,8 @@ }; }; - ## Prepare dev-test-build script: - dev-test-build = pkgs.writeShellApplication { - name = "cabal-dev-test-build"; - text = builtins.readFile ./nix/dev-test-build.sh; - runtimeInputs = [ pkgs.bash pkgs.bc pkgs.moreutils ]; - }; + ## Get the cabal-verify command: + cabal-verify = pkgs.callPackage ./nix/cabal-verify { }; ## Prepare Nix shell: thisShell = thisHaskell.shellFor { @@ -55,7 +51,7 @@ thisHaskell.weeder ## Our development scripts: - dev-test-build + cabal-verify ## Other build inputs for various development requirements: pkgs.docker-client diff --git a/nix/cabal-verify/default.nix b/nix/cabal-verify/default.nix new file mode 100644 index 0000000..9cc0903 --- /dev/null +++ b/nix/cabal-verify/default.nix @@ -0,0 +1,26 @@ +{ lib +, writeShellApplication +, bash +, coreutils +, moreutils +, yq-go +}: + +writeShellApplication { + name = "cabal-verify"; + + text = builtins.readFile ./script.sh; + + runtimeInputs = [ + bash + coreutils + moreutils + yq-go + ]; + + meta = with lib; { + description = "Run project verification checks (format, lint, build, test, docs, etc...)"; + platforms = platforms.all; + }; +} + diff --git a/nix/cabal-verify/script.sh b/nix/cabal-verify/script.sh new file mode 100644 index 0000000..160d3f0 --- /dev/null +++ b/nix/cabal-verify/script.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +############### +## VARIABLES ## +############### + +_clean=false +_cabal="$(command -v cabal)" + +##################### +## TERMINAL COLORS ## +##################### + +if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]]; then + BOLD="$(tput bold)" + RESET="$(tput sgr0)" + RED="$(tput setaf 1)" + GREEN="$(tput setaf 2)" + BLUE="$(tput setaf 4)" +else + BOLD="" + RESET="" + RED="" + GREEN="" + BLUE="" +fi + +####################### +## UTILITY FUNCTIONS ## +####################### + +_usage() { + echo "Usage: ${0} [OPTIONS]" + echo "" + echo " Runs all checks and tests for the project." + echo "" + echo "Options:" + echo " -c, --clean Clean the project before running tests." + echo " -h, --help Show this help message and exit." +} + +_suc() { + printf "%s%s✅ %s%s\n" "${BOLD}" "${GREEN}" "${1}" "${RESET}" +} + +_err() { + printf "%s%s❌ %s%s\n" "${BOLD}" "${RED}" "${1}" "${RESET}" +} + +_get_now_ms() { + date +%s%3N +} + +_diff_ms_to_s() { + local _ms="${1}" + printf '%d.%03d' "$((_ms / 1000))" "$((_ms % 1000))" +} + +_run_check() { + local _title="${1}" + shift + + printf "%s%s🔵 Running %s%s " "${BOLD}" "${BLUE}" "${_title}" "${RESET}" + + local _ms_since + local _ms_until + local _ms_total + local _captured + + _ms_since="$(_get_now_ms)" + + if _captured="$(chronic -- "${@}" 2>&1)"; then + _ms_until="$(_get_now_ms)" + _ms_total=$((_ms_until - _ms_since)) + _suc "$(_diff_ms_to_s "${_ms_total}")s" + else + _ms_until="$(_get_now_ms)" + _ms_total=$((_ms_until - _ms_since)) + _err "$(_diff_ms_to_s "${_ms_total}")s" + >&2 printf "%s\n" "${_captured}" + exit 1 + fi +} + +#################################### +## COMMAND-LINE ARGUMENTS PARSING ## +#################################### + +# cabal external command support +if [[ -n "${CABAL:-}" ]]; then + _cabal="${CABAL}" + shift +fi + +while [[ $# -gt 0 ]]; do + case "${1}" in + --) + shift + break + ;; + -c | --clean) + _clean=true + shift + ;; + -h | --help) + _usage + exit 0 + ;; + -*) + _usage + echo "" + >&2 _err "Invalid option: ${1}" + exit 1 + ;; + *) + break + ;; + esac +done + +############### +## PROCEDURE ## +############### + +_script_start_ms="$(_get_now_ms)" + +${_clean} && _run_check "clean" "${_cabal}" clean && _run_check "v1-clean" "${_cabal}" v1-clean + +_run_check "hpack (v$(hpack --numeric-version))" \ + hpack + +_run_check "fourmolu (v$(fourmolu --version | head -n1 | cut -d' ' -f2))" \ + fourmolu --quiet --mode check app/ src/ test/ + +_run_check "prettier (v$(prettier --version))" \ + prettier --check . + +_run_check "nixpkgs-fmt (v$(nixpkgs-fmt --version 2>&1 | cut -d' ' -f2))" \ + find . -iname "*.nix" -exec nixpkgs-fmt --check {} \; + +_run_check "hlint (v$(hlint --numeric-version))" \ + hlint app/ src/ test/ + +_run_check "cabal build (v$("${_cabal}" --numeric-version))" \ + "${_cabal}" build -O0 + +_run_check "cabal run (v$("${_cabal}" --numeric-version))" \ + "${_cabal}" run "$(yq ".executables | keys | .[0]" package.yaml)" -O0 -- --version + +_run_check "cabal test (v$("${_cabal}" --numeric-version))" \ + "${_cabal}" v1-test --ghc-options="-O0" + +_run_check "weeder (v$(weeder --version | head -n1 | cut -d' ' -f3))" \ + weeder + +_run_check "stan ($(stan --version | head -n 1 | cut -f 2 -d " "))" \ + stan --hiedir ./dist-newstyle + +_run_check "cabal haddock (v$("${_cabal}" --numeric-version))" \ + "${_cabal}" haddock -O0 \ + --haddock-quickjump \ + --haddock-hyperlink-source \ + --haddock-html-location="https://hackage.haskell.org/package/\$pkg-\$version/docs" + +_suc "All checks passed in $(_diff_ms_to_s $(($(_get_now_ms) - _script_start_ms)))s." diff --git a/nix/dev-test-build.sh b/nix/dev-test-build.sh deleted file mode 100644 index 80652b4..0000000 --- a/nix/dev-test-build.sh +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env bash - -## Purpose: This script is used to run all the necessary checks and -## tests for the project. - -## Fail on any error: -set -e - -## Declare default styles: -_sty_bold="" -_sty_underline="" -_sty_standout="" -_sty_normal="" -_sty_black="" -_sty_red="" -_sty_green="" -_sty_yellow="" -_sty_blue="" -_sty_magenta="" -_sty_cyan="" -_sty_white="" - -## Set styles if we are on terminal: -if test -t 1; then - ## Check if the terminal supports colors: - ncolors=$(tput colors) - - ## Defines styles: - if test -n "$ncolors" && test "${ncolors}" -ge 8; then - _sty_bold="$(tput bold)" - _sty_underline="$(tput smul)" - _sty_standout="$(tput smso)" - _sty_normal="$(tput sgr0)" - _sty_black="$(tput setaf 0)" - _sty_red="$(tput setaf 1)" - _sty_green="$(tput setaf 2)" - _sty_yellow="$(tput setaf 3)" - _sty_blue="$(tput setaf 4)" - _sty_magenta="$(tput setaf 5)" - _sty_cyan="$(tput setaf 6)" - _sty_white="$(tput setaf 7)" - fi -fi - -_clean="" - -## Check if we are an cabal external command: -CABAL="${CABAL:-}" -if [ -n "${CABAL}" ]; then - _cabal="${CABAL}" - shift -else - _cabal="$(command -v cabal)" -fi - -## Parse options: -while getopts ":c" opt; do - case ${opt} in - c) - _clean="true" - ;; - ?) - echo "Invalid option: -${OPTARG}." - exit 1 - ;; - esac -done - -_get_now() { - t=${EPOCHREALTIME} # remove the decimal separator (s → µs) - t=${t%???} # remove the last three digits (µs → ms) - echo "${t}" -} - -_get_diff() { - printf "scale=3; %s - %s\n" "${2}" "${1}" | bc -} - -_print_header() { - printf "${_sty_bold}${_sty_blue}🔵 Running %s${_sty_normal}" "${1}" -} - -_print_success() { - _start="${1}" - _until="${2}" - _elapsed=$(_get_diff "${_start}" "${_until}") - printf "${_sty_bold}${_sty_green} ✅ %ss${_sty_normal}\n" "${_elapsed}" -} - -_clean() { - _print_header "clean" - _start=$(_get_now) - chronic -- "${_cabal}" clean && chronic -- "${_cabal}" v1-clean - _print_success "${_start}" "$(_get_now)" -} - -_hpack() { - _print_header "hpack (v$(hpack --numeric-version))" - _start=$(_get_now) - chronic -- hpack - _print_success "${_start}" "$(_get_now)" -} - -_fourmolu() { - _print_header "fourmolu (v$(fourmolu --version | head -n1 | cut -d' ' -f2))" - _start=$(_get_now) - chronic -- fourmolu --quiet --mode check app/ src/ test/ - _print_success "${_start}" "$(_get_now)" -} - -_prettier() { - _print_header "prettier (v$(prettier --version))" - _start=$(_get_now) - chronic -- prettier --check . - _print_success "${_start}" "$(_get_now)" -} - -_nixpkgs_fmt() { - _print_header "nixpkgs-fmt (v$(nixpkgs-fmt --version 2>&1 | cut -d' ' -f2))" - _start=$(_get_now) - chronic -- find . -iname "*.nix" -not -path "*/nix/sources.nix" -exec nixpkgs-fmt --check {} \; - _print_success "${_start}" "$(_get_now)" -} - -_hlint() { - _print_header "hlint (v$(hlint --numeric-version))" - _start=$(_get_now) - chronic -- hlint app/ src/ test/ - _print_success "${_start}" "$(_get_now)" -} - -_cabal_build() { - _print_header "cabal build (v$("${_cabal}" --numeric-version))" - _start=$(_get_now) - chronic -- "${_cabal}" build -O0 - _print_success "${_start}" "$(_get_now)" -} - -_cabal_run() { - _print_header "cabal run (v$("${_cabal}" --numeric-version))" - _start=$(_get_now) - chronic -- "${_cabal}" run -O0 haskell-template-hebele -- --version - _print_success "${_start}" "$(_get_now)" -} - -_cabal_test() { - _print_header "cabal test (v$("${_cabal}" --numeric-version))" - _start=$(_get_now) - chronic -- "${_cabal}" v1-test - _print_success "${_start}" "$(_get_now)" -} - -_weeder() { - _print_header "weeder (v$(weeder --version | head -n1 | cut -d' ' -f3))" - _start=$(_get_now) - chronic -- weeder - _print_success "${_start}" "$(_get_now)" -} - -_stan() { - _print_header "stan ($(stan --version | head -n 1 | cut -f 2 -d " "))" - _start=$(_get_now) - chronic -- stan --hiedir ./dist-newstyle - _print_success "${_start}" "$(_get_now)" -} - -_cabal_haddock() { - _print_header "cabal haddock (v$("${_cabal}" --numeric-version))" - _start=$(_get_now) - chronic -- "${_cabal}" haddock -O0 \ - --haddock-quickjump \ - --haddock-hyperlink-source \ - --haddock-html-location="https://hackage.haskell.org/package/\$pkg-\$version/docs" - _print_success "${_start}" "$(_get_now)" -} - -_scr_start=$(_get_now) -if [ -n "${_clean}" ]; then - _clean -fi -_hpack -_fourmolu -_prettier -_nixpkgs_fmt -_hlint -_cabal_build -_cabal_run -_cabal_test -_stan -_weeder -_cabal_haddock -printf "Finished all in %ss\n" "$(_get_diff "${_scr_start}" "$(_get_now)")" From e2d70ad2e5f4573e0543d03325ae3b1c1bcb2c43 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Fri, 26 Dec 2025 11:47:32 +0800 Subject: [PATCH 03/19] chore(nix): use flake-parts instead of flake-utils --- flake.lock | 46 ++-- flake.nix | 204 +++++++++--------- nix/flake-modules/read-yaml/default.nix | 7 + .../read-yaml/function.nix} | 0 4 files changed, 133 insertions(+), 124 deletions(-) create mode 100644 nix/flake-modules/read-yaml/default.nix rename nix/{read-yaml.nix => flake-modules/read-yaml/function.nix} (100%) diff --git a/flake.lock b/flake.lock index 34d6f67..b1a714b 100644 --- a/flake.lock +++ b/flake.lock @@ -1,20 +1,20 @@ { "nodes": { - "flake-utils": { + "flake-parts": { "inputs": { - "systems": "systems" + "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "lastModified": 1765835352, + "narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "a34fae9c08a15ad73f295041fec82323541400a9", "type": "github" }, "original": { - "owner": "numtide", - "repo": "flake-utils", + "owner": "hercules-ci", + "repo": "flake-parts", "type": "github" } }, @@ -34,26 +34,26 @@ "type": "github" } }, - "root": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" - } - }, - "systems": { + "nixpkgs-lib": { "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "lastModified": 1765674936, + "narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85", "type": "github" }, "original": { - "owner": "nix-systems", - "repo": "default", + "owner": "nix-community", + "repo": "nixpkgs.lib", "type": "github" } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index be2c54d..9f5f63c 100644 --- a/flake.nix +++ b/flake.nix @@ -3,120 +3,122 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; - flake-utils.url = "github:numtide/flake-utils"; + flake-parts.url = "github:hercules-ci/flake-parts"; }; - outputs = { self, nixpkgs, flake-utils, ... }: - flake-utils.lib.eachDefaultSystem (system: - let - ## Import nixpkgs: - pkgs = import nixpkgs { inherit system; }; + outputs = inputs @ { nixpkgs, flake-parts, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ + ./nix/flake-modules/read-yaml + ]; - ## Load readYAML helper: - readYAML = pkgs.callPackage ./nix/read-yaml.nix { }; + systems = nixpkgs.lib.systems.flakeExposed; - ## Read package information: - package = readYAML ./package.yaml; + perSystem = { config, self', inputs', pkgs, system, readYAML, ... }: + let + ## Read package information: + package = readYAML ./package.yaml; - ## Get our Haskell: - thisHaskell = pkgs.haskellPackages.override { - overrides = self: super: { - ${package.name} = self.callCabal2nix package.name ./. { }; + ## Get our Haskell: + thisHaskell = pkgs.haskellPackages.override { + overrides = self: super: { + ${package.name} = self.callCabal2nix package.name ./. { }; + }; }; - }; - - ## Get the cabal-verify command: - cabal-verify = pkgs.callPackage ./nix/cabal-verify { }; - - ## Prepare Nix shell: - thisShell = thisHaskell.shellFor { - ## Define packages for the shell: - packages = p: [ p.${package.name} ]; - - ## Enable Hoogle: - withHoogle = false; - - ## Build inputs for development shell: - buildInputs = [ - ## Haskell related build inputs: - ## TODO: Once we are on ghc > 9.10, enable apply-refact again. - # thisHaskell.apply-refact - thisHaskell.cabal-fmt - thisHaskell.cabal-install - thisHaskell.cabal2nix - thisHaskell.fourmolu - thisHaskell.haskell-language-server - thisHaskell.hlint - thisHaskell.hpack - thisHaskell.weeder - - ## Our development scripts: - cabal-verify - - ## Other build inputs for various development requirements: - pkgs.docker-client - pkgs.git - pkgs.nil - pkgs.nixpkgs-fmt - pkgs.nodePackages.prettier - pkgs.upx - ]; - }; - thisPackage = pkgs.haskell.lib.justStaticExecutables ( - thisHaskell.${package.name}.overrideAttrs (oldAttrs: { - nativeBuildInputs = (oldAttrs.nativeBuildInputs or [ ]) ++ [ + ## Get the cabal-verify command: + cabal-verify = pkgs.callPackage ./nix/cabal-verify { }; + + ## Prepare Nix shell: + thisShell = thisHaskell.shellFor { + ## Define packages for the shell: + packages = p: [ p.${package.name} ]; + + ## Enable Hoogle: + withHoogle = false; + + ## Build inputs for development shell: + buildInputs = [ + ## Haskell related build inputs: + ## TODO: Once we are on ghc > 9.10, enable apply-refact again. + # thisHaskell.apply-refact + thisHaskell.cabal-fmt + thisHaskell.cabal-install + thisHaskell.cabal2nix + thisHaskell.fourmolu + thisHaskell.haskell-language-server + thisHaskell.hlint + thisHaskell.hpack + thisHaskell.weeder + + ## Our development scripts: + cabal-verify + + ## Other build inputs for various development requirements: + pkgs.docker-client pkgs.git - pkgs.makeWrapper - pkgs.ronn + pkgs.nil + pkgs.nixpkgs-fmt + pkgs.nodePackages.prettier + pkgs.upx ]; + }; - postFixup = (oldAttrs.postFixup or "") + '' - ## Create output directories: - mkdir -p $out/{bin} - - ## Wrap program to add PATHs to dependencies: - wrapProgram $out/bin/${package.name} --prefix PATH : ${pkgs.lib.makeBinPath []} + thisPackage = pkgs.haskell.lib.justStaticExecutables ( + thisHaskell.${package.name}.overrideAttrs (oldAttrs: { + nativeBuildInputs = (oldAttrs.nativeBuildInputs or [ ]) ++ [ + pkgs.git + pkgs.makeWrapper + pkgs.ronn + ]; + + postFixup = (oldAttrs.postFixup or "") + '' + ## Create output directories: + mkdir -p $out/{bin} + + ## Wrap program to add PATHs to dependencies: + wrapProgram $out/bin/${package.name} --prefix PATH : ${pkgs.lib.makeBinPath []} + ''; + }) + ); + + thisDocker = pkgs.dockerTools.buildImage { + name = "${package.name}"; + tag = "v${package.version}"; + created = "now"; + + copyToRoot = pkgs.buildEnv { + name = "image-root"; + paths = [ pkgs.cacert ]; + pathsToLink = [ "/etc" ]; + }; + + runAsRoot = '' + #!${pkgs.runtimeShell} + ${pkgs.dockerTools.shadowSetup} + groupadd -r users + useradd -r -g users patron ''; - }) - ); - - thisDocker = pkgs.dockerTools.buildImage { - name = "${package.name}"; - tag = "v${package.version}"; - created = "now"; - - copyToRoot = pkgs.buildEnv { - name = "image-root"; - paths = [ pkgs.cacert ]; - pathsToLink = [ "/etc" ]; - }; - runAsRoot = '' - #!${pkgs.runtimeShell} - ${pkgs.dockerTools.shadowSetup} - groupadd -r users - useradd -r -g users patron - ''; - - config = { - User = "patron"; - Entrypoint = [ "${thisPackage}/bin/${package.name}" ]; - Cmd = null; + config = { + User = "patron"; + Entrypoint = [ "${thisPackage}/bin/${package.name}" ]; + Cmd = null; + }; + }; + in + { + ## Project packages output: + packages = { + "${package.name}" = thisPackage; + docker = thisDocker; + default = thisPackage; }; - }; - in - { - ## Project packages output: - packages = { - "${package.name}" = thisPackage; - docker = thisDocker; - default = self.packages.${system}.${package.name}; - }; - ## Project development shell output: - devShells = { - default = thisShell; + ## Project development shell output: + devShells = { + default = thisShell; + }; }; - }); + }; } diff --git a/nix/flake-modules/read-yaml/default.nix b/nix/flake-modules/read-yaml/default.nix new file mode 100644 index 0000000..1d14a26 --- /dev/null +++ b/nix/flake-modules/read-yaml/default.nix @@ -0,0 +1,7 @@ +{ ... }: { + perSystem = { pkgs, ... }: { + _module.args = { + readYAML = pkgs.callPackage ./function.nix { }; + }; + }; +} diff --git a/nix/read-yaml.nix b/nix/flake-modules/read-yaml/function.nix similarity index 100% rename from nix/read-yaml.nix rename to nix/flake-modules/read-yaml/function.nix From 22e94c06d56833521f45235f9422190335405647 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Fri, 26 Dec 2025 12:04:08 +0800 Subject: [PATCH 04/19] test(ci): use a lighter shell for CI tests --- .github/workflows/check.yaml | 2 +- flake.nix | 80 ++++++++++++++++++++---------------- 2 files changed, 45 insertions(+), 37 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 47d7a3d..45a69cb 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -17,7 +17,7 @@ jobs: - name: "Check, Test and Build" run: | - nix develop --command cabal verify + nix develop .#ci --command cabal verify - name: "Build Docker Image" run: | diff --git a/flake.nix b/flake.nix index 9f5f63c..92a9eec 100644 --- a/flake.nix +++ b/flake.nix @@ -26,42 +26,50 @@ }; }; - ## Get the cabal-verify command: - cabal-verify = pkgs.callPackage ./nix/cabal-verify { }; - - ## Prepare Nix shell: - thisShell = thisHaskell.shellFor { - ## Define packages for the shell: + ## Common build inputs for both development and CI environments: + buildInputsCommon = [ + ## Essential Haskell tools: + thisHaskell.cabal-install + thisHaskell.fourmolu + thisHaskell.hlint + thisHaskell.hpack + thisHaskell.stan + thisHaskell.weeder + + ## Other essentials: + pkgs.git + pkgs.nixpkgs-fmt + pkgs.prettier + + ## Our development scripts: + (pkgs.callPackage ./nix/cabal-verify { }) + ]; + + ## Development-only inputs: + buildInputsDevOnly = [ + ## Haskell development tools: + thisHaskell.haskell-language-server + thisHaskell.cabal-fmt + thisHaskell.cabal2nix + + ## Other development tools: + pkgs.docker-client + pkgs.nil + pkgs.upx + ]; + + ## Development shell: + devShell = thisHaskell.shellFor { packages = p: [ p.${package.name} ]; - - ## Enable Hoogle: withHoogle = false; + buildInputs = buildInputsCommon ++ buildInputsDevOnly; + }; - ## Build inputs for development shell: - buildInputs = [ - ## Haskell related build inputs: - ## TODO: Once we are on ghc > 9.10, enable apply-refact again. - # thisHaskell.apply-refact - thisHaskell.cabal-fmt - thisHaskell.cabal-install - thisHaskell.cabal2nix - thisHaskell.fourmolu - thisHaskell.haskell-language-server - thisHaskell.hlint - thisHaskell.hpack - thisHaskell.weeder - - ## Our development scripts: - cabal-verify - - ## Other build inputs for various development requirements: - pkgs.docker-client - pkgs.git - pkgs.nil - pkgs.nixpkgs-fmt - pkgs.nodePackages.prettier - pkgs.upx - ]; + ## CI shell (minimal, fast): + ciShell = thisHaskell.shellFor { + packages = p: [ p.${package.name} ]; + withHoogle = false; + buildInputs = buildInputsCommon; }; thisPackage = pkgs.haskell.lib.justStaticExecutables ( @@ -69,7 +77,6 @@ nativeBuildInputs = (oldAttrs.nativeBuildInputs or [ ]) ++ [ pkgs.git pkgs.makeWrapper - pkgs.ronn ]; postFixup = (oldAttrs.postFixup or "") + '' @@ -115,9 +122,10 @@ default = thisPackage; }; - ## Project development shell output: + ## Project development shells: devShells = { - default = thisShell; + default = devShell; + ci = ciShell; }; }; }; From 68a4703fd1f487de4b7aa158136c5c1019356968 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Fri, 26 Dec 2025 12:25:14 +0800 Subject: [PATCH 05/19] test(ci): revisit check workflow, separate devShell preparation step --- .github/workflows/{check.yaml => check-verify.yaml} | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) rename .github/workflows/{check.yaml => check-verify.yaml} (59%) diff --git a/.github/workflows/check.yaml b/.github/workflows/check-verify.yaml similarity index 59% rename from .github/workflows/check.yaml rename to .github/workflows/check-verify.yaml index 45a69cb..c20daf1 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check-verify.yaml @@ -1,4 +1,4 @@ -name: "Check, Test and Build Codebase" +name: "Check, Lint, Test and Build Codebase" on: pull_request: @@ -10,12 +10,16 @@ jobs: steps: - name: "Checkout Codebase" - uses: "actions/checkout@v4" + uses: "actions/checkout@v6" - name: "Install Nix" - uses: "DeterminateSystems/nix-installer-action@v17" + uses: "DeterminateSystems/nix-installer-action@v21" - - name: "Check, Test and Build" + - name: "Prepare CI devShell" + run: | + nix develop .#ci --command true + + - name: "Verify Codebase" run: | nix develop .#ci --command cabal verify From f5354f0cbf18ad326dc6f15d2af91d6898889778 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Fri, 26 Dec 2025 12:55:32 +0800 Subject: [PATCH 06/19] chore(ci): check static executable builds on demand --- .github/workflows/check-static.yaml | 16 ++++++ .gitignore | 2 + build-static.sh | 75 ++++++++++------------------- flake.nix | 1 - stack.yaml | 31 ++++++++++++ 5 files changed, 74 insertions(+), 51 deletions(-) create mode 100644 .github/workflows/check-static.yaml create mode 100644 stack.yaml diff --git a/.github/workflows/check-static.yaml b/.github/workflows/check-static.yaml new file mode 100644 index 0000000..3570368 --- /dev/null +++ b/.github/workflows/check-static.yaml @@ -0,0 +1,16 @@ +name: "Check Static Build" + +on: + workflow_dispatch: + +jobs: + check: + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout Codebase" + uses: "actions/checkout@v6" + + - name: "Build Static Exectutable" + run: | + bash ./build-static.sh diff --git a/.gitignore b/.gitignore index 9c826fb..5a72982 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,9 @@ *~ /.direnv /.envrc +/.stack-work /dist /dist-newstyle /result +/stack.yaml.lock /tmp diff --git a/build-static.sh b/build-static.sh index eb2c2ef..f6374f1 100644 --- a/build-static.sh +++ b/build-static.sh @@ -1,33 +1,23 @@ #!/usr/bin/env bash ## NOTE: Things would be much easier if we could use Nix, but we can -## not (or I find it rather tedious). So, we have to use Docker. -## -## Also, `cabal install` does not work with -## `--enable-executable-static` flag. So, we have to use `cabal build` -## instead. Finally, `cabal build` does not work with -## `--enable-executable-stripping`, hence the `strip` command usage. +## not (or I find it rather tedious). So, we have to use Docker and +## Haskell Stack to build our static binary. -## Get extra parameters for docker run: -docker_run_opts=("$@") +## Executable name: +EXECUTABLE_NAME="$(yq ".executables | keys | .[0]" package.yaml)" + +## Stackage resolver: +STACKAGE_RESOLVER="$(yq ".resolver" stack.yaml)" ## GHC version: -GHC_VERSION="9.8.4" +GHC_VERSION="$(curl -s "https://www.stackage.org/${STACKAGE_RESOLVER}" | grep -oP 'ghc-\K[0-9.]+' | head -n1)" ## Docker image: DOCKER_IMAGE="quay.io/benz0li/ghc-musl:${GHC_VERSION}" -## Executable name: -EXECUTABLE_NAME="haskell-template-hebele" - -## Get the kernel name: -FINAL_KERNEL_NAME="$(docker run "${docker_run_opts[@]}" --rm "${DOCKER_IMAGE}" uname --kernel-name)" - -## Get the machine architecture: -FINAL_MACHINE_ARCH="$(docker run "${docker_run_opts[@]}" --rm "${DOCKER_IMAGE}" uname --machine)" - ## Final executable name: -FINAL_EXECUTABLE_NAME="${EXECUTABLE_NAME}-static-${FINAL_KERNEL_NAME}-${FINAL_MACHINE_ARCH}" +FINAL_EXECUTABLE_NAME="${EXECUTABLE_NAME}-static-$(uname --kernel-name | tr '[:upper:]' '[:lower:]')-$(uname --machine)" ## Final executable path: FINAL_EXECUTABLE_PATH="/tmp/${FINAL_EXECUTABLE_NAME}" @@ -35,49 +25,34 @@ FINAL_EXECUTABLE_PATH="/tmp/${FINAL_EXECUTABLE_NAME}" ## Docker container name: CONTAINER_NAME="static-builder-for-${EXECUTABLE_NAME}" -echo "Docker image: ${DOCKER_IMAGE}" -echo "Docker container name: ${CONTAINER_NAME}" -echo "Final executable name: ${FINAL_EXECUTABLE_NAME}" -echo "Final executable path: ${FINAL_EXECUTABLE_PATH}" -echo "Building static binary for ${FINAL_KERNEL_NAME} on ${FINAL_MACHINE_ARCH} using GHC ${GHC_VERSION}" - -## Create/update .cabal file: -hpack - -## Cleanup first: -cabal clean -cabal v1-clean - -## First, pin all packages as per Nix: -cabal freeze - ## Run the Docker container: -docker run "${docker_run_opts[@]}" -i --detach -v "$(pwd):/app" --name "${CONTAINER_NAME}" "${DOCKER_IMAGE}" /bin/bash +docker run -i --detach -v "$(pwd):/app" --name "${CONTAINER_NAME}" "${DOCKER_IMAGE}" /bin/bash ## Whitelist codebase directory for Git queries: docker exec "${CONTAINER_NAME}" git config --global --add safe.directory /app -## Update cabal database: -docker exec "${CONTAINER_NAME}" cabal update +## Cleanup inside the container: +docker exec -w "/app" "${CONTAINER_NAME}" cabal clean +docker exec -w "/app" "${CONTAINER_NAME}" cabal v1-clean +docker exec -w "/app" "${CONTAINER_NAME}" stack clean --full ## Build the static binary: -docker exec -w "/app" "${CONTAINER_NAME}" cabal build --enable-executable-static +docker exec -w "/app" "${CONTAINER_NAME}" stack build -## Get the path to the executable: -BUILD_PATH="$(docker exec -w "/app" "${CONTAINER_NAME}" cabal list-bin "${EXECUTABLE_NAME}")" +## Install the static binary to our local-bin-path (/tmp): +docker exec -w "/app" "${CONTAINER_NAME}" stack install -## Strip debugging symbols: -docker exec "${CONTAINER_NAME}" strip "${BUILD_PATH}" - -## Copy the binary to the host: -docker cp "${CONTAINER_NAME}:${BUILD_PATH}" "${FINAL_EXECUTABLE_PATH}" +## Install upx: +docker exec -w "/app" "${CONTAINER_NAME}" apk add upx ## Compress the executable: -upx "${FINAL_EXECUTABLE_PATH}" +docker exec -w "/app" "${CONTAINER_NAME}" upx "/tmp/${EXECUTABLE_NAME}" + +## Copy the binary to the host: +docker cp "${CONTAINER_NAME}:/tmp/${EXECUTABLE_NAME}" "${FINAL_EXECUTABLE_PATH}" ## Cleanup: -docker exec -w "/app" "${CONTAINER_NAME}" cabal clean -docker exec -w "/app" "${CONTAINER_NAME}" cabal v1-clean +docker exec -w "/app" "${CONTAINER_NAME}" stack clean --full docker rm -f "${CONTAINER_NAME}" -rm cabal.project.freeze file "${FINAL_EXECUTABLE_PATH}" +find "${FINAL_EXECUTABLE_PATH}" diff --git a/flake.nix b/flake.nix index 92a9eec..e8d9315 100644 --- a/flake.nix +++ b/flake.nix @@ -55,7 +55,6 @@ ## Other development tools: pkgs.docker-client pkgs.nil - pkgs.upx ]; ## Development shell: diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..6687f3d --- /dev/null +++ b/stack.yaml @@ -0,0 +1,31 @@ +## This resolver should correspond to our nixpkgs version (at least baseline): +resolver: lts-24.25 + +## Our local packages: +packages: + - . + +## Use the GHC provided by our Docker build image; never download another one: +system-ghc: true +install-ghc: false + +## Allow running as a different user inside Docker containers: +allow-different-user: true + +## Set the output path for our compiled binaries (stack install): +local-bin-path: /tmp + +## Configure Cabal to build static executables (no shared/dynamic): +configure-options: + "$locals": + - --disable-shared + - --disable-executable-dynamic + - --enable-executable-static + +## Make smaller libs without any profiling: +build: + library-profiling: false + library-stripping: true + +## Extra dependencies pinned to specific commits: +extra-deps: [] From 74cbcd9a5974a0e213c88eb8b6e55c9732ecd937 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Fri, 26 Dec 2025 18:23:24 +0800 Subject: [PATCH 07/19] feat(test): add taplo for linting and formatting TOML files --- .stan.toml | 6 +++--- .taplo.toml | 25 +++++++++++++++++++++++++ flake.nix | 1 + nix/cabal-verify/script.sh | 6 ++++++ 4 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 .taplo.toml diff --git a/.stan.toml b/.stan.toml index 2b8e44b..54246a4 100644 --- a/.stan.toml +++ b/.stan.toml @@ -3,6 +3,6 @@ # In serveral places Stack uses 4-tuples and in one place Stack uses a # 5-tuple. [[check]] - id = "STAN-0302" - scope = "all" - type = "Exclude" +id = "STAN-0302" +scope = "all" +type = "Exclude" diff --git a/.taplo.toml b/.taplo.toml new file mode 100644 index 0000000..93da9dd --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,25 @@ +#:schema taplo://taplo.toml + +include = ["*.toml"] +exclude = [] + +[formatting] + +align_entries = false # Align entries vertically. Entries that have table headers, comments, or blank lines between them are not aligned.(default false) +align_comments = true # Align consecutive comments after entries and items vertically. This applies to comments that are after entries or array items.(default true) +array_trailing_comma = true # Put trailing commas for multiline arrays.(default true) +array_auto_expand = true # Automatically expand arrays to multiple lines (default true) +array_auto_collapse = false # Automatically collapse arrays if they fit in one line.(default true) +compact_arrays = true # Omit whitespace padding inside single-line arrays.(default true) +compact_inline_tables = false # Omit whitespace padding inside inline tables.(default false) +inline_table_expand = true # Expand values (e.g. arrays) inside inline tables.(default true) +compact_entries = false # Omit whitespace around =. (default false) +column_width = 80 # Target maximum column width after which arrays are expanded into new lines.(default 80) +indent_tables = false # Indent subtables if they come in order(default false) +indent_entries = false # Indent entries under tables.(default false) +indent_string = " " # Indentation to use, should be tabs or spaces but technically could be anything. 2 spaces (" ") +trailing_newline = true # Add trailing newline to the source. (default true) +reorder_keys = false # Alphabetically reorder keys that are not separated by blank lines. (default false) +reorder_arrays = false # Alphabetically reorder array values that are not separated by blank lines. (default false) +allowed_blank_lines = 1 # The maximum amount of consecutive blank lines allowed. (default 2) +crlf = false # Use CRLF line endings. (default false) diff --git a/flake.nix b/flake.nix index e8d9315..a435e95 100644 --- a/flake.nix +++ b/flake.nix @@ -40,6 +40,7 @@ pkgs.git pkgs.nixpkgs-fmt pkgs.prettier + pkgs.taplo ## Our development scripts: (pkgs.callPackage ./nix/cabal-verify { }) diff --git a/nix/cabal-verify/script.sh b/nix/cabal-verify/script.sh index 160d3f0..e747bc6 100644 --- a/nix/cabal-verify/script.sh +++ b/nix/cabal-verify/script.sh @@ -140,6 +140,12 @@ _run_check "prettier (v$(prettier --version))" \ _run_check "nixpkgs-fmt (v$(nixpkgs-fmt --version 2>&1 | cut -d' ' -f2))" \ find . -iname "*.nix" -exec nixpkgs-fmt --check {} \; +_run_check "taplo lint (v$(taplo --version | cut -f2 -d" "))" \ + taplo lint + +_run_check "taplo format (v$(taplo --version | cut -f2 -d" "))" \ + taplo format --check + _run_check "hlint (v$(hlint --numeric-version))" \ hlint app/ src/ test/ From 6a1fff13675408cab99bd4e33737def13fed27c6 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Fri, 26 Dec 2025 18:52:46 +0800 Subject: [PATCH 08/19] feat(nix): switch from nixpkgs-fmt to nixfmt for Nix formatting --- flake.nix | 2 +- nix/cabal-verify/script.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index a435e95..4d2d9d9 100644 --- a/flake.nix +++ b/flake.nix @@ -38,7 +38,7 @@ ## Other essentials: pkgs.git - pkgs.nixpkgs-fmt + pkgs.nixfmt-rfc-style pkgs.prettier pkgs.taplo diff --git a/nix/cabal-verify/script.sh b/nix/cabal-verify/script.sh index e747bc6..9fa4b99 100644 --- a/nix/cabal-verify/script.sh +++ b/nix/cabal-verify/script.sh @@ -137,8 +137,8 @@ _run_check "fourmolu (v$(fourmolu --version | head -n1 | cut -d' ' -f2))" \ _run_check "prettier (v$(prettier --version))" \ prettier --check . -_run_check "nixpkgs-fmt (v$(nixpkgs-fmt --version 2>&1 | cut -d' ' -f2))" \ - find . -iname "*.nix" -exec nixpkgs-fmt --check {} \; +_run_check "nixfmt (v$(nixfmt --version | cut -f2 -d" "))" \ + find . -iname "*.nix" -exec nixfmt --check {} + _run_check "taplo lint (v$(taplo --version | cut -f2 -d" "))" \ taplo lint From 950aff618375d6e0bf28342cdb50dee280976928 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Fri, 26 Dec 2025 19:01:51 +0800 Subject: [PATCH 09/19] refactor(test): revisit cabal-verify script --- nix/cabal-verify/script.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/nix/cabal-verify/script.sh b/nix/cabal-verify/script.sh index 9fa4b99..dd8d401 100644 --- a/nix/cabal-verify/script.sh +++ b/nix/cabal-verify/script.sh @@ -131,21 +131,21 @@ ${_clean} && _run_check "clean" "${_cabal}" clean && _run_check "v1-clean" "${_c _run_check "hpack (v$(hpack --numeric-version))" \ hpack -_run_check "fourmolu (v$(fourmolu --version | head -n1 | cut -d' ' -f2))" \ - fourmolu --quiet --mode check app/ src/ test/ +_run_check "nixfmt (v$(nixfmt --numeric-version))" \ + find . -type f -iname "*.nix" -exec nixfmt --check {} + _run_check "prettier (v$(prettier --version))" \ prettier --check . -_run_check "nixfmt (v$(nixfmt --version | cut -f2 -d" "))" \ - find . -iname "*.nix" -exec nixfmt --check {} + - _run_check "taplo lint (v$(taplo --version | cut -f2 -d" "))" \ taplo lint _run_check "taplo format (v$(taplo --version | cut -f2 -d" "))" \ taplo format --check +_run_check "fourmolu (v$(fourmolu --version | head -n1 | cut -f2 -d" "))" \ + fourmolu --quiet --mode check app/ src/ test/ + _run_check "hlint (v$(hlint --numeric-version))" \ hlint app/ src/ test/ @@ -158,10 +158,10 @@ _run_check "cabal run (v$("${_cabal}" --numeric-version))" \ _run_check "cabal test (v$("${_cabal}" --numeric-version))" \ "${_cabal}" v1-test --ghc-options="-O0" -_run_check "weeder (v$(weeder --version | head -n1 | cut -d' ' -f3))" \ +_run_check "weeder (v$(weeder --version | head -n1 | cut -f3 -d" "))" \ weeder -_run_check "stan ($(stan --version | head -n 1 | cut -f 2 -d " "))" \ +_run_check "stan ($(stan --version | head -n1 | cut -f2 -d" "))" \ stan --hiedir ./dist-newstyle _run_check "cabal haddock (v$("${_cabal}" --numeric-version))" \ From 482167172e64dee5008848f90e3a60661f23db65 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Fri, 26 Dec 2025 19:07:14 +0800 Subject: [PATCH 10/19] feat(test): add Nix linter (statix) --- flake.nix | 1 + nix/cabal-verify/script.sh | 3 +++ 2 files changed, 4 insertions(+) diff --git a/flake.nix b/flake.nix index 4d2d9d9..4e95d6c 100644 --- a/flake.nix +++ b/flake.nix @@ -40,6 +40,7 @@ pkgs.git pkgs.nixfmt-rfc-style pkgs.prettier + pkgs.statix pkgs.taplo ## Our development scripts: diff --git a/nix/cabal-verify/script.sh b/nix/cabal-verify/script.sh index dd8d401..9444a24 100644 --- a/nix/cabal-verify/script.sh +++ b/nix/cabal-verify/script.sh @@ -134,6 +134,9 @@ _run_check "hpack (v$(hpack --numeric-version))" \ _run_check "nixfmt (v$(nixfmt --numeric-version))" \ find . -type f -iname "*.nix" -exec nixfmt --check {} + +_run_check "statix (v$(statix --version | cut -f2 -d" "))" \ + statix check + _run_check "prettier (v$(prettier --version))" \ prettier --check . From b03b7fda63e55d643f50c9dcaf24cd6294d043f8 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Fri, 26 Dec 2025 19:18:41 +0800 Subject: [PATCH 11/19] feat(test): add shellcheck to test suite --- flake.nix | 1 + nix/cabal-verify/script.sh | 3 +++ 2 files changed, 4 insertions(+) diff --git a/flake.nix b/flake.nix index 4e95d6c..09d160d 100644 --- a/flake.nix +++ b/flake.nix @@ -40,6 +40,7 @@ pkgs.git pkgs.nixfmt-rfc-style pkgs.prettier + pkgs.shellcheck pkgs.statix pkgs.taplo diff --git a/nix/cabal-verify/script.sh b/nix/cabal-verify/script.sh index 9444a24..f9ac6a2 100644 --- a/nix/cabal-verify/script.sh +++ b/nix/cabal-verify/script.sh @@ -137,6 +137,9 @@ _run_check "nixfmt (v$(nixfmt --numeric-version))" \ _run_check "statix (v$(statix --version | cut -f2 -d" "))" \ statix check +_run_check "shellcheck (v$(shellcheck --version | grep "^version" | head -n1 | cut -f2 -d" "))" \ + find . -type f -iname "*.sh" -exec shellcheck {} + + _run_check "prettier (v$(prettier --version))" \ prettier --check . From 1d659debfec1171b5ec82db7b9b586f098bc33b6 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Fri, 26 Dec 2025 19:24:33 +0800 Subject: [PATCH 12/19] feat(nix): add shfmt for shell-script formatting --- flake.nix | 1 + nix/cabal-verify/script.sh | 3 +++ 2 files changed, 4 insertions(+) diff --git a/flake.nix b/flake.nix index 09d160d..ea2fc91 100644 --- a/flake.nix +++ b/flake.nix @@ -41,6 +41,7 @@ pkgs.nixfmt-rfc-style pkgs.prettier pkgs.shellcheck + pkgs.shfmt pkgs.statix pkgs.taplo diff --git a/nix/cabal-verify/script.sh b/nix/cabal-verify/script.sh index f9ac6a2..3e5cdf3 100644 --- a/nix/cabal-verify/script.sh +++ b/nix/cabal-verify/script.sh @@ -137,6 +137,9 @@ _run_check "nixfmt (v$(nixfmt --numeric-version))" \ _run_check "statix (v$(statix --version | cut -f2 -d" "))" \ statix check +_run_check "shfmt (v$(shfmt --version))" \ + find . -type f -iname "*.sh" -exec shfmt --diff {} + + _run_check "shellcheck (v$(shellcheck --version | grep "^version" | head -n1 | cut -f2 -d" "))" \ find . -type f -iname "*.sh" -exec shellcheck {} + From b7351b3984c7b44e896271d6fc0e0a0a2f0d27ec Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Fri, 26 Dec 2025 19:27:52 +0800 Subject: [PATCH 13/19] refactor(nix): reformat .nix files --- flake.nix | 16 +++++++++++++--- nix/cabal-verify/default.nix | 14 +++++++------- nix/flake-modules/read-yaml/default.nix | 13 ++++++++----- nix/flake-modules/read-yaml/function.nix | 8 +++----- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/flake.nix b/flake.nix index ea2fc91..c9128bb 100644 --- a/flake.nix +++ b/flake.nix @@ -6,7 +6,8 @@ flake-parts.url = "github:hercules-ci/flake-parts"; }; - outputs = inputs @ { nixpkgs, flake-parts, ... }: + outputs = + inputs@{ nixpkgs, flake-parts, ... }: flake-parts.lib.mkFlake { inherit inputs; } { imports = [ ./nix/flake-modules/read-yaml @@ -14,7 +15,16 @@ systems = nixpkgs.lib.systems.flakeExposed; - perSystem = { config, self', inputs', pkgs, system, readYAML, ... }: + perSystem = + { + config, + self', + inputs', + pkgs, + system, + readYAML, + ... + }: let ## Read package information: package = readYAML ./package.yaml; @@ -87,7 +97,7 @@ mkdir -p $out/{bin} ## Wrap program to add PATHs to dependencies: - wrapProgram $out/bin/${package.name} --prefix PATH : ${pkgs.lib.makeBinPath []} + wrapProgram $out/bin/${package.name} --prefix PATH : ${pkgs.lib.makeBinPath [ ]} ''; }) ); diff --git a/nix/cabal-verify/default.nix b/nix/cabal-verify/default.nix index 9cc0903..f385fba 100644 --- a/nix/cabal-verify/default.nix +++ b/nix/cabal-verify/default.nix @@ -1,9 +1,10 @@ -{ lib -, writeShellApplication -, bash -, coreutils -, moreutils -, yq-go +{ + lib, + writeShellApplication, + bash, + coreutils, + moreutils, + yq-go, }: writeShellApplication { @@ -23,4 +24,3 @@ writeShellApplication { platforms = platforms.all; }; } - diff --git a/nix/flake-modules/read-yaml/default.nix b/nix/flake-modules/read-yaml/default.nix index 1d14a26..c1acf21 100644 --- a/nix/flake-modules/read-yaml/default.nix +++ b/nix/flake-modules/read-yaml/default.nix @@ -1,7 +1,10 @@ -{ ... }: { - perSystem = { pkgs, ... }: { - _module.args = { - readYAML = pkgs.callPackage ./function.nix { }; +{ ... }: +{ + perSystem = + { pkgs, ... }: + { + _module.args = { + readYAML = pkgs.callPackage ./function.nix { }; + }; }; - }; } diff --git a/nix/flake-modules/read-yaml/function.nix b/nix/flake-modules/read-yaml/function.nix index d89721a..472d172 100644 --- a/nix/flake-modules/read-yaml/function.nix +++ b/nix/flake-modules/read-yaml/function.nix @@ -18,10 +18,8 @@ path: let - jsonOutputDrv = - runCommand - "from-yaml" - { nativeBuildInputs = [ remarshal ]; } - "remarshal -if yaml -i \"${path}\" -of json -o \"$out\""; + jsonOutputDrv = runCommand "from-yaml" { + nativeBuildInputs = [ remarshal ]; + } "remarshal -if yaml -i \"${path}\" -of json -o \"$out\""; in builtins.fromJSON (builtins.readFile jsonOutputDrv) From f4711d60f93b86e1d6fea487f0d734cb836568eb Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Fri, 26 Dec 2025 19:29:35 +0800 Subject: [PATCH 14/19] refactor(nix): attend Nix lint warnings --- nix/flake-modules/read-yaml/default.nix | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nix/flake-modules/read-yaml/default.nix b/nix/flake-modules/read-yaml/default.nix index c1acf21..689b80d 100644 --- a/nix/flake-modules/read-yaml/default.nix +++ b/nix/flake-modules/read-yaml/default.nix @@ -1,5 +1,4 @@ -{ ... }: -{ +_: { perSystem = { pkgs, ... }: { From 4bc6bcffd90d387c654322bb3bee56fa6e846e0b Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Fri, 26 Dec 2025 19:31:29 +0800 Subject: [PATCH 15/19] refactor(sh): reformat .sh files --- run-template.sh | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/run-template.sh b/run-template.sh index af0df3d..74750e9 100644 --- a/run-template.sh +++ b/run-template.sh @@ -26,7 +26,7 @@ EOF read -p "Do you want to proceed? (y/N) " -n 1 -r if [[ ! $REPLY =~ ^[Yy]$ ]]; then - >&2 echo "Exiting..." + >&2 echo "Exiting..." fi echo @@ -89,28 +89,28 @@ EOF echo read -p "Do you want to proceed? (y/N) " -n 1 -r if [[ ! $REPLY =~ ^[Yy]$ ]]; then - >&2 echo "Exiting..." + >&2 echo "Exiting..." fi echo _files() { - find . -type d \( -path ./dist -o -path ./dist-newstyle -o -path ./.direnv -o -path ./.git \) -prune -o -type f -a -not -name "run-template.sh" -print + find . -type d \( -path ./dist -o -path ./dist-newstyle -o -path ./.direnv -o -path ./.git \) -prune -o -type f -a -not -name "run-template.sh" -print } _update() { - _file="${1}" - _fpath_def="$(echo "${_def_name}" | sed "s/\-/_/g")" - _fpath_new="$(echo "${_var_name}" | sed "s/\-/_/g")" - sed -i "s|${_def_github}|${_var_github}|g" "${_file}" - sed -i "s|${_def_copyright}|${_var_copyright}|g" "${_file}" - sed -i "s|${_def_author}|${_var_author}|g" "${_file}" - sed -i "s|${_def_maintainer}|${_var_maintainer}|g" "${_file}" - sed -i "s|${_def_name}|${_var_name}|g" "${_file}" - sed -i "s|${_def_title}|${_var_title}|g" "${_file}" - sed -i "s|${_fpath_def}|${_fpath_new}|g" "${_file}" + _file="${1}" + _fpath_def="$(echo "${_def_name}" | sed "s/\-/_/g")" + _fpath_new="$(echo "${_var_name}" | sed "s/\-/_/g")" + sed -i "s|${_def_github}|${_var_github}|g" "${_file}" + sed -i "s|${_def_copyright}|${_var_copyright}|g" "${_file}" + sed -i "s|${_def_author}|${_var_author}|g" "${_file}" + sed -i "s|${_def_maintainer}|${_var_maintainer}|g" "${_file}" + sed -i "s|${_def_name}|${_var_name}|g" "${_file}" + sed -i "s|${_def_title}|${_var_title}|g" "${_file}" + sed -i "s|${_fpath_def}|${_fpath_new}|g" "${_file}" } _files | sort | while read -r _path; do - echo "Processing ${_path}" - _update "${_path}" + echo "Processing ${_path}" + _update "${_path}" done From 8e6543832585c11eb522d967cefd7ddd7e01e7b5 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Fri, 26 Dec 2025 19:34:12 +0800 Subject: [PATCH 16/19] refactor(sh): attend shellcheck warnings --- run-template.sh | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/run-template.sh b/run-template.sh index 74750e9..c52b702 100644 --- a/run-template.sh +++ b/run-template.sh @@ -24,7 +24,7 @@ your answers. EOF -read -p "Do you want to proceed? (y/N) " -n 1 -r +read -rp "Do you want to proceed? (y/N) " -n 1 if [[ ! $REPLY =~ ^[Yy]$ ]]; then >&2 echo "Exiting..." fi @@ -33,42 +33,42 @@ echo echo echo "1/6. Project Name: The project name must be a valid Haskell package name. See ." echo -read -p "Enter the project name [${_def_name}]: " _var_name +read -rp "Enter the project name [${_def_name}]: " _var_name _var_name="${_var_name:-"${_def_name}"}" echo echo echo "2/6. Project Title: The project title appearing on the README header, CLI header etc..." echo -read -p "Enter the project title [${_def_title}]: " _var_title +read -rp "Enter the project title [${_def_title}]: " _var_title _var_title="${_var_title:-"${_def_title}"}" echo echo echo "3/6. GitHub Repository: This must be in the form of /." echo -read -p "Enter the GitHub repository [${_def_github}]: " _var_github +read -rp "Enter the GitHub repository [${_def_github}]: " _var_github _var_github="${_var_github:-"${_def_github}"}" echo echo echo "4/6. Author: Who is the author? See " echo -read -p "Enter the author [${_def_author}]: " _var_author +read -rp "Enter the author [${_def_author}]: " _var_author _var_author="${_var_author:-"${_def_author}"}" echo echo echo "5/6. Maintainer: Who is the maintainer? See " echo -read -p "Enter the maintainer [${_def_maintainer}]: " _var_maintainer +read -rp "Enter the maintainer [${_def_maintainer}]: " _var_maintainer _var_maintainer="${_var_maintainer:-"${_def_maintainer}"}" echo echo echo "6/6. Copyright: What is the copyright notice? See " echo -read -p "Enter the copyright [${_def_copyright}]: " _var_copyright +read -rp "Enter the copyright [${_def_copyright}]: " _var_copyright _var_copyright="${_var_copyright:-"${_def_copyright}"}" echo @@ -87,7 +87,7 @@ cat <&2 echo "Exiting..." fi @@ -99,8 +99,8 @@ _files() { _update() { _file="${1}" - _fpath_def="$(echo "${_def_name}" | sed "s/\-/_/g")" - _fpath_new="$(echo "${_var_name}" | sed "s/\-/_/g")" + _fpath_def="${_def_name//-/_}" + _fpath_new="${_var_name//-/_}" sed -i "s|${_def_github}|${_var_github}|g" "${_file}" sed -i "s|${_def_copyright}|${_var_copyright}|g" "${_file}" sed -i "s|${_def_author}|${_var_author}|g" "${_file}" From ad21da8a4ab2109affe7f88a967f8bea791dc95e Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Fri, 26 Dec 2025 19:45:02 +0800 Subject: [PATCH 17/19] fix(prettier): revisit prettier configuration --- .prettierignore | 2 +- .prettierrc.json | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.prettierignore b/.prettierignore index 9161ae5..3e84bb9 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,3 @@ +LICENSE.md dist-newstyle/ dist/ -*.md diff --git a/.prettierrc.json b/.prettierrc.json index e7ffdfe..51edb1d 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,6 +1,21 @@ { "tabWidth": 2, + "printWidth": 120, "singleQuote": false, "trailingComma": "es5", - "printWidth": 120 + "overrides": [ + { + "files": "*.md", + "options": { + "printWidth": 80, + "proseWrap": "always" + } + }, + { + "files": "package.yaml", + "options": { + "singleQuote": true + } + } + ] } From 9ef0536056d425897ece1b74cdf2509579733c27 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Fri, 26 Dec 2025 19:45:28 +0800 Subject: [PATCH 18/19] refactor(format): reformat codebase as per updated prettier config --- README.md | 30 ++++++++---------------------- package.yaml | 8 ++++---- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 5162714..ef7a82a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # Haskell Project Template -This is an opinionated template for creating Haskell projects. It uses -[Nix] [Flakes], [hpack] and [cabal]. +This is an opinionated template for creating Haskell projects. It uses [Nix] +[Flakes], [hpack] and [cabal]. > **TODO** Provide minimum viable documentation. ## Quickstart -Create your repository from this template, clone it on your computer -and enter its directory. +Create your repository from this template, clone it on your computer and enter +its directory. Then, run following to configure your project: @@ -16,8 +16,8 @@ Then, run following to configure your project: bash ./run-template.sh ``` -It will prompt some questions and configure your project according to -your answers. +It will prompt some questions and configure your project according to your +answers. Once it is configured, provision `direnv`. You can copy the `.envrc.tmpl`: @@ -49,22 +49,8 @@ rm run-template.sh ## Development -Big, long build command for the impatient: - -```sh -hpack && - direnv reload && - fourmolu -i app/ src/ test/ && - prettier --write . && - find . -iname "*.nix" -print0 | xargs --null nixpkgs-fmt && - hlint app/ src/ test/ && - cabal build -O0 && - cabal run -O0 haskell-template-hebele -- --version && - cabal v1-test && - cabal haddock -O0 -``` - -To run checks, linters, tests and build the codebase in the development environment, run: +To run checks, linters, tests and build the codebase in the development +environment, run: ```sh cabal-verify diff --git a/package.yaml b/package.yaml index 1d9bcbe..4f90a27 100644 --- a/package.yaml +++ b/package.yaml @@ -1,10 +1,10 @@ name: haskell-template-hebele version: 0.0.0 -github: "vst/haskell-template-hebele" +github: 'vst/haskell-template-hebele' license: MIT -author: "Vehbi Sinan Tunalioglu" -maintainer: "vst@vsthost.com" -copyright: "Copyright (c) 2024 Vehbi Sinan Tunalioglu" +author: 'Vehbi Sinan Tunalioglu' +maintainer: 'vst@vsthost.com' +copyright: 'Copyright (c) 2024 Vehbi Sinan Tunalioglu' extra-source-files: - README.md From 453385639b5c01239eead9897e6a934921f6eda7 Mon Sep 17 00:00:00 2001 From: Vehbi Sinan Tunalioglu Date: Fri, 26 Dec 2025 20:02:48 +0800 Subject: [PATCH 19/19] docs: update README --- README.md | 47 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ef7a82a..d7e4d94 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,25 @@ This is an opinionated template for creating Haskell projects. It uses [Nix] [Flakes], [hpack] and [cabal]. -> **TODO** Provide minimum viable documentation. +## Features + +- Nix flake-based dev/CI shells with a pinned toolchain (nixpkgs + flake-parts). +- hpack-driven Cabal setup (`package.yaml` -> `*.cabal`). +- `cabal-verify` for a full verify pass: format, lint, build, tests, docs. +- Static executable builds for Nix (`justStaticExecutables`) plus a Docker + image. +- `build-static.sh` for producing a musl-linked static binary via Stack. +- Preconfigured formatting and linting tools (fourmolu, hlint, weeder, stan, + nixfmt, statix, shfmt, shellcheck, taplo, prettier). +- Template bootstrap script (`run-template.sh`) to rename and configure the + project. ## Quickstart Create your repository from this template, clone it on your computer and enter its directory. -Then, run following to configure your project: +Then run the following to configure your project: ```sh bash ./run-template.sh @@ -39,8 +50,6 @@ development environment: nix develop ``` -And run the big, long build command as given in the next section. - Finally, you can remove the `run-template.sh` script: ```sh @@ -68,9 +77,39 @@ As of Cabal 3.12, you can now run the above as an external `cabal` command: cabal verify [-c|--clean] ``` +`cabal-verify` is the "all checks" entrypoint. It runs, in order: + +- `hpack` to regenerate the `.cabal` file. +- Format/lint for Nix, shell, TOML, Markdown/JSON, and Haskell. +- `cabal build`, a basic `cabal run` (with `--version`), and `cabal test`. +- `weeder` for dead code, `stan` for static analysis, and `cabal haddock`. + +The script stops on the first failure and prints the captured output to keep CI +logs readable. + +## Static compilation + +For a portable, fully static binary (musl-linked), run: + +```sh +./build-static.sh +``` + +This script uses [Docker], [Stack], and [ghc-musl], then compresses the binary +with `upx` and copies it to `/tmp/-static--`. + +We keep a `stack.yaml` because Stack is the most reliable way to build a static +musl binary without Nix. The resolver should match the nixpkgs baseline Haskell +package set (and thus the [Stackage LTS] used by nixpkgs) to avoid GHC or +dependency mismatches between Nix and the static build pipeline. + [Nix]: https://nixos.org [Flakes]: https://wiki.nixos.org/wiki/Flakes [hpack]: https://github.com/sol/hpack [cabal]: https://www.haskell.org/cabal +[Docker]: https://www.docker.com +[Stack]: https://docs.haskellstack.org/en/stable/ +[ghc-musl]: https://github.com/benz0li/ghc-musl +[Stackage LTS]: https://www.stackage.org/lts