├── .config ├── semgrep.yaml └── flakebox │ ├── id │ └── shellHook.sh ├── .envrc ├── .gitignore ├── _typos.toml ├── misc └── git-hooks │ ├── commit-template.txt │ ├── commit-msg │ └── pre-commit ├── .rustfmt.toml ├── rustfmt.toml ├── terraform ├── remote_s3_upload │ ├── bin │ │ └── md5-out-wrap │ └── main.tf ├── remote_s3 │ └── main.tf ├── install │ └── main.tf ├── store_s3 │ └── main.tf ├── instance │ └── main.tf └── instance_multi │ └── main.tf ├── src ├── opts.rs ├── misc.rs ├── data_dir.rs ├── config.rs ├── bin │ └── npcnix.rs └── lib.rs ├── Cargo.toml ├── .github └── workflows │ ├── flakebox-flakehub-publish.yml │ └── flakebox-ci.yml ├── modules └── npcnix.nix ├── justfile ├── flake.nix ├── README.md ├── flake.lock └── Cargo.lock /.config/semgrep.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.direnv 3 | /result 4 | -------------------------------------------------------------------------------- /_typos.toml: -------------------------------------------------------------------------------- 1 | [default.extend-words] 2 | substituters = "substituters" 3 | -------------------------------------------------------------------------------- /misc/git-hooks/commit-template.txt: -------------------------------------------------------------------------------- 1 | 2 | # Explain *why* this change is being made width limit ->| 3 | -------------------------------------------------------------------------------- /.config/flakebox/id: -------------------------------------------------------------------------------- 1 | b5effc88dcb7b75eed2d40277d5948a7050c82f2fe3d4a18a9b471c22631f9ea3d4f4e7d31f2f8205a457c722a5aa4ab9210908fc9ce183dcb95ce71a1991142 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 | -------------------------------------------------------------------------------- /terraform/remote_s3_upload/bin/md5-out-wrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | if [ -z "$1" ]; then 6 | >&2 echo "Missing path" 7 | exit 1 8 | fi 9 | 10 | path="$1" 11 | shift 1 12 | 13 | rm -f "$path" || true 14 | 15 | "$@" 16 | 17 | echo '{"md5sum": "'"$(md5sum "$path" | awk '{ print $1 }')"'","path":"'"$path"'"}' 18 | -------------------------------------------------------------------------------- /src/opts.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::Parser; 4 | 5 | use crate::data_dir::DataDir; 6 | 7 | #[derive(Parser, Debug, Clone)] 8 | pub struct Common { 9 | #[arg(long, env = "NPCNIX_DATA_DIR", default_value = "/var/lib/npcnix")] 10 | data_dir: PathBuf, 11 | } 12 | 13 | impl Common { 14 | pub fn data_dir(&self) -> DataDir { 15 | DataDir::new(&self.data_dir) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /terraform/remote_s3/main.tf: -------------------------------------------------------------------------------- 1 | variable "store" { 2 | description = "npcnix_s3_store module to use" 3 | } 4 | 5 | variable "name" { 6 | description = "Name of the file to put in the store (e.g. 'dev.npcnix')" 7 | } 8 | 9 | locals { 10 | filename = "${var.name}.npcnix" 11 | } 12 | 13 | output "filename" { 14 | value = local.filename 15 | } 16 | 17 | output "store" { 18 | value = var.store 19 | } 20 | 21 | output "bucket" { 22 | value = var.store.bucket 23 | } 24 | 25 | output "key" { 26 | value = "${var.store.prefix}/${local.filename}" 27 | } 28 | 29 | output "url" { 30 | value = "s3://${var.store.bucket.id}/${var.store.prefix}/${local.filename}" 31 | } 32 | 33 | output "region" { 34 | value = var.store.region 35 | } 36 | -------------------------------------------------------------------------------- /misc/git-hooks/commit-msg: -------------------------------------------------------------------------------- 1 | #!/nix/store/8fv91097mbh5049i9rglc73dx6kjg3qk-bash-5.2-p15/bin/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 recommit to fix your commit" 22 | exit 1 23 | fi 24 | -------------------------------------------------------------------------------- /.config/flakebox/shellHook.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | dot_git="$(git rev-parse --git-common-dir)" 3 | if [[ ! -d "${dot_git}/hooks" ]]; then mkdir -p "${dot_git}/hooks"; fi 4 | rm -f "${dot_git}/hooks/commit-msg" 5 | ln -sf "$(pwd)/misc/git-hooks/commit-msg" "${dot_git}/hooks/commit-msg" 6 | 7 | dot_git="$(git rev-parse --git-common-dir)" 8 | if [[ ! -d "${dot_git}/hooks" ]]; then mkdir -p "${dot_git}/hooks"; fi 9 | rm -f "${dot_git}/hooks/pre-commit" 10 | ln -sf "$(pwd)/misc/git-hooks/pre-commit" "${dot_git}/hooks/pre-commit" 11 | 12 | # set template 13 | git config commit.template misc/git-hooks/commit-template.txt 14 | 15 | if [ -n "${DIRENV_IN_ENVRC:-}" ]; then 16 | # and not set DIRENV_LOG_FORMAT 17 | if [ -n "${DIRENV_LOG_FORMAT:-}" ]; then 18 | >&2 echo "💡 Set 'DIRENV_LOG_FORMAT=\"\"' in your shell environment variables for a cleaner output of direnv" 19 | fi 20 | fi 21 | 22 | >&2 echo "💡 Run 'just' for a list of available 'just ...' helper recipes" 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "npcnix" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [profile.release] 9 | opt-level = "z" 10 | lto = true 11 | strip = "debuginfo" 12 | debug = 0 13 | 14 | [dependencies] 15 | anyhow = "1.0.70" 16 | chrono = { version = "0.4.24", features = ["serde", "clock"] } 17 | clap = { version = "4.2.1", features = ["derive", "env"] } 18 | fd-lock = "3.0.12" 19 | md-5 = "0.10.5" 20 | # log = { version = "0.4.17", features = ["kv_unstable"] } 21 | rand = "0.8.5" 22 | serde = { version = "1.0.160", features = ["derive"] } 23 | serde_json = "1.0.95" 24 | signal-hook = "0.3.15" 25 | tar = "0.4.38" 26 | tempfile = "3.5.0" 27 | tracing = "0.1.37" 28 | tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } 29 | ureq = { version = "2.6.2", features = ["rustls-native-certs"] } 30 | url = { version = "2.3.1", features = ["serde"] } 31 | zstd = "0.12.3" 32 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/misc.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | use std::path::Path; 3 | 4 | use serde::Serialize; 5 | 6 | pub fn store_json_pretty_to_file(path: &Path, val: &T) -> anyhow::Result<()> 7 | where 8 | T: Serialize, 9 | { 10 | Ok(store_to_file_with(path, |f| { 11 | serde_json::to_writer_pretty(f, val).map_err(Into::into) 12 | }) 13 | .and_then(|res| res)?) 14 | } 15 | 16 | pub fn store_str_to_file(path: &Path, s: &str) -> io::Result<()> { 17 | store_to_file_with(path, |f| f.write_all(s.as_bytes())).and_then(|res| res) 18 | } 19 | 20 | pub fn store_to_file_with(path: &Path, f: F) -> io::Result> 21 | where 22 | F: Fn(&mut dyn io::Write) -> Result<(), E>, 23 | { 24 | std::fs::create_dir_all(path.parent().expect("Not a root path"))?; 25 | let tmp_path = path.with_extension("tmp"); 26 | let mut file = std::fs::File::create(&tmp_path)?; 27 | if let Err(e) = f(&mut file) { 28 | return Ok(Err(e)); 29 | } 30 | file.flush()?; 31 | file.sync_data()?; 32 | drop(file); 33 | std::fs::rename(tmp_path, path)?; 34 | Ok(Ok(())) 35 | } 36 | -------------------------------------------------------------------------------- /terraform/remote_s3_upload/main.tf: -------------------------------------------------------------------------------- 1 | variable "remote" { 2 | } 3 | 4 | variable "local_dst_dir" { 5 | default = null 6 | } 7 | 8 | variable "flake_dir" { 9 | description = "Directory containing flake.nix file to upload (e.g. '../../nixos')" 10 | } 11 | 12 | variable "include" { 13 | description = "Subdirectories to include" 14 | type = list(string) 15 | } 16 | 17 | locals { 18 | local_dst_dir = var.local_dst_dir != null ? var.local_dst_dir : path.root 19 | } 20 | 21 | # generate nixos config file 22 | data "external" "npcnix-pack" { 23 | program = concat( 24 | ["${path.module}/bin/md5-out-wrap", "${local.local_dst_dir}/${var.remote.filename}"], 25 | ["npcnix", "pack", "--src", "${var.flake_dir}", "--dst", "${local.local_dst_dir}/${var.remote.filename}"], 26 | flatten([for dir in var.include : ["--include", dir]]) 27 | ) 28 | } 29 | 30 | 31 | # upload the config to npcnix location 32 | resource "aws_s3_object" "remote" { 33 | bucket = var.remote.store.bucket.id 34 | key = "${var.remote.store.prefix}/${var.remote.filename}" 35 | 36 | source = data.external.npcnix-pack.result.path 37 | etag = data.external.npcnix-pack.result.md5sum 38 | } 39 | -------------------------------------------------------------------------------- /terraform/install/main.tf: -------------------------------------------------------------------------------- 1 | variable "npcnix_install_url" { 2 | type = string 3 | default = "github:rustshop/npcnix?rev=421c1f3c38bed2ca4af54a659d6e64e71e5e146c#install" 4 | } 5 | 6 | variable "extra_substituters" { 7 | type = list(string) 8 | default = ["https://rustshop.cachix.org"] 9 | } 10 | 11 | variable "extra_trusted_public_keys" { 12 | type = list(string) 13 | default = ["rustshop.cachix.org-1:VD3xhDANGzOZTKuGPHcW7KOTZS0DPoPQSxXB00Yt0ZQ="] 14 | } 15 | 16 | output "install_script" { 17 | value = <&2 echo "Typos found: Valid new words can be added to '.typos.toml'" 78 | return 1 79 | fi 80 | 81 | # fix all typos 82 | [no-exit-message] 83 | typos-fix-all: 84 | just typos -w 85 | 86 | # THIS FILE IS AUTOGENERATED FROM FLAKEBOX CONFIGURATION 87 | -------------------------------------------------------------------------------- /.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 flake check .# 15 | 16 | ' 17 | strategy: 18 | matrix: 19 | host: 20 | - macos 21 | - linux 22 | include: 23 | - host: linux 24 | runs-on: ubuntu-latest 25 | timeout: 60 26 | - host: macos 27 | runs-on: macos-12 28 | timeout: 60 29 | timeout-minutes: ${{ matrix.timeout }} 30 | flake: 31 | name: Flake self-check 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Check Nix flake inputs 36 | uses: DeterminateSystems/flake-checker-action@v5 37 | with: 38 | fail-mode: true 39 | lint: 40 | name: Lint 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v4 44 | - name: Install Nix 45 | uses: DeterminateSystems/nix-installer-action@v4 46 | - name: Magic Nix Cache 47 | uses: DeterminateSystems/magic-nix-cache-action@v2 48 | - name: Cargo Cache 49 | uses: actions/cache@v3 50 | with: 51 | key: ${{ runner.os }}-${{ hashFiles('Cargo.lock') }} 52 | path: ~/.cargo 53 | - name: Commit Check 54 | run: '# run the same check that git `pre-commit` hook does 55 | 56 | nix develop --ignore-environment .# --command ./misc/git-hooks/pre-commit 57 | 58 | ' 59 | name: CI 60 | 'on': 61 | merge_group: 62 | branches: 63 | - master 64 | - main 65 | pull_request: 66 | branches: 67 | - master 68 | - main 69 | push: 70 | branches: 71 | - master 72 | - main 73 | tags: 74 | - v* 75 | workflow_dispatch: {} 76 | 77 | 78 | # THIS FILE IS AUTOGENERATED FROM FLAKEBOX CONFIGURATION 79 | -------------------------------------------------------------------------------- /src/data_dir.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use anyhow::Context; 5 | use url::Url; 6 | 7 | use crate::config; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct DataDir { 11 | path: PathBuf, 12 | } 13 | 14 | impl DataDir { 15 | pub fn new(path: &Path) -> Self { 16 | Self { 17 | path: path.to_owned(), 18 | } 19 | } 20 | 21 | pub fn activate_lock(&self) -> anyhow::Result>> { 22 | if self.config_exist()? { 23 | Ok(Some(fd_lock::RwLock::new(fs::File::create( 24 | self.path.join("activate.lock"), 25 | )?))) 26 | } else { 27 | Ok(None) 28 | } 29 | } 30 | 31 | /// Load currently configured `remote` from config if not overridden 32 | pub fn get_current_remote_with_opt_override( 33 | &self, 34 | remote: Option<&Url>, 35 | ) -> anyhow::Result { 36 | remote 37 | .cloned() 38 | .ok_or(()) 39 | .or_else(|_| -> anyhow::Result { Ok(self.load_config()?.remote()?.clone()) }) 40 | } 41 | 42 | /// Load currently configured `configuration` if not overridden 43 | pub fn get_current_configuration_with_opt_override( 44 | &self, 45 | configuration: Option<&str>, 46 | ) -> anyhow::Result { 47 | configuration 48 | .map(ToOwned::to_owned) 49 | .ok_or(()) 50 | .or_else(|_| -> anyhow::Result { 51 | Ok(self.load_config()?.configuration()?.to_owned()) 52 | }) 53 | } 54 | 55 | fn config_file_path(&self) -> PathBuf { 56 | self.path.join("config.json") 57 | } 58 | 59 | pub fn config_exist(&self) -> anyhow::Result { 60 | Ok(self.config_file_path().try_exists()?) 61 | } 62 | 63 | pub fn load_config(&self) -> anyhow::Result { 64 | let config_path = self.config_file_path(); 65 | if config_path.exists() { 66 | config::Config::load(&self.config_file_path()).context("Failed to load config") 67 | } else { 68 | Ok(Default::default()) 69 | } 70 | } 71 | 72 | pub fn store_config(&self, config: &config::Config) -> anyhow::Result<()> { 73 | fs::create_dir_all(&self.path) 74 | .with_context(|| format!("Failed to create data directory: {}", self.path.display()))?; 75 | config 76 | .store(&self.config_file_path()) 77 | .context("Failed to store config") 78 | } 79 | 80 | pub fn update_last_reconfiguration( 81 | &self, 82 | configuration: &str, 83 | etag: &str, 84 | ) -> anyhow::Result<()> { 85 | self.store_config( 86 | &self 87 | .load_config()? 88 | .with_updated_last_reconfiguration(configuration, etag), 89 | ) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /misc/git-hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/nix/store/8fv91097mbh5049i9rglc73dx6kjg3qk-bash-5.2-p15/bin/bash 2 | #!/usr/bin/env bash 3 | 4 | set -euo pipefail 5 | 6 | set +e 7 | git diff-files --quiet 8 | is_unclean=$? 9 | set -e 10 | 11 | # Revert `git stash` on exit 12 | function revert_git_stash { 13 | >&2 echo "Unstashing uncommitted changes..." 14 | git stash pop -q 15 | } 16 | 17 | # Stash pending changes and revert them when script ends 18 | if [ -z "${NO_STASH:-}" ] && [ $is_unclean -ne 0 ]; then 19 | >&2 echo "Stashing uncommitted changes..." 20 | GIT_LITERAL_PATHSPECS=0 git stash -q --keep-index 21 | trap revert_git_stash EXIT 22 | fi 23 | 24 | export FLAKEBOX_GIT_LS 25 | FLAKEBOX_GIT_LS="$(git ls-files)" 26 | export FLAKEBOX_GIT_LS_TEXT 27 | FLAKEBOX_GIT_LS_TEXT="$(echo "$FLAKEBOX_GIT_LS" | grep -v -E "\.(png|ods|jpg|jpeg|woff2|keystore|wasm|ttf|jar|ico|gif)\$")" 28 | 29 | 30 | function check_nothing() { 31 | true 32 | } 33 | export -f check_nothing 34 | 35 | function check_cargo_fmt() { 36 | set -euo pipefail 37 | 38 | cargo fmt --all --check 39 | 40 | } 41 | export -f check_cargo_fmt 42 | 43 | function check_cargo_lock() { 44 | set -euo pipefail 45 | 46 | # https://users.rust-lang.org/t/check-if-the-cargo-lock-is-up-to-date-without-building-anything/91048/5 47 | cargo update --workspace --locked 48 | 49 | } 50 | export -f check_cargo_lock 51 | 52 | function check_leftover_dbg() { 53 | set -euo pipefail 54 | 55 | errors="" 56 | for path in $(echo "$FLAKEBOX_GIT_LS_TEXT" | grep '.*\.rs'); do 57 | if grep 'dbg!(' "$path" > /dev/null; then 58 | >&2 echo "$path contains dbg! macro" 59 | errors="true" 60 | fi 61 | done 62 | 63 | if [ -n "$errors" ]; then 64 | >&2 echo "Fix the problems above or use --no-verify" 1>&2 65 | return 1 66 | fi 67 | 68 | } 69 | export -f check_leftover_dbg 70 | 71 | function check_semgrep() { 72 | set -euo pipefail 73 | 74 | # semgrep is not available on MacOS 75 | if ! command -v semgrep > /dev/null ; then 76 | >&2 echo "Skipping semgrep check: not available" 77 | return 0 78 | fi 79 | 80 | if [ ! -f .config/semgrep.yaml ] ; then 81 | >&2 echo "Skipping semgrep check: .config/semgrep.yaml doesn't exist" 82 | return 0 83 | fi 84 | 85 | if [ ! -s .config/semgrep.yaml ] ; then 86 | >&2 echo "Skipping semgrep check: .config/semgrep.yaml empty" 87 | return 0 88 | fi 89 | 90 | env SEMGREP_ENABLE_VERSION_CHECK=0 \ 91 | semgrep -q --error --config .config/semgrep.yaml 92 | 93 | } 94 | export -f check_semgrep 95 | 96 | function check_shellcheck() { 97 | set -euo pipefail 98 | 99 | for path in $(echo "$FLAKEBOX_GIT_LS_TEXT" | grep -E '.*\.sh$'); do 100 | shellcheck --severity=warning "$path" 101 | done 102 | 103 | } 104 | export -f check_shellcheck 105 | 106 | function check_trailing_newline() { 107 | set -euo pipefail 108 | 109 | errors="" 110 | for path in $(echo "$FLAKEBOX_GIT_LS_TEXT"); do 111 | 112 | # extra branches for clarity 113 | if [ ! -s "$path" ]; then 114 | # echo "$path is empty" 115 | true 116 | elif [ -z "$(tail -c 1 < "$path")" ]; then 117 | # echo "$path ends with a newline or with a null byte" 118 | true 119 | else 120 | >&2 echo "$path doesn't end with a newline" 1>&2 121 | errors="true" 122 | fi 123 | done 124 | 125 | if [ -n "$errors" ]; then 126 | >&2 echo "Fix the problems above or use --no-verify" 1>&2 127 | return 1 128 | fi 129 | 130 | } 131 | export -f check_trailing_newline 132 | 133 | function check_trailing_whitespace() { 134 | set -euo pipefail 135 | 136 | if ! git diff --check HEAD ; then 137 | echo "Trailing whitespace detected. Please remove them before committing." 138 | return 1 139 | fi 140 | 141 | } 142 | export -f check_trailing_whitespace 143 | 144 | function check_typos() { 145 | set -euo pipefail 146 | 147 | if ! echo "$FLAKEBOX_GIT_LS_TEXT" | typos --stdin-paths ; then 148 | >&2 echo "Typos found: Valid new words can be added to '.typos.toml'" 149 | return 1 150 | fi 151 | 152 | } 153 | export -f check_typos 154 | 155 | parallel \ 156 | --nonotice \ 157 | ::: \ 158 | check_cargo_fmt \ 159 | check_cargo_lock \ 160 | check_leftover_dbg \ 161 | check_semgrep \ 162 | check_shellcheck \ 163 | check_trailing_newline \ 164 | check_trailing_whitespace \ 165 | check_typos \ 166 | check_nothing 167 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Control your NixOS instances system configuration from a centrally managed location."; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | flakebox = { 8 | url = "github:rustshop/flakebox?rev=b07a9f3d17d400464210464e586f76223306f62d"; 9 | }; 10 | }; 11 | 12 | outputs = { self, nixpkgs, flake-utils, flakebox }: { 13 | nixosModules = { 14 | npcnix = import ./modules/npcnix.nix { inherit self; }; 15 | default = self.nixosModules.npcnix; 16 | }; 17 | 18 | nixosConfigurations = 19 | let 20 | system = "x86_64-linux"; 21 | pkgs = import nixpkgs { 22 | inherit system; 23 | }; 24 | in 25 | { 26 | basic = nixpkgs.lib.nixosSystem { 27 | inherit system pkgs; 28 | 29 | modules = [ 30 | 31 | ({ modulesPath, ... }: { 32 | imports = [ "${modulesPath}/virtualisation/amazon-image.nix" ]; 33 | ec2.hvm = true; 34 | 35 | 36 | system.stateVersion = "23.05"; 37 | }) 38 | 39 | self.nixosModules.default 40 | ]; 41 | }; 42 | }; 43 | } 44 | // flake-utils.lib.eachDefaultSystem (system: 45 | let 46 | pkgs = import nixpkgs { 47 | inherit system; 48 | }; 49 | lib = pkgs.lib; 50 | 51 | projectName = "npcnix"; 52 | 53 | flakeboxLib = flakebox.lib.${system} { 54 | config = { }; 55 | }; 56 | 57 | buildPaths = [ 58 | "Cargo.toml" 59 | "Cargo.lock" 60 | ".cargo" 61 | "src" 62 | "README.md" 63 | ]; 64 | 65 | buildSrc = flakeboxLib.filterSubPaths { 66 | root = builtins.path { 67 | name = projectName; 68 | path = ./.; 69 | }; 70 | paths = buildPaths; 71 | }; 72 | 73 | multiBuild = 74 | (flakeboxLib.craneMultiBuild { }) (craneLib': 75 | let 76 | craneLib = (craneLib'.overrideArgs { 77 | pname = projectName; 78 | src = buildSrc; 79 | buildInputs = [ 80 | pkgs.openssl 81 | 82 | ]; 83 | nativeBuildInputs = [ 84 | pkgs.pkg-config 85 | ]; 86 | }); 87 | in 88 | { 89 | npcnix = craneLib.buildPackage { }; 90 | }); 91 | 92 | npcnixPkgWrapped = pkgs.writeShellScriptBin "npcnix" '' 93 | exec env \ 94 | NPCNIX_AWS_CLI=''${NPCNIX_AWS_CLI:-${pkgs.awscli2}/bin/aws} \ 95 | NPCNIX_NIXOS_REBUILD=''${NPCNIX_NIXOS_REBUILD:-${pkgs.nixos-rebuild}/bin/nixos-rebuild} \ 96 | PATH="${pkgs.git}/bin:$PATH" \ 97 | ${multiBuild.npcnix}/bin/npcnix "$@" 98 | ''; 99 | in 100 | { 101 | packages = { 102 | default = npcnixPkgWrapped; 103 | npcnix-unwrapped = multiBuild.npcnix; 104 | npcnix = npcnixPkgWrapped; 105 | install = pkgs.writeShellScriptBin "npcnix-install" '' 106 | set -e 107 | npcnix_swapfile="/npcnix-install-swapfile" 108 | 109 | function cleanup() { 110 | if [ -e "$npcnix_swapfile" ]; then 111 | >&2 echo "Cleaning up temporary swap file..." 112 | ${pkgs.util-linux}/bin/swapoff "$npcnix_swapfile" || true 113 | ${pkgs.coreutils}/bin/rm -f "$npcnix_swapfile" || true 114 | fi 115 | } 116 | # clean unconditionally, in case we left over something in a previous run, etc. 117 | trap cleanup EXIT 118 | 119 | if [ "$(${pkgs.util-linux}/bin/swapon --noheadings --raw | ${pkgs.coreutils}/bin/wc -l )" = "0" ] ; then 120 | >&2 echo "No swap detected. Creating a temporary swap file..." 121 | if [ ! -e "$npcnix_swapfile" ]; then 122 | # it has been experimentally verified, that 2G should be enough 123 | # bootstrap even on AWS EC2 t3.nano instances 124 | ${pkgs.util-linux}/bin/fallocate -l 2G "$npcnix_swapfile" || true 125 | fi 126 | chmod 600 "$npcnix_swapfile" && \ 127 | ${pkgs.util-linux}/bin/mkswap "$npcnix_swapfile" && \ 128 | ${pkgs.util-linux}/bin/swapon "$npcnix_swapfile" || \ 129 | true 130 | fi 131 | 132 | ${npcnixPkgWrapped}/bin/npcnix install "$@" 133 | cleanup 134 | ''; 135 | }; 136 | 137 | checks = { 138 | npcnix = self.packages.${system}.npcnix; 139 | install = self.packages.${system}.install; 140 | } // lib.optionalAttrs (pkgs.stdenv.system == "x86_64-linux") { 141 | nixosConfiguration = self.nixosConfigurations.basic.config.system.build.toplevel; 142 | }; 143 | 144 | devShells = { 145 | default = flakeboxLib.mkDevShell { }; 146 | }; 147 | } 148 | ); 149 | } 150 | 151 | -------------------------------------------------------------------------------- /terraform/instance/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | } 6 | } 7 | } 8 | 9 | variable "remote" {} 10 | variable "configuration" { 11 | default = null 12 | } 13 | variable "install" { 14 | default = null 15 | } 16 | 17 | variable "root_access" { 18 | default = false 19 | type = bool 20 | } 21 | variable "root_ssh_keys" { 22 | default = [] 23 | type = list(string) 24 | } 25 | 26 | variable "pre_install_script" { 27 | default = "" 28 | } 29 | 30 | variable "post_install_script" { 31 | default = "" 32 | } 33 | 34 | variable "iam_policies" { 35 | type = map(any) 36 | default = {} 37 | } 38 | variable "hostname" {} 39 | variable "dns_zone" {} 40 | variable "subnet" {} 41 | variable "eip" { 42 | default = false 43 | } 44 | 45 | variable "ami" {} 46 | variable "instance_type" { default = "t3.micro" } 47 | variable "root_volume_size" { 48 | default = 8 49 | } 50 | variable "public_tcp_ports" { 51 | default = [] 52 | } 53 | variable "internal_tcp_ports" { 54 | default = [22] 55 | } 56 | 57 | output "instance" { 58 | value = aws_instance.instance 59 | } 60 | 61 | output "eip" { 62 | value = try(aws_eip.eip[0], null) 63 | } 64 | 65 | locals { 66 | append_root_ssh_keys_cmd = join("\n", 67 | [for key in var.root_ssh_keys : "echo \"${key}\" >> /root/.ssh/authorized_keys"] 68 | ) 69 | 70 | clear_root_ssh_keys_cmd = < /root/.ssh/authorized_keys 73 | chown root: /root/.ssh/authorized_keys 74 | chmod 0600 /root/.ssh/authorized_keys 75 | EOF 76 | 77 | write_root_ssh_keys_cmd = < Control your NixOS instances system configuration from a centrally managed location. 4 | 5 | ## Overview 6 | 7 | If you are already using NixOS flakes to configure your NixOS 8 | systems, why bother using ssh to change their configurations, 9 | if you could just ... let them configure themselves automatically, 10 | on their own. 11 | 12 | The plan is as follows: 13 | 14 | First, prepare a location that can store and serve files 15 | (an npcnix *store*) - e.g. an S3 bucket (or a prefix inside it). 16 | 17 | Within it, publish compressed flakes under certain addresses (an npcnix *remote*s) - e.g. a keys in a S3 bucket. 18 | 19 | Configure your NixOS hosts/images to run an initialization script 20 | at the first boot that will download a flake from a given *remote* 21 | and switch to a given NixOS *configuration* inside it, or use 22 | a pre-built system image that includes `npcnix` support. 23 | 24 | Each NixOS *configuration* should enable a `npcnix` system daemon, 25 | that will periodically check (and reconfigure if needed) the system 26 | following updates published in the *remote*. 27 | 28 | Not exactly a Kubernetes cluster but with a somewhat similar 29 | approach of agentes reacting to updates published in a central 30 | location. Yet simpler, easy to set up, understand and customize, 31 | and can go a long way to help manage a small to medium size herd 32 | of NixOS-based machines. 33 | 34 | It integrates well with existing infrastructure, CI systems and 35 | tooling, especially in cloud environments. 36 | 37 | In combination with internal remote builders and Nix caching 38 | servers (or corresponding services like cachix) it can work very 39 | effectively. 40 | 41 | Since the npcnix-managed systems "pull" their configuration, 42 | security posture of the whole system can be improved (in comparison 43 | to an ssh-based approach), as active remote access is not even necessary, 44 | and permission system can be centralized around the *store* write privileges. 45 | 46 | 47 | ## Setting up in AWS using Terraform 48 | 49 | My use case involves AWS as a cloud, S3 as a cheap, yet abundant *store*, 50 | with a built-in and integrated (in AWS) permission system, and Terraform 51 | integration for convenience. 52 | 53 | The guide will assume you're familiar with the products and tools 54 | used. 55 | 56 | It shouldn't be difficult with a bit of cloud/system administration 57 | skills to implement `npcnix` in any other environment. 58 | 59 | All the terraform modules used here are in the `./terraform` directory. You should 60 | probably pin them, or even just use as a reference. 61 | 62 | So first, we need a bucket to store the config: 63 | 64 | ```terraform 65 | resource "aws_s3_bucket" "config" { 66 | bucket = "some-config" 67 | } 68 | 69 | resource "aws_s3_bucket_public_access_block" "config" { 70 | bucket = aws_s3_bucket.config.id 71 | 72 | block_public_acls = true 73 | block_public_policy = true 74 | ignore_public_acls = true 75 | } 76 | ``` 77 | 78 | And then let's carve out a part of it for *remotes* (compressed Nix flakes): 79 | 80 | ```terraform 81 | module "npcnix_s3_store" { 82 | source = "github.com/rustshop/npcnix//terraform/store_s3?ref=a1dd4621a56724fe36ca8940eb7172dd0f4be986" 83 | 84 | bucket = aws_s3_bucket.config 85 | prefix = "npcnix/remotes" 86 | } 87 | ``` 88 | 89 | We're going to need a boot script: 90 | 91 | ```terraform 92 | module "npcnix_install" { 93 | source = "github.com/rustshop/npcnix//terraform/install?ref=a1dd4621a56724fe36ca8940eb7172dd0f4be986" 94 | } 95 | ``` 96 | 97 | Finally, a remote along with the command that will pack and upload to it: 98 | 99 | ```terraform 100 | module "remote_dev" { 101 | source = "github.com/rustshop/npcnix//terraform/remote_s3?ref=a1dd4621a56724fe36ca8940eb7172dd0f4be986" 102 | 103 | name = "dev" 104 | store = var.npcnix_s3_store 105 | } 106 | 107 | module "remote_dev_upload" { 108 | source = "github.com/rustshop/npcnix//terraform/remote_s3_upload?ref=a1dd4621a56724fe36ca8940eb7172dd0f4be986" 109 | 110 | remote = module.remote_dev 111 | flake_dir = "../../configurations" 112 | include = [] 113 | } 114 | ``` 115 | 116 | And a EC2 instance that will bootstrap itself using the installation script, 117 | have some alternative root ssh access (for debugging any issues) and 118 | then configure itself to use `"host"` NixOS configuration from flake 119 | in `../../configurations`. 120 | 121 | ```terraform 122 | module "host" { 123 | source = "github.com/rustshop/npcnix//terraform/instance?ref=a1dd4621a56724fe36ca8940eb7172dd0f4be986" 124 | 125 | providers = { 126 | aws = aws 127 | } 128 | 129 | remote = module.remote_dev 130 | install = module.npcnix_install 131 | root_access = true 132 | root_ssh_keys = local.fallback_ssh_keys 133 | 134 | hostname = "host" 135 | subnet = module.vpc-us-east-1.subnets["public-a"] 136 | dns_zone = aws_route53_zone.dev 137 | ami = local.ami.nixos_22_11.us-east-1 138 | instance_type = "t3.nano" 139 | 140 | pre_install_script = local.user_data_network_init 141 | 142 | public_tcp_ports = [22] 143 | } 144 | ``` 145 | 146 | And that's basically it for Terraform configuration required. 147 | 148 | On `terraform apply`, local `npcnix pack` will pack the Nix flake from `../../configurations`, and upload it to a remote. On start the system daemon will execute script prepared by `npcnix_install` that will configure `npcnix` on the machine, download the packed flake, and switch the configuration. As long as that configuration has a npcnix NixOS module enabled, a system daemon will keep monitoring the remote and switching to the desired configuration. 149 | 150 | With just one command, you can start one or more machines that will automatically provision themselves with the desired configuration. 151 | 152 | ## FAQ 153 | 154 | ### What about destination machines having to build each configuration? 155 | 156 | Use a build cache and/or remote builder machine. Both the installation script module and the npcnix itself can use it. You can populate it from your local machine or CI of some kind. 157 | -------------------------------------------------------------------------------- /terraform/instance_multi/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | } 6 | } 7 | } 8 | 9 | variable "remote" {} 10 | variable "configuration" { 11 | default = null 12 | } 13 | variable "install" { 14 | default = null 15 | } 16 | 17 | variable "root_access" { 18 | default = false 19 | type = bool 20 | } 21 | variable "root_ssh_keys" { 22 | default = [] 23 | type = list(string) 24 | } 25 | 26 | variable "pre_install_script" { 27 | default = "" 28 | } 29 | 30 | variable "post_install_script" { 31 | default = "" 32 | } 33 | 34 | variable "iam_policies" { 35 | type = map(any) 36 | default = {} 37 | } 38 | 39 | variable "hosts" { 40 | # { 41 | # "name" = { 42 | # subnet = 43 | # 44 | # } 45 | # } 46 | } 47 | 48 | 49 | variable "hostname_base" {} 50 | variable "dns_zone" {} 51 | variable "vpc" {} 52 | variable "eip" { 53 | default = false 54 | } 55 | 56 | variable "ami" {} 57 | variable "instance_type" { default = "t3.micro" } 58 | variable "root_volume_size" { 59 | default = 8 60 | } 61 | 62 | variable "public_tcp_ports" { 63 | default = [] 64 | } 65 | variable "internal_tcp_ports" { 66 | default = [22] 67 | } 68 | 69 | output "instance" { 70 | value = aws_instance.instance 71 | } 72 | 73 | 74 | locals { 75 | append_root_ssh_keys_cmd = join("\n", 76 | [for key in var.root_ssh_keys : "echo \"${key}\" >> /root/.ssh/authorized_keys"] 77 | ) 78 | 79 | clear_root_ssh_keys_cmd = < /root/.ssh/authorized_keys 82 | chown root: /root/.ssh/authorized_keys 83 | chmod 0600 /root/.ssh/authorized_keys 84 | EOF 85 | 86 | write_root_ssh_keys_cmd = < u64 { 11 | 5 12 | } 13 | 14 | fn default_max_sleep_secs() -> u64 { 15 | 120 16 | } 17 | 18 | fn default_max_sleep_after_hours() -> u64 { 19 | 24 20 | } 21 | #[derive(Serialize, Deserialize, Debug, Copy, Clone)] 22 | #[serde(tag = "type")] 23 | #[serde(rename_all = "snake_case")] 24 | pub enum ConfigPaused { 25 | Indefinitely, 26 | Until { 27 | until: chrono::DateTime, 28 | }, 29 | } 30 | impl ConfigPaused { 31 | pub fn combine(self, other: Self) -> ConfigPaused { 32 | match (self, other) { 33 | (ConfigPaused::Indefinitely, _) | (_, ConfigPaused::Indefinitely) => { 34 | ConfigPaused::Indefinitely 35 | } 36 | (ConfigPaused::Until { until: until1 }, ConfigPaused::Until { until: until2 }) => { 37 | Self::Until { 38 | until: cmp::max(until1, until2), 39 | } 40 | } 41 | } 42 | } 43 | 44 | fn is_expired(self) -> bool { 45 | match self { 46 | ConfigPaused::Indefinitely => false, 47 | ConfigPaused::Until { until } => until <= Utc::now(), 48 | } 49 | } 50 | } 51 | 52 | /// Persistent config (`/var/lib/npcnix/config.json`) 53 | #[derive(Serialize, Deserialize, Debug, Clone)] 54 | #[serde(rename_all = "snake_case")] 55 | pub struct Config { 56 | remote: Option, 57 | remote_region: Option, 58 | configuration: Option, 59 | last_reconfiguration: chrono::DateTime, 60 | last_etag: String, 61 | last_configuration: String, 62 | #[serde(default = "default_min_sleep_secs")] 63 | min_sleep_secs: u64, 64 | #[serde(default = "default_max_sleep_secs")] 65 | max_sleep_secs: u64, 66 | #[serde(default = "default_max_sleep_after_hours")] 67 | max_sleep_after_hours: u64, 68 | 69 | #[serde(skip_serializing_if = "Option::is_none")] 70 | paused: Option, 71 | } 72 | 73 | impl Default for Config { 74 | fn default() -> Self { 75 | Self { 76 | remote: None, 77 | remote_region: None, 78 | configuration: None, 79 | last_reconfiguration: chrono::Utc::now(), 80 | last_etag: "".into(), 81 | last_configuration: "".into(), 82 | min_sleep_secs: default_min_sleep_secs(), 83 | max_sleep_secs: default_max_sleep_secs(), 84 | max_sleep_after_hours: default_max_sleep_after_hours(), 85 | paused: None, 86 | } 87 | } 88 | } 89 | 90 | impl Config { 91 | pub fn load(path: &Path) -> anyhow::Result { 92 | Ok(serde_json::from_reader::<_, Self>(std::fs::File::open(path)?)?.expire_paused()) 93 | } 94 | 95 | pub fn store(&self, path: &Path) -> anyhow::Result<()> { 96 | crate::misc::store_json_pretty_to_file(path, &self.clone().expire_paused()) 97 | } 98 | 99 | pub fn expire_paused(self) -> Self { 100 | if self.is_paused() { 101 | self 102 | } else { 103 | Self { 104 | paused: None, 105 | ..self 106 | } 107 | } 108 | } 109 | 110 | pub fn with_configuration(self, configuration: &str) -> Self { 111 | Self { 112 | configuration: Some(configuration.into()), 113 | ..self 114 | } 115 | } 116 | 117 | /// Like [`Self::with_configuration`] but if `init` is `true` will not 118 | /// overwrite the existing value 119 | pub fn with_configuration_maybe_init(self, configuration: &str, init: bool) -> Self { 120 | if !init || self.configuration.is_none() { 121 | self.with_configuration(configuration) 122 | } else { 123 | self 124 | } 125 | } 126 | 127 | pub fn with_remote(self, remote: &Url) -> Self { 128 | Self { 129 | remote: Some(remote.clone()), 130 | ..self 131 | } 132 | } 133 | 134 | pub fn with_remote_region(self, remote_region: Option<&str>) -> Self { 135 | Self { 136 | remote_region: remote_region.map(ToString::to_string), 137 | ..self 138 | } 139 | } 140 | 141 | pub fn with_paused_until(self, until: chrono::DateTime) -> Self { 142 | let until = ConfigPaused::Until { until }; 143 | Self { 144 | paused: Some( 145 | self.paused 146 | .map(|current| current.combine(until)) 147 | .unwrap_or(until), 148 | ), 149 | ..self 150 | } 151 | } 152 | 153 | pub fn with_paused_indefinitely(self) -> Self { 154 | Self { 155 | paused: Some(ConfigPaused::Indefinitely), 156 | ..self 157 | } 158 | } 159 | 160 | pub fn with_unpaused(self) -> Self { 161 | Self { 162 | paused: None, 163 | ..self 164 | } 165 | } 166 | 167 | pub fn is_paused(&self) -> bool { 168 | self.paused 169 | .map(|paused| !paused.is_expired()) 170 | .unwrap_or(false) 171 | } 172 | 173 | pub fn status_string(&self) -> String { 174 | match self.paused { 175 | Some(paused) if !paused.is_expired() => match paused { 176 | ConfigPaused::Indefinitely => "paused (indefinitely)".to_string(), 177 | ConfigPaused::Until { until } => { 178 | let duration = until.signed_duration_since(Utc::now()); 179 | format!( 180 | "paused (until {}; <={}h)", 181 | until.to_rfc3339_opts(chrono::SecondsFormat::Secs, true), 182 | duration.num_hours() + 1 183 | ) 184 | } 185 | }, 186 | _ => "active".to_string(), 187 | } 188 | } 189 | 190 | /// Like [`Self:with_remote`] but if `init` is `true` will not overwrite the 191 | /// existing value 192 | pub fn with_remote_maybe_init(self, remote: &Url, init: bool) -> Self { 193 | if !init || self.remote.is_none() { 194 | self.with_remote(remote) 195 | } else { 196 | self 197 | } 198 | } 199 | 200 | pub fn with_updated_last_reconfiguration(self, configuration: &str, etag: &str) -> Self { 201 | Self { 202 | last_configuration: configuration.to_owned(), 203 | last_etag: etag.to_owned(), 204 | last_reconfiguration: chrono::Utc::now(), 205 | ..self 206 | } 207 | } 208 | 209 | pub fn remote(&self) -> anyhow::Result<&Url> { 210 | self.remote 211 | .as_ref() 212 | .ok_or_else(|| format_err!("Remote not set")) 213 | } 214 | 215 | pub fn region_opt(&self) -> Option<&str> { 216 | self.remote_region.as_deref() 217 | } 218 | 219 | pub fn configuration(&self) -> anyhow::Result<&str> { 220 | self.configuration 221 | .as_deref() 222 | .ok_or_else(|| format_err!("configuration not set")) 223 | } 224 | 225 | pub fn cur_rng_sleep_time(&self) -> chrono::Duration { 226 | use rand::Rng; 227 | 228 | let since_last_update = cmp::max( 229 | chrono::Duration::seconds(1), 230 | chrono::Utc::now() - self.last_reconfiguration, 231 | ); 232 | 233 | let duration_ratio = (since_last_update.num_seconds() as f32 234 | / self.max_sleep_after_hours.saturating_mul(60 * 60) as f32) 235 | .clamp(0f32, 1f32); 236 | assert!(0f32 <= duration_ratio); 237 | 238 | let avg_duration_secs = (self.min_sleep_secs as f32 239 | + duration_ratio * self.max_sleep_secs.saturating_sub(self.min_sleep_secs) as f32) 240 | .clamp(0.01, 60f32 * 60f32); 241 | let rnd_time = 242 | rand::thread_rng().gen_range(avg_duration_secs * 0.5..=avg_duration_secs * 1.5); 243 | assert!(0f32 < rnd_time); 244 | 245 | chrono::Duration::seconds(cmp::max(self.min_sleep_secs as i64, rnd_time as i64)) 246 | } 247 | 248 | pub fn rng_sleep(&self) { 249 | let duration = self.cur_rng_sleep_time(); 250 | debug!(duration = %duration, "Sleeping"); 251 | thread::sleep(duration.to_std().expect("Can't be negative")); 252 | } 253 | 254 | pub fn last_configuration(&self) -> &str { 255 | &self.last_configuration 256 | } 257 | 258 | pub fn last_etag(&self) -> &str { 259 | &self.last_etag 260 | } 261 | } 262 | 263 | impl fmt::Display for Config { 264 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 265 | f.write_str(&serde_json::to_string_pretty(self).map_err(|_e| fmt::Error)?) 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "android-nixpkgs": { 4 | "inputs": { 5 | "devshell": "devshell", 6 | "flake-utils": "flake-utils_2", 7 | "nixpkgs": [ 8 | "flakebox", 9 | "nixpkgs" 10 | ] 11 | }, 12 | "locked": { 13 | "lastModified": 1695500413, 14 | "narHash": "sha256-yinrAWIc4XZbWQoXOYkUO0lCNQ5z/vMyl+QCYuIwdPc=", 15 | "owner": "dpc", 16 | "repo": "android-nixpkgs", 17 | "rev": "2e42268a196375ce9b010a10ec5250d2f91a09b4", 18 | "type": "github" 19 | }, 20 | "original": { 21 | "owner": "dpc", 22 | "repo": "android-nixpkgs", 23 | "rev": "2e42268a196375ce9b010a10ec5250d2f91a09b4", 24 | "type": "github" 25 | } 26 | }, 27 | "crane": { 28 | "inputs": { 29 | "flake-compat": "flake-compat", 30 | "flake-utils": "flake-utils_3", 31 | "nixpkgs": [ 32 | "flakebox", 33 | "nixpkgs" 34 | ], 35 | "rust-overlay": "rust-overlay" 36 | }, 37 | "locked": { 38 | "lastModified": 1696222002, 39 | "narHash": "sha256-2xCGgEXOIJsaS6zFUhST/t7kiroMofjViw01w65JEfs=", 40 | "owner": "dpc", 41 | "repo": "crane", 42 | "rev": "bf5f4b71b446e5784900ee9ae0f2569e5250e360", 43 | "type": "github" 44 | }, 45 | "original": { 46 | "owner": "dpc", 47 | "repo": "crane", 48 | "rev": "bf5f4b71b446e5784900ee9ae0f2569e5250e360", 49 | "type": "github" 50 | } 51 | }, 52 | "devshell": { 53 | "inputs": { 54 | "nixpkgs": [ 55 | "flakebox", 56 | "android-nixpkgs", 57 | "nixpkgs" 58 | ], 59 | "systems": "systems_2" 60 | }, 61 | "locked": { 62 | "lastModified": 1695195896, 63 | "narHash": "sha256-pq9q7YsGXnQzJFkR5284TmxrLNFc0wo4NQ/a5E93CQU=", 64 | "owner": "numtide", 65 | "repo": "devshell", 66 | "rev": "05d40d17bf3459606316e3e9ec683b784ff28f16", 67 | "type": "github" 68 | }, 69 | "original": { 70 | "owner": "numtide", 71 | "repo": "devshell", 72 | "type": "github" 73 | } 74 | }, 75 | "fenix": { 76 | "inputs": { 77 | "nixpkgs": [ 78 | "flakebox", 79 | "nixpkgs" 80 | ], 81 | "rust-analyzer-src": "rust-analyzer-src" 82 | }, 83 | "locked": { 84 | "lastModified": 1695104496, 85 | "narHash": "sha256-B+itawD+o58jjolphhMLnSvtY6aPAZtExE24Eqq5FBA=", 86 | "owner": "nix-community", 87 | "repo": "fenix", 88 | "rev": "82619f17173aae0216eedd589445541a7804ba4e", 89 | "type": "github" 90 | }, 91 | "original": { 92 | "owner": "nix-community", 93 | "repo": "fenix", 94 | "type": "github" 95 | } 96 | }, 97 | "flake-compat": { 98 | "flake": false, 99 | "locked": { 100 | "lastModified": 1673956053, 101 | "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", 102 | "owner": "edolstra", 103 | "repo": "flake-compat", 104 | "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", 105 | "type": "github" 106 | }, 107 | "original": { 108 | "owner": "edolstra", 109 | "repo": "flake-compat", 110 | "type": "github" 111 | } 112 | }, 113 | "flake-utils": { 114 | "inputs": { 115 | "systems": "systems" 116 | }, 117 | "locked": { 118 | "lastModified": 1709126324, 119 | "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=", 120 | "owner": "numtide", 121 | "repo": "flake-utils", 122 | "rev": "d465f4819400de7c8d874d50b982301f28a84605", 123 | "type": "github" 124 | }, 125 | "original": { 126 | "owner": "numtide", 127 | "repo": "flake-utils", 128 | "type": "github" 129 | } 130 | }, 131 | "flake-utils_2": { 132 | "inputs": { 133 | "systems": "systems_3" 134 | }, 135 | "locked": { 136 | "lastModified": 1694529238, 137 | "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", 138 | "owner": "numtide", 139 | "repo": "flake-utils", 140 | "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", 141 | "type": "github" 142 | }, 143 | "original": { 144 | "owner": "numtide", 145 | "repo": "flake-utils", 146 | "type": "github" 147 | } 148 | }, 149 | "flake-utils_3": { 150 | "inputs": { 151 | "systems": "systems_4" 152 | }, 153 | "locked": { 154 | "lastModified": 1681202837, 155 | "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", 156 | "owner": "numtide", 157 | "repo": "flake-utils", 158 | "rev": "cfacdce06f30d2b68473a46042957675eebb3401", 159 | "type": "github" 160 | }, 161 | "original": { 162 | "owner": "numtide", 163 | "repo": "flake-utils", 164 | "type": "github" 165 | } 166 | }, 167 | "flake-utils_4": { 168 | "inputs": { 169 | "systems": "systems_5" 170 | }, 171 | "locked": { 172 | "lastModified": 1694529238, 173 | "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", 174 | "owner": "numtide", 175 | "repo": "flake-utils", 176 | "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", 177 | "type": "github" 178 | }, 179 | "original": { 180 | "owner": "numtide", 181 | "repo": "flake-utils", 182 | "type": "github" 183 | } 184 | }, 185 | "flakebox": { 186 | "inputs": { 187 | "android-nixpkgs": "android-nixpkgs", 188 | "crane": "crane", 189 | "fenix": "fenix", 190 | "flake-utils": "flake-utils_4", 191 | "nixpkgs": "nixpkgs", 192 | "nixpkgs-unstable": "nixpkgs-unstable" 193 | }, 194 | "locked": { 195 | "lastModified": 1696575782, 196 | "narHash": "sha256-pAAqtj0EkVWvirmW3p2qnYgRZduMSn4j2IYn5gm4K1A=", 197 | "owner": "rustshop", 198 | "repo": "flakebox", 199 | "rev": "b07a9f3d17d400464210464e586f76223306f62d", 200 | "type": "github" 201 | }, 202 | "original": { 203 | "owner": "rustshop", 204 | "repo": "flakebox", 205 | "rev": "b07a9f3d17d400464210464e586f76223306f62d", 206 | "type": "github" 207 | } 208 | }, 209 | "nixpkgs": { 210 | "locked": { 211 | "lastModified": 1695825837, 212 | "narHash": "sha256-4Ne11kNRnQsmSJCRSSNkFRSnHC4Y5gPDBIQGjjPfJiU=", 213 | "owner": "nixos", 214 | "repo": "nixpkgs", 215 | "rev": "5cfafa12d57374f48bcc36fda3274ada276cf69e", 216 | "type": "github" 217 | }, 218 | "original": { 219 | "owner": "nixos", 220 | "ref": "nixos-23.05", 221 | "repo": "nixpkgs", 222 | "type": "github" 223 | } 224 | }, 225 | "nixpkgs-unstable": { 226 | "locked": { 227 | "lastModified": 1696193975, 228 | "narHash": "sha256-mnQjUcYgp9Guu3RNVAB2Srr1TqKcPpRXmJf4LJk6KRY=", 229 | "owner": "nixos", 230 | "repo": "nixpkgs", 231 | "rev": "fdd898f8f79e8d2f99ed2ab6b3751811ef683242", 232 | "type": "github" 233 | }, 234 | "original": { 235 | "owner": "nixos", 236 | "ref": "nixos-unstable", 237 | "repo": "nixpkgs", 238 | "type": "github" 239 | } 240 | }, 241 | "nixpkgs_2": { 242 | "locked": { 243 | "lastModified": 1709128929, 244 | "narHash": "sha256-GWrv9a+AgGhG4/eI/CyVVIIygia7cEy68Huv3P8oyaw=", 245 | "owner": "NixOS", 246 | "repo": "nixpkgs", 247 | "rev": "c8e74c2f83fe12b4e5a8bd1abbc090575b0f7611", 248 | "type": "github" 249 | }, 250 | "original": { 251 | "owner": "NixOS", 252 | "ref": "nixos-23.11", 253 | "repo": "nixpkgs", 254 | "type": "github" 255 | } 256 | }, 257 | "root": { 258 | "inputs": { 259 | "flake-utils": "flake-utils", 260 | "flakebox": "flakebox", 261 | "nixpkgs": "nixpkgs_2" 262 | } 263 | }, 264 | "rust-analyzer-src": { 265 | "flake": false, 266 | "locked": { 267 | "lastModified": 1695027950, 268 | "narHash": "sha256-u4lq0LBmXqEfppyeYQqg6w+4Abk2qDnmsPEF+C2xFdQ=", 269 | "owner": "rust-lang", 270 | "repo": "rust-analyzer", 271 | "rev": "258b15c506a2d3ad862fd17ae24eaf272443f477", 272 | "type": "github" 273 | }, 274 | "original": { 275 | "owner": "rust-lang", 276 | "ref": "nightly", 277 | "repo": "rust-analyzer", 278 | "type": "github" 279 | } 280 | }, 281 | "rust-overlay": { 282 | "inputs": { 283 | "flake-utils": [ 284 | "flakebox", 285 | "crane", 286 | "flake-utils" 287 | ], 288 | "nixpkgs": [ 289 | "flakebox", 290 | "crane", 291 | "nixpkgs" 292 | ] 293 | }, 294 | "locked": { 295 | "lastModified": 1683080331, 296 | "narHash": "sha256-nGDvJ1DAxZIwdn6ww8IFwzoHb2rqBP4wv/65Wt5vflk=", 297 | "owner": "oxalica", 298 | "repo": "rust-overlay", 299 | "rev": "d59c3fa0cba8336e115b376c2d9e91053aa59e56", 300 | "type": "github" 301 | }, 302 | "original": { 303 | "owner": "oxalica", 304 | "repo": "rust-overlay", 305 | "type": "github" 306 | } 307 | }, 308 | "systems": { 309 | "locked": { 310 | "lastModified": 1681028828, 311 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 312 | "owner": "nix-systems", 313 | "repo": "default", 314 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 315 | "type": "github" 316 | }, 317 | "original": { 318 | "owner": "nix-systems", 319 | "repo": "default", 320 | "type": "github" 321 | } 322 | }, 323 | "systems_2": { 324 | "locked": { 325 | "lastModified": 1681028828, 326 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 327 | "owner": "nix-systems", 328 | "repo": "default", 329 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 330 | "type": "github" 331 | }, 332 | "original": { 333 | "owner": "nix-systems", 334 | "repo": "default", 335 | "type": "github" 336 | } 337 | }, 338 | "systems_3": { 339 | "locked": { 340 | "lastModified": 1681028828, 341 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 342 | "owner": "nix-systems", 343 | "repo": "default", 344 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 345 | "type": "github" 346 | }, 347 | "original": { 348 | "owner": "nix-systems", 349 | "repo": "default", 350 | "type": "github" 351 | } 352 | }, 353 | "systems_4": { 354 | "locked": { 355 | "lastModified": 1681028828, 356 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 357 | "owner": "nix-systems", 358 | "repo": "default", 359 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 360 | "type": "github" 361 | }, 362 | "original": { 363 | "owner": "nix-systems", 364 | "repo": "default", 365 | "type": "github" 366 | } 367 | }, 368 | "systems_5": { 369 | "locked": { 370 | "lastModified": 1681028828, 371 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 372 | "owner": "nix-systems", 373 | "repo": "default", 374 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 375 | "type": "github" 376 | }, 377 | "original": { 378 | "owner": "nix-systems", 379 | "repo": "default", 380 | "type": "github" 381 | } 382 | } 383 | }, 384 | "root": "root", 385 | "version": 7 386 | } 387 | -------------------------------------------------------------------------------- /src/bin/npcnix.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../../README.md")] 2 | use std::ffi::OsString; 3 | use std::io; 4 | use std::io::Write as _; 5 | use std::path::PathBuf; 6 | 7 | use clap::{Parser, Subcommand, ValueEnum}; 8 | use npcnix::data_dir::DataDir; 9 | use tracing::trace; 10 | use tracing_subscriber::layer::SubscriberExt; 11 | use tracing_subscriber::util::SubscriberInitExt; 12 | use tracing_subscriber::{EnvFilter, Layer}; 13 | use url::Url; 14 | 15 | #[derive(Parser, Debug, Clone)] 16 | struct Opts { 17 | #[clap(flatten)] 18 | common: npcnix::opts::Common, 19 | 20 | #[command(subcommand)] 21 | command: Command, 22 | } 23 | 24 | impl Opts { 25 | pub fn data_dir(&self) -> DataDir { 26 | self.common.data_dir() 27 | } 28 | } 29 | 30 | #[derive(Subcommand, Debug, Clone)] 31 | pub enum Command { 32 | /// Configuration options 33 | Config { 34 | #[command(subcommand)] 35 | command: Option, 36 | }, 37 | /// Status 38 | Status, 39 | /// Activate a NixOS configuration from a Nix Flake in a local directory 40 | Activate(ActivateOpts), 41 | /// Pack a Nix Flake in a local directory into a remote-like packed Nix 42 | /// Flake file 43 | Pack(PackOpts), 44 | /// Pull a packed Nix Flake from a remote and extra to a directory 45 | Pull(PullOpts), 46 | /// Pack a Nix Flake in a local directory into a packed Nix Flake file and 47 | /// upload to a remote 48 | Push(PushOpts), 49 | /// Install npcnix on the machine 50 | Install(InstallOpts), 51 | /// Run as a daemon periodically activating NixOS configuration from the 52 | /// remote 53 | Follow(FollowOpts), 54 | /// Permanently or temporarily pause the npcnix daemon 55 | Pause(PauseOpts), 56 | /// Unpause the npcnix daemon 57 | Unpause, 58 | } 59 | 60 | #[derive(Subcommand, Debug, Clone)] 61 | pub enum ConfigOpts { 62 | Show, 63 | /// Change daemon settings 64 | Set { 65 | /// Only update if not already set 66 | #[arg(long)] 67 | init: bool, 68 | 69 | #[command(subcommand)] 70 | value: SetOpts, 71 | }, 72 | } 73 | 74 | #[derive(Parser, Debug, Clone)] 75 | pub struct PullOpts { 76 | /// Override the remote from config 77 | #[arg(long)] 78 | remote: Option, 79 | 80 | #[arg(long)] 81 | /// Destination directory 82 | dst: PathBuf, 83 | } 84 | 85 | #[derive(Parser, Debug, Clone)] 86 | pub struct PauseOpts { 87 | /// Pause for this many hours 88 | #[arg(long, group("duration"))] 89 | hours: Option, 90 | 91 | #[arg(long, group("duration"))] 92 | minutes: Option, 93 | } 94 | 95 | #[derive(Parser, Debug, Clone)] 96 | pub struct ActivateCommonOpts { 97 | #[arg(long)] 98 | extra_substituters: Vec, 99 | 100 | #[arg(long)] 101 | extra_trusted_public_keys: Vec, 102 | } 103 | 104 | #[derive(Parser, Debug, Clone)] 105 | pub struct ActivateOpts { 106 | #[arg(long, default_value = ".")] 107 | /// Source directory 108 | src: PathBuf, 109 | 110 | #[arg(long)] 111 | /// Configuration to apply 112 | configuration: Option, 113 | 114 | #[command(flatten)] 115 | activate: ActivateCommonOpts, 116 | } 117 | 118 | #[derive(Parser, Debug, Clone)] 119 | pub struct InstallOpts { 120 | #[arg(long)] 121 | /// Remote to use for the host 122 | remote: Url, 123 | 124 | #[arg(long)] 125 | /// Region to use for the remote access (typically s3 bucket) 126 | remote_region: Option, 127 | 128 | #[arg(long)] 129 | /// Configuration to use for the host 130 | configuration: String, 131 | 132 | #[arg(long)] 133 | /// Configuration to activate (as an intermediate step) 134 | initial_configuration: Option, 135 | 136 | #[command(flatten)] 137 | activate: ActivateCommonOpts, 138 | } 139 | 140 | impl From for npcnix::ActivateOpts { 141 | fn from(value: ActivateCommonOpts) -> Self { 142 | npcnix::ActivateOpts { 143 | extra_substituters: value.extra_substituters, 144 | extra_trusted_public_keys: value.extra_trusted_public_keys, 145 | } 146 | } 147 | } 148 | 149 | #[derive(Parser, Debug, Clone)] 150 | pub struct PackCommonOpts { 151 | /// Source directory 152 | #[arg(long)] 153 | src: PathBuf, 154 | 155 | /// Include this subdirectory (can be specified multiple times; default: 156 | /// all) 157 | #[arg(long)] 158 | include: Vec, 159 | } 160 | 161 | #[derive(Parser, Debug, Clone)] 162 | pub struct PushOpts { 163 | #[command(flatten)] 164 | pack: PackCommonOpts, 165 | 166 | /// To prevent accidental push, remote is required 167 | #[arg(long)] 168 | remote: Url, 169 | } 170 | 171 | #[derive(Parser, Debug, Clone)] 172 | pub struct PackOpts { 173 | #[command(flatten)] 174 | pack: PackCommonOpts, 175 | 176 | /// Destination file 177 | #[arg(long)] 178 | dst: PathBuf, 179 | } 180 | 181 | #[derive(Subcommand, Debug, Clone)] 182 | pub enum SetOpts { 183 | Remote { url: Url }, 184 | Configuration { configuration: String }, 185 | } 186 | 187 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default)] 188 | pub enum Once { 189 | /// Finish on any success 190 | #[default] 191 | Any, 192 | /// Finish on first activation of a new config 193 | Activate, 194 | } 195 | 196 | impl From for npcnix::Once { 197 | fn from(value: Once) -> Self { 198 | match value { 199 | Once::Any => npcnix::Once::Any, 200 | Once::Activate => npcnix::Once::Activate, 201 | } 202 | } 203 | } 204 | 205 | #[derive(Parser, Debug, Clone)] 206 | pub struct FollowOpts { 207 | #[command(flatten)] 208 | activate: ActivateCommonOpts, 209 | 210 | /// Stop after first success 211 | #[arg(long)] 212 | once: Option>, 213 | 214 | /// Ignore etag and assume configuration changed 215 | #[arg(long)] 216 | ignore_etag: bool, 217 | } 218 | 219 | impl FollowOpts { 220 | fn once(&self) -> Option { 221 | match self.once { 222 | Some(Some(o)) => Some(o.into()), 223 | Some(None) => Some(npcnix::Once::Any), 224 | None => None, 225 | } 226 | } 227 | } 228 | pub fn tracing_init() -> anyhow::Result<()> { 229 | let filter_layer = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); 230 | let fmt_layer = tracing_subscriber::fmt::layer() 231 | .with_writer(io::stderr) 232 | .with_filter(filter_layer); 233 | 234 | tracing_subscriber::registry().with(fmt_layer).init(); 235 | Ok(()) 236 | } 237 | 238 | fn main() -> anyhow::Result<()> { 239 | tracing_init()?; 240 | trace!("Staring npcnix"); 241 | let opts = Opts::parse(); 242 | 243 | match opts.command { 244 | Command::Pull(ref pull_opts) => npcnix::pull( 245 | &opts 246 | .data_dir() 247 | .get_current_remote_with_opt_override(pull_opts.remote.as_ref())?, 248 | &pull_opts.dst, 249 | )?, 250 | Command::Push(ref push_opts) => npcnix::push( 251 | &push_opts.pack.src, 252 | &push_opts.clone().pack.include.into_iter().collect(), 253 | &push_opts.remote, 254 | )?, 255 | Command::Pack(ref pack_opts) => npcnix::pack( 256 | &pack_opts.pack.src, 257 | &pack_opts.clone().pack.include.into_iter().collect(), 258 | &pack_opts.dst, 259 | )?, 260 | Command::Config { ref command } => match command { 261 | Some(ConfigOpts::Show) | None => { 262 | let _ = write!(std::io::stdout(), "{}\n", opts.data_dir().load_config()?); 263 | } 264 | Some(ConfigOpts::Set { init, ref value }) => match value { 265 | SetOpts::Remote { ref url } => opts.data_dir().store_config( 266 | &opts 267 | .data_dir() 268 | .load_config()? 269 | .with_remote_maybe_init(url, *init), 270 | )?, 271 | SetOpts::Configuration { ref configuration } => opts.data_dir().store_config( 272 | &opts 273 | .data_dir() 274 | .load_config()? 275 | .with_configuration_maybe_init(configuration, *init), 276 | )?, 277 | }, 278 | }, 279 | Command::Status => { 280 | let status_string = opts.data_dir().load_config()?.status_string(); 281 | let _ = write!(std::io::stdout(), "{}\n", status_string); 282 | } 283 | Command::Activate(ref activate_opts) => { 284 | if opts.data_dir().config_exist()? { 285 | let configuration = opts 286 | .data_dir() 287 | .get_current_configuration_with_opt_override( 288 | activate_opts.configuration.as_deref(), 289 | )?; 290 | npcnix::activate( 291 | Some(&opts.data_dir()), 292 | &activate_opts.src, 293 | &configuration, 294 | &activate_opts.clone().activate.into(), 295 | )?; 296 | } else { 297 | npcnix::activate( 298 | None, 299 | &activate_opts.src, 300 | activate_opts.configuration.as_deref().ok_or_else(|| { 301 | anyhow::format_err!("Must pass configuration to activate") 302 | })?, 303 | &activate_opts.clone().activate.into(), 304 | )?; 305 | } 306 | } 307 | Command::Follow(ref follow_opts) => { 308 | npcnix::follow( 309 | &opts.data_dir(), 310 | &follow_opts.clone().activate.into(), 311 | None, 312 | follow_opts.once(), 313 | follow_opts.ignore_etag, 314 | )?; 315 | } 316 | Command::Pause(PauseOpts { hours, minutes }) => { 317 | let config = opts.data_dir().load_config()?; 318 | 319 | let config = if let Some(minutes) = minutes { 320 | config.with_paused_until( 321 | chrono::Utc::now() 322 | + chrono::Duration::seconds(TryFrom::try_from(minutes.saturating_mul(60))?), 323 | ) 324 | } else if let Some(hours) = hours { 325 | config.with_paused_until( 326 | chrono::Utc::now() 327 | + chrono::Duration::seconds(TryFrom::try_from( 328 | hours.saturating_mul(60 * 60), 329 | )?), 330 | ) 331 | } else { 332 | config.with_paused_indefinitely() 333 | }; 334 | 335 | opts.data_dir().store_config(&config)?; 336 | } 337 | Command::Unpause => { 338 | let config = opts.data_dir().load_config()?; 339 | opts.data_dir().store_config(&config.with_unpaused())?; 340 | } 341 | Command::Install(InstallOpts { 342 | ref remote, 343 | ref remote_region, 344 | ref configuration, 345 | ref initial_configuration, 346 | ref activate, 347 | }) => { 348 | opts.data_dir().store_config( 349 | &opts 350 | .data_dir() 351 | .load_config()? 352 | .with_remote(remote) 353 | .with_remote_region(remote_region.as_deref()) 354 | .with_configuration(configuration), 355 | )?; 356 | 357 | npcnix::follow( 358 | &opts.data_dir(), 359 | &activate.clone().into(), 360 | initial_configuration.as_deref(), 361 | Some(npcnix::Once::Any), 362 | false, 363 | )?; 364 | } 365 | } 366 | 367 | Ok(()) 368 | } 369 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | use std::collections::HashSet; 4 | use std::ffi::OsString; 5 | use std::fs; 6 | use std::io::{self, Read, Write}; 7 | use std::ops::ControlFlow; 8 | use std::path::Path; 9 | use std::process::{self, Stdio}; 10 | use std::sync::atomic::{AtomicBool, Ordering}; 11 | use std::sync::Arc; 12 | 13 | use anyhow::{bail, format_err, Context}; 14 | use config::Config; 15 | use data_dir::DataDir; 16 | use serde::Deserialize; 17 | use signal_hook::consts::TERM_SIGNALS; 18 | use signal_hook::flag; 19 | use tracing::{debug, error, info, trace, warn}; 20 | use url::Url; 21 | 22 | pub mod config; 23 | pub mod data_dir; 24 | pub mod misc; 25 | pub mod opts; 26 | 27 | pub trait CommandExt { 28 | fn log_debug(&mut self) -> &mut Self; 29 | } 30 | 31 | impl CommandExt for process::Command { 32 | fn log_debug(&mut self) -> &mut Self { 33 | debug!( 34 | cmd = [self.get_program()] 35 | .into_iter() 36 | .chain(self.get_args()) 37 | .map(|s| s.to_string_lossy()) 38 | .collect::>() 39 | .join(" "), 40 | "Executing command" 41 | ); 42 | self 43 | } 44 | } 45 | 46 | pub fn aws_cli_path() -> OsString { 47 | std::env::var_os("NPCNIX_AWS_CLI").unwrap_or_else(|| OsString::from("aws")) 48 | } 49 | 50 | pub fn nixos_rebuild_path() -> OsString { 51 | std::env::var_os("NPCNIX_NIXOS_REBUILD").unwrap_or_else(|| OsString::from("nixos-rebuild")) 52 | } 53 | 54 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 55 | pub enum Once { 56 | Any, 57 | Activate, 58 | } 59 | 60 | pub fn pull(remote: &Url, dst: &Path) -> anyhow::Result<()> { 61 | let scheme = remote.scheme(); 62 | let (reader, mut child) = match scheme { 63 | "s3" => pull_s3(remote)?, 64 | _ => anyhow::bail!("Protocol not supported: {scheme}"), 65 | }; 66 | 67 | unpack_archive_to(reader, dst)?; 68 | child.wait()?; 69 | 70 | Ok(()) 71 | } 72 | 73 | pub fn push(src: &Path, include: &HashSet, remote: &url::Url) -> anyhow::Result<()> { 74 | verify_flake_src(src)?; 75 | let scheme = remote.scheme(); 76 | let (mut writer, mut child) = match scheme { 77 | "s3" => push_s3(remote)?, 78 | _ => anyhow::bail!("Protocol not supported: {scheme}"), 79 | }; 80 | 81 | pack_archive_from(src, include, &mut writer).context("Failed to pack the src archive")?; 82 | writer.flush()?; 83 | drop(writer); 84 | 85 | child.wait()?; 86 | 87 | Ok(()) 88 | } 89 | 90 | pub fn get_etag(remote: &Url, config: &Config) -> anyhow::Result { 91 | let scheme = remote.scheme(); 92 | Ok(match scheme { 93 | "s3" => get_etag_s3(remote, config.region_opt())?, 94 | _ => anyhow::bail!("Protocol not supported: {scheme}"), 95 | }) 96 | } 97 | 98 | #[derive(Debug, Clone)] 99 | pub struct ActivateOpts { 100 | pub extra_substituters: Vec, 101 | pub extra_trusted_public_keys: Vec, 102 | } 103 | 104 | pub fn with_activate_lock( 105 | data_dir: Option<&DataDir>, 106 | f: impl FnOnce() -> anyhow::Result, 107 | ) -> anyhow::Result { 108 | let mut lock = data_dir 109 | .map(|data_dir| data_dir.activate_lock()) 110 | .transpose()? 111 | .flatten(); 112 | 113 | // Workaround: due to `&mut` aliasing limitations, it seems impossible to 114 | // `try_write` first, then `write` if previous one failed, on the same 115 | // locked file. So we open same files twice instead. 116 | let mut lock2 = data_dir 117 | .map(|data_dir| data_dir.activate_lock()) 118 | .transpose()? 119 | .flatten(); 120 | 121 | let _lock = match lock.as_mut().map(|lock| lock.try_write()).transpose() { 122 | Ok(_lock) => { 123 | return f(); 124 | } 125 | Err(e) => { 126 | if e.kind() != io::ErrorKind::WouldBlock { 127 | return Err(e)?; 128 | } 129 | 130 | warn!("Waiting for another instance to finish"); 131 | lock2.as_mut().map(|lock| lock.write()).transpose()? 132 | } 133 | }; 134 | 135 | f() 136 | } 137 | 138 | pub fn activate( 139 | data_dir: Option<&DataDir>, 140 | src: &Path, 141 | configuration: &str, 142 | activate_opts: &ActivateOpts, 143 | ) -> Result<(), anyhow::Error> { 144 | with_activate_lock(data_dir, || { 145 | // Note: we load every time, in case settings changed 146 | activate_inner(src, configuration, activate_opts)?; 147 | data_dir 148 | .map(|data_dir| data_dir.update_last_reconfiguration(configuration, "")) 149 | .transpose() 150 | })?; 151 | Ok(()) 152 | } 153 | 154 | fn activate_inner( 155 | src: &Path, 156 | configuration: &str, 157 | activate_opts: &ActivateOpts, 158 | ) -> Result<(), anyhow::Error> { 159 | verify_flake_src(src)?; 160 | info!( 161 | configuration, 162 | src = %src.display(), 163 | "Activating configuration" 164 | ); 165 | let mut cmd = process::Command::new(nixos_rebuild_path()); 166 | cmd.args(["switch", "-L"]); 167 | 168 | for subscriber in &activate_opts.extra_substituters { 169 | cmd.args(["--option", "extra-substituters", subscriber]); 170 | } 171 | for key in &activate_opts.extra_trusted_public_keys { 172 | cmd.args(["--option", "extra-trusted-public-keys", key]); 173 | } 174 | 175 | cmd.args(["--flake", &format!(".#{configuration}")]) 176 | .current_dir(src); 177 | 178 | let status = cmd 179 | .log_debug() 180 | .status() 181 | .context("Calling `nixos-rebuild` failed")?; 182 | if !status.success() { 183 | bail!("nixos-rebuild returned exit code={:?}", status.code()); 184 | } 185 | Ok(()) 186 | } 187 | 188 | pub fn pack(src: &Path, include: &HashSet, dst: &Path) -> anyhow::Result<()> { 189 | verify_flake_src(src)?; 190 | 191 | let tmp_dst = dst.with_extension("tmp"); 192 | let file = fs::OpenOptions::new() 193 | .write(true) 194 | .create(true) 195 | .truncate(true) 196 | .open(&tmp_dst) 197 | .with_context(|| format!("Could not create temporary file: {}", tmp_dst.display()))?; 198 | let mut writer = io::BufWriter::new(&file); 199 | 200 | pack_archive_from(src, include, &mut writer) 201 | .with_context(|| format!("Failed to pack the src archive: {}", src.display()))?; 202 | writer.flush()?; 203 | drop(writer); 204 | file.sync_data()?; 205 | drop(file); 206 | std::fs::rename(&tmp_dst, dst).with_context(|| { 207 | format!( 208 | "Could not rename temporary file: {} to the final destination: {}", 209 | tmp_dst.display(), 210 | dst.display() 211 | ) 212 | })?; 213 | Ok(()) 214 | } 215 | 216 | fn verify_flake_src(src: &Path) -> anyhow::Result<()> { 217 | if !src.join("flake.nix").exists() { 218 | anyhow::bail!( 219 | "Flake source directory {} does not contain flake.nix file", 220 | src.display() 221 | ); 222 | } 223 | Ok(()) 224 | } 225 | 226 | #[derive(Deserialize)] 227 | struct EtagResponse { 228 | #[serde(rename = "ETag")] 229 | etag: String, 230 | } 231 | 232 | fn get_etag_s3(remote: &Url, region: Option<&str>) -> anyhow::Result { 233 | let output = process::Command::new(aws_cli_path()) 234 | .args( 235 | [ 236 | "s3api", 237 | "get-object-attributes", 238 | "--bucket", 239 | remote 240 | .host_str() 241 | .ok_or_else(|| format_err!("Invalid URL"))?, 242 | "--key", 243 | remote 244 | .path() 245 | .split_once('/') 246 | .ok_or_else(|| format_err!("Path doesn't start with a /"))? 247 | .1, 248 | "--object-attributes", 249 | "ETag", 250 | ] 251 | .into_iter() 252 | .chain(if let Some(region) = region { 253 | vec!["--region", region] 254 | } else { 255 | vec![] 256 | }), 257 | ) 258 | .log_debug() 259 | .output() 260 | .context("`aws` cli failed")?; 261 | 262 | if !output.status.success() { 263 | bail!( 264 | "aws s3api get-object-attributes returned code={:?} stdout={} stderr={}", 265 | output.status.code(), 266 | String::from_utf8_lossy(&output.stdout), 267 | String::from_utf8_lossy(&output.stderr), 268 | ) 269 | } 270 | let resp: EtagResponse = serde_json::from_slice(&output.stdout)?; 271 | 272 | Ok(resp.etag) 273 | } 274 | 275 | fn pull_s3(remote: &Url) -> anyhow::Result<(impl Read, process::Child)> { 276 | // by default this has 60s read & connect timeouts, so should not just 277 | // hang, so no need for extra timeouts, I guess 278 | let mut child = process::Command::new(aws_cli_path()) 279 | .args(["s3", "cp", remote.as_str(), "-"]) 280 | .stdout(Stdio::piped()) 281 | .log_debug() 282 | .spawn() 283 | .context("`aws` cli failed")?; 284 | 285 | let stdout = child.stdout.take().unwrap(); 286 | 287 | Ok((stdout, child)) 288 | } 289 | 290 | fn push_s3(remote: &Url) -> anyhow::Result<(impl Write, process::Child)> { 291 | let mut child = process::Command::new(aws_cli_path()) 292 | .args(["s3", "cp", "-", remote.as_str()]) 293 | .stdin(Stdio::piped()) 294 | .log_debug() 295 | .spawn() 296 | .context("`aws` cli failed")?; 297 | 298 | let stdin = child.stdin.take().unwrap(); 299 | 300 | Ok((stdin, child)) 301 | } 302 | 303 | fn unpack_archive_to(reader: impl Read, dst: &Path) -> io::Result<()> { 304 | fs::create_dir_all(dst)?; 305 | 306 | let decoder = zstd::stream::Decoder::new(reader)?; 307 | let mut archive = tar::Archive::new(decoder); 308 | archive.unpack(dst)?; 309 | 310 | Ok(()) 311 | } 312 | 313 | fn pack_archive_from( 314 | src: &Path, 315 | include: &HashSet, 316 | writer: impl Write, 317 | ) -> io::Result<()> { 318 | let encoder = zstd::stream::Encoder::new(writer, 0)?; 319 | let mut builder = tar::Builder::new(encoder); 320 | let paths = fs::read_dir(src)?; 321 | for path in paths { 322 | let entry = path?; 323 | let path = entry.path(); 324 | let file_name = path 325 | .file_name() 326 | .expect("read_dir must return only items with valid file_name"); 327 | let metadata = path.symlink_metadata()?; 328 | trace!( 329 | src = %path.display(), 330 | "Considering path for archive inclusion" 331 | ); 332 | if metadata.is_dir() { 333 | if include.is_empty() || include.contains(file_name) { 334 | trace!(src = %path.display(), "Packing directory"); 335 | builder.append_dir_all(file_name, &path)?; 336 | } else { 337 | debug!( 338 | src = %path.display(), 339 | "Ignoring directory with no 'include'" 340 | ); 341 | } 342 | } else if metadata.is_symlink() { 343 | let path_target = path.read_link()?; 344 | if !path_target.is_absolute() { 345 | trace!(src = %path.display(), 346 | target = %path_target.display(), 347 | "Packing relative symlink"); 348 | builder.append_path_with_name(&path, file_name)?; 349 | } else { 350 | warn!( 351 | src = %path.display(), 352 | "Ignoring absolute symlink" 353 | ); 354 | } 355 | } else if metadata.is_file() { 356 | trace!(src = %path.display(), "Packing file"); 357 | builder.append_path_with_name(&path, file_name)?; 358 | } else { 359 | warn!(src = %path.display(), "Ignoring unknown file type"); 360 | } 361 | } 362 | builder.into_inner()?.finish()?; 363 | 364 | Ok(()) 365 | } 366 | 367 | pub fn follow( 368 | data_dir: &DataDir, 369 | activate_opts: &ActivateOpts, 370 | override_configuration: Option<&str>, 371 | once: Option, 372 | ignore_etag: bool, 373 | ) -> anyhow::Result<()> { 374 | let shutdown_requested = Arc::new(AtomicBool::new(false)); 375 | let shutdown_on_signal = Arc::new(AtomicBool::new(false)); 376 | 377 | for sig in TERM_SIGNALS { 378 | // On first signal, mark shutdown as requested 379 | flag::register(*sig, Arc::clone(&shutdown_requested))?; 380 | // Also make the second signal shutdown immediately 381 | flag::register(*sig, Arc::clone(&shutdown_on_signal))?; 382 | 383 | // If shutdown_on_signal was already set, shutdown immediately on signal 384 | flag::register_conditional_shutdown(*sig, 1, Arc::clone(&shutdown_on_signal))?; 385 | } 386 | 387 | while !shutdown_requested.load(Ordering::SeqCst) { 388 | if let ControlFlow::Break(()) = follow_inner( 389 | data_dir, 390 | activate_opts, 391 | override_configuration, 392 | once, 393 | ignore_etag, 394 | )? { 395 | break; 396 | } 397 | 398 | // reload the config, just in case it changed in the meantime 399 | let config = data_dir.load_config()?; 400 | // During sleep, shutdown immediately on any signal 401 | shutdown_on_signal.store(true, Ordering::SeqCst); 402 | config.rng_sleep(); 403 | shutdown_on_signal.store(false, Ordering::SeqCst); 404 | } 405 | Ok(()) 406 | } 407 | 408 | fn follow_inner( 409 | data_dir: &DataDir, 410 | activate_opts: &ActivateOpts, 411 | override_configuration: Option<&str>, 412 | once: Option, 413 | ignore_etag: bool, 414 | ) -> Result, anyhow::Error> { 415 | with_activate_lock(Some(data_dir), || { 416 | // Note: we load every time, in case settings changed 417 | let config = data_dir.load_config()?; 418 | 419 | if config.is_paused() { 420 | info!("Paused"); 421 | } else { 422 | match follow_inner_try(&config, activate_opts, override_configuration, ignore_etag) { 423 | Ok(res) => { 424 | match res { 425 | Some((ref configuration, ref etag)) => { 426 | data_dir.update_last_reconfiguration(configuration, etag)?; 427 | info!(etag, "Successfully activated new configuration"); 428 | } 429 | None => { 430 | debug!("Remote not changed"); 431 | } 432 | } 433 | match (once, res.is_some()) { 434 | (None, _) => {} 435 | (Some(Once::Activate), false) => {} 436 | (Some(Once::Any), _) | (Some(Once::Activate), true) => { 437 | debug!("Exiting after success due to `once` option"); 438 | return Ok(ControlFlow::Break(())); 439 | } 440 | } 441 | } 442 | Err(e) => error!(error = %e, "Failed to activate new configuration"), 443 | } 444 | } 445 | Ok(ControlFlow::Continue(())) 446 | }) 447 | } 448 | 449 | pub fn follow_inner_try( 450 | config: &Config, 451 | activate_opts: &ActivateOpts, 452 | override_configuration: Option<&str>, 453 | ignore_etag: bool, 454 | ) -> anyhow::Result> { 455 | let configuration = override_configuration 456 | .map(Ok) 457 | .unwrap_or_else(|| config.configuration())?; 458 | 459 | let etag = self::get_etag(config.remote()?, config)?; 460 | 461 | if !ignore_etag && config.last_configuration() == configuration && config.last_etag() == etag { 462 | return Ok(None); 463 | } 464 | 465 | let tmp_dir = tempfile::TempDir::new()?; 466 | self::pull(config.remote()?, tmp_dir.path())?; 467 | self::activate_inner(tmp_dir.path(), configuration, activate_opts)?; 468 | 469 | Ok(Some((configuration.to_string(), etag))) 470 | } 471 | -------------------------------------------------------------------------------- /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 = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "android_system_properties" 13 | version = "0.1.5" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 16 | dependencies = [ 17 | "libc", 18 | ] 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.2.6" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "342258dd14006105c2b75ab1bd7543a03bdf0cfc94383303ac212a04939dff6f" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-wincon", 29 | "concolor-override", 30 | "concolor-query", 31 | "is-terminal", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "0.3.5" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "23ea9e81bd02e310c216d080f6223c179012256e5151c41db88d12c88a1684d2" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.1.1" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "a7d1bb534e9efed14f3e5f44e7dd1a4f709384023a4165199a4241e18dff0116" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-wincon" 52 | version = "0.2.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "c3127af6145b149f3287bb9a0d10ad9c5692dba8c53ad48285e5bec4063834fa" 55 | dependencies = [ 56 | "anstyle", 57 | "windows-sys 0.45.0", 58 | ] 59 | 60 | [[package]] 61 | name = "anyhow" 62 | version = "1.0.70" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" 65 | 66 | [[package]] 67 | name = "autocfg" 68 | version = "1.1.0" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 71 | 72 | [[package]] 73 | name = "base64" 74 | version = "0.13.1" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 77 | 78 | [[package]] 79 | name = "base64" 80 | version = "0.21.0" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" 83 | 84 | [[package]] 85 | name = "bitflags" 86 | version = "1.3.2" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 89 | 90 | [[package]] 91 | name = "block-buffer" 92 | version = "0.10.4" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 95 | dependencies = [ 96 | "generic-array", 97 | ] 98 | 99 | [[package]] 100 | name = "bumpalo" 101 | version = "3.12.0" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" 104 | 105 | [[package]] 106 | name = "cc" 107 | version = "1.0.79" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 110 | dependencies = [ 111 | "jobserver", 112 | ] 113 | 114 | [[package]] 115 | name = "cfg-if" 116 | version = "1.0.0" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 119 | 120 | [[package]] 121 | name = "chrono" 122 | version = "0.4.24" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" 125 | dependencies = [ 126 | "iana-time-zone", 127 | "js-sys", 128 | "num-integer", 129 | "num-traits", 130 | "serde", 131 | "time", 132 | "wasm-bindgen", 133 | "winapi", 134 | ] 135 | 136 | [[package]] 137 | name = "clap" 138 | version = "4.2.1" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "046ae530c528f252094e4a77886ee1374437744b2bff1497aa898bbddbbb29b3" 141 | dependencies = [ 142 | "clap_builder", 143 | "clap_derive", 144 | "once_cell", 145 | ] 146 | 147 | [[package]] 148 | name = "clap_builder" 149 | version = "4.2.1" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "223163f58c9a40c3b0a43e1c4b50a9ce09f007ea2cb1ec258a687945b4b7929f" 152 | dependencies = [ 153 | "anstream", 154 | "anstyle", 155 | "bitflags", 156 | "clap_lex", 157 | "strsim", 158 | ] 159 | 160 | [[package]] 161 | name = "clap_derive" 162 | version = "4.2.0" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" 165 | dependencies = [ 166 | "heck", 167 | "proc-macro2", 168 | "quote", 169 | "syn 2.0.13", 170 | ] 171 | 172 | [[package]] 173 | name = "clap_lex" 174 | version = "0.4.1" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" 177 | 178 | [[package]] 179 | name = "codespan-reporting" 180 | version = "0.11.1" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" 183 | dependencies = [ 184 | "termcolor", 185 | "unicode-width", 186 | ] 187 | 188 | [[package]] 189 | name = "concolor-override" 190 | version = "1.0.0" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "a855d4a1978dc52fb0536a04d384c2c0c1aa273597f08b77c8c4d3b2eec6037f" 193 | 194 | [[package]] 195 | name = "concolor-query" 196 | version = "0.3.3" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "88d11d52c3d7ca2e6d0040212be9e4dbbcd78b6447f535b6b561f449427944cf" 199 | dependencies = [ 200 | "windows-sys 0.45.0", 201 | ] 202 | 203 | [[package]] 204 | name = "core-foundation" 205 | version = "0.9.3" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 208 | dependencies = [ 209 | "core-foundation-sys", 210 | "libc", 211 | ] 212 | 213 | [[package]] 214 | name = "core-foundation-sys" 215 | version = "0.8.4" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" 218 | 219 | [[package]] 220 | name = "crc32fast" 221 | version = "1.3.2" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 224 | dependencies = [ 225 | "cfg-if", 226 | ] 227 | 228 | [[package]] 229 | name = "crypto-common" 230 | version = "0.1.6" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 233 | dependencies = [ 234 | "generic-array", 235 | "typenum", 236 | ] 237 | 238 | [[package]] 239 | name = "cxx" 240 | version = "1.0.94" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" 243 | dependencies = [ 244 | "cc", 245 | "cxxbridge-flags", 246 | "cxxbridge-macro", 247 | "link-cplusplus", 248 | ] 249 | 250 | [[package]] 251 | name = "cxx-build" 252 | version = "1.0.94" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" 255 | dependencies = [ 256 | "cc", 257 | "codespan-reporting", 258 | "once_cell", 259 | "proc-macro2", 260 | "quote", 261 | "scratch", 262 | "syn 2.0.13", 263 | ] 264 | 265 | [[package]] 266 | name = "cxxbridge-flags" 267 | version = "1.0.94" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" 270 | 271 | [[package]] 272 | name = "cxxbridge-macro" 273 | version = "1.0.94" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" 276 | dependencies = [ 277 | "proc-macro2", 278 | "quote", 279 | "syn 2.0.13", 280 | ] 281 | 282 | [[package]] 283 | name = "digest" 284 | version = "0.10.6" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" 287 | dependencies = [ 288 | "block-buffer", 289 | "crypto-common", 290 | ] 291 | 292 | [[package]] 293 | name = "errno" 294 | version = "0.3.0" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" 297 | dependencies = [ 298 | "errno-dragonfly", 299 | "libc", 300 | "windows-sys 0.45.0", 301 | ] 302 | 303 | [[package]] 304 | name = "errno-dragonfly" 305 | version = "0.1.2" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 308 | dependencies = [ 309 | "cc", 310 | "libc", 311 | ] 312 | 313 | [[package]] 314 | name = "fastrand" 315 | version = "1.9.0" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" 318 | dependencies = [ 319 | "instant", 320 | ] 321 | 322 | [[package]] 323 | name = "fd-lock" 324 | version = "3.0.12" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "39ae6b3d9530211fb3b12a95374b8b0823be812f53d09e18c5675c0146b09642" 327 | dependencies = [ 328 | "cfg-if", 329 | "rustix", 330 | "windows-sys 0.48.0", 331 | ] 332 | 333 | [[package]] 334 | name = "filetime" 335 | version = "0.2.21" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" 338 | dependencies = [ 339 | "cfg-if", 340 | "libc", 341 | "redox_syscall 0.2.16", 342 | "windows-sys 0.48.0", 343 | ] 344 | 345 | [[package]] 346 | name = "flate2" 347 | version = "1.0.25" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" 350 | dependencies = [ 351 | "crc32fast", 352 | "miniz_oxide", 353 | ] 354 | 355 | [[package]] 356 | name = "form_urlencoded" 357 | version = "1.1.0" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 360 | dependencies = [ 361 | "percent-encoding", 362 | ] 363 | 364 | [[package]] 365 | name = "generic-array" 366 | version = "0.14.7" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 369 | dependencies = [ 370 | "typenum", 371 | "version_check", 372 | ] 373 | 374 | [[package]] 375 | name = "getrandom" 376 | version = "0.2.9" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" 379 | dependencies = [ 380 | "cfg-if", 381 | "libc", 382 | "wasi 0.11.0+wasi-snapshot-preview1", 383 | ] 384 | 385 | [[package]] 386 | name = "heck" 387 | version = "0.4.1" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 390 | 391 | [[package]] 392 | name = "hermit-abi" 393 | version = "0.3.1" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 396 | 397 | [[package]] 398 | name = "iana-time-zone" 399 | version = "0.1.56" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" 402 | dependencies = [ 403 | "android_system_properties", 404 | "core-foundation-sys", 405 | "iana-time-zone-haiku", 406 | "js-sys", 407 | "wasm-bindgen", 408 | "windows", 409 | ] 410 | 411 | [[package]] 412 | name = "iana-time-zone-haiku" 413 | version = "0.1.1" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" 416 | dependencies = [ 417 | "cxx", 418 | "cxx-build", 419 | ] 420 | 421 | [[package]] 422 | name = "idna" 423 | version = "0.3.0" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 426 | dependencies = [ 427 | "unicode-bidi", 428 | "unicode-normalization", 429 | ] 430 | 431 | [[package]] 432 | name = "instant" 433 | version = "0.1.12" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 436 | dependencies = [ 437 | "cfg-if", 438 | ] 439 | 440 | [[package]] 441 | name = "io-lifetimes" 442 | version = "1.0.10" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" 445 | dependencies = [ 446 | "hermit-abi", 447 | "libc", 448 | "windows-sys 0.48.0", 449 | ] 450 | 451 | [[package]] 452 | name = "is-terminal" 453 | version = "0.4.6" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "256017f749ab3117e93acb91063009e1f1bb56d03965b14c2c8df4eb02c524d8" 456 | dependencies = [ 457 | "hermit-abi", 458 | "io-lifetimes", 459 | "rustix", 460 | "windows-sys 0.45.0", 461 | ] 462 | 463 | [[package]] 464 | name = "itoa" 465 | version = "1.0.6" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" 468 | 469 | [[package]] 470 | name = "jobserver" 471 | version = "0.1.26" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" 474 | dependencies = [ 475 | "libc", 476 | ] 477 | 478 | [[package]] 479 | name = "js-sys" 480 | version = "0.3.61" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" 483 | dependencies = [ 484 | "wasm-bindgen", 485 | ] 486 | 487 | [[package]] 488 | name = "lazy_static" 489 | version = "1.4.0" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 492 | 493 | [[package]] 494 | name = "libc" 495 | version = "0.2.141" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" 498 | 499 | [[package]] 500 | name = "link-cplusplus" 501 | version = "1.0.8" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" 504 | dependencies = [ 505 | "cc", 506 | ] 507 | 508 | [[package]] 509 | name = "linux-raw-sys" 510 | version = "0.3.1" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" 513 | 514 | [[package]] 515 | name = "log" 516 | version = "0.4.17" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 519 | dependencies = [ 520 | "cfg-if", 521 | ] 522 | 523 | [[package]] 524 | name = "matchers" 525 | version = "0.1.0" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 528 | dependencies = [ 529 | "regex-automata", 530 | ] 531 | 532 | [[package]] 533 | name = "md-5" 534 | version = "0.10.5" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" 537 | dependencies = [ 538 | "digest", 539 | ] 540 | 541 | [[package]] 542 | name = "miniz_oxide" 543 | version = "0.6.2" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" 546 | dependencies = [ 547 | "adler", 548 | ] 549 | 550 | [[package]] 551 | name = "npcnix" 552 | version = "0.1.0" 553 | dependencies = [ 554 | "anyhow", 555 | "chrono", 556 | "clap", 557 | "fd-lock", 558 | "md-5", 559 | "rand", 560 | "serde", 561 | "serde_json", 562 | "signal-hook", 563 | "tar", 564 | "tempfile", 565 | "tracing", 566 | "tracing-subscriber", 567 | "ureq", 568 | "url", 569 | "zstd", 570 | ] 571 | 572 | [[package]] 573 | name = "nu-ansi-term" 574 | version = "0.46.0" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 577 | dependencies = [ 578 | "overload", 579 | "winapi", 580 | ] 581 | 582 | [[package]] 583 | name = "num-integer" 584 | version = "0.1.45" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 587 | dependencies = [ 588 | "autocfg", 589 | "num-traits", 590 | ] 591 | 592 | [[package]] 593 | name = "num-traits" 594 | version = "0.2.15" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 597 | dependencies = [ 598 | "autocfg", 599 | ] 600 | 601 | [[package]] 602 | name = "once_cell" 603 | version = "1.17.1" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 606 | 607 | [[package]] 608 | name = "openssl-probe" 609 | version = "0.1.5" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 612 | 613 | [[package]] 614 | name = "overload" 615 | version = "0.1.1" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 618 | 619 | [[package]] 620 | name = "percent-encoding" 621 | version = "2.2.0" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 624 | 625 | [[package]] 626 | name = "pin-project-lite" 627 | version = "0.2.9" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 630 | 631 | [[package]] 632 | name = "pkg-config" 633 | version = "0.3.26" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 636 | 637 | [[package]] 638 | name = "ppv-lite86" 639 | version = "0.2.17" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 642 | 643 | [[package]] 644 | name = "proc-macro2" 645 | version = "1.0.56" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" 648 | dependencies = [ 649 | "unicode-ident", 650 | ] 651 | 652 | [[package]] 653 | name = "quote" 654 | version = "1.0.26" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" 657 | dependencies = [ 658 | "proc-macro2", 659 | ] 660 | 661 | [[package]] 662 | name = "rand" 663 | version = "0.8.5" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 666 | dependencies = [ 667 | "libc", 668 | "rand_chacha", 669 | "rand_core", 670 | ] 671 | 672 | [[package]] 673 | name = "rand_chacha" 674 | version = "0.3.1" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 677 | dependencies = [ 678 | "ppv-lite86", 679 | "rand_core", 680 | ] 681 | 682 | [[package]] 683 | name = "rand_core" 684 | version = "0.6.4" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 687 | dependencies = [ 688 | "getrandom", 689 | ] 690 | 691 | [[package]] 692 | name = "redox_syscall" 693 | version = "0.2.16" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 696 | dependencies = [ 697 | "bitflags", 698 | ] 699 | 700 | [[package]] 701 | name = "redox_syscall" 702 | version = "0.3.5" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" 705 | dependencies = [ 706 | "bitflags", 707 | ] 708 | 709 | [[package]] 710 | name = "regex" 711 | version = "1.7.3" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" 714 | dependencies = [ 715 | "regex-syntax", 716 | ] 717 | 718 | [[package]] 719 | name = "regex-automata" 720 | version = "0.1.10" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 723 | dependencies = [ 724 | "regex-syntax", 725 | ] 726 | 727 | [[package]] 728 | name = "regex-syntax" 729 | version = "0.6.29" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 732 | 733 | [[package]] 734 | name = "ring" 735 | version = "0.16.20" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" 738 | dependencies = [ 739 | "cc", 740 | "libc", 741 | "once_cell", 742 | "spin", 743 | "untrusted", 744 | "web-sys", 745 | "winapi", 746 | ] 747 | 748 | [[package]] 749 | name = "rustix" 750 | version = "0.37.7" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "2aae838e49b3d63e9274e1c01833cc8139d3fec468c3b84688c628f44b1ae11d" 753 | dependencies = [ 754 | "bitflags", 755 | "errno", 756 | "io-lifetimes", 757 | "libc", 758 | "linux-raw-sys", 759 | "windows-sys 0.45.0", 760 | ] 761 | 762 | [[package]] 763 | name = "rustls" 764 | version = "0.20.8" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" 767 | dependencies = [ 768 | "log", 769 | "ring", 770 | "sct", 771 | "webpki", 772 | ] 773 | 774 | [[package]] 775 | name = "rustls-native-certs" 776 | version = "0.6.2" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" 779 | dependencies = [ 780 | "openssl-probe", 781 | "rustls-pemfile", 782 | "schannel", 783 | "security-framework", 784 | ] 785 | 786 | [[package]] 787 | name = "rustls-pemfile" 788 | version = "1.0.2" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" 791 | dependencies = [ 792 | "base64 0.21.0", 793 | ] 794 | 795 | [[package]] 796 | name = "ryu" 797 | version = "1.0.13" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" 800 | 801 | [[package]] 802 | name = "schannel" 803 | version = "0.1.21" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" 806 | dependencies = [ 807 | "windows-sys 0.42.0", 808 | ] 809 | 810 | [[package]] 811 | name = "scratch" 812 | version = "1.0.5" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" 815 | 816 | [[package]] 817 | name = "sct" 818 | version = "0.7.0" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" 821 | dependencies = [ 822 | "ring", 823 | "untrusted", 824 | ] 825 | 826 | [[package]] 827 | name = "security-framework" 828 | version = "2.8.2" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" 831 | dependencies = [ 832 | "bitflags", 833 | "core-foundation", 834 | "core-foundation-sys", 835 | "libc", 836 | "security-framework-sys", 837 | ] 838 | 839 | [[package]] 840 | name = "security-framework-sys" 841 | version = "2.8.0" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" 844 | dependencies = [ 845 | "core-foundation-sys", 846 | "libc", 847 | ] 848 | 849 | [[package]] 850 | name = "serde" 851 | version = "1.0.160" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" 854 | dependencies = [ 855 | "serde_derive", 856 | ] 857 | 858 | [[package]] 859 | name = "serde_derive" 860 | version = "1.0.160" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" 863 | dependencies = [ 864 | "proc-macro2", 865 | "quote", 866 | "syn 2.0.13", 867 | ] 868 | 869 | [[package]] 870 | name = "serde_json" 871 | version = "1.0.95" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" 874 | dependencies = [ 875 | "itoa", 876 | "ryu", 877 | "serde", 878 | ] 879 | 880 | [[package]] 881 | name = "sharded-slab" 882 | version = "0.1.4" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" 885 | dependencies = [ 886 | "lazy_static", 887 | ] 888 | 889 | [[package]] 890 | name = "signal-hook" 891 | version = "0.3.15" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" 894 | dependencies = [ 895 | "libc", 896 | "signal-hook-registry", 897 | ] 898 | 899 | [[package]] 900 | name = "signal-hook-registry" 901 | version = "1.4.1" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 904 | dependencies = [ 905 | "libc", 906 | ] 907 | 908 | [[package]] 909 | name = "smallvec" 910 | version = "1.10.0" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 913 | 914 | [[package]] 915 | name = "spin" 916 | version = "0.5.2" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" 919 | 920 | [[package]] 921 | name = "strsim" 922 | version = "0.10.0" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 925 | 926 | [[package]] 927 | name = "syn" 928 | version = "1.0.109" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 931 | dependencies = [ 932 | "proc-macro2", 933 | "quote", 934 | "unicode-ident", 935 | ] 936 | 937 | [[package]] 938 | name = "syn" 939 | version = "2.0.13" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec" 942 | dependencies = [ 943 | "proc-macro2", 944 | "quote", 945 | "unicode-ident", 946 | ] 947 | 948 | [[package]] 949 | name = "tar" 950 | version = "0.4.38" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" 953 | dependencies = [ 954 | "filetime", 955 | "libc", 956 | "xattr", 957 | ] 958 | 959 | [[package]] 960 | name = "tempfile" 961 | version = "3.5.0" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" 964 | dependencies = [ 965 | "cfg-if", 966 | "fastrand", 967 | "redox_syscall 0.3.5", 968 | "rustix", 969 | "windows-sys 0.45.0", 970 | ] 971 | 972 | [[package]] 973 | name = "termcolor" 974 | version = "1.2.0" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" 977 | dependencies = [ 978 | "winapi-util", 979 | ] 980 | 981 | [[package]] 982 | name = "thread_local" 983 | version = "1.1.7" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" 986 | dependencies = [ 987 | "cfg-if", 988 | "once_cell", 989 | ] 990 | 991 | [[package]] 992 | name = "time" 993 | version = "0.1.45" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" 996 | dependencies = [ 997 | "libc", 998 | "wasi 0.10.0+wasi-snapshot-preview1", 999 | "winapi", 1000 | ] 1001 | 1002 | [[package]] 1003 | name = "tinyvec" 1004 | version = "1.6.0" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1007 | dependencies = [ 1008 | "tinyvec_macros", 1009 | ] 1010 | 1011 | [[package]] 1012 | name = "tinyvec_macros" 1013 | version = "0.1.1" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1016 | 1017 | [[package]] 1018 | name = "tracing" 1019 | version = "0.1.37" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 1022 | dependencies = [ 1023 | "cfg-if", 1024 | "pin-project-lite", 1025 | "tracing-attributes", 1026 | "tracing-core", 1027 | ] 1028 | 1029 | [[package]] 1030 | name = "tracing-attributes" 1031 | version = "0.1.23" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" 1034 | dependencies = [ 1035 | "proc-macro2", 1036 | "quote", 1037 | "syn 1.0.109", 1038 | ] 1039 | 1040 | [[package]] 1041 | name = "tracing-core" 1042 | version = "0.1.30" 1043 | source = "registry+https://github.com/rust-lang/crates.io-index" 1044 | checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" 1045 | dependencies = [ 1046 | "once_cell", 1047 | "valuable", 1048 | ] 1049 | 1050 | [[package]] 1051 | name = "tracing-log" 1052 | version = "0.1.3" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" 1055 | dependencies = [ 1056 | "lazy_static", 1057 | "log", 1058 | "tracing-core", 1059 | ] 1060 | 1061 | [[package]] 1062 | name = "tracing-subscriber" 1063 | version = "0.3.16" 1064 | source = "registry+https://github.com/rust-lang/crates.io-index" 1065 | checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" 1066 | dependencies = [ 1067 | "matchers", 1068 | "nu-ansi-term", 1069 | "once_cell", 1070 | "regex", 1071 | "sharded-slab", 1072 | "smallvec", 1073 | "thread_local", 1074 | "tracing", 1075 | "tracing-core", 1076 | "tracing-log", 1077 | ] 1078 | 1079 | [[package]] 1080 | name = "typenum" 1081 | version = "1.16.0" 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" 1083 | checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" 1084 | 1085 | [[package]] 1086 | name = "unicode-bidi" 1087 | version = "0.3.13" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" 1090 | 1091 | [[package]] 1092 | name = "unicode-ident" 1093 | version = "1.0.8" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 1096 | 1097 | [[package]] 1098 | name = "unicode-normalization" 1099 | version = "0.1.22" 1100 | source = "registry+https://github.com/rust-lang/crates.io-index" 1101 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 1102 | dependencies = [ 1103 | "tinyvec", 1104 | ] 1105 | 1106 | [[package]] 1107 | name = "unicode-width" 1108 | version = "0.1.10" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 1111 | 1112 | [[package]] 1113 | name = "untrusted" 1114 | version = "0.7.1" 1115 | source = "registry+https://github.com/rust-lang/crates.io-index" 1116 | checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" 1117 | 1118 | [[package]] 1119 | name = "ureq" 1120 | version = "2.6.2" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d" 1123 | dependencies = [ 1124 | "base64 0.13.1", 1125 | "flate2", 1126 | "log", 1127 | "once_cell", 1128 | "rustls", 1129 | "rustls-native-certs", 1130 | "url", 1131 | "webpki", 1132 | "webpki-roots", 1133 | ] 1134 | 1135 | [[package]] 1136 | name = "url" 1137 | version = "2.3.1" 1138 | source = "registry+https://github.com/rust-lang/crates.io-index" 1139 | checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" 1140 | dependencies = [ 1141 | "form_urlencoded", 1142 | "idna", 1143 | "percent-encoding", 1144 | "serde", 1145 | ] 1146 | 1147 | [[package]] 1148 | name = "utf8parse" 1149 | version = "0.2.1" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1152 | 1153 | [[package]] 1154 | name = "valuable" 1155 | version = "0.1.0" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 1158 | 1159 | [[package]] 1160 | name = "version_check" 1161 | version = "0.9.4" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1164 | 1165 | [[package]] 1166 | name = "wasi" 1167 | version = "0.10.0+wasi-snapshot-preview1" 1168 | source = "registry+https://github.com/rust-lang/crates.io-index" 1169 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 1170 | 1171 | [[package]] 1172 | name = "wasi" 1173 | version = "0.11.0+wasi-snapshot-preview1" 1174 | source = "registry+https://github.com/rust-lang/crates.io-index" 1175 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1176 | 1177 | [[package]] 1178 | name = "wasm-bindgen" 1179 | version = "0.2.84" 1180 | source = "registry+https://github.com/rust-lang/crates.io-index" 1181 | checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" 1182 | dependencies = [ 1183 | "cfg-if", 1184 | "wasm-bindgen-macro", 1185 | ] 1186 | 1187 | [[package]] 1188 | name = "wasm-bindgen-backend" 1189 | version = "0.2.84" 1190 | source = "registry+https://github.com/rust-lang/crates.io-index" 1191 | checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" 1192 | dependencies = [ 1193 | "bumpalo", 1194 | "log", 1195 | "once_cell", 1196 | "proc-macro2", 1197 | "quote", 1198 | "syn 1.0.109", 1199 | "wasm-bindgen-shared", 1200 | ] 1201 | 1202 | [[package]] 1203 | name = "wasm-bindgen-macro" 1204 | version = "0.2.84" 1205 | source = "registry+https://github.com/rust-lang/crates.io-index" 1206 | checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" 1207 | dependencies = [ 1208 | "quote", 1209 | "wasm-bindgen-macro-support", 1210 | ] 1211 | 1212 | [[package]] 1213 | name = "wasm-bindgen-macro-support" 1214 | version = "0.2.84" 1215 | source = "registry+https://github.com/rust-lang/crates.io-index" 1216 | checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" 1217 | dependencies = [ 1218 | "proc-macro2", 1219 | "quote", 1220 | "syn 1.0.109", 1221 | "wasm-bindgen-backend", 1222 | "wasm-bindgen-shared", 1223 | ] 1224 | 1225 | [[package]] 1226 | name = "wasm-bindgen-shared" 1227 | version = "0.2.84" 1228 | source = "registry+https://github.com/rust-lang/crates.io-index" 1229 | checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" 1230 | 1231 | [[package]] 1232 | name = "web-sys" 1233 | version = "0.3.61" 1234 | source = "registry+https://github.com/rust-lang/crates.io-index" 1235 | checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" 1236 | dependencies = [ 1237 | "js-sys", 1238 | "wasm-bindgen", 1239 | ] 1240 | 1241 | [[package]] 1242 | name = "webpki" 1243 | version = "0.22.0" 1244 | source = "registry+https://github.com/rust-lang/crates.io-index" 1245 | checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" 1246 | dependencies = [ 1247 | "ring", 1248 | "untrusted", 1249 | ] 1250 | 1251 | [[package]] 1252 | name = "webpki-roots" 1253 | version = "0.22.6" 1254 | source = "registry+https://github.com/rust-lang/crates.io-index" 1255 | checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" 1256 | dependencies = [ 1257 | "webpki", 1258 | ] 1259 | 1260 | [[package]] 1261 | name = "winapi" 1262 | version = "0.3.9" 1263 | source = "registry+https://github.com/rust-lang/crates.io-index" 1264 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1265 | dependencies = [ 1266 | "winapi-i686-pc-windows-gnu", 1267 | "winapi-x86_64-pc-windows-gnu", 1268 | ] 1269 | 1270 | [[package]] 1271 | name = "winapi-i686-pc-windows-gnu" 1272 | version = "0.4.0" 1273 | source = "registry+https://github.com/rust-lang/crates.io-index" 1274 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1275 | 1276 | [[package]] 1277 | name = "winapi-util" 1278 | version = "0.1.5" 1279 | source = "registry+https://github.com/rust-lang/crates.io-index" 1280 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1281 | dependencies = [ 1282 | "winapi", 1283 | ] 1284 | 1285 | [[package]] 1286 | name = "winapi-x86_64-pc-windows-gnu" 1287 | version = "0.4.0" 1288 | source = "registry+https://github.com/rust-lang/crates.io-index" 1289 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1290 | 1291 | [[package]] 1292 | name = "windows" 1293 | version = "0.48.0" 1294 | source = "registry+https://github.com/rust-lang/crates.io-index" 1295 | checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" 1296 | dependencies = [ 1297 | "windows-targets 0.48.0", 1298 | ] 1299 | 1300 | [[package]] 1301 | name = "windows-sys" 1302 | version = "0.42.0" 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" 1304 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 1305 | dependencies = [ 1306 | "windows_aarch64_gnullvm 0.42.2", 1307 | "windows_aarch64_msvc 0.42.2", 1308 | "windows_i686_gnu 0.42.2", 1309 | "windows_i686_msvc 0.42.2", 1310 | "windows_x86_64_gnu 0.42.2", 1311 | "windows_x86_64_gnullvm 0.42.2", 1312 | "windows_x86_64_msvc 0.42.2", 1313 | ] 1314 | 1315 | [[package]] 1316 | name = "windows-sys" 1317 | version = "0.45.0" 1318 | source = "registry+https://github.com/rust-lang/crates.io-index" 1319 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 1320 | dependencies = [ 1321 | "windows-targets 0.42.2", 1322 | ] 1323 | 1324 | [[package]] 1325 | name = "windows-sys" 1326 | version = "0.48.0" 1327 | source = "registry+https://github.com/rust-lang/crates.io-index" 1328 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1329 | dependencies = [ 1330 | "windows-targets 0.48.0", 1331 | ] 1332 | 1333 | [[package]] 1334 | name = "windows-targets" 1335 | version = "0.42.2" 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" 1337 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 1338 | dependencies = [ 1339 | "windows_aarch64_gnullvm 0.42.2", 1340 | "windows_aarch64_msvc 0.42.2", 1341 | "windows_i686_gnu 0.42.2", 1342 | "windows_i686_msvc 0.42.2", 1343 | "windows_x86_64_gnu 0.42.2", 1344 | "windows_x86_64_gnullvm 0.42.2", 1345 | "windows_x86_64_msvc 0.42.2", 1346 | ] 1347 | 1348 | [[package]] 1349 | name = "windows-targets" 1350 | version = "0.48.0" 1351 | source = "registry+https://github.com/rust-lang/crates.io-index" 1352 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 1353 | dependencies = [ 1354 | "windows_aarch64_gnullvm 0.48.0", 1355 | "windows_aarch64_msvc 0.48.0", 1356 | "windows_i686_gnu 0.48.0", 1357 | "windows_i686_msvc 0.48.0", 1358 | "windows_x86_64_gnu 0.48.0", 1359 | "windows_x86_64_gnullvm 0.48.0", 1360 | "windows_x86_64_msvc 0.48.0", 1361 | ] 1362 | 1363 | [[package]] 1364 | name = "windows_aarch64_gnullvm" 1365 | version = "0.42.2" 1366 | source = "registry+https://github.com/rust-lang/crates.io-index" 1367 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 1368 | 1369 | [[package]] 1370 | name = "windows_aarch64_gnullvm" 1371 | version = "0.48.0" 1372 | source = "registry+https://github.com/rust-lang/crates.io-index" 1373 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 1374 | 1375 | [[package]] 1376 | name = "windows_aarch64_msvc" 1377 | version = "0.42.2" 1378 | source = "registry+https://github.com/rust-lang/crates.io-index" 1379 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 1380 | 1381 | [[package]] 1382 | name = "windows_aarch64_msvc" 1383 | version = "0.48.0" 1384 | source = "registry+https://github.com/rust-lang/crates.io-index" 1385 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 1386 | 1387 | [[package]] 1388 | name = "windows_i686_gnu" 1389 | version = "0.42.2" 1390 | source = "registry+https://github.com/rust-lang/crates.io-index" 1391 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 1392 | 1393 | [[package]] 1394 | name = "windows_i686_gnu" 1395 | version = "0.48.0" 1396 | source = "registry+https://github.com/rust-lang/crates.io-index" 1397 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 1398 | 1399 | [[package]] 1400 | name = "windows_i686_msvc" 1401 | version = "0.42.2" 1402 | source = "registry+https://github.com/rust-lang/crates.io-index" 1403 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 1404 | 1405 | [[package]] 1406 | name = "windows_i686_msvc" 1407 | version = "0.48.0" 1408 | source = "registry+https://github.com/rust-lang/crates.io-index" 1409 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 1410 | 1411 | [[package]] 1412 | name = "windows_x86_64_gnu" 1413 | version = "0.42.2" 1414 | source = "registry+https://github.com/rust-lang/crates.io-index" 1415 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 1416 | 1417 | [[package]] 1418 | name = "windows_x86_64_gnu" 1419 | version = "0.48.0" 1420 | source = "registry+https://github.com/rust-lang/crates.io-index" 1421 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 1422 | 1423 | [[package]] 1424 | name = "windows_x86_64_gnullvm" 1425 | version = "0.42.2" 1426 | source = "registry+https://github.com/rust-lang/crates.io-index" 1427 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 1428 | 1429 | [[package]] 1430 | name = "windows_x86_64_gnullvm" 1431 | version = "0.48.0" 1432 | source = "registry+https://github.com/rust-lang/crates.io-index" 1433 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 1434 | 1435 | [[package]] 1436 | name = "windows_x86_64_msvc" 1437 | version = "0.42.2" 1438 | source = "registry+https://github.com/rust-lang/crates.io-index" 1439 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 1440 | 1441 | [[package]] 1442 | name = "windows_x86_64_msvc" 1443 | version = "0.48.0" 1444 | source = "registry+https://github.com/rust-lang/crates.io-index" 1445 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 1446 | 1447 | [[package]] 1448 | name = "xattr" 1449 | version = "0.2.3" 1450 | source = "registry+https://github.com/rust-lang/crates.io-index" 1451 | checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" 1452 | dependencies = [ 1453 | "libc", 1454 | ] 1455 | 1456 | [[package]] 1457 | name = "zstd" 1458 | version = "0.12.3+zstd.1.5.2" 1459 | source = "registry+https://github.com/rust-lang/crates.io-index" 1460 | checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" 1461 | dependencies = [ 1462 | "zstd-safe", 1463 | ] 1464 | 1465 | [[package]] 1466 | name = "zstd-safe" 1467 | version = "6.0.5+zstd.1.5.4" 1468 | source = "registry+https://github.com/rust-lang/crates.io-index" 1469 | checksum = "d56d9e60b4b1758206c238a10165fbcae3ca37b01744e394c463463f6529d23b" 1470 | dependencies = [ 1471 | "libc", 1472 | "zstd-sys", 1473 | ] 1474 | 1475 | [[package]] 1476 | name = "zstd-sys" 1477 | version = "2.0.8+zstd.1.5.5" 1478 | source = "registry+https://github.com/rust-lang/crates.io-index" 1479 | checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" 1480 | dependencies = [ 1481 | "cc", 1482 | "libc", 1483 | "pkg-config", 1484 | ] 1485 | --------------------------------------------------------------------------------