diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 000000000000..9bd350c60be6 --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,48 @@ +name: Nix + +# Builds pandoc with the flake (haskell-flake) on Linux and macOS. +on: + push: + branches: + - 'main' + - 'haskell-flake-revamp' + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: nix-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + nix-build: + name: nix build (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + steps: + - uses: actions/checkout@v4 + + - uses: cachix/install-nix-action@v30 + with: + extra_nix_config: | + experimental-features = nix-command flakes + accept-flake-config = true + + # Cache the /nix/store across runs so dependencies don't recompile. + - uses: nix-community/cache-nix-action@v6 + with: + primary-key: nix-${{ matrix.os }}-${{ hashFiles('flake.lock', 'flake.nix', '**/*.cabal', 'cabal.project') }} + restore-prefixes-first-match: nix-${{ matrix.os }}- + + - name: nix build + run: nix build .#default --print-build-logs + + - name: pandoc --version + run: nix run .#default -- --version diff --git a/flake.lock b/flake.lock index f4673bcd69fc..be769886a6de 100644 --- a/flake.lock +++ b/flake.lock @@ -1,26 +1,26 @@ { "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": 1778716662, + "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb", "type": "github" }, "original": { - "owner": "numtide", - "repo": "flake-utils", + "owner": "hercules-ci", + "repo": "flake-parts", "type": "github" } }, - "flake-utils_2": { + "flake-utils": { "inputs": { - "systems": "systems_2" + "systems": "systems" }, "locked": { "lastModified": 1731533236, @@ -38,16 +38,16 @@ }, "ghc-wasm-meta": { "inputs": { - "flake-utils": "flake-utils_2", + "flake-utils": "flake-utils", "nixpkgs": "nixpkgs" }, "locked": { "host": "gitlab.haskell.org", - "lastModified": 1780507186, - "narHash": "sha256-qp/sLxsc3mHrXyVRTWn0zmqWMg+60G36zUGLeLhGIHQ=", + "lastModified": 1781438969, + "narHash": "sha256-jWaiQZaEh9XOSA+7H1AeZla64Ynr1o7Jlrj8/Qbuaq8=", "owner": "haskell-wasm", "repo": "ghc-wasm-meta", - "rev": "ba0c8238314857ef1d5db1f20bb17b96178c9bd3", + "rev": "f3a56954c179598b8a4164779f0fcc4789e141c9", "type": "gitlab" }, "original": { @@ -57,13 +57,28 @@ "type": "gitlab" } }, + "haskell-flake": { + "locked": { + "lastModified": 1779033714, + "narHash": "sha256-hhqI8XWucXQB5Yo6KwECkD2nldqNSd+ftfY3YoJT2/A=", + "owner": "srid", + "repo": "haskell-flake", + "rev": "375c8e3bf7c43a08eb74b60552de96f2561ade76", + "type": "github" + }, + "original": { + "owner": "srid", + "repo": "haskell-flake", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1780383649, - "narHash": "sha256-FS3od1iIyWYz68tqZGGc92Po7Ji+G2NEm6+a4s4RI0w=", + "lastModified": 1781236690, + "narHash": "sha256-ox8n9kxKUnMP2Q9sq2NWul2lOUzNFwlvlCVtH4hP0hY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "595ee2b1a31a50efcb8db3824999abb98ec1d0c9", + "rev": "b5e61da309336bdba24f964dce712a5ab8e0092e", "type": "github" }, "original": { @@ -73,25 +88,42 @@ "type": "github" } }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1777168982, + "narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, "nixpkgs_2": { "locked": { - "lastModified": 1780659424, - "narHash": "sha256-9XpsomaGHOBlDCkxXLgT1JTjRs+lYMSlPFOKGnp7tQc=", - "owner": "NixOS", + "lastModified": 1781454065, + "narHash": "sha256-d2xfDjnfRuf/xYGdu9VVRHiav/2w5hDL/5cw2TuVAXw=", + "owner": "nixos", "repo": "nixpkgs", - "rev": "5b05148328954d65995fcf531709c0f55689f514", + "rev": "9eac87a12312b8f60dd52e1c6e1a265f6fc7f5fc", "type": "github" }, "original": { - "owner": "NixOS", + "owner": "nixos", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { - "flake-utils": "flake-utils", + "flake-parts": "flake-parts", "ghc-wasm-meta": "ghc-wasm-meta", + "haskell-flake": "haskell-flake", "nixpkgs": "nixpkgs_2" } }, @@ -109,21 +141,6 @@ "repo": "default", "type": "github" } - }, - "systems_2": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 4cc2dc1efd99..97db7e536830 100644 --- a/flake.nix +++ b/flake.nix @@ -1,75 +1,164 @@ { description = "pandoc"; + # Uses haskell-flake (https://haskell.nixos.asia) to build the local Haskell + # packages (pandoc, pandoc-cli, pandoc-lua-engine, pandoc-server) defined in + # cabal.project. + # + # nix build # builds the `pandoc` executable (from pandoc-cli) + # nix run # runs it + # nix develop # dev shell (cabal, HLS, the wasm toolchain, etc.) inputs = { - nixpkgs.url = "github:NixOS/nixpkgs"; - flake-utils.url = "github:numtide/flake-utils"; + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; + haskell-flake.url = "github:srid/haskell-flake"; + + # WASM toolchain, used (in the dev shell) to build pandoc for wasm32. ghc-wasm-meta.url = "gitlab:haskell-wasm/ghc-wasm-meta?host=gitlab.haskell.org"; }; - outputs = { self, nixpkgs, flake-utils, ghc-wasm-meta }: - flake-utils.lib.eachDefaultSystem (system: - let - pkgs = nixpkgs.legacyPackages.${system}; + outputs = inputs@{ flake-parts, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { + systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ]; + imports = [ inputs.haskell-flake.flakeModule ]; + + perSystem = { self', pkgs, lib, system, ... }: + let + # Single source of truth for dependency versions: pandoc's own + # `stack.yaml` `extra-deps`. haskell-flake's package set lags pandoc's + # `build-depends`, so we apply the very same overrides stack does. + # Parsed at eval time (IFD: yaml -> json). + stackExtraDeps = builtins.fromJSON (builtins.readFile + (pkgs.runCommand "stack-extra-deps.json" { } '' + ${pkgs.yq-go}/bin/yq -o=json '.extra-deps' \ + ${inputs.self + "/stack.yaml"} > $out + '')); - haskellPackages = pkgs.haskellPackages; - # Or, override some dependencies as follows: - # haskellPackages = (pkgs.haskellPackages.override { - # all-cabal-hashes = pkgs.fetchurl { - # # The hash in the URL is just a git commit hash - # url = "https://github.com/commercialhaskell/all-cabal-hashes/archive/2e3f153549871ada6ecbf36719339a5da051bc76.tar.gz"; - # sha256 = "ymHZb6wCZlrtKb+T+iOL17jyN8IzA7s52uiamUIkWNI="; - # }; - # }).extend(self: super: { - # citeproc = pkgs.haskell.lib.dontCheck (self.callHackage "citeproc" "0.9.0.1" {}); - # texmath = self.callHackage "texmath" "0.12.10.2" {}; - # }); + # "hslua-module-doclayout-1.2.1.1" -> name + version + # (Hackage versions contain no '-', so version = last '-'-segment). + parseDep = s: + let parts = lib.splitString "-" s; + in lib.nameValuePair + (lib.concatStringsSep "-" (lib.init parts)) + (lib.last parts); - jailbreakUnbreak = pkg: - pkgs.haskell.lib.doJailbreak (pkg.overrideAttrs (_: { meta = { }; })); + # name -> version, derived from stack.yaml's plain `pkg-version` entries. + bumpedDeps = + lib.listToAttrs + (map parseDep (builtins.filter builtins.isString stackExtraDeps)) + // { + # nixpkgs ships these older than stack's LTS resolver does, so they + # are not pinned in stack.yaml but still need overriding here. + hslua-core = "2.3.2.1"; + hslua-classes = "2.3.2"; + hslua-aeson = "2.3.2"; + }; - # PUT YOUR PACKAGE NAME HERE: - packageName = "pandoc"; + # git `source-repository-package`s from stack.yaml (currently texmath). + # The package name is taken from the repo basename. + gitSources = lib.listToAttrs (map + (d: lib.nameValuePair + (lib.removeSuffix ".git" (baseNameOf d.git)) + { source = builtins.fetchGit { url = d.git; rev = d.commit; allRefs = true; }; }) + (builtins.filter builtins.isAttrs stackExtraDeps)); - wasmToolchain = ghc-wasm-meta.packages.${system}.default; - # Alternatively, if you want a specific "bundle": - # wasmToolchain = ghc-wasm-meta.packages.${system}.all_9_14; - in { - packages.${packageName} = - haskellPackages.callCabal2nix packageName self rec { - # Dependency overrides go here + # The sub-packages' `COPYING.md` is a `../COPYING.md` symlink that + # dangles once the sub-directory is isolated as a source, breaking + # the `license-file` copy during install. Replace it with the real + # file from the repo root. + fixLicense = pkg: pkg.overrideAttrs (oa: { + postPatch = (oa.postPatch or "") + '' + rm -f COPYING.md + cp ${inputs.self}/COPYING.md COPYING.md + ''; + }); + in + { + haskellProjects.default = { + # pandoc 3.10's dependencies (citeproc 0.13, typst 0.10, commonmark + # 0.3, ...) are newer than the all-cabal-hashes snapshot pinned in + # nixpkgs, so `callHackage` can't find them. Point the package set at + # a recent all-cabal-hashes that contains them. + basePackages = pkgs.haskellPackages.override { + all-cabal-hashes = pkgs.fetchurl { + url = "https://github.com/commercialhaskell/all-cabal-hashes/archive/de045027bcffb533171385da3b1e97aa05ba7633.tar.gz"; + sha256 = "01pqcqyc0yab7d7xvs2bwqvy4rqi7arbpw4823hd93gqkl6yics7"; + }; }; - packages.default = self.packages.${system}.${packageName}; - defaultPackage = self.packages.${system}.default; + # haskell-flake's cabal.project parser can't handle pandoc's + # `if arch(wasm32)` conditional blocks, so disable auto-discovery + # and list the packages explicitly. Sub-package sources are derived + # from the flake root (`self`) so they are recognized as local + # (i.e. under `projectRoot`). + defaults.packages = lib.mkForce { }; + + packages = { + # Local packages (from cabal.project). + pandoc.source = inputs.self; + pandoc-cli.source = inputs.self + "/pandoc-cli"; + pandoc-lua-engine.source = inputs.self + "/pandoc-lua-engine"; + pandoc-server.source = inputs.self + "/pandoc-server"; + } + // gitSources + // lib.mapAttrs (_: version: { source = version; }) bumpedDeps; + + settings = { + pandoc = { + # Build the self-contained executable (mirrors cabal.project's + # `+embed_data_files`), so the binary works without a data dir. + cabalFlags.embed_data_files = true; + # Skip the (slow) test suite and haddock in `nix build`. + check = false; + haddock = false; + }; + # The sub-package `.cabal` files reference `license-file: COPYING.md` + # via a `../COPYING.md` symlink that dangles once the sub-directory + # is isolated as a source; building from sdist would fail on it. The + # license file isn't needed to compile, so build from source directly. + pandoc-cli = { check = false; haddock = false; buildFromSdist = false; custom = fixLicense; }; + pandoc-lua-engine = { check = false; haddock = false; buildFromSdist = false; custom = fixLicense; }; + pandoc-server = { check = false; haddock = false; buildFromSdist = false; custom = fixLicense; }; + + # The git sources (texmath) and the bumped Hackage deps: skip + # tests/haddock and relax stale bounds against this GHC's boot libs. + } // lib.genAttrs + (builtins.attrNames bumpedDeps ++ builtins.attrNames gitSources) + (_: { check = false; haddock = false; jailbreak = true; }); + + # Dev shell (`nix develop`). haskell-flake already provides + # cabal-install, haskell-language-server, ghcid and hlint. + devShell = { + tools = hp: { + inherit (hp) + cabal-plan + hpc + stylish-haskell + eventlog2html + profiterole + profiteur; + }; + mkShellArgs.nativeBuildInputs = + [ inputs.ghc-wasm-meta.packages.${system}.default ] + ++ (with pkgs; [ + zlib.dev + git + gnumake + bashInteractive + ripgrep + unzip + jq + libxml2 # for xmllint + epubcheck # for validate-epub + nodejs # for validate-epub + ]); + }; - devShells.default = pkgs.mkShell { - buildInputs = with pkgs; [ - wasmToolchain - haskellPackages.haskell-language-server - haskellPackages.hlint - haskellPackages.cabal-install - haskellPackages.cabal-plan - haskellPackages.hpc - haskellPackages.ghcid - haskellPackages.stylish-haskell - haskellPackages.eventlog2html - haskellPackages.profiterole - haskellPackages.profiteur - zlib.dev - git - gnumake - bashInteractive - ripgrep - unzip - jq - libxml2 # for xmllint - epubcheck # for validate-epub - nodejs # for validate-epub - ]; - inputsFrom = map (__getAttr "env") (__attrValues self.packages.${system}); + autoWire = [ "packages" "apps" "devShells" ]; }; - devShell = self.devShells.${system}.default; - }); + # `nix build` / `nix run` -> the `pandoc` executable from pandoc-cli. + packages.default = self'.packages.pandoc-cli; + }; + }; }