# flakelite -- Framework for making flakes simple # Copyright (C) 2023 Archit Gupta # SPDX-License-Identifier: MIT nixpkgs: let inherit (builtins) intersectAttrs isPath readDir; inherit (nixpkgs.lib) attrNames attrVals callPackageWith composeManyExtensions filter filterAttrs foldAttrs foldl functionArgs genAttrs hasSuffix isFunction isList isString listToAttrs mapAttrs mapAttrsToList mapAttrs' mergeAttrs nameValuePair optional optionalAttrs optionalString parseDrvName pathExists pipe recursiveUpdate removePrefix removeSuffix zipAttrsWith; exports = { inherit mkFlake systems importDir autoImport autoImportAttrs defaultPkgName supportedSystem mergeModules moduleAttrs rootAttrs ensureFn fnConcat fnUpdate callFn callPkg callPkgs tryImport; }; builtinModule = { src, inputs, root }: { withOverlays = params: [ (final: prev: { flakelite = params // { inputs' = mapAttrs (_: mapAttrs (_: v: v.${prev.system} or { })) inputs; meta = { platforms = root.systems; } // optionalAttrs (root ? description) { inherit (root) description; } // optionalAttrs (root ? license) { license = if isList root.license then attrVals root.license final.lib.licenses else final.lib.licenses.${root.license}; }; }; }) ]; checks = { pkgs, lib, ... }: (optionalAttrs (pathExists (src + /.editorconfig)) { editorconfig = "${lib.getExe pkgs.editorconfig-checker}" + optionalString (!pathExists (src + /.ecrc)) " -disable-indent-size -disable-max-line-length"; }); devTools = pkgs: with pkgs; [ nixpkgs-fmt nodePackages.prettier ]; formatters = { "*.nix" = "nixpkgs-fmt"; "*.md | *.json | *.yml" = "prettier --write"; }; }; importDir = path: genAttrs (pipe (readDir path) [ attrNames (filter (s: s != "default.nix")) (filter (hasSuffix ".nix")) (map (removeSuffix ".nix")) (map (removePrefix "_")) ]) (p: import (path + (if pathExists (path + "/_${p}.nix") then "/_${p}.nix" else "/${p}.nix"))); autoImport = dir: attr: if pathExists (dir + "/${attr}.nix") then import (dir + "/${attr}.nix") else if pathExists (dir + "/${attr}/default.nix") then import (dir + "/${attr}") else if pathExists (dir + "/${attr}") then importDir (dir + "/${attr}") else null; moduleAttrs = [ "withOverlay" "withOverlays" "package" "packages" "devTools" "devShell" "devShells" "env" "overlay" "overlays" "app" "apps" "checks" "nixosModule" "nixosModules" "nixosConfigurations" "template" "templates" "formatters" ]; rootAttrs = moduleAttrs ++ [ "systems" "perSystem" "outputs" "nixDir" ]; autoImportAttrs = dir: attrs: filterAttrs (_: v: v != null) (genAttrs attrs (autoImport dir)); ensureFn = v: if isFunction v then v else _: v; fnConcat = f1: f2: args: (f1 args) ++ (f2 args); fnUpdate = f1: f2: args: (f1 args) // (f2 args); mergeModules = m1: m2: { withOverlays = m1.withOverlays ++ m2.withOverlays; packages = m1.packages // m2.packages; devTools = fnConcat m1.devTools m2.devTools; devShells = fnUpdate m1.devShells m2.devShells; env = fnUpdate m1.env m2.env; overlays = zipAttrsWith (_: composeManyExtensions) [ m1.overlays m2.overlays ]; apps = fnUpdate m1.apps m2.apps; checks = fnUpdate m1.checks m2.checks; nixosModules = m1.nixosModules // m2.nixosModules; nixosConfigurations = m1.nixosConfigurations // m2.nixosConfigurations; templates = m1.templates // m2.templates; formatters = fnUpdate m1.formatters m2.formatters; }; callFn = args: f: let f' = ensureFn f; in if functionArgs f' == { } then f' args else f' (intersectAttrs (functionArgs f) args); tryImport = x: if (isPath x) || (isString x) then import x else x; callPkg = args: f: let f' = ensureFn (tryImport f); in if functionArgs f' == { } then f' args else (args.callPackage or (callPackageWith args)) f' { }; callPkgs = pkgs: mapAttrs (_: callPkg pkgs); defaultPkgName = root: pkg: root.name or pkg.pname or (parseDrvName pkg).name; supportedSystem = { lib, stdenv, ... }: pkg: if pkg ? meta.platforms then lib.meta.availableOn stdenv.hostPlatform pkg else true; systems = rec { linuxDefault = [ "x86_64-linux" "aarch64-linux" ]; linuxAll = linuxDefault ++ [ "armv6l-linux" "armv7l-linux" "i686-linux" ]; }; mkFlake = src: inputs: root: let modules = root.modules or pipe (inputs // { self = { }; }) [ (filterAttrs (_: v: v ? flakeliteModule)) (mapAttrsToList (_: v: v.flakeliteModule)) ]; inputs' = { inherit nixpkgs; } // inputs; nonSysArgs = exports // { args = nonSysArgs; flakelite = exports; inherit src; inputs = inputs'; root = root'; inherit (inputs.nixpkgs) lib; }; applyNonSysArgs = callFn nonSysArgs; moduleAttrDefaults = { withOverlays = [ ]; packages = { }; devTools = _: [ ]; devShells = _: { }; env = _: { }; overlays = { }; apps = _: { }; checks = _: { }; nixosModules = { }; nixosConfigurations = { }; templates = { }; formatters = _: { }; }; normalizeModule = module: let module' = moduleAttrDefaults // module; in module' // { withOverlays = (applyNonSysArgs module'.withOverlays) ++ optional (module' ? withOverlay) module'.withOverlay; packages = (applyNonSysArgs module'.packages) // optionalAttrs (module' ? package) { default = module'.package; }; devTools = ensureFn module'.devTools; devShells = fnUpdate (ensureFn module'.devShells) (_: optionalAttrs (module' ? devShell) { default = module'.devShell; }); env = ensureFn module'.env; overlays = (applyNonSysArgs module'.overlays) // optionalAttrs (module' ? overlay) { default = module'.overlay; }; apps = fnUpdate (ensureFn module'.apps) (_: optionalAttrs (module' ? app) { default = module'.app; }); checks = ensureFn module'.checks; nixosModules = (applyNonSysArgs module'.nixosModules) // optionalAttrs (module' ? nixosModule) { default = module'.nixosModule; }; nixosConfigurations = applyNonSysArgs module'.nixosConfigurations; templates = (applyNonSysArgs module'.templates) // optionalAttrs (module' ? template) { default = module'.template; }; formatters = ensureFn module'.formatters; }; root' = let nixDir = root.nixDir or (src + ./nix); fullRoot = (autoImportAttrs nixDir rootAttrs) // root; in normalizeModule fullRoot // { systems = applyNonSysArgs fullRoot.systems or systems.linuxDefault; perSystem = ensureFn fullRoot.perSystem or (_: { }); outputs = applyNonSysArgs fullRoot.outputs or { }; inherit nixDir; raw = root; }; merged = foldl mergeModules moduleAttrDefaults ((map (m: normalizeModule (applyNonSysArgs m)) ([ builtinModule ] ++ modules)) ++ [ root' ]); pkgsFor = system: import inputs'.nixpkgs { inherit system; overlays = merged.withOverlays ++ [ (final: _: callPkgs final merged.packages) ]; }; systemPkgs = listToAttrs (map (system: nameValuePair system (pkgsFor system)) root'.systems); mkCheck = pkgs: name: cmd: if pkgs.lib.isDerivation cmd then cmd else pkgs.runCommand "check-${name}" { } '' cp --no-preserve=mode -r ${src} src cd src ${cmd} touch $out ''; isApp = x: (x ? type) && (x.type == "app") && (x ? program); mkApp = pkgs: app: let app' = callFn pkgs app; in if isApp app' then app' else { type = "app"; program = "${app'}"; }; eachSystem = fn: foldAttrs mergeAttrs { } (map (system: mapAttrs (_: v: { ${system} = v; }) (fn systemPkgs.${system})) root'.systems); recUpdateSets = foldl (acc: new: recursiveUpdate acc ((ensureFn new) acc)) { }; replaceDefault = set: if set ? default then (removeAttrs set [ "default" ]) // { ${defaultPkgName root' set.default} = set.default; } else set; in recUpdateSets [ (optionalAttrs (merged.packages != { }) ({ overlays.default = final: _: callPkgs (final.appendOverlays merged.withOverlays) (replaceDefault merged.packages); } // eachSystem (pkgs: rec { packages = filterAttrs (_: supportedSystem pkgs) (intersectAttrs merged.packages pkgs); checks = mapAttrs' (n: nameValuePair ("packages-" + n)) packages; }))) (prev: optionalAttrs (merged.overlays != { }) ({ overlays = zipAttrsWith (_: composeManyExtensions) [ (prev.overlays or { }) merged.overlays ]; })) (eachSystem ({ pkgs, lib, ... }: optionalAttrs (merged.formatters pkgs != { }) rec { formatter = pkgs.writeShellScriptBin "formatter" '' PATH=${lib.makeBinPath (merged.devTools pkgs)} for f in "$@"; do if [ -d "$f" ]; then ${pkgs.fd}/bin/fd "$f" -Htf -x "$0" else case "$(${pkgs.coreutils}/bin/basename "$f")" in ${toString (mapAttrsToList (n: v: "${n}) ${v} \"$f\";;") (merged.formatters pkgs))} esac fi done &>/dev/null ''; checks.formatting = mkCheck pkgs "formatting" '' ${lib.getExe formatter} . ${pkgs.diffutils}/bin/diff -qr ${src} . |\ sed 's/Files .* and \(.*\) differ/File \1 not formatted/g' ''; })) (eachSystem (pkgs: let checks = mapAttrs (mkCheck pkgs) (merged.checks pkgs); in optionalAttrs (checks != { }) { inherit checks; })) (eachSystem (pkgs: let apps = mapAttrs (_: mkApp pkgs) (merged.apps pkgs); in optionalAttrs (apps != { }) { inherit apps; })) (optionalAttrs (merged.nixosModules != { }) { inherit (merged) nixosModules; }) (optionalAttrs (merged.nixosConfigurations != { }) { inherit (merged) nixosConfigurations; checks = recUpdateSets (mapAttrsToList (n: v: { ${v.config.nixpkgs.system}."nixos-${n}" = v.config.system.build.toplevel; }) merged.nixosConfigurations); }) (optionalAttrs (merged.templates != { }) { inherit (merged) templates; }) (prev: eachSystem ({ pkgs, system, mkShell, ... }: { devShells.default = mkShell (merged.env pkgs // { inputsFrom = optional (prev ? packages.${system}.default) prev.packages.${system}.default; packages = merged.devTools pkgs; }); } // (callPkgs pkgs (merged.devShells pkgs)))) (eachSystem root'.perSystem) root'.outputs ]; in exports