Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion lib/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,5 @@ in

mkTool = import ./tool.nix { inherit lib mkScript pkgs; };

mkWatch = import ./watch.nix { inherit lib pkgs; };
inherit (import ./watch.nix { inherit lib pkgs; }) mkWatch mkWatchMany;
}
171 changes: 121 additions & 50 deletions lib/watch.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,127 @@
pkgs,
}:

{
command,
paths ? [ "." ],
extensions ? [ ],
ignore ? [ ],
debounce ? null,
package ? pkgs.watchexec,
packages ? [ ],
...
}@args:

assert lib.assertMsg (paths != [ ]) "mkWatch: 'paths' must not be empty";

let
taskModuleArgs = builtins.removeAttrs args [
"command"
"paths"
"extensions"
"ignore"
"debounce"
"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}";
mkWatchexecCmd =
{
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}";

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

mkWatchMany =
{
watchers,
package ? pkgs.watchexec,
packages ? [ ],
exitMsg ? "Shutting down",
...
}@args:
assert lib.assertMsg (watchers != [ ]) "mkWatchMany: 'watchers' must not be empty";
let
taskModuleArgs = builtins.removeAttrs args [
"watchers"
"package"
"packages"
"exitMsg"
];
Comment thread
lucperkins marked this conversation as resolved.

# Resolve each watcher's package (explicit > shared default).
watchexecPkg = map (w: w // { package = w.package or package; }) watchers;
watcherCmds = map mkWatchexecCmd watchexecPkg;
watcherPackages = map (w: w.package) watchexecPkg;

command = ''
pids=()

shutdown() {
trap - INT TERM
echo "${exitMsg}" >&2
kill "''${pids[@]}" 2>/dev/null
wait "''${pids[@]}" 2>/dev/null
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
trap shutdown INT TERM

${lib.concatMapStringsSep "\n" (c: "${c} & pids+=($!)") watcherCmds}

wait
'';
in
taskModuleArgs
// {
raw = true;
skip = true;
packages = lib.unique (packages ++ watcherPackages);
inherit command;
};
in
taskModuleArgs
// {
raw = true;
skip = true;
packages = packages ++ [ package ];
command = watchexecCmd;
{
inherit mkWatch mkWatchMany;
}