├── .cargo └── config.toml ├── .config ├── semgrep.yaml └── flakebox │ ├── .gitignore │ ├── id │ ├── bin │ └── flakebox-in-each-cargo-workspace │ └── shellHook.sh ├── .envrc ├── .gitignore ├── misc └── git-hooks │ ├── commit-template.txt │ ├── commit-msg │ └── pre-commit ├── rustfmt.toml ├── .rustfmt.toml ├── nix └── lib.nix ├── src ├── root │ ├── dot.rs │ └── dto.rs ├── util.rs ├── test.rs ├── main.rs └── root.rs ├── .github └── workflows │ ├── flakebox-flakehub-publish.yml │ └── flakebox-ci.yml ├── Cargo.toml ├── flake.nix ├── README.md ├── justfile ├── flake.lock └── Cargo.lock /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.config/semgrep.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.config/flakebox/.gitignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.direnv 3 | /result 4 | -------------------------------------------------------------------------------- /misc/git-hooks/commit-template.txt: -------------------------------------------------------------------------------- 1 | 2 | # Explain *why* this change is being made width limit ->| 3 | -------------------------------------------------------------------------------- /.config/flakebox/id: -------------------------------------------------------------------------------- 1 | 49d08c9a7de01cf07832b0988dfec632c4a6d3717ed5619bf20224675001c38de0ecf44683db5fb6eee87e404d0bc388eaf6550748f7bb8c325b383a015340dd 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | group_imports = "StdExternalCrate" 2 | wrap_comments = true 3 | format_code_in_doc_comments = true 4 | imports_granularity = "Module" 5 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | group_imports = "StdExternalCrate" 2 | wrap_comments = true 3 | format_code_in_doc_comments = true 4 | imports_granularity = "Module" 5 | edition = "2021" 6 | -------------------------------------------------------------------------------- /.config/flakebox/bin/flakebox-in-each-cargo-workspace: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Run a given command in every directory that contains cargo workspace 3 | # Right now it just scans for `Cargo.lock` 4 | 5 | set -euo pipefail 6 | 7 | find . -name Cargo.lock | while read -r path ; do 8 | ( 9 | cd "$(dirname "$path")" 10 | "$@" 11 | ) 12 | done 13 | -------------------------------------------------------------------------------- /nix/lib.nix: -------------------------------------------------------------------------------- 1 | { lib }: { 2 | cleanSourceWithRel = { src, filter }: 3 | let 4 | baseStr = builtins.toString src; 5 | in 6 | lib.cleanSourceWith { 7 | inherit src; 8 | filter = path: type: 9 | let 10 | relPath = lib.removePrefix baseStr (toString path); 11 | in 12 | filter relPath type; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/root/dot.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug, Clone)] 4 | pub(crate) struct KeyData { 5 | pub(crate) locked_until: Option>, 6 | pub(crate) last_lock: chrono::DateTime, 7 | } 8 | 9 | #[derive(Serialize, Deserialize, Debug, Clone)] 10 | #[serde(rename_all = "snake_case")] 11 | /// Persistent data file at `/fs_dir_cache.json` 12 | pub(crate) struct RootData { 13 | pub(crate) keys: KeyData, 14 | } 15 | -------------------------------------------------------------------------------- /misc/git-hooks/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Sanitize file first, by removing leading lines that are empty or start with a hash, 3 | # as `convco` currently does not do it automatically (but git will) 4 | # TODO: next release of convco should be able to do it automatically 5 | MESSAGE="$( 6 | while read -r line ; do 7 | # skip any initial comments (possibly from previous run) 8 | if [ -z "${body_detected:-}" ] && { [[ "$line" =~ ^#.*$ ]] || [ "$line" == "" ]; }; then 9 | continue 10 | fi 11 | body_detected="true" 12 | 13 | echo "$line" 14 | done < "$1" 15 | )" 16 | 17 | # convco fails on fixup!, so remove fixup! prefix 18 | MESSAGE="${MESSAGE#fixup! }" 19 | if ! convco check --from-stdin <<<"$MESSAGE" ; then 20 | >&2 echo "Please follow conventional commits(https://www.conventionalcommits.org)" 21 | >&2 echo "Use git commit to fix your commit" 22 | exit 1 23 | fi 24 | -------------------------------------------------------------------------------- /.github/workflows/flakebox-flakehub-publish.yml: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOGENERATED FROM FLAKEBOX CONFIGURATION 2 | 3 | jobs: 4 | flakehub-publish: 5 | permissions: 6 | contents: read 7 | id-token: write 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | ref: ${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' 13 | }} 14 | - name: Install Nix 15 | uses: DeterminateSystems/nix-installer-action@v4 16 | - name: Flakehub Push 17 | uses: DeterminateSystems/flakehub-push@main 18 | with: 19 | name: ${{ github.repository }} 20 | tag: ${{ inputs.tag }} 21 | visibility: public 22 | name: Publish to Flakehub 23 | 'on': 24 | push: 25 | tags: 26 | - v?[0-9]+.[0-9]+.[0-9]+* 27 | workflow_dispatch: 28 | inputs: 29 | tags: 30 | description: The existing tag to publish to FlakeHub 31 | required: true 32 | type: string 33 | 34 | 35 | # THIS FILE IS AUTOGENERATED FROM FLAKEBOX CONFIGURATION 36 | -------------------------------------------------------------------------------- /.config/flakebox/shellHook.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | root="$(git rev-parse --show-toplevel)" 3 | dot_git="$(git rev-parse --git-common-dir)" 4 | if [[ ! -d "${dot_git}/hooks" ]]; then mkdir -p "${dot_git}/hooks"; fi 5 | # fix old bug 6 | rm -f "${dot_git}/hooks/comit-msg" 7 | rm -f "${dot_git}/hooks/commit-msg" 8 | ln -sf "${root}/misc/git-hooks/commit-msg" "${dot_git}/hooks/commit-msg" 9 | 10 | root="$(git rev-parse --show-toplevel)" 11 | dot_git="$(git rev-parse --git-common-dir)" 12 | if [[ ! -d "${dot_git}/hooks" ]]; then mkdir -p "${dot_git}/hooks"; fi 13 | # fix old bug 14 | rm -f "${dot_git}/hooks/pre-comit" 15 | rm -f "${dot_git}/hooks/pre-commit" 16 | ln -sf "${root}/misc/git-hooks/pre-commit" "${dot_git}/hooks/pre-commit" 17 | 18 | # set template 19 | git config commit.template misc/git-hooks/commit-template.txt 20 | 21 | if ! flakebox lint --silent; then 22 | >&2 echo "ℹ️ Project recommendations detected. Run 'flakebox lint' for more info." 23 | fi 24 | 25 | if [ -n "${DIRENV_IN_ENVRC:-}" ]; then 26 | # and not set DIRENV_LOG_FORMAT 27 | if [ -n "${DIRENV_LOG_FORMAT:-}" ]; then 28 | >&2 echo "💡 Set 'DIRENV_LOG_FORMAT=\"\"' in your shell environment variables for a cleaner output of direnv" 29 | fi 30 | fi 31 | 32 | >&2 echo "💡 Run 'just' for a list of available 'just ...' helper recipes" 33 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fs-dir-cache" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MPL-2.0" 6 | description = "A CLI tool for CIs and build scripts, making file system based caching easy and correct (locking, eviction, etc.) " 7 | repository = "https://github.com/dpc/fs-dir-cache" 8 | authors = ["Dawid Ciężarkiewicz "] 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | [[bin]] 12 | name = "fs-dir-cache" 13 | path = "src/main.rs" 14 | 15 | [profile.dev] 16 | debug = "line-tables-only" 17 | 18 | [profile.release] 19 | debug = "line-tables-only" 20 | 21 | [profile.ci] 22 | inherits = "dev" 23 | incremental = false 24 | debug = "line-tables-only" 25 | lto = "off" 26 | 27 | [dependencies] 28 | anyhow = "1.0.75" 29 | blake3 = "1.4.1" 30 | chrono = { version = "0.4.26", features = ["serde", "clock"] } 31 | clap = { version = "4.4.0", features = ["derive", "env"] } 32 | convi = { version = "0.0.7", features = ["min_target_pointer_width_32"] } 33 | fs2 = "0.4.3" 34 | rand = "0.8.5" 35 | serde = { version = "1.0.187", features = ["derive"] } 36 | serde_json = "1.0.105" 37 | tracing = "0.1.37" 38 | tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } 39 | 40 | [dev-dependencies] 41 | assert_cmd = "2.0.16" 42 | tempfile = "3.13.0" 43 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io::{self, Write}; 3 | use std::path::Path; 4 | 5 | use serde::Serialize; 6 | use tracing::debug; 7 | 8 | pub fn open_lock_file(root_path: &Path) -> anyhow::Result { 9 | let path = root_path.join("lock"); 10 | debug!(path = %path.display(), "Opening lock file..."); 11 | let file = fs::OpenOptions::new() 12 | .create(true) 13 | .append(true) 14 | .read(true) 15 | .open(path.clone())?; 16 | debug!(path = %path.display(), "Opened lock file"); 17 | Ok(file) 18 | } 19 | 20 | pub fn store_json_pretty_to_file(path: &Path, val: &T) -> anyhow::Result<()> 21 | where 22 | T: Serialize, 23 | { 24 | Ok(store_to_file_with(path, |f| { 25 | serde_json::to_writer_pretty(f, val).map_err(Into::into) 26 | }) 27 | .and_then(|res| res)?) 28 | } 29 | 30 | pub fn store_to_file_with(path: &Path, f: F) -> io::Result> 31 | where 32 | F: Fn(&mut dyn io::Write) -> Result<(), E>, 33 | { 34 | std::fs::create_dir_all(path.parent().expect("Not a root path"))?; 35 | let tmp_path = path.with_extension("tmp"); 36 | let mut file = std::fs::File::create(&tmp_path)?; 37 | if let Err(e) = f(&mut file) { 38 | return Ok(Err(e)); 39 | } 40 | file.flush()?; 41 | file.sync_data()?; 42 | drop(file); 43 | std::fs::rename(tmp_path, path)?; 44 | Ok(Ok(())) 45 | } 46 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A CLI tool for CIs and build scripts, making file system based caching easy and correct (locking, eviction, etc.) "; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | flakebox = { 8 | url = "github:rustshop/flakebox?rev=acc5c7cf5aeb64cba3fa39c93372b00f9470d1de"; 9 | inputs.nixpkgs.follows = "nixpkgs"; 10 | }; 11 | }; 12 | 13 | outputs = { self, nixpkgs, flake-utils, flakebox }: 14 | flake-utils.lib.eachDefaultSystem (system: 15 | let 16 | projectName = "fs-dir-cache"; 17 | 18 | flakeboxLib = flakebox.lib.${system} { 19 | config = { 20 | github.ci.buildOutputs = [ ".#ci.${projectName}" ]; 21 | }; 22 | }; 23 | 24 | buildPaths = [ 25 | "Cargo.toml" 26 | "Cargo.lock" 27 | ".cargo" 28 | "src" 29 | ]; 30 | 31 | buildSrc = flakeboxLib.filterSubPaths { 32 | root = builtins.path { 33 | name = projectName; 34 | path = ./.; 35 | }; 36 | paths = buildPaths; 37 | }; 38 | 39 | multiBuild = 40 | (flakeboxLib.craneMultiBuild { }) (craneLib': 41 | let 42 | craneLib = (craneLib'.overrideArgs { 43 | pname = projectName; 44 | src = buildSrc; 45 | nativeBuildInputs = [ ]; 46 | }); 47 | in 48 | { 49 | ${projectName} = craneLib.buildPackage { }; 50 | }); 51 | in 52 | { 53 | packages.default = multiBuild.${projectName}; 54 | 55 | legacyPackages = multiBuild; 56 | 57 | devShells = flakeboxLib.mkShells { }; 58 | } 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/root/dto.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::path::PathBuf; 3 | 4 | use chrono::{DateTime, Utc}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Serialize, Deserialize, Debug, Clone)] 8 | pub struct KeyData { 9 | pub locked_until: chrono::DateTime, 10 | pub lock_id: String, 11 | pub last_lock: chrono::DateTime, 12 | pub socket_path: Option, 13 | } 14 | 15 | impl KeyData { 16 | pub fn is_timelocked(&self, now: DateTime) -> bool { 17 | now < self.locked_until 18 | } 19 | 20 | pub fn is_last_used_before(&self, deadline: DateTime) -> bool { 21 | self.last_lock < deadline 22 | } 23 | 24 | pub fn expires_in(&self, now: DateTime) -> chrono::Duration { 25 | self.locked_until.signed_duration_since(now) 26 | } 27 | 28 | pub fn lock( 29 | &mut self, 30 | now: DateTime, 31 | lock_id: &str, 32 | timeout_secs: f64, 33 | socket_path: Option, 34 | ) -> anyhow::Result<&mut Self> { 35 | self.locked_until = now 36 | .checked_add_signed(chrono::Duration::milliseconds( 37 | (timeout_secs * 1000.0).round() as i64, 38 | )) 39 | .ok_or_else(|| anyhow::format_err!("Timeout overflow"))?; 40 | self.last_lock = now; 41 | self.lock_id = lock_id.to_owned(); 42 | self.socket_path = socket_path; 43 | 44 | Ok(self) 45 | } 46 | 47 | pub fn unlock(&mut self, now: DateTime) -> &mut Self { 48 | self.locked_until = now; 49 | debug_assert!(!self.is_timelocked(now)); 50 | self 51 | } 52 | 53 | pub fn new(now: DateTime) -> Self { 54 | let s = Self { 55 | locked_until: now, 56 | lock_id: "".to_owned(), 57 | last_lock: now, 58 | socket_path: None, 59 | }; 60 | debug_assert!(!s.is_timelocked(now)); 61 | s 62 | } 63 | } 64 | #[derive(Serialize, Deserialize, Debug, Clone, Default)] 65 | #[serde(rename_all = "snake_case")] 66 | /// Persistent data file at `/fs_dir_cache.json` 67 | pub struct RootData { 68 | pub keys: BTreeMap, 69 | } 70 | -------------------------------------------------------------------------------- /.github/workflows/flakebox-ci.yml: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOGENERATED FROM FLAKEBOX CONFIGURATION 2 | 3 | jobs: 4 | build: 5 | name: Build 6 | runs-on: ${{ matrix.runs-on }} 7 | steps: 8 | - uses: actions/checkout@v4 9 | - name: Install Nix 10 | uses: DeterminateSystems/nix-installer-action@v4 11 | - name: Magic Nix Cache 12 | uses: DeterminateSystems/magic-nix-cache-action@v2 13 | - name: Build on ${{ matrix.host }} 14 | run: nix build -L .#ci.fs-dir-cache 15 | strategy: 16 | matrix: 17 | host: 18 | - macos-x86_64 19 | - macos-aarch64 20 | - linux 21 | include: 22 | - host: linux 23 | runs-on: ubuntu-latest 24 | timeout: 60 25 | - host: macos-x86_64 26 | runs-on: macos-12 27 | timeout: 60 28 | - host: macos-aarch64 29 | runs-on: macos-14 30 | timeout: 60 31 | timeout-minutes: ${{ matrix.timeout }} 32 | flake: 33 | name: Flake self-check 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Check Nix flake inputs 38 | uses: DeterminateSystems/flake-checker-action@v5 39 | with: 40 | fail-mode: true 41 | lint: 42 | name: Lint 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v4 46 | - name: Install Nix 47 | uses: DeterminateSystems/nix-installer-action@v4 48 | - name: Magic Nix Cache 49 | uses: DeterminateSystems/magic-nix-cache-action@v2 50 | - name: Cargo Cache 51 | uses: actions/cache@v3 52 | with: 53 | key: ${{ runner.os }}-${{ hashFiles('Cargo.lock') }} 54 | path: ~/.cargo 55 | - name: Commit Check 56 | run: '# run the same check that git `pre-commit` hook does 57 | 58 | nix develop --ignore-environment .#lint --command ./misc/git-hooks/pre-commit 59 | 60 | ' 61 | name: CI 62 | 'on': 63 | merge_group: 64 | branches: 65 | - master 66 | - main 67 | pull_request: 68 | branches: 69 | - master 70 | - main 71 | push: 72 | branches: 73 | - master 74 | - main 75 | tags: 76 | - v* 77 | workflow_dispatch: {} 78 | 79 | 80 | # THIS FILE IS AUTOGENERATED FROM FLAKEBOX CONFIGURATION 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FS Dir Cache 2 | 3 | A CLI tool for CIs and build scripts, making file system based 4 | caching easy and correct (locking, eviction, etc.) 5 | 6 | When working on build systems / CIs it's often a requirement to 7 | utilize some best effort caching, with locking and eviction. 8 | 9 | Not exactly rocket science, but non-trivial enough to not want 10 | to implement and maintain ad-hoc. 11 | 12 | `fs-dir-cache` aims to be a simple to use utility from inside 13 | other scripts and programs taking care of the details. 14 | 15 | ## Example 16 | 17 | This is an example, where a CI runner can persist files between runs, 18 | and it's used to to reuse build artifacts between builds that are likely 19 | building the same build to speed everything up: 20 | 21 | 22 | ```bash 23 | #!/usr/bin/env bash 24 | 25 | set -euo pipefail 26 | 27 | job_name="$1" 28 | shift 1 29 | 30 | if [ -z "$job_name" ]; then 31 | >&2 "error: no job name" 32 | exit 1 33 | fi 34 | 35 | export FS_DIR_CACHE_LOCK_TIMEOUT_SECS="$((60 * 30))" # unlock after timeout in case our job fails misereably and/or hangs 36 | 37 | export FS_DIR_CACHE_ROOT="$HOME/.cache/fs-dir-cache" # directory to hold all cache (sub)directories 38 | export FS_DIR_CACHE_LOCK_ID="pid-$$-rnd-$RANDOM" # acquire lock based on the current pid and something random (just in case pid gets reused) 39 | export FS_DIR_CACHE_KEY_NAME="$job_name" # the base name of our key 40 | 41 | log_file="$FS_DIR_CACHE_ROOT/log" 42 | 43 | fs-dir-cache gc unused --seconds "$((5 * 24 * 60 * 60))" # delete caches not used in more than a 5 days 44 | 45 | export log_file # log when each job starte and ended 46 | export job_name 47 | src_dir=$(pwd) 48 | export src_dir 49 | 50 | # This bash command will be executed with a CWD set to the allocated directory 51 | function run_in_cache() { 52 | echo "$(date --rfc-3339=seconds) RUN job=$job_name dir=$(pwd)" >> "$log_file" 53 | >&2 echo "$(date --rfc-3339=seconds) RUN job=$job_name dir=$(pwd)" 54 | CARGO_BUILD_TARGET_DIR="$(pwd)" 55 | export CARGO_BUILD_TARGET_DIR 56 | cd "$src_dir" 57 | 58 | function on_exit() { 59 | local exit_code=$? 60 | 61 | echo "$(date --rfc-3339=seconds) END job=$job_name code=$exit_code" >> "$log_file" 62 | >&2 echo "$(date --rfc-3339=seconds) END job=$job_name code=$exit_code" 63 | 64 | exit $exit_code 65 | } 66 | trap on_exit EXIT 67 | 68 | "$@" 69 | } 70 | export -f run_in_cache 71 | 72 | 73 | fs-dir-cache exec \ 74 | --key-file Cargo.lock 75 | --key-str "${CARGO_PROFILE-:dev}" \ 76 | --key-file flake.lock \ 77 | -- \ 78 | bash -c 'run_in_cache "$@"' _ "$@" 79 | ``` 80 | 81 | Using just one tool, it's easy to get correct and practical caching including: 82 | 83 | * locking (including fallback timeouts) 84 | * evicition 85 | * timeouts 86 | -------------------------------------------------------------------------------- /src/test.rs: -------------------------------------------------------------------------------- 1 | use std::os::unix::ffi::OsStringExt; 2 | use std::path::PathBuf; 3 | use std::process::Stdio; 4 | use std::str::FromStr; 5 | use std::time::Duration; 6 | use std::{ffi, fs, thread}; 7 | 8 | use anyhow::Result; 9 | use assert_cmd::assert::OutputAssertExt as _; 10 | use assert_cmd::cargo; 11 | 12 | #[test] 13 | fn sanity_check() -> Result<()> { 14 | let root_dir = tempfile::tempdir()?; 15 | 16 | thread::scope(|s| -> Result<()> { 17 | for _ in 0..10 { 18 | s.spawn(|| -> Result<()> { 19 | let mut cmd = our_bin_cmd(); 20 | 21 | cmd.env("FS_DIR_CACHE_ROOT", root_dir.path()); 22 | cmd.stderr(Stdio::inherit()); 23 | cmd.args([ 24 | "lock", 25 | "--key-name", 26 | "keyname", 27 | "--lock-id", 28 | "lockid", 29 | "--timeout-secs", 30 | "5", 31 | ]); 32 | 33 | let dir_str = ffi::OsString::from_str( 34 | String::from_utf8( 35 | cmd.output()?.assert().success().get_output().stdout.clone(), 36 | )? 37 | .trim(), 38 | )?; 39 | let dir_path = PathBuf::from(&dir_str); 40 | let testfile_path = dir_path.join("test"); 41 | 42 | fs::write(&testfile_path, [])?; 43 | thread::sleep(Duration::from_millis(200)); 44 | fs::remove_file(&testfile_path)?; 45 | 46 | let mut cmd = our_bin_cmd(); 47 | 48 | cmd.stderr(Stdio::inherit()); 49 | cmd.env("FS_DIR_CACHE_ROOT", root_dir.path()); 50 | cmd.args(["unlock", "--lock-id", "lockid"]); 51 | cmd.args([ 52 | ffi::OsString::from_vec("--dir".as_bytes().to_vec()), 53 | dir_str, 54 | ]); 55 | cmd.assert().success(); 56 | Ok(()) 57 | }); 58 | 59 | s.spawn(|| -> Result<()> { 60 | let mut cmd = our_bin_cmd(); 61 | 62 | cmd.stderr(Stdio::inherit()); 63 | cmd.env("FS_DIR_CACHE_ROOT", root_dir.path()); 64 | cmd.args([ 65 | "exec", 66 | "--key-name", 67 | "keyname", 68 | "--", 69 | "bash", 70 | "-c", 71 | "set -e; test ! -e test; touch test; sleep .2; test -e test; rm test", 72 | ]); 73 | 74 | cmd.assert().success(); 75 | 76 | Ok(()) 77 | }); 78 | } 79 | Ok(()) 80 | })?; 81 | 82 | Ok(()) 83 | } 84 | 85 | fn our_bin_cmd() -> std::process::Command { 86 | std::process::Command::new(cargo::cargo_bin(env!("CARGO_PKG_NAME"))) 87 | } 88 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOGENERATED FROM FLAKEBOX CONFIGURATION 2 | 3 | alias b := build 4 | alias c := check 5 | alias t := test 6 | 7 | 8 | [private] 9 | default: 10 | @just --list 11 | 12 | 13 | # run `cargo build` on everything 14 | build *ARGS="--workspace --all-targets": 15 | #!/usr/bin/env bash 16 | set -euo pipefail 17 | if [ ! -f Cargo.toml ]; then 18 | cd {{invocation_directory()}} 19 | fi 20 | cargo build {{ARGS}} 21 | 22 | 23 | # run `cargo check` on everything 24 | check *ARGS="--workspace --all-targets": 25 | #!/usr/bin/env bash 26 | set -euo pipefail 27 | if [ ! -f Cargo.toml ]; then 28 | cd {{invocation_directory()}} 29 | fi 30 | cargo check {{ARGS}} 31 | 32 | 33 | # run all checks recommended before opening a PR 34 | final-check: lint clippy 35 | #!/usr/bin/env bash 36 | set -euo pipefail 37 | if [ ! -f Cargo.toml ]; then 38 | cd {{invocation_directory()}} 39 | fi 40 | just test 41 | 42 | 43 | # run code formatters 44 | format: 45 | #!/usr/bin/env bash 46 | set -euo pipefail 47 | if [ ! -f Cargo.toml ]; then 48 | cd {{invocation_directory()}} 49 | fi 50 | cargo fmt --all 51 | nixfmt $(git ls-files | grep "\.nix$") 52 | 53 | 54 | # run lints (git pre-commit hook) 55 | lint: 56 | #!/usr/bin/env bash 57 | set -euo pipefail 58 | env NO_STASH=true $(git rev-parse --git-common-dir)/hooks/pre-commit 59 | 60 | 61 | # run tests 62 | test: build 63 | #!/usr/bin/env bash 64 | set -euo pipefail 65 | if [ ! -f Cargo.toml ]; then 66 | cd {{invocation_directory()}} 67 | fi 68 | cargo test 69 | 70 | 71 | # run and restart on changes 72 | watch *ARGS="-x run": 73 | #!/usr/bin/env bash 74 | set -euo pipefail 75 | if [ ! -f Cargo.toml ]; then 76 | cd {{invocation_directory()}} 77 | fi 78 | env RUST_LOG=${RUST_LOG:-debug} cargo watch {{ARGS}} 79 | 80 | 81 | # run `cargo clippy` on everything 82 | clippy *ARGS="--locked --offline --workspace --all-targets": 83 | cargo clippy {{ARGS}} -- --deny warnings --allow deprecated 84 | 85 | # run `cargo clippy --fix` on everything 86 | clippy-fix *ARGS="--locked --offline --workspace --all-targets": 87 | cargo clippy {{ARGS}} --fix 88 | 89 | 90 | # run `semgrep` 91 | semgrep: 92 | env SEMGREP_ENABLE_VERSION_CHECK=0 \ 93 | semgrep --error --no-rewrite-rule-ids --config .config/semgrep.yaml 94 | 95 | 96 | # check typos 97 | [no-exit-message] 98 | typos *PARAMS: 99 | #!/usr/bin/env bash 100 | set -eo pipefail 101 | 102 | export FLAKEBOX_GIT_LS 103 | FLAKEBOX_GIT_LS="$(git ls-files)" 104 | export FLAKEBOX_GIT_LS_TEXT 105 | FLAKEBOX_GIT_LS_TEXT="$(echo "$FLAKEBOX_GIT_LS" | grep -v -E "^db/|\.(png|ods|jpg|jpeg|woff2|keystore|wasm|ttf|jar|ico)\$")" 106 | 107 | 108 | if ! echo "$FLAKEBOX_GIT_LS_TEXT" | typos {{PARAMS}} --file-list - --force-exclude ; then 109 | >&2 echo "Typos found: Valid new words can be added to '.typos.toml'" 110 | return 1 111 | fi 112 | 113 | # fix all typos 114 | [no-exit-message] 115 | typos-fix-all: 116 | just typos -w 117 | 118 | # THIS FILE IS AUTOGENERATED FROM FLAKEBOX CONFIGURATION 119 | -------------------------------------------------------------------------------- /misc/git-hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | set +e 6 | git diff-files --quiet 7 | is_unclean=$? 8 | set -e 9 | 10 | # Revert `git stash` on exit 11 | function revert_git_stash { 12 | >&2 echo "Unstashing uncommitted changes..." 13 | git stash pop -q 14 | } 15 | 16 | # Stash pending changes and revert them when script ends 17 | if [ -z "${NO_STASH:-}" ] && [ $is_unclean -ne 0 ]; then 18 | >&2 echo "Stashing uncommitted changes..." 19 | GIT_LITERAL_PATHSPECS=0 git stash -q --keep-index 20 | trap revert_git_stash EXIT 21 | fi 22 | 23 | export FLAKEBOX_GIT_LS 24 | FLAKEBOX_GIT_LS="$(git ls-files)" 25 | export FLAKEBOX_GIT_LS_TEXT 26 | FLAKEBOX_GIT_LS_TEXT="$(echo "$FLAKEBOX_GIT_LS" | grep -v -E "\.(png|ods|jpg|jpeg|woff2|keystore|wasm|ttf|jar|ico|gif)\$")" 27 | 28 | 29 | function check_nothing() { 30 | true 31 | } 32 | export -f check_nothing 33 | 34 | function check_cargo_fmt() { 35 | set -euo pipefail 36 | 37 | flakebox-in-each-cargo-workspace cargo fmt --all --check 38 | 39 | } 40 | export -f check_cargo_fmt 41 | 42 | function check_cargo_lock() { 43 | set -euo pipefail 44 | 45 | # https://users.rust-lang.org/t/check-if-the-cargo-lock-is-up-to-date-without-building-anything/91048/5 46 | flakebox-in-each-cargo-workspace cargo update --workspace --locked |& while read -r note ; do echo "$note (cargo)"; done 47 | 48 | } 49 | export -f check_cargo_lock 50 | 51 | function check_leftover_dbg() { 52 | set -euo pipefail 53 | 54 | errors="" 55 | for path in $(echo "$FLAKEBOX_GIT_LS_TEXT" | grep '.*\.rs'); do 56 | if grep 'dbg!(' "$path" > /dev/null; then 57 | >&2 echo "$path contains dbg! macro" 58 | errors="true" 59 | fi 60 | done 61 | 62 | if [ -n "$errors" ]; then 63 | >&2 echo "Fix the problems above or use --no-verify" 1>&2 64 | return 1 65 | fi 66 | 67 | } 68 | export -f check_leftover_dbg 69 | 70 | function check_semgrep() { 71 | set -euo pipefail 72 | 73 | # semgrep is not available on MacOS 74 | if ! command -v semgrep > /dev/null ; then 75 | >&2 echo "Skipping semgrep check: not available" 76 | return 0 77 | fi 78 | 79 | if [ ! -f .config/semgrep.yaml ] ; then 80 | >&2 echo "Skipping semgrep check: .config/semgrep.yaml doesn't exist" 81 | return 0 82 | fi 83 | 84 | if [ ! -s .config/semgrep.yaml ] ; then 85 | >&2 echo "Skipping semgrep check: .config/semgrep.yaml empty" 86 | return 0 87 | fi 88 | 89 | env SEMGREP_ENABLE_VERSION_CHECK=0 \ 90 | semgrep -q --error --no-rewrite-rule-ids --config .config/semgrep.yaml 91 | 92 | } 93 | export -f check_semgrep 94 | 95 | function check_shellcheck() { 96 | set -euo pipefail 97 | 98 | for path in $(echo "$FLAKEBOX_GIT_LS_TEXT" | grep -E '.*\.sh$'); do 99 | shellcheck --severity=warning "$path" 100 | done 101 | 102 | } 103 | export -f check_shellcheck 104 | 105 | function check_trailing_newline() { 106 | set -euo pipefail 107 | 108 | errors="" 109 | for path in $(echo "$FLAKEBOX_GIT_LS_TEXT"); do 110 | 111 | # extra branches for clarity 112 | if [ ! -s "$path" ]; then 113 | # echo "$path is empty" 114 | true 115 | elif [ -z "$(tail -c 1 < "$path")" ]; then 116 | # echo "$path ends with a newline or with a null byte" 117 | true 118 | else 119 | >&2 echo "$path doesn't end with a newline" 1>&2 120 | errors="true" 121 | fi 122 | done 123 | 124 | if [ -n "$errors" ]; then 125 | >&2 echo "Fix the problems above or use --no-verify" 1>&2 126 | return 1 127 | fi 128 | 129 | } 130 | export -f check_trailing_newline 131 | 132 | function check_trailing_whitespace() { 133 | set -euo pipefail 134 | 135 | rev="HEAD" 136 | if ! git rev-parse -q 1>/dev/null HEAD 2>/dev/null ; then 137 | >&2 echo "Warning: no commits yet, checking against --root" 138 | rev="--root" 139 | fi 140 | if ! git diff --check $rev ; then 141 | >&2 echo "Trailing whitespace detected. Please remove them before committing." 142 | return 1 143 | fi 144 | 145 | } 146 | export -f check_trailing_whitespace 147 | 148 | function check_typos() { 149 | set -euo pipefail 150 | 151 | if ! echo "$FLAKEBOX_GIT_LS_TEXT" | typos --file-list - --force-exclude ; then 152 | >&2 echo "Typos found: Valid new words can be added to '.typos.toml'" 153 | return 1 154 | fi 155 | 156 | } 157 | export -f check_typos 158 | 159 | parallel \ 160 | --nonotice \ 161 | ::: \ 162 | check_cargo_fmt \ 163 | check_cargo_lock \ 164 | check_leftover_dbg \ 165 | check_semgrep \ 166 | check_shellcheck \ 167 | check_trailing_newline \ 168 | check_trailing_whitespace \ 169 | check_typos \ 170 | check_nothing 171 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "android-nixpkgs": { 4 | "inputs": { 5 | "devshell": "devshell", 6 | "flake-utils": "flake-utils_3", 7 | "nixpkgs": [ 8 | "flakebox", 9 | "nixpkgs" 10 | ] 11 | }, 12 | "locked": { 13 | "lastModified": 1727381935, 14 | "narHash": "sha256-G2fOYRZM7bXK5eBb+GK3k/WmO+q5JA/GtFwSPc3kdc8=", 15 | "owner": "tadfisher", 16 | "repo": "android-nixpkgs", 17 | "rev": "522d86121cbd413aff922c54f38106ecf8740107", 18 | "type": "github" 19 | }, 20 | "original": { 21 | "owner": "tadfisher", 22 | "repo": "android-nixpkgs", 23 | "rev": "522d86121cbd413aff922c54f38106ecf8740107", 24 | "type": "github" 25 | } 26 | }, 27 | "crane": { 28 | "inputs": { 29 | "nixpkgs": [ 30 | "flakebox", 31 | "nixpkgs" 32 | ] 33 | }, 34 | "locked": { 35 | "lastModified": 1717383740, 36 | "narHash": "sha256-559HbY4uhNeoYvK3H6AMZAtVfmR3y8plXZ1x6ON/cWU=", 37 | "owner": "ipetkov", 38 | "repo": "crane", 39 | "rev": "b65673fce97d277934488a451724be94cc62499a", 40 | "type": "github" 41 | }, 42 | "original": { 43 | "owner": "ipetkov", 44 | "repo": "crane", 45 | "rev": "b65673fce97d277934488a451724be94cc62499a", 46 | "type": "github" 47 | } 48 | }, 49 | "devshell": { 50 | "inputs": { 51 | "flake-utils": "flake-utils_2", 52 | "nixpkgs": [ 53 | "flakebox", 54 | "android-nixpkgs", 55 | "nixpkgs" 56 | ] 57 | }, 58 | "locked": { 59 | "lastModified": 1717408969, 60 | "narHash": "sha256-Q0OEFqe35fZbbRPPRdrjTUUChKVhhWXz3T9ZSKmaoVY=", 61 | "owner": "numtide", 62 | "repo": "devshell", 63 | "rev": "1ebbe68d57457c8cae98145410b164b5477761f4", 64 | "type": "github" 65 | }, 66 | "original": { 67 | "owner": "numtide", 68 | "repo": "devshell", 69 | "type": "github" 70 | } 71 | }, 72 | "fenix": { 73 | "inputs": { 74 | "nixpkgs": [ 75 | "flakebox", 76 | "nixpkgs" 77 | ], 78 | "rust-analyzer-src": "rust-analyzer-src" 79 | }, 80 | "locked": { 81 | "lastModified": 1727418876, 82 | "narHash": "sha256-a4iKujD+2XqzT1QXWzStu31qzpZGJi7K6AS5AlpSDY0=", 83 | "owner": "nix-community", 84 | "repo": "fenix", 85 | "rev": "6ed50e3b7558220f1854cf348fcbf40540709fc3", 86 | "type": "github" 87 | }, 88 | "original": { 89 | "owner": "nix-community", 90 | "repo": "fenix", 91 | "type": "github" 92 | } 93 | }, 94 | "flake-utils": { 95 | "inputs": { 96 | "systems": "systems" 97 | }, 98 | "locked": { 99 | "lastModified": 1726560853, 100 | "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", 101 | "owner": "numtide", 102 | "repo": "flake-utils", 103 | "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", 104 | "type": "github" 105 | }, 106 | "original": { 107 | "owner": "numtide", 108 | "repo": "flake-utils", 109 | "type": "github" 110 | } 111 | }, 112 | "flake-utils_2": { 113 | "inputs": { 114 | "systems": "systems_2" 115 | }, 116 | "locked": { 117 | "lastModified": 1701680307, 118 | "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", 119 | "owner": "numtide", 120 | "repo": "flake-utils", 121 | "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", 122 | "type": "github" 123 | }, 124 | "original": { 125 | "owner": "numtide", 126 | "repo": "flake-utils", 127 | "type": "github" 128 | } 129 | }, 130 | "flake-utils_3": { 131 | "inputs": { 132 | "systems": "systems_3" 133 | }, 134 | "locked": { 135 | "lastModified": 1710146030, 136 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 137 | "owner": "numtide", 138 | "repo": "flake-utils", 139 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 140 | "type": "github" 141 | }, 142 | "original": { 143 | "owner": "numtide", 144 | "repo": "flake-utils", 145 | "type": "github" 146 | } 147 | }, 148 | "flake-utils_4": { 149 | "inputs": { 150 | "systems": [ 151 | "flakebox", 152 | "systems" 153 | ] 154 | }, 155 | "locked": { 156 | "lastModified": 1726560853, 157 | "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", 158 | "owner": "numtide", 159 | "repo": "flake-utils", 160 | "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", 161 | "type": "github" 162 | }, 163 | "original": { 164 | "owner": "numtide", 165 | "repo": "flake-utils", 166 | "type": "github" 167 | } 168 | }, 169 | "flakebox": { 170 | "inputs": { 171 | "android-nixpkgs": "android-nixpkgs", 172 | "crane": "crane", 173 | "fenix": "fenix", 174 | "flake-utils": "flake-utils_4", 175 | "nixpkgs": [ 176 | "nixpkgs" 177 | ], 178 | "systems": "systems_4" 179 | }, 180 | "locked": { 181 | "lastModified": 1729019740, 182 | "narHash": "sha256-cvgTs6FmELlMrvAWlnIp5AGm69jOQKb9COvo+HDK9ys=", 183 | "owner": "rustshop", 184 | "repo": "flakebox", 185 | "rev": "acc5c7cf5aeb64cba3fa39c93372b00f9470d1de", 186 | "type": "github" 187 | }, 188 | "original": { 189 | "owner": "rustshop", 190 | "repo": "flakebox", 191 | "rev": "acc5c7cf5aeb64cba3fa39c93372b00f9470d1de", 192 | "type": "github" 193 | } 194 | }, 195 | "nixpkgs": { 196 | "locked": { 197 | "lastModified": 1730200266, 198 | "narHash": "sha256-l253w0XMT8nWHGXuXqyiIC/bMvh1VRszGXgdpQlfhvU=", 199 | "owner": "NixOS", 200 | "repo": "nixpkgs", 201 | "rev": "807e9154dcb16384b1b765ebe9cd2bba2ac287fd", 202 | "type": "github" 203 | }, 204 | "original": { 205 | "owner": "NixOS", 206 | "ref": "nixos-unstable", 207 | "repo": "nixpkgs", 208 | "type": "github" 209 | } 210 | }, 211 | "root": { 212 | "inputs": { 213 | "flake-utils": "flake-utils", 214 | "flakebox": "flakebox", 215 | "nixpkgs": "nixpkgs" 216 | } 217 | }, 218 | "rust-analyzer-src": { 219 | "flake": false, 220 | "locked": { 221 | "lastModified": 1727376968, 222 | "narHash": "sha256-0Vmk8/7uOg7IxZOQIcY2deuUnTUTm1d/mZe8yycbCAA=", 223 | "owner": "rust-lang", 224 | "repo": "rust-analyzer", 225 | "rev": "c88ea11832277b6c010088d658965c39c1181d20", 226 | "type": "github" 227 | }, 228 | "original": { 229 | "owner": "rust-lang", 230 | "ref": "nightly", 231 | "repo": "rust-analyzer", 232 | "type": "github" 233 | } 234 | }, 235 | "systems": { 236 | "locked": { 237 | "lastModified": 1681028828, 238 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 239 | "owner": "nix-systems", 240 | "repo": "default", 241 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 242 | "type": "github" 243 | }, 244 | "original": { 245 | "owner": "nix-systems", 246 | "repo": "default", 247 | "type": "github" 248 | } 249 | }, 250 | "systems_2": { 251 | "locked": { 252 | "lastModified": 1681028828, 253 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 254 | "owner": "nix-systems", 255 | "repo": "default", 256 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 257 | "type": "github" 258 | }, 259 | "original": { 260 | "owner": "nix-systems", 261 | "repo": "default", 262 | "type": "github" 263 | } 264 | }, 265 | "systems_3": { 266 | "locked": { 267 | "lastModified": 1681028828, 268 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 269 | "owner": "nix-systems", 270 | "repo": "default", 271 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 272 | "type": "github" 273 | }, 274 | "original": { 275 | "owner": "nix-systems", 276 | "repo": "default", 277 | "type": "github" 278 | } 279 | }, 280 | "systems_4": { 281 | "locked": { 282 | "lastModified": 1681028828, 283 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 284 | "owner": "nix-systems", 285 | "repo": "default", 286 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 287 | "type": "github" 288 | }, 289 | "original": { 290 | "owner": "nix-systems", 291 | "repo": "default", 292 | "type": "github" 293 | } 294 | } 295 | }, 296 | "root": "root", 297 | "version": 7 298 | } 299 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod root; 2 | mod util; 3 | 4 | use std::path::{Path, PathBuf}; 5 | use std::{ffi, fs, io, process}; 6 | 7 | use anyhow::{bail, format_err, Context, Result}; 8 | use chrono::Utc; 9 | use clap::{Args, Parser, Subcommand}; 10 | use rand::distributions::{Alphanumeric, DistString}; 11 | use root::{mk_lock, Root}; 12 | use tracing::{debug, error, warn}; 13 | use tracing_subscriber::EnvFilter; 14 | 15 | const LOG_TARGET: &str = "fs_dir_cache"; 16 | 17 | #[derive(Parser)] 18 | #[command(author, version, about, long_about = None)] 19 | struct Opts { 20 | #[command(subcommand)] 21 | command: Commands, 22 | } 23 | 24 | #[derive(Args)] 25 | struct CommonLockOpts { 26 | /// Root cache dir 27 | /// 28 | /// Dir that will hold all the cache key subdirs 29 | #[arg(long, env = "FS_DIR_CACHE_ROOT")] 30 | root: PathBuf, 31 | 32 | /// Name of the cache 33 | /// 34 | /// Base part of the unique key identifying cache subdir 35 | #[arg(long, env = "FS_DIR_CACHE_KEY_NAME")] 36 | key_name: String, 37 | 38 | /// A string to hash into the final cache subdir id 39 | /// 40 | /// Can be passed multiple times (order is significant). 41 | #[arg(long)] 42 | key_str: Vec, 43 | 44 | /// A path to a file to hash the content of into the final cache 45 | /// subdir id 46 | /// 47 | /// Can be passed multiple times (order is significant). 48 | #[arg(long)] 49 | key_file: Vec, 50 | } 51 | 52 | #[derive(Args)] 53 | struct LockOpts { 54 | /// An id of a lock to use for `unlock` 55 | #[arg(long, env = "FS_DIR_CACHE_LOCK_ID")] 56 | lock_id: String, 57 | 58 | /// Unlock automatically after given amount of seconds, in case cleanup 59 | /// never happens 60 | #[arg(long)] 61 | #[arg(long, env = "FS_DIR_CACHE_LOCK_TIMEOUT_SECS")] 62 | timeout_secs: f64, 63 | } 64 | 65 | #[derive(Args, Debug)] 66 | /// Unlock the cache key dir 67 | struct UnlockOpts { 68 | /// Cache key dir 69 | #[arg(long)] 70 | dir: PathBuf, 71 | 72 | /// Lock used during `unlock` 73 | #[arg(long, env = "FS_DIR_CACHE_LOCK_ID")] 74 | lock_id: String, 75 | } 76 | 77 | #[derive(Args)] 78 | /// Garbage collect cache keys 79 | struct GC { 80 | /// Root cache dir 81 | #[arg(long, env = "FS_DIR_CACHE_ROOT")] 82 | root: PathBuf, 83 | 84 | #[command(subcommand)] 85 | mode: GCModeCommand, 86 | } 87 | 88 | #[derive(Args)] 89 | struct ExecOpts { 90 | #[clap(flatten)] 91 | opts: CommonLockOpts, 92 | 93 | #[arg(trailing_var_arg = true, allow_hyphen_values = true)] 94 | exec: Vec, 95 | } 96 | 97 | #[derive(Subcommand)] 98 | enum Commands { 99 | /// Acquire a lock on cache key subdir in a given cache root 100 | /// directory. Waits if key is already locked. 101 | Lock { 102 | #[clap(flatten)] 103 | common: CommonLockOpts, 104 | #[clap(flatten)] 105 | lock: LockOpts, 106 | }, 107 | /// Unlock the lock manually 108 | Unlock(UnlockOpts), 109 | /// Run a command with lock acquired, allows for automatic and reliable 110 | /// unlocking after command finishes. 111 | Exec(ExecOpts), 112 | GC(GC), 113 | } 114 | 115 | #[derive(Subcommand)] 116 | enum GCModeCommand { 117 | /// Delete all cache subdirectories used last more than N 118 | Unused { 119 | #[arg(long)] 120 | seconds: u64, 121 | }, 122 | } 123 | 124 | fn main() -> Result<()> { 125 | init_logging(); 126 | let opts = Opts::parse(); 127 | 128 | match opts.command { 129 | Commands::Lock { 130 | common: common_opts, 131 | lock: lock_opts, 132 | } => println!("{}", lock(Some(lock_opts), common_opts, None)?.display()), 133 | Commands::Unlock(unlock_opts) => { 134 | unlock(unlock_opts)?; 135 | } 136 | Commands::GC(gc_options) => gc(gc_options)?, 137 | Commands::Exec(exec_opts) => run_exec(exec_opts)?, 138 | } 139 | 140 | Ok(()) 141 | } 142 | 143 | fn run_exec(ExecOpts { opts, exec }: ExecOpts) -> Result<()> { 144 | if exec.is_empty() { 145 | bail!("Missing command"); 146 | } 147 | let cmd_str = exec 148 | .join(&ffi::OsString::from(" ")) 149 | .to_string_lossy() 150 | .to_string(); 151 | 152 | let root = std::fs::canonicalize(&opts.root)?; 153 | 154 | let sock_path = root.join(PathBuf::from(format!( 155 | "lock-{}", 156 | Alphanumeric.sample_string(&mut rand::thread_rng(), 16) 157 | ))); 158 | 159 | debug!( 160 | target: LOG_TARGET, 161 | sock_path = %sock_path.display(), 162 | "Binding liveness socket" 163 | ); 164 | 165 | let _lock = mk_lock(&sock_path)?; 166 | 167 | let exec_dir = lock(None, opts, Some(sock_path.clone()))?; 168 | 169 | fs::create_dir_all(&exec_dir)?; 170 | 171 | debug!( 172 | target: LOG_TARGET, 173 | cmd = ?exec, ?exec_dir, "Executing user command" 174 | ); 175 | if !process::Command::new(&exec[0]) 176 | .args(&exec[1..]) 177 | .current_dir(exec_dir) 178 | .status() 179 | .context("Executing user command failed")? 180 | .success() 181 | { 182 | error!(cmd = %cmd_str, "User command failed"); 183 | bail!("User command failed"); 184 | } 185 | 186 | if let Err(err) = fs::remove_file(&sock_path) { 187 | warn!(%err, sock_path=%sock_path.display(), "Error removing liveness socket") 188 | } 189 | 190 | Ok(()) 191 | } 192 | 193 | fn gc(gc_options: GC) -> Result<()> { 194 | match gc_options.mode { 195 | GCModeCommand::Unused { seconds } => { 196 | let mut root = Root::new(&gc_options.root)?; 197 | 198 | let now = Utc::now(); 199 | let deadline = now 200 | .checked_sub_signed(chrono::Duration::seconds( 201 | i64::try_from(seconds).map_err(|_e| anyhow::format_err!("Timeout overflow"))?, 202 | )) 203 | .ok_or_else(|| anyhow::format_err!("Timeout overflow"))?; 204 | 205 | debug!( 206 | target: LOG_TARGET, 207 | %now, %deadline, "Looking for unused keys" 208 | ); 209 | 210 | root.with_lock(|root| { 211 | let mut data = root.load_data()?; 212 | 213 | let to_delete = data 214 | .keys 215 | .iter() 216 | .filter(|(key, v)| { 217 | debug!( 218 | target: LOG_TARGET, 219 | key, last_locked = %v.last_lock, locked_until = %v.locked_until, "Checking key" 220 | ); 221 | !v.is_timelocked(now) && v.is_last_used_before(deadline) 222 | }) 223 | .map(|(k, _v)| k.to_owned()).collect::>(); 224 | 225 | for key in to_delete { 226 | let key_dir = root.key_dir_path(&key); 227 | if key_dir.try_exists()? { 228 | debug!( 229 | target: LOG_TARGET, 230 | key_dir = %key_dir.display(), "Deleting key dir" 231 | ); 232 | fs::remove_dir_all(&key_dir).with_context(|| "Failed to delete")?; 233 | } else { 234 | debug!( 235 | target: LOG_TARGET, 236 | key_dir = %key_dir.display(), "Does not exist" 237 | ) 238 | } 239 | data.keys.remove(&key); 240 | root.store_data(&data)?; 241 | println!("{}", key_dir.display()); 242 | } 243 | 244 | Ok(()) 245 | }) 246 | } 247 | } 248 | } 249 | 250 | fn lock( 251 | lock_opts: Option, 252 | common_opts: CommonLockOpts, 253 | socket_path: Option, 254 | ) -> Result { 255 | let mut root = Root::new(&common_opts.root)?; 256 | 257 | let key = format!("{}-{}", common_opts.key_name, get_cache_key(&common_opts)?); 258 | root.with_lock(|root| { 259 | root.lock_key( 260 | &key, 261 | &lock_opts 262 | .as_ref() 263 | .map(|o| o.lock_id.clone()) 264 | .unwrap_or_else(|| format!("exec-{}", std::process::id())), 265 | lock_opts.map(|o| o.timeout_secs).unwrap_or_default(), 266 | socket_path, 267 | ) 268 | }) 269 | } 270 | 271 | fn unlock(unlock_opts: UnlockOpts) -> Result<()> { 272 | let (root_dir, key) = split_key_dir_path(&unlock_opts.dir)?; 273 | let mut root = Root::new(root_dir)?; 274 | 275 | root.with_lock(|root| root.unlock_key(&key, unlock_opts.lock_id)) 276 | } 277 | 278 | fn split_key_dir_path(dir: &Path) -> Result<(PathBuf, String)> { 279 | let key = dir 280 | .file_name() 281 | .ok_or_else(|| format_err!("Path ends with invalid component: {}", dir.display()))? 282 | .to_str() 283 | .ok_or_else(|| format_err!("Path contains invalid characters: {}", dir.display()))? 284 | .to_owned(); 285 | 286 | let parent = dir 287 | .parent() 288 | .ok_or_else(|| format_err!("Can't figure out parent: {}", dir.display()))? 289 | .to_owned(); 290 | 291 | Ok((parent, key)) 292 | } 293 | 294 | fn get_cache_key(common_lock_opts: &CommonLockOpts) -> Result { 295 | let mut hasher = blake3::Hasher::new(); 296 | hasher.update(common_lock_opts.key_name.as_bytes()); 297 | for key_str in &common_lock_opts.key_str { 298 | hasher.update(key_str.as_bytes()); 299 | } 300 | for key_file in &common_lock_opts.key_file { 301 | let mut reader = fs::File::open(key_file) 302 | .with_context(|| format!("Failed to open {}", key_file.display()))?; 303 | io::copy(&mut reader, &mut hasher) 304 | .with_context(|| format!("Failed to read {}", key_file.display()))?; 305 | } 306 | 307 | Ok(hasher.finalize().to_hex().to_string()) 308 | } 309 | 310 | fn init_logging() { 311 | let subscriber = tracing_subscriber::fmt() 312 | .with_writer(std::io::stderr) // Print to stderr 313 | .with_env_filter( 314 | EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), 315 | ) 316 | .finish(); 317 | 318 | tracing::subscriber::set_global_default(subscriber).expect("Failed to set tracing subscriber"); 319 | } 320 | 321 | #[cfg(test)] 322 | mod test; 323 | -------------------------------------------------------------------------------- /src/root.rs: -------------------------------------------------------------------------------- 1 | mod dto; 2 | 3 | use std::collections::btree_map::Entry; 4 | use std::io::{self}; 5 | #[cfg(not(target_os = "macos"))] 6 | use std::os::unix::net::{UnixListener, UnixStream}; 7 | use std::path::{Path, PathBuf}; 8 | use std::time::Duration; 9 | use std::{fs, thread}; 10 | 11 | use anyhow::{bail, Result}; 12 | use chrono::Utc; 13 | use convi::ExpectFrom; 14 | use fs2::FileExt; 15 | use tracing::{debug, info, warn}; 16 | 17 | use crate::{util, LOG_TARGET}; 18 | 19 | /// Root directory of a cache 20 | pub struct Root { 21 | path: PathBuf, 22 | lock_file: fs::File, 23 | } 24 | 25 | impl Root { 26 | pub fn new(path: impl Into) -> Result { 27 | let path = path.into(); 28 | ensure_root_exists(&path)?; 29 | 30 | let lock = util::open_lock_file(&path)?; 31 | 32 | Ok(Self { 33 | path, 34 | lock_file: lock, 35 | }) 36 | } 37 | 38 | pub fn with_lock(&mut self, f: impl FnOnce(&mut LockedRoot) -> Result) -> Result { 39 | f(&mut LockedRoot::new(&self.path, &mut self.lock_file)?) 40 | } 41 | } 42 | 43 | fn ensure_root_exists(dir: &PathBuf) -> Result<()> { 44 | if !dir.try_exists()? { 45 | info!( 46 | target: LOG_TARGET, 47 | dir = %dir.display(), "Creating root dir" 48 | ); 49 | fs::create_dir_all(dir)?; 50 | } 51 | Ok(()) 52 | } 53 | 54 | /// A handle passed to `with_lock` argument after root was acquired 55 | pub struct LockedRoot<'a> { 56 | path: &'a PathBuf, 57 | lock_file: &'a mut fs::File, 58 | locked: bool, 59 | } 60 | 61 | impl<'a> Drop for LockedRoot<'a> { 62 | fn drop(&mut self) { 63 | if self.locked { 64 | let Ok(()) = self.lock_file.unlock() else { 65 | warn!("Failed to release the cache lock file"); 66 | return; 67 | }; 68 | self.locked = false; 69 | } 70 | } 71 | } 72 | 73 | impl<'a> LockedRoot<'a> { 74 | fn new(path: &'a PathBuf, lock_file: &'a mut fs::File) -> Result { 75 | let mut locked_root = Self { 76 | path, 77 | lock_file, 78 | locked: false, 79 | }; 80 | locked_root.lock()?; 81 | Ok(locked_root) 82 | } 83 | 84 | fn lock(&mut self) -> Result<()> { 85 | debug!( 86 | target: LOG_TARGET, 87 | path = %self.path.display(), "Acquiring cache lock..." 88 | ); 89 | if self.lock_file.try_lock_exclusive().is_err() { 90 | info!( 91 | target: LOG_TARGET, 92 | "Cache lock taken, waiting..." 93 | ); 94 | self.lock_file.lock_exclusive()?; 95 | }; 96 | debug!( 97 | target: LOG_TARGET, 98 | "Acquired cache lock" 99 | ); 100 | self.locked = true; 101 | Ok(()) 102 | } 103 | 104 | fn unlock(&mut self) -> Result<()> { 105 | self.ensure_locked()?; 106 | debug!( 107 | target: LOG_TARGET, 108 | path = %self.path.display(), "Releasing cache lock..." 109 | ); 110 | self.lock_file.unlock()?; 111 | self.locked = false; 112 | Ok(()) 113 | } 114 | 115 | fn data_file_path(&self) -> PathBuf { 116 | self.path.join("fs-dir-cache.json") 117 | } 118 | 119 | fn ensure_locked(&self) -> anyhow::Result<()> { 120 | if !self.locked { 121 | bail!("LockedRoot no longer valid"); 122 | } 123 | Ok(()) 124 | } 125 | 126 | pub fn r#yield(&mut self, duration: Duration) -> Result<()> { 127 | self.yield_with(|| { 128 | thread::sleep(duration); 129 | }) 130 | } 131 | 132 | pub fn r#yield_with(&mut self, f: F) -> Result 133 | where 134 | F: FnOnce() -> R, 135 | { 136 | self.unlock()?; 137 | let r = f(); 138 | self.lock()?; 139 | Ok(r) 140 | } 141 | 142 | pub fn load_data(&self) -> Result { 143 | self.ensure_locked()?; 144 | let path = self.data_file_path(); 145 | if !path.try_exists()? { 146 | return Ok(Default::default()); 147 | } 148 | Ok(serde_json::from_reader::<_, _>(std::fs::File::open(path)?)?) 149 | } 150 | 151 | pub fn store_data(&mut self, data: &dto::RootData) -> Result<()> { 152 | util::store_json_pretty_to_file(&self.data_file_path(), data) 153 | } 154 | 155 | pub fn lock_key( 156 | &mut self, 157 | key: &str, 158 | lock_id: &str, 159 | timeout_secs: f64, 160 | new_socket_path: Option, 161 | ) -> Result { 162 | let locking_start = Utc::now(); 163 | let mut had_to_wait = false; 164 | let data = loop { 165 | let mut data = self.load_data()?; 166 | 167 | let now = Utc::now(); 168 | match data.keys.entry(key.to_owned()) { 169 | Entry::Vacant(e) => { 170 | e.insert( 171 | dto::KeyData::new(now) 172 | .lock(now, lock_id, timeout_secs, new_socket_path.clone())? 173 | .to_owned(), 174 | ); 175 | break data; 176 | } 177 | Entry::Occupied(mut e) => { 178 | if let Some(prev_sock_path) = e.get().socket_path.as_ref() { 179 | if let Ok(s) = try_lock(prev_sock_path) { 180 | info!( 181 | target: LOG_TARGET, 182 | key, 183 | lock_id, 184 | sock_path = %prev_sock_path.display(), 185 | "Previous lock holder still alive (potentially)" 186 | ); 187 | had_to_wait |= true; 188 | self.r#yield_with(|| { 189 | let _ = clear_lock(s, prev_sock_path).inspect_err(|err| { 190 | info!( 191 | %err, 192 | "Error during waiting for / clearing the old lock" 193 | ) 194 | }); 195 | })?; 196 | } else { 197 | debug!( 198 | target: LOG_TARGET, 199 | key, 200 | lock_id, 201 | sock_path = %prev_sock_path.display(), 202 | "Previous lock holder gone" 203 | ); 204 | rm_prev_sock_path(prev_sock_path); 205 | e.get_mut().lock( 206 | now, 207 | lock_id, 208 | timeout_secs, 209 | new_socket_path.clone(), 210 | )?; 211 | break data; 212 | } 213 | } else if !e.get().is_timelocked(now) { 214 | debug!( 215 | target: LOG_TARGET, 216 | key, lock_id, "Previous lock expired" 217 | ); 218 | if let Some(prev_sock_path) = e.get().socket_path.as_ref() { 219 | rm_prev_sock_path(prev_sock_path); 220 | } 221 | e.get_mut() 222 | .lock(now, lock_id, timeout_secs, new_socket_path.clone())?; 223 | break data; 224 | } else { 225 | let expires_in_msecs = e.get().expires_in(now).num_milliseconds(); 226 | let duration = Duration::from_millis(u64::expect_from( 227 | (expires_in_msecs / 100).clamp(10, 10000), 228 | )); 229 | info!( 230 | target: LOG_TARGET, 231 | key, 232 | lock_id, 233 | expires_in_msecs, 234 | "Waiting for the key lock to be released..." 235 | ); 236 | had_to_wait |= true; 237 | self.r#yield(duration)?; 238 | } 239 | } 240 | } 241 | }; 242 | 243 | self.store_data(&data)?; 244 | 245 | if had_to_wait { 246 | info!( 247 | target: LOG_TARGET, 248 | key, 249 | lock_id, 250 | wait_secs=%Utc::now().signed_duration_since(locking_start).num_seconds(), 251 | "Acquired lock" 252 | ); 253 | } 254 | Ok(self.path.join(key)) 255 | } 256 | 257 | pub fn unlock_key(&mut self, key: &str, lock_id: String) -> Result<()> { 258 | let mut data = self.load_data()?; 259 | 260 | if let Some(key_data) = data.keys.get_mut(key) { 261 | if key_data.lock_id != lock_id { 262 | bail!( 263 | "Key {} lock id does not match; used = {}, owner = {}", 264 | key, 265 | lock_id, 266 | key_data.lock_id 267 | ); 268 | } 269 | let now = Utc::now(); 270 | if !key_data.is_timelocked(now) { 271 | warn!(key, "Lock already expired"); 272 | } 273 | key_data.unlock(now); 274 | self.store_data(&data)?; 275 | } else { 276 | bail!("Key {} does not exist", key); 277 | } 278 | 279 | Ok(()) 280 | } 281 | 282 | pub fn key_dir_path(&self, key: &str) -> PathBuf { 283 | self.path.join(key) 284 | } 285 | } 286 | 287 | fn rm_prev_sock_path(prev_sock_path: &Path) { 288 | if let Err(err) = fs::remove_file(prev_sock_path) { 289 | if err.kind() != io::ErrorKind::NotFound { 290 | warn!(target: LOG_TARGET, 291 | %err, 292 | sock_path = %prev_sock_path.display(), 293 | "Could not remove stale unix socket"); 294 | } 295 | } 296 | } 297 | 298 | // On Darwin Unix Sockets are not automatically removed, and linger, 299 | // with processes that try to connect to them just hanging. This makes them 300 | // unsuitable for our needs. Just use a file that we lock exclusively. 301 | #[cfg(target_os = "macos")] 302 | pub fn mk_lock(path: &Path) -> Result { 303 | let lock_file = fs::File::create(path)?; 304 | lock_file.lock_exclusive()?; 305 | Ok(lock_file) 306 | } 307 | 308 | // On Linux we can use Unix Sockets as they disappear automatically, 309 | // which is nice. 310 | #[cfg(not(target_os = "macos"))] 311 | pub fn mk_lock(path: &Path) -> Result { 312 | use std::os::unix::net::UnixStream; 313 | 314 | let socket = UnixListener::bind(path)?; 315 | 316 | assert!(UnixStream::connect(path).is_ok()); 317 | 318 | Ok(socket) 319 | } 320 | 321 | #[cfg(target_os = "macos")] 322 | pub fn try_lock(path: &Path) -> Result { 323 | let lock_file = fs::File::open(path)?; 324 | Ok(lock_file) 325 | } 326 | 327 | #[cfg(not(target_os = "macos"))] 328 | pub fn try_lock(path: &Path) -> Result { 329 | let socket = UnixStream::connect(path)?; 330 | 331 | Ok(socket) 332 | } 333 | 334 | #[cfg(target_os = "macos")] 335 | pub fn clear_lock(file: fs::File, path: &Path) -> Result<()> { 336 | file.lock_exclusive()?; 337 | 338 | // We want to unlock the file, even if we failed to remove it 339 | let rm_res = fs::remove_file(path); 340 | 341 | file.unlock()?; 342 | 343 | // if removing failed, report it now 344 | rm_res?; 345 | 346 | Ok(()) 347 | } 348 | 349 | #[cfg(not(target_os = "macos"))] 350 | pub fn clear_lock(mut s: UnixStream, _path: &Path) -> Result<()> { 351 | // we are just waiting to get disconnected here 352 | 353 | use std::io::Read as _; 354 | 355 | // ignore, we *will* disconnect, nothing interesting about it 356 | let _ = s.read(&mut [0]); 357 | 358 | Ok(()) 359 | } 360 | -------------------------------------------------------------------------------- /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 = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "android-tzdata" 16 | version = "0.1.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 19 | 20 | [[package]] 21 | name = "android_system_properties" 22 | version = "0.1.5" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 25 | dependencies = [ 26 | "libc", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.17" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "is_terminal_polyfill", 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle" 46 | version = "1.0.10" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.6" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.1.2" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 64 | dependencies = [ 65 | "windows-sys 0.59.0", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "3.0.6" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 73 | dependencies = [ 74 | "anstyle", 75 | "windows-sys 0.59.0", 76 | ] 77 | 78 | [[package]] 79 | name = "anyhow" 80 | version = "1.0.92" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13" 83 | 84 | [[package]] 85 | name = "arrayref" 86 | version = "0.3.9" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" 89 | 90 | [[package]] 91 | name = "arrayvec" 92 | version = "0.7.6" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 95 | 96 | [[package]] 97 | name = "assert_cmd" 98 | version = "2.0.16" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" 101 | dependencies = [ 102 | "anstyle", 103 | "bstr", 104 | "doc-comment", 105 | "libc", 106 | "predicates", 107 | "predicates-core", 108 | "predicates-tree", 109 | "wait-timeout", 110 | ] 111 | 112 | [[package]] 113 | name = "autocfg" 114 | version = "1.4.0" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 117 | 118 | [[package]] 119 | name = "bitflags" 120 | version = "2.6.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 123 | 124 | [[package]] 125 | name = "blake3" 126 | version = "1.5.4" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" 129 | dependencies = [ 130 | "arrayref", 131 | "arrayvec", 132 | "cc", 133 | "cfg-if", 134 | "constant_time_eq", 135 | ] 136 | 137 | [[package]] 138 | name = "bstr" 139 | version = "1.10.0" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" 142 | dependencies = [ 143 | "memchr", 144 | "regex-automata 0.4.8", 145 | "serde", 146 | ] 147 | 148 | [[package]] 149 | name = "bumpalo" 150 | version = "3.16.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 153 | 154 | [[package]] 155 | name = "byteorder" 156 | version = "1.5.0" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 159 | 160 | [[package]] 161 | name = "cc" 162 | version = "1.1.31" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" 165 | dependencies = [ 166 | "shlex", 167 | ] 168 | 169 | [[package]] 170 | name = "cfg-if" 171 | version = "1.0.0" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 174 | 175 | [[package]] 176 | name = "chrono" 177 | version = "0.4.38" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 180 | dependencies = [ 181 | "android-tzdata", 182 | "iana-time-zone", 183 | "js-sys", 184 | "num-traits", 185 | "serde", 186 | "wasm-bindgen", 187 | "windows-targets", 188 | ] 189 | 190 | [[package]] 191 | name = "clap" 192 | version = "4.5.20" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" 195 | dependencies = [ 196 | "clap_builder", 197 | "clap_derive", 198 | ] 199 | 200 | [[package]] 201 | name = "clap_builder" 202 | version = "4.5.20" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" 205 | dependencies = [ 206 | "anstream", 207 | "anstyle", 208 | "clap_lex", 209 | "strsim", 210 | ] 211 | 212 | [[package]] 213 | name = "clap_derive" 214 | version = "4.5.18" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 217 | dependencies = [ 218 | "heck", 219 | "proc-macro2", 220 | "quote", 221 | "syn", 222 | ] 223 | 224 | [[package]] 225 | name = "clap_lex" 226 | version = "0.7.2" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" 229 | 230 | [[package]] 231 | name = "colorchoice" 232 | version = "1.0.3" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 235 | 236 | [[package]] 237 | name = "constant_time_eq" 238 | version = "0.3.1" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" 241 | 242 | [[package]] 243 | name = "convi" 244 | version = "0.0.7" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "224cf0d105c40605a6297922765b9d5ecaf0798a3006f29962208457dd2122ec" 247 | 248 | [[package]] 249 | name = "core-foundation-sys" 250 | version = "0.8.7" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 253 | 254 | [[package]] 255 | name = "difflib" 256 | version = "0.4.0" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 259 | 260 | [[package]] 261 | name = "doc-comment" 262 | version = "0.3.3" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 265 | 266 | [[package]] 267 | name = "errno" 268 | version = "0.3.12" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" 271 | dependencies = [ 272 | "libc", 273 | "windows-sys 0.59.0", 274 | ] 275 | 276 | [[package]] 277 | name = "fastrand" 278 | version = "2.1.1" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" 281 | 282 | [[package]] 283 | name = "fs-dir-cache" 284 | version = "0.1.0" 285 | dependencies = [ 286 | "anyhow", 287 | "assert_cmd", 288 | "blake3", 289 | "chrono", 290 | "clap", 291 | "convi", 292 | "fs2", 293 | "rand", 294 | "serde", 295 | "serde_json", 296 | "tempfile", 297 | "tracing", 298 | "tracing-subscriber", 299 | ] 300 | 301 | [[package]] 302 | name = "fs2" 303 | version = "0.4.3" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" 306 | dependencies = [ 307 | "libc", 308 | "winapi", 309 | ] 310 | 311 | [[package]] 312 | name = "getrandom" 313 | version = "0.2.15" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 316 | dependencies = [ 317 | "cfg-if", 318 | "libc", 319 | "wasi", 320 | ] 321 | 322 | [[package]] 323 | name = "heck" 324 | version = "0.5.0" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 327 | 328 | [[package]] 329 | name = "iana-time-zone" 330 | version = "0.1.61" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 333 | dependencies = [ 334 | "android_system_properties", 335 | "core-foundation-sys", 336 | "iana-time-zone-haiku", 337 | "js-sys", 338 | "wasm-bindgen", 339 | "windows-core", 340 | ] 341 | 342 | [[package]] 343 | name = "iana-time-zone-haiku" 344 | version = "0.1.2" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 347 | dependencies = [ 348 | "cc", 349 | ] 350 | 351 | [[package]] 352 | name = "is_terminal_polyfill" 353 | version = "1.70.1" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 356 | 357 | [[package]] 358 | name = "itoa" 359 | version = "1.0.11" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 362 | 363 | [[package]] 364 | name = "js-sys" 365 | version = "0.3.72" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" 368 | dependencies = [ 369 | "wasm-bindgen", 370 | ] 371 | 372 | [[package]] 373 | name = "lazy_static" 374 | version = "1.5.0" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 377 | 378 | [[package]] 379 | name = "libc" 380 | version = "0.2.172" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 383 | 384 | [[package]] 385 | name = "linux-raw-sys" 386 | version = "0.4.14" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 389 | 390 | [[package]] 391 | name = "log" 392 | version = "0.4.22" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 395 | 396 | [[package]] 397 | name = "matchers" 398 | version = "0.1.0" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 401 | dependencies = [ 402 | "regex-automata 0.1.10", 403 | ] 404 | 405 | [[package]] 406 | name = "memchr" 407 | version = "2.7.4" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 410 | 411 | [[package]] 412 | name = "nu-ansi-term" 413 | version = "0.46.0" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 416 | dependencies = [ 417 | "overload", 418 | "winapi", 419 | ] 420 | 421 | [[package]] 422 | name = "num-traits" 423 | version = "0.2.19" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 426 | dependencies = [ 427 | "autocfg", 428 | ] 429 | 430 | [[package]] 431 | name = "once_cell" 432 | version = "1.20.2" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 435 | 436 | [[package]] 437 | name = "overload" 438 | version = "0.1.1" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 441 | 442 | [[package]] 443 | name = "pin-project-lite" 444 | version = "0.2.15" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" 447 | 448 | [[package]] 449 | name = "ppv-lite86" 450 | version = "0.2.20" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 453 | dependencies = [ 454 | "zerocopy", 455 | ] 456 | 457 | [[package]] 458 | name = "predicates" 459 | version = "3.1.2" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" 462 | dependencies = [ 463 | "anstyle", 464 | "difflib", 465 | "predicates-core", 466 | ] 467 | 468 | [[package]] 469 | name = "predicates-core" 470 | version = "1.0.8" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" 473 | 474 | [[package]] 475 | name = "predicates-tree" 476 | version = "1.0.11" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" 479 | dependencies = [ 480 | "predicates-core", 481 | "termtree", 482 | ] 483 | 484 | [[package]] 485 | name = "proc-macro2" 486 | version = "1.0.89" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" 489 | dependencies = [ 490 | "unicode-ident", 491 | ] 492 | 493 | [[package]] 494 | name = "quote" 495 | version = "1.0.37" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 498 | dependencies = [ 499 | "proc-macro2", 500 | ] 501 | 502 | [[package]] 503 | name = "rand" 504 | version = "0.8.5" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 507 | dependencies = [ 508 | "libc", 509 | "rand_chacha", 510 | "rand_core", 511 | ] 512 | 513 | [[package]] 514 | name = "rand_chacha" 515 | version = "0.3.1" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 518 | dependencies = [ 519 | "ppv-lite86", 520 | "rand_core", 521 | ] 522 | 523 | [[package]] 524 | name = "rand_core" 525 | version = "0.6.4" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 528 | dependencies = [ 529 | "getrandom", 530 | ] 531 | 532 | [[package]] 533 | name = "regex" 534 | version = "1.11.1" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 537 | dependencies = [ 538 | "aho-corasick", 539 | "memchr", 540 | "regex-automata 0.4.8", 541 | "regex-syntax 0.8.5", 542 | ] 543 | 544 | [[package]] 545 | name = "regex-automata" 546 | version = "0.1.10" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 549 | dependencies = [ 550 | "regex-syntax 0.6.29", 551 | ] 552 | 553 | [[package]] 554 | name = "regex-automata" 555 | version = "0.4.8" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" 558 | dependencies = [ 559 | "aho-corasick", 560 | "memchr", 561 | "regex-syntax 0.8.5", 562 | ] 563 | 564 | [[package]] 565 | name = "regex-syntax" 566 | version = "0.6.29" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 569 | 570 | [[package]] 571 | name = "regex-syntax" 572 | version = "0.8.5" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 575 | 576 | [[package]] 577 | name = "rustix" 578 | version = "0.38.38" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" 581 | dependencies = [ 582 | "bitflags", 583 | "errno", 584 | "libc", 585 | "linux-raw-sys", 586 | "windows-sys 0.52.0", 587 | ] 588 | 589 | [[package]] 590 | name = "ryu" 591 | version = "1.0.18" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 594 | 595 | [[package]] 596 | name = "serde" 597 | version = "1.0.214" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" 600 | dependencies = [ 601 | "serde_derive", 602 | ] 603 | 604 | [[package]] 605 | name = "serde_derive" 606 | version = "1.0.214" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" 609 | dependencies = [ 610 | "proc-macro2", 611 | "quote", 612 | "syn", 613 | ] 614 | 615 | [[package]] 616 | name = "serde_json" 617 | version = "1.0.132" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" 620 | dependencies = [ 621 | "itoa", 622 | "memchr", 623 | "ryu", 624 | "serde", 625 | ] 626 | 627 | [[package]] 628 | name = "sharded-slab" 629 | version = "0.1.7" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 632 | dependencies = [ 633 | "lazy_static", 634 | ] 635 | 636 | [[package]] 637 | name = "shlex" 638 | version = "1.3.0" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 641 | 642 | [[package]] 643 | name = "smallvec" 644 | version = "1.13.2" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 647 | 648 | [[package]] 649 | name = "strsim" 650 | version = "0.11.1" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 653 | 654 | [[package]] 655 | name = "syn" 656 | version = "2.0.86" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" 659 | dependencies = [ 660 | "proc-macro2", 661 | "quote", 662 | "unicode-ident", 663 | ] 664 | 665 | [[package]] 666 | name = "tempfile" 667 | version = "3.13.0" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" 670 | dependencies = [ 671 | "cfg-if", 672 | "fastrand", 673 | "once_cell", 674 | "rustix", 675 | "windows-sys 0.59.0", 676 | ] 677 | 678 | [[package]] 679 | name = "termtree" 680 | version = "0.4.1" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" 683 | 684 | [[package]] 685 | name = "thread_local" 686 | version = "1.1.8" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 689 | dependencies = [ 690 | "cfg-if", 691 | "once_cell", 692 | ] 693 | 694 | [[package]] 695 | name = "tracing" 696 | version = "0.1.40" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 699 | dependencies = [ 700 | "pin-project-lite", 701 | "tracing-attributes", 702 | "tracing-core", 703 | ] 704 | 705 | [[package]] 706 | name = "tracing-attributes" 707 | version = "0.1.27" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 710 | dependencies = [ 711 | "proc-macro2", 712 | "quote", 713 | "syn", 714 | ] 715 | 716 | [[package]] 717 | name = "tracing-core" 718 | version = "0.1.32" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 721 | dependencies = [ 722 | "once_cell", 723 | "valuable", 724 | ] 725 | 726 | [[package]] 727 | name = "tracing-log" 728 | version = "0.2.0" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 731 | dependencies = [ 732 | "log", 733 | "once_cell", 734 | "tracing-core", 735 | ] 736 | 737 | [[package]] 738 | name = "tracing-subscriber" 739 | version = "0.3.18" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" 742 | dependencies = [ 743 | "matchers", 744 | "nu-ansi-term", 745 | "once_cell", 746 | "regex", 747 | "sharded-slab", 748 | "smallvec", 749 | "thread_local", 750 | "tracing", 751 | "tracing-core", 752 | "tracing-log", 753 | ] 754 | 755 | [[package]] 756 | name = "unicode-ident" 757 | version = "1.0.13" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 760 | 761 | [[package]] 762 | name = "utf8parse" 763 | version = "0.2.2" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 766 | 767 | [[package]] 768 | name = "valuable" 769 | version = "0.1.0" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 772 | 773 | [[package]] 774 | name = "wait-timeout" 775 | version = "0.2.0" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 778 | dependencies = [ 779 | "libc", 780 | ] 781 | 782 | [[package]] 783 | name = "wasi" 784 | version = "0.11.0+wasi-snapshot-preview1" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 787 | 788 | [[package]] 789 | name = "wasm-bindgen" 790 | version = "0.2.95" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" 793 | dependencies = [ 794 | "cfg-if", 795 | "once_cell", 796 | "wasm-bindgen-macro", 797 | ] 798 | 799 | [[package]] 800 | name = "wasm-bindgen-backend" 801 | version = "0.2.95" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" 804 | dependencies = [ 805 | "bumpalo", 806 | "log", 807 | "once_cell", 808 | "proc-macro2", 809 | "quote", 810 | "syn", 811 | "wasm-bindgen-shared", 812 | ] 813 | 814 | [[package]] 815 | name = "wasm-bindgen-macro" 816 | version = "0.2.95" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" 819 | dependencies = [ 820 | "quote", 821 | "wasm-bindgen-macro-support", 822 | ] 823 | 824 | [[package]] 825 | name = "wasm-bindgen-macro-support" 826 | version = "0.2.95" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" 829 | dependencies = [ 830 | "proc-macro2", 831 | "quote", 832 | "syn", 833 | "wasm-bindgen-backend", 834 | "wasm-bindgen-shared", 835 | ] 836 | 837 | [[package]] 838 | name = "wasm-bindgen-shared" 839 | version = "0.2.95" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" 842 | 843 | [[package]] 844 | name = "winapi" 845 | version = "0.3.9" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 848 | dependencies = [ 849 | "winapi-i686-pc-windows-gnu", 850 | "winapi-x86_64-pc-windows-gnu", 851 | ] 852 | 853 | [[package]] 854 | name = "winapi-i686-pc-windows-gnu" 855 | version = "0.4.0" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 858 | 859 | [[package]] 860 | name = "winapi-x86_64-pc-windows-gnu" 861 | version = "0.4.0" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 864 | 865 | [[package]] 866 | name = "windows-core" 867 | version = "0.52.0" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 870 | dependencies = [ 871 | "windows-targets", 872 | ] 873 | 874 | [[package]] 875 | name = "windows-sys" 876 | version = "0.52.0" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 879 | dependencies = [ 880 | "windows-targets", 881 | ] 882 | 883 | [[package]] 884 | name = "windows-sys" 885 | version = "0.59.0" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 888 | dependencies = [ 889 | "windows-targets", 890 | ] 891 | 892 | [[package]] 893 | name = "windows-targets" 894 | version = "0.52.6" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 897 | dependencies = [ 898 | "windows_aarch64_gnullvm", 899 | "windows_aarch64_msvc", 900 | "windows_i686_gnu", 901 | "windows_i686_gnullvm", 902 | "windows_i686_msvc", 903 | "windows_x86_64_gnu", 904 | "windows_x86_64_gnullvm", 905 | "windows_x86_64_msvc", 906 | ] 907 | 908 | [[package]] 909 | name = "windows_aarch64_gnullvm" 910 | version = "0.52.6" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 913 | 914 | [[package]] 915 | name = "windows_aarch64_msvc" 916 | version = "0.52.6" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 919 | 920 | [[package]] 921 | name = "windows_i686_gnu" 922 | version = "0.52.6" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 925 | 926 | [[package]] 927 | name = "windows_i686_gnullvm" 928 | version = "0.52.6" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 931 | 932 | [[package]] 933 | name = "windows_i686_msvc" 934 | version = "0.52.6" 935 | source = "registry+https://github.com/rust-lang/crates.io-index" 936 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 937 | 938 | [[package]] 939 | name = "windows_x86_64_gnu" 940 | version = "0.52.6" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 943 | 944 | [[package]] 945 | name = "windows_x86_64_gnullvm" 946 | version = "0.52.6" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 949 | 950 | [[package]] 951 | name = "windows_x86_64_msvc" 952 | version = "0.52.6" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 955 | 956 | [[package]] 957 | name = "zerocopy" 958 | version = "0.7.35" 959 | source = "registry+https://github.com/rust-lang/crates.io-index" 960 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 961 | dependencies = [ 962 | "byteorder", 963 | "zerocopy-derive", 964 | ] 965 | 966 | [[package]] 967 | name = "zerocopy-derive" 968 | version = "0.7.35" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 971 | dependencies = [ 972 | "proc-macro2", 973 | "quote", 974 | "syn", 975 | ] 976 | --------------------------------------------------------------------------------