/* This is the library of nix builders. Some rules to follow: - Keep this code as minimal as possible. I'd rather write Haskell than Nix, wouldn't you? - Try to reuse as much upstream Nix as possible. */ { analysisJSON, bild, }: with bild; let analysis = builtins.fromJSON analysisJSON; # common bash functions for the builder commonBash = builtins.toFile "common.bash" '' # Check that a command succeeds, fail and log if not. function check { $@ || { echo "fail: $name: $3"; exit 1; } } ''; build = _: target: let name = target.out; root = builtins.getEnv "CODEROOT"; mainModule = target.mainModule; compileLine = lib.strings.concatStringsSep " " ([target.compiler] ++ target.compilerFlags); allSources = target.srcs ++ [target.quapath]; isEmpty = x: x == null || x == []; skip = ["_" ".direnv"]; # Normalize paths by removing leading "./" normalize = p: lib.strings.removePrefix "./" p; # Given a list of path parts, produce all cumulative prefixes: # ["a","b","c"] -> ["a","a/b","a/b/c"] dirPrefixes = parts: if parts == [] then [] else let hd = lib.lists.head parts; tl = lib.lists.tail parts; rest = dirPrefixes tl; in [hd] ++ (lib.lists.map (r: "${hd}/${r}") rest); # Normalize all source file paths (relative to root) allSourcesRel = lib.lists.map normalize allSources; # Allowed directories are the ancestors of all source files, plus the repo root "" allowedDirs = lib.lists.unique ( [""] ++ lib.lists.concatMap (p: let parts = lib.strings.splitString "/" p; in dirPrefixes (lib.lists.init parts)) allSourcesRel ); filter = file: type: if lib.lists.elem (builtins.baseNameOf file) skip then false else if type == "directory" then let rel = lib.strings.removePrefix "${root}/" file; rel' = normalize rel; in lib.lists.elem rel' allowedDirs else if type == "regular" then let rel = lib.strings.removePrefix "${root}/" file; rel' = normalize rel; in lib.lists.elem rel' allSourcesRel else false; # remove empty directories, leftover from the src filter preBuild = "find . -type d -empty -delete"; src = lib.sources.cleanSourceWith { inherit filter; src = lib.sources.cleanSource root; }; langdeps_ = if isEmpty target.langdeps then [] else lib.attrsets.attrVals target.langdeps (lib.attrsets.getAttrFromPath (lib.strings.splitString "." target.packageSet) bild); sysdeps_ = if isEmpty target.sysdeps then [] else lib.attrsets.attrVals target.sysdeps pkgs; rundeps_ = if isEmpty target.rundeps then [] else lib.attrsets.attrVals target.rundeps pkgs; CODEROOT = "."; builders = { base = stdenv.mkDerivation rec { inherit name src CODEROOT preBuild; buildInputs = langdeps_ ++ sysdeps_; installPhase = "install -D ${name} $out/bin/${name}"; buildPhase = compileLine; }; haskell = if (target.hsGraph or null) == null then # Monolithic build (fallback for TH/cycles) stdenv.mkDerivation rec { inherit name src CODEROOT preBuild; nativeBuildInputs = [makeWrapper]; buildInputs = sysdeps_ ++ [ (haskell.ghcWith (p: (lib.attrsets.attrVals target.langdeps p))) ]; buildPhase = compileLine; installPhase = '' install -D ${name} $out/bin/${name} wrapProgram $out/bin/${name} \ --prefix PATH : ${lib.makeBinPath rundeps_} ''; } else # Per-module incremental build let graph = target.hsGraph; ghcPkg = haskell.ghcWith (p: (lib.attrsets.attrVals target.langdeps p)); # Helper to sanitize module names for Nix attr names sanitize = builtins.replaceStrings ["."] ["_"]; # Create source filter for a single module mkModuleSrc = modulePath: let moduleFiles = [modulePath]; moduleAllSources = moduleFiles; moduleAllSourcesRel = lib.lists.map normalize moduleAllSources; moduleAllowedDirs = lib.lists.unique ( [""] ++ lib.lists.concatMap (p: let parts = lib.strings.splitString "/" p; in dirPrefixes (lib.lists.init parts)) moduleAllSourcesRel ); moduleFilter = file: type: if lib.lists.elem (builtins.baseNameOf file) skip then false else if type == "directory" then let rel = lib.strings.removePrefix "${root}/" file; rel' = normalize rel; in lib.lists.elem rel' moduleAllowedDirs else if type == "regular" then let rel = lib.strings.removePrefix "${root}/" file; rel' = normalize rel; in lib.lists.elem rel' moduleAllSourcesRel else false; in lib.sources.cleanSourceWith { filter = moduleFilter; src = lib.sources.cleanSource root; }; # Build one module derivation mkModuleDrv = modName: node: depDrvs: stdenv.mkDerivation { name = "hs-mod-${sanitize modName}"; src = mkModuleSrc node.nodePath; inherit CODEROOT; nativeBuildInputs = []; buildInputs = sysdeps_ ++ depDrvs; builder = "${stdenv.shell}"; args = [ "-c" (let copyDeps = lib.strings.concatMapStringsSep "\n" (d: '' ${pkgs.coreutils}/bin/cp -rfL ${d}/hidir/. . 2>/dev/null || true ${pkgs.coreutils}/bin/cp -rfL ${d}/odir/. . 2>/dev/null || true ${pkgs.coreutils}/bin/chmod -R +w . 2>/dev/null || true '') depDrvs; in '' set -eu ${pkgs.coreutils}/bin/cp -rL $src/. . ${pkgs.coreutils}/bin/chmod -R +w . ${copyDeps} ${ghcPkg}/bin/ghc -c \ -Wall -Werror -haddock -Winvalid-haddock \ -i. \ ${node.nodePath} ${pkgs.coreutils}/bin/mkdir -p $out/hidir $out/odir ${pkgs.findutils}/bin/find . -name '*.hi' -exec ${pkgs.coreutils}/bin/cp --parents {} $out/hidir/ \; ${pkgs.findutils}/bin/find . -name '*.o' -exec ${pkgs.coreutils}/bin/cp --parents {} $out/odir/ \; '') ]; }; # Recursive attrset of all module derivations # mapAttrs' creates {sanitized-name = drv}, while nodeImports use original names modules = lib.fix (self: lib.mapAttrs' (modName: node: lib.nameValuePair (sanitize modName) ( mkModuleDrv modName node (map (dep: builtins.getAttr (sanitize dep) self) node.nodeImports) )) graph.graphModules); in # Final link derivation stdenv.mkDerivation rec { inherit name CODEROOT src; nativeBuildInputs = [makeWrapper]; dontConfigure = true; dontStrip = true; dontPatchShebangs = true; buildPhase = let pkgFlags = lib.strings.concatMapStringsSep " " (p: "-package ${p}") target.langdeps; copyHiFiles = lib.strings.concatMapStringsSep "\n" (drv: "cp -rL ${drv}/hidir/. . 2>/dev/null || true") (lib.attrsets.attrValues modules); in '' set -eu ${copyHiFiles} chmod -R +w . || true ${ghcPkg}/bin/ghc --make \ ${target.quapath} \ -i. \ ${pkgFlags} \ -threaded \ -o ${name} \ ${lib.optionalString (target.mainModule != "Main") "-main-is ${target.mainModule}"} ''; installPhase = '' install -D ${name} $out/bin/${name} ${lib.optionalString (rundeps_ != []) '' wrapProgram $out/bin/${name} \ --prefix PATH : ${lib.makeBinPath rundeps_} ''} ''; }; c = stdenv.mkDerivation rec { inherit name src CODEROOT preBuild; buildInputs = langdeps_ ++ sysdeps_; installPhase = "install -D ${name} $out/bin/${name}"; buildPhase = lib.strings.concatStringsSep " " [ compileLine ( if isEmpty langdeps_ then "" else "$(pkg-config --cflags ${ lib.strings.concatStringsSep " " target.langdeps })" ) ( if isEmpty sysdeps_ then "" else "$(pkg-config --libs ${ lib.strings.concatStringsSep " " target.sysdeps })" ) ]; }; python = python.buildPythonApplication rec { inherit name src CODEROOT; nativeBuildInputs = [makeWrapper]; propagatedBuildInputs = langdeps_ ++ sysdeps_ ++ rundeps_; buildInputs = sysdeps_; nativeCheckInputs = [pkgs.ruff python.packages.mypy]; checkPhase = '' . ${commonBash} cp ${../../pyproject.toml} ./pyproject.toml check ruff format --exclude 'setup.py' --check . # ignore EXE here to support run.sh shebangs check ruff check \ --ignore EXE \ --exclude 'setup.py' \ --exclude '__init__.py' \ . touch ./py.typed check python -m mypy \ --explicit-package-bases \ --no-color-output \ --exclude 'setup\.py$' \ . ''; installCheck = '' . ${commonBash} check python -m ${mainModule} test ''; preBuild = '' # remove empty directories, leftover from the src filter find . -type d -empty -delete # initialize remaining dirs as python modules find . -type d -exec touch {}/__init__.py \; # generate a minimal setup.py cat > setup.py << EOF from setuptools import find_packages, setup setup( name="${name}", entry_points={"console_scripts":["${name} = ${mainModule}:main"]}, version="0.0.0", url="https://git.bensima.com/omni.git", author="dev", author_email="dev@bensima.com", description="nil", packages=find_packages(), install_requires=[], ) EOF ''; pythonImportsCheck = [mainModule]; # sanity check }; }; in builders.${target.builder}; # the bild caller gives us the Analysis type, which is a hashmap, but i need to # return a single drv, so just take the first one for now. ideally i would only # pass Target, one at a time, (perhaps parallelized in haskell land) and then i # wouldn't need all of this let nesting in builtins.head (lib.attrsets.mapAttrsToList build analysis)