diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml new file mode 100644 index 0000000..ae2636e --- /dev/null +++ b/.github/workflows/checks.yaml @@ -0,0 +1,30 @@ +name: Checks + +on: + pull_request: + push: + branches: + - main + +jobs: + checks: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - uses: DeterminateSystems/determinate-nix-action@v3 + + - uses: DeterminateSystems/flakehub-cache-action@v3 + + - name: Flake checks (root) + run: | + nix flake check --all-systems + + - name: Flake checks (example) + run: | + nix flake check --all-systems ./example diff --git a/.github/workflows/flakehub-publish-rolling.yaml b/.github/workflows/flakehub-publish-rolling.yaml index 61203af..40c385d 100644 --- a/.github/workflows/flakehub-publish-rolling.yaml +++ b/.github/workflows/flakehub-publish-rolling.yaml @@ -17,6 +17,7 @@ jobs: persist-credentials: false - uses: DeterminateSystems/determinate-nix-action@v3 + - uses: DeterminateSystems/flakehub-push@main with: name: DeterminateSystems/up diff --git a/README.md b/README.md index 731c925..51f0fba 100644 --- a/README.md +++ b/README.md @@ -358,6 +358,29 @@ These attributes are available: | `package` | The watchexec package to use | `pkgs.watchexec` | | `excludeShellChecks` | [shellcheck] rules to disable in the command | `[ ]` | +There's also a function called `mkWatchMany` that enables you to run multiple watchexec commands at the same time by specifying a `watchers` list: + +```nix +{ + dev = pkgs.lib.mkWatchMany { + description = "Watch/build Rust and Protobuf"; + packages = with pkgs; [ buf cargo ]; + watchers = [ + { + command = "buf generate"; + extensions = [ "proto" ]; + paths = [ "proto" ]; + } + { + command = "cargo check"; + extensions = [ "rs" ]; + paths = [ "src" ]; + } + ]; + }; +} +``` + ## Environment variable sets There are two types of environment variable sets: **static** and **computed**. diff --git a/example/flake.nix b/example/flake.nix index aa8431f..7407129 100644 --- a/example/flake.nix +++ b/example/flake.nix @@ -149,6 +149,30 @@ aliases = [ "l" ]; command = "buf lint"; }; + + watch-all = pkgs.lib.mkWatchMany { + description = "Multiple watch tasks"; + aliases = [ "wa" ]; + watchers = [ + { + command = "buf generate"; + paths = [ + "proto" + "buf.gen.yaml" + ]; + extensions = [ + "proto" + "yaml" + ]; + } + { + command = "cargo check"; + paths = [ "src" ]; + extensions = [ "rs" ]; + } + ]; + }; + watch-gen = pkgs.lib.mkWatch { description = "Regenerate stubs on .proto change"; aliases = [ @@ -187,7 +211,7 @@ }; schemas = { - inherit (inputs.flake-schemas.schemas) devShells schemas; + inherit (inputs.flake-schemas.schemas) devShells overlays schemas; } // { inherit (inputs.up.exportedSchemas) processTrees taskRunners; diff --git a/example/src/bin/server.rs b/example/src/bin/server.rs index 314d5b2..3fa02ec 100644 --- a/example/src/bin/server.rs +++ b/example/src/bin/server.rs @@ -24,7 +24,7 @@ impl GreeterService for Greeter { let message = if name.is_empty() { "hello, stranger".into() } else { - format!("hello, {name}") + format!("hello there, {name}") }; tracing::info!(%name, "say_hello"); Ok(Response::new(SayHelloResponse { message })) diff --git a/lib/default.nix b/lib/default.nix index 32765f9..2ef8c6d 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -56,8 +56,20 @@ let }; taskModule = import ./task.nix { inherit lib mkScript pkgs; }; + + mkProcessTree = import ./process-tree.nix { + inherit + lib + mkScript + pkgs + processModule + taskModule + ; + }; in { + inherit mkProcessTree; + mkBenchmarkTask = import ./benchmark.nix { inherit lib @@ -75,16 +87,6 @@ in ]; }).config; - mkProcessTree = import ./process-tree.nix { - inherit - lib - mkScript - pkgs - processModule - taskModule - ; - }; - mkTaskRunner = import ./task-runner.nix { inherit lib @@ -96,5 +98,5 @@ in mkTool = import ./tool.nix { inherit lib mkScript pkgs; }; - mkWatch = import ./watch.nix { inherit lib pkgs; }; + inherit (import ./watch.nix { inherit lib mkProcessTree pkgs; }) mkWatch mkWatchMany; } diff --git a/lib/process-tree.nix b/lib/process-tree.nix index bd34d4e..e54f041 100644 --- a/lib/process-tree.nix +++ b/lib/process-tree.nix @@ -23,6 +23,10 @@ let type = types.listOf types.str; default = [ ]; }; + tui = mkOption { + type = types.bool; + default = false; + }; package = mkOption { type = types.package; default = pkgs.process-compose; @@ -192,6 +196,7 @@ let { json = builtins.toJSON (stripNulls { inherit (config) log_level; + is_tui_disabled = !config.tui; log_location = "/tmp/pc-debug.log"; processes = lib.mapAttrs serializeProcess allProcesses; }); diff --git a/lib/watch.nix b/lib/watch.nix index 8511ea3..3297004 100644 --- a/lib/watch.nix +++ b/lib/watch.nix @@ -1,58 +1,138 @@ { lib, + mkProcessTree, pkgs, }: -{ - command, - paths ? [ "." ], - extensions ? [ ], - ignore ? [ ], - debounce ? null, - package ? pkgs.watchexec, - packages ? [ ], - ... -}@args: +let + mkWatchexecCmd = + { + name ? "watch", + command, + paths ? [ "." ], + extensions ? [ ], + ignore ? [ ], + debounce ? null, + package ? pkgs.watchexec, + }: + assert lib.assertMsg (paths != [ ]) "mkWatchexecCmd: 'paths' must not be empty"; + let + prefix = lib.escapeShellArgs ( + [ (lib.getExe package) ] + ++ lib.concatMap (p: [ + "--watch" + p + ]) paths + ++ lib.optionals (extensions != [ ]) [ + "--exts" + (lib.concatStringsSep "," extensions) + ] + ++ lib.concatMap (p: [ + "--ignore" + p + ]) ignore + ++ lib.optionals (debounce != null) [ + "--debounce" + (toString debounce) + ] + ++ [ "--" ] + ); + in + "${prefix} ${command}"; -assert lib.assertMsg (paths != [ ]) "mkWatch: 'paths' must not be empty"; + mkWatch = + { + command, + paths ? [ "." ], + extensions ? [ ], + ignore ? [ ], + debounce ? null, + package ? pkgs.watchexec, + packages ? [ ], + ... + }@args: + let + taskModuleArgs = builtins.removeAttrs args [ + "command" + "paths" + "extensions" + "ignore" + "debounce" + "package" + "packages" + ]; + watchexecCmd = mkWatchexecCmd { + inherit + command + paths + extensions + ignore + debounce + package + ; + }; + in + taskModuleArgs + // { + raw = true; + skip = true; + packages = packages ++ [ package ]; + command = watchexecCmd; + }; -let - taskModuleArgs = builtins.removeAttrs args [ - "command" - "paths" - "extensions" - "ignore" - "debounce" - "package" - "packages" - ]; + mkWatchMany = + { + name ? "watch-all", + watchers, + package ? pkgs.watchexec, + packages ? [ ], + ... + }@args: + assert lib.assertMsg (watchers != [ ]) "mkWatchMany: 'watchers' must not be empty"; + let + taskModuleArgs = builtins.removeAttrs args [ + "name" + "watchers" + "package" + "packages" + ]; - watchexecPrefix = lib.escapeShellArgs ( - [ (lib.getExe package) ] - ++ lib.concatMap (p: [ - "--watch" - p - ]) paths - ++ lib.optionals (extensions != [ ]) [ - "--exts" - (lib.concatStringsSep "," extensions) - ] - ++ lib.concatMap (p: [ - "--ignore" - p - ]) ignore - ++ lib.optionals (debounce != null) [ - "--debounce" - (toString debounce) - ] - ++ [ "--" ] - ); - watchexecCmd = "${watchexecPrefix} ${command}"; + # Resolve each watcher's package and give it a stable process name. + indexed = lib.imap0 (i: w: { + inherit i; + watcher = w // { + package = w.package or package; + }; + }) watchers; + + # Process name: either user-supplied `name`, or `watcher-`. + processNameOf = { i, watcher }: watcher.name or "watcher-${toString i}"; + + processes = lib.listToAttrs ( + map (entry: { + name = processNameOf entry; + value = { + command = mkWatchexecCmd entry.watcher; + packages = [ entry.watcher.package ]; + }; + }) indexed + ); + in + taskModuleArgs + // { + raw = true; + skip = true; + command = + (mkProcessTree { + inherit + name + packages + processes + ; + }) + + "/bin/${name}"; + }; in -taskModuleArgs -// { - raw = true; - skip = true; - packages = packages ++ [ package ]; - command = watchexecCmd; +{ + inherit mkWatch mkWatchMany; }