diff --git a/README.md b/README.md index 813725e..ea2e0ad 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## What's blueprint? -Blueprint is a light framework that replaces Nix glue code with a regular folder structure. Focus on deploying your infrastructure / package sets instead of reinventing the wheel. +Blueprint replaces Nix glue code by a regular folder structure. Focus on deploying your infrastructure / package sets instead of reinventing the wheel. The ambition is to handle all the workflows to reduce the cost of self-hosting infrastructure (we're not quite there yet). @@ -27,18 +27,20 @@ In some ways, this is the spiritual successor to `flake-utils`, my first attempt at making flakes easier to use. What it's good for: + * Home and SME configurations * Package sets What it's bad for: + * Complicated setups (although we try to provide gracefull fallback) * Developer environments (see devenv.sh) ## Design principles +* User workflows come first. * KISS. We don't need complicated module systems with infinite recursions. * 1:1 mapping. Keep the mapping between attributes predictable. -* Think about user workflows. ## Features @@ -54,14 +56,6 @@ What it's bad for: * Darwin configurations * devshell -## Blacklisted inputs - -In order to avoid name clashes, avoid loading inputs with the following names: -* lib -* pname -* system -* pkgs - ## Packages folder If the ./pkgs folder exists, load every sub-folder in it and map it to the `packages` output. @@ -80,12 +74,9 @@ Don't Don't - ## Related projects * [flake-utils](https://github.com/numtide/flake-utils) the OG for flake libraries. -* [flake-utils-plus]() extending flake-utils with more stuff. * [flake-parts](https://flake.parts) uses the Nix module system. It's too complicated for my taste. -* [std]() ?? -* [snowflake-lib](TODO) - +* [std](https://github.com/divnix/std) +* [snowflake-lib](https://github.com/snowfallorg/lib) diff --git a/docs/folder-structure.md b/docs/folder-structure.md index 0d687e2..bea8cc3 100644 --- a/docs/folder-structure.md +++ b/docs/folder-structure.md @@ -2,13 +2,17 @@ ## High-level -* `devshell.nix` for your developer shell. +* `devshells/` for devshells. * `hosts/` for machine configurations. * `lib/` for Nix functions. * `modules/` for NixOS and other modules. -* `pkgs/` for packages. +* `packages/` for packages. * `templates/` for flake templates. +* `devshell.nix` for the default devshell +* `formatter.nix` for the default formatter +* `package.nix` for the default package + ## File arguments Each file typically gets passed a number of arguments. @@ -20,23 +24,26 @@ Some of the files are instantiated multiple times, once per configured system. S Those take the following arguments: * `inputs`: maps to the flake inputs. +* `flake`: maps to the flake itself. It's a shorthand for `inputs.self`. * `system`: the current system attribute. * `perSystem`: contains the packages of all the inputs, filtered per system. Eg: `perSystem.nixos-anywhere.default` is a shorthand for `inputs.nixos-anywhere.packages..default`. -* `flake`: points to the current flake. It's a shorthand for `inputs.self`. * `pkgs`: and instance of nixpkgs, see [configuration](configuration.md) on how it's configured. ## Mapping -### `devshell.nix` +### `devshell.nix`, `devshells/(.nix|/default.nix)` Contains the developer shell if specified. -Called with the [per-system](#per-system) attributes. +Inputs: + +The [per-system](#per-system) values, plus the `pname` attribute. Flake outputs: -* `devShells..default` -* `checks..devshell` + +* `devShells..` +* `checks..devshell-` #### Example @@ -50,7 +57,7 @@ pkgs.mkShell { } ``` -### `hosts//(configuration.nix|darwin-configuration.nix)` +### `hosts//(configuration.nix|darwin-configuration.nix)` Each folder contains either a NixOS or nix-darwin configuration: @@ -59,10 +66,12 @@ Each folder contains either a NixOS or nix-darwin configuration: Evaluates to a NixOS configuration. Additional values passed: + * `inputs` maps to the current flake inputs. * `flake` maps to `inputs.self`. Flake outputs: + * `nixosConfigurations.` * `checks..nixos-` - contains the system closure. @@ -87,6 +96,7 @@ Flake outputs: Evaluates to a [nix-darwin](https://github.com/LnL7/nix-darwin) configuration. To support it, also add the following lines to the `flake.nix` file: + ```nix { inputs.nix-darwin.url = "github:LnL7/nix-darwin"; @@ -94,10 +104,12 @@ To support it, also add the following lines to the `flake.nix` file: ``` Additional values passed: + * `inputs` maps to the current flake inputs. * `flake` maps to `inputs.self`. Flake outputs: + * `darwinConfiguration.` * `checks..darwin-` - contains the system closure. @@ -105,17 +117,22 @@ Flake outputs: Loaded if it exists. -It takes the `inputs` as a positional argument. +Inputs: + +* `flake` +* `inputs` + +Flake outputs: + +* `lib` - contains the return value of `lib/default.nix` Eg: + ```nix -inputs: +{ flake, inputs }: { } ``` -Flake outputs: -* `lib` - contains the return value of `lib/default.nix` - ### `modules//(|.nix)` Where the type can be: @@ -125,24 +142,28 @@ Where the type can be: These and other unrecognized types also make to `modules..`. -### `packages//(default.nix|package.nix)` +### `package.nix`, `formatter.nix`, `packages/(.nix|/default.nix)` + +This `packages/` folder contains all your packages. -This folder contains all your packages. +For single-package repositories, we also allow a top-level `package.nix` that +maps to the "default" package. + +Inputs: + +The [per-system](#per-system) values, plus the `pname` attribute. Flake outputs: + * `packages..` - will contain the package * `checks..pkgs-` - also contains the package for `nix flake check`. * `checks..pkgs--` - adds all the package `passthru.tests` -#### `default.nix` +#### `default.nix` or top-level `package.nix` Takes the "per-system" arguments. On top of this, it also takes a `pname` argument. -#### `package.nix` - -Use this when copying packages from `nixpkgs/pkgs/by-name`. `pkgs.callPackage` is called onto it. - #### `templates//` Use this if you want your project to be initializable using `nix flake init`. @@ -152,4 +173,5 @@ This is what is used by blueprint in the [getting started](getting-started.md) s If no name is passed, it will look for the "default" folder. Flake outputs: -* `templates.` + +* `templates. -> path` diff --git a/flake.nix b/flake.nix index efcb1f9..184e791 100644 --- a/flake.nix +++ b/flake.nix @@ -9,8 +9,8 @@ outputs = inputs: let - # use self to create self - blueprint = import ./lib inputs; + # Use self to create self + blueprint = import ./lib { inherit inputs; }; in blueprint { inherit inputs; }; } diff --git a/formatter.nix b/formatter.nix new file mode 100644 index 0000000..377c572 --- /dev/null +++ b/formatter.nix @@ -0,0 +1,65 @@ +{ + pname, + pkgs, + flake, +}: +let + formatter = pkgs.writeShellApplication { + name = pname; + + runtimeInputs = [ + pkgs.deadnix + pkgs.nixfmt-rfc-style + ]; + + text = '' + set -euo pipefail + set -x + + deadnix --no-lambda-pattern-names --edit "$@" + + nixfmt "$@" + ''; + + meta = { + description = "format your project"; + }; + }; + + check = + pkgs.runCommand "format-check" + { + nativeBuildInputs = [ + formatter + pkgs.git + ]; + + # only check on Linux + meta.platforms = pkgs.lib.platforms.linux; + } + '' + export HOME=$NIX_BUILD_TOP/home + + # keep timestamps so that treefmt is able to detect mtime changes + cp --no-preserve=mode --preserve=timestamps -r ${flake} source + cd source + git init --quiet + git add . + shopt -s globstar + ${pname} **/*.nix + if ! git diff --exit-code; then + echo "-------------------------------" + echo "aborting due to above changes ^" + exit 1 + fi + touch $out + ''; +in +formatter +// { + passthru = formatter.passthru // { + tests = { + check = check; + }; + }; +} diff --git a/lib/default.nix b/lib/default.nix index 3a92739..cf6bb08 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -1,38 +1,32 @@ -{ nixpkgs, ... }@bpInputs: +{ inputs, ... }: # A bunch of helper utilities for the project let + bpInputs = inputs; + nixpkgs = bpInputs.nixpkgs; lib = nixpkgs.lib; # A generator for the top-level attributes of the flake. # - # Designed to work with nix-systems + # Designed to work with https://github.com/nix-systems mkEachSystem = { inputs, + flake, systems, nixpkgs, }: let - # make compatible with github:nix-systems/default - sys = if lib.isList systems then systems else import systems; - # memoize the args per system - args = lib.genAttrs sys ( + + # Memoize the args per system + args = lib.genAttrs systems ( system: let - # resolve the packages for each input + # Resolve the packages for each input. perSystem = lib.mapAttrs ( - _: flake: (flake.packages or flake.legacyPackages or { }).${system} or { } + _: flake: flake.legacyPackages.${system} or { } // flake.packages.${system} or { } ) inputs; - in - { - # add system as a special, non-overridable value - inherit inputs perSystem system; - # add shortcut for self - self = throw "self was renamed to flake"; - flake = inputs.self; - - # handle nixpkgs specially. + # Handle nixpkgs specially. pkgs = if (nixpkgs.config or { }) == { } then perSystem.nixpkgs @@ -41,15 +35,24 @@ let inherit system; config = nixpkgs.config; }; - } + in + lib.makeScope lib.callPackageWith (_: { + inherit + inputs + perSystem + flake + pkgs + system + ; + }) ); in - f: lib.genAttrs sys (system: f args.${system}); + f: lib.genAttrs systems (system: f args.${system}); - ifDir = path: lib.optionalAttrs (builtins.pathExists path); + optionalPathAttrs = path: f: lib.optionalAttrs (builtins.pathExists path) (f path); # Imports the path and pass the `args` to it if it exists, otherwise, return an empty attrset. - tryImport = path: args: ifDir path (import path args); + tryImport = path: args: optionalPathAttrs path (path: import path args); # Maps all the nix files and folders in a directory to name -> path. importDir = @@ -58,7 +61,7 @@ let entries = builtins.readDir path; # Get paths to directories - onlyDirs = lib.filterAttrs (name: type: type == "directory") entries; + onlyDirs = lib.filterAttrs (_name: type: type == "directory") entries; dirPaths = lib.mapAttrs (name: type: { path = path + "/${name}"; inherit type; @@ -84,7 +87,7 @@ let in lib.optionalAttrs (builtins.pathExists path) (fn combined); - entriesPath = lib.mapAttrs (name: { path, type }: path); + entriesPath = lib.mapAttrs (_name: { path, type }: path); # Prefixes all the keys of an attrset with the given prefix withPrefix = @@ -106,202 +109,250 @@ let _: x: if x.meta ? platforms then lib.elem system x.meta.platforms else true # keep every package that has no meta.platforms ) attrs; - # Create a new flake blueprint - mkFlake = + mkBlueprint' = { - # Pass the flake inputs to the blueprint inputs, - # Load the blueprint from this path - prefix ? null, - # Used to configure nixpkgs - nixpkgs ? { - config = { }; - }, - # The systems to generate the flake for - systems ? inputs.systems or bpInputs.systems, + nixpkgs, + flake, + src, + systems, }: - ( - { inputs }: - let - eachSystem = mkEachSystem { inherit inputs nixpkgs systems; }; - - src = - if prefix == null then - inputs.self - else if builtins.isPath prefix then - prefix - else if builtins.isString prefix then - "${inputs.self}/${prefix}" - else - throw "${builtins.typeOf prefix} is not supported for the prefix"; - - hosts = importDir (src + "/hosts") ( - entries: - let - # Something to pass to all the systems - specialArgs = { - inherit inputs; - # shortcut for self - self = throw "self was renamed to flake"; - flake = inputs.self; - }; + let + specialArgs = { + inherit inputs flake; + self = throw "self was renamed to flake"; + }; - loadNixOS = - path: - # FIXME: we assume it's using the nixpkgs input. How do you switch to another one? - inputs.nixpkgs.lib.nixosSystem { - modules = [ path ]; - inherit specialArgs; - }; + eachSystem = mkEachSystem { + inherit + inputs + flake + nixpkgs + systems + ; + }; - loadNixDarwin = - path: - # FIXME: we assume it's using the nix-darwin input. How do you switch to another one? - (inputs.nix-darwin.lib.darwinSystem { - modules = [ path ]; - inherit specialArgs; - }) - // { - # FIXME: upstream https://github.com/NixOS/nixpkgs/pull/197547 - class = "nix-darwin"; - }; + hosts = importDir (src + "/hosts") ( + entries: + let + loadNixOS = + path: + # FIXME: we assume it's using the nixpkgs input. How do you switch to another one? + inputs.nixpkgs.lib.nixosSystem { + modules = [ path ]; + inherit specialArgs; + }; - loadHost = - name: - { path, type }: - if builtins.pathExists (path + "/configuration.nix") then - loadNixOS (path + "/configuration.nix") - else if builtins.pathExists (path + "/darwin-configuration.nix") then - loadNixDarwin (path + "/darwin-configuration.nix") - else - throw "host '${name}' does not have a configuration"; - in - lib.mapAttrs loadHost entries - ); - - hostsByCategory = lib.mapAttrs (_: hosts: lib.listToAttrs hosts) ( - lib.groupBy ( - x: - if isNixOS x.value then - "nixosConfigurations" - else if isNixDarwin x.value then - "darwinConfigurations" + loadNixDarwin = + path: + # FIXME: we assume it's using the nix-darwin input. How do you switch to another one? + (inputs.nix-darwin.lib.darwinSystem { + modules = [ path ]; + inherit specialArgs; + }) + // { + # FIXME: upstream https://github.com/NixOS/nixpkgs/pull/197547 + class = "nix-darwin"; + }; + + loadHost = + name: + { path, type }: + if builtins.pathExists (path + "/configuration.nix") then + loadNixOS (path + "/configuration.nix") + else if builtins.pathExists (path + "/darwin-configuration.nix") then + loadNixDarwin (path + "/darwin-configuration.nix") else - throw "host '${x.name}' of class '${x.value.class or "unknown"}' not supported" - ) (lib.attrsToList hosts) - ); - - modules = { - common = importDir (src + "/modules/common") entriesPath; - darwin = importDir (src + "/modules/darwin") entriesPath; - home = importDir (src + "/modules/home") entriesPath; - nixos = importDir (src + "/modules/nixos") entriesPath; - }; - in - # FIXME: maybe there are two layers to this. The blueprint, and then the mapping to flake outputs. - { - # Pick self.packages.${system}.formatter or fallback on nixfmt-rfc-style - formatter = eachSystem ( - { pkgs, perSystem, ... }: perSystem.self.formatter or pkgs.nixfmt-rfc-style - ); - - lib = tryImport (src + "/lib") inputs; - - # expose the functor to the top-level - # FIXME: only if it exists - __functor = x: inputs.self.lib.__functor x; - - devShells = eachSystem ( - args: - if builtins.pathExists (src + "/devshell.nix") then - # FIXME: do we want to support multiple shells? - { default = import (src + "/devshell.nix") args; } + throw "host '${name}' does not have a configuration"; + in + lib.mapAttrs loadHost entries + ); + + hostsByCategory = lib.mapAttrs (_: hosts: lib.listToAttrs hosts) ( + lib.groupBy ( + x: + if isNixOS x.value then + "nixosConfigurations" + else if isNixDarwin x.value then + "darwinConfigurations" else - # TODO: what would a default shell look like? - { } - ); - - packages = - lib.traceIf (builtins.pathExists (src + "/pkgs")) "blueprint: the /pkgs folder is now /packages" - importDir - (src + "/packages") - ( - entries: - eachSystem ( - { pkgs, ... }@args: - lib.mapAttrs ( - pname: - { path, type }: - if type == "directory" && !builtins.pathExists (path + "/default.nix") then - pkgs.callPackage "${toString path}/package.nix" { } - else - import path (args // { inherit pname; }) - ) entries - ) - ); + throw "host '${x.name}' of class '${x.value.class or "unknown"}' not supported" + ) (lib.attrsToList hosts) + ); + + modules = { + common = importDir (src + "/modules/common") entriesPath; + darwin = importDir (src + "/modules/darwin") entriesPath; + home = importDir (src + "/modules/home") entriesPath; + nixos = importDir (src + "/modules/nixos") entriesPath; + }; + in + # FIXME: maybe there are two layers to this. The blueprint, and then the mapping to flake outputs. + { + formatter = eachSystem ( + { pkgs, perSystem, ... }: perSystem.self.formatter or pkgs.nixfmt-rfc-style + ); - darwinConfigurations = hostsByCategory.darwinConfigurations or { }; - nixosConfigurations = hostsByCategory.nixosConfigurations or { }; + lib = tryImport (src + "/lib") specialArgs; - inherit modules; - darwinModules = modules.darwin; - homeModules = modules.home; - # TODO: how to extract NixOS tests? - nixosModules = modules.nixos; + # expose the functor to the top-level + # FIXME: only if it exists + __functor = x: inputs.self.lib.__functor x; - templates = importDir (src + "/templates") ( - entries: - lib.mapAttrs ( - name: - { path, type }: + devShells = + (optionalPathAttrs (src + "/devshells") ( + path: + importDir path ( + entries: + eachSystem ( + { newScope, ... }: + lib.mapAttrs (pname: { path, type }: newScope { inherit pname; } path { }) entries + ) + ) + )) + // (optionalPathAttrs (src + "/devshell.nix") ( + path: + eachSystem ( + { newScope, ... }: { - path = path; - # FIXME: how can we add something more meaningful? - description = name; + default = newScope { pname = "default"; } path { }; } - ) entries - ); - - checks = eachSystem ( - { system, ... }: - lib.mergeAttrsList [ - # add all the supported packages, and their passthru.tests to checks - (withPrefix "pkgs-" ( - lib.concatMapAttrs ( - pname: package: + ) + )); + + packages = + lib.traceIf (builtins.pathExists (src + "/pkgs")) "blueprint: the /pkgs folder is now /packages" + ( + (optionalPathAttrs (src + "/packages") ( + path: + importDir path ( + entries: + eachSystem ( + { newScope, ... }: + lib.mapAttrs (pname: { path, type }: newScope { inherit pname; } path { }) entries + ) + ) + )) + // (optionalPathAttrs (src + "/package.nix") ( + path: + eachSystem ( + { newScope, ... }: { - ${pname} = package; + default = newScope { pname = "default"; } path { }; } - # also add the passthru.tests to the checks - // (lib.mapAttrs' (tname: test: { - name = "${pname}-${tname}"; - value = test; - }) (filterPlatforms system (package.passthru.tests or { }))) - ) (filterPlatforms system (inputs.self.packages.${system} or { })) - )) - # build all the devshells - (withPrefix "devshell-" (inputs.self.devShells.${system} or { })) - # add nixos system closures to checks - (withPrefix "nixos-" ( - lib.mapAttrs (_: x: x.config.system.build.toplevel) ( - lib.filterAttrs (_: x: x.pkgs.system == system) (inputs.self.nixosConfigurations or { }) ) )) - # add darwin system closures to checks - (withPrefix "darwin-" ( - lib.mapAttrs (_: x: x.system) ( - lib.filterAttrs (_: x: x.pkgs.system == system) (inputs.self.darwinConfigurations or { }) + // (optionalPathAttrs (src + "/formatter.nix") ( + path: + eachSystem ( + { newScope, ... }: + { + formatter = newScope { pname = "formatter"; } path { }; + } ) )) - ] - ); - } - ) - { inputs = bpInputs // inputs; }; + ); + + darwinConfigurations = hostsByCategory.darwinConfigurations or { }; + nixosConfigurations = hostsByCategory.nixosConfigurations or { }; + + inherit modules; + darwinModules = modules.darwin; + homeModules = modules.home; + # TODO: how to extract NixOS tests? + nixosModules = modules.nixos; + + templates = importDir (src + "/templates") ( + entries: + lib.mapAttrs ( + name: + { path, type }: + { + path = path; + # FIXME: how can we add something more meaningful? + description = name; + } + ) entries + ); + + checks = eachSystem ( + { system, ... }: + lib.mergeAttrsList [ + # add all the supported packages, and their passthru.tests to checks + (withPrefix "pkgs-" ( + lib.concatMapAttrs ( + pname: package: + { + ${pname} = package; + } + # also add the passthru.tests to the checks + // (lib.mapAttrs' (tname: test: { + name = "${pname}-${tname}"; + value = test; + }) (filterPlatforms system (package.passthru.tests or { }))) + ) (filterPlatforms system (inputs.self.packages.${system} or { })) + )) + # build all the devshells + (withPrefix "devshell-" (inputs.self.devShells.${system} or { })) + # add nixos system closures to checks + (withPrefix "nixos-" ( + lib.mapAttrs (_: x: x.config.system.build.toplevel) ( + lib.filterAttrs (_: x: x.pkgs.system == system) (inputs.self.nixosConfigurations or { }) + ) + )) + # add darwin system closures to checks + (withPrefix "darwin-" ( + lib.mapAttrs (_: x: x.system) ( + lib.filterAttrs (_: x: x.pkgs.system == system) (inputs.self.darwinConfigurations or { }) + ) + )) + ] + ); + }; + + # Create a new flake blueprint + mkBlueprint = + { + # Pass the flake inputs to blueprint + inputs, + # Load the blueprint from this path + prefix ? null, + # Used to configure nixpkgs + nixpkgs ? { + config = { }; + }, + # The systems to generate the flake for + systems ? inputs.systems or bpInputs.systems, + }: + mkBlueprint' { + inputs = bpInputs // inputs; + flake = inputs.self; + + inherit nixpkgs; + + src = + if prefix == null then + inputs.self + else if builtins.isPath prefix then + prefix + else if builtins.isString prefix then + "${inputs.self}/${prefix}" + else + throw "${builtins.typeOf prefix} is not supported for the prefix"; + + # Make compatible with github:nix-systems/default + systems = if lib.isList systems then systems else import systems; + }; in { - inherit mkFlake; + inherit + filterPlatforms + importDir + mkBlueprint + tryImport + withPrefix + ; # Make this callable - __functor = _: mkFlake; + __functor = _: mkBlueprint; } diff --git a/packages/bp/package.nix b/package.nix similarity index 69% rename from packages/bp/package.nix rename to package.nix index 2c4e7aa..d0b7149 100644 --- a/packages/bp/package.nix +++ b/package.nix @@ -1,11 +1,7 @@ -{ - nixos-rebuild, - runCommand, - writeShellApplication, -}: +{ pname, pkgs }: let - bp = writeShellApplication { - name = "bp"; + bp = pkgs.writeShellApplication { + name = pname; runtimeInputs = [ ]; @@ -18,7 +14,7 @@ let shift # Allow running the command as a user export SUDO_USER=1 - echo ${nixos-rebuild}/bin/nixos-rebuild --flake . switch "$@" + echo ${pkgs.nixos-rebuild}/bin/nixos-rebuild --flake . switch "$@" ;; *) echo "Usage: bp [switch]" @@ -36,7 +32,7 @@ bp # https://github.com/NixOS/nixpkgs/pull/320973 passthru = bp.passthru // { tests = { - does-it-run = runCommand "bp-does-it-run" { } '' + does-it-run = pkgs.runCommand "bp-does-it-run" { } '' ${bp}/bin/bp --help > $out ''; }; diff --git a/templates/default/flake.nix b/templates/default/flake.nix index 4531a5b..27ce2ee 100644 --- a/templates/default/flake.nix +++ b/templates/default/flake.nix @@ -8,6 +8,6 @@ blueprint.inputs.nixpkgs.follows = "nixpkgs"; }; - # Keep the magic invocations to minimum. + # Load the blueprint outputs = inputs: inputs.blueprint { inherit inputs; }; }