Compare commits

..

No commits in common. "main" and "1.8.5" have entirely different histories.
main ... 1.8.5

239 changed files with 2435 additions and 29705 deletions

View file

@ -1,11 +0,0 @@
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
[target.armv7-unknown-linux-musleabihf]
linker = "arm-linux-musleabihf-gcc"
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
[target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-musl-gcc"

View file

@ -1,12 +0,0 @@
-W clippy::nursery
-W clippy::pedantic
-A clippy::module-name-repetitions
-A clippy::similar-names
-A clippy::cognitive-complexity
-A clippy::too-many-lines
-A clippy::missing-errors-doc
-A clippy::missing-panics-doc
-A clippy::default-trait-access
-A clippy::enum-glob-use
-A clippy::option-if-let-else
-A clippy::blocks-in-conditions

View file

@ -1,21 +0,0 @@
root = true
[*]
indent_style = tab
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[cli/tests/snapshots/*]
indent_style = space
trim_trailing_whitespace = false
[*.{md,ronn}]
indent_style = space
indent_size = 4
[*.{cff,yml}]
indent_size = 2
indent_style = space

3
.gitattributes vendored
View file

@ -1,3 +0,0 @@
Cargo.lock merge=binary
doc/watchexec.* merge=binary
completions/* merge=binary

View file

@ -1,17 +0,0 @@
---
name: Bug report
about: Something is wrong
title: ''
labels: bug, need-info
assignees: ''
---
Please delete this template text before filing, but you _need_ to include the following:
- Watchexec's version
- The OS you're using
- A log with `-vvv --log-file` (if it has sensitive info you can email it at felix@passcod.name — do that _after_ filing so you can reference the issue ID)
- A sample command that you've run that has the issue
Thank you

View file

@ -1,24 +0,0 @@
---
name: Feature request
about: Something is missing
title: ''
labels: feature
assignees: ''
---
<!-- Please note that this project has a high threshold for changing default behaviour or breaking compatibility. If your feature or change can be done without breaking, present it that way. -->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
If proposing a new CLI option, option names you think would fit.
**Additional context**
Add any other context about the feature request here.

View file

@ -1,23 +0,0 @@
---
name: Regression
about: Something changed unexpectedly
title: ''
labels: ''
assignees: ''
---
**What used to happen**
**What happens now**
**Details**
- Latest version that worked:
- Earliest version that doesn't: (don't sweat testing earlier versions if you don't remember or have time, your current version will do)
- OS:
- A debug log with `-vvv --log-file`:
```
```
<!-- You may truncate the log to just the part supporting your report if you're confident the rest is irrelevant. If it contains sensitive information (if you can't reduce/reproduce outside of work you'd rather remain private, you can either redact it or send it by email.) -->

View file

@ -1,50 +0,0 @@
# Dependabot dependency version checks / updates
version: 2
updates:
- package-ecosystem: "github-actions"
# Workflow files stored in the
# default location of `.github/workflows`
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/crates/cli"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/crates/lib"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/crates/events"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/crates/signals"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/crates/supervisor"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/crates/filterer/ignore"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/crates/filterer/globset"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/crates/bosion"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/crates/ignore-files"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/crates/project-origins"
schedule:
interval: "weekly"

View file

@ -1,62 +0,0 @@
name: Clippy
on:
workflow_dispatch:
pull_request:
push:
branches:
- main
tags-ignore:
- "*"
env:
CARGO_TERM_COLOR: always
CARGO_UNSTABLE_SPARSE_REGISTRY: "true"
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs:
clippy:
strategy:
fail-fast: false
matrix:
platform:
- ubuntu
- windows
- macos
name: Clippy on ${{ matrix.platform }}
runs-on: "${{ matrix.platform }}-latest"
steps:
- uses: actions/checkout@v4
- name: Configure toolchain
run: |
rustup toolchain install stable --profile minimal --no-self-update --component clippy
rustup default stable
# https://github.com/actions/cache/issues/752
- if: ${{ runner.os == 'Windows' }}
name: Use GNU tar
shell: cmd
run: |
echo "Adding GNU tar to PATH"
echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%"
- name: Configure caching
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Clippy
run: cargo clippy -- $(cat .clippy-lints | tr -d '\r' | xargs)
shell: bash

View file

@ -1,26 +0,0 @@
{
dist_version: "0.0.2",
releases: [{
app_name: "watchexec",
app_version: $version,
changelog_title: "CLI \($version)",
artifacts: [ $files | split("\n") | .[] | {
name: .,
kind: (if (. | test("[.](deb|rpm)$")) then "installer" else "executable-zip" end),
target_triples: (. | [capture("watchexec-[^-]+-(?<target>[^.]+)[.].+").target]),
assets: ([[
{
kind: "executable",
name: (if (. | test("windows")) then "watchexec.exe" else "watchexec" end),
path: "\(
capture("(?<dir>watchexec-[^-]+-[^.]+)[.].+").dir
)\(
if (. | test("windows")) then "\\watchexec.exe" else "/watchexec" end
)",
},
(if (. | test("[.](deb|rpm)$")) then null else {kind: "readme", name: "README.md"} end),
(if (. | test("[.](deb|rpm)$")) then null else {kind: "license", name: "LICENSE"} end)
][] | select(. != null)])
} ]
}]
}

View file

@ -1,325 +0,0 @@
name: CLI Release
on:
workflow_dispatch:
push:
tags:
- "v*.*.*"
env:
CARGO_TERM_COLOR: always
CARGO_UNSTABLE_SPARSE_REGISTRY: "true"
jobs:
info:
name: Gather info
runs-on: ubuntu-latest
outputs:
cli_version: ${{ steps.version.outputs.cli_version }}
steps:
- uses: actions/checkout@v4
- name: Extract version
id: version
shell: bash
run: |
set -euxo pipefail
version=$(grep -m1 -F 'version =' crates/cli/Cargo.toml | cut -d\" -f2)
if [[ -z "$version" ]]; then
echo "Error: no version :("
exit 1
fi
echo "cli_version=$version" >> $GITHUB_OUTPUT
build:
strategy:
matrix:
name:
- linux-amd64-gnu
- linux-amd64-musl
- linux-i686-musl
- linux-armhf-gnu
- linux-arm64-gnu
- linux-arm64-musl
- linux-s390x-gnu
- linux-ppc64le-gnu
- mac-x86-64
- mac-arm64
- windows-x86-64
include:
- name: linux-amd64-gnu
os: ubuntu-latest
target: x86_64-unknown-linux-gnu
cross: false
experimental: false
- name: linux-amd64-musl
os: ubuntu-latest
target: x86_64-unknown-linux-musl
cross: true
experimental: false
- name: linux-i686-musl
os: ubuntu-latest
target: i686-unknown-linux-musl
cross: true
experimental: true
- name: linux-armhf-gnu
os: ubuntu-latest
target: armv7-unknown-linux-gnueabihf
cross: true
experimental: false
- name: linux-arm64-gnu
os: ubuntu-latest
target: aarch64-unknown-linux-gnu
cross: true
experimental: false
- name: linux-arm64-musl
os: ubuntu-latest
target: aarch64-unknown-linux-musl
cross: true
experimental: true
- name: linux-s390x-gnu
os: ubuntu-latest
target: s390x-unknown-linux-gnu
cross: true
experimental: false
- name: linux-ppc64le-gnu
os: ubuntu-latest
target: powerpc64le-unknown-linux-gnu
cross: true
experimental: false
- name: mac-x86-64
os: macos-latest
target: x86_64-apple-darwin
cross: false
experimental: false
- name: mac-arm64
os: macos-latest
target: aarch64-apple-darwin
cross: true
experimental: false
- name: windows-x86-64
os: windows-latest
target: x86_64-pc-windows-msvc
cross: false
experimental: false
#- name: windows-arm64
# os: windows-latest
# target: aarch64-pc-windows-msvc
# cross: true
# experimental: true
name: Binaries for ${{ matrix.name }}
needs: info
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
env:
version: ${{ needs.info.outputs.cli_version }}
dst: watchexec-${{ needs.info.outputs.cli_version }}-${{ matrix.target }}
steps:
- uses: actions/checkout@v4
# https://github.com/actions/cache/issues/752
- if: ${{ runner.os == 'Windows' }}
name: Use GNU tar
shell: cmd
run: |
echo "Adding GNU tar to PATH"
echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%"
- name: Configure caching
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-${{ matrix.target }}-
${{ runner.os }}-cargo-
- run: sudo apt update
if: startsWith(matrix.os, 'ubuntu-')
- name: Add musl tools
run: sudo apt install -y musl musl-dev musl-tools
if: endsWith(matrix.target, '-musl')
- name: Add aarch-gnu tools
run: sudo apt install -y gcc-aarch64-linux-gnu
if: startsWith(matrix.target, 'aarch64-unknown-linux')
- name: Add arm7hf-gnu tools
run: sudo apt install -y gcc-arm-linux-gnueabihf
if: startsWith(matrix.target, 'armv7-unknown-linux-gnueabihf')
- name: Add s390x-gnu tools
run: sudo apt install -y gcc-s390x-linux-gnu
if: startsWith(matrix.target, 's390x-unknown-linux-gnu')
- name: Add ppc64le-gnu tools
run: sudo apt install -y gcc-powerpc64le-linux-gnu
if: startsWith(matrix.target, 'powerpc64le-unknown-linux-gnu')
- name: Install cargo-deb
if: startsWith(matrix.name, 'linux-')
uses: taiki-e/install-action@v2
with:
tool: cargo-deb
- name: Install cargo-generate-rpm
if: startsWith(matrix.name, 'linux-')
uses: taiki-e/install-action@v2
with:
tool: cargo-generate-rpm
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
rustup default stable
rustup target add ${{ matrix.target }}
- name: Install cross
if: matrix.cross
uses: taiki-e/install-action@v2
with:
tool: cross
- name: Build
shell: bash
run: |
${{ matrix.cross && 'cross' || 'cargo' }} build \
-p watchexec-cli \
--release --locked \
--target ${{ matrix.target }}
- name: Make manpage
shell: bash
run: |
cargo run -p watchexec-cli \
${{ (!matrix.cross) && '--release --target' || '' }} \
${{ (!matrix.cross) && matrix.target || '' }} \
--locked -- --manual > doc/watchexec.1
- name: Make completions
shell: bash
run: |
bin/completions \
${{ (!matrix.cross) && '--release --target' || '' }} \
${{ (!matrix.cross) && matrix.target || '' }} \
--locked
- name: Package
shell: bash
run: |
set -euxo pipefail
ext=""
[[ "${{ matrix.name }}" == windows-* ]] && ext=".exe"
bin="target/${{ matrix.target }}/release/watchexec${ext}"
objcopy --compress-debug-sections "$bin" || true
mkdir "$dst"
mkdir -p "target/release"
cp "$bin" "target/release/" # workaround for cargo-deb silliness with targets
cp "$bin" "$dst/"
cp -r crates/cli/README.md LICENSE completions doc/{logo.svg,watchexec.1{,.*}} "$dst/"
- name: Archive (tar)
if: '! startsWith(matrix.name, ''windows-'')'
run: tar cavf "$dst.tar.xz" "$dst"
- name: Archive (deb)
if: startsWith(matrix.name, 'linux-')
run: cargo deb -p watchexec-cli --no-build --no-strip --target ${{ matrix.target }} --output "$dst.deb"
- name: Archive (rpm)
if: startsWith(matrix.name, 'linux-')
shell: bash
run: |
set -euxo pipefail
shopt -s globstar
cargo generate-rpm -p crates/cli --target "${{ matrix.target }}" --target-dir "target/${{ matrix.target }}"
mv target/**/*.rpm "$dst.rpm"
- name: Archive (zip)
if: startsWith(matrix.name, 'windows-')
shell: bash
run: 7z a "$dst.zip" "$dst"
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.name }}
retention-days: 1
path: |
watchexec-*.tar.xz
watchexec-*.tar.zst
watchexec-*.deb
watchexec-*.rpm
watchexec-*.zip
upload:
needs: [build, info]
name: Checksum and publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install b3sum
uses: taiki-e/install-action@v2
with:
tool: b3sum
- uses: actions/download-artifact@v4
with:
merge-multiple: true
- name: Dist manifest
run: |
jq -ncf .github/workflows/dist-manifest.jq \
--arg version "${{ needs.info.outputs.cli_version }}" \
--arg files "$(ls watchexec-*)" \
> dist-manifest.json
- name: Bulk checksums
run: |
b3sum watchexec-* | tee B3SUMS
sha512sum watchexec-* | tee SHA512SUMS
sha256sum watchexec-* | tee SHA256SUMS
- name: File checksums
run: |
for file in watchexec-*; do
b3sum --no-names $file > "$file.b3"
sha256sum $file | cut -d ' ' -f1 > "$file.sha256"
sha512sum $file | cut -d ' ' -f1 > "$file.sha512"
done
- uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564
with:
tag_name: v${{ needs.info.outputs.cli_version }}
name: CLI v${{ needs.info.outputs.cli_version }}
append_body: true
files: |
dist-manifest.json
watchexec-*.tar.xz
watchexec-*.tar.zst
watchexec-*.deb
watchexec-*.rpm
watchexec-*.zip
*SUMS
*.b3
*.sha*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,148 +0,0 @@
name: Test suite
on:
workflow_dispatch:
pull_request:
types:
- opened
- reopened
- synchronize
push:
branches:
- main
tags-ignore:
- "*"
env:
CARGO_TERM_COLOR: always
CARGO_UNSTABLE_SPARSE_REGISTRY: "true"
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
strategy:
fail-fast: false
matrix:
platform:
- macos
- ubuntu
- windows
name: Test ${{ matrix.platform }}
runs-on: "${{ matrix.platform }}-latest"
steps:
- uses: actions/checkout@v4
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
rustup default stable
# https://github.com/actions/cache/issues/752
- if: ${{ runner.os == 'Windows' }}
name: Use GNU tar
shell: cmd
run: |
echo "Adding GNU tar to PATH"
echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%"
- name: Cargo caching
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-stable-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-stable-
${{ runner.os }}-cargo-
- name: Compilation caching
uses: actions/cache@v4
with:
path: target/
key: ${{ runner.os }}-target-stable-${{ hashFiles('**/Cargo.lock') }}
- name: Run test suite
run: cargo test
- name: Run watchexec-events integration tests
run: cargo test -p watchexec-events -F serde
- name: Check that CLI runs
run: cargo run -p watchexec-cli -- -1 echo
- name: Install coreutils on mac
if: ${{ matrix.platform == 'macos' }}
run: brew install coreutils
- name: Run watchexec integration tests (unix)
if: ${{ matrix.platform != 'windows' }}
run: crates/cli/run-tests.sh
shell: bash
env:
WATCHEXEC_BIN: target/debug/watchexec
- name: Run bosion integration tests
run: ./run-tests.sh
working-directory: crates/bosion
shell: bash
- name: Generate manpage
run: cargo run -p watchexec-cli -- --manual > doc/watchexec.1
- name: Check that manpage is up to date
run: git diff --exit-code -- doc/
- name: Generate completions
run: bin/completions
- name: Check that completions are up to date
run: git diff --exit-code -- completions/
cross-checks:
name: Checks only against select targets
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
rustup default stable
sudo apt-get install -y musl-tools
rustup target add x86_64-unknown-linux-musl
- name: Install cross
uses: taiki-e/install-action@v2
with:
tool: cross
- name: Cargo caching
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-stable-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-stable-
${{ runner.os }}-cargo-
- run: cargo check --target x86_64-unknown-linux-musl
- run: cross check --target x86_64-unknown-freebsd
- run: cross check --target x86_64-unknown-netbsd
tests-pass:
if: always()
name: Tests pass
needs:
- test
- cross-checks
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}

2
.gitignore vendored
View file

@ -1,3 +1 @@
target target
/watchexec-*
watchexec.*.log

View file

@ -1 +0,0 @@
hard_tabs = true

40
.travis.yml Normal file
View file

@ -0,0 +1,40 @@
language: rust
cache: cargo
env:
global:
- PROJECT_NAME=watchexec
matrix:
include:
- os: linux
rust: stable
env: TARGET=x86_64-unknown-linux-gnu
- os: osx
rust: stable
env: TARGET=x86_64-apple-darwin
script:
- bash ci/script.sh
before_deploy:
- bash ci/before_deploy.sh
deploy:
provider: releases
api_key:
secure: UsikH6ulNyA6vHcbNP16evFpHW9A/ns8+sP2eXWRgW9lmp2Fjwi+xiZaQdqvcUU/yxhjysnCVAvMKzIHKaWofxNLn/8H7s1+f0KeEYW3bJLKyc06vtNX41PTqFZ7/jFEYOroyKrLVzWz4oqRUs+nHZcFUM/stUfyRJe4RCX8MZ1rQ7d7LWl1DazaxXIwJ/0h1q2ywxW9UMeP1xLaUL0Wk2pKuvQ0YXG3ahxZ4mcfBmj4fQaW6ByifioATbCinoQRqGyR+tKKTWQtk/7qcyluMsIWtHOiVf3q8VW6DcV4xqNJig0jdPQvP2iciK0ajl6yvVc8rZkgTtbg8EKMFcx4QI15W5fi9lHt10FN1dkj0NYgZE78zz/7He0P8arsOj88zIGGT2t2CQyIWCkHLQscCDgJl7aTYD0BX5BiU6VfDxIx0KnLKe/+YIZETIZTF8GhnUIIdY04I+gao8MrzhkbFBJvUQje9nzHu2NOUAongj0u9ZEvJyX3vVV52/yv9IJAXqxqSoW4Uj1eJ3vOlLLM6WDPrGDwQjpdhQUt3PShElge3kOEhL1sFOAt58i8rCqKgnBFBlrYpZxD1vXrHaWSExAULI17c5h7Zysg7F42BShR0cJn326pM6slxWsp/d8PB7/neQ85jlRy6WCf/yxY38arJ7UpQIvnxrZuHzzH4vI=
file_glob: true
file: ${PROJECT_NAME}-${TRAVIS_TAG}-${TARGET}.*
skip_cleanup: true
on:
tags: true
branches:
only:
- master
- "/^\\d+\\.\\d+\\.\\d+.*$/"
notifications:
email:
on_success: never

View file

@ -1,17 +0,0 @@
cff-version: 1.2.0
message: |
If you use this software, please cite it using these metadata.
title: "Watchexec: a tool to react to filesystem changes, and a crate ecosystem to power it"
version: "2.2.0"
date-released: 2024-10-14
repository-code: https://github.com/watchexec/watchexec
license: Apache-2.0
authors:
- family-names: Green
given-names: Matt
- family-names: Saparelli
given-names: Félix
orcid: https://orcid.org/0000-0002-2010-630X

View file

@ -1,122 +0,0 @@
# Contribution guidebook
This is a fairly free-form project, with low contribution traffic.
Maintainers:
- Félix Saparelli (@passcod) (active)
- Matt Green (@mattgreen) (original author, mostly checked out)
There are a few anti goals:
- Calling watchexec is to be a **simple** exercise that remains intuitive. As a specific point, it
should not involve any piping or require xargs.
- Watchexec will not be tied to any particular ecosystem or language. Projects that themselves use
watchexec (the library) can be focused on a particular domain (for example Cargo Watch for Rust),
but watchexec itself will remain generic, usable for any purpose.
## Debugging
To enable verbose logging in tests, run with:
```console
$ env RUST_LOG=watchexec=trace,info RUST_TEST_THREADS=1 RUST_NOCAPTURE=1 cargo test --test testfile -- testname
```
To use [Tokio Console](https://github.com/tokio-rs/console):
1. Add `--cfg tokio_unstable` to your `RUSTFLAGS`.
2. Run the CLI with the `dev-console` feature.
## PR etiquette
- Maintainers are busy or may not have the bandwidth, be patient.
- Do _not_ change the version number in the PR.
- Do _not_ change Cargo.toml or other project metadata, unless specifically asked for, or if that's
the point of the PR (like adding a crates.io category).
Apart from that, welcome and thank you for your time!
## Releasing
```
cargo release -p crate-name --execute patch # or minor, major
```
When a CLI release is done, the [release notes](https://github.com/watchexec/watchexec/releases) should be edited with the changelog.
### Release order
Use this command to see the tree of workspace dependencies:
```console
$ cargo tree -p watchexec-cli | rg -F '(/' --color=never | sed 's/ v[0-9].*//'
```
## Overview
The architecture of watchexec is roughly:
- sources gather events
- events are debounced and filtered
- event(s) make it through the debounce/filters and trigger an "action"
- `on_action` handler is called, returning an `Outcome`
- outcome is processed into managing the command that watchexec is running
- outcome can also be to exit
- when a command is started, the `on_pre_spawn` and `on_post_spawn` handlers are called
- commands are also a source of events, so e.g. "command has finished" is handled by `on_action`
And this is the startup sequence:
- init config sets basic immutable facts about the runtime
- runtime starts:
- source workers start, and are passed their runtime config
- action worker starts, and is passed its runtime config
- (unless `--postpone` is given) a synthetic event is injected to kickstart things
## Guides
These are generic guides for implementing specific bits of functionality.
### Adding an event source
- add a worker for "sourcing" events. Looking at the [signal source
worker](https://github.com/watchexec/watchexec/blob/main/crates/lib/src/signal/source.rs) is
probably easiest to get started here.
- because we may not always want to enable this event source, and just to be flexible, add [runtime
config](https://github.com/watchexec/watchexec/blob/main/crates/lib/src/config.rs) for the source.
- for convenience, probably add [a method on the runtime
config](https://github.com/watchexec/watchexec/blob/main/crates/lib/src/config.rs) which
configures the most common usecase.
- because watchexec is reconfigurable, in the worker you'll need to react to config changes. Look at
how the [fs worker does it](https://github.com/watchexec/watchexec/blob/main/crates/lib/src/fs.rs)
for reference.
- you may need to [add to the event tag
enum](https://github.com/watchexec/watchexec/blob/main/crates/lib/src/event.rs).
- if you do, you should [add support to the "tagged
filterer"](https://github.com/watchexec/watchexec/blob/main/crates/filterer/tagged/src/parse.rs),
but this can be done in follow-up work.
### Process a new event in the CLI
- add an option to the
[args](https://github.com/watchexec/watchexec/blob/main/crates/cli/src/args.rs) if necessary
- add to the [runtime
config](https://github.com/watchexec/watchexec/blob/main/crates/cli/src/config/runtime.rs) when
the option is present
- process relevant events [in the action
handler](https://github.com/watchexec/watchexec/blob/main/crates/cli/src/config/runtime.rs)
---
vim: tw=100

4608
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,43 +1,50 @@
[workspace] [package]
resolver = "2" name = "watchexec"
members = [ version = "1.8.5"
"crates/lib", authors = ["Matt Green <mattgreenrocks@gmail.com>"]
"crates/cli", description = "Executes commands in response to file modifications"
"crates/events", documentation = "https://github.com/mattgreen/watchexec"
"crates/signals", homepage = "https://github.com/mattgreen/watchexec"
"crates/supervisor", repository = "https://github.com/mattgreen/watchexec"
"crates/filterer/globset", readme = "README.md"
"crates/filterer/ignore", keywords = ["watcher", "inotify", "fsevents", "kqueue"]
"crates/bosion", categories = ["command-line-utilities"]
"crates/ignore-files", license = "Apache-2.0"
"crates/project-origins",
]
[workspace.dependencies] [profile.dev]
miette = "5.10.0" panic = "abort"
tempfile = "3.8.0"
tracing-test = "0.2.4"
rand = "0.8"
uuid = "1.5.0"
[profile.release] [profile.release]
lto = true lto = true
debug = 1 # for stack traces panic = "abort"
codegen-units = 1
strip = "symbols"
[profile.dev.build-override] [dependencies]
opt-level = 0 glob = "0.2.11"
codegen-units = 1024 globset = "0.2"
debug = false lazy_static = "0.2.8"
debug-assertions = false log = "0.3.8"
overflow-checks = false notify = "4.0"
incremental = false
[profile.release.build-override] [dev-dependencies]
opt-level = 0 mktemp = "0.3.1"
codegen-units = 1024
debug = false [dependencies.clap]
debug-assertions = false version = "~2.26"
overflow-checks = false default-features = false
incremental = false features = ["wrap_help"]
[dependencies.env_logger]
version = "0.4"
default-features = false
features = []
[target.'cfg(unix)'.dependencies]
nix = "0.9"
[target.'cfg(windows)'.dependencies]
winapi = "0.2.8"
kernel32-sys = "0.2.2"
[[bin]]
name = "watchexec"
doc = false

30
Makefile Normal file
View file

@ -0,0 +1,30 @@
LATEST_TAG=$(shell git tag | tail -n1)
.PHONY: doc test
debug: src/* Cargo.toml
@cargo build
release: src/* Cargo.toml
@cargo build --release
clean:
@cargo clean
test:
@cargo test
doc: doc/watchexec.1.ronn
@ronn doc/watchexec.1.ronn
cargo-release:
@cargo publish
homebrew-release:
@brew bump-formula-pr --strict --url="https://github.com/mattgreen/watchexec/archive/$(LATEST_TAG).tar.gz" watchexec
install: release
@cp target/release/watchexec /usr/bin
uninstall:
@rm /usr/bin/watchexec

114
README.md
View file

@ -1,84 +1,100 @@
[![CI status on main branch](https://github.com/watchexec/watchexec/actions/workflows/tests.yml/badge.svg)](https://github.com/watchexec/watchexec/actions/workflows/tests.yml) # watchexec
# Watchexec [![Build Status](https://travis-ci.org/mattgreen/watchexec.svg?branch=master)](https://travis-ci.org/mattgreen/watchexec)
[![Build status](https://ci.appveyor.com/api/projects/status/ivxu31g4rcf4740t?svg=true)](https://ci.appveyor.com/project/mattgreen/watchexec)
[![Crates.io status](https://img.shields.io/crates/v/watchexec.svg)](https://crates.io/crates/watchexec)
Software development often involves running the same commands over and over. Boring! Software development often involves running the same commands over and over. Boring!
`watchexec` is a simple, standalone tool that watches a path and runs a command whenever it detects modifications. `watchexec` is a **simple**, standalone tool that watches a path and runs a command whenever it detects modifications.
Example use cases: Example use cases:
* Automatically run unit tests * Automatically run unit tests
* Run linters/syntax checkers * Run linters/syntax checkers
* Rebuild artifacts
## Features ## Features
* Simple invocation and use, does not require a cryptic command line involving `xargs` * Simple invocation and use
* Runs on OS X, Linux, and Windows * Runs on OS X, Linux and Windows
* Monitors current directory and all subdirectories for changes * Monitors current directory and all subdirectories for changes
* Uses most efficient event polling mechanism for your platform (except for [BSD](https://github.com/passcod/rsnotify#todo))
* Coalesces multiple filesystem events into one, for editors that use swap/backup files during saving * Coalesces multiple filesystem events into one, for editors that use swap/backup files during saving
* Loads `.gitignore` and `.ignore` files * By default, uses `.gitignore` to determine which files to ignore notifications for
* Uses process groups to keep hold of forking programs * Support for watching files with a specific extension
* Provides the paths that changed in environment variables or STDIN * Support for filtering/ignoring events based on glob patterns
* Does not require a language runtime, not tied to any particular language or ecosystem * Launches child processes in a new process group
* [And more!](./crates/cli/#features) * Sets the following environment variables in the child process:
* `$WATCHEXEC_UPDATED_PATH` the path of the first file that triggered a change
* `$WATCHEXEC_COMMON_PATH` the longest common path of all of the files that triggered a change
* Optionally clears screen between executions
* Optionally restarts the command with every modification (good for servers)
* Does not require a language runtime
## Anti-Features
## Quick start * Not tied to any particular language or ecosystem
* Does not require a cryptic command line involving `xargs`
Watch all JavaScript, CSS and HTML files in the current directory and all subdirectories for changes, running `npm run build` when a change is detected: ## Usage Examples
$ watchexec -e js,css,html npm run build Watch all JavaScript, CSS and HTML files in the current directory and all subdirectories for changes, running `make` when a change is detected:
$ watchexec --exts js,css,html make
Call `make test` when any file changes in this directory/subdirectory, except for everything below `target`:
$ watchexec -i target make test
Call `ls -la` when any file changes in this directory/subdirectory:
$ watchexec -- ls -la
Call/restart `python server.py` when any Python file in the current directory (and all subdirectories) changes: Call/restart `python server.py` when any Python file in the current directory (and all subdirectories) changes:
$ watchexec -r -e py -- python server.py $ watchexec -e py -r python server.py
More usage examples: [in the CLI README](./crates/cli/#usage-examples)! Call/restart `my_server` when any file in the current directory (and all subdirectories) changes, sending `SIGKILL` to stop the child process:
## Install $ watchexec -r -s SIGKILL my_server
<a href="https://repology.org/project/watchexec/versions"><img align="right" src="https://repology.org/badge/vertical-allrepos/watchexec.svg" alt="Packaging status"></a> Send a SIGHUP to the child process upon changes (Note: with using `-n | --no-shell` here, we're executing `my_server` directly, instead of wrapping it in a shell:
- With [your package manager](./doc/packages.md) for Arch, Debian, Homebrew, Nix, Scoop, Chocolatey… $ watchexec -n -s SIGHUP my_server
- From binary with [Binstall](https://github.com/cargo-bins/cargo-binstall): `cargo binstall watchexec-cli`
- As [pre-built binary package from Github](https://github.com/watchexec/watchexec/releases/latest)
- From source with Cargo: `cargo install --locked watchexec-cli`
All options in detail: [in the CLI README](./crates/cli/#installation), Run `make` when any file changes, using the `.gitignore` file in the current directory to filter:
in the online help (`watchexec -h`, `watchexec --help`, or `watchexec --manual`),
and [in the manual page](./doc/watchexec.1.md).
$ watchexec make
## Augment Run `make` when any file in `lib` or `src` changes:
Watchexec pairs well with: $ watchexec -w lib -w src make
- [checkexec](https://github.com/kurtbuilds/checkexec): to run only when source files are newer than a target file ## Installation
- [just](https://github.com/casey/just): a modern alternative to `make`
- [systemfd](https://github.com/mitsuhiko/systemfd): socket-passing in development
## Extend ### Cargo
- [watchexec library](./crates/lib/): to create more specialised watchexec-powered tools. watchexec requires Rust 1.16 or later. You can install it using cargo:
- [watchexec-events](./crates/events/): event types for watchexec.
- [watchexec-signals](./crates/signals/): signal types for watchexec.
- [watchexec-supervisor](./crates/supervisor/): process lifecycle manager (the _exec_ part of watchexec).
- [clearscreen](https://github.com/watchexec/clearscreen): to clear the (terminal) screen on every platform.
- [command group](https://github.com/watchexec/command-group): to run commands in process groups.
- [ignore files](./crates/ignore-files/): to find, parse, and interpret ignore files.
- [project origins](./crates/project-origins/): to find the origin(s) directory of a project.
- [notify](https://github.com/notify-rs/notify): to respond to file modifications (third-party).
### Downstreams $ cargo install watchexec
Selected downstreams of watchexec and associated crates: ### OS X with Homebrew
- [cargo watch](https://github.com/watchexec/cargo-watch): a specialised watcher for Rust/Cargo projects. $ brew install watchexec
- [cargo lambda](https://github.com/cargo-lambda/cargo-lambda): a dev tool for Rust-powered AWS Lambda functions.
- [create-rust-app](https://create-rust-app.dev): a template for Rust+React web apps. ### Linux
- [dotter](https://github.com/supercuber/dotter): a dotfile manager.
- [ghciwatch](https://github.com/mercurytechnologies/ghciwatch): a specialised watcher for Haskell projects. For now, use the GitHub Releases tab to obtain the binary. PRs for packaging in various distros are welcomed.
- [tectonic](https://tectonic-typesetting.github.io/book/latest/): a TeX/LaTeX typesetting system.
### Windows
Use the GitHub Releases tab to obtain the binary. In the future, I'll look at adding it to Chocolatey.
## Building
Rust 1.16 or later is required.
## Credits
* [notify](https://github.com/passcod/rsnotify) for doing most of the heavy-lifting
* [globset](https://crates.io/crates/globset) for super-fast glob matching

45
appveyor.yml Normal file
View file

@ -0,0 +1,45 @@
environment:
global:
PROJECT_NAME: watchexec
matrix:
- TARGET: x86_64-pc-windows-gnu
CHANNEL: stable
# Install Rust and Cargo
# (Based on from https://github.com/rust-lang/libc/blob/master/appveyor.yml)
install:
- curl -sSf -o rustup-init.exe https://win.rustup.rs/
- rustup-init.exe --default-host %TARGET% --default-toolchain %CHANNEL% -y
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
- if defined MSYS2_BITS set PATH=%PATH%;C:\msys64\mingw%MSYS2_BITS%\bin
- rustc -V
- cargo -V
# ???
build: false
# Equivalent to Travis' `script` phase
test_script:
- cargo test --verbose
before_deploy:
# Generate artifacts for release
- cargo build --release
- mkdir staging
- copy target\release\watchexec.exe staging
- cd staging
- 7z a ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip *
- appveyor PushArtifact ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip
deploy:
description: 'Automatically deployed release'
artifact: /.*\.zip/
auth_token:
secure: h1ufwX1COhKiHGgBY22L37kd1kPCTEpkiGlw4mx/7Ib5nS3SlxRvkuKwteh5sJkh
provider: GitHub
on:
appveyor_repo_tag: true
branches:
only:
- master

View file

@ -1,7 +0,0 @@
#!/bin/sh
cargo run -p watchexec-cli $* -- --completions bash > completions/bash
cargo run -p watchexec-cli $* -- --completions elvish > completions/elvish
cargo run -p watchexec-cli $* -- --completions fish > completions/fish
cargo run -p watchexec-cli $* -- --completions nu > completions/nu
cargo run -p watchexec-cli $* -- --completions powershell > completions/powershell
cargo run -p watchexec-cli $* -- --completions zsh > completions/zsh

View file

@ -1,10 +0,0 @@
#!/usr/bin/env node
const id = Math.floor(Math.random() * 100);
let n = 0;
const m = 5;
while (n < m) {
n += 1;
console.log(`[${id} : ${n}/${m}] ${new Date}`);
await new Promise(done => setTimeout(done, 2000));
}

View file

@ -1,3 +0,0 @@
#!/bin/sh
cargo run -p watchexec-cli -- --manual > doc/watchexec.1
pandoc doc/watchexec.1 -t markdown > doc/watchexec.1.md

15
ci/before_deploy.sh Normal file
View file

@ -0,0 +1,15 @@
# Build script shamelessly stolen from ripgrep :)
cargo build --target $TARGET --release
build_dir=$(mktemp -d 2>/dev/null || mktemp -d -t tmp)
out_dir=$(pwd)
name="${PROJECT_NAME}-${TRAVIS_TAG}-${TARGET}"
mkdir "$build_dir/$name"
cp target/$TARGET/release/watchexec "$build_dir/$name/"
cp {doc/watchexec.1,LICENSE} "$build_dir/$name/"
pushd $build_dir
tar czf "$out_dir/$name.tar.gz" *
popd

3
ci/script.sh Normal file
View file

@ -0,0 +1,3 @@
cargo clean --target $TARGET --verbose
cargo build --target $TARGET --verbose
cargo test --target $TARGET --verbose

View file

@ -1,230 +0,0 @@
_watchexec() {
local i cur prev opts cmd
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
cmd=""
opts=""
for i in ${COMP_WORDS[@]}
do
case "${cmd},${i}" in
",$1")
cmd="watchexec"
;;
*)
;;
esac
done
case "${cmd}" in
watchexec)
opts="-w -W -F -c -o -r -s -d -p -n -E -1 -N -q -e -f -j -i -v -h -V --watch --watch-non-recursive --watch-file --clear --on-busy-update --restart --signal --stop-signal --stop-timeout --map-signal --debounce --stdin-quit --no-vcs-ignore --no-project-ignore --no-global-ignore --no-default-ignore --no-discover-ignore --ignore-nothing --postpone --delay-run --poll --shell --no-environment --emit-events-to --only-emit-events --env --no-process-group --wrap-process --notify --color --timings --quiet --bell --project-origin --workdir --exts --filter --filter-file --filter-prog --ignore --ignore-file --fs-events --no-meta --print-events --manual --completions --verbose --log-file --help --version [COMMAND]..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--watch)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-w)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--watch-non-recursive)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-W)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--watch-file)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-F)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--clear)
COMPREPLY=($(compgen -W "clear reset" -- "${cur}"))
return 0
;;
-c)
COMPREPLY=($(compgen -W "clear reset" -- "${cur}"))
return 0
;;
--on-busy-update)
COMPREPLY=($(compgen -W "queue do-nothing restart signal" -- "${cur}"))
return 0
;;
-o)
COMPREPLY=($(compgen -W "queue do-nothing restart signal" -- "${cur}"))
return 0
;;
--signal)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-s)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--stop-signal)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--stop-timeout)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--map-signal)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--debounce)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-d)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--delay-run)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--poll)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--shell)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--emit-events-to)
COMPREPLY=($(compgen -W "environment stdio file json-stdio json-file none" -- "${cur}"))
return 0
;;
--env)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-E)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--wrap-process)
COMPREPLY=($(compgen -W "group session none" -- "${cur}"))
return 0
;;
--color)
COMPREPLY=($(compgen -W "auto always never" -- "${cur}"))
return 0
;;
--project-origin)
COMPREPLY=()
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o plusdirs
fi
return 0
;;
--workdir)
COMPREPLY=()
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o plusdirs
fi
return 0
;;
--exts)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-e)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--filter)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-f)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--filter-file)
local oldifs
if [ -n "${IFS+x}" ]; then
oldifs="$IFS"
fi
IFS=$'\n'
COMPREPLY=($(compgen -f "${cur}"))
if [ -n "${oldifs+x}" ]; then
IFS="$oldifs"
fi
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o filenames
fi
return 0
;;
--filter-prog)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-j)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--ignore)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-i)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--ignore-file)
local oldifs
if [ -n "${IFS+x}" ]; then
oldifs="$IFS"
fi
IFS=$'\n'
COMPREPLY=($(compgen -f "${cur}"))
if [ -n "${oldifs+x}" ]; then
IFS="$oldifs"
fi
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o filenames
fi
return 0
;;
--fs-events)
COMPREPLY=($(compgen -W "access create remove rename modify metadata" -- "${cur}"))
return 0
;;
--completions)
COMPREPLY=($(compgen -W "bash elvish fish nu powershell zsh" -- "${cur}"))
return 0
;;
--log-file)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
esac
}
if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
complete -F _watchexec -o nosort -o bashdefault -o default watchexec
else
complete -F _watchexec -o bashdefault -o default watchexec
fi

View file

@ -1,95 +0,0 @@
use builtin;
use str;
set edit:completion:arg-completer[watchexec] = {|@words|
fn spaces {|n|
builtin:repeat $n ' ' | str:join ''
}
fn cand {|text desc|
edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc
}
var command = 'watchexec'
for word $words[1..-1] {
if (str:has-prefix $word '-') {
break
}
set command = $command';'$word
}
var completions = [
&'watchexec'= {
cand -w 'Watch a specific file or directory'
cand --watch 'Watch a specific file or directory'
cand -W 'Watch a specific directory, non-recursively'
cand --watch-non-recursive 'Watch a specific directory, non-recursively'
cand -F 'Watch files and directories from a file'
cand --watch-file 'Watch files and directories from a file'
cand -c 'Clear screen before running command'
cand --clear 'Clear screen before running command'
cand -o 'What to do when receiving events while the command is running'
cand --on-busy-update 'What to do when receiving events while the command is running'
cand -s 'Send a signal to the process when it''s still running'
cand --signal 'Send a signal to the process when it''s still running'
cand --stop-signal 'Signal to send to stop the command'
cand --stop-timeout 'Time to wait for the command to exit gracefully'
cand --map-signal 'Translate signals from the OS to signals to send to the command'
cand -d 'Time to wait for new events before taking action'
cand --debounce 'Time to wait for new events before taking action'
cand --delay-run 'Sleep before running the command'
cand --poll 'Poll for filesystem changes'
cand --shell 'Use a different shell'
cand --emit-events-to 'Configure event emission'
cand -E 'Add env vars to the command'
cand --env 'Add env vars to the command'
cand --wrap-process 'Configure how the process is wrapped'
cand --color 'When to use terminal colours'
cand --project-origin 'Set the project origin'
cand --workdir 'Set the working directory'
cand -e 'Filename extensions to filter to'
cand --exts 'Filename extensions to filter to'
cand -f 'Filename patterns to filter to'
cand --filter 'Filename patterns to filter to'
cand --filter-file 'Files to load filters from'
cand -j '[experimental] Filter programs'
cand --filter-prog '[experimental] Filter programs'
cand -i 'Filename patterns to filter out'
cand --ignore 'Filename patterns to filter out'
cand --ignore-file 'Files to load ignores from'
cand --fs-events 'Filesystem events to filter to'
cand --completions 'Generate a shell completions script'
cand --log-file 'Write diagnostic logs to a file'
cand -r 'Restart the process if it''s still running'
cand --restart 'Restart the process if it''s still running'
cand --stdin-quit 'Exit when stdin closes'
cand --no-vcs-ignore 'Don''t load gitignores'
cand --no-project-ignore 'Don''t load project-local ignores'
cand --no-global-ignore 'Don''t load global ignores'
cand --no-default-ignore 'Don''t use internal default ignores'
cand --no-discover-ignore 'Don''t discover ignore files at all'
cand --ignore-nothing 'Don''t ignore anything at all'
cand -p 'Wait until first change before running command'
cand --postpone 'Wait until first change before running command'
cand -n 'Shorthand for ''--shell=none'''
cand --no-environment 'Deprecated shorthand for ''--emit-events=none'''
cand --only-emit-events 'Only emit events to stdout, run no commands'
cand --no-process-group 'Don''t use a process group'
cand -1 'Testing only: exit Watchexec after the first run'
cand -N 'Alert when commands start and end'
cand --notify 'Alert when commands start and end'
cand --timings 'Print how long the command took to run'
cand -q 'Don''t print starting and stopping messages'
cand --quiet 'Don''t print starting and stopping messages'
cand --bell 'Ring the terminal bell on command completion'
cand --no-meta 'Don''t emit fs events for metadata changes'
cand --print-events 'Print events that trigger actions'
cand --manual 'Show the manual page'
cand -v 'Set diagnostic log level'
cand --verbose 'Set diagnostic log level'
cand -h 'Print help (see more with ''--help'')'
cand --help 'Print help (see more with ''--help'')'
cand -V 'Print version'
cand --version 'Print version'
}
]
$completions[$command]
}

View file

@ -1,52 +0,0 @@
complete -c watchexec -s w -l watch -d 'Watch a specific file or directory' -r -F
complete -c watchexec -s W -l watch-non-recursive -d 'Watch a specific directory, non-recursively' -r -F
complete -c watchexec -s F -l watch-file -d 'Watch files and directories from a file' -r -F
complete -c watchexec -s c -l clear -d 'Clear screen before running command' -r -f -a "{clear\t'',reset\t''}"
complete -c watchexec -s o -l on-busy-update -d 'What to do when receiving events while the command is running' -r -f -a "{queue\t'',do-nothing\t'',restart\t'',signal\t''}"
complete -c watchexec -s s -l signal -d 'Send a signal to the process when it\'s still running' -r
complete -c watchexec -l stop-signal -d 'Signal to send to stop the command' -r
complete -c watchexec -l stop-timeout -d 'Time to wait for the command to exit gracefully' -r
complete -c watchexec -l map-signal -d 'Translate signals from the OS to signals to send to the command' -r
complete -c watchexec -s d -l debounce -d 'Time to wait for new events before taking action' -r
complete -c watchexec -l delay-run -d 'Sleep before running the command' -r
complete -c watchexec -l poll -d 'Poll for filesystem changes' -r
complete -c watchexec -l shell -d 'Use a different shell' -r
complete -c watchexec -l emit-events-to -d 'Configure event emission' -r -f -a "{environment\t'',stdio\t'',file\t'',json-stdio\t'',json-file\t'',none\t''}"
complete -c watchexec -s E -l env -d 'Add env vars to the command' -r
complete -c watchexec -l wrap-process -d 'Configure how the process is wrapped' -r -f -a "{group\t'',session\t'',none\t''}"
complete -c watchexec -l color -d 'When to use terminal colours' -r -f -a "{auto\t'',always\t'',never\t''}"
complete -c watchexec -l project-origin -d 'Set the project origin' -r -f -a "(__fish_complete_directories)"
complete -c watchexec -l workdir -d 'Set the working directory' -r -f -a "(__fish_complete_directories)"
complete -c watchexec -s e -l exts -d 'Filename extensions to filter to' -r
complete -c watchexec -s f -l filter -d 'Filename patterns to filter to' -r
complete -c watchexec -l filter-file -d 'Files to load filters from' -r -F
complete -c watchexec -s j -l filter-prog -d '[experimental] Filter programs' -r
complete -c watchexec -s i -l ignore -d 'Filename patterns to filter out' -r
complete -c watchexec -l ignore-file -d 'Files to load ignores from' -r -F
complete -c watchexec -l fs-events -d 'Filesystem events to filter to' -r -f -a "{access\t'',create\t'',remove\t'',rename\t'',modify\t'',metadata\t''}"
complete -c watchexec -l completions -d 'Generate a shell completions script' -r -f -a "{bash\t'',elvish\t'',fish\t'',nu\t'',powershell\t'',zsh\t''}"
complete -c watchexec -l log-file -d 'Write diagnostic logs to a file' -r -F
complete -c watchexec -s r -l restart -d 'Restart the process if it\'s still running'
complete -c watchexec -l stdin-quit -d 'Exit when stdin closes'
complete -c watchexec -l no-vcs-ignore -d 'Don\'t load gitignores'
complete -c watchexec -l no-project-ignore -d 'Don\'t load project-local ignores'
complete -c watchexec -l no-global-ignore -d 'Don\'t load global ignores'
complete -c watchexec -l no-default-ignore -d 'Don\'t use internal default ignores'
complete -c watchexec -l no-discover-ignore -d 'Don\'t discover ignore files at all'
complete -c watchexec -l ignore-nothing -d 'Don\'t ignore anything at all'
complete -c watchexec -s p -l postpone -d 'Wait until first change before running command'
complete -c watchexec -s n -d 'Shorthand for \'--shell=none\''
complete -c watchexec -l no-environment -d 'Deprecated shorthand for \'--emit-events=none\''
complete -c watchexec -l only-emit-events -d 'Only emit events to stdout, run no commands'
complete -c watchexec -l no-process-group -d 'Don\'t use a process group'
complete -c watchexec -s 1 -d 'Testing only: exit Watchexec after the first run'
complete -c watchexec -s N -l notify -d 'Alert when commands start and end'
complete -c watchexec -l timings -d 'Print how long the command took to run'
complete -c watchexec -s q -l quiet -d 'Don\'t print starting and stopping messages'
complete -c watchexec -l bell -d 'Ring the terminal bell on command completion'
complete -c watchexec -l no-meta -d 'Don\'t emit fs events for metadata changes'
complete -c watchexec -l print-events -d 'Print events that trigger actions'
complete -c watchexec -l manual -d 'Show the manual page'
complete -c watchexec -s v -l verbose -d 'Set diagnostic log level'
complete -c watchexec -s h -l help -d 'Print help (see more with \'--help\')'
complete -c watchexec -s V -l version -d 'Print version'

View file

@ -1,90 +0,0 @@
module completions {
def "nu-complete watchexec screen_clear" [] {
[ "clear" "reset" ]
}
def "nu-complete watchexec on_busy_update" [] {
[ "queue" "do-nothing" "restart" "signal" ]
}
def "nu-complete watchexec emit_events_to" [] {
[ "environment" "stdio" "file" "json-stdio" "json-file" "none" ]
}
def "nu-complete watchexec wrap_process" [] {
[ "group" "session" "none" ]
}
def "nu-complete watchexec color" [] {
[ "auto" "always" "never" ]
}
def "nu-complete watchexec filter_fs_events" [] {
[ "access" "create" "remove" "rename" "modify" "metadata" ]
}
def "nu-complete watchexec completions" [] {
[ "bash" "elvish" "fish" "nu" "powershell" "zsh" ]
}
# Execute commands when watched files change
export extern watchexec [
...command: string # Command to run on changes
--watch(-w): string # Watch a specific file or directory
--watch-non-recursive(-W): string # Watch a specific directory, non-recursively
--watch-file(-F): string # Watch files and directories from a file
--clear(-c): string@"nu-complete watchexec screen_clear" # Clear screen before running command
--on-busy-update(-o): string@"nu-complete watchexec on_busy_update" # What to do when receiving events while the command is running
--restart(-r) # Restart the process if it's still running
--signal(-s): string # Send a signal to the process when it's still running
--stop-signal: string # Signal to send to stop the command
--stop-timeout: string # Time to wait for the command to exit gracefully
--map-signal: string # Translate signals from the OS to signals to send to the command
--debounce(-d): string # Time to wait for new events before taking action
--stdin-quit # Exit when stdin closes
--no-vcs-ignore # Don't load gitignores
--no-project-ignore # Don't load project-local ignores
--no-global-ignore # Don't load global ignores
--no-default-ignore # Don't use internal default ignores
--no-discover-ignore # Don't discover ignore files at all
--ignore-nothing # Don't ignore anything at all
--postpone(-p) # Wait until first change before running command
--delay-run: string # Sleep before running the command
--poll: string # Poll for filesystem changes
--shell: string # Use a different shell
-n # Shorthand for '--shell=none'
--no-environment # Deprecated shorthand for '--emit-events=none'
--emit-events-to: string@"nu-complete watchexec emit_events_to" # Configure event emission
--only-emit-events # Only emit events to stdout, run no commands
--env(-E): string # Add env vars to the command
--no-process-group # Don't use a process group
--wrap-process: string@"nu-complete watchexec wrap_process" # Configure how the process is wrapped
-1 # Testing only: exit Watchexec after the first run
--notify(-N) # Alert when commands start and end
--color: string@"nu-complete watchexec color" # When to use terminal colours
--timings # Print how long the command took to run
--quiet(-q) # Don't print starting and stopping messages
--bell # Ring the terminal bell on command completion
--project-origin: string # Set the project origin
--workdir: string # Set the working directory
--exts(-e): string # Filename extensions to filter to
--filter(-f): string # Filename patterns to filter to
--filter-file: string # Files to load filters from
--filter-prog(-j): string # [experimental] Filter programs
--ignore(-i): string # Filename patterns to filter out
--ignore-file: string # Files to load ignores from
--fs-events: string@"nu-complete watchexec filter_fs_events" # Filesystem events to filter to
--no-meta # Don't emit fs events for metadata changes
--print-events # Print events that trigger actions
--manual # Show the manual page
--completions: string@"nu-complete watchexec completions" # Generate a shell completions script
--verbose(-v) # Set diagnostic log level
--log-file: string # Write diagnostic logs to a file
--help(-h) # Print help (see more with '--help')
--version(-V) # Print version
]
}
export use completions *

View file

@ -1,101 +0,0 @@
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
Register-ArgumentCompleter -Native -CommandName 'watchexec' -ScriptBlock {
param($wordToComplete, $commandAst, $cursorPosition)
$commandElements = $commandAst.CommandElements
$command = @(
'watchexec'
for ($i = 1; $i -lt $commandElements.Count; $i++) {
$element = $commandElements[$i]
if ($element -isnot [StringConstantExpressionAst] -or
$element.StringConstantType -ne [StringConstantType]::BareWord -or
$element.Value.StartsWith('-') -or
$element.Value -eq $wordToComplete) {
break
}
$element.Value
}) -join ';'
$completions = @(switch ($command) {
'watchexec' {
[CompletionResult]::new('-w', '-w', [CompletionResultType]::ParameterName, 'Watch a specific file or directory')
[CompletionResult]::new('--watch', '--watch', [CompletionResultType]::ParameterName, 'Watch a specific file or directory')
[CompletionResult]::new('-W', '-W ', [CompletionResultType]::ParameterName, 'Watch a specific directory, non-recursively')
[CompletionResult]::new('--watch-non-recursive', '--watch-non-recursive', [CompletionResultType]::ParameterName, 'Watch a specific directory, non-recursively')
[CompletionResult]::new('-F', '-F ', [CompletionResultType]::ParameterName, 'Watch files and directories from a file')
[CompletionResult]::new('--watch-file', '--watch-file', [CompletionResultType]::ParameterName, 'Watch files and directories from a file')
[CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'Clear screen before running command')
[CompletionResult]::new('--clear', '--clear', [CompletionResultType]::ParameterName, 'Clear screen before running command')
[CompletionResult]::new('-o', '-o', [CompletionResultType]::ParameterName, 'What to do when receiving events while the command is running')
[CompletionResult]::new('--on-busy-update', '--on-busy-update', [CompletionResultType]::ParameterName, 'What to do when receiving events while the command is running')
[CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'Send a signal to the process when it''s still running')
[CompletionResult]::new('--signal', '--signal', [CompletionResultType]::ParameterName, 'Send a signal to the process when it''s still running')
[CompletionResult]::new('--stop-signal', '--stop-signal', [CompletionResultType]::ParameterName, 'Signal to send to stop the command')
[CompletionResult]::new('--stop-timeout', '--stop-timeout', [CompletionResultType]::ParameterName, 'Time to wait for the command to exit gracefully')
[CompletionResult]::new('--map-signal', '--map-signal', [CompletionResultType]::ParameterName, 'Translate signals from the OS to signals to send to the command')
[CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'Time to wait for new events before taking action')
[CompletionResult]::new('--debounce', '--debounce', [CompletionResultType]::ParameterName, 'Time to wait for new events before taking action')
[CompletionResult]::new('--delay-run', '--delay-run', [CompletionResultType]::ParameterName, 'Sleep before running the command')
[CompletionResult]::new('--poll', '--poll', [CompletionResultType]::ParameterName, 'Poll for filesystem changes')
[CompletionResult]::new('--shell', '--shell', [CompletionResultType]::ParameterName, 'Use a different shell')
[CompletionResult]::new('--emit-events-to', '--emit-events-to', [CompletionResultType]::ParameterName, 'Configure event emission')
[CompletionResult]::new('-E', '-E ', [CompletionResultType]::ParameterName, 'Add env vars to the command')
[CompletionResult]::new('--env', '--env', [CompletionResultType]::ParameterName, 'Add env vars to the command')
[CompletionResult]::new('--wrap-process', '--wrap-process', [CompletionResultType]::ParameterName, 'Configure how the process is wrapped')
[CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to use terminal colours')
[CompletionResult]::new('--project-origin', '--project-origin', [CompletionResultType]::ParameterName, 'Set the project origin')
[CompletionResult]::new('--workdir', '--workdir', [CompletionResultType]::ParameterName, 'Set the working directory')
[CompletionResult]::new('-e', '-e', [CompletionResultType]::ParameterName, 'Filename extensions to filter to')
[CompletionResult]::new('--exts', '--exts', [CompletionResultType]::ParameterName, 'Filename extensions to filter to')
[CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'Filename patterns to filter to')
[CompletionResult]::new('--filter', '--filter', [CompletionResultType]::ParameterName, 'Filename patterns to filter to')
[CompletionResult]::new('--filter-file', '--filter-file', [CompletionResultType]::ParameterName, 'Files to load filters from')
[CompletionResult]::new('-j', '-j', [CompletionResultType]::ParameterName, '[experimental] Filter programs')
[CompletionResult]::new('--filter-prog', '--filter-prog', [CompletionResultType]::ParameterName, '[experimental] Filter programs')
[CompletionResult]::new('-i', '-i', [CompletionResultType]::ParameterName, 'Filename patterns to filter out')
[CompletionResult]::new('--ignore', '--ignore', [CompletionResultType]::ParameterName, 'Filename patterns to filter out')
[CompletionResult]::new('--ignore-file', '--ignore-file', [CompletionResultType]::ParameterName, 'Files to load ignores from')
[CompletionResult]::new('--fs-events', '--fs-events', [CompletionResultType]::ParameterName, 'Filesystem events to filter to')
[CompletionResult]::new('--completions', '--completions', [CompletionResultType]::ParameterName, 'Generate a shell completions script')
[CompletionResult]::new('--log-file', '--log-file', [CompletionResultType]::ParameterName, 'Write diagnostic logs to a file')
[CompletionResult]::new('-r', '-r', [CompletionResultType]::ParameterName, 'Restart the process if it''s still running')
[CompletionResult]::new('--restart', '--restart', [CompletionResultType]::ParameterName, 'Restart the process if it''s still running')
[CompletionResult]::new('--stdin-quit', '--stdin-quit', [CompletionResultType]::ParameterName, 'Exit when stdin closes')
[CompletionResult]::new('--no-vcs-ignore', '--no-vcs-ignore', [CompletionResultType]::ParameterName, 'Don''t load gitignores')
[CompletionResult]::new('--no-project-ignore', '--no-project-ignore', [CompletionResultType]::ParameterName, 'Don''t load project-local ignores')
[CompletionResult]::new('--no-global-ignore', '--no-global-ignore', [CompletionResultType]::ParameterName, 'Don''t load global ignores')
[CompletionResult]::new('--no-default-ignore', '--no-default-ignore', [CompletionResultType]::ParameterName, 'Don''t use internal default ignores')
[CompletionResult]::new('--no-discover-ignore', '--no-discover-ignore', [CompletionResultType]::ParameterName, 'Don''t discover ignore files at all')
[CompletionResult]::new('--ignore-nothing', '--ignore-nothing', [CompletionResultType]::ParameterName, 'Don''t ignore anything at all')
[CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Wait until first change before running command')
[CompletionResult]::new('--postpone', '--postpone', [CompletionResultType]::ParameterName, 'Wait until first change before running command')
[CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'Shorthand for ''--shell=none''')
[CompletionResult]::new('--no-environment', '--no-environment', [CompletionResultType]::ParameterName, 'Deprecated shorthand for ''--emit-events=none''')
[CompletionResult]::new('--only-emit-events', '--only-emit-events', [CompletionResultType]::ParameterName, 'Only emit events to stdout, run no commands')
[CompletionResult]::new('--no-process-group', '--no-process-group', [CompletionResultType]::ParameterName, 'Don''t use a process group')
[CompletionResult]::new('-1', '-1', [CompletionResultType]::ParameterName, 'Testing only: exit Watchexec after the first run')
[CompletionResult]::new('-N', '-N ', [CompletionResultType]::ParameterName, 'Alert when commands start and end')
[CompletionResult]::new('--notify', '--notify', [CompletionResultType]::ParameterName, 'Alert when commands start and end')
[CompletionResult]::new('--timings', '--timings', [CompletionResultType]::ParameterName, 'Print how long the command took to run')
[CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Don''t print starting and stopping messages')
[CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Don''t print starting and stopping messages')
[CompletionResult]::new('--bell', '--bell', [CompletionResultType]::ParameterName, 'Ring the terminal bell on command completion')
[CompletionResult]::new('--no-meta', '--no-meta', [CompletionResultType]::ParameterName, 'Don''t emit fs events for metadata changes')
[CompletionResult]::new('--print-events', '--print-events', [CompletionResultType]::ParameterName, 'Print events that trigger actions')
[CompletionResult]::new('--manual', '--manual', [CompletionResultType]::ParameterName, 'Show the manual page')
[CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Set diagnostic log level')
[CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Set diagnostic log level')
[CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
[CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
[CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version')
break
}
})
$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
Sort-Object -Property ListItemText
}

View file

@ -1,103 +0,0 @@
#compdef watchexec
autoload -U is-at-least
_watchexec() {
typeset -A opt_args
typeset -a _arguments_options
local ret=1
if is-at-least 5.2; then
_arguments_options=(-s -S -C)
else
_arguments_options=(-s -C)
fi
local context curcontext="$curcontext" state line
_arguments "${_arguments_options[@]}" : \
'*-w+[Watch a specific file or directory]:PATH:_files' \
'*--watch=[Watch a specific file or directory]:PATH:_files' \
'*-W+[Watch a specific directory, non-recursively]:PATH:_files' \
'*--watch-non-recursive=[Watch a specific directory, non-recursively]:PATH:_files' \
'-F+[Watch files and directories from a file]:PATH:_files' \
'--watch-file=[Watch files and directories from a file]:PATH:_files' \
'-c+[Clear screen before running command]' \
'--clear=[Clear screen before running command]' \
'-o+[What to do when receiving events while the command is running]:MODE:(queue do-nothing restart signal)' \
'--on-busy-update=[What to do when receiving events while the command is running]:MODE:(queue do-nothing restart signal)' \
'(-r --restart)-s+[Send a signal to the process when it'\''s still running]:SIGNAL: ' \
'(-r --restart)--signal=[Send a signal to the process when it'\''s still running]:SIGNAL: ' \
'--stop-signal=[Signal to send to stop the command]:SIGNAL: ' \
'--stop-timeout=[Time to wait for the command to exit gracefully]:TIMEOUT: ' \
'*--map-signal=[Translate signals from the OS to signals to send to the command]:SIGNAL:SIGNAL: ' \
'-d+[Time to wait for new events before taking action]:TIMEOUT: ' \
'--debounce=[Time to wait for new events before taking action]:TIMEOUT: ' \
'--delay-run=[Sleep before running the command]:DURATION: ' \
'--poll=[Poll for filesystem changes]' \
'--shell=[Use a different shell]:SHELL: ' \
'--emit-events-to=[Configure event emission]:MODE:(environment stdio file json-stdio json-file none)' \
'*-E+[Add env vars to the command]:KEY=VALUE: ' \
'*--env=[Add env vars to the command]:KEY=VALUE: ' \
'--wrap-process=[Configure how the process is wrapped]:MODE:(group session none)' \
'--color=[When to use terminal colours]:MODE:(auto always never)' \
'--project-origin=[Set the project origin]:DIRECTORY:_files -/' \
'--workdir=[Set the working directory]:DIRECTORY:_files -/' \
'*-e+[Filename extensions to filter to]:EXTENSIONS: ' \
'*--exts=[Filename extensions to filter to]:EXTENSIONS: ' \
'*-f+[Filename patterns to filter to]:PATTERN: ' \
'*--filter=[Filename patterns to filter to]:PATTERN: ' \
'*--filter-file=[Files to load filters from]:PATH:_files' \
'*-j+[\[experimental\] Filter programs]:EXPRESSION: ' \
'*--filter-prog=[\[experimental\] Filter programs]:EXPRESSION: ' \
'*-i+[Filename patterns to filter out]:PATTERN: ' \
'*--ignore=[Filename patterns to filter out]:PATTERN: ' \
'*--ignore-file=[Files to load ignores from]:PATH:_files' \
'*--fs-events=[Filesystem events to filter to]:EVENTS:(access create remove rename modify metadata)' \
'(--manual)--completions=[Generate a shell completions script]:COMPLETIONS:(bash elvish fish nu powershell zsh)' \
'--log-file=[Write diagnostic logs to a file]' \
'(-o --on-busy-update)-r[Restart the process if it'\''s still running]' \
'(-o --on-busy-update)--restart[Restart the process if it'\''s still running]' \
'--stdin-quit[Exit when stdin closes]' \
'--no-vcs-ignore[Don'\''t load gitignores]' \
'--no-project-ignore[Don'\''t load project-local ignores]' \
'--no-global-ignore[Don'\''t load global ignores]' \
'--no-default-ignore[Don'\''t use internal default ignores]' \
'--no-discover-ignore[Don'\''t discover ignore files at all]' \
'--ignore-nothing[Don'\''t ignore anything at all]' \
'-p[Wait until first change before running command]' \
'--postpone[Wait until first change before running command]' \
'-n[Shorthand for '\''--shell=none'\'']' \
'--no-environment[Deprecated shorthand for '\''--emit-events=none'\'']' \
'(--completions --manual)--only-emit-events[Only emit events to stdout, run no commands]' \
'--no-process-group[Don'\''t use a process group]' \
'-1[Testing only\: exit Watchexec after the first run]' \
'-N[Alert when commands start and end]' \
'--notify[Alert when commands start and end]' \
'--timings[Print how long the command took to run]' \
'-q[Don'\''t print starting and stopping messages]' \
'--quiet[Don'\''t print starting and stopping messages]' \
'--bell[Ring the terminal bell on command completion]' \
'(--fs-events)--no-meta[Don'\''t emit fs events for metadata changes]' \
'--print-events[Print events that trigger actions]' \
'(--completions)--manual[Show the manual page]' \
'*-v[Set diagnostic log level]' \
'*--verbose[Set diagnostic log level]' \
'-h[Print help (see more with '\''--help'\'')]' \
'--help[Print help (see more with '\''--help'\'')]' \
'-V[Print version]' \
'--version[Print version]' \
'*::command -- Command to run on changes:_cmdstring' \
&& ret=0
}
(( $+functions[_watchexec_commands] )) ||
_watchexec_commands() {
local commands; commands=()
_describe -t commands 'watchexec commands' commands "$@"
}
if [ "$funcstack[1]" = "_watchexec" ]; then
_watchexec "$@"
else
compdef _watchexec watchexec
fi

View file

@ -1,27 +0,0 @@
# Changelog
## Next (YYYY-MM-DD)
## v1.1.1 (2024-10-14)
- Deps: gix 0.66
## v1.1.0 (2024-05-16)
- Add `git-describe` support (#832, by @lu-zero)
## v1.0.3 (2024-04-20)
- Deps: gix 0.62
## v1.0.2 (2023-11-26)
- Deps: upgrade to gix 0.55
## v1.0.1 (2023-07-02)
- Deps: upgrade to gix 0.44
## v1.0.0 (2023-03-05)
- Initial release.

View file

@ -1,40 +0,0 @@
[package]
name = "bosion"
version = "1.1.1"
authors = ["Félix Saparelli <felix@passcod.name>"]
license = "Apache-2.0 OR MIT"
description = "Gather build information for verbose versions flags"
keywords = ["version", "git", "verbose", "long"]
documentation = "https://docs.rs/bosion"
repository = "https://github.com/watchexec/watchexec"
readme = "README.md"
rust-version = "1.64.0"
edition = "2021"
[dependencies.time]
version = "0.3.30"
features = ["macros", "formatting"]
[dependencies.gix]
version = "0.66.0"
optional = true
default-features = false
features = ["revision"]
[features]
default = ["git", "reproducible", "std"]
### Read from git repo, provide GIT_* vars
git = ["dep:gix"]
### Read from SOURCE_DATE_EPOCH when available
reproducible = []
### Provide a long_version_with() function to add extra info
###
### Specifically this is std support for the _using_ crate, not for the bosion crate itself. It's
### assumed that the bosion crate is always std, as it runs in build.rs.
std = []

View file

@ -1,147 +0,0 @@
# Bosion
_Gather build information for verbose versions flags._
- **[API documentation][docs]**.
- Licensed under [Apache 2.0][license] or [MIT](https://passcod.mit-license.org).
- Status: maintained.
[docs]: https://docs.rs/bosion
[license]: ../../LICENSE
## Quick start
In your `Cargo.toml`:
```toml
[build-dependencies]
bosion = "1.1.1"
```
In your `build.rs`:
```rust ,no_run
fn main() {
bosion::gather();
}
```
In your `src/main.rs`:
```rust ,ignore
include!(env!("BOSION_PATH"));
fn main() {
// default output, like rustc -Vv
println!("{}", Bosion::LONG_VERSION);
// with additional fields
println!("{}", Bosion::long_version_with(&[
("custom data", "value"),
("LLVM version", "15.0.6"),
]));
// enabled features like +feature +an-other
println!("{}", Bosion::CRATE_FEATURE_STRING);
// the raw data
println!("{}", Bosion::GIT_COMMIT_HASH);
println!("{}", Bosion::GIT_COMMIT_SHORTHASH);
println!("{}", Bosion::GIT_COMMIT_DATE);
println!("{}", Bosion::GIT_COMMIT_DATETIME);
println!("{}", Bosion::CRATE_VERSION);
println!("{:?}", Bosion::CRATE_FEATURES);
println!("{}", Bosion::BUILD_DATE);
println!("{}", Bosion::BUILD_DATETIME);
}
```
## Advanced usage
Generating a struct with public visibility:
```rust ,no_run
// build.rs
bosion::gather_pub();
```
Customising the output file and struct names:
```rust ,no_run
// build.rs
bosion::gather_to("buildinfo.rs", "Build", /* public? */ false);
```
Outputting build-time environment variables instead of source:
```rust ,ignore
// build.rs
bosion::gather_to_env();
// src/main.rs
fn main() {
println!("{}", env!("BOSION_GIT_COMMIT_HASH"));
println!("{}", env!("BOSION_GIT_COMMIT_SHORTHASH"));
println!("{}", env!("BOSION_GIT_COMMIT_DATE"));
println!("{}", env!("BOSION_GIT_COMMIT_DATETIME"));
println!("{}", env!("BOSION_BUILD_DATE"));
println!("{}", env!("BOSION_BUILD_DATETIME"));
println!("{}", env!("BOSION_CRATE_VERSION"));
println!("{}", env!("BOSION_CRATE_FEATURES")); // comma-separated
}
```
Custom env prefix:
```rust ,no_run
// build.rs
bosion::gather_to_env_with_prefix("MYAPP_");
```
## Features
- `reproducible`: reads [`SOURCE_DATE_EPOCH`](https://reproducible-builds.org/docs/source-date-epoch/) (default).
- `git`: enables gathering git information (default).
- `std`: enables the `long_version_with` method (default).
Specifically, this is about the downstream crate's std support, not Bosion's, which always requires std.
## Why not...?
- [bugreport](https://github.com/sharkdp/bugreport): runtime library, for bug information.
- [git-testament](https://github.com/kinnison/git-testament): uses the `git` CLI instead of gitoxide.
- [human-panic](https://github.com/rust-cli/human-panic): runtime library, for panics.
- [shadow-rs](https://github.com/baoyachi/shadow-rs): uses libgit2 instead of gitoxide, doesn't rebuild on git changes.
- [vergen](https://github.com/rustyhorde/vergen): uses the `git` CLI instead of gitoxide.
Bosion also requires no dependencies outside of build.rs, and was specifically made for crates
installed in a variety of ways, like with `cargo install`, from pre-built binary, from source with
git, or from source without git (like a tarball), on a variety of platforms. Its default output with
[clap](https://clap.rs) is almost exactly like `rustc -Vv`.
## Examples
The [examples](./examples) directory contains a practical and runnable [clap-based example](./examples/clap/), as well
as several other crates which are actually used for integration testing.
Here is the output for the Watchexec CLI:
```plain
watchexec 1.21.1 (5026793 2023-03-05)
commit-hash: 5026793a12ff895edf2dafb92111e7bd1767650e
commit-date: 2023-03-05
build-date: 2023-03-05
release: 1.21.1
features:
```
For comparison, here's `rustc -Vv`:
```plain
rustc 1.67.1 (d5a82bbd2 2023-02-07)
binary: rustc
commit-hash: d5a82bbd26e1ad8b7401f6a718a9c57c96905483
commit-date: 2023-02-07
host: x86_64-unknown-linux-gnu
release: 1.67.1
LLVM version: 15.0.6
```

File diff suppressed because it is too large Load diff

View file

@ -1,19 +0,0 @@
[package]
name = "bosion-example-clap"
version = "0.1.0"
publish = false
edition = "2021"
[workspace]
[features]
default = ["foo"]
foo = []
[build-dependencies.bosion]
version = "*"
path = "../.."
[dependencies.clap]
version = "4.1.8"
features = ["cargo", "derive"]

View file

@ -1,3 +0,0 @@
fn main() {
bosion::gather();
}

View file

@ -1,41 +0,0 @@
use clap::Parser;
include!(env!("BOSION_PATH"));
#[derive(Parser)]
#[clap(version, long_version = Bosion::LONG_VERSION)]
struct Args {
#[clap(long)]
extras: bool,
#[clap(long)]
features: bool,
#[clap(long)]
dates: bool,
#[clap(long)]
describe: bool,
}
fn main() {
let args = Args::parse();
if args.extras {
println!(
"{}",
Bosion::long_version_with(&[("extra", "field"), ("custom", "1.2.3"),])
);
} else if args.features {
println!("Features: {}", Bosion::CRATE_FEATURE_STRING);
} else if args.dates {
println!("commit date: {}", Bosion::GIT_COMMIT_DATE);
println!("commit datetime: {}", Bosion::GIT_COMMIT_DATETIME);
println!("build date: {}", Bosion::BUILD_DATE);
println!("build datetime: {}", Bosion::BUILD_DATETIME);
} else if args.describe {
println!("commit description: {}", Bosion::GIT_COMMIT_DESCRIPTION);
} else {
println!("{}", Bosion::LONG_VERSION);
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,20 +0,0 @@
[package]
name = "bosion-test-default"
version = "0.1.0"
publish = false
edition = "2021"
[workspace]
[features]
default = ["foo"]
foo = []
[build-dependencies.bosion]
version = "*"
path = "../.."
[dependencies]
leon = { version = "2.0.1", default-features = false }
snapbox = "0.5.9"
time = { version = "0.3.30", features = ["formatting", "macros"] }

View file

@ -1,3 +0,0 @@
fn main() {
bosion::gather();
}

View file

@ -1,68 +0,0 @@
#[cfg(test)]
pub(crate) fn git_commit_info(format: &str) -> String {
let output = std::process::Command::new("git")
.arg("show")
.arg("--no-notes")
.arg("--no-patch")
.arg(format!("--pretty=format:{format}"))
.output()
.expect("git");
String::from_utf8(output.stdout)
.expect("git")
.trim()
.to_string()
}
#[macro_export]
macro_rules! test_snapshot {
($name:ident, $actual:expr) => {
#[cfg(test)]
#[test]
fn $name() {
use std::str::FromStr;
let gittime = ::time::OffsetDateTime::from_unix_timestamp(
i64::from_str(&crate::common::git_commit_info("%ct")).expect("git i64"),
)
.expect("git time");
::snapbox::Assert::new().matches(
::leon::Template::parse(
std::fs::read_to_string(format!("../snapshots/{}.txt", stringify!($name)))
.expect("read file")
.trim(),
)
.expect("leon parse")
.render(&[
(
"today date".to_string(),
::time::OffsetDateTime::now_utc()
.format(::time::macros::format_description!("[year]-[month]-[day]"))
.unwrap(),
),
("git hash".to_string(), crate::common::git_commit_info("%H")),
(
"git shorthash".to_string(),
crate::common::git_commit_info("%h"),
),
(
"git date".to_string(),
gittime
.format(::time::macros::format_description!("[year]-[month]-[day]"))
.expect("git date format"),
),
(
"git datetime".to_string(),
gittime
.format(::time::macros::format_description!(
"[year]-[month]-[day] [hour]:[minute]:[second]"
))
.expect("git time format"),
),
])
.expect("leon render"),
$actual,
);
}
};
}

View file

@ -1,27 +0,0 @@
include!(env!("BOSION_PATH"));
mod common;
fn main() {}
test_snapshot!(crate_version, Bosion::CRATE_VERSION);
test_snapshot!(crate_features, format!("{:#?}", Bosion::CRATE_FEATURES));
test_snapshot!(build_date, Bosion::BUILD_DATE);
test_snapshot!(build_datetime, Bosion::BUILD_DATETIME);
test_snapshot!(git_commit_hash, Bosion::GIT_COMMIT_HASH);
test_snapshot!(git_commit_shorthash, Bosion::GIT_COMMIT_SHORTHASH);
test_snapshot!(git_commit_date, Bosion::GIT_COMMIT_DATE);
test_snapshot!(git_commit_datetime, Bosion::GIT_COMMIT_DATETIME);
test_snapshot!(default_long_version, Bosion::LONG_VERSION);
test_snapshot!(
default_long_version_with,
Bosion::long_version_with(&[("extra", "field"), ("custom", "1.2.3")])
);

View file

@ -1,336 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anstream"
version = "0.6.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
[[package]]
name = "anstyle-parse"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]]
name = "bosion"
version = "1.1.0"
dependencies = [
"time",
]
[[package]]
name = "bosion-test-no-git"
version = "0.1.0"
dependencies = [
"bosion",
"leon",
"snapbox",
"time",
]
[[package]]
name = "colorchoice"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "leon"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52df920dfe9751d43501ff2ee12dd81c457d9e810d3f64b5200ee461fe73800b"
dependencies = [
"thiserror",
]
[[package]]
name = "normalize-line-endings"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "proc-macro2"
version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "similar"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e"
[[package]]
name = "snapbox"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37d101fcafc8e73748fd8a1b7048f5979f93d372fd17027d7724c1643bc379b"
dependencies = [
"anstream",
"anstyle",
"normalize-line-endings",
"similar",
"snapbox-macros",
]
[[package]]
name = "snapbox-macros"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16569f53ca23a41bb6f62e0a5084aa1661f4814a67fa33696a79073e03a664af"
dependencies = [
"anstream",
]
[[package]]
name = "syn"
version = "2.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "time"
version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

View file

@ -1,22 +0,0 @@
[package]
name = "bosion-test-no-git"
version = "0.1.0"
publish = false
edition = "2021"
[workspace]
[features]
default = ["foo"]
foo = []
[build-dependencies.bosion]
version = "*"
path = "../.."
default-features = false
features = ["std"]
[dependencies]
leon = { version = "2.0.1", default-features = false }
snapbox = "0.5.9"
time = { version = "0.3.30", features = ["formatting", "macros"] }

View file

@ -1,3 +0,0 @@
fn main() {
bosion::gather();
}

View file

@ -1,20 +0,0 @@
include!(env!("BOSION_PATH"));
#[path = "../../default/src/common.rs"]
mod common;
fn main() {}
test_snapshot!(crate_version, Bosion::CRATE_VERSION);
test_snapshot!(crate_features, format!("{:#?}", Bosion::CRATE_FEATURES));
test_snapshot!(build_date, Bosion::BUILD_DATE);
test_snapshot!(build_datetime, Bosion::BUILD_DATETIME);
test_snapshot!(no_git_long_version, Bosion::LONG_VERSION);
test_snapshot!(
no_git_long_version_with,
Bosion::long_version_with(&[("extra", "field"), ("custom", "1.2.3")])
);

View file

@ -1,336 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anstream"
version = "0.6.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
[[package]]
name = "anstyle-parse"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]]
name = "bosion"
version = "1.1.0"
dependencies = [
"time",
]
[[package]]
name = "bosion-test-no-std"
version = "0.1.0"
dependencies = [
"bosion",
"leon",
"snapbox",
"time",
]
[[package]]
name = "colorchoice"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "leon"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52df920dfe9751d43501ff2ee12dd81c457d9e810d3f64b5200ee461fe73800b"
dependencies = [
"thiserror",
]
[[package]]
name = "normalize-line-endings"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "proc-macro2"
version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "similar"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e"
[[package]]
name = "snapbox"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37d101fcafc8e73748fd8a1b7048f5979f93d372fd17027d7724c1643bc379b"
dependencies = [
"anstream",
"anstyle",
"normalize-line-endings",
"similar",
"snapbox-macros",
]
[[package]]
name = "snapbox-macros"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16569f53ca23a41bb6f62e0a5084aa1661f4814a67fa33696a79073e03a664af"
dependencies = [
"anstream",
]
[[package]]
name = "syn"
version = "2.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "time"
version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

View file

@ -1,27 +0,0 @@
[package]
name = "bosion-test-no-std"
version = "0.1.0"
publish = false
edition = "2021"
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
[workspace]
[features]
default = ["foo"]
foo = []
[build-dependencies.bosion]
version = "*"
path = "../.."
default-features = false
[dependencies]
leon = { version = "2.0.1", default-features = false }
snapbox = "0.5.9"
time = { version = "0.3.30", features = ["formatting", "macros"] }

View file

@ -1,3 +0,0 @@
fn main() {
bosion::gather();
}

View file

@ -1,32 +0,0 @@
#![cfg_attr(not(test), no_main)]
#![cfg_attr(not(test), no_std)]
#[cfg(not(test))]
use core::panic::PanicInfo;
#[cfg(not(test))]
#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
loop {}
}
include!(env!("BOSION_PATH"));
#[cfg(test)]
#[path = "../../default/src/common.rs"]
mod common;
#[cfg(test)]
mod test {
use super::*;
test_snapshot!(crate_version, Bosion::CRATE_VERSION);
test_snapshot!(crate_features, format!("{:#?}", Bosion::CRATE_FEATURES));
test_snapshot!(build_date, Bosion::BUILD_DATE);
test_snapshot!(build_datetime, Bosion::BUILD_DATETIME);
test_snapshot!(no_git_long_version, Bosion::LONG_VERSION);
}

View file

@ -1 +0,0 @@
{today date}

View file

@ -1 +0,0 @@
{today date} [..]

View file

@ -1,4 +0,0 @@
[
"default",
"foo",
]

View file

@ -1 +0,0 @@
0.1.0

View file

@ -1,6 +0,0 @@
0.1.0 ({git shorthash} {git date}) +foo
commit-hash: {git hash}
commit-date: {git date}
build-date: {today date}
release: 0.1.0
features: default,foo

View file

@ -1,8 +0,0 @@
0.1.0 ({git shorthash} {git date}) +foo
commit-hash: {git hash}
commit-date: {git date}
build-date: {today date}
release: 0.1.0
features: default,foo
extra: field
custom: 1.2.3

View file

@ -1 +0,0 @@
{git date}

View file

@ -1 +0,0 @@
{git datetime}

View file

@ -1 +0,0 @@
{git hash}

View file

@ -1 +0,0 @@
{git shorthash}

View file

@ -1,4 +0,0 @@
0.1.0 ({today date}) +foo
build-date: {today date}
release: 0.1.0
features: default,foo

View file

@ -1,6 +0,0 @@
0.1.0 ({today date}) +foo
build-date: {today date}
release: 0.1.0
features: default,foo
extra: field
custom: 1.2.3

View file

@ -1,17 +0,0 @@
pre-release-commit-message = "release: bosion v{{version}}"
tag-prefix = "bosion-"
tag-message = "bosion {{version}}"
[[pre-release-replacements]]
file = "CHANGELOG.md"
search = "^## Next.*$"
replace = "## Next (YYYY-MM-DD)\n\n## v{{version}} ({{date}})"
prerelease = true
max = 1
[[pre-release-replacements]]
file = "README.md"
search = "^bosion = \".*\"$"
replace = "bosion = \"{{version}}\""
prerelease = true
max = 1

View file

@ -1,11 +0,0 @@
#!/bin/bash
set -euo pipefail
for test in default no-git no-std; do
echo "Testing $test"
pushd examples/$test
cargo check
cargo test
popd
done

View file

@ -1,172 +0,0 @@
use std::{env::var, path::PathBuf};
use time::{format_description::FormatItem, macros::format_description, OffsetDateTime};
/// Gathered build-time information
///
/// This struct contains all the information gathered by `bosion`. It is not meant to be used
/// directly under normal circumstances, but is public for documentation purposes and if you wish
/// to build your own frontend for whatever reason. In that case, note that no effort has been made
/// to make this usable outside of the build.rs environment.
///
/// The `git` field is only available when the `git` feature is enabled, and if there is a git
/// repository to read from. The repository is discovered by walking up the directory tree until one
/// is found, which means workspaces or more complex monorepos are automatically supported. If there
/// are any errors reading the repository, the `git` field will be `None` and a rustc warning will
/// be printed.
#[derive(Debug, Clone)]
pub struct Info {
/// The crate version, as read from the `CARGO_PKG_VERSION` environment variable.
pub crate_version: String,
/// The crate features, as found by the presence of `CARGO_FEATURE_*` environment variables.
///
/// These are normalised to lowercase and have underscores replaced by hyphens.
pub crate_features: Vec<String>,
/// The build date, in the format `YYYY-MM-DD`, at UTC.
///
/// This is either current as of build time, or from the timestamp specified by the
/// `SOURCE_DATE_EPOCH` environment variable, for
/// [reproducible builds](https://reproducible-builds.org/).
pub build_date: String,
/// The build datetime, in the format `YYYY-MM-DD HH:MM:SS`, at UTC.
///
/// This is either current as of build time, or from the timestamp specified by the
/// `SOURCE_DATE_EPOCH` environment variable, for
/// [reproducible builds](https://reproducible-builds.org/).
pub build_datetime: String,
/// Git repository information, if available.
pub git: Option<GitInfo>,
}
trait ErrString<T> {
fn err_string(self) -> Result<T, String>;
}
impl<T, E> ErrString<T> for Result<T, E>
where
E: std::fmt::Display,
{
fn err_string(self) -> Result<T, String> {
self.map_err(|e| e.to_string())
}
}
const DATE_FORMAT: &[FormatItem<'static>] = format_description!("[year]-[month]-[day]");
const DATETIME_FORMAT: &[FormatItem<'static>] =
format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
impl Info {
/// Gathers build-time information
///
/// This is not meant to be used directly under normal circumstances, but is public if you wish
/// to build your own frontend for whatever reason. In that case, note that no effort has been
/// made to make this usable outside of the build.rs environment.
pub fn gather() -> Result<Self, String> {
let build_date = Self::build_date()?;
Ok(Self {
crate_version: var("CARGO_PKG_VERSION").err_string()?,
crate_features: Self::features(),
build_date: build_date.format(DATE_FORMAT).err_string()?,
build_datetime: build_date.format(DATETIME_FORMAT).err_string()?,
#[cfg(feature = "git")]
git: GitInfo::gather()
.map_err(|e| {
println!("cargo:warning=git info gathering failed: {e}");
})
.ok(),
#[cfg(not(feature = "git"))]
git: None,
})
}
fn build_date() -> Result<OffsetDateTime, String> {
if cfg!(feature = "reproducible") {
if let Ok(date) = var("SOURCE_DATE_EPOCH") {
if let Ok(date) = date.parse::<i64>() {
return OffsetDateTime::from_unix_timestamp(date).err_string();
}
}
}
Ok(OffsetDateTime::now_utc())
}
fn features() -> Vec<String> {
let mut features = Vec::new();
for (key, _) in std::env::vars() {
if let Some(stripped) = key.strip_prefix("CARGO_FEATURE_") {
features.push(stripped.replace('_', "-").to_lowercase().to_string());
}
}
features
}
pub(crate) fn set_reruns(&self) {
if cfg!(feature = "reproducible") {
println!("cargo:rerun-if-env-changed=SOURCE_DATE_EPOCH");
}
if let Some(git) = &self.git {
let git_head = git.git_root.join("HEAD");
println!("cargo:rerun-if-changed={}", git_head.display());
}
}
}
/// Git repository information
#[derive(Debug, Clone)]
pub struct GitInfo {
/// The absolute path to the git repository's data folder.
///
/// In a normal repository, this is `.git`, _not_ the index or working directory.
pub git_root: PathBuf,
/// The full hash of the current commit.
///
/// Note that this makes no effore to handle dirty working directories, so it may not be
/// representative of the current state of the code.
pub git_hash: String,
/// The short hash of the current commit.
///
/// This is read from git and not truncated manually, so it may be longer than 7 characters.
pub git_shorthash: String,
/// The date of the current commit, in the format `YYYY-MM-DD`, at UTC.
pub git_date: String,
/// The datetime of the current commit, in the format `YYYY-MM-DD HH:MM:SS`, at UTC.
pub git_datetime: String,
/// The `git describe` equivalent output
pub git_description: String,
}
#[cfg(feature = "git")]
impl GitInfo {
fn gather() -> Result<Self, String> {
use std::path::Path;
let (path, _) = gix::discover::upwards(Path::new(".")).err_string()?;
let repo = gix::discover(path).err_string()?;
let head = repo.head_commit().err_string()?;
let time = head.time().err_string()?;
let timestamp = OffsetDateTime::from_unix_timestamp(time.seconds as _).err_string()?;
Ok(Self {
git_root: repo.path().canonicalize().err_string()?,
git_hash: head.id().to_string(),
git_shorthash: head.short_id().err_string()?.to_string(),
git_date: timestamp.format(DATE_FORMAT).err_string()?,
git_datetime: timestamp.format(DATETIME_FORMAT).err_string()?,
git_description: head.describe().format().err_string()?.to_string(),
})
}
}

View file

@ -1,263 +0,0 @@
#![doc = include_str!("../README.md")]
use std::{env::var, fs::File, io::Write, path::PathBuf};
pub use info::*;
mod info;
/// Gather build-time information for the current crate
///
/// See the crate-level documentation for a guide. This function is a convenience wrapper around
/// [`gather_to`] with the most common defaults: it writes to `bosion.rs` a pub(crate) struct named
/// `Bosion`.
pub fn gather() {
gather_to("bosion.rs", "Bosion", false);
}
/// Gather build-time information for the current crate (public visibility)
///
/// See the crate-level documentation for a guide. This function is a convenience wrapper around
/// [`gather_to`]: it writes to `bosion.rs` a pub struct named `Bosion`.
pub fn gather_pub() {
gather_to("bosion.rs", "Bosion", true);
}
/// Gather build-time information for the current crate (custom output)
///
/// Gathers a limited set of build-time information for the current crate and writes it to a file.
/// The file is always written to the `OUT_DIR` directory, as per Cargo conventions. It contains a
/// zero-size struct with a bunch of associated constants containing the gathered information, and a
/// `long_version_with` function (when the `std` feature is enabled) that takes a slice of extra
/// key-value pairs to append in the same format.
///
/// `public` controls whether the struct is `pub` (true) or `pub(crate)` (false).
///
/// The generated code is entirely documented, and will appear in your documentation (in docs.rs, it
/// only will if visibility is public).
///
/// See [`Info`] for a list of gathered data.
///
/// The constants include all the information from [`Info`], as well as the following:
///
/// - `LONG_VERSION`: A clap-ready long version string, including the crate version, features, build
/// date, and git information when available.
/// - `CRATE_FEATURE_STRING`: A string containing the crate features, in the format `+feat1 +feat2`.
///
/// We also instruct rustc to rerun the build script if the environment changes, as necessary.
pub fn gather_to(filename: &str, structname: &str, public: bool) {
let path = PathBuf::from(var("OUT_DIR").expect("bosion")).join(filename);
println!("cargo:rustc-env=BOSION_PATH={}", path.display());
let info = Info::gather().expect("bosion");
info.set_reruns();
let Info {
crate_version,
crate_features,
build_date,
build_datetime,
git,
} = info;
let crate_feature_string = crate_features
.iter()
.filter(|feat| *feat != "default")
.map(|feat| format!("+{feat}"))
.collect::<Vec<_>>()
.join(" ");
let crate_feature_list = crate_features.join(",");
let viz = if public { "pub" } else { "pub(crate)" };
let (git_render, long_version) = if let Some(GitInfo {
git_hash,
git_shorthash,
git_date,
git_datetime,
git_description,
..
}) = git
{
(format!(
"
/// The git commit hash
///
/// This is the full hash of the commit that was built. Note that if the repository was
/// dirty, this will be the hash of the last commit, not including the changes.
pub const GIT_COMMIT_HASH: &'static str = {git_hash:?};
/// The git commit hash, shortened
///
/// This is the shortened hash of the commit that was built. Same caveats as with
/// `GIT_COMMIT_HASH` apply. The length of the hash is as short as possible while still
/// being unambiguous, at build time. For large repositories, this may be longer than 7
/// characters.
pub const GIT_COMMIT_SHORTHASH: &'static str = {git_shorthash:?};
/// The git commit date
///
/// This is the date (`YYYY-MM-DD`) of the commit that was built. Same caveats as with
/// `GIT_COMMIT_HASH` apply.
pub const GIT_COMMIT_DATE: &'static str = {git_date:?};
/// The git commit date and time
///
/// This is the date and time (`YYYY-MM-DD HH:MM:SS`) of the commit that was built. Same
/// caveats as with `GIT_COMMIT_HASH` apply.
pub const GIT_COMMIT_DATETIME: &'static str = {git_datetime:?};
/// The git description
///
/// This is the string equivalent to what `git describe` would output
pub const GIT_COMMIT_DESCRIPTION: &'static str = {git_description:?};
"
), format!("{crate_version} ({git_shorthash} {git_date}) {crate_feature_string}\ncommit-hash: {git_hash}\ncommit-date: {git_date}\nbuild-date: {build_date}\nrelease: {crate_version}\nfeatures: {crate_feature_list}"))
} else {
(String::new(), format!("{crate_version} ({build_date}) {crate_feature_string}\nbuild-date: {build_date}\nrelease: {crate_version}\nfeatures: {crate_feature_list}"))
};
#[cfg(feature = "std")]
let long_version_with_fn = r#"
/// Returns the long version string with extra information tacked on
///
/// This is the same as `LONG_VERSION` but takes a slice of key-value pairs to append to the
/// end in the same format.
pub fn long_version_with(extra: &[(&str, &str)]) -> String {
let mut output = Self::LONG_VERSION.to_string();
for (k, v) in extra {
output.push_str(&format!("\n{k}: {v}"));
}
output
}
"#;
#[cfg(not(feature = "std"))]
let long_version_with_fn = "";
let bosion_version = env!("CARGO_PKG_VERSION");
let render = format!(
r#"
/// Build-time information
///
/// This struct is generated by the [bosion](https://docs.rs/bosion) crate at build time.
///
/// Bosion version: {bosion_version}
#[derive(Debug, Clone, Copy)]
{viz} struct {structname};
#[allow(dead_code)]
impl {structname} {{
/// Clap-compatible long version string
///
/// At minimum, this will be the crate version and build date.
///
/// It presents as a first "summary" line like `crate_version (build_date) features`,
/// followed by `key: value` pairs. This is the same format used by `rustc -Vv`.
///
/// If git info is available, it also includes the git hash, short hash and commit date,
/// and swaps the build date for the commit date in the summary line.
pub const LONG_VERSION: &'static str = {long_version:?};
/// The crate version, as reported by Cargo
///
/// You should probably prefer reading the `CARGO_PKG_VERSION` environment variable.
pub const CRATE_VERSION: &'static str = {crate_version:?};
/// The crate features
///
/// This is a list of the features that were enabled when this crate was built,
/// lowercased and with underscores replaced by hyphens.
pub const CRATE_FEATURES: &'static [&'static str] = &{crate_features:?};
/// The crate features, as a string
///
/// This is in format `+feature +feature2 +feature3`, lowercased with underscores
/// replaced by hyphens.
pub const CRATE_FEATURE_STRING: &'static str = {crate_feature_string:?};
/// The build date
///
/// This is the date that the crate was built, in the format `YYYY-MM-DD`. If the
/// environment variable `SOURCE_DATE_EPOCH` was set, it's used instead of the current
/// time, for [reproducible builds](https://reproducible-builds.org/).
pub const BUILD_DATE: &'static str = {build_date:?};
/// The build datetime
///
/// This is the date and time that the crate was built, in the format
/// `YYYY-MM-DD HH:MM:SS`. If the environment variable `SOURCE_DATE_EPOCH` was set, it's
/// used instead of the current time, for
/// [reproducible builds](https://reproducible-builds.org/).
pub const BUILD_DATETIME: &'static str = {build_datetime:?};
{git_render}
{long_version_with_fn}
}}
"#
);
let mut file = File::create(path).expect("bosion");
file.write_all(render.as_bytes()).expect("bosion");
}
/// Gather build-time information and write it to the environment
///
/// See the crate-level documentation for a guide. This function is a convenience wrapper around
/// [`gather_to_env_with_prefix`] with the most common default prefix of `BOSION_`.
pub fn gather_to_env() {
gather_to_env_with_prefix("BOSION_");
}
/// Gather build-time information and write it to the environment
///
/// Gathers a limited set of build-time information for the current crate and makes it available to
/// the crate as build environment variables. This is an alternative to [`include!`]ing a file which
/// is generated at build time, like for [`gather`] and variants, which doesn't create any new code
/// and doesn't include any information in the binary that you do not explicitly use.
///
/// The environment variables are prefixed with the given string, which should be generally be
/// uppercase and end with an underscore.
///
/// See [`Info`] for a list of gathered data.
///
/// Unlike [`gather`], there is no Clap-ready `LONG_VERSION` string, but you can of course generate
/// one yourself from the environment variables.
///
/// We also instruct rustc to rerun the build script if the environment changes, as necessary.
pub fn gather_to_env_with_prefix(prefix: &str) {
let info = Info::gather().expect("bosion");
info.set_reruns();
let Info {
crate_version,
crate_features,
build_date,
build_datetime,
git,
} = info;
println!("cargo:rustc-env={prefix}CRATE_VERSION={crate_version}");
println!(
"cargo:rustc-env={prefix}CRATE_FEATURES={}",
crate_features.join(",")
);
println!("cargo:rustc-env={prefix}BUILD_DATE={build_date}");
println!("cargo:rustc-env={prefix}BUILD_DATETIME={build_datetime}");
if let Some(GitInfo {
git_hash,
git_shorthash,
git_date,
git_datetime,
git_description,
..
}) = git
{
println!("cargo:rustc-env={prefix}GIT_COMMIT_HASH={git_hash}");
println!("cargo:rustc-env={prefix}GIT_COMMIT_SHORTHASH={git_shorthash}");
println!("cargo:rustc-env={prefix}GIT_COMMIT_DATE={git_date}");
println!("cargo:rustc-env={prefix}GIT_COMMIT_DATETIME={git_datetime}");
println!("cargo:rustc-env={prefix}GIT_COMMIT_DESCRIPTION={git_description}");
}
}

View file

@ -1,197 +0,0 @@
[package]
name = "watchexec-cli"
version = "2.2.0"
authors = ["Félix Saparelli <felix@passcod.name>", "Matt Green <mattgreenrocks@gmail.com>"]
license = "Apache-2.0"
description = "Executes commands in response to file modifications"
keywords = ["watcher", "filesystem", "cli", "watchexec"]
categories = ["command-line-utilities"]
documentation = "https://watchexec.github.io/docs/#watchexec"
homepage = "https://watchexec.github.io"
repository = "https://github.com/watchexec/watchexec"
readme = "README.md"
edition = "2021"
[[bin]]
name = "watchexec"
path = "src/main.rs"
[dependencies]
ahash = "0.8.6" # needs to be in sync with jaq's
argfile = "0.2.0"
chrono = "0.4.31"
clap_complete = "4.4.4"
clap_complete_nushell = "4.4.2"
clap_mangen = "0.2.15"
clearscreen = "3.0.0"
dashmap = "6.1.0"
dirs = "5.0.0"
dunce = "1.0.4"
futures = "0.3.29"
humantime = "2.1.0"
indexmap = "2.2.6" # needs to be in sync with jaq's
is-terminal = "0.4.4"
jaq-core = "1.2.1"
jaq-interpret = "1.2.1"
jaq-parse = "1.0.2"
jaq-std = "1.2.1"
jaq-syn = "1.1.0"
notify-rust = "4.9.0"
once_cell = "1.17.1"
serde_json = "1.0.107"
tempfile = "3.8.1"
termcolor = "1.4.0"
tracing = "0.1.40"
tracing-appender = "0.2.3"
which = "6.0.1"
[dependencies.blake3]
version = "1.3.3"
features = ["rayon"]
[dependencies.clap]
version = "4.4.7"
features = ["cargo", "derive", "env", "wrap_help"]
[dependencies.console-subscriber]
version = "0.4.0"
optional = true
[dependencies.eyra]
version = "0.19.0"
features = ["log", "env_logger"]
optional = true
[dependencies.ignore-files]
version = "3.0.2"
path = "../ignore-files"
[dependencies.miette]
version = "7.2.0"
features = ["fancy"]
[dependencies.pid1]
version = "0.1.1"
optional = true
[dependencies.project-origins]
version = "1.4.0"
path = "../project-origins"
[dependencies.watchexec]
version = "5.0.0"
path = "../lib"
[dependencies.watchexec-events]
version = "4.0.0"
path = "../events"
features = ["serde"]
[dependencies.watchexec-signals]
version = "4.0.0"
path = "../signals"
[dependencies.watchexec-filterer-globset]
version = "6.0.0"
path = "../filterer/globset"
[dependencies.tokio]
version = "1.33.0"
features = [
"fs",
"io-std",
"process",
"rt",
"rt-multi-thread",
"signal",
"sync",
]
[dependencies.tracing-subscriber]
version = "0.3.6"
features = [
"env-filter",
"fmt",
"json",
"tracing-log",
"ansi",
]
[target.'cfg(target_env = "musl")'.dependencies]
mimalloc = "0.1.39"
[build-dependencies]
embed-resource = "2.4.0"
[build-dependencies.bosion]
version = "1.1.1"
path = "../bosion"
[dev-dependencies]
tracing-test = "0.2.4"
uuid = { workspace = true, features = [ "v4", "fast-rng" ] }
rand = { workspace = true }
[features]
default = ["pid1"]
## Build using Eyra's pure-Rust libc
eyra = ["dep:eyra"]
## Enables PID1 handling.
pid1 = ["dep:pid1"]
## Enables logging for PID1 handling.
pid1-withlog = ["pid1"]
## For debugging only: enables the Tokio Console.
dev-console = ["dep:console-subscriber"]
[package.metadata.binstall]
pkg-url = "{ repo }/releases/download/v{ version }/watchexec-{ version }-{ target }.{ archive-format }"
bin-dir = "watchexec-{ version }-{ target }/{ bin }{ binary-ext }"
pkg-fmt = "txz"
[package.metadata.binstall.overrides.x86_64-pc-windows-msvc]
pkg-fmt = "zip"
[package.metadata.deb]
maintainer = "Félix Saparelli <felix@passcod.name>"
license-file = ["../../LICENSE", "0"]
section = "utility"
depends = "libc6, libgcc-s1" # not needed for musl, but see below
# conf-files = [] # look me up when config file lands
assets = [
["../../target/release/watchexec", "usr/bin/watchexec", "755"],
["README.md", "usr/share/doc/watchexec/README", "644"],
["../../doc/watchexec.1.md", "usr/share/doc/watchexec/watchexec.1.md", "644"],
["../../doc/watchexec.1", "usr/share/man/man1/watchexec.1", "644"],
["../../completions/bash", "usr/share/bash-completion/completions/watchexec", "644"],
["../../completions/fish", "usr/share/fish/vendor_completions.d/watchexec.fish", "644"],
["../../completions/zsh", "usr/share/zsh/site-functions/_watchexec", "644"],
["../../doc/logo.svg", "usr/share/icons/hicolor/scalable/apps/watchexec.svg", "644"],
]
[package.metadata.generate-rpm]
assets = [
{ source = "../../target/release/watchexec", dest = "/usr/bin/watchexec", mode = "755" },
{ source = "README.md", dest = "/usr/share/doc/watchexec/README", mode = "644", doc = true },
{ source = "../../doc/watchexec.1.md", dest = "/usr/share/doc/watchexec/watchexec.1.md", mode = "644", doc = true },
{ source = "../../doc/watchexec.1", dest = "/usr/share/man/man1/watchexec.1", mode = "644" },
{ source = "../../completions/bash", dest = "/usr/share/bash-completion/completions/watchexec", mode = "644" },
{ source = "../../completions/fish", dest = "/usr/share/fish/vendor_completions.d/watchexec.fish", mode = "644" },
{ source = "../../completions/zsh", dest = "/usr/share/zsh/site-functions/_watchexec", mode = "644" },
{ source = "../../doc/logo.svg", dest = "/usr/share/icons/hicolor/scalable/apps/watchexec.svg", mode = "644" },
# set conf = true for config file when that lands
]
auto-req = "disabled"
# technically incorrect when using musl, but these are probably
# present on every rpm-using system, so let's worry about it if
# someone asks.
[package.metadata.generate-rpm.requires]
glibc = "*"
libgcc = "*"

View file

@ -1,197 +0,0 @@
# Watchexec CLI
A simple standalone tool that watches a path and runs a command whenever it detects modifications.
Example use cases:
* Automatically run unit tests
* Run linters/syntax checkers
## Features
* Simple invocation and use
* Runs on Linux, Mac, Windows, and more
* Monitors current directory and all subdirectories for changes
* Uses efficient event polling mechanism (on Linux, Mac, Windows, BSD)
* Coalesces multiple filesystem events into one, for editors that use swap/backup files during saving
* By default, uses `.gitignore`, `.ignore`, and other such files to determine which files to ignore notifications for
* Support for watching files with a specific extension
* Support for filtering/ignoring events based on [glob patterns](https://docs.rs/globset/*/globset/#syntax)
* Launches the command in a new process group (can be disabled with `--no-process-group`)
* Optionally clears screen between executions
* Optionally restarts the command with every modification (good for servers)
* Optionally sends a desktop notification on command start and end
* Does not require a language runtime
* Sets the following environment variables in the process:
`$WATCHEXEC_COMMON_PATH` is set to the longest common path of all of the below variables, and so should be prepended to each path to obtain the full/real path.
| Variable name | Event kind |
|---|---|
| `$WATCHEXEC_CREATED_PATH` | files/folders were created |
| `$WATCHEXEC_REMOVED_PATH` | files/folders were removed |
| `$WATCHEXEC_RENAMED_PATH` | files/folders were renamed |
| `$WATCHEXEC_WRITTEN_PATH` | files/folders were modified |
| `$WATCHEXEC_META_CHANGED_PATH` | files/folders' metadata were modified |
| `$WATCHEXEC_OTHERWISE_CHANGED_PATH` | every other kind of event |
These variables may contain multiple paths: these are separated by the platform's path separator, as with the `PATH` system environment variable. On Unix that is `:`, and on Windows `;`. Within each variable, paths are deduplicated and sorted in binary order (i.e. neither Unicode nor locale aware).
This can be disabled with `--emit-events=none` or changed to JSON events on STDIN with `--emit-events=json-stdio`.
## Anti-Features
* Not tied to any particular language or ecosystem
* Not tied to Git or the presence of a repository/project
* Does not require a cryptic command line involving `xargs`
## Usage Examples
Watch all JavaScript, CSS and HTML files in the current directory and all subdirectories for changes, running `make` when a change is detected:
$ watchexec --exts js,css,html make
Call `make test` when any file changes in this directory/subdirectory, except for everything below `target`:
$ watchexec -i "target/**" make test
Call `ls -la` when any file changes in this directory/subdirectory:
$ watchexec -- ls -la
Call/restart `python server.py` when any Python file in the current directory (and all subdirectories) changes:
$ watchexec -e py -r python server.py
Call/restart `my_server` when any file in the current directory (and all subdirectories) changes, sending `SIGKILL` to stop the command:
$ watchexec -r --stop-signal SIGKILL my_server
Send a SIGHUP to the command upon changes (Note: using `-n` here we're executing `my_server` directly, instead of wrapping it in a shell:
$ watchexec -n --signal SIGHUP my_server
Run `make` when any file changes, using the `.gitignore` file in the current directory to filter:
$ watchexec make
Run `make` when any file in `lib` or `src` changes:
$ watchexec -w lib -w src make
Run `bundle install` when the `Gemfile` changes:
$ watchexec -w Gemfile bundle install
Run two commands:
$ watchexec 'date; make'
Get desktop ("toast") notifications when the command starts and finishes:
$ watchexec -N go build
Only run when files are created:
$ watchexec --fs-events create -- s3 sync . s3://my-bucket
If you come from `entr`, note that the watchexec command is run in a shell by default. You can use `-n` or `--shell=none` to not do that:
$ watchexec -n -- echo ';' lorem ipsum
On Windows, you may prefer to use Powershell:
$ watchexec --shell=pwsh -- Test-Connection example.com
You can eschew running commands entirely and get a stream of events to process on your own:
```console
$ watchexec --emit-events-to=json-stdio --only-emit-events
{"tags":[{"kind":"source","source":"filesystem"},{"kind":"fs","simple":"modify","full":"Modify(Data(Any))"},{"kind":"path","absolute":"/home/code/rust/watchexec/crates/cli/README.md","filetype":"file"}]}
{"tags":[{"kind":"source","source":"filesystem"},{"kind":"fs","simple":"modify","full":"Modify(Data(Any))"},{"kind":"path","absolute":"/home/code/rust/watchexec/crates/lib/Cargo.toml","filetype":"file"}]}
{"tags":[{"kind":"source","source":"filesystem"},{"kind":"fs","simple":"modify","full":"Modify(Data(Any))"},{"kind":"path","absolute":"/home/code/rust/watchexec/crates/cli/src/args.rs","filetype":"file"}]}
```
Print the time commands take to run:
```console
$ watchexec --timings -- make
[Running: make]
...
[Command was successful, lasted 52.748081074s]
```
## Installation
### Package manager
Watchexec is in many package managers. A full list of [known packages](../../doc/packages.md) is available,
and there may be more out there! Please contribute any you find to the list :)
Common package managers:
- Alpine: `$ apk add watchexec`
- ArchLinux: `$ pacman -S watchexec`
- Nix: `$ nix-shell -p watchexec`
- Debian/Ubuntu via [apt.cli.rs](https://apt.cli.rs): `$ apt install watchexec`
- Homebrew on Mac: `$ brew install watchexec`
- Chocolatey on Windows: `#> choco install watchexec`
### [Binstall](https://github.com/cargo-bins/cargo-binstall)
$ cargo binstall watchexec-cli
### Pre-built binaries
Use the download section on [Github](https://github.com/watchexec/watchexec/releases/latest)
or [the website](https://watchexec.github.io/downloads/) to obtain the package appropriate for your
platform and architecture, extract it, and place it in your `PATH`.
There are also Debian/Ubuntu (DEB) and Fedora/RedHat (RPM) packages.
Checksums and signatures are available.
### Cargo (from source)
Only the latest Rust stable is supported, but older versions may work.
$ cargo install watchexec-cli
## Shell completions
Currently available shell completions:
- bash: `completions/bash` should be installed to `/usr/share/bash-completion/completions/watchexec`
- elvish: `completions/elvish` should be installed to `$XDG_CONFIG_HOME/elvish/completions/`
- fish: `completions/fish` should be installed to `/usr/share/fish/vendor_completions.d/watchexec.fish`
- nu: `completions/nu` should be installed to `$XDG_CONFIG_HOME/nu/completions/`
- powershell: `completions/powershell` should be installed to `$PROFILE/`
- zsh: `completions/zsh` should be installed to `/usr/share/zsh/site-functions/_watchexec`
If not bundled, you can generate completions for your shell with `watchexec --completions <shell>`.
## Manual
There's a manual page at `doc/watchexec.1`. Install it to `/usr/share/man/man1/`.
If not bundled, you can generate a manual page with `watchexec --manual > /path/to/watchexec.1`, or view it inline with `watchexec --manual` (requires `man`).
You can also [read a text version](../../doc/watchexec.1.md).
Note that it is automatically generated from the help text, so it is not as pretty as a carefully hand-written one.
## Advanced builds
These are additional options available with custom builds by setting features:
### PID1
If you're using Watchexec as PID1 (most frequently in containers or namespaces), and it's not doing what you expect, you can create a build with PID1 early logging: `--features pid1-withlog`.
If you don't need PID1 support, or if you're doing something that conflicts with this program's PID1 support, you can disable it with `--no-default-features`.
### Eyra
[Eyra](https://github.com/sunfishcode/eyra) is a system to build Linux programs with no dependency on C code (in the libc path). To build Watchexec like this, use `--features eyra` and a Nightly compiler.
This feature also lets you get early logging into program startup, with `RUST_LOG=trace`.

View file

@ -1,8 +0,0 @@
fn main() {
embed_resource::compile("watchexec-manifest.rc", embed_resource::NONE);
bosion::gather();
if std::env::var("CARGO_FEATURE_EYRA").is_ok() {
println!("cargo:rustc-link-arg=-nostartfiles");
}
}

View file

@ -1,7 +0,0 @@
#!/bin/bash
set -euxo pipefail
watchexec=${WATCHEXEC_BIN:-watchexec}
$watchexec -1 --env FOO=BAR echo '$FOO' | grep BAR

View file

@ -1,7 +0,0 @@
#!/bin/bash
set -euxo pipefail
watchexec=${WATCHEXEC_BIN:-watchexec}
$watchexec -1 -n echo 'foo bar' | grep 'foo bar'

View file

@ -1,7 +0,0 @@
#!/bin/bash
set -euxo pipefail
watchexec=${WATCHEXEC_BIN:-watchexec}
timeout -s9 30s sh -c "sleep 10 | $watchexec --stdin-quit echo"

View file

@ -1,7 +0,0 @@
#!/bin/bash
set -euxo pipefail
watchexec=${WATCHEXEC_BIN:-watchexec}
$watchexec -1 -- echo @trailingargfile

View file

@ -1,26 +0,0 @@
pre-release-commit-message = "release: cli v{{version}}"
tag-prefix = ""
tag-message = "watchexec {{version}}"
pre-release-hook = ["sh", "-c", "cd ../.. && bin/completions && bin/manpage"]
[[pre-release-replacements]]
file = "watchexec.exe.manifest"
search = "^ version=\"[\\d.]+[.]0\""
replace = " version=\"{{version}}.0\""
prerelease = false
max = 1
[[pre-release-replacements]]
file = "../../CITATION.cff"
search = "^version: \"?[\\d.]+(-.+)?\"?"
replace = "version: \"{{version}}\""
prerelease = true
max = 1
[[pre-release-replacements]]
file = "../../CITATION.cff"
search = "^date-released: .+"
replace = "date-released: {{date}}"
prerelease = true
max = 1

View file

@ -1,13 +0,0 @@
#!/bin/bash
set -euo pipefail
export WATCHEXEC_BIN=$(realpath ${WATCHEXEC_BIN:-$(which watchexec)})
cd "$(dirname "${BASH_SOURCE[0]}")/integration"
for test in *.sh; do
echo
echo
echo "======= Testing $test ======="
./$test
done

File diff suppressed because it is too large Load diff

View file

@ -1,132 +0,0 @@
use std::{env::var, io::stderr, path::PathBuf};
use clap::{ArgAction, Parser, ValueHint};
use miette::{bail, Result};
use tokio::fs::metadata;
use tracing::{info, warn};
use tracing_appender::{non_blocking, non_blocking::WorkerGuard, rolling};
#[derive(Debug, Clone, Parser)]
pub struct LoggingArgs {
/// Set diagnostic log level
///
/// This enables diagnostic logging, which is useful for investigating bugs or gaining more
/// insight into faulty filters or "missing" events. Use multiple times to increase verbosity.
///
/// Goes up to '-vvvv'. When submitting bug reports, default to a '-vvv' log level.
///
/// You may want to use with '--log-file' to avoid polluting your terminal.
///
/// Setting $RUST_LOG also works, and takes precedence, but is not recommended. However, using
/// $RUST_LOG is the only way to get logs from before these options are parsed.
#[arg(
long,
short,
help_heading = super::OPTSET_DEBUGGING,
action = ArgAction::Count,
default_value = "0",
num_args = 0,
)]
pub verbose: u8,
/// Write diagnostic logs to a file
///
/// This writes diagnostic logs to a file, instead of the terminal, in JSON format. If a log
/// level was not already specified, this will set it to '-vvv'.
///
/// If a path is not provided, the default is the working directory. Note that with
/// '--ignore-nothing', the write events to the log will likely get picked up by Watchexec,
/// causing a loop; prefer setting a path outside of the watched directory.
///
/// If the path provided is a directory, a file will be created in that directory. The file name
/// will be the current date and time, in the format 'watchexec.YYYY-MM-DDTHH-MM-SSZ.log'.
#[arg(
long,
help_heading = super::OPTSET_DEBUGGING,
num_args = 0..=1,
default_missing_value = ".",
value_hint = ValueHint::AnyPath,
value_name = "PATH",
)]
pub log_file: Option<PathBuf>,
}
pub fn preargs() -> bool {
let mut log_on = false;
#[cfg(feature = "dev-console")]
match console_subscriber::try_init() {
Ok(_) => {
warn!("dev-console enabled");
log_on = true;
}
Err(e) => {
eprintln!("Failed to initialise tokio console, falling back to normal logging\n{e}")
}
}
if !log_on && var("RUST_LOG").is_ok() {
match tracing_subscriber::fmt::try_init() {
Ok(()) => {
warn!(RUST_LOG=%var("RUST_LOG").unwrap(), "logging configured from RUST_LOG");
log_on = true;
}
Err(e) => eprintln!("Failed to initialise logging with RUST_LOG, falling back\n{e}"),
}
}
log_on
}
pub async fn postargs(args: &LoggingArgs) -> Result<Option<WorkerGuard>> {
if args.verbose == 0 {
return Ok(None);
}
let (log_writer, guard) = if let Some(file) = &args.log_file {
let is_dir = metadata(&file).await.map_or(false, |info| info.is_dir());
let (dir, filename) = if is_dir {
(
file.to_owned(),
PathBuf::from(format!(
"watchexec.{}.log",
chrono::Utc::now().format("%Y-%m-%dT%H-%M-%SZ")
)),
)
} else if let (Some(parent), Some(file_name)) = (file.parent(), file.file_name()) {
(parent.into(), PathBuf::from(file_name))
} else {
bail!("Failed to determine log file name");
};
non_blocking(rolling::never(dir, filename))
} else {
non_blocking(stderr())
};
let mut builder = tracing_subscriber::fmt().with_env_filter(match args.verbose {
0 => unreachable!("checked by if earlier"),
1 => "warn",
2 => "info",
3 => "debug",
_ => "trace",
});
if args.verbose > 2 {
use tracing_subscriber::fmt::format::FmtSpan;
builder = builder.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE);
}
match if args.log_file.is_some() {
builder.json().with_writer(log_writer).try_init()
} else if args.verbose > 3 {
builder.pretty().with_writer(log_writer).try_init()
} else {
builder.with_writer(log_writer).try_init()
} {
Ok(()) => info!("logging initialised"),
Err(e) => eprintln!("Failed to initialise logging, continuing with none\n{e}"),
}
Ok(Some(guard))
}

View file

@ -1,708 +0,0 @@
use std::{
borrow::Cow,
collections::HashMap,
env::var,
ffi::{OsStr, OsString},
fs::File,
io::{IsTerminal, Write},
process::Stdio,
sync::{
atomic::{AtomicBool, AtomicU8, Ordering},
Arc,
},
time::Duration,
};
use clearscreen::ClearScreen;
use miette::{miette, IntoDiagnostic, Report, Result};
use notify_rust::Notification;
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
use tokio::{process::Command as TokioCommand, time::sleep};
use tracing::{debug, debug_span, error, instrument, trace, trace_span, Instrument};
use watchexec::{
action::ActionHandler,
command::{Command, Program, Shell, SpawnOptions},
error::RuntimeError,
job::{CommandState, Job},
sources::fs::Watcher,
Config, ErrorHook, Id,
};
use watchexec_events::{Event, Keyboard, ProcessEnd, Tag};
use watchexec_signals::Signal;
use crate::{
args::{Args, ClearMode, ColourMode, EmitEvents, OnBusyUpdate, SignalMapping, WrapMode},
state::RotatingTempFile,
};
use crate::{emits::events_to_simple_format, state::State};
#[derive(Clone, Copy, Debug)]
struct OutputFlags {
quiet: bool,
colour: ColorChoice,
timings: bool,
bell: bool,
toast: bool,
}
pub fn make_config(args: &Args, state: &State) -> Result<Config> {
let _span = debug_span!("args-runtime").entered();
let config = Config::default();
config.on_error(|err: ErrorHook| {
if let RuntimeError::IoError {
about: "waiting on process group",
..
} = err.error
{
// "No child processes" and such
// these are often spurious, so condemn them to -v only
error!("{}", err.error);
return;
}
if cfg!(debug_assertions) {
eprintln!("[[{:?}]]", err.error);
}
eprintln!("[[Error (not fatal)]]\n{}", Report::new(err.error));
});
config.pathset(args.paths.clone());
config.throttle(args.debounce.0);
config.keyboard_events(args.stdin_quit);
if let Some(interval) = args.poll {
config.file_watcher(Watcher::Poll(interval.0));
}
let once = args.once;
let clear = args.screen_clear;
let emit_events_to = args.emit_events_to;
let emit_file = state.emit_file.clone();
if args.only_emit_events {
config.on_action(move |mut action| {
// if we got a terminate or interrupt signal, quit
if action
.signals()
.any(|sig| sig == Signal::Terminate || sig == Signal::Interrupt)
{
// no need to be graceful as there's no commands
action.quit();
return action;
}
// clear the screen before printing events
if let Some(mode) = clear {
match mode {
ClearMode::Clear => {
clearscreen::clear().ok();
}
ClearMode::Reset => {
reset_screen();
}
}
}
match emit_events_to {
EmitEvents::Stdio => {
println!(
"{}",
events_to_simple_format(action.events.as_ref()).unwrap_or_default()
);
}
EmitEvents::JsonStdio => {
for event in action.events.iter().filter(|e| !e.is_empty()) {
println!("{}", serde_json::to_string(event).unwrap_or_default());
}
}
other => unreachable!(
"emit_events_to should have been validated earlier: {:?}",
other
),
}
action
});
return Ok(config);
}
let delay_run = args.delay_run.map(|ts| ts.0);
let on_busy = args.on_busy_update;
let stdin_quit = args.stdin_quit;
let signal = args.signal;
let stop_signal = args.stop_signal;
let stop_timeout = args.stop_timeout.0;
let print_events = args.print_events;
let outflags = OutputFlags {
quiet: args.quiet,
colour: match args.color {
ColourMode::Auto if !std::io::stdin().is_terminal() => ColorChoice::Never,
ColourMode::Auto => ColorChoice::Auto,
ColourMode::Always => ColorChoice::Always,
ColourMode::Never => ColorChoice::Never,
},
timings: args.timings,
bell: args.bell,
toast: args.notify,
};
let workdir = Arc::new(args.workdir.clone());
let mut add_envs = HashMap::new();
for pair in &args.env {
if let Some((k, v)) = pair.split_once('=') {
add_envs.insert(k.to_owned(), OsString::from(v));
} else {
return Err(miette!("{pair} is not in key=value format"));
}
}
debug!(
?add_envs,
"additional environment variables to add to command"
);
let id = Id::default();
let command = interpret_command_args(args)?;
let signal_map: Arc<HashMap<Signal, Option<Signal>>> = Arc::new(
args.signal_map
.iter()
.copied()
.map(|SignalMapping { from, to }| (from, to))
.collect(),
);
let queued = Arc::new(AtomicBool::new(false));
let quit_again = Arc::new(AtomicU8::new(0));
config.on_action_async(move |mut action| {
let add_envs = add_envs.clone();
let command = command.clone();
let emit_file = emit_file.clone();
let queued = queued.clone();
let quit_again = quit_again.clone();
let signal_map = signal_map.clone();
let workdir = workdir.clone();
Box::new(
async move {
trace!(events=?action.events, "handling action");
let add_envs = add_envs.clone();
let command = command.clone();
let emit_file = emit_file.clone();
let queued = queued.clone();
let quit_again = quit_again.clone();
let signal_map = signal_map.clone();
let workdir = workdir.clone();
trace!("set spawn hook for workdir and environment variables");
let job = action.get_or_create_job(id, move || command.clone());
let events = action.events.clone();
job.set_spawn_hook(move |command, _| {
let add_envs = add_envs.clone();
let emit_file = emit_file.clone();
let events = events.clone();
if let Some(ref workdir) = workdir.as_ref() {
debug!(?workdir, "set command workdir");
command.command_mut().current_dir(workdir);
}
emit_events_to_command(
command.command_mut(),
events,
emit_file,
emit_events_to,
add_envs,
);
});
let show_events = {
let events = action.events.clone();
move || {
if print_events {
trace!("print events to stderr");
for (n, event) in events.iter().enumerate() {
eprintln!("[EVENT {n}] {event}");
}
}
}
};
let clear_screen = {
let events = action.events.clone();
move || {
if let Some(mode) = clear {
match mode {
ClearMode::Clear => {
clearscreen::clear().ok();
debug!("cleared screen");
}
ClearMode::Reset => {
reset_screen();
debug!("hard-reset screen");
}
}
}
// re-show events after clearing
if print_events {
trace!("print events to stderr");
for (n, event) in events.iter().enumerate() {
eprintln!("[EVENT {n}] {event}");
}
}
}
};
let quit = |mut action: ActionHandler| {
match quit_again.fetch_add(1, Ordering::Relaxed) {
0 => {
eprintln!("[Waiting {stop_timeout:?} for processes to exit before stopping...]");
// eprintln!("[Waiting {stop_timeout:?} for processes to exit before stopping... Ctrl-C again to exit faster]");
// see TODO in action/worker.rs
action.quit_gracefully(
stop_signal.unwrap_or(Signal::Terminate),
stop_timeout,
);
}
1 => {
action.quit_gracefully(Signal::ForceStop, Duration::ZERO);
}
_ => {
action.quit();
}
}
action
};
if once {
debug!("debug mode: run once and quit");
show_events();
if let Some(delay) = delay_run {
job.run_async(move |_| {
Box::new(async move {
sleep(delay).await;
})
});
}
// this blocks the event loop, but also this is a debug feature so i don't care
job.start().await;
job.to_wait().await;
return quit(action);
}
let is_keyboard_eof = action
.events
.iter()
.any(|e| e.tags.contains(&Tag::Keyboard(Keyboard::Eof)));
if stdin_quit && is_keyboard_eof {
debug!("keyboard EOF, quit");
show_events();
return quit(action);
}
let signals: Vec<Signal> = action.signals().collect();
trace!(?signals, "received some signals");
// if we got a terminate or interrupt signal and they're not mapped, quit
if (signals.contains(&Signal::Terminate)
&& !signal_map.contains_key(&Signal::Terminate))
|| (signals.contains(&Signal::Interrupt)
&& !signal_map.contains_key(&Signal::Interrupt))
{
debug!("unmapped terminate or interrupt signal, quit");
show_events();
return quit(action);
}
// pass all other signals on
for signal in signals {
match signal_map.get(&signal) {
Some(Some(mapped)) => {
debug!(?signal, ?mapped, "passing mapped signal");
job.signal(*mapped);
}
Some(None) => {
debug!(?signal, "discarding signal");
}
None => {
debug!(?signal, "passing signal on");
job.signal(signal);
}
}
}
// only filesystem events below here (or empty synthetic events)
if action.paths().next().is_none() && !action.events.iter().any(|e| e.is_empty()) {
debug!("no filesystem or synthetic events, skip without doing more");
show_events();
return action;
}
show_events();
if let Some(delay) = delay_run {
trace!("delaying run by sleeping inside the job");
job.run_async(move |_| {
Box::new(async move {
sleep(delay).await;
})
});
}
trace!("querying job state via run_async");
job.run_async({
let job = job.clone();
move |context| {
let job = job.clone();
let is_running = matches!(context.current, CommandState::Running { .. });
Box::new(async move {
let innerjob = job.clone();
if is_running {
trace!(?on_busy, "job is running, decide what to do");
match on_busy {
OnBusyUpdate::DoNothing => {}
OnBusyUpdate::Signal => {
job.signal(if cfg!(windows) {
Signal::ForceStop
} else {
stop_signal.or(signal).unwrap_or(Signal::Terminate)
});
}
OnBusyUpdate::Restart if cfg!(windows) => {
job.restart();
job.run(move |context| {
clear_screen();
setup_process(
innerjob.clone(),
context.command.clone(),
outflags,
)
});
}
OnBusyUpdate::Restart => {
job.restart_with_signal(
stop_signal.unwrap_or(Signal::Terminate),
stop_timeout,
);
job.run(move |context| {
clear_screen();
setup_process(
innerjob.clone(),
context.command.clone(),
outflags,
)
});
}
OnBusyUpdate::Queue => {
let job = job.clone();
let already_queued =
queued.fetch_or(true, Ordering::SeqCst);
if already_queued {
debug!("next start is already queued, do nothing");
} else {
debug!("queueing next start of job");
tokio::spawn({
let queued = queued.clone();
async move {
trace!("waiting for job to finish");
job.to_wait().await;
trace!("job finished, starting queued");
job.start();
job.run(move |context| {
clear_screen();
setup_process(
innerjob.clone(),
context.command.clone(),
outflags,
)
})
.await;
trace!("resetting queued state");
queued.store(false, Ordering::SeqCst);
}
});
}
}
}
} else {
trace!("job is not running, start it");
job.start();
job.run(move |context| {
clear_screen();
setup_process(
innerjob.clone(),
context.command.clone(),
outflags,
)
});
}
})
}
});
action
}
.instrument(trace_span!("action handler")),
)
});
Ok(config)
}
#[instrument(level = "debug")]
fn interpret_command_args(args: &Args) -> Result<Arc<Command>> {
let mut cmd = args.command.clone();
if cmd.is_empty() {
panic!("(clap) Bug: command is not present");
}
let shell = if args.no_shell {
None
} else {
let shell = args.shell.clone().or_else(|| var("SHELL").ok());
match shell
.as_deref()
.or_else(|| {
if cfg!(not(windows)) {
Some("sh")
} else if var("POWERSHELL_DISTRIBUTION_CHANNEL").is_ok()
&& (which::which("pwsh").is_ok() || which::which("pwsh.exe").is_ok())
{
trace!("detected pwsh");
Some("pwsh")
} else if var("PSModulePath").is_ok()
&& (which::which("powershell").is_ok()
|| which::which("powershell.exe").is_ok())
{
trace!("detected powershell");
Some("powershell")
} else {
Some("cmd")
}
})
.or(Some("default"))
{
Some("") => return Err(RuntimeError::CommandShellEmptyShell).into_diagnostic(),
Some("none") | None => None,
#[cfg(windows)]
Some("cmd") | Some("cmd.exe") | Some("CMD") | Some("CMD.EXE") => Some(Shell::cmd()),
Some(other) => {
let sh = other.split_ascii_whitespace().collect::<Vec<_>>();
// UNWRAP: checked by Some("")
#[allow(clippy::unwrap_used)]
let (shprog, shopts) = sh.split_first().unwrap();
Some(Shell {
prog: shprog.into(),
options: shopts.iter().map(|s| (*s).to_string()).collect(),
program_option: Some(Cow::Borrowed(OsStr::new("-c"))),
})
}
}
};
let program = if let Some(shell) = shell {
Program::Shell {
shell,
command: cmd.join(" "),
args: Vec::new(),
}
} else {
Program::Exec {
prog: cmd.remove(0).into(),
args: cmd,
}
};
Ok(Arc::new(Command {
program,
options: SpawnOptions {
grouped: matches!(args.wrap_process, WrapMode::Group),
session: matches!(args.wrap_process, WrapMode::Session),
..Default::default()
},
}))
}
#[instrument(level = "trace")]
fn setup_process(job: Job, command: Arc<Command>, outflags: OutputFlags) {
if outflags.toast {
Notification::new()
.summary("Watchexec: change detected")
.body(&format!("Running {command}"))
.show()
.map_or_else(
|err| {
eprintln!("[[Failed to send desktop notification: {err}]]");
},
drop,
);
}
if !outflags.quiet {
let mut stderr = StandardStream::stderr(outflags.colour);
stderr.reset().ok();
stderr
.set_color(ColorSpec::new().set_fg(Some(Color::Green)))
.ok();
writeln!(&mut stderr, "[Running: {command}]").ok();
stderr.reset().ok();
}
tokio::spawn(async move {
job.to_wait().await;
job.run(move |context| end_of_process(context.current, outflags));
});
}
#[instrument(level = "trace")]
fn end_of_process(state: &CommandState, outflags: OutputFlags) {
let CommandState::Finished {
status,
started,
finished,
} = state
else {
return;
};
let duration = *finished - *started;
let timing = if outflags.timings {
format!(", lasted {duration:?}")
} else {
String::new()
};
let (msg, fg) = match status {
ProcessEnd::ExitError(code) => (format!("Command exited with {code}{timing}"), Color::Red),
ProcessEnd::ExitSignal(sig) => {
(format!("Command killed by {sig:?}{timing}"), Color::Magenta)
}
ProcessEnd::ExitStop(sig) => (format!("Command stopped by {sig:?}{timing}"), Color::Blue),
ProcessEnd::Continued => (format!("Command continued{timing}"), Color::Cyan),
ProcessEnd::Exception(ex) => (
format!("Command ended by exception {ex:#x}{timing}"),
Color::Yellow,
),
ProcessEnd::Success => (format!("Command was successful{timing}"), Color::Green),
};
if outflags.toast {
Notification::new()
.summary("Watchexec: command ended")
.body(&msg)
.show()
.map_or_else(
|err| {
eprintln!("[[Failed to send desktop notification: {err}]]");
},
drop,
);
}
if !outflags.quiet {
let mut stderr = StandardStream::stderr(outflags.colour);
stderr.reset().ok();
stderr.set_color(ColorSpec::new().set_fg(Some(fg))).ok();
writeln!(&mut stderr, "[{msg}]").ok();
stderr.reset().ok();
}
if outflags.bell {
let mut stdout = std::io::stdout();
stdout.write_all(b"\x07").ok();
stdout.flush().ok();
}
}
#[instrument(level = "trace")]
fn emit_events_to_command(
command: &mut TokioCommand,
events: Arc<[Event]>,
emit_file: RotatingTempFile,
emit_events_to: EmitEvents,
mut add_envs: HashMap<String, OsString>,
) {
use crate::emits::*;
let mut stdin = None;
match emit_events_to {
EmitEvents::Environment => {
add_envs.extend(emits_to_environment(&events));
}
EmitEvents::Stdio => match emits_to_file(&emit_file, &events)
.and_then(|path| File::open(path).into_diagnostic())
{
Ok(file) => {
stdin.replace(Stdio::from(file));
}
Err(err) => {
error!("Failed to write events to stdin, continuing without it: {err}");
}
},
EmitEvents::File => match emits_to_file(&emit_file, &events) {
Ok(path) => {
add_envs.insert("WATCHEXEC_EVENTS_FILE".into(), path.into());
}
Err(err) => {
error!("Failed to write WATCHEXEC_EVENTS_FILE, continuing without it: {err}");
}
},
EmitEvents::JsonStdio => match emits_to_json_file(&emit_file, &events)
.and_then(|path| File::open(path).into_diagnostic())
{
Ok(file) => {
stdin.replace(Stdio::from(file));
}
Err(err) => {
error!("Failed to write events to stdin, continuing without it: {err}");
}
},
EmitEvents::JsonFile => match emits_to_json_file(&emit_file, &events) {
Ok(path) => {
add_envs.insert("WATCHEXEC_EVENTS_FILE".into(), path.into());
}
Err(err) => {
error!("Failed to write WATCHEXEC_EVENTS_FILE, continuing without it: {err}");
}
},
EmitEvents::None => {}
}
for (k, v) in add_envs {
debug!(?k, ?v, "inserting environment variable");
command.env(k, v);
}
if let Some(stdin) = stdin {
debug!("set command stdin");
command.stdin(stdin);
}
}
pub(crate) fn reset_screen() {
for cs in [
ClearScreen::WindowsCooked,
ClearScreen::WindowsVt,
ClearScreen::VtLeaveAlt,
ClearScreen::VtWellDone,
ClearScreen::default(),
] {
cs.clear().ok();
}
}

View file

@ -1,222 +0,0 @@
use std::{
collections::HashSet,
path::{Path, PathBuf},
};
use ignore_files::{IgnoreFile, IgnoreFilesFromOriginArgs};
use miette::{miette, IntoDiagnostic, Result};
use project_origins::ProjectType;
use tokio::fs::canonicalize;
use tracing::{debug, info, warn};
use watchexec::paths::common_prefix;
use crate::args::Args;
pub async fn project_origin(args: &Args) -> Result<PathBuf> {
let project_origin = if let Some(origin) = &args.project_origin {
debug!(?origin, "project origin override");
canonicalize(origin).await.into_diagnostic()?
} else {
let homedir = match dirs::home_dir() {
None => None,
Some(dir) => Some(canonicalize(dir).await.into_diagnostic()?),
};
debug!(?homedir, "home directory");
let homedir_requested = homedir.as_ref().map_or(false, |home| {
args.paths
.binary_search_by_key(home, |w| PathBuf::from(w.clone()))
.is_ok()
});
debug!(
?homedir_requested,
"resolved whether the homedir is explicitly requested"
);
let mut origins = HashSet::new();
for path in &args.paths {
origins.extend(project_origins::origins(path).await);
}
match (homedir, homedir_requested) {
(Some(ref dir), false) if origins.contains(dir) => {
debug!("removing homedir from origins");
origins.remove(dir);
}
_ => {}
}
if origins.is_empty() {
debug!("no origins, using current directory");
origins.insert(args.workdir.clone().unwrap());
}
debug!(?origins, "resolved all project origins");
// This canonicalize is probably redundant
canonicalize(
common_prefix(&origins)
.ok_or_else(|| miette!("no common prefix, but this should never fail"))?,
)
.await
.into_diagnostic()?
};
debug!(?project_origin, "resolved common/project origin");
Ok(project_origin)
}
pub async fn vcs_types(origin: &Path) -> Vec<ProjectType> {
let vcs_types = project_origins::types(origin)
.await
.into_iter()
.filter(|pt| pt.is_vcs())
.collect::<Vec<_>>();
info!(?vcs_types, "effective vcs types");
vcs_types
}
pub async fn ignores(args: &Args, vcs_types: &[ProjectType]) -> Result<Vec<IgnoreFile>> {
let origin = args.project_origin.clone().unwrap();
let mut skip_git_global_excludes = false;
let mut ignores = if args.no_project_ignore {
Vec::new()
} else {
let ignore_files = args.ignore_files.iter().map(|path| {
if path.is_absolute() {
path.into()
} else {
origin.join(path)
}
});
let (mut ignores, errors) = ignore_files::from_origin(
IgnoreFilesFromOriginArgs::new_unchecked(
&origin,
args.paths.iter().map(PathBuf::from),
ignore_files,
)
.canonicalise()
.await
.into_diagnostic()?,
)
.await;
for err in errors {
warn!("while discovering project-local ignore files: {}", err);
}
debug!(?ignores, "discovered ignore files from project origin");
if !vcs_types.is_empty() {
ignores = ignores
.into_iter()
.filter(|ig| match ig.applies_to {
Some(pt) if pt.is_vcs() => vcs_types.contains(&pt),
_ => true,
})
.inspect(|ig| {
if let IgnoreFile {
applies_to: Some(ProjectType::Git),
applies_in: None,
..
} = ig
{
warn!("project git config overrides the global excludes");
skip_git_global_excludes = true;
}
})
.collect::<Vec<_>>();
debug!(?ignores, "filtered ignores to only those for project vcs");
}
ignores
};
let global_ignores = if args.no_global_ignore {
Vec::new()
} else {
let (mut global_ignores, errors) = ignore_files::from_environment(Some("watchexec")).await;
for err in errors {
warn!("while discovering global ignore files: {}", err);
}
debug!(?global_ignores, "discovered ignore files from environment");
if skip_git_global_excludes {
global_ignores = global_ignores
.into_iter()
.filter(|gig| {
!matches!(
gig,
IgnoreFile {
applies_to: Some(ProjectType::Git),
applies_in: None,
..
}
)
})
.collect::<Vec<_>>();
debug!(
?global_ignores,
"filtered global ignores to exclude global git ignores"
);
}
global_ignores
};
ignores.extend(global_ignores.into_iter().filter(|ig| match ig.applies_to {
Some(pt) if pt.is_vcs() => vcs_types.contains(&pt),
_ => true,
}));
debug!(
?ignores,
?vcs_types,
"combined and applied overall vcs filter over ignores"
);
ignores.extend(args.ignore_files.iter().map(|ig| IgnoreFile {
applies_to: None,
applies_in: None,
path: ig.clone(),
}));
debug!(
?ignores,
?args.ignore_files,
"combined with ignore files from command line / env"
);
if args.no_project_ignore {
ignores = ignores
.into_iter()
.filter(|ig| {
!ig.applies_in
.as_ref()
.map_or(false, |p| p.starts_with(&origin))
})
.collect::<Vec<_>>();
debug!(
?ignores,
"filtered ignores to exclude project-local ignores"
);
}
if args.no_global_ignore {
ignores = ignores
.into_iter()
.filter(|ig| ig.applies_in.is_some())
.collect::<Vec<_>>();
debug!(?ignores, "filtered ignores to exclude global ignores");
}
if args.no_vcs_ignore {
ignores = ignores
.into_iter()
.filter(|ig| ig.applies_to.is_none())
.collect::<Vec<_>>();
debug!(?ignores, "filtered ignores to exclude VCS-specific ignores");
}
info!(files=?ignores.iter().map(|ig| ig.path.as_path()).collect::<Vec<_>>(), "found some ignores");
Ok(ignores)
}

View file

@ -1,71 +0,0 @@
use std::{ffi::OsString, fmt::Write, path::PathBuf};
use miette::{IntoDiagnostic, Result};
use watchexec::paths::summarise_events_to_env;
use watchexec_events::{filekind::FileEventKind, Event, Tag};
use crate::state::RotatingTempFile;
pub fn emits_to_environment(events: &[Event]) -> impl Iterator<Item = (String, OsString)> {
summarise_events_to_env(events.iter())
.into_iter()
.map(|(k, v)| (format!("WATCHEXEC_{k}_PATH"), v))
}
pub fn events_to_simple_format(events: &[Event]) -> Result<String> {
let mut buf = String::new();
for event in events {
let feks = event
.tags
.iter()
.filter_map(|tag| match tag {
Tag::FileEventKind(kind) => Some(kind),
_ => None,
})
.collect::<Vec<_>>();
for path in event.paths().map(|(p, _)| p) {
if feks.is_empty() {
writeln!(&mut buf, "other:{}", path.to_string_lossy()).into_diagnostic()?;
continue;
}
for fek in &feks {
writeln!(
&mut buf,
"{}:{}",
match fek {
FileEventKind::Any | FileEventKind::Other => "other",
FileEventKind::Access(_) => "access",
FileEventKind::Create(_) => "create",
FileEventKind::Modify(_) => "modify",
FileEventKind::Remove(_) => "remove",
},
path.to_string_lossy()
)
.into_diagnostic()?;
}
}
}
Ok(buf)
}
pub fn emits_to_file(target: &RotatingTempFile, events: &[Event]) -> Result<PathBuf> {
target.rotate()?;
target.write(events_to_simple_format(events)?.as_bytes())?;
Ok(target.path())
}
pub fn emits_to_json_file(target: &RotatingTempFile, events: &[Event]) -> Result<PathBuf> {
target.rotate()?;
for event in events {
if event.is_empty() {
continue;
}
target.write(&serde_json::to_vec(event).into_diagnostic()?)?;
target.write(b"\n")?;
}
Ok(target.path())
}

View file

@ -1,188 +0,0 @@
use std::{
ffi::OsString,
path::{Path, PathBuf, MAIN_SEPARATOR},
sync::Arc,
};
use miette::{IntoDiagnostic, Result};
use tokio::io::{AsyncBufReadExt, BufReader};
use tracing::{info, trace, trace_span};
use watchexec::{error::RuntimeError, filter::Filterer};
use watchexec_events::{
filekind::{FileEventKind, ModifyKind},
Event, Priority, Tag,
};
use watchexec_filterer_globset::GlobsetFilterer;
use crate::args::{Args, FsEvent};
pub(crate) mod parse;
mod proglib;
mod progs;
mod syncval;
/// A custom filterer that combines the library's Globset filterer and a switch for --no-meta
#[derive(Debug)]
pub struct WatchexecFilterer {
inner: GlobsetFilterer,
fs_events: Vec<FsEvent>,
progs: Option<progs::FilterProgs>,
}
impl Filterer for WatchexecFilterer {
#[tracing::instrument(level = "trace", skip(self))]
fn check_event(&self, event: &Event, priority: Priority) -> Result<bool, RuntimeError> {
for tag in &event.tags {
if let Tag::FileEventKind(fek) = tag {
let normalised = match fek {
FileEventKind::Access(_) => FsEvent::Access,
FileEventKind::Modify(ModifyKind::Name(_)) => FsEvent::Rename,
FileEventKind::Modify(ModifyKind::Metadata(_)) => FsEvent::Metadata,
FileEventKind::Modify(_) => FsEvent::Modify,
FileEventKind::Create(_) => FsEvent::Create,
FileEventKind::Remove(_) => FsEvent::Remove,
_ => continue,
};
trace!(allowed=?self.fs_events, this=?normalised, "check against fs event filter");
if !self.fs_events.contains(&normalised) {
return Ok(false);
}
}
}
trace!("check against original event");
if !self.inner.check_event(event, priority)? {
return Ok(false);
}
if let Some(progs) = &self.progs {
trace!("check against program filters");
if !progs.check(event)? {
return Ok(false);
}
}
Ok(true)
}
}
impl WatchexecFilterer {
/// Create a new filterer from the given arguments
pub async fn new(args: &Args) -> Result<Arc<Self>> {
let project_origin = args.project_origin.clone().unwrap();
let workdir = args.workdir.clone().unwrap();
let ignore_files = if args.no_discover_ignore {
Vec::new()
} else {
let vcs_types = crate::dirs::vcs_types(&project_origin).await;
crate::dirs::ignores(args, &vcs_types).await?
};
let mut ignores = Vec::new();
if !args.no_default_ignore {
ignores.extend([
(format!("**{MAIN_SEPARATOR}.DS_Store"), None),
(String::from("watchexec.*.log"), None),
(String::from("*.py[co]"), None),
(String::from("#*#"), None),
(String::from(".#*"), None),
(String::from(".*.kate-swp"), None),
(String::from(".*.sw?"), None),
(String::from(".*.sw?x"), None),
(format!("**{MAIN_SEPARATOR}.bzr{MAIN_SEPARATOR}**"), None),
(format!("**{MAIN_SEPARATOR}_darcs{MAIN_SEPARATOR}**"), None),
(
format!("**{MAIN_SEPARATOR}.fossil-settings{MAIN_SEPARATOR}**"),
None,
),
(format!("**{MAIN_SEPARATOR}.git{MAIN_SEPARATOR}**"), None),
(format!("**{MAIN_SEPARATOR}.hg{MAIN_SEPARATOR}**"), None),
(format!("**{MAIN_SEPARATOR}.pijul{MAIN_SEPARATOR}**"), None),
(format!("**{MAIN_SEPARATOR}.svn{MAIN_SEPARATOR}**"), None),
]);
}
let whitelist = args
.paths
.iter()
.map(|p| p.into())
.filter(|p: &PathBuf| p.is_file());
let mut filters = args
.filter_patterns
.iter()
.map(|f| (f.to_owned(), Some(workdir.clone())))
.collect::<Vec<_>>();
for filter_file in &args.filter_files {
filters.extend(read_filter_file(filter_file).await?);
}
ignores.extend(
args.ignore_patterns
.iter()
.map(|f| (f.to_owned(), Some(workdir.clone()))),
);
let exts = args
.filter_extensions
.iter()
.map(|e| OsString::from(e.strip_prefix('.').unwrap_or(e)));
info!("initialising Globset filterer");
Ok(Arc::new(Self {
inner: GlobsetFilterer::new(
project_origin,
filters,
ignores,
whitelist,
ignore_files,
exts,
)
.await
.into_diagnostic()?,
fs_events: args.filter_fs_events.clone(),
progs: if args.filter_programs_parsed.is_empty() {
None
} else {
Some(progs::FilterProgs::new(args)?)
},
}))
}
}
async fn read_filter_file(path: &Path) -> Result<Vec<(String, Option<PathBuf>)>> {
let _span = trace_span!("loading filter file", ?path).entered();
let file = tokio::fs::File::open(path).await.into_diagnostic()?;
let metadata_len = file
.metadata()
.await
.map(|m| usize::try_from(m.len()))
.unwrap_or(Ok(0))
.into_diagnostic()?;
let filter_capacity = if metadata_len == 0 {
0
} else {
metadata_len / 20
};
let mut filters = Vec::with_capacity(filter_capacity);
let reader = BufReader::new(file);
let mut lines = reader.lines();
while let Some(line) = lines.next_line().await.into_diagnostic()? {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
trace!(?line, "adding filter line");
filters.push((line.to_owned(), Some(path.to_owned())));
}
Ok(filters)
}

View file

@ -1,17 +0,0 @@
use miette::{miette, Result};
pub fn parse_filter_program((n, prog): (usize, String)) -> Result<jaq_syn::Main> {
let parser = jaq_parse::main();
let (main, errs) = jaq_parse::parse(&prog, parser);
if !errs.is_empty() {
let errs = errs
.into_iter()
.map(|err| err.to_string())
.collect::<Vec<_>>()
.join("\n");
return Err(miette!("{}", errs).wrap_err(format!("failed to load filter program #{}", n)));
}
main.ok_or_else(|| miette!("failed to load filter program #{} (no reason given)", n))
}

View file

@ -1,27 +0,0 @@
use jaq_interpret::ParseCtx;
use miette::Result;
use tracing::debug;
mod file;
mod hash;
mod kv;
mod macros;
mod output;
pub fn jaq_lib() -> Result<ParseCtx> {
let mut jaq = ParseCtx::new(Vec::new());
debug!("loading jaq core library");
jaq.insert_natives(jaq_core::core());
debug!("loading jaq std library");
jaq.insert_defs(jaq_std::std());
debug!("loading jaq watchexec library");
file::load(&mut jaq);
hash::load(&mut jaq);
kv::load(&mut jaq);
output::load(&mut jaq);
Ok(jaq)
}

View file

@ -1,173 +0,0 @@
use std::{
fs::{metadata, File, FileType, Metadata},
io::{BufReader, Read},
iter::once,
time::{SystemTime, UNIX_EPOCH},
};
use jaq_interpret::{Error, Native, ParseCtx, Val};
use serde_json::{json, Value};
use tracing::{debug, error, trace};
use super::macros::*;
pub fn load(jaq: &mut ParseCtx) {
trace!("jaq: add file_read filter");
jaq.insert_native(
"file_read".into(),
1,
Native::new({
move |args, (ctx, val)| {
let path = match &val {
Val::Str(v) => v.to_string(),
_ => return_err!(Err(Error::str("expected string (path) but got {val:?}"))),
};
let bytes = match int_arg!(args, 0, ctx, &val) {
Ok(v) => v,
Err(e) => return_err!(Err(e)),
};
Box::new(once(Ok(match File::open(&path) {
Ok(file) => {
let buf_reader = BufReader::new(file);
let mut limited = buf_reader.take(bytes);
let mut buffer = String::with_capacity(bytes as _);
match limited.read_to_string(&mut buffer) {
Ok(read) => {
debug!("jaq: read {read} bytes from {path:?}");
Val::Str(buffer.into())
}
Err(err) => {
error!("jaq: failed to read from {path:?}: {err:?}");
Val::Null
}
}
}
Err(err) => {
error!("jaq: failed to open file {path:?}: {err:?}");
Val::Null
}
})))
}
}),
);
trace!("jaq: add file_meta filter");
jaq.insert_native(
"file_meta".into(),
0,
Native::new({
move |_, (_, val)| {
let path = match &val {
Val::Str(v) => v.to_string(),
_ => return_err!(Err(Error::str("expected string (path) but got {val:?}"))),
};
Box::new(once(Ok(match metadata(&path) {
Ok(meta) => Val::from(json_meta(meta)),
Err(err) => {
error!("jaq: failed to open {path:?}: {err:?}");
Val::Null
}
})))
}
}),
);
trace!("jaq: add file_size filter");
jaq.insert_native(
"file_size".into(),
0,
Native::new({
move |_, (_, val)| {
let path = match &val {
Val::Str(v) => v.to_string(),
_ => return_err!(Err(Error::str("expected string (path) but got {val:?}"))),
};
Box::new(once(Ok(match metadata(&path) {
Ok(meta) => Val::Int(meta.len() as _),
Err(err) => {
error!("jaq: failed to open {path:?}: {err:?}");
Val::Null
}
})))
}
}),
);
}
fn json_meta(meta: Metadata) -> Value {
let perms = meta.permissions();
let mut val = json!({
"type": filetype_str(meta.file_type()),
"size": meta.len(),
"modified": fs_time(meta.modified()),
"accessed": fs_time(meta.accessed()),
"created": fs_time(meta.created()),
"dir": meta.is_dir(),
"file": meta.is_file(),
"symlink": meta.is_symlink(),
"readonly": perms.readonly(),
});
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let map = val.as_object_mut().unwrap();
map.insert(
"mode".to_string(),
Value::String(format!("{:o}", perms.mode())),
);
map.insert("mode_byte".to_string(), Value::from(perms.mode()));
map.insert(
"executable".to_string(),
Value::Bool(perms.mode() & 0o111 != 0),
);
}
val
}
fn filetype_str(filetype: FileType) -> &'static str {
#[cfg(unix)]
{
use std::os::unix::fs::FileTypeExt;
if filetype.is_char_device() {
return "char";
} else if filetype.is_block_device() {
return "block";
} else if filetype.is_fifo() {
return "fifo";
} else if filetype.is_socket() {
return "socket";
}
}
#[cfg(windows)]
{
use std::os::windows::fs::FileTypeExt;
if filetype.is_symlink_dir() {
return "symdir";
} else if filetype.is_symlink_file() {
return "symfile";
}
}
if filetype.is_dir() {
"dir"
} else if filetype.is_file() {
"file"
} else if filetype.is_symlink() {
"symlink"
} else {
"unknown"
}
}
fn fs_time(time: std::io::Result<SystemTime>) -> Option<u64> {
time.ok()
.and_then(|time| time.duration_since(UNIX_EPOCH).ok())
.map(|dur| dur.as_secs())
}

View file

@ -1,62 +0,0 @@
use std::{fs::File, io::Read, iter::once};
use jaq_interpret::{Error, Native, ParseCtx, Val};
use tracing::{debug, error, trace};
use super::macros::*;
pub fn load(jaq: &mut ParseCtx) {
trace!("jaq: add hash filter");
jaq.insert_native(
"hash".into(),
0,
Native::new({
move |_, (_, val)| {
let string = match &val {
Val::Str(v) => v.to_string(),
_ => return_err!(Err(Error::str("expected string but got {val:?}"))),
};
Box::new(once(Ok(Val::Str(
blake3::hash(string.as_bytes()).to_hex().to_string().into(),
))))
}
}),
);
trace!("jaq: add file_hash filter");
jaq.insert_native(
"file_hash".into(),
0,
Native::new({
move |_, (_, val)| {
let path = match &val {
Val::Str(v) => v.to_string(),
_ => return_err!(Err(Error::str("expected string but got {val:?}"))),
};
Box::new(once(Ok(match File::open(&path) {
Ok(mut file) => {
const BUFFER_SIZE: usize = 1024 * 1024;
let mut hasher = blake3::Hasher::new();
let mut buf = vec![0; BUFFER_SIZE];
while let Ok(bytes) = file.read(&mut buf) {
debug!("jaq: read {bytes} bytes from {path:?}");
if bytes == 0 {
break;
}
hasher.update(&buf[..bytes]);
buf = vec![0; BUFFER_SIZE];
}
Val::Str(hasher.finalize().to_hex().to_string().into())
}
Err(err) => {
error!("jaq: failed to open file {path:?}: {err:?}");
Val::Null
}
})))
}
}),
);
}

View file

@ -1,69 +0,0 @@
use std::{iter::once, sync::Arc};
use dashmap::DashMap;
use jaq_interpret::{Error, Native, ParseCtx, Val};
use once_cell::sync::OnceCell;
use tracing::trace;
use crate::filterer::syncval::SyncVal;
use super::macros::*;
type KvStore = Arc<DashMap<String, SyncVal>>;
fn kv_store() -> KvStore {
static KV_STORE: OnceCell<KvStore> = OnceCell::new();
KV_STORE.get_or_init(|| KvStore::default()).clone()
}
pub fn load(jaq: &mut ParseCtx) {
trace!("jaq: add kv_clear filter");
jaq.insert_native(
"kv_clear".into(),
0,
Native::new({
move |_, (_, val)| {
let kv = kv_store();
kv.clear();
Box::new(once(Ok(val)))
}
}),
);
trace!("jaq: add kv_store filter");
jaq.insert_native(
"kv_store".into(),
1,
Native::new({
move |args, (ctx, val)| {
let kv = kv_store();
let key = match string_arg!(args, 0, ctx, val) {
Ok(v) => v,
Err(e) => return_err!(Err(e)),
};
kv.insert(key, (&val).into());
Box::new(once(Ok(val)))
}
}),
);
trace!("jaq: add kv_fetch filter");
jaq.insert_native(
"kv_fetch".into(),
1,
Native::new({
move |args, (ctx, val)| {
let kv = kv_store();
let key = match string_arg!(args, 0, ctx, val) {
Ok(v) => v,
Err(e) => return_err!(Err(e)),
};
Box::new(once(Ok(kv
.get(&key)
.map(|val| val.value().into())
.unwrap_or(Val::Null))))
}
}),
);
}

View file

@ -1,30 +0,0 @@
macro_rules! return_err {
($err:expr) => {
return Box::new(once($err))
};
}
pub(crate) use return_err;
macro_rules! string_arg {
($args:expr, $n:expr, $ctx:expr, $val:expr) => {
match ::jaq_interpret::FilterT::run($args.get($n), ($ctx.clone(), $val.clone())).next() {
Some(Ok(Val::Str(v))) => Ok(v.to_string()),
Some(Ok(val)) => Err(Error::str(format!("expected string but got {val:?}"))),
Some(Err(e)) => Err(e),
None => Err(Error::str("value expected but none found")),
}
};
}
pub(crate) use string_arg;
macro_rules! int_arg {
($args:expr, $n:expr, $ctx:expr, $val:expr) => {
match ::jaq_interpret::FilterT::run($args.get($n), ($ctx.clone(), $val.clone())).next() {
Some(Ok(Val::Int(v))) => Ok(v as _),
Some(Ok(val)) => Err(Error::str(format!("expected int but got {val:?}"))),
Some(Err(e)) => Err(e),
None => Err(Error::str("value expected but none found")),
}
};
}
pub(crate) use int_arg;

View file

@ -1,83 +0,0 @@
use std::iter::once;
use jaq_interpret::{Error, Native, ParseCtx, Val};
use tracing::{debug, error, info, trace, warn};
use super::macros::*;
macro_rules! log_action {
($level:expr, $val:expr) => {
match $level.to_ascii_lowercase().as_str() {
"trace" => trace!("jaq: {}", $val),
"debug" => debug!("jaq: {}", $val),
"info" => info!("jaq: {}", $val),
"warn" => warn!("jaq: {}", $val),
"error" => error!("jaq: {}", $val),
_ => return_err!(Err(Error::str("invalid log level"))),
}
};
}
pub fn load(jaq: &mut ParseCtx) {
trace!("jaq: add log filter");
jaq.insert_native(
"log".into(),
1,
Native::with_update(
|args, (ctx, val)| {
let level = match string_arg!(args, 0, ctx, val) {
Ok(v) => v,
Err(e) => return_err!(Err(e)),
};
log_action!(level, val);
// passthrough
Box::new(once(Ok(val)))
},
|args, (ctx, val), _| {
let level = match string_arg!(args, 0, ctx, val) {
Ok(v) => v,
Err(e) => return_err!(Err(e)),
};
log_action!(level, val);
// passthrough
Box::new(once(Ok(val)))
},
),
);
trace!("jaq: add printout filter");
jaq.insert_native(
"printout".into(),
0,
Native::with_update(
|_, (_, val)| {
println!("{}", val);
Box::new(once(Ok(val)))
},
|_, (_, val), _| {
println!("{}", val);
Box::new(once(Ok(val)))
},
),
);
trace!("jaq: add printerr filter");
jaq.insert_native(
"printerr".into(),
0,
Native::with_update(
|_, (_, val)| {
eprintln!("{}", val);
Box::new(once(Ok(val)))
},
|_, (_, val), _| {
eprintln!("{}", val);
Box::new(once(Ok(val)))
},
),
);
}

View file

@ -1,143 +0,0 @@
use std::{iter::empty, marker::PhantomData};
use jaq_interpret::{Ctx, FilterT, RcIter, Val};
use miette::miette;
use tokio::{
sync::{mpsc, oneshot},
task::{block_in_place, spawn_blocking},
};
use tracing::{error, trace, warn};
use watchexec::error::RuntimeError;
use watchexec_events::Event;
use crate::args::Args;
const BUFFER: usize = 128;
#[derive(Debug)]
pub struct FilterProgs {
channel: Requester<Event, bool>,
}
#[derive(Debug, Clone)]
pub struct Requester<S, R> {
sender: mpsc::Sender<(S, oneshot::Sender<R>)>,
_receiver: PhantomData<R>,
}
impl<S, R> Requester<S, R>
where
S: Send + Sync,
R: Send + Sync,
{
pub fn new(capacity: usize) -> (Self, mpsc::Receiver<(S, oneshot::Sender<R>)>) {
let (sender, receiver) = mpsc::channel(capacity);
(
Self {
sender,
_receiver: PhantomData,
},
receiver,
)
}
pub fn call(&self, value: S) -> Result<R, RuntimeError> {
// FIXME: this should really be async with a timeout, but that needs filtering in general
// to be async, which should be done at some point
block_in_place(|| {
let (sender, receiver) = oneshot::channel();
self.sender.blocking_send((value, sender)).map_err(|err| {
RuntimeError::External(miette!("filter progs internal channel: {}", err).into())
})?;
receiver
.blocking_recv()
.map_err(|err| RuntimeError::External(Box::new(err)))
})
}
}
impl FilterProgs {
pub fn check(&self, event: &Event) -> Result<bool, RuntimeError> {
self.channel.call(event.clone())
}
pub fn new(args: &Args) -> miette::Result<Self> {
let progs = args.filter_programs_parsed.clone();
eprintln!(
"EXPERIMENTAL: filter programs are unstable and may change/vanish without notice"
);
let (requester, mut receiver) = Requester::<Event, bool>::new(BUFFER);
let task =
spawn_blocking(move || {
'chan: while let Some((event, sender)) = receiver.blocking_recv() {
let val = serde_json::to_value(&event)
.map_err(|err| miette!("failed to serialize event: {}", err))
.map(Val::from)?;
for (n, prog) in progs.iter().enumerate() {
trace!(?n, "trying filter program");
let mut jaq = super::proglib::jaq_lib()?;
let filter = jaq.compile(prog.clone());
if !jaq.errs.is_empty() {
for (error, span) in jaq.errs {
error!(%error, "failed to compile filter program #{n}@{}:{}", span.start, span.end);
}
continue;
}
let inputs = RcIter::new(empty());
let mut results = filter.run((Ctx::new([], &inputs), val.clone()));
if let Some(res) = results.next() {
match res {
Ok(Val::Bool(false)) => {
trace!(
?n,
verdict = false,
"filter program finished; fail so stopping there"
);
sender
.send(false)
.unwrap_or_else(|_| warn!("failed to send filter result"));
continue 'chan;
}
Ok(Val::Bool(true)) => {
trace!(
?n,
verdict = true,
"filter program finished; pass so trying next"
);
continue;
}
Ok(val) => {
error!(?n, ?val, "filter program returned non-boolean, ignoring and trying next");
continue;
}
Err(err) => {
error!(?n, error=%err, "filter program failed, so trying next");
continue;
}
}
}
}
trace!("all filters failed, sending pass as default");
sender
.send(true)
.unwrap_or_else(|_| warn!("failed to send filter result"));
}
Ok(()) as miette::Result<()>
});
tokio::spawn(async {
match task.await {
Ok(Ok(())) => {}
Ok(Err(err)) => error!("filter progs task failed: {}", err),
Err(err) => error!("filter progs task panicked: {}", err),
}
});
Ok(Self { channel: requester })
}
}

View file

@ -1,71 +0,0 @@
/// Jaq's [Val](jaq_interpret::Val) uses Rc, but we want to use in Sync contexts. UGH!
use std::{rc::Rc, sync::Arc};
use indexmap::IndexMap;
use jaq_interpret::Val;
#[derive(Clone, Debug)]
pub enum SyncVal {
Null,
Bool(bool),
Int(isize),
Float(f64),
Num(Arc<str>),
Str(Arc<str>),
Arr(Arc<[SyncVal]>),
Obj(Arc<IndexMap<Arc<str>, SyncVal>>),
}
impl From<&Val> for SyncVal {
fn from(val: &Val) -> Self {
match val {
Val::Null => Self::Null,
Val::Bool(b) => Self::Bool(*b),
Val::Int(i) => Self::Int(*i),
Val::Float(f) => Self::Float(*f),
Val::Num(s) => Self::Num(s.to_string().into()),
Val::Str(s) => Self::Str(s.to_string().into()),
Val::Arr(a) => Self::Arr({
let mut arr = Vec::with_capacity(a.len());
for v in a.iter() {
arr.push(v.into());
}
arr.into()
}),
Val::Obj(m) => Self::Obj(Arc::new({
let mut map = IndexMap::new();
for (k, v) in m.iter() {
map.insert(k.to_string().into(), v.into());
}
map
})),
}
}
}
impl From<&SyncVal> for Val {
fn from(val: &SyncVal) -> Self {
match val {
SyncVal::Null => Self::Null,
SyncVal::Bool(b) => Self::Bool(*b),
SyncVal::Int(i) => Self::Int(*i),
SyncVal::Float(f) => Self::Float(*f),
SyncVal::Num(s) => Self::Num(s.to_string().into()),
SyncVal::Str(s) => Self::Str(s.to_string().into()),
SyncVal::Arr(a) => Self::Arr({
let mut arr = Vec::with_capacity(a.len());
for v in a.iter() {
arr.push(v.into());
}
arr.into()
}),
SyncVal::Obj(m) => Self::Obj(Rc::new({
let mut map: IndexMap<_, _, ahash::RandomState> = Default::default();
for (k, v) in m.iter() {
map.insert(k.to_string().into(), v.into());
}
map
})),
}
}
}

View file

@ -1,128 +0,0 @@
#![deny(rust_2018_idioms)]
#![allow(clippy::missing_const_for_fn, clippy::future_not_send)]
use std::{io::Write, process::Stdio};
use args::{Args, ShellCompletion};
use clap::CommandFactory;
use clap_complete::{Generator, Shell};
use clap_mangen::Man;
use is_terminal::IsTerminal;
use miette::{IntoDiagnostic, Result};
use tokio::{io::AsyncWriteExt, process::Command};
use tracing::{debug, info};
use watchexec::Watchexec;
use watchexec_events::{Event, Priority};
use crate::filterer::WatchexecFilterer;
pub mod args;
mod config;
mod dirs;
mod emits;
mod filterer;
mod state;
async fn run_watchexec(args: Args) -> Result<()> {
info!(version=%env!("CARGO_PKG_VERSION"), "constructing Watchexec from CLI");
let state = state::State::default();
let config = config::make_config(&args, &state)?;
config.filterer(WatchexecFilterer::new(&args).await?);
info!("initialising Watchexec runtime");
let wx = Watchexec::with_config(config)?;
if !args.postpone {
debug!("kicking off with empty event");
wx.send_event(Event::default(), Priority::Urgent).await?;
}
info!("running main loop");
wx.main().await.into_diagnostic()??;
if matches!(args.screen_clear, Some(args::ClearMode::Reset)) {
config::reset_screen();
}
info!("done with main loop");
Ok(())
}
async fn run_manpage(_args: Args) -> Result<()> {
info!(version=%env!("CARGO_PKG_VERSION"), "constructing manpage");
let man = Man::new(Args::command().long_version(None));
let mut buffer: Vec<u8> = Default::default();
man.render(&mut buffer).into_diagnostic()?;
if std::io::stdout().is_terminal() && which::which("man").is_ok() {
let mut child = Command::new("man")
.arg("-l")
.arg("-")
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.kill_on_drop(true)
.spawn()
.into_diagnostic()?;
child
.stdin
.as_mut()
.unwrap()
.write_all(&buffer)
.await
.into_diagnostic()?;
if let Some(code) = child
.wait()
.await
.into_diagnostic()?
.code()
.and_then(|code| if code == 0 { None } else { Some(code) })
{
return Err(miette::miette!("Exited with status code {}", code));
}
} else {
std::io::stdout()
.lock()
.write_all(&buffer)
.into_diagnostic()?;
}
Ok(())
}
#[allow(clippy::unused_async)]
async fn run_completions(shell: ShellCompletion) -> Result<()> {
fn generate(generator: impl Generator) {
let mut cmd = Args::command();
clap_complete::generate(generator, &mut cmd, "watchexec", &mut std::io::stdout());
}
info!(version=%env!("CARGO_PKG_VERSION"), "constructing completions");
match shell {
ShellCompletion::Bash => generate(Shell::Bash),
ShellCompletion::Elvish => generate(Shell::Elvish),
ShellCompletion::Fish => generate(Shell::Fish),
ShellCompletion::Nu => generate(clap_complete_nushell::Nushell),
ShellCompletion::Powershell => generate(Shell::PowerShell),
ShellCompletion::Zsh => generate(Shell::Zsh),
}
Ok(())
}
pub async fn run() -> Result<()> {
let (args, _log_guard) = args::get_args().await?;
if args.manual {
run_manpage(args).await
} else if let Some(shell) = args.completions {
run_completions(shell).await
} else {
run_watchexec(args).await
}
}

View file

@ -1,22 +0,0 @@
#[cfg(feature = "eyra")]
extern crate eyra;
use miette::IntoDiagnostic;
#[cfg(target_env = "musl")]
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
fn main() -> miette::Result<()> {
#[cfg(feature = "pid1")]
pid1::Pid1Settings::new()
.enable_log(cfg!(feature = "pid1-withlog"))
.launch()
.into_diagnostic()?;
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async { watchexec_cli::run().await })
}

View file

@ -1,48 +0,0 @@
use std::{
env::var_os,
io::Write,
path::PathBuf,
sync::{Arc, Mutex},
};
use miette::{IntoDiagnostic, Result};
use tempfile::NamedTempFile;
#[derive(Clone, Debug, Default)]
pub struct State {
pub emit_file: RotatingTempFile,
}
#[derive(Clone, Debug, Default)]
pub struct RotatingTempFile(Arc<Mutex<Option<NamedTempFile>>>);
impl RotatingTempFile {
pub fn rotate(&self) -> Result<()> {
// implicitly drops the old file
*self.0.lock().unwrap() = Some(
if let Some(dir) = var_os("WATCHEXEC_TMPDIR") {
NamedTempFile::new_in(dir)
} else {
NamedTempFile::new()
}
.into_diagnostic()?,
);
Ok(())
}
pub fn write(&self, data: &[u8]) -> Result<()> {
if let Some(file) = self.0.lock().unwrap().as_mut() {
file.write_all(data).into_diagnostic()?;
}
Ok(())
}
pub fn path(&self) -> PathBuf {
if let Some(file) = self.0.lock().unwrap().as_ref() {
file.path().to_owned()
} else {
PathBuf::new()
}
}
}

View file

@ -1,121 +0,0 @@
use std::path::PathBuf;
use std::{fs, sync::OnceLock};
use miette::{Context, IntoDiagnostic, Result};
use rand::Rng;
static PLACEHOLDER_DATA: OnceLock<String> = OnceLock::new();
fn get_placeholder_data() -> &'static str {
PLACEHOLDER_DATA.get_or_init(|| "PLACEHOLDER\n".repeat(500))
}
/// The amount of nesting that will be used for generated files
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum GeneratedFileNesting {
/// Only one level of files
Flat,
/// Random, up to a certiain maximum
RandomToMax(usize),
}
/// Configuration for creating testing subfolders
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TestSubfolderConfiguration {
/// The amount of nesting that will be used when folders are generated
pub(crate) nesting: GeneratedFileNesting,
/// Number of files the folder should contain
pub(crate) file_count: usize,
/// Subfolder name
pub(crate) name: String,
}
/// Options for generating test files
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(crate) struct GenerateTestFilesArgs {
/// The path where the files should be generated
/// if None, the current working directory will be used.
pub(crate) path: Option<PathBuf>,
/// Configurations for subfolders to generate
pub(crate) subfolder_configs: Vec<TestSubfolderConfiguration>,
}
/// Generate test files
///
/// This returns the same number of paths that were requested via subfolder_configs.
pub(crate) fn generate_test_files(args: GenerateTestFilesArgs) -> Result<Vec<PathBuf>> {
// Use or create a temporary directory for the test files
let tmpdir = if let Some(p) = args.path {
p
} else {
tempfile::tempdir()
.into_diagnostic()
.wrap_err("failed to build tempdir")?
.into_path()
};
let mut paths = vec![tmpdir.clone()];
// Generate subfolders matching each config
for subfolder_config in args.subfolder_configs.iter() {
// Create the subfolder path
let subfolder_path = tmpdir.join(&subfolder_config.name);
fs::create_dir(&subfolder_path)
.into_diagnostic()
.wrap_err(format!(
"failed to create path for dir [{}]",
subfolder_path.display()
))?;
paths.push(subfolder_path.clone());
// Fill the subfolder with files
match subfolder_config.nesting {
GeneratedFileNesting::Flat => {
for idx in 0..subfolder_config.file_count {
// Write stub file contents
fs::write(
subfolder_path.join(format!("stub-file-{idx}")),
get_placeholder_data(),
)
.into_diagnostic()
.wrap_err(format!(
"failed to write temporary file in subfolder {} @ idx {idx}",
subfolder_path.display()
))?;
}
}
GeneratedFileNesting::RandomToMax(max_depth) => {
let mut generator = rand::thread_rng();
for idx in 0..subfolder_config.file_count {
// Build a randomized path up to max depth
let mut generated_path = subfolder_path.clone();
let depth = generator.gen_range(0..max_depth);
for _ in 0..depth {
generated_path.push("stub-dir");
}
// Create the path
fs::create_dir_all(&generated_path)
.into_diagnostic()
.wrap_err(format!(
"failed to create randomly generated path [{}]",
generated_path.display()
))?;
// Write stub file contents @ the new randomized path
fs::write(
generated_path.join(format!("stub-file-{idx}")),
get_placeholder_data(),
)
.into_diagnostic()
.wrap_err(format!(
"failed to write temporary file in subfolder {} @ idx {idx}",
subfolder_path.display()
))?;
}
}
}
}
Ok(paths)
}

View file

@ -1,146 +0,0 @@
use std::{
path::{Path, PathBuf},
process::Stdio,
time::Duration,
};
use miette::{IntoDiagnostic, Result, WrapErr};
use tokio::{process::Command, time::Instant};
use tracing_test::traced_test;
use uuid::Uuid;
mod common;
use common::{generate_test_files, GenerateTestFilesArgs};
use crate::common::{GeneratedFileNesting, TestSubfolderConfiguration};
/// Directory name that will be sued for the dir that *should* be watched
const WATCH_DIR_NAME: &str = "watch";
/// The token that watch will echo every time a match is found
const WATCH_TOKEN: &str = "updated";
/// Ensure that watchexec runtime does not increase with the
/// number of *ignored* files in a given folder
///
/// This test creates two separate folders, one small and the other large
///
/// Each folder has two subfolders:
/// - a shallow one to be watched, with a few files of single depth (20 files)
/// - a deep one to be ignored, with many files at varying depths (small case 200 files, large case 200,000 files)
///
/// watchexec, when executed on *either* folder should *not* experience a more
/// than 10x degradation in performance, because the vast majority of the files
/// are supposed to be ignored to begin with.
///
/// When running the CLI on the root folders, it should *not* take a long time to start de
#[tokio::test]
#[traced_test]
async fn e2e_ignore_many_files_200_000() -> Result<()> {
// Create a tempfile so that drop will clean it up
let small_test_dir = tempfile::tempdir()
.into_diagnostic()
.wrap_err("failed to create tempdir for test use")?;
// Determine the watchexec bin to use & build arguments
let wexec_bin = std::env::var("TEST_WATCHEXEC_BIN").unwrap_or(
option_env!("CARGO_BIN_EXE_watchexec")
.map(std::string::ToString::to_string)
.unwrap_or("watchexec".into()),
);
let token = format!("{WATCH_TOKEN}-{}", Uuid::new_v4());
let args: Vec<String> = vec![
"-1".into(), // exit as soon as watch completes
"--watch".into(),
WATCH_DIR_NAME.into(),
"echo".into(),
token.clone(),
];
// Generate a small directory of files containing dirs that *will* and will *not* be watched
let [ref root_dir_path, _, _] = generate_test_files(GenerateTestFilesArgs {
path: Some(PathBuf::from(small_test_dir.path())),
subfolder_configs: vec![
// Shallow folder will have a small number of files and won't be watched
TestSubfolderConfiguration {
name: "watch".into(),
nesting: GeneratedFileNesting::Flat,
file_count: 5,
},
// Deep folder will have *many* amll files and will be watched
TestSubfolderConfiguration {
name: "unrelated".into(),
nesting: GeneratedFileNesting::RandomToMax(42),
file_count: 200,
},
],
})?[..] else {
panic!("unexpected number of paths returned from generate_test_files");
};
// Get the number of elapsed
let small_elapsed = run_watchexec_cmd(&wexec_bin, root_dir_path, args.clone()).await?;
// Create a tempfile so that drop will clean it up
let large_test_dir = tempfile::tempdir()
.into_diagnostic()
.wrap_err("failed to create tempdir for test use")?;
// Generate a *large* directory of files
let [ref root_dir_path, _, _] = generate_test_files(GenerateTestFilesArgs {
path: Some(PathBuf::from(large_test_dir.path())),
subfolder_configs: vec![
// Shallow folder will have a small number of files and won't be watched
TestSubfolderConfiguration {
name: "watch".into(),
nesting: GeneratedFileNesting::Flat,
file_count: 5,
},
// Deep folder will have *many* amll files and will be watched
TestSubfolderConfiguration {
name: "unrelated".into(),
nesting: GeneratedFileNesting::RandomToMax(42),
file_count: 200_000,
},
],
})?[..] else {
panic!("unexpected number of paths returned from generate_test_files");
};
// Get the number of elapsed
let large_elapsed = run_watchexec_cmd(&wexec_bin, root_dir_path, args.clone()).await?;
// We expect the ignores to not impact watchexec startup time at all
// whether there are 200 files in there or 200k
assert!(
large_elapsed < small_elapsed * 10,
"200k ignore folder ({:?}) took more than 10x more time ({:?}) than 200 ignore folder ({:?})",
large_elapsed,
small_elapsed * 10,
small_elapsed,
);
Ok(())
}
/// Run a watchexec command once
async fn run_watchexec_cmd(
wexec_bin: impl AsRef<str>,
dir: impl AsRef<Path>,
args: impl Into<Vec<String>>,
) -> Result<Duration> {
// Build the subprocess command
let mut cmd = Command::new(wexec_bin.as_ref());
cmd.args(args.into());
cmd.current_dir(dir);
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let start = Instant::now();
cmd.kill_on_drop(true)
.output()
.await
.into_diagnostic()
.wrap_err("fixed")?;
Ok(start.elapsed())
}

View file

@ -1,2 +0,0 @@
#define RT_MANIFEST 24
1 RT_MANIFEST "watchexec.exe.manifest"

View file

@ -1,41 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
type="win32"
name="Watchexec.Cli.watchexec"
version="2.2.0.0"
/>
<trustInfo>
<security>
<!--
UAC settings:
- app should run at same integrity level as calling process
- app does not need to manipulate windows belonging to
higher-integrity-level processes
-->
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10, 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings xmlns:ws="http://schemas.microsoft.com/SMI/2020/WindowsSettings">
<ws:longPathAware xmlns:ws="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</ws:longPathAware>
<ws:activeCodePage xmlns:ws="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</ws:activeCodePage>
<ws:heapType xmlns:ws="http://schemas.microsoft.com/SMI/2020/WindowsSettings">SegmentHeap</ws:heapType>
</windowsSettings>
</application>
</assembly>

Some files were not shown because too many files have changed in this diff Show more