diff --git a/.envrc b/.envrc new file mode 100755 index 00000000..e835c4dd --- /dev/null +++ b/.envrc @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Environment variables for the project. See https://direnv.net/ +if nix flake metadata &>/dev/null; then + use flake +else + use nix +fi diff --git a/default.nix b/default.nix index 050aba61..a2d59d6f 100644 --- a/default.nix +++ b/default.nix @@ -13,6 +13,18 @@ let cargoToml = with builtins; (fromTOML (readFile ./Cargo.toml)); + # Use the Nix module system to validate the treefmt config file format. + evalModule = config: + lib.evalModules { + modules = [ + { + _module.args = { inherit nixpkgs lib treefmt; }; + } + ./module-options.nix + config + ]; + }; + # What is used when invoking `nix run github:numtide/treefmt` treefmt = rustPackages.rustPlatform.buildRustPackage { inherit (cargoToml.package) name version; @@ -34,6 +46,12 @@ let cargoLock.lockFile = ./Cargo.lock; meta.description = "one CLI to format the code tree"; + + passthru.withConfig = config: + let + mod = evalModule config; + in + mod.config.build.wrapper; }; # Add all the dependencies of treefmt, plus more build tools @@ -68,7 +86,13 @@ let }); in { - inherit treefmt devShell; + inherit treefmt devShell evalModule; + + # module that generates and wraps the treefmt config with Nix + module = ./module-options.nix; + + # reduce a bit of repetition + inherit (treefmt.passthru) withConfig; # A collection of packages for the project docs = nixpkgs.callPackage ./docs { }; diff --git a/flake.lock b/flake.lock index 9c02b5d4..ebad395c 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1661009076, - "narHash": "sha256-phAE40gctVygRq3G3B6LhvD7u2qdQT21xsz8DdRDYFo=", + "lastModified": 1663686189, + "narHash": "sha256-cgWGo0SLM4RX0DqodeFHsU0PRGlmz6AOSF1lmX21bvg=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "850d8a76026127ef02f040fb0dcfdb8b749dd9d9", + "rev": "f17e9dba098c0a7bca10029bec5384fd6425c157", "type": "github" }, "original": { @@ -22,11 +22,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1661239106, - "narHash": "sha256-C5OCLnrv2c4CHs9DMEtYKkjJmGL7ySAZ1PqPkHBonxQ=", + "lastModified": 1663669617, + "narHash": "sha256-yUrzkRDc6P3hsI5TdQ5+q8gnyjadNvaM3MMEsEVS8qk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "963d27a0767422be9b8686a8493dcade6acee992", + "rev": "41ac0bd371618db6dd67fd952cc5b3d6a9955a15", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 570ccd23..878f57b4 100644 --- a/flake.nix +++ b/flake.nix @@ -18,9 +18,22 @@ }; in { - inherit packages; + # This contains a mix of packages, modules, ... + legacyPackages = packages; devShells.default = packages.devShell; + + # In Nix 2.8 you can run `nix fmt` to format this whole repo. + # + # Because we load the treefmt.toml and don't define links to the + # packages in Nix, the formatter has to run inside of `nix develop` + # to have the various tools on the PATH. + # + # It also assumes that the project root has a flake.nix (override this by setting `projectRootFile`). + formatter = packages.treefmt.withConfig { + settings = nixpkgs.lib.importTOML ./treefmt.toml; + projectRootFile = "flake.nix"; + }; }; }; } diff --git a/module-options.nix b/module-options.nix new file mode 100644 index 00000000..b41d85c2 --- /dev/null +++ b/module-options.nix @@ -0,0 +1,113 @@ +{ lib, nixpkgs, treefmt, ... }: +let + # A new kind of option type that calls lib.getExe on derivations + exeType = lib.mkOptionType { + name = "exe"; + description = "Path to executable"; + check = (x: lib.isString x || builtins.isPath x || lib.isDerivation x); + merge = loc: defs: + let res = lib.mergeOneOption loc defs; in + if lib.isString res || builtins.isPath res then + "${res}" + else + lib.getExe res; + }; + + # The schema of the treefmt.toml data structure. + configSchema = with lib; { + excludes = mkOption { + description = "A global list of paths to exclude. Supports glob."; + type = types.listOf types.str; + default = [ ]; + example = [ "./node_modules/**" ]; + }; + + formatter = mkOption { + type = types.attrsOf (types.submodule [{ + options = { + command = mkOption { + description = "Executable obeying the treefmt formatter spec"; + type = exeType; + }; + + options = mkOption { + description = "List of arguments to pass to the command"; + type = types.listOf types.str; + default = [ ]; + }; + + includes = mkOption { + description = "List of files to include for formatting. Supports globbing."; + type = types.listOf types.str; + }; + + excludes = mkOption { + description = "List of files to exclude for formatting. Supports globbing. Takes precedence over the includes."; + type = types.listOf types.str; + default = [ ]; + }; + }; + }]); + default = { }; + description = "Set of formatters to use"; + }; + }; + + configFormat = nixpkgs.formats.toml { }; +in +{ + # Schema + options = { + settings = configSchema; + + package = lib.mkOption { + description = "Package wrapped in the build.wrapper output"; + type = lib.types.package; + default = treefmt; + }; + + projectRootFile = lib.mkOption { + description = '' + File to look for to determine the root of the project in the + build.wrapper. + ''; + example = "flake.nix"; + }; + + # Outputs + build = { + configFile = lib.mkOption { + description = '' + Contains the generated config file derived from the settings. + ''; + type = lib.types.path; + }; + wrapper = lib.mkOption { + description = '' + The treefmt package, wrapped with the config file. + ''; + type = lib.types.package; + }; + }; + }; + # Config + config.build = { + configFile = configFormat.generate "treefmt.toml" config.settings; + + wrapper = nixpkgs.writeShellScriptBin "treefmt" '' + find_up() ( + ancestors=() + while [[ ! -f "$1" ]]; do + ancestors+=("$PWD") + if [[ $PWD == / ]]; then + echo "ERROR: Unable to locate the projectRootFile ($1) in any of: ''${ancestors[*]@Q}" >&2 + exit 1 + fi + cd .. + done + ) + tree_root=$(find_up "${config.projectRootFile}") + exec ${config.package}/bin/treefmt --config-file ${config.build.configFile} "$@" --tree-root "$tree_root" + ''; + }; +};