From b06dc22101f847f947b1af4ebb7911ecfcd64c13 Mon Sep 17 00:00:00 2001 From: Ali Caglayan Date: Sat, 16 May 2026 18:32:23 +0100 Subject: [PATCH 1/4] nix: cross compile dune to windows Signed-off-by: Ali Caglayan --- flake.nix | 29 ++++++++++++++++-- nix/dune-package.nix | 71 ++++++++++++++++++++++++++++++++++++++++++-- nix/dune-target.nix | 61 +++++++++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 nix/dune-target.nix diff --git a/flake.nix b/flake.nix index 6e170f7534b..20a13268daf 100644 --- a/flake.nix +++ b/flake.nix @@ -60,8 +60,26 @@ inherit (nixpkgsOcaml.odoc) version src; doCheck = false; }); + # Templates the cross-compiled dune binary used by + # `windows-static`. Lives at the top-level scope so + # `nix-overlays`' cross-overlay sees it during scope + # construction and `fixOCamlPackage` can pair the cross + # derivation with this native template via + # `findNativePackage`. + dune_target = oself.callPackage ./nix/dune-target.nix { }; } ); + # Keep `ocaml-ng.ocamlPackages_5_4` in sync with the override + # above. nix-overlays' `cross/ocaml.nix:46` looks up the native + # OCaml via `buildPackages.ocaml-ng."ocamlPackages_5_4"`, so + # without this the cross-target side uses our overridden ocaml + # while the native side (used to build findlib's `nativeBuild- + # Inputs` etc.) uses the unoverridden ocaml+flambda — the + # mismatch shows up as cross `ocamlopt` rejecting native + # `topdirs.cmx` ("not a compilation unit description"). + ocaml-ng = super.ocaml-ng // { + ocamlPackages_5_4 = self.ocamlPackages; + }; }) melange.overlays.default ]; @@ -106,9 +124,14 @@ src = ./.; }; in - { - inherit (dune-package) default dune-static; - dune = self.packages.${pkgs.stdenv.hostPlatform.system}.default; + rec { + inherit (dune-package) + default + musl-static + windows-static + ; + dune = default; + dune-static = musl-static; } ); diff --git a/nix/dune-package.nix b/nix/dune-package.nix index 9bb0a2eacc8..eb63d12fba8 100644 --- a/nix/dune-package.nix +++ b/nix/dune-package.nix @@ -1,6 +1,12 @@ # The dune package, built from the source tree at `src`. # -# Returns `default` (native build) and `dune-static` (musl64 cross build). +# Returns: +# - `default`: native build +# - `musl-static`: x86_64 musl static cross build (nixpkgs' dune with the +# source tree's bootstrap invoked under `--static`) +# - `windows-static`: mingw static cross build (via `dune build -x windows`, +# driven from the `dune-target.nix` derivation) +# - `dune-static`: alias for `musl-static` { nixpkgs, ocaml-overlays, @@ -38,6 +44,11 @@ let ] ); + # Static musl build: nixpkgs' own dune, pointed at this source tree and + # bootstrapped with `--static`. Matches the setup on `main`. The + # `dune-target.nix` cross machinery is windows-only for now — once + # `nix-overlays` applies its cross-overlay to `pkgsCross.musl64` (it + # currently only applies the static-overlay) we can move this over too. dune-static-overlay = self: super: { ocamlPackages = super.ocaml-ng.ocamlPackages_5_4.overrideScope ( oself: osuper: { @@ -53,6 +64,56 @@ let ocaml-overlays.overlays.default dune-static-overlay ]; + + # Used by `windows-static`. The `dune_target` overlay is applied only to + # the cross pkgs we're building against, so it never leaks into the dev + # shells. We also widen `meta.platforms` on `ocaml` and on every + # `buildDunePackage` derivation in that scope: nixpkgs' ocaml-side metadata + # excludes mingw even though those packages build fine there, and widening + # avoids the `NIXPKGS_ALLOW_UNSUPPORTED_SYSTEM=1` escape hatch at the call + # site. + duneFor = + crossPkgs: + let + withDuneTarget = crossPkgs.appendOverlays [ + (self: super: { + ocamlPackages = super.ocamlPackages.overrideScope ( + oself: osuper: { + # ocaml itself excludes mingw via `meta.platforms`. Widen it + # so cross builds against this scope evaluate. + ocaml = osuper.ocaml.overrideAttrs (o: { + meta = (o.meta or { }) // { platforms = lib.platforms.all; }; + }); + buildDunePackage = + arg: + let + widen = a: a // { + meta = (a.meta or { }) // { platforms = lib.platforms.all; }; + }; + in + # nix-overlays' `buildDunePackage` accepts either an attrset + # or a `final: attrset` function (see `cross/ocaml.nix`). + osuper.buildDunePackage ( + if builtins.isFunction arg then final: widen (arg final) else widen arg + ); + } + ); + }) + ]; + in + withDuneTarget.ocamlPackages.dune_target.overrideAttrs { src = dune-source; }; + + overrideOcaml = + ocamlOverride: crossPkgs: + crossPkgs.appendOverlays [ + (self: super: { + ocamlPackages = super.ocamlPackages.overrideScope ( + oself: osuper: { + ocaml = osuper.ocaml.override ocamlOverride; + } + ); + }) + ]; in { default = @@ -78,5 +139,11 @@ in "LIBDIR=$(OCAMLFIND_DESTDIR)" ]; }; - dune-static = pkgs-static.pkgsCross.musl64.ocamlPackages.dune; + + musl-static = pkgs-static.pkgsCross.musl64.ocamlPackages.dune; + + # `framePointerSupport` is forced off because mingw can't build OCaml + windows-static = duneFor ( + overrideOcaml { framePointerSupport = false; } pkgs.pkgsCross.mingwW64Static + ); } diff --git a/nix/dune-target.nix b/nix/dune-target.nix new file mode 100644 index 00000000000..7651e62088a --- /dev/null +++ b/nix/dune-target.nix @@ -0,0 +1,61 @@ +# Cross-compile-friendly dune derivation. Native: a buildDunePackage template +# (not buildable natively by design — exists so findlib config has dune's deps +# in scope). Cross: overrides build/install phases to use `dune build -x +# `. +{ + lib, + stdenv, + buildPackages, + buildDunePackage, + ocaml, + dune_3, + csexp, + pp, + ppx_expect, + re, + spawn, + uutf, +}: +let + isCross = stdenv.hostPlatform.config != stdenv.buildPlatform.config; + # Must match the toolchain name nix-overlays sets in the findlib config + # (`cross/ocaml.nix`). Otherwise dune's `-x` lookup misses and falls back + # to a native build. + crossName = + if stdenv.hostPlatform.isMinGW then "windows" else lib.head (lib.splitString "-" stdenv.system); + installedName = if stdenv.hostPlatform.isMinGW then "dune.exe" else "dune"; + + template = buildDunePackage { + pname = "dune_target"; + inherit (dune_3) version src; + buildInputs = [ + csexp + pp + ppx_expect + re + spawn + uutf + ]; + }; +in +if !isCross then + template +else + assert stdenv.hostPlatform.isMinGW -> lib.versionAtLeast ocaml.version "5.4"; + template.overrideAttrs (o: { + dontAddPrefix = true; + dontFixup = true; + depsBuildBuild = [ buildPackages.stdenv.cc ]; + nativeBuildInputs = lib.remove buildPackages.stdenv.cc (o.nativeBuildInputs or [ ]); + buildPhase = '' + runHook preBuild + dune build -x ${crossName} ''${enableParallelBuilding:+-j $NIX_BUILD_CORES} bin/main.exe + runHook postBuild + ''; + installPhase = '' + runHook preInstall + mkdir -p $out/bin + cp _build/default.${crossName}/bin/main.exe $out/bin/${installedName} + runHook postInstall + ''; + }) From 150737a1f2c1505689cf101a6562882288faafbc Mon Sep 17 00:00:00 2001 From: Ali Caglayan Date: Tue, 23 Jun 2026 09:05:46 +0100 Subject: [PATCH 2/4] ci: gate binary uploads on workflow_dispatch input Signed-off-by: Ali Caglayan --- .github/workflows/binaries.yml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml index 1ba5264daf4..d7d03e468ad 100644 --- a/.github/workflows/binaries.yml +++ b/.github/workflows/binaries.yml @@ -1,7 +1,17 @@ name: Binaries on: + push: + branches: + - main + pull_request: + merge_group: workflow_dispatch: + inputs: + upload: + description: Upload built binaries as artifacts + type: boolean + default: false jobs: binary: @@ -12,10 +22,10 @@ jobs: include: - os: macos-15-intel name: x86_64-apple-darwin - installable: .# + installable: .#dune - os: macos-14 name: aarch64-apple-darwin - installable: .# + installable: .#dune - os: ubuntu-22.04 name: x86_64-unknown-linux-musl installable: .#dune-static @@ -27,11 +37,13 @@ jobs: - uses: cachix/install-nix-action@v31 - run: echo "(version $(git describe --always --dirty --abbrev=7))" >> dune-project - run: nix build ${{ matrix.installable }} - - uses: actions/upload-artifact@v7 + - if: ${{ inputs.upload }} + uses: actions/upload-artifact@v7 with: path: result/bin/dune name: dune-${{ matrix.name }} combine: + if: ${{ inputs.upload }} runs-on: ubuntu-latest needs: binary steps: From dd6f1a7536369cfbdf57eeddab7769158a810f41 Mon Sep 17 00:00:00 2001 From: Ali Caglayan Date: Tue, 23 Jun 2026 12:08:11 +0100 Subject: [PATCH 3/4] ci: cross-build dune to windows on every PR Signed-off-by: Ali Caglayan --- .github/workflows/binaries.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml index d7d03e468ad..b5cef16bc86 100644 --- a/.github/workflows/binaries.yml +++ b/.github/workflows/binaries.yml @@ -15,10 +15,13 @@ on: jobs: binary: - name: Create + name: ${{ matrix.name }} strategy: fail-fast: false matrix: + # `name` follows the LLVM/Rust target-triple convention + # (`---`). For Windows, `-gnu` denotes the + # MinGW (GNU) ABI, as opposed to `-msvc`. include: - os: macos-15-intel name: x86_64-apple-darwin @@ -29,6 +32,10 @@ jobs: - os: ubuntu-22.04 name: x86_64-unknown-linux-musl installable: .#dune-static + - os: ubuntu-22.04 + name: x86_64-pc-windows-gnu + installable: .#windows-static + binary: dune.exe runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v7 @@ -40,7 +47,7 @@ jobs: - if: ${{ inputs.upload }} uses: actions/upload-artifact@v7 with: - path: result/bin/dune + path: result/bin/${{ matrix.binary || 'dune' }} name: dune-${{ matrix.name }} combine: if: ${{ inputs.upload }} From 622c37ce8b5b0d573befc6ececa5109180779b9e Mon Sep 17 00:00:00 2001 From: Ali Caglayan Date: Tue, 23 Jun 2026 12:14:55 +0100 Subject: [PATCH 4/4] ci: cache nix store for binaries job Signed-off-by: Ali Caglayan --- .github/workflows/binaries.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml index b5cef16bc86..ce33bf190e6 100644 --- a/.github/workflows/binaries.yml +++ b/.github/workflows/binaries.yml @@ -42,6 +42,12 @@ jobs: with: fetch-depth: 0 # for git describe - uses: cachix/install-nix-action@v31 + - uses: nix-community/cache-nix-action@v7 + with: + primary-key: | + nix-${{ runner.os }}-${{ github.job }}-${{ matrix.name }}--${{ hashFiles('**/*.nix', '**/flake.lock') }} + restore-prefixes-first-match: nix-${{ runner.os }}-${{ github.job }}-${{ matrix.name }}-- + gc-max-store-size-linux: 2G - run: echo "(version $(git describe --always --dirty --abbrev=7))" >> dune-project - run: nix build ${{ matrix.installable }} - if: ${{ inputs.upload }}