├── .github ├── package └── workflows │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Justfile ├── LICENSE.md ├── README.md └── src └── main.rs /.github/package: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euxo pipefail 4 | 5 | 6 | VERSION=${REF#"refs/tags/"} 7 | DIST=`pwd`/dist 8 | # This var can be modified if need be. 9 | BIN=${GITHUB_REPOSITORY##*/} 10 | 11 | echo "Packaging $BIN $VERSION for $TARGET..." 12 | 13 | test -f Cargo.lock || cargo generate-lockfile 14 | 15 | echo "Building $BIN..." 16 | RUSTFLAGS="--deny warnings --codegen target-feature=+crt-static $TARGET_RUSTFLAGS" \ 17 | cargo build --bin $BIN --target $TARGET --release 18 | EXECUTABLE=target/$TARGET/release/$BIN 19 | 20 | if [[ $OS == windows-2016 ]]; then 21 | EXECUTABLE=$EXECUTABLE.exe 22 | fi 23 | 24 | echo "Copying release files..." 25 | mkdir dist 26 | cp \ 27 | $EXECUTABLE \ 28 | Cargo.lock \ 29 | Cargo.toml \ 30 | LICENSE.md \ 31 | README.md \ 32 | $DIST 33 | 34 | cd $DIST 35 | echo "Creating release archive..." 36 | case $OS in 37 | ubuntu-latest | macos-latest) 38 | ARCHIVE=$DIST/$BIN-$VERSION-$TARGET.tar.gz 39 | tar czf $ARCHIVE * 40 | echo "::set-output name=archive::$ARCHIVE" 41 | ;; 42 | windows-2016) 43 | ARCHIVE=$DIST/$BIN-$VERSION-$TARGET.zip 44 | 7z a $ARCHIVE * 45 | echo "::set-output name=archive::`pwd -W`/$BIN-$VERSION-$TARGET.zip" 46 | ;; 47 | esac 48 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | defaults: 9 | run: 10 | shell: bash 11 | 12 | jobs: 13 | all: 14 | name: All 15 | 16 | strategy: 17 | matrix: 18 | target: 19 | - aarch64-unknown-linux-musl 20 | - armv7-unknown-linux-musleabihf 21 | - x86_64-apple-darwin 22 | - x86_64-pc-windows-msvc 23 | - x86_64-unknown-linux-musl 24 | include: 25 | - target: aarch64-unknown-linux-musl 26 | os: ubuntu-latest 27 | native: false 28 | target_rustflags: '--codegen linker=aarch64-linux-gnu-gcc' 29 | - target: armv7-unknown-linux-musleabihf 30 | os: ubuntu-latest 31 | native: false 32 | target_rustflags: '--codegen linker=arm-linux-gnueabihf-gcc' 33 | - target: x86_64-apple-darwin 34 | os: macos-latest 35 | native: true 36 | target_rustflags: '' 37 | - target: x86_64-pc-windows-msvc 38 | os: windows-2016 39 | native: true 40 | target_rustflags: '' 41 | - target: x86_64-unknown-linux-musl 42 | os: ubuntu-latest 43 | native: true 44 | target_rustflags: '' 45 | 46 | runs-on: ${{matrix.os}} 47 | 48 | steps: 49 | - uses: actions/checkout@v2 50 | 51 | - name: Install Rust Toolchain Components 52 | uses: actions-rs/toolchain@v1 53 | with: 54 | override: true 55 | target: ${{ matrix.target }} 56 | toolchain: stable 57 | 58 | - uses: Swatinem/rust-cache@v1 59 | 60 | - name: Install AArch64 Toolchain 61 | if: ${{ matrix.target == 'aarch64-unknown-linux-musl' }} 62 | run: | 63 | sudo apt-get update 64 | sudo apt-get install gcc-aarch64-linux-gnu 65 | - name: Install ARM7 Toolchain 66 | if: ${{ matrix.target == 'armv7-unknown-linux-musleabihf' }} 67 | run: | 68 | sudo apt-get update 69 | sudo apt-get install gcc-arm-linux-gnueabihf 70 | - name: Test 71 | if: matrix.native 72 | run: cargo test --all --target ${{ matrix.target }} 73 | 74 | - name: Package 75 | id: package 76 | env: 77 | TARGET: ${{ matrix.target }} 78 | REF: ${{ github.ref }} 79 | OS: ${{ matrix.os }} 80 | TARGET_RUSTFLAGS: ${{ matrix.target_rustflags }} 81 | run: ./.github/package 82 | shell: bash 83 | 84 | - name: Publish Archive 85 | uses: softprops/action-gh-release@v0.1.5 86 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 87 | with: 88 | draft: false 89 | files: ${{ steps.package.outputs.archive }} 90 | prerelease: ${{ contains(github.ref_name, '-') }} ## looking for a bare semver number rather than a tagged one like 1.0.0-beta.2 91 | env: 92 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 93 | 94 | - name: Publish Changelog 95 | uses: softprops/action-gh-release@v0.1.5 96 | if: >- 97 | ${{ 98 | startsWith(github.ref, 'refs/tags/') 99 | && matrix.target == 'x86_64-unknown-linux-musl' 100 | }} 101 | with: 102 | draft: false 103 | files: CHANGELOG.md 104 | prerelease: ${{ contains(github.ref_name, '-') }} ## looking for a bare semver number rather than a tagged one like 1.0.0-beta.2 105 | env: 106 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 107 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | 12 | defaults: 13 | run: 14 | shell: bash 15 | 16 | 17 | jobs: 18 | run-test: 19 | permissions: 20 | contents: read 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions-rs/toolchain@v1 26 | with: 27 | toolchain: stable 28 | - uses: Swatinem/rust-cache@v1 29 | - run: cargo install just 30 | - run: just test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | .env* 4 | !.env.example -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "atty" 7 | version = "0.2.14" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 10 | dependencies = [ 11 | "hermit-abi", 12 | "libc", 13 | "winapi", 14 | ] 15 | 16 | [[package]] 17 | name = "autocfg" 18 | version = "1.0.1" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 21 | 22 | [[package]] 23 | name = "bitflags" 24 | version = "1.3.2" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 27 | 28 | [[package]] 29 | name = "cfg-if" 30 | version = "1.0.0" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 33 | 34 | [[package]] 35 | name = "checkexec" 36 | version = "0.2.0" 37 | dependencies = [ 38 | "clap", 39 | "shell-escape", 40 | "tempdir", 41 | "tempfile", 42 | ] 43 | 44 | [[package]] 45 | name = "clap" 46 | version = "3.0.0-rc.4" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "967965e82fc46fee1a88147a7a977a66d615ed5f83eb95b18577b342c08f90ff" 49 | dependencies = [ 50 | "atty", 51 | "bitflags", 52 | "indexmap", 53 | "os_str_bytes", 54 | "strsim", 55 | "termcolor", 56 | "textwrap", 57 | ] 58 | 59 | [[package]] 60 | name = "fuchsia-cprng" 61 | version = "0.1.1" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 64 | 65 | [[package]] 66 | name = "getrandom" 67 | version = "0.2.3" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" 70 | dependencies = [ 71 | "cfg-if", 72 | "libc", 73 | "wasi", 74 | ] 75 | 76 | [[package]] 77 | name = "hashbrown" 78 | version = "0.11.2" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 81 | 82 | [[package]] 83 | name = "hermit-abi" 84 | version = "0.1.19" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 87 | dependencies = [ 88 | "libc", 89 | ] 90 | 91 | [[package]] 92 | name = "indexmap" 93 | version = "1.7.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" 96 | dependencies = [ 97 | "autocfg", 98 | "hashbrown", 99 | ] 100 | 101 | [[package]] 102 | name = "libc" 103 | version = "0.2.112" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" 106 | 107 | [[package]] 108 | name = "memchr" 109 | version = "2.4.1" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 112 | 113 | [[package]] 114 | name = "os_str_bytes" 115 | version = "6.0.0" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" 118 | dependencies = [ 119 | "memchr", 120 | ] 121 | 122 | [[package]] 123 | name = "ppv-lite86" 124 | version = "0.2.15" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" 127 | 128 | [[package]] 129 | name = "rand" 130 | version = "0.4.6" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 133 | dependencies = [ 134 | "fuchsia-cprng", 135 | "libc", 136 | "rand_core 0.3.1", 137 | "rdrand", 138 | "winapi", 139 | ] 140 | 141 | [[package]] 142 | name = "rand" 143 | version = "0.8.4" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" 146 | dependencies = [ 147 | "libc", 148 | "rand_chacha", 149 | "rand_core 0.6.3", 150 | "rand_hc", 151 | ] 152 | 153 | [[package]] 154 | name = "rand_chacha" 155 | version = "0.3.1" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 158 | dependencies = [ 159 | "ppv-lite86", 160 | "rand_core 0.6.3", 161 | ] 162 | 163 | [[package]] 164 | name = "rand_core" 165 | version = "0.3.1" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 168 | dependencies = [ 169 | "rand_core 0.4.2", 170 | ] 171 | 172 | [[package]] 173 | name = "rand_core" 174 | version = "0.4.2" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 177 | 178 | [[package]] 179 | name = "rand_core" 180 | version = "0.6.3" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 183 | dependencies = [ 184 | "getrandom", 185 | ] 186 | 187 | [[package]] 188 | name = "rand_hc" 189 | version = "0.3.1" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" 192 | dependencies = [ 193 | "rand_core 0.6.3", 194 | ] 195 | 196 | [[package]] 197 | name = "rdrand" 198 | version = "0.4.0" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 201 | dependencies = [ 202 | "rand_core 0.3.1", 203 | ] 204 | 205 | [[package]] 206 | name = "redox_syscall" 207 | version = "0.2.10" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" 210 | dependencies = [ 211 | "bitflags", 212 | ] 213 | 214 | [[package]] 215 | name = "remove_dir_all" 216 | version = "0.5.3" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 219 | dependencies = [ 220 | "winapi", 221 | ] 222 | 223 | [[package]] 224 | name = "shell-escape" 225 | version = "0.1.5" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" 228 | 229 | [[package]] 230 | name = "strsim" 231 | version = "0.10.0" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 234 | 235 | [[package]] 236 | name = "tempdir" 237 | version = "0.3.7" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" 240 | dependencies = [ 241 | "rand 0.4.6", 242 | "remove_dir_all", 243 | ] 244 | 245 | [[package]] 246 | name = "tempfile" 247 | version = "3.2.0" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" 250 | dependencies = [ 251 | "cfg-if", 252 | "libc", 253 | "rand 0.8.4", 254 | "redox_syscall", 255 | "remove_dir_all", 256 | "winapi", 257 | ] 258 | 259 | [[package]] 260 | name = "termcolor" 261 | version = "1.1.2" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 264 | dependencies = [ 265 | "winapi-util", 266 | ] 267 | 268 | [[package]] 269 | name = "textwrap" 270 | version = "0.14.2" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" 273 | 274 | [[package]] 275 | name = "wasi" 276 | version = "0.10.2+wasi-snapshot-preview1" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 279 | 280 | [[package]] 281 | name = "winapi" 282 | version = "0.3.9" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 285 | dependencies = [ 286 | "winapi-i686-pc-windows-gnu", 287 | "winapi-x86_64-pc-windows-gnu", 288 | ] 289 | 290 | [[package]] 291 | name = "winapi-i686-pc-windows-gnu" 292 | version = "0.4.0" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 295 | 296 | [[package]] 297 | name = "winapi-util" 298 | version = "0.1.5" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 301 | dependencies = [ 302 | "winapi", 303 | ] 304 | 305 | [[package]] 306 | name = "winapi-x86_64-pc-windows-gnu" 307 | version = "0.4.0" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 310 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "checkexec" 3 | version = "0.2.0" 4 | edition = "2018" 5 | authors = ["Kurt Wolf "] 6 | description = "Conditionally run a command in a CLI (like Make, as a standalone tool)" 7 | license = "MIT" 8 | repository = "https://github.com/kurtbuilds/checkexec" 9 | 10 | # categories are https://crates.io/category_slugs 11 | categories = [ 12 | "development-tools", 13 | "development-tools::build-utils", 14 | "command-line-utilities", 15 | ] 16 | 17 | keywords = [ 18 | "make", 19 | "just", 20 | "makefile", 21 | "task", 22 | "build", 23 | ] 24 | 25 | [dependencies] 26 | clap = { version = "3.0.0-rc.4", features = ["color", "env", "suggestions"]} 27 | shell-escape = "0.1.5" 28 | tempdir = "0.3.7" 29 | 30 | [dev-dependencies] 31 | tempfile = "3.2.0" 32 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | set dotenv-load := false 2 | 3 | help: 4 | @just --list --unsorted 5 | 6 | build: 7 | cargo build 8 | alias b := build 9 | 10 | run *args: 11 | cargo run {{args}} 12 | alias r := run 13 | 14 | release: 15 | cargo build --release 16 | 17 | install: 18 | cargo install --path . 19 | 20 | bootstrap: 21 | cargo install cargo-edit 22 | 23 | test *args: 24 | cargo test {{args}} 25 | 26 | check: 27 | cargo check 28 | alias c := check 29 | 30 | fix: 31 | cargo clippy --fix 32 | 33 | # Bump version. level=major,minor,patch 34 | version level: 35 | git diff-index --exit-code HEAD > /dev/null || ! echo You have untracked changes. Commit your changes before bumping the version. 36 | cargo set-version --bump {{level}} 37 | cargo update # This bumps Cargo.lock 38 | VERSION=$(rg "version = \"([0-9.]+)\"" -or '$1' Cargo.toml | head -n1) && \ 39 | git commit -am "Bump version to $VERSION" && \ 40 | git tag v$VERSION && \ 41 | git push origin v$VERSION 42 | git push 43 | 44 | publish: 45 | cargo publish 46 | 47 | patch: test 48 | just version patch 49 | just publish 50 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © `` `` 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | 5 | GitHub Contributors 6 | 7 | 8 | Stars 9 | 10 | 11 | Build Status 12 | 13 | 14 | Downloads 15 | 16 | 17 | Crates.io 18 | 19 | 20 |

21 | 22 | # Checkexec 23 | 24 | `checkexec` is a tool to conditionally execute commands only when files in a dependency list have been updated. 25 | 26 | This tool provides the behavior of `make` as a standalone executable, where a command is only run if any of its 27 | dependencies have been updated. Like `make`, `checkexec` runs a command only if the modified time of any dependency 28 | is newer than the modified time of the target. 29 | 30 | # Usage 31 | 32 | The arguments are: ` -- `. The `--` is a required separator. 33 | 34 | checkexec build/my-bin src/my-program.c -- cc -o build/my-bin src/my-program.c 35 | 36 | `checkexec` executes the command directly, so shell constructs like '&&' and '||' are not supported. 37 | You can use `/bin/bash -c` as the command, but escaping is tricky. You're likely better off using two invocations of 38 | `checkexec`. 39 | 40 | You can infer the dependency list with `--infer`, where checkexec will inspect the arguments of `` for 41 | accessible paths. `--infer` will fail if no files are found. 42 | 43 | checkexec build/my-bin --infer -- cc -o build/my-bin src/my-program.c 44 | 45 | # Installation 46 | 47 | cargo install checkexec 48 | 49 | # Usage Notes 50 | 51 | `checkexec` is great for when you build files from other files. Instead of relying on 52 | ecosystem-specific tools, you can use `checkexec` as part of any build tool. Here are some examples: 53 | 54 | - You build, resize, or sample images as part of your build command, but don't want to rebuild them unless needed. 55 | - You build C libaries as part of your Python, Rust, Node (or any other) build process. 56 | - You build Sass/Less/SCSS files and don't want to re-build them unnecessarily. 57 | 58 | `checkexec` pairs well with these tools: 59 | 60 | - [`just`](https://github.com/casey/just) fixes numerous problems with `make`, and `checkexec` adds back the 61 | conditional rebuild functionality of `make`. Together, they create a modular and modern build process and 62 | command runner. 63 | - [`watchexec`](https://github.com/watchexec/watchexec) provides live reloading/rebuilding, while `checkexec` 64 | has callable behavior, useful as a build step or on CI. The naming similarity is intentional. 65 | - [`fd`](https://github.com/sharkdp/fd) makes it easy to specify a dependency file list. Example here: 66 | 67 | ```bash 68 | # Only run your command if a rust file has changed. Note cargo does approximately the 69 | # same thing natively, but you can easily tailor this structure to a custom case. 70 | checkexec target/debug/hello $(fd -e rs . src) -- cargo build 71 | ``` 72 | 73 | ### Exit codes 74 | 75 | `checkexec` exit codes behave as you would expect, specifically: 76 | 77 | - 0 (success) if the command is not run (i.e. target is up to date) 78 | - 1 if a provided dependency or the command is not found 79 | - Otherwise, when the command is run, checkexec will pass through the command's exit code. 80 | 81 | # Contributing 82 | 83 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. 84 | Any contributions you make are **greatly appreciated**. 85 | 86 | If you have a suggestion that would make this better, please fork the repo and create a pull request. 87 | You can also simply open an issue with the tag "enhancement". 88 | Don't forget to give the project a star! 89 | 90 |

(back to top)

91 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::fmt::{Display}; 3 | use std::path::{Path}; 4 | use std::process::{exit, Command}; 5 | 6 | use clap::{App, AppSettings, Arg}; 7 | use std::fs; 8 | use shell_escape::escape; 9 | 10 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 11 | 12 | 13 | struct Error { 14 | message: String, 15 | } 16 | 17 | impl std::fmt::Debug for Error { 18 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 19 | write!(f, "{}", self.message) 20 | } 21 | } 22 | 23 | impl std::fmt::Display for Error { 24 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 25 | write!(f, "{}", self.message) 26 | } 27 | } 28 | 29 | impl std::error::Error for Error {} 30 | 31 | macro_rules! err { 32 | ($($arg:tt)*) => { 33 | Error { 34 | message: format!($($arg)*), 35 | } 36 | } 37 | } 38 | 39 | 40 | fn infer_dependencies<'a>(command: &[&'a str]) -> Result, Error> { 41 | let inferred_deps = command.iter() 42 | .filter_map(|s| fs::metadata(s).ok().map(|_| *s)) 43 | .collect::>(); 44 | if inferred_deps.is_empty() { 45 | Err(err!("--infer must find at least one accessible file in command arguments. Command arguments are: {}", 46 | command.iter().map(|s| format!("\"{}\"", s)).collect::>().join(" ") 47 | )) 48 | } else { 49 | Ok(inferred_deps) 50 | } 51 | } 52 | 53 | 54 | fn should_execute + Display>(target: &str, dependencies: &[T]) -> Result { 55 | match fs::metadata(target) { 56 | Ok(meta) => { 57 | let modified = meta.modified().unwrap(); 58 | for dependency in dependencies { 59 | let dep_meta = fs::metadata(&dependency) 60 | .map_err(|_| err!("{}: Could not read file metadata", &dependency))?; 61 | if dep_meta.modified().unwrap() > modified { 62 | return Ok(true); 63 | } 64 | } 65 | Ok(false) 66 | } 67 | Err(_) => Ok(true) 68 | } 69 | } 70 | 71 | 72 | fn main() -> std::result::Result<(), Error> { 73 | let args = App::new("checkexec") 74 | .version(VERSION) 75 | .about("Conditionally run a command (like `make`)") 76 | .setting(AppSettings::ArgRequiredElseHelp) 77 | .setting(AppSettings::TrailingVarArg) 78 | .arg(Arg::new("target") 79 | .help("The file created by this checkexec execution.") 80 | .required(true) 81 | ) 82 | .arg(Arg::new("verbose") 83 | .long("verbose") 84 | .short('v') 85 | .takes_value(false) 86 | ) 87 | .arg(Arg::new("infer") 88 | .long("infer") 89 | .takes_value(false) 90 | .conflicts_with("dependencies") 91 | .help("Infer the dependency list. The inference takes all arguments to the command, filters it for files, and uses that list. \ 92 | --infer causes checkexec to fail if it creates an empty dependency list.") 93 | ) 94 | .arg(Arg::new("dependencies").min_values(0) 95 | .help("The list of files") 96 | ) 97 | .arg(Arg::new("command").min_values(1) 98 | .last(true) 99 | .required(true) 100 | .help("The command to execute if the check passes.") 101 | ) 102 | .get_matches(); 103 | 104 | let verbose = args.is_present("verbose"); 105 | 106 | let target = args.value_of("target").unwrap(); 107 | let command_args = args.values_of("command").unwrap().into_iter().skip(1).collect::>(); 108 | let dependencies = if args.is_present("infer") { 109 | infer_dependencies(&command_args)? 110 | } else { 111 | args.values_of("dependencies").map(|d| d.collect::>()).unwrap_or_default() 112 | } 113 | .iter() 114 | .flat_map(|s| s.split('\n')) 115 | .collect::>(); 116 | 117 | if verbose { 118 | eprintln!("Found {} dependencies:\n{}", dependencies.len(), dependencies.iter().map(|d| escape(Cow::Borrowed(d))).collect::>().join("\n")); 119 | } 120 | 121 | if should_execute(target, &dependencies)? { 122 | let command = args.values_of("command").unwrap().collect::>(); 123 | if verbose { 124 | eprintln!("{} {}", command[0], command.iter().skip(1).map(|s| format!("\"{}\"", s)).collect::>().join(" ")); 125 | } 126 | let output = Command::new(command[0]) 127 | .args(command[1..].iter()) 128 | .status() 129 | .map_err(|_| err!("{}: command not found", command[0]))?; 130 | exit(output.code().unwrap()); 131 | } 132 | 133 | Ok(()) 134 | } 135 | 136 | 137 | #[cfg(test)] 138 | mod test { 139 | use std::io::Write; 140 | use super::*; 141 | use tempfile::{TempDir, tempdir}; 142 | 143 | struct TempFiles { 144 | #[allow(dead_code)] 145 | dir: TempDir, 146 | pub files: Vec, 147 | } 148 | 149 | fn touch(path: &str) -> std::io::Result<()> { 150 | let mut file = fs::File::create(path).unwrap(); 151 | file.write_all(b"") 152 | } 153 | 154 | fn touch_and_untouch(touched: usize, untouched: usize) -> TempFiles { 155 | let tempdir = tempdir().unwrap(); 156 | let dir = tempdir.path(); 157 | let mut files: Vec = Vec::new(); 158 | files.extend((0..touched).map(|i| dir.join(i.to_string()).to_str().unwrap().to_string())); 159 | files.extend((touched..(touched + untouched)).map(|i| dir.join(i.to_string()).to_str().unwrap().to_string())); 160 | for file in files.iter().take(touched) { 161 | touch(file).unwrap(); 162 | // tries to eliminate ties between files. 1ms should be more than enough 163 | // and we dont need a ton of tests for this program where 1ms is noticeable. 164 | // apparently 1ms isn't enough for github actions because ...... reasons? 165 | std::thread::sleep(std::time::Duration::from_millis(10)); 166 | } 167 | TempFiles { 168 | dir: tempdir, 169 | files, 170 | } 171 | } 172 | 173 | #[test] 174 | fn test_infer_dependencies() { 175 | let TempFiles { dir: _dir, files } = touch_and_untouch(3, 0); 176 | let dependencies = infer_dependencies(&["cc", 177 | &files[0], 178 | &files[1]]).unwrap(); 179 | assert_eq!(dependencies, vec![&files[0], &files[1]]); 180 | } 181 | 182 | #[test] 183 | fn test_no_inferred_dependencies_errors() { 184 | let TempFiles { dir: _dir, files } = touch_and_untouch(0, 1); 185 | assert!(infer_dependencies(&["cc", 186 | &files[0]]).is_err()) 187 | } 188 | 189 | #[test] 190 | fn test_should_execute_errors_on_failed_dependency_access() { 191 | let TempFiles { dir: _dir, files } = touch_and_untouch(1, 1); 192 | assert!(should_execute(&files[0], &files[1..]).is_err(), "Should have failed to access file"); 193 | } 194 | 195 | #[test] 196 | fn test_should_execute_target_doesnt_exist() { 197 | let TempFiles { dir: _dir, files } = touch_and_untouch(1, 1); 198 | assert!(should_execute(&files[1], &files[0..1]).unwrap(), "Should execute because target doesn't exist"); 199 | } 200 | 201 | #[test] 202 | fn test_should_not_execute_newer_target() { 203 | let TempFiles { dir: _dir, files } = touch_and_untouch(2, 0); 204 | assert!(!should_execute(&files[1], &files[0..1]).unwrap(), "Should not execute because target is newer"); 205 | } 206 | 207 | #[test] 208 | fn test_should_execute_newer_dependencies() { 209 | let TempFiles { dir: _dir, files } = touch_and_untouch(2, 0); 210 | assert!(should_execute(&files[0], &files[1..]).unwrap()) 211 | } 212 | } 213 | --------------------------------------------------------------------------------