diff --git a/.github/workflows/telomare-ci.yml b/.github/workflows/telomare-ci.yml index ed69e10..18e6105 100644 --- a/.github/workflows/telomare-ci.yml +++ b/.github/workflows/telomare-ci.yml @@ -10,18 +10,18 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: cachix/install-nix-action@v25 + - uses: cachix/install-nix-action@v31 with: install_url: https://releases.nixos.org/nix/nix-2.31.5/install - nix_path: nixpkgs=channel:nixos-unstable extra_nix_config: | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} + accept-flake-config = true - uses: DeterminateSystems/magic-nix-cache-action@v2 - - uses: cachix/cachix-action@v14 + - uses: cachix/cachix-action@v17 with: name: telomare signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' - extraPullNames: nix-community + extraPullNames: ekala-corepkgs - name: Build and test run: | echo nix build and tests: @@ -60,17 +60,18 @@ jobs: token: ${{ secrets.API_TOKEN_GITHUB }} path: ./stand-in-language.github.io fetch-depth: 0 - - uses: cachix/install-nix-action@v25 + - uses: cachix/install-nix-action@v31 with: install_url: https://releases.nixos.org/nix/nix-2.31.5/install extra_nix_config: | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} + accept-flake-config = true - uses: DeterminateSystems/magic-nix-cache-action@v2 - - uses: cachix/cachix-action@v14 + - uses: cachix/cachix-action@v17 with: name: telomare signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' - extraPullNames: nix-community + extraPullNames: ekala-corepkgs - name: haddock build run: | ls diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cbb82d..3dc2842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,26 @@ computes a store path without building it, and `nix path-info` rejects a path that does not exist. The apps are now build inputs of the script. * Moved to GHC 9.10.3 and `cabal-version` 3.12, and updated every flake input. +* Moved the Nix build from nixpkgs + flake-parts + haskell-flake to the + ekala package ecosystem (ekapkgs): `corepkgs` for the base system and the + GHC 9.10.3 bindist (added upstream for this, corepkgs#178 and #180), plus + the `haskell-pkgs` snapshot (Stackage LTS 24), consumed with + `corepkgs.lib.mkFlake`. The Nix code lives in `nix/`; the package + expression is a checked-in cabal2nix file (`nix/telomare-cabal2nix.nix`, + regenerate when `build-depends` change); `default.nix` and `shell.nix` + read the pins from `flake.lock` instead of going through flake-compat. + `apps.telomare-lsp` now carries the version stamp like `apps.lsp`. The + default dev shell is the minimum that builds the project (GHC and + cabal-install); `nix develop .#full` adds haskell-language-server, hlint, + stylish-haskell and ghcid (haskell-language-server needs its closure in + one Cabal-syntax 3.14 scope; `nix/haskell.nix` carries that override until + ekala-project/haskell-pkgs#4 or its corepkgs equivalent lands), and hoogle + is no longer built for either. `nix run .#push-cachix` publishes every + declared development shell with its environment closure and uses the + cachix and nix on PATH (the snapshot's cachix does not build, its amazonka + 2.0 predates GHC 9.8, and corepkgs' Nix is a from-source build); `nix + flake check` builds and shellchecks it. `systems` is `x86_64-linux` until + corepkgs marks aarch64-linux supported. ## 0.1.0.0 -- YYYY-mm-dd diff --git a/README.md b/README.md index 1f60d41..dee1a22 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,25 @@ This project is in active development. Do expect bugs and general trouble, and p ```sh # Install cachix with nix-env or adding `cachix` to your `/etc/nixos/configuration.nix`'s' `environment.systemPackages` if in NixOS. $ cachix use telomare + $ cachix use ekala-corepkgs ``` + The build comes from [ekapkgs](https://github.com/ekala-project/ekapkgs-roadmap) + rather than nixpkgs: `corepkgs` for the base system and the GHC 9.10.3 + bindist, `haskell-pkgs` for the Hackage snapshot. `telomare` caches + everything the flake builds, the compiler included; `ekala-corepkgs` holds + the base system. The flake names both caches in its `nixConfig`, so + accepting that when Nix asks does the same job. (Maintainers fill the + `telomare` cache with `nix run .#push-cachix`, which publishes the package + and both development shells and uses the `cachix` and `nix` already on + your PATH.) 4. Enter a Nix shell. This will setup an environment where all external dependencies will be available (such as `cabal` for building): ```sh $ nix develop # or nix develop -c zsh + $ nix develop .#full # the same plus haskell-language-server, hlint, stylish-haskell and ghcid ``` + (`nix develop` takes its interactive bash from whatever your flake registry + calls `nixpkgs`; that is Nix's doing, not this flake's, which has no nixpkgs + input.) 5. Build the project: ```sh $ cabal build # or nix build diff --git a/default.nix b/default.nix index f8a169d..34a44f1 100644 --- a/default.nix +++ b/default.nix @@ -1,9 +1 @@ -(import ( - let - lock = builtins.fromJSON (builtins.readFile ./flake.lock); - in fetchTarball { - url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; - sha256 = lock.nodes.flake-compat.locked.narHash; } -) { - src = ./.; -}).defaultNix +(import ./nix/legacy.nix { }).packages.default diff --git a/flake.lock b/flake.lock index e7accdd..ae5e5e5 100644 --- a/flake.lock +++ b/flake.lock @@ -1,91 +1,97 @@ { "nodes": { - "flake-compat": { - "flake": false, + "corepkgs": { + "inputs": { + "nix-lib": "nix-lib", + "systems": "systems", + "treefmt-nix": "treefmt-nix" + }, "locked": { - "lastModified": 1767039857, - "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "lastModified": 1789490790, + "narHash": "sha256-0eKcT8t8mbBDWWAK+IcBVXy0OgAi/R96EReuPEo3xHY=", + "owner": "ekala-project", + "repo": "corepkgs", + "rev": "c3bf28c491df0a5d4618a0cf2fcdfa842955a17b", "type": "github" }, "original": { - "owner": "edolstra", - "repo": "flake-compat", + "owner": "ekala-project", + "repo": "corepkgs", "type": "github" } }, - "flake-parts": { - "inputs": { - "nixpkgs-lib": "nixpkgs-lib" - }, + "haskell-pkgs": { + "flake": false, "locked": { - "lastModified": 1785627969, - "narHash": "sha256-4dtXQk/NMePegK/nWp5NSeuZKLATItOq61lpEvmXqGw=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "427bf4bd9435fdf21321c8cc628c24efc14c0f7a", + "lastModified": 1788554633, + "narHash": "sha256-3RuM+RoiTjNfGZPUQNGUIBFvekFsPu3/X4nOvoAO4iI=", + "owner": "ekala-project", + "repo": "haskell-pkgs", + "rev": "427f504a7812d9ab787f1c819cc4c42483358b10", "type": "github" }, "original": { - "owner": "hercules-ci", - "repo": "flake-parts", + "owner": "ekala-project", + "repo": "haskell-pkgs", "type": "github" } }, - "haskell-flake": { + "nix-lib": { "locked": { - "lastModified": 1785340889, - "narHash": "sha256-W3VSD3CKNF/WSLwS+A4ru1pXb2J8GN1xi4MjWacpwJA=", - "owner": "srid", - "repo": "haskell-flake", - "rev": "ec69967e95d7e93b58d9f7cdd384432cb42d37ee", + "lastModified": 1787401631, + "narHash": "sha256-Qko+hdbdmfUT+wSTlcc7TOAMHGhyMI9sdRlKzcyzDqk=", + "owner": "ekala-project", + "repo": "nix-lib", + "rev": "a52fa865318d189e15ecc37ed1d26815309e4e4e", "type": "github" }, "original": { - "owner": "srid", - "repo": "haskell-flake", + "owner": "ekala-project", + "repo": "nix-lib", "type": "github" } }, - "nixpkgs": { + "root": { + "inputs": { + "corepkgs": "corepkgs", + "haskell-pkgs": "haskell-pkgs" + } + }, + "systems": { "locked": { - "lastModified": 1786663261, - "narHash": "sha256-4i71SJ3IppFoMDwReAzq9Fnltu1Vw8Tbp1Ck0aSzehI=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "b3ec2901a0e069d8882e2182949dfb958ac33e40", + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "type": "github" }, "original": { - "owner": "nixos", - "repo": "nixpkgs", + "owner": "nix-systems", + "repo": "default", "type": "github" } }, - "nixpkgs-lib": { + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "corepkgs", + "nix-lib" + ] + }, "locked": { - "lastModified": 1785031560, - "narHash": "sha256-OmshNvn2vupOFpYinLUu+1Dnpu4n7Q5N3ggGVNHpkUI=", - "owner": "nix-community", - "repo": "nixpkgs.lib", - "rev": "0e79af5e3d4dcfcd676ab5ba3f95d2e3352e078c", + "lastModified": 1786901030, + "narHash": "sha256-WSFCsDSE5ffgD2MqzkM2CYjeFiKhRF/dJUN8uedb6YE=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "27b3b12a8e6375f28ebe122f07d230ca5459bbfa", "type": "github" }, "original": { - "owner": "nix-community", - "repo": "nixpkgs.lib", + "owner": "numtide", + "repo": "treefmt-nix", "type": "github" } - }, - "root": { - "inputs": { - "flake-compat": "flake-compat", - "flake-parts": "flake-parts", - "haskell-flake": "haskell-flake", - "nixpkgs": "nixpkgs" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 1f877f5..76090ab 100644 --- a/flake.nix +++ b/flake.nix @@ -1,259 +1,72 @@ { + description = "Telomare: a simple but robust virtual machine"; + inputs = { - nixpkgs.url = "github:nixos/nixpkgs"; - flake-parts.url = "github:hercules-ci/flake-parts"; - haskell-flake.url = "github:srid/haskell-flake"; - flake-compat = { - url = "github:edolstra/flake-compat"; + # The ekala package ecosystem. corepkgs supplies stdenv, the compilers + # and the Haskell build machinery; haskell-pkgs supplies the Hackage + # snapshot as a module for corepkgs' `config.overlays.haskell`. They are + # consumed directly rather than through the ekapkgs aggregate, which + # pins corepkgs from its own lock file. + corepkgs.url = "github:ekala-project/corepkgs"; + # corepkgs' formatter pulls nixpkgs in through treefmt-nix; nothing this + # flake evaluates uses it, so it follows an input that is already here + # rather than adding a nixpkgs checkout to the lock and to the cache push. + corepkgs.inputs.treefmt-nix.inputs.nixpkgs.follows = "corepkgs/nix-lib"; + haskell-pkgs = { + url = "github:ekala-project/haskell-pkgs"; flake = false; }; }; - outputs = inputs@{ self, nixpkgs, flake-compat, flake-parts, haskell-flake, ... }: - flake-parts.lib.mkFlake { inherit inputs; } { - systems = [ "x86_64-linux" "aarch64-linux" ]; - imports = [ inputs.haskell-flake.flakeModule ]; - perSystem = { self', system, pkgs, ... }: - let - # The GHC package set used for the build and all tooling. This is - # nixpkgs' default (curated) set, so package coverage and HLS - # support are the best available. - hsPkgs = pkgs.haskell.packages.ghc910; - lspVersion = - if self ? lastModifiedDate then - let - timestamp = self.lastModifiedDate; - year = builtins.substring 0 4 timestamp; - month = builtins.substring 4 2 timestamp; - day = builtins.substring 6 2 timestamp; - hour = builtins.substring 8 2 timestamp; - minute = builtins.substring 10 2 timestamp; - in "${year}-${month}-${day}T${hour}:${minute}Z" - else - "unknown"; - - telomareLsp = pkgs.writeShellApplication { - name = "telomare-lsp"; - text = '' - export TELOMARE_LSP_VERSION="${lspVersion}" - exec "${self.packages.${system}.telomare}/bin/telomare-lsp" "$@" - ''; - }; - - # Format and lint the tracked Haskell files. `--check` reports needed - # changes without applying them; otherwise formatting is applied in - # place. Scoping to `git ls-files` is what keeps this identical to CI: - # recursing over `.` locally wanders into untracked trees like - # .direnv/ and dist-newstyle/ and aborts on read-only store files. - telomareFormat = pkgs.writeShellApplication { - name = "telomare-format"; - runtimeInputs = [ - pkgs.diffutils - pkgs.git - hsPkgs.hlint - hsPkgs.stylish-haskell - ]; - text = '' - mapfile -t hs_files < <(git ls-files '*.hs') - if [ "''${#hs_files[@]}" -eq 0 ]; then - echo "No tracked Haskell files found" - exit 0 - fi - - format_status=0 - if [ "''${1:-}" = "--check" ]; then - tmp_dir="$(mktemp -d)" - trap 'rm -rf "$tmp_dir"' EXIT - for hs_file in "''${hs_files[@]}"; do - formatted_file="$tmp_dir/$(basename "$hs_file")" - stylish-haskell "$hs_file" > "$formatted_file" - if ! cmp -s "$hs_file" "$formatted_file"; then - printf '%s needs formatting. Suggested diff:\n' "$hs_file" - diff -u "$hs_file" "$formatted_file" || true - format_status=1 - fi - done - else - echo "Formatting ''${#hs_files[@]} tracked Haskell files" - stylish-haskell -i "''${hs_files[@]}" - fi - - lint_status=0 - hlint "''${hs_files[@]}" || lint_status=$? - - if [ "$format_status" -ne 0 ]; then - printf 'Formatting check failed\n' - fi - if [ "$lint_status" -ne 0 ]; then - printf 'Linting check failed\n' - fi - if [ "$format_status" -ne 0 ] || [ "$lint_status" -ne 0 ]; then - exit 1 - fi - - printf 'Formatting and linting are OK\n' - ''; - }; - - telomareFormatLint = pkgs.writeShellScriptBin "telomare-format-lint-check" '' - exec ${telomareFormat}/bin/telomare-format --check - ''; - in { - haskellProjects.default = { - basePackages = hsPkgs; - devShell = { - enable = true; - tools = hp: { - inherit (hp) cabal-install haskell-language-server; - }; - }; - }; - - packages.default = self'.packages.telomare; - - apps.default = { - type = "app"; - program = self.packages.${system}.telomare + "/bin/telomare"; - }; - apps.repl = { - type = "app"; - program = self.packages.${system}.telomare + "/bin/telomare-repl"; - }; - apps.lsp = { - type = "app"; - program = "${telomareLsp}/bin/telomare-lsp"; - }; - apps.format = { - type = "app"; - program = "${telomareFormat}/bin/telomare-format"; - }; - apps.format-lint = { - type = "app"; - program = "${telomareFormatLint}/bin/telomare-format-lint-check"; - }; - apps.push-cachix = { - type = "app"; - program = "${pkgs.writeShellApplication { - name = "telomare-push-cachix"; - runtimeInputs = [ - pkgs.cachix - pkgs.jq - pkgs.nixVersions.nix_2_31 - ]; - text = '' - cache_name=telomare - tmp_dir="$(mktemp -d)" - trap 'rm -rf "$tmp_dir"' EXIT - - direct_paths="$tmp_dir/direct-paths" - closure_paths="$tmp_dir/closure-paths" - key_paths="$tmp_dir/key-paths" - : > "$direct_paths" - : > "$key_paths" - - build_target() { - local target="$1" - local output_path - printf 'Building %s\n' "$target" - output_path="$(nix build --no-link --print-out-paths "$target")" - printf '%s\n' "$output_path" >> "$direct_paths" - printf '%s\n' "$output_path" >> "$key_paths" - } - - build_target ".#packages.${system}.default" - build_target ".#checks.${system}.default" - build_target ".#devShells.${system}.default" - - printf 'Building nix develop environment closure\n' - dev_env_profile="$tmp_dir/dev-env-profile" - nix print-dev-env --profile "$dev_env_profile" ".#devShells.${system}.default" >/dev/null - dev_env_path="$(nix path-info "$dev_env_profile")" - printf '%s\n' "$dev_env_path" >> "$direct_paths" - printf '%s\n' "$dev_env_path" >> "$key_paths" - - printf 'Building legacy default.nix with nix-build\n' - legacy_build_path="$(nix-build --no-out-link)" - printf '%s\n' "$legacy_build_path" >> "$direct_paths" - printf '%s\n' "$legacy_build_path" >> "$key_paths" - - printf 'Building legacy shell.nix closure with nix-store\n' - legacy_shell_drv="$(nix-instantiate shell.nix)" - legacy_shell_path="$(nix-store --realise "$legacy_shell_drv")" - printf '%s\n' "$legacy_shell_path" >> "$direct_paths" - printf '%s\n' "$legacy_shell_path" >> "$key_paths" - nix-store --query --requisites --include-outputs "$legacy_shell_drv" >> "$direct_paths" - - printf 'Archiving flake source and inputs\n' - nix flake archive --json \ - | jq -r '.. | objects | .path? // empty' \ - >> "$direct_paths" - - # The shell apps. Naming them by interpolation rather than by - # `nix eval` of `apps..program` makes them build inputs of - # this script, so they are realised whenever it runs; an evaluated - # path is merely a name, and `nix path-info` rejects it when the - # derivation behind it has not been built. The `default` and `repl` - # apps need no entry: they live in the package built above. - printf 'Including the shell apps\n' - printf '%s\n' \ - "${telomareLsp}" \ - "${telomareFormat}" \ - "${telomareFormatLint}" \ - >> "$direct_paths" - - sort -u "$direct_paths" \ - | xargs nix path-info --recursive \ - | sort -u \ - > "$closure_paths" - - path_count="$(wc -l < "$closure_paths")" - printf 'Pushing %s store paths to Cachix cache %s\n' "$path_count" "$cache_name" - cachix push "$cache_name" < "$closure_paths" - - printf 'Verifying key paths in Cachix cache %s\n' "$cache_name" - while IFS= read -r key_path; do - printf 'Verifying %s\n' "$key_path" - nix path-info --store "https://$cache_name.cachix.org" "$key_path" >/dev/null - done < "$key_paths" - - printf 'Cachix push completed for cache %s\n' "$cache_name" - ''; - }}/bin/telomare-push-cachix"; - }; + # An input's nixConfig is not applied transitively, so the caches this build + # can draw on are named here: `telomare` holds everything this flake builds, + # the compiler included; `ekala-corepkgs` holds the base system. + nixConfig = { + extra-substituters = [ + "https://telomare.cachix.org" + "https://ekala-corepkgs.cachix.org" + ]; + extra-trusted-public-keys = [ + "telomare.cachix.org-1:H0qRjVstxtb9oyEPvDDpmPSLyJ9oViAsTgwR02ra6Dk=" + "ekala-corepkgs.cachix.org-1:DcZV+vegWoEzacbSdXFXU4S7728C0eS9RfGpKeyHd6w=" + ]; + }; - # `nix flake check` builds the packages and verifies formatting and - # linting. The flake source only contains tracked files, so this - # covers the same file set as `nix run .#format` / `.#format-lint` - # and the CI format/lint jobs. - checks = self'.packages // { - format-lint = pkgs.runCommand "telomare-format-lint-check" - { - nativeBuildInputs = [ - pkgs.diffutils - pkgs.findutils - hsPkgs.hlint - hsPkgs.stylish-haskell - ]; - LC_ALL = "C.UTF-8"; - } '' - cp -r ${self} source - chmod -R u+w source - cd source - find . -type f -name '*.hs' -print0 | xargs -0 stylish-haskell -i - cd .. - if ! diff -ru ${self} source; then - echo "Formatting check failed: stylish-haskell has the suggestions diffed above." - echo "Run 'nix run .#format' to apply them." - exit 1 - fi - cd source - if ! find . -type f -name '*.hs' -print0 | xargs -0 hlint; then - echo "Linting check failed: fix the hints above or add exceptions to .hlint.yaml." - exit 1 - fi - touch $out - ''; + outputs = + { + self, + corepkgs, + haskell-pkgs, + }: + let + # mkFlake's per-system functions see only the package set, so what + # depends on the flake itself is captured here: the source, and the + # checkout timestamp the LSP reports as its version. + lspVersion = + if self ? lastModifiedDate then + let + timestamp = self.lastModifiedDate; + year = builtins.substring 0 4 timestamp; + month = builtins.substring 4 2 timestamp; + day = builtins.substring 6 2 timestamp; + hour = builtins.substring 8 2 timestamp; + minute = builtins.substring 10 2 timestamp; + in + "${year}-${month}-${day}T${hour}:${minute}Z" + else + "unknown"; + project = pkgs: import ./nix { + inherit pkgs lspVersion; + src = self; }; + in + corepkgs.lib.mkFlake { + # corepkgs' stdenv is checked on x86_64-linux alone for now. + systems = [ "x86_64-linux" ]; + modules = [ (import "${haskell-pkgs}/pkgs-module.nix") ]; + packages = pkgs: (project pkgs).packages; + devShells = pkgs: (project pkgs).devShells; + checks = pkgs: (project pkgs).checks; + apps = pkgs: (project pkgs).apps; }; - }; } diff --git a/nix/default.nix b/nix/default.nix new file mode 100644 index 0000000..91508d7 --- /dev/null +++ b/nix/default.nix @@ -0,0 +1,60 @@ +# Everything the flake exposes, as plain Nix over a package set: the package, +# the development shell, the apps and the checks. flake.nix calls this with +# the flake's own source and `self`-derived version stamp; default.nix and +# shell.nix call it through nix/legacy.nix. +{ + pkgs, + src, + lspVersion ? "unknown", +}: +let + hs = import ./haskell.nix { inherit pkgs; }; + telomare = import ./telomare.nix { + inherit pkgs src; + hsPkgs = hs.hsPkgs; + }; + devShells = import ./devshell.nix { + inherit telomare; + hsPkgs = hs.hsPkgs; + tools = hs.tools; + }; + tools = import ./tools.nix { + inherit + pkgs + src + lspVersion + telomare + ; + tools = hs.tools; + executables = hs.executables; + devShellNames = builtins.attrNames devShells; + }; + packages = { + inherit telomare; + default = telomare; + }; +in +{ + inherit packages devShells; + + # One app per executable under its own name (`nix run .#telomare-repl`, as + # CI does), plus the short names and the tooling. + apps = { + default = "${telomare}/bin/telomare"; + telomare = "${telomare}/bin/telomare"; + repl = "${telomare}/bin/telomare-repl"; + telomare-repl = "${telomare}/bin/telomare-repl"; + lsp = "${tools.telomareLsp}/bin/telomare-lsp"; + telomare-lsp = "${tools.telomareLsp}/bin/telomare-lsp"; + format = "${tools.telomareFormat}/bin/telomare-format"; + format-lint = "${tools.telomareFormatLint}/bin/telomare-format-lint-check"; + push-cachix = "${tools.pushCachix}/bin/telomare-push-cachix"; + }; + + # `nix flake check` builds the package — with its five test suites — and + # verifies formatting and linting. + checks = packages // { + format-lint = tools.formatLintCheck; + push-cachix = tools.pushCachix; + }; +} diff --git a/nix/devshell.nix b/nix/devshell.nix new file mode 100644 index 0000000..ba76a6b --- /dev/null +++ b/nix/devshell.nix @@ -0,0 +1,22 @@ +# `nix develop`: the project's dependencies, GHC and cabal-install — the +# minimum that builds telomare. `nix develop .#full` adds the editor and +# linting tools: haskell-language-server, hlint, stylish-haskell, ghcid. +# Hoogle is not built for either shell (haskell-flake used to build a +# database over every dependency); `withHoogle = true` brings it back. +{ hsPkgs, tools, telomare }: +let + shell = extra: hsPkgs.shellFor { + packages = _: [ telomare ]; + withHoogle = false; + nativeBuildInputs = [ tools.cabal-install ] ++ extra; + }; +in +{ + default = shell [ ]; + full = shell [ + tools.haskell-language-server + tools.hlint + tools.stylish-haskell + tools.ghcid + ]; +} diff --git a/nix/haskell.nix b/nix/haskell.nix new file mode 100644 index 0000000..0ece10c --- /dev/null +++ b/nix/haskell.nix @@ -0,0 +1,92 @@ +# The GHC package set the build and every tool come from — the one place the +# compiler is named. corepkgs ships GHC as bindists; 9.10.3 is the one this +# project's `cabal-version: 3.12` needs (its Cabal is 3.12.1.0), and the +# haskell-pkgs snapshot (Stackage LTS 24) is built for it. +{ pkgs }: +let + compose = pkgs.haskell.lib.compose; + + # haskell-language-server's formatters (ormolu 0.8, fourmolu 0.19) need + # Cabal-syntax 3.14 where GHC 9.10 bundles 3.12; with the bundled one, the + # closure ends up with two Cabal-syntax instances and Cabal refuses to + # configure. Build the whole closure in one 3.14 scope, the way nixpkgs' + # configuration-common.nix does. This is ekala-project/haskell-pkgs#4 carried + # locally; delete it when that (or a corepkgs equivalent) lands upstream. + hlsOverlay = + final: prev: + let + hlsScope = lself: lsuper: { + Cabal-syntax = lself.Cabal-syntax_3_14_2_0; + Cabal = lself.Cabal_3_14_2_0; + # Bounded to Cabal-syntax < 3.13; picks 3.14 once the bound is lifted. + cabal-install-parsers = compose.doJailbreak lsuper.cabal-install-parsers; + # The default 0.1.0.2 wants Cabal 3.12; 0.1.1.0 is the Cabal-syntax 3.14 release. + extensions = compose.doJailbreak lself.extensions_0_1_1_0; + # Depends on Cabal for its Setup.hs only; keep the bundled one so there + # is a single ghc-paths in the set. + ghc-paths = lsuper.ghc-paths.override { Cabal = null; }; + }; + inHlsScope = names: pkgs.lib.genAttrs names (name: prev.${name}.overrideScope hlsScope); + hlsClosure = inHlsScope [ + "haskell-language-server" + "hls-plugin-api" + "ghcide" + "hlint" + "ormolu" + "fourmolu" + "lsp-types" + ]; + in + { + # A versioned Cabal must be built against its own Cabal-syntax, not the + # compiler's bundled one. + Cabal_3_14_2_0 = prev.Cabal_3_14_2_0.override { Cabal-syntax = final.Cabal-syntax_3_14_2_0; }; + Cabal_3_16_1_0 = prev.Cabal_3_16_1_0.override { Cabal-syntax = final.Cabal-syntax_3_16_1_0; }; + # deepseq upper bound excludes the 1.5.1.0 that GHC 9.10 ships. + hw-fingertree = compose.doJailbreak prev.hw-fingertree; + } + // hlsClosure + // { + # haskell-language-server links its executables dynamically by default; + # unless the builder is told so, the fixup check finds an RPATH into + # /build. Same override as nixpkgs' configuration-nix.nix. + haskell-language-server = compose.overrideCabal (_: { + enableSharedExecutables = true; + }) hlsClosure.haskell-language-server; + }; + + hsPkgs = pkgs.haskell.packages.ghc9103Binary.extend hlsOverlay; +in +{ + inherit hsPkgs; + tools = { + inherit (hsPkgs) + hlint + stylish-haskell + ghcid + haskell-language-server + ; + # cabal-install 3.16 builds against Cabal 3.16, not the 3.12 that GHC + # 9.10 bundles, so its scope gets the snapshot's newer Cabal (as nixpkgs + # does). The snapshot also marks it broken although none of its + # dependencies is; it builds. + cabal-install = compose.markUnbroken ( + hsPkgs.cabal-install.overrideScope ( + final: _: { + Cabal = final.Cabal_3_16_1_0; + Cabal-syntax = final.Cabal-syntax_3_16_1_0; + } + ) + ); + }; + # shellcheck, which every shell app's check phase runs, from the same set: + # haskell-pkgs exposes it at the top level too, but built with the default + # (9.8.4) compiler, which would mean a second GHC and its closure for one + # executable. (cachix is not built here at all: on this snapshot its + # amazonka 2.0 dependencies do not compile with GHC 9.8 or 9.10 — the + # Hackage release predates both — so `nix run .#push-cachix` takes cachix, + # like nix, from the caller's PATH; see nix/tools.nix.) + executables = { + shellcheck = compose.justStaticExecutables hsPkgs.ShellCheck; + }; +} diff --git a/nix/legacy.nix b/nix/legacy.nix new file mode 100644 index 0000000..81d2b21 --- /dev/null +++ b/nix/legacy.nix @@ -0,0 +1,23 @@ +# What default.nix and shell.nix build: the project over the pinned package +# set, from a cleaned copy of the working tree. (The flake builds from its +# own source, which is the tracked files.) +{ + system ? builtins.currentSystem, +}: +let + pkgs = import ./pkgs.nix { inherit system; }; + inherit (pkgs) lib; + src = lib.cleanSourceWith { + src = ./..; + filter = + path: type: + lib.cleanSourceFilter path type + && !(builtins.elem (baseNameOf path) [ + "dist-newstyle" + ".direnv" + "result" + ]) + && !(lib.hasSuffix ".telc" path); + }; +in +import ./. { inherit pkgs src; } diff --git a/nix/pins.nix b/nix/pins.nix new file mode 100644 index 0000000..305282d --- /dev/null +++ b/nix/pins.nix @@ -0,0 +1,27 @@ +# The flake's inputs for the flake-less entry points (default.nix, shell.nix), +# read from flake.lock so that both roads lead to the same package set — the +# way ekapkgs' own pins.nix does it. +let + lock = builtins.fromJSON (builtins.readFile ../flake.lock); + fetch = + name: + let + node = lock.nodes.${name}.locked; + in + if node.type == "path" then + builtins.fetchTree { inherit (node) type path narHash; } + else + builtins.fetchTree { + inherit (node) + type + owner + repo + rev + narHash + ; + }; +in +{ + corepkgs = fetch "corepkgs"; + haskell-pkgs = fetch "haskell-pkgs"; +} diff --git a/nix/pkgs.nix b/nix/pkgs.nix new file mode 100644 index 0000000..d7edcd0 --- /dev/null +++ b/nix/pkgs.nix @@ -0,0 +1,12 @@ +# The package set: corepkgs with the haskell-pkgs snapshot folded in, as the +# flake builds it, for callers without a flake. +{ + system ? builtins.currentSystem, +}: +let + pins = import ./pins.nix; +in +import pins.corepkgs { + inherit system; + modules = [ (import "${pins.haskell-pkgs}/pkgs-module.nix") ]; +} diff --git a/nix/telomare-cabal2nix.nix b/nix/telomare-cabal2nix.nix new file mode 100644 index 0000000..75be333 --- /dev/null +++ b/nix/telomare-cabal2nix.nix @@ -0,0 +1,42 @@ +# Generated by cabal2nix from telomare.cabal; regenerate when build-depends +# change: +# +# nix run nixpkgs#cabal2nix -- --compiler=ghc-9.10.3 . > nix/telomare-cabal2nix.nix +# +# then put `src = ./..;` back. It is checked in because the ekapkgs package +# set does not (yet) carry cabal2nix at the top level, so `callCabal2nix` +# cannot run at evaluation time; a dependency missing here fails `Setup +# configure` loudly, which is the drift check. +{ mkDerivation, aeson, base, binary, bytestring, containers +, cryptonite, data-fix, deepseq, Diff, directory, dlist, filepath +, free, genvalidity, haskeline, hspec, lens, lib, lsp, lsp-types +, megaparsec, memory, mtl, optparse-applicative, pretty-show +, process, QuickCheck, recursion-schemes, stm, strict, tasty +, tasty-hunit, tasty-quickcheck, text, time, transformers +, utf8-string, validity +}: +mkDerivation { + pname = "telomare"; + version = "0.1.0.0"; + src = ./..; + isLibrary = true; + isExecutable = true; + libraryHaskellDepends = [ + base binary bytestring containers cryptonite data-fix deepseq dlist + filepath free genvalidity lens megaparsec memory mtl + recursion-schemes transformers utf8-string validity + ]; + executableHaskellDepends = [ + aeson base containers directory filepath free haskeline lens lsp + lsp-types megaparsec mtl optparse-applicative process + recursion-schemes stm strict text time + ]; + testHaskellDepends = [ + base bytestring containers data-fix Diff free hspec megaparsec mtl + pretty-show QuickCheck recursion-schemes strict tasty tasty-hunit + tasty-quickcheck + ]; + homepage = "https://github.com/Stand-In-Language/stand-in-language#readme"; + description = "A simple but robust virtual machine"; + license = lib.meta.getLicenseFromSpdxId "Apache-2.0"; +} diff --git a/nix/telomare.nix b/nix/telomare.nix new file mode 100644 index 0000000..9593575 --- /dev/null +++ b/nix/telomare.nix @@ -0,0 +1,19 @@ +# The package. The expression cabal2nix generated from telomare.cabal sits in +# telomare-cabal2nix.nix; this is the policy on top of it, chosen to match +# what haskell-flake did for this project: build from `cabal sdist` so that +# anything the test suites open at run time has to be declared in +# `extra-source-files`, run the suites (ekapkgs defaults to skipping them), +# and skip haddock and library profiling, which nothing consumes. +{ pkgs, hsPkgs, src }: +let + compose = pkgs.haskell.lib.compose; +in +pkgs.lib.pipe (hsPkgs.callPackage ./telomare-cabal2nix.nix { }) [ + (compose.overrideCabal (_: { + inherit src; + doCheck = true; + doHaddock = false; + enableLibraryProfiling = false; + })) + compose.buildFromSdist +] diff --git a/nix/tools.nix b/nix/tools.nix new file mode 100644 index 0000000..17263c0 --- /dev/null +++ b/nix/tools.nix @@ -0,0 +1,248 @@ +# The shell apps and the format/lint check, as they were in the flake before +# the move to ekapkgs. Scripts are linted with shellcheck explicitly: +# corepkgs' `writeShellApplication` refers to a `shellcheck-minimal` that the +# package set does not define, so the default check phase cannot evaluate. +# `gitMinimal` rather than `git`: the scripts only list and read tracked files, +# and the full git brings its manual (asciidoc, perl) into the closure. +{ + pkgs, + src, + lspVersion, + telomare, + tools, + executables, + devShellNames, +}: +let + inherit (pkgs) lib; + system = pkgs.stdenv.hostPlatform.system; + + mkScript = + args: + pkgs.writeShellApplication ( + args + // { + checkPhase = '' + runHook preCheck + ${pkgs.stdenv.shellDryRun} "$target" + ${lib.getExe executables.shellcheck} "$target" + runHook postCheck + ''; + } + ); + + # `telomare-lsp` reports the checkout's timestamp as its version; the flake + # knows it, so hand it over. The binary shells out to git for the same + # purpose, hence git on its PATH. + telomareLsp = mkScript { + name = "telomare-lsp"; + runtimeInputs = [ pkgs.gitMinimal ]; + text = '' + export TELOMARE_LSP_VERSION="${lspVersion}" + exec "${telomare}/bin/telomare-lsp" "$@" + ''; + }; + + # Format and lint the tracked Haskell files. `--check` reports needed + # changes without applying them; otherwise formatting is applied in + # place. Scoping to `git ls-files` is what keeps this identical to CI: + # recursing over `.` locally wanders into untracked trees like + # .direnv/ and dist-newstyle/ and aborts on read-only store files. + telomareFormat = mkScript { + name = "telomare-format"; + runtimeInputs = [ + pkgs.diffutils + pkgs.gitMinimal + tools.hlint + tools.stylish-haskell + ]; + text = '' + mapfile -t hs_files < <(git ls-files '*.hs') + if [ "''${#hs_files[@]}" -eq 0 ]; then + echo "No tracked Haskell files found" + exit 0 + fi + + format_status=0 + if [ "''${1:-}" = "--check" ]; then + tmp_dir="$(mktemp -d)" + trap 'rm -rf "$tmp_dir"' EXIT + for hs_file in "''${hs_files[@]}"; do + formatted_file="$tmp_dir/$(basename "$hs_file")" + stylish-haskell "$hs_file" > "$formatted_file" + if ! cmp -s "$hs_file" "$formatted_file"; then + printf '%s needs formatting. Suggested diff:\n' "$hs_file" + diff -u "$hs_file" "$formatted_file" || true + format_status=1 + fi + done + else + echo "Formatting ''${#hs_files[@]} tracked Haskell files" + stylish-haskell -i "''${hs_files[@]}" + fi + + lint_status=0 + hlint "''${hs_files[@]}" || lint_status=$? + + if [ "$format_status" -ne 0 ]; then + printf 'Formatting check failed\n' + fi + if [ "$lint_status" -ne 0 ]; then + printf 'Linting check failed\n' + fi + if [ "$format_status" -ne 0 ] || [ "$lint_status" -ne 0 ]; then + exit 1 + fi + + printf 'Formatting and linting are OK\n' + ''; + }; + + telomareFormatLint = pkgs.writeShellScriptBin "telomare-format-lint-check" '' + exec ${telomareFormat}/bin/telomare-format --check + ''; + + # `nix flake check` verifies formatting and linting over the flake source, + # which is the tracked files — the same set `nix run .#format` and the CI + # format/lint steps see. + formatLintCheck = + pkgs.runCommand "telomare-format-lint-check" + { + nativeBuildInputs = [ + pkgs.diffutils + pkgs.findutils + tools.hlint + tools.stylish-haskell + ]; + LC_ALL = "C.UTF-8"; + } + '' + cp -r ${src} source + chmod -R u+w source + cd source + find . -type f -name '*.hs' -print0 | xargs -0 stylish-haskell -i + cd .. + if ! diff -ru ${src} source; then + echo "Formatting check failed: stylish-haskell has the suggestions diffed above." + echo "Run 'nix run .#format' to apply them." + exit 1 + fi + cd source + if ! find . -type f -name '*.hs' -print0 | xargs -0 hlint; then + echo "Linting check failed: fix the hints above or add exceptions to .hlint.yaml." + exit 1 + fi + touch $out + ''; + + # Neither cachix nor nix is pinned here: the ekapkgs snapshot has no cachix + # that builds (its amazonka 2.0 dependencies predate GHC 9.8), corepkgs' + # Nix is a from-source build nothing else needs, and this is a maintainer's + # tool, so it uses the cachix already installed for `cachix use telomare` + # and the nix the flake is being used with. + pushCachix = mkScript { + name = "telomare-push-cachix"; + runtimeInputs = [ pkgs.jq ]; + text = '' + for tool in cachix nix; do + if ! command -v "$tool" >/dev/null; then + echo "$tool not found on PATH: install it (https://docs.cachix.org) and log in first" >&2 + exit 1 + fi + done + cache_name=telomare + tmp_dir="$(mktemp -d)" + trap 'rm -rf "$tmp_dir"' EXIT + + direct_paths="$tmp_dir/direct-paths" + closure_paths="$tmp_dir/closure-paths" + key_paths="$tmp_dir/key-paths" + : > "$direct_paths" + : > "$key_paths" + + build_target() { + local target="$1" + local output_path + printf 'Building %s\n' "$target" + output_path="$(nix build --no-link --print-out-paths "$target")" + printf '%s\n' "$output_path" >> "$direct_paths" + printf '%s\n' "$output_path" >> "$key_paths" + } + + build_target ".#packages.${system}.default" + build_target ".#checks.${system}.default" + + # Include every declared shell and the environment used by nix develop. + # In particular, the full shell carries HLS and the editor tools. + for shell_name in ${lib.escapeShellArgs devShellNames}; do + shell_target=".#devShells.${system}.$shell_name" + build_target "$shell_target" + printf 'Building nix develop environment closure for %s\n' "$shell_name" + dev_env_profile="$tmp_dir/dev-env-profile-$shell_name" + nix print-dev-env --profile "$dev_env_profile" "$shell_target" >/dev/null + dev_env_path="$(nix path-info "$dev_env_profile")" + printf '%s\n' "$dev_env_path" >> "$direct_paths" + printf '%s\n' "$dev_env_path" >> "$key_paths" + done + + printf 'Building legacy default.nix with nix-build\n' + legacy_build_path="$(nix-build --no-out-link)" + printf '%s\n' "$legacy_build_path" >> "$direct_paths" + printf '%s\n' "$legacy_build_path" >> "$key_paths" + + printf 'Building legacy shell.nix closure with nix-store\n' + legacy_shell_drv="$(nix-instantiate shell.nix)" + legacy_shell_path="$(nix-store --realise "$legacy_shell_drv")" + printf '%s\n' "$legacy_shell_path" >> "$direct_paths" + printf '%s\n' "$legacy_shell_path" >> "$key_paths" + nix-store --query --requisites --include-outputs "$legacy_shell_drv" >> "$direct_paths" + + printf 'Archiving flake source and inputs\n' + nix flake archive --json \ + | jq -r '.. | objects | .path? // empty' \ + >> "$direct_paths" + + # The shell apps. Naming them by interpolation rather than by + # `nix eval` of `apps..program` makes them build inputs of + # this script, so they are realised whenever it runs; an evaluated + # path is merely a name, and `nix path-info` rejects it when the + # derivation behind it has not been built. The `default` and `repl` + # apps need no entry: they live in the package built above. + printf 'Including the shell apps\n' + printf '%s\n' \ + "${telomareLsp}" \ + "${telomareFormat}" \ + "${telomareFormatLint}" \ + >> "$direct_paths" + + sort -u "$direct_paths" \ + | xargs nix path-info --recursive \ + | sort -u \ + > "$closure_paths" + + path_count="$(wc -l < "$closure_paths")" + printf 'Pushing %s store paths to Cachix cache %s\n' "$path_count" "$cache_name" + cachix push "$cache_name" < "$closure_paths" + + # A lookup that missed before the push is remembered by Nix for an + # hour (narinfo-cache-negative-ttl), so verify without that memory. + printf 'Verifying key paths in Cachix cache %s\n' "$cache_name" + while IFS= read -r key_path; do + printf 'Verifying %s\n' "$key_path" + nix path-info --store "https://$cache_name.cachix.org" \ + --narinfo-cache-negative-ttl 0 "$key_path" >/dev/null + done < "$key_paths" + + printf 'Cachix push completed for cache %s\n' "$cache_name" + ''; + }; +in +{ + inherit + telomareLsp + telomareFormat + telomareFormatLint + formatLintCheck + pushCachix + ; +} diff --git a/shell.nix b/shell.nix index b769069..1502f23 100644 --- a/shell.nix +++ b/shell.nix @@ -1,9 +1 @@ -(import ( - let - lock = builtins.fromJSON (builtins.readFile ./flake.lock); - in fetchTarball { - url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; - sha256 = lock.nodes.flake-compat.locked.narHash; } -) { - src = ./.; -}).shellNix +(import ./nix/legacy.nix { }).devShells.default