diff --git a/README.md b/README.md index 7b1dc00..ac9f26e 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,35 @@ The flake schemas are also helpful: } ``` +## Toolchains + +**Toolchains** are configurable sets of tools that you can add to a dev shell. + +```nix +{ + devShells = forEachSupportedSystem ( + { pkgs, system }: + { + default = + let + pythonToolchain = inputs.up.toolchains.${system}.python { uv = true; }; + rustToolchain = inputs.up.toolchains.${system}.rust { channel = "nightly"; }; + in + pkgs.mkShell { + packages = [ + pythonToolchain.packages + rustToolchain.packages + ]; + + shellHook = '' + ${pythonToolchain.shellHook} + ''; + }; + } + ); +} +``` + ## Process trees **Process trees** are process-compose configurations built with Nix. diff --git a/flake.lock b/flake.lock index d735ee2..0ccde9e 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,25 @@ { "nodes": { + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1775029908, + "narHash": "sha256-QuPn+EN/097aBLeSqbQ7vOwc5TSOb68bAxg1+mknfmw=", + "rev": "380f1969f440e683333af5746caac76811b4a1a8", + "revCount": 2612, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/nix-community/fenix/0.1.2612%2Brev-380f1969f440e683333af5746caac76811b4a1a8/019d4845-48ec-7293-9e63-11b3620f4a54/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/nix-community/fenix/0.1" + } + }, "flake-schemas": { "locked": { "lastModified": 1775244557, @@ -30,9 +50,27 @@ }, "root": { "inputs": { + "fenix": "fenix", "flake-schemas": "flake-schemas", "nixpkgs": "nixpkgs" } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1774948198, + "narHash": "sha256-oVPo0/3CXM/5uFKu1ZwP7osSV2tiQIFU09Y3UzNbm7g=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "63b3eff38ef1c216480147dd53b0e4365d55f269", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 1cdadcc..5b99885 100644 --- a/flake.nix +++ b/flake.nix @@ -4,6 +4,10 @@ inputs = { nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1"; flake-schemas.url = "https://flakehub.com/f/DeterminateSystems/flake-schemas/0"; + fenix = { + url = "https://flakehub.com/f/nix-community/fenix/0.1"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = @@ -26,7 +30,6 @@ pkgs = import inputs.nixpkgs { inherit system; config = { - allowBroken = true; allowUnfree = true; }; overlays = [ self.overlays.default ]; @@ -38,26 +41,58 @@ devShells = forEachSupportedSystem ( { pkgs, system }: { - default = pkgs.mkShellNoCC { - packages = with pkgs; [ - self.taskRunners.${system}.default - self.formatter.${system} - ]; - shellHook = '' - ${self.taskRunners.${system}.default.shellHook} - ''; - }; + default = + let + toolchains = self.toolchains.${system}; + + pythonToolchain = toolchains.python { + uv = true; + }; + + rustToolchain = toolchains.rust { + channel = "stable"; + envSrcPath = true; + }; + + phpToolchain = toolchains.php { }; + + terraformToolchain = toolchains.terraform { + plugins = [ + "hashicorp_aws" + "hashicorp_google" + "hashicorp_kubernetes" + ]; + }; + in + pkgs.mkShellNoCC { + packages = with pkgs; [ + self.taskRunners.${system}.default + self.formatter.${system} + + phpToolchain.packages + pythonToolchain.packages + rustToolchain.packages + terraformToolchain.packages + ]; + shellHook = '' + ${self.taskRunners.${system}.default.shellHook} + ${pythonToolchain.shellHook} + ${phpToolchain.shellHook} + ''; + env = rustToolchain.env // self.envVars.postgres; + }; } ); formatter = forEachSupportedSystem ({ pkgs, ... }: pkgs.nixfmt); - lib = import ./nix/lib.nix { inherit lib; }; + lib = import ./lib { inherit lib; }; processTrees = forEachSupportedSystem ( { pkgs, system }: { - data = pkgs.lib.mkProcessTree { + postgres = pkgs.lib.mkProcessTree { + name = "postgres-process-tree"; description = "Run Postgres locally"; packages = with pkgs; [ @@ -65,12 +100,7 @@ redis ]; - environment = { - PGDATA = ".state/postgres"; - PGDATABASE = "testing"; - PGHOST = "127.0.0.1"; - PGPORT = "5432"; - }; + environment = self.envVars.postgres; processes = { postgres-setup = { @@ -157,10 +187,180 @@ } ); + toolchains = forEachSupportedSystem ( + { pkgs, system }: + { + go = + { + version ? null, + }: + { + packages = + if version == null then + pkgs.go + else + pkgs.${"go_1_${builtins.replaceStrings [ "." ] [ "_" ] (toString version)}"}; + }; + + terraform = + { + plugins ? [ ], + }: + let + tfPkg = pkgs.terraform; + in + { + packages = if plugins == [ ] then tfPkg else tfPkg.withPlugins (p: map (name: p.${name}) plugins); + shellHook = ""; + env = { + TF_CLI_ARGS = "-no-color"; + }; + }; + + js = + { + nodejs ? false, + bun ? false, + npm ? false, + pnpm ? false, + }: + { + packages = pkgs.symlinkJoin { + name = "js-env"; + paths = + lib.optional nodejs pkgs.nodejs + ++ lib.optional bun pkgs.bun + ++ lib.optional npm pkgs.npm + ++ lib.optional pnpm pkgs.pnpm; + }; + }; + + php = + { + version ? "8.3", + ini ? "", + fpm ? { + pools = { }; + }, + }: + let + phpPkg = + pkgs.${"php${builtins.replaceStrings [ "." ] [ "" ] version}"} + or (throw "unknown php version: ${version}"); + + fpmConf = pkgs.writeText "php-fpm.conf" ( + lib.concatStrings ( + lib.mapAttrsToList ( + poolName: pool: + '' + [${poolName}] + '' + + lib.concatStrings (lib.mapAttrsToList (k: v: "${k} = ${v}\n") (pool.settings or { })) + ) (fpm.pools or { }) + ) + ); + + shellHook = lib.optionalString (fpm.pools != { }) '' + php-fpm -y ${fpmConf} -D + trap "php-fpm -y ${fpmConf} -F -R 2>/dev/null" EXIT + ''; + in + { + packages = + if ini == "" then + phpPkg + else + phpPkg.buildEnv { + extraConfig = ini; + }; + shellHook = lib.optionalString (fpm.pools != { }) '' + php-fpm -y ${fpmConf} -D + trap "php-fpm -y ${fpmConf} -F -R 2>/dev/null" EXIT + ''; + env = { }; + }; + + python = + { + version ? null, + uv ? false, + venv ? { + enable = false; + requirements = ""; + }, + }: + let + pythonPkg = + if version == null then + pkgs.python3 + else + pkgs.${"python${builtins.replaceStrings [ "." ] [ "" ] version}"} + or (throw "unknown python version: ${version}"); + in + { + packages = pkgs.symlinkJoin { + name = "python-env"; + paths = [ pythonPkg ] ++ lib.optional uv pkgs.uv; + }; + + shellHook = lib.optionalString (venv.enable or false) '' + uv venv .venv + ${lib.optionalString (venv.requirements or "" != "") '' + uv pip install ${venv.requirements} + ''} + ''; + }; + + rust = + { + stable ? true, + channel ? null, + targets ? [ ], + envSrcPath ? false, + }: + let + fenixPkgs = inputs.fenix.packages.${system}; + + rustToolchain = + ( + if channel != null then + { + "stable" = fenixPkgs.stable; + "nightly" = fenixPkgs.latest; + "beta" = fenixPkgs.beta; + } + .${channel} or (throw "unknown rust channel: ${channel}") + else if stable then + fenixPkgs.stable + else + fenixPkgs.latest + ).toolchain; + + targetStdlibs = map (target: fenixPkgs.targets.${target}.stable.rust-std) targets; + + packages = fenixPkgs.combine ([ rustToolchain ] ++ targetStdlibs); + in + { + env = lib.optionalAttrs envSrcPath { + RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library"; + }; + + inherit packages; + }; + } + ); + + envVars.postgres = { + PGDATA = ".state/postgres"; + PGDATABASE = "testing"; + PGHOST = "127.0.0.1"; + PGPORT = "5432"; + }; + overlays.default = final: prev: { lib = prev.lib - // (import ./nix/lib.nix { + // (import ./lib { inherit (prev) lib; pkgs = prev; }); @@ -196,6 +396,27 @@ ); }; + toolchains = { + version = 1; + doc = '' + The `toolchains` output provides language toolchain builder functions. + Each toolchain returns `{ packages, shellHook }`. + ''; + appendSystem = true; + inventory = + output: + inputs.flake-schemas.lib.mkChildren ( + builtins.mapAttrs (system: toolchains: { + forSystems = [ system ]; + children = builtins.mapAttrs (_name: toolchain: { + forSystems = [ system ]; + evalChecks.isFunction = builtins.isFunction toolchain; + what = "language toolchain builder"; + }) toolchains; + }) output + ); + }; + envVars = { version = 1; doc = '' diff --git a/nix/lib.nix b/lib/default.nix similarity index 80% rename from nix/lib.nix rename to lib/default.nix index c3f573f..c691fc8 100644 --- a/nix/lib.nix +++ b/lib/default.nix @@ -4,20 +4,22 @@ let taskModule = import ./task.nix { inherit lib pkgs; }; in { - mkTaskRunner = import ./runner.nix { + mkTaskRunner = import ./task-runner.nix { inherit lib pkgs taskModule ; }; - mkProcessTree = import ./processes.nix { + + mkProcessTree = import ./process-tree.nix { inherit lib pkgs taskModule ; }; + mkTask = args: (lib.evalModules { diff --git a/nix/processes.nix b/lib/process-tree.nix similarity index 97% rename from nix/processes.nix rename to lib/process-tree.nix index dc3553e..28402fb 100644 --- a/nix/processes.nix +++ b/lib/process-tree.nix @@ -106,6 +106,10 @@ let type = types.nullOr types.str; default = null; }; + package = mkOption { + type = types.package; + default = pkgs.process-compose; + }; log_level = mkOption { type = types.enum [ "info" @@ -206,7 +210,7 @@ let in pkgs.writeShellApplication { inherit (config) name; - runtimeInputs = [ pkgs.process-compose ]; + runtimeInputs = [ config.package ]; text = '' export PATH="${lib.concatStringsSep ":" (map (p: "${p}/bin") config.packages)}:$PATH" diff --git a/nix/runner.nix b/lib/task-runner.nix similarity index 100% rename from nix/runner.nix rename to lib/task-runner.nix diff --git a/nix/task.nix b/lib/task.nix similarity index 100% rename from nix/task.nix rename to lib/task.nix diff --git a/lib/toolchain.nix b/lib/toolchain.nix new file mode 100644 index 0000000..e69de29