Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/checks.yaml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .github/workflows/flakehub-publish-rolling.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
persist-credentials: false

- uses: DeterminateSystems/determinate-nix-action@v3

- uses: DeterminateSystems/flakehub-push@main
with:
name: DeterminateSystems/up
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**.
Expand Down
26 changes: 25 additions & 1 deletion example/flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion example/src/bin/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }))
Expand Down
24 changes: 13 additions & 11 deletions lib/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -75,16 +87,6 @@ in
];
}).config;

mkProcessTree = import ./process-tree.nix {
inherit
lib
mkScript
pkgs
processModule
taskModule
;
};

mkTaskRunner = import ./task-runner.nix {
inherit
lib
Expand All @@ -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;
}
5 changes: 5 additions & 0 deletions lib/process-tree.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
});
Expand Down
176 changes: 128 additions & 48 deletions lib/watch.nix
Original file line number Diff line number Diff line change
@@ -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)
Comment on lines +34 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, explore the repository structure
git ls-files | head -20

Repository: DeterminateSystems/up

Length of output: 480


🏁 Script executed:

# Find and read lib/watch.nix around lines 34-36
if [ -f lib/watch.nix ]; then
  wc -l lib/watch.nix
fi

Repository: DeterminateSystems/up

Length of output: 83


🏁 Script executed:

# Read the relevant section from lib/watch.nix
if [ -f lib/watch.nix ]; then
  sed -n '30,40p' lib/watch.nix
fi

Repository: DeterminateSystems/up

Length of output: 292


🏁 Script executed:

# Search for debounce usage in lib/process-tree.nix
if [ -f lib/process-tree.nix ]; then
  rg -n "debounce" lib/process-tree.nix -A 2 -B 2
fi

Repository: DeterminateSystems/up

Length of output: 412


🌐 Web query:

watchexec --debounce duration syntax milliseconds documentation

💡 Result:

The syntax for the --debounce option in watchexec is -d, --debounce , where specifies the time to wait for new events before taking action (default: 50ms). It accepts: - A unit-less value in milliseconds (e.g., 100 for 100ms). This is deprecated, will warn, and become an error in the future. - A time span value (e.g., 5sec, 20ms, 1min 30s, 30min). Example: watchexec -d 100 command or watchexec --debounce 500ms command. Setting to 0 is highly discouraged. This is from the official watchexec manual.

Citations:


Add explicit ms unit to --debounce argument.

Watchexec accepts unit-less debounce values as milliseconds but marks this form deprecated and will error on it in future versions. The lib/process-tree.nix path already uses "${toString w.debounce}ms", so this inconsistency makes the mkWatchexecCmd helper fragile and diverges from the established pattern.

🔧 Proposed fix
         ++ lib.optionals (debounce != null) [
           "--debounce"
-          (toString debounce)
+          "${toString debounce}ms"
         ]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
++ lib.optionals (debounce != null) [
"--debounce"
(toString debounce)
+ lib.optionals (debounce != null) [
"--debounce"
"${toString debounce}ms"
]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/watch.nix` around lines 34 - 36, The mkWatchexecCmd snippet that appends
the "--debounce" argument currently passes a unit-less number via (toString
debounce); change it to append the explicit milliseconds unit (e.g. "${toString
debounce}ms" or equivalent string concatenation) so the debounce value matches
the pattern used in lib/process-tree.nix (which uses "${toString w.debounce}ms")
and avoids future watchexec errors; update the array element built in
lib.optionals (debounce != null) to include the "ms" suffix.

]
++ [ "--" ]
);
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"
];
Comment thread
lucperkins marked this conversation as resolved.

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-<index>`.
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
);
Comment on lines +108 to +119
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject duplicate watcher names before building processes.

If two watchers resolve to the same name here, builtins.listToAttrs collapses the duplicate and only one process survives, so one watcher never starts. That can happen with repeated explicit names or with an explicit name colliding with the default watcher-<index> fallback. (releases.nixos.org)

🔧 Proposed fix
       # Process name: either user-supplied `name`, or `watcher-<index>`.
       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
-      );
+      processNames = map processNameOf indexed;
+
+      processes =
+        assert lib.assertMsg
+          (lib.length processNames == lib.length (lib.unique processNames))
+          "mkWatchMany: watcher names must be unique";
+        lib.listToAttrs (
+          map (entry: {
+            name = processNameOf entry;
+            value = {
+              command = mkWatchexecCmd entry.watcher;
+              packages = [ entry.watcher.package ];
+            };
+          }) indexed
+        );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Process name: either user-supplied `name`, or `watcher-<index>`.
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
);
# Process name: either user-supplied `name`, or `watcher-<index>`.
processNameOf = { i, watcher }: watcher.name or "watcher-${toString i}";
processNames = map processNameOf indexed;
processes =
assert lib.assertMsg
(lib.length processNames == lib.length (lib.unique processNames))
"mkWatchMany: watcher names must be unique";
lib.listToAttrs (
map (entry: {
name = processNameOf entry;
value = {
command = mkWatchexecCmd entry.watcher;
packages = [ entry.watcher.package ];
};
}) indexed
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/watch.nix` around lines 108 - 119, The code currently builds processes
using processNameOf and then passes them to builtins.listToAttrs which silently
collapses duplicate names; detect duplicates first by mapping each indexed entry
through processNameOf to a list of resolved names, check for any duplicates
(same name appearing more than once), and if found raise a clear error (e.g.,
via throw or lib.error) listing the duplicated name(s) and their watcher
indices; only proceed to build processes with listToAttrs if no duplicates are
detected. Ensure you reference the existing processNameOf and processes logic
when adding the duplicate-checking step so the validation happens before
listToAttrs is invoked.

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;
}