├── .gitignore ├── rust-toolchain.toml ├── .beardist.json ├── docker-bake.hcl ├── src ├── utils.rs ├── indented_writer.rs ├── command.rs ├── homebrew │ └── tests.rs ├── system.rs ├── k8s.rs ├── github.rs ├── target_spec.rs ├── homebrew.rs ├── cargo.rs └── main.rs ├── Justfile ├── LICENSE-MIT ├── Cargo.toml ├── clippy.toml ├── README.md ├── repack.sh ├── .github └── workflows │ └── build.yml ├── Dockerfile ├── LICENSE-APACHE └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | manifest.json 3 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.86.0" 3 | components = ["rustfmt", "clippy"] 4 | -------------------------------------------------------------------------------- /.beardist.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "org": "bearcove", 4 | "name": "beardist", 5 | "cargo": { 6 | "bins": ["beardist"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docker-bake.hcl: -------------------------------------------------------------------------------- 1 | group "default" { 2 | targets = ["base", "beardist"] 3 | } 4 | 5 | target "base" { 6 | context = "." 7 | dockerfile = "Dockerfile" 8 | target = "base" 9 | tags = ["ghcr.io/bearcove/base:latest"] 10 | platforms = ["linux/amd64", "linux/arm64"] 11 | output = ["type=registry"] 12 | } 13 | 14 | target "beardist" { 15 | context = "." 16 | dockerfile = "Dockerfile" 17 | args = {} 18 | tags = ["ghcr.io/bearcove/beardist:latest"] 19 | platforms = ["linux/amd64", "linux/arm64"] 20 | output = ["type=registry"] 21 | } 22 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::owo_colors::OwoColorize; 2 | 3 | pub fn format_bytes(bytes: u64) -> String { 4 | const UNITS: [&str; 5] = ["Bytes", "KB", "MB", "GB", "TB"]; 5 | if bytes == 0 { 6 | return "0 Byte".to_string(); 7 | } 8 | let i = (bytes as f64).log(1024.0).floor() as usize; 9 | format!( 10 | "{:.2} {}", 11 | (bytes as f64) / 1024f64.powi(i as i32), 12 | UNITS[i] 13 | ) 14 | } 15 | 16 | pub fn format_secret(secret: &str) -> String { 17 | if secret.len() >= 4 { 18 | format!( 19 | "{}{}{}", 20 | &secret[..2], 21 | "...".dimmed(), 22 | &secret[secret.len() - 2..] 23 | ) 24 | } else { 25 | "(too short)".dimmed().to_string() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | BASE := "ghcr.io/bearcove" 2 | 3 | check: 4 | cargo clippy --all-targets --all-features -- -D warnings 5 | cargo nextest run --no-capture 6 | 7 | push: 8 | #!/usr/bin/env -S bash -euxo pipefail 9 | PLATFORMS=${PLATFORMS:-"linux/arm64,linux/amd64"} 10 | 11 | # Define tags array 12 | tags=() 13 | 14 | # Check if we're on a tag and get the version 15 | if [[ "${GITHUB_REF:-}" == refs/tags/* ]]; then 16 | TAG_VERSION="${GITHUB_REF#refs/tags/}" 17 | # Remove 'v' prefix if present 18 | TAG_VERSION="${TAG_VERSION#v}" 19 | echo -e "\033[1;33m📦 Detected tag: ${TAG_VERSION}\033[0m" 20 | 21 | # Add version-specific tags 22 | tags+=("--tag" "{{BASE}}/beardist:${TAG_VERSION}") 23 | fi 24 | 25 | # Add latest tag 26 | tags+=("--tag" "{{BASE}}/beardist:latest") 27 | 28 | # Build for all platforms at once 29 | docker buildx build \ 30 | --target beardist \ 31 | --platform "${PLATFORMS}" \ 32 | "${tags[@]}" \ 33 | --push \ 34 | . 35 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "beardist" 3 | version = "1.1.0" 4 | edition = "2024" 5 | license = "Apache-2.0/MIT" 6 | description = "Builds Rust code in CI and uploads it to GitHub Releases / registry" 7 | authors = ["Amos Wenger "] 8 | repository = "https://github.com/bearcove/beardist" 9 | keywords = ["distribution", "tool"] 10 | categories = ["command-line-utilities"] 11 | 12 | [dependencies] 13 | camino = "1.1.9" 14 | clap = { version = "4.5.37", features = ["derive"] } 15 | color-eyre = "0.6.3" 16 | log = "0.4.27" 17 | owo-colors = "4.2.0" 18 | reqwest = { version = "0.12.15", features = ["blocking", "json"] } 19 | serde = { version = "1.0.219", features = ["derive"] } 20 | sha2 = "0.10.8" 21 | env_logger = "0.11.8" 22 | convert_case = "0.8.0" 23 | eyre = "0.6.12" 24 | serde_json = "1.0.140" 25 | fs-err = "3.1.0" 26 | ignore = "0.4.23" 27 | tempfile = "3.19.1" 28 | indexmap = "2.9.0" 29 | timelord = { version = "3.0.2" } 30 | glob = "0.3.2" 31 | rand = "0.9.1" 32 | hostname = "0.4.1" 33 | sys-info = "0.9.1" 34 | regex = "1.11.1" 35 | semver = "1.0.26" 36 | url = "2.5.4" 37 | 38 | [dev-dependencies] 39 | tempfile = "3.19.1" 40 | -------------------------------------------------------------------------------- /src/indented_writer.rs: -------------------------------------------------------------------------------- 1 | pub(crate) struct IndentedWriter<'a> { 2 | inner: &'a mut String, 3 | indent_level: usize, 4 | } 5 | 6 | impl<'a> IndentedWriter<'a> { 7 | pub(crate) fn new(inner: &'a mut String) -> Self { 8 | IndentedWriter { 9 | inner, 10 | indent_level: 1, 11 | } 12 | } 13 | 14 | #[allow(dead_code)] 15 | pub(crate) fn indented(&mut self) -> IndentedWriter { 16 | IndentedWriter { 17 | inner: self.inner, 18 | indent_level: self.indent_level + 1, 19 | } 20 | } 21 | } 22 | 23 | impl std::fmt::Write for IndentedWriter<'_> { 24 | fn write_str(&mut self, s: &str) -> std::fmt::Result { 25 | // This function writes the input string to the inner buffer, 26 | // adding indentation at the start of each line. 27 | let mut needs_indent = self.inner.is_empty() || self.inner.ends_with('\n'); 28 | 29 | for c in s.chars() { 30 | if needs_indent && c != '\n' { 31 | for _ in 0..self.indent_level { 32 | self.inner.push_str(" "); 33 | } 34 | needs_indent = false; 35 | } 36 | self.inner.push(c); 37 | if c == '\n' { 38 | needs_indent = true; 39 | } 40 | } 41 | Ok(()) 42 | } 43 | } 44 | 45 | pub(crate) trait Indented { 46 | fn indented(&mut self) -> IndentedWriter; 47 | } 48 | 49 | impl Indented for String { 50 | fn indented(&mut self) -> IndentedWriter { 51 | IndentedWriter::new(self) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | # clippy.toml 2 | 3 | disallowed-methods = [ 4 | # Disallow std::fs::read, write, etc. 5 | { path = "std::fs::read", reason = "Use `fs_err::read` instead" }, 6 | { path = "std::fs::write", reason = "Use `fs_err::write` instead" }, 7 | { path = "std::fs::File::open", reason = "Use `fs_err::File::open` instead" }, 8 | { path = "std::fs::File::create", reason = "Use `fs_err::File::create` instead" }, 9 | { path = "std::fs::remove_file", reason = "Use `fs_err::remove_file` instead" }, 10 | { path = "std::fs::remove_dir_all", reason = "Use `fs_err::remove_dir_all` instead" }, 11 | { path = "std::fs::create_dir", reason = "Use `fs_err::create_dir` instead" }, 12 | { path = "std::fs::create_dir_all", reason = "Use `fs_err::create_dir_all` instead" }, 13 | { path = "std::fs::copy", reason = "Use `fs_err::copy` instead" }, 14 | { path = "std::fs::rename", reason = "Use `fs_err::rename` instead" }, 15 | { path = "std::fs::metadata", reason = "Use `fs_err::metadata` instead" }, 16 | { path = "std::fs::symlink_metadata", reason = "Use `fs_err::symlink_metadata` instead" }, 17 | { path = "std::fs::read_dir", reason = "Use `fs_err::read_dir` instead" }, 18 | { path = "std::fs::canonicalize", reason = "Use `fs_err::canonicalize` instead" }, 19 | { path = "std::fs::read_to_string", reason = "Use `fs_err::read_to_string` instead" }, 20 | { path = "std::fs::read_link", reason = "Use `fs_err::read_link` instead" }, 21 | { path = "std::fs::hard_link", reason = "Use `fs_err::hard_link` instead" }, 22 | { path = "std::fs::symlink", reason = "Use `fs_err::symlink` instead" }, 23 | { path = "std::fs::set_permissions", reason = "Use `fs_err::set_permissions` instead" }, 24 | # You can add more as needed 25 | ] 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # beardist 2 | 3 | Thanks to all individual and corporate sponsors, without whom this work could not exist: 4 | 5 |

6 | 7 | 8 | Ko-fi 9 | 10 | 11 | 12 | 13 | GitHub Sponsors 14 | 15 | 16 | 17 | 18 | Patreon 19 | 20 | 21 | 22 | 23 | Zed 24 | 25 | 26 | 27 | 28 | Depot 29 | 30 |

31 | 32 | 33 | Builds a rust binary with fast defaults, uploads it to GitHub Releases. 34 | 35 | Then, later, updates the formula for it in a homebrew tap. 36 | 37 | A lot of things are hardcoded specifically for 38 | 39 | ## License 40 | 41 | Licensed under either of: 42 | 43 | - Apache License, Version 2.0 ([LICENSE-APACHE](https://github.com/facet-rs/facet/blob/main/LICENSE-APACHE) or ) 44 | - MIT license ([LICENSE-MIT](https://github.com/facet-rs/facet/blob/main/LICENSE-MIT) or ) 45 | 46 | at your option. 47 | -------------------------------------------------------------------------------- /repack.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # This script creates a container image for beardist 5 | # The process involves: 6 | # 1. Creating an OCI layout directory for beardist 7 | # 2. Adding beardist to the image 8 | # 3. Pushing the final image with proper tags 9 | 10 | if [ "${IMAGE_PLATFORM}" != "linux/arm64" ] && [ "${IMAGE_PLATFORM}" != "linux/amd64" ]; then 11 | echo -e "\033[1;31m❌ Error: IMAGE_PLATFORM must be set to linux/arm64 or linux/amd64\033[0m" >&2 12 | exit 1 13 | fi 14 | 15 | ARCH_NAME=$(echo "${IMAGE_PLATFORM}" | cut -d'/' -f2) 16 | 17 | # Check if we're on a tag and get the version 18 | TAG_VERSION="" 19 | if [[ "${GITHUB_REF:-}" == refs/tags/* ]]; then 20 | TAG_VERSION="${GITHUB_REF#refs/tags/}" 21 | # Remove 'v' prefix if present 22 | TAG_VERSION="${TAG_VERSION#v}" 23 | echo -e "\033[1;33m📦 Detected tag: ${TAG_VERSION}\033[0m" 24 | fi 25 | 26 | # Declare variables 27 | OCI_LAYOUT_DIR="/tmp/beardist-oci-layout" 28 | OUTPUT_DIR="/tmp/beardist-output" 29 | IMAGE_NAME="ghcr.io/bearcove/beardist:${TAG_VERSION:+${TAG_VERSION}-}${ARCH_NAME}" 30 | BASE_IMAGE="ghcr.io/bearcove/build:latest-${ARCH_NAME}" 31 | 32 | # Clean up and create layout directory 33 | rm -rf "$OCI_LAYOUT_DIR" 34 | mkdir -p "$OCI_LAYOUT_DIR/usr/bin" 35 | 36 | # Copy beardist to the layout directory 37 | echo -e "\033[1;34m📦 Copying beardist binary to layout directory\033[0m" 38 | cp -v "$OUTPUT_DIR/beardist" "$OCI_LAYOUT_DIR/usr/bin/" 39 | 40 | # Reset all timestamps to epoch for reproducible builds 41 | touch -t 197001010000.00 "$OCI_LAYOUT_DIR/usr/bin/beardist" 42 | 43 | # Create the image 44 | echo -e "\033[1;36m🔄 Creating image from base\033[0m" 45 | regctl image mod "$BASE_IMAGE" --create "$IMAGE_NAME" \ 46 | --layer-add "dir=$OCI_LAYOUT_DIR" 47 | 48 | # Push the image 49 | echo -e "\033[1;32m🚀 Pushing image: \033[1;35m$IMAGE_NAME\033[0m" 50 | regctl image copy "$IMAGE_NAME"{,} 51 | 52 | # Push tagged image if we're in CI and there's a tag 53 | if [ -n "${CI:-}" ] && [ -n "${GITHUB_REF:-}" ]; then 54 | if [[ "$GITHUB_REF" == refs/tags/* ]]; then 55 | TAG=${GITHUB_REF#refs/tags/} 56 | if [[ "$TAG" == v* ]]; then 57 | TAG=${TAG#v} 58 | fi 59 | TAGGED_IMAGE_NAME="ghcr.io/bearcove/beardist:$TAG" 60 | echo -e "\033[1;32m🏷️ Tagging and pushing: \033[1;35m$TAGGED_IMAGE_NAME\033[0m" 61 | regctl image copy "$IMAGE_NAME" "$TAGGED_IMAGE_NAME" 62 | fi 63 | fi 64 | 65 | # Test the image if not in CI 66 | if [ -z "${CI:-}" ]; then 67 | echo -e "\033[1;34m🧪 Testing image locally\033[0m" 68 | docker pull "$IMAGE_NAME" 69 | docker run --rm "$IMAGE_NAME" beardist --help 70 | 71 | # Display image info 72 | echo -e "\033[1;35m📋 Image layer information:\033[0m" 73 | docker image inspect "$IMAGE_NAME" --format '{{.RootFS.Layers | len}} layers' 74 | fi 75 | -------------------------------------------------------------------------------- /src/command.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::owo_colors::OwoColorize; 2 | use eyre::Context; 3 | use indexmap::IndexMap; 4 | use log::*; 5 | use owo_colors::Style; 6 | use std::process::{Command, Stdio}; 7 | 8 | pub(crate) fn run_command( 9 | command: &str, 10 | args: &[&str], 11 | env: Option>, 12 | ) -> eyre::Result<()> { 13 | use std::process::{Command, Stdio}; 14 | use std::time::Instant; 15 | 16 | debug!( 17 | "🚀 Running command: {} {}", 18 | command.cyan(), 19 | args.join(" ").cyan() 20 | ); 21 | 22 | let mut cmd = Command::new(command); 23 | cmd.args(args) 24 | .stdin(Stdio::null()) 25 | .stdout(Stdio::inherit()) 26 | .stderr(Stdio::inherit()); 27 | 28 | if let Some(env_vars) = env { 29 | cmd.envs(env_vars); 30 | } 31 | 32 | let start_time = Instant::now(); 33 | let status = cmd.status()?; 34 | let duration = start_time.elapsed(); 35 | 36 | let status_icon = if status.success() { "✅" } else { "❌" }; 37 | let status_message = if status.success() { 38 | "executed successfully" 39 | } else { 40 | "failed" 41 | }; 42 | let status_style = if status.success() { 43 | Style::new().green() 44 | } else { 45 | Style::new().red() 46 | }; 47 | 48 | let log_message = format!( 49 | "{} Command '{}' with args '{}' {} in {:.2?} with status code {}", 50 | status_icon, 51 | command.cyan(), 52 | args.join(" ").cyan(), 53 | status_style.style(status_message), 54 | duration.cyan(), 55 | status.code().unwrap_or(-1).to_string().yellow() 56 | ); 57 | 58 | if status.success() { 59 | debug!("{}", log_message); 60 | } else { 61 | error!("{}", log_message); 62 | } 63 | if !status.success() { 64 | error!("We really needed that command to work, so we're going to bail out now. Buh-bye.",); 65 | std::process::exit(status.code().unwrap_or(-1)); 66 | } 67 | 68 | Ok(()) 69 | } 70 | 71 | pub(crate) fn get_cmd_stdout( 72 | command: &str, 73 | args: &[&str], 74 | env: Option>, 75 | ) -> eyre::Result { 76 | debug!( 77 | "🚀 Running command: {} {}", 78 | command.cyan(), 79 | args.join(" ").cyan() 80 | ); 81 | 82 | let mut cmd = Command::new(command); 83 | cmd.args(args) 84 | .stdin(Stdio::null()) 85 | .stdout(Stdio::piped()) 86 | .stderr(Stdio::piped()); 87 | 88 | if let Some(env_vars) = env { 89 | cmd.envs(env_vars); 90 | } 91 | 92 | let output = cmd 93 | .output() 94 | .wrap_err_with(|| format!("while running {} {}", command.cyan(), args.join(" ").cyan()))?; 95 | 96 | if !output.status.success() { 97 | error!( 98 | "Command failed with exit code {}", 99 | output.status.code().unwrap_or(-1) 100 | ); 101 | eprintln!("STDOUT: {}", String::from_utf8_lossy(&output.stdout)); 102 | eprintln!("STDERR: {}", String::from_utf8_lossy(&output.stderr)); 103 | std::process::exit(output.status.code().unwrap_or(-1)); 104 | } 105 | 106 | let stdout = String::from_utf8(output.stdout)?; 107 | Ok(stdout) 108 | } 109 | 110 | pub(crate) fn get_trimmed_cmd_stdout( 111 | command: &str, 112 | args: &[&str], 113 | env: Option>, 114 | ) -> eyre::Result { 115 | let stdout = get_cmd_stdout(command, args, env)?; 116 | Ok(stdout.trim().to_string()) 117 | } 118 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: check 2 | on: 3 | push: 4 | branches: [main] 5 | tags: 6 | - "*" 7 | pull_request: 8 | branches: [main] 9 | jobs: 10 | mac-build: 11 | runs-on: depot-macos-latest 12 | env: 13 | # TODO: save/restore 14 | BEARDIST_CACHE_DIR: /tmp/beardist-cache 15 | BEARDIST_ARTIFACT_NAME: aarch64-apple-darwin 16 | GH_READWRITE_TOKEN: ${{ secrets.GH_READWRITE_TOKEN }} 17 | CLICOLOR: 1 18 | CLICOLOR_FORCE: 1 19 | steps: 20 | - name: Check out repository code 21 | uses: actions/checkout@v4 22 | - name: Restore cache 23 | uses: actions/cache@v4 24 | with: 25 | path: /tmp/beardist-cache 26 | key: ${{ runner.os }}-${{ matrix.artifact }}-beardist-${{ hashFiles('**/Cargo.lock') }} 27 | restore-keys: | 28 | ${{ runner.os }}-${{ matrix.artifact }}-beardist- 29 | - name: Download beardist 30 | run: | 31 | curl -L -o beardist.tar.xz https://github.com/bearcove/beardist/releases/download/v1.0.2/aarch64-apple-darwin.tar.xz 32 | tar -xf beardist.tar.xz 33 | chmod +x beardist 34 | sudo mv beardist /usr/local/bin/ 35 | - name: Install cargo-binstall and other tools 36 | run: | 37 | curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash 38 | cargo binstall -y cargo-sweep cargo-nextest 39 | - name: Build 40 | shell: bash 41 | run: | 42 | beardist build 43 | linux-build: 44 | strategy: 45 | matrix: 46 | include: 47 | - runs-on: depot-ubuntu-24.04-16 48 | platform: linux/amd64 49 | artifact: x86_64-unknown-linux-gnu 50 | - runs-on: depot-ubuntu-24.04-arm-16 51 | artifact: aarch64-unknown-linux-gnu 52 | platform: linux/arm64 53 | runs-on: ${{ matrix.runs-on }} 54 | steps: 55 | - name: Check out repository code 56 | uses: actions/checkout@v4 57 | - name: Login to GitHub Container Registry 58 | uses: docker/login-action@v3 59 | with: 60 | registry: ghcr.io 61 | username: ${{ github.actor }} 62 | password: ${{ secrets.GH_READWRITE_TOKEN }} 63 | - name: Set up Docker Buildx 64 | uses: docker/setup-buildx-action@v3 65 | - name: Build 66 | shell: bash 67 | run: | 68 | docker buildx bake --set "*.platform=${{ matrix.platform }}" --set "*.tags=ghcr.io/bearcove/beardist:${{ matrix.platform == 'linux/arm64' && 'arm64' || 'amd64' }}-latest" 69 | multi-platform-manifest: 70 | needs: [linux-build] 71 | runs-on: depot-ubuntu-24.04-4 72 | steps: 73 | - name: Set up Docker Buildx 74 | uses: docker/setup-buildx-action@v3 75 | - name: Login to GitHub Container Registry 76 | uses: docker/login-action@v3 77 | with: 78 | registry: ghcr.io 79 | username: ${{ github.actor }} 80 | password: ${{ secrets.GH_READWRITE_TOKEN }} 81 | - name: Create and push multi-platform manifest 82 | run: | 83 | docker buildx imagetools create -t ghcr.io/bearcove/beardist:latest \ 84 | ghcr.io/bearcove/beardist:amd64-latest \ 85 | ghcr.io/bearcove/beardist:arm64-latest 86 | trigger-formula-update: 87 | needs: [mac-build, linux-build] 88 | if: startsWith(github.ref, 'refs/tags/') 89 | runs-on: depot-ubuntu-24.04-2 90 | env: 91 | GH_READWRITE_TOKEN: ${{ secrets.GH_READWRITE_TOKEN }} 92 | steps: 93 | - name: Check out repository code 94 | uses: actions/checkout@v4 95 | 96 | - name: Trigger formula update 97 | run: | 98 | curl -f -X POST \ 99 | -H "Authorization: token $GH_READWRITE_TOKEN" \ 100 | -H "Accept: application/json" \ 101 | -H "Content-Type: application/json" \ 102 | -d '{"ref": "main", "inputs": {"repository": "'$GITHUB_REPOSITORY'"}}' \ 103 | https://api.github.com/repos/bearcove/tap/actions/workflows/bump.yml/dispatches 104 | -------------------------------------------------------------------------------- /src/homebrew/tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use log::{LevelFilter, info}; 3 | use std::time::Instant; 4 | use tempfile::TempDir; 5 | 6 | #[test] 7 | fn test_generate_and_audit_formula() -> eyre::Result<()> { 8 | // Install log handler 9 | env_logger::builder().filter_level(LevelFilter::Info).init(); 10 | 11 | info!("Starting test_generate_and_audit_formula"); 12 | 13 | // Create a temporary directory 14 | let start = Instant::now(); 15 | let tmp_dir = TempDir::new()?; 16 | let formula_dir = tmp_dir.path().join("Formula"); 17 | fs_err::create_dir_all(&formula_dir)?; 18 | info!( 19 | "Created temporary directory in {}ms", 20 | start.elapsed().as_millis() 21 | ); 22 | 23 | // Generate formula content 24 | let start = Instant::now(); 25 | let homebrew_config = TapConfig { 26 | formulas: vec![Formula { 27 | repo: "testowner/foobar".to_string(), 28 | homepage: "https://example.com".to_string(), 29 | desc: "Sample generated by beardist".to_string(), 30 | license: "MIT".to_string(), 31 | bins: vec!["foobar".to_string()], 32 | deps: vec!["ffmpeg".to_string()], 33 | }], 34 | }; 35 | let dry_run = true; 36 | info!("Generated sample config"); 37 | let formula = homebrew_config.formulas.first().unwrap().clone(); 38 | let client = Arc::new(Client::new()); 39 | let github_version = "8.0.0".to_string(); 40 | let context = HomebrewContext::new(client.clone(), formula, github_version, dry_run)? 41 | .expect("Failed to create HomebrewContext"); 42 | 43 | let mac_binary = context.get_binary("https://example.com/mac")?; 44 | let linux_x86_64_binary = context.get_binary("https://example.com/linux-x86_64")?; 45 | let linux_aarch64_binary = context.get_binary("https://example.com/linux-aarch64")?; 46 | 47 | let binaries = Binaries { 48 | linux_x86_64: linux_x86_64_binary, 49 | linux_aarch64: linux_aarch64_binary, 50 | mac: mac_binary, 51 | }; 52 | let formula_content = context.generate_homebrew_formula(binaries)?; 53 | info!( 54 | "Generated formula content in {}ms", 55 | start.elapsed().as_millis() 56 | ); 57 | 58 | // Write formula to file 59 | let start = Instant::now(); 60 | let formula_path = formula_dir.join("foobar.rb"); 61 | fs_err::write(&formula_path, &formula_content)?; 62 | info!("Wrote formula to file in {}ms", start.elapsed().as_millis()); 63 | 64 | // Show formula contents 65 | info!("Showing formula contents..."); 66 | eprintln!("==============================================================="); 67 | eprintln!("{}", formula_content); 68 | eprintln!("==============================================================="); 69 | 70 | // List directory contents 71 | info!("Listing directory contents..."); 72 | let ls_output = std::process::Command::new("ls") 73 | .args(["-lhA"]) 74 | .current_dir(&tmp_dir) 75 | .output()?; 76 | info!( 77 | "ls -lhA output:\n{}", 78 | String::from_utf8_lossy(&ls_output.stdout) 79 | ); 80 | 81 | // Show directory tree 82 | info!("Showing directory tree..."); 83 | let tree_output = std::process::Command::new("tree") 84 | .args(["-ah"]) 85 | .current_dir(&tmp_dir) 86 | .output()?; 87 | info!( 88 | "tree -ah output:\n{}", 89 | String::from_utf8_lossy(&tree_output.stdout) 90 | ); 91 | 92 | // Run brew style on the formula file 93 | info!("Running brew style..."); 94 | let start = Instant::now(); 95 | let status = std::process::Command::new("brew") 96 | .arg("style") 97 | .arg(&formula_path) 98 | .status()?; 99 | let style_duration = start.elapsed(); 100 | info!( 101 | "Brew style completed in {}ms with exit status {}", 102 | style_duration.as_millis(), 103 | status.code().unwrap_or(-1) 104 | ); 105 | 106 | // Check if audit passed 107 | assert!(status.success(), "brew audit failed"); 108 | info!("Brew audit passed successfully"); 109 | 110 | // Do upload dry run 111 | context.update_formula()?; 112 | 113 | Ok(()) 114 | } 115 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | #################################################################################################### 4 | FROM ubuntu:24.04 AS base 5 | 6 | RUN export DEBIAN_FRONTEND=noninteractive \ 7 | && apt-get update \ 8 | && apt-get install --no-install-recommends -y \ 9 | bash \ 10 | bsdmainutils \ 11 | bzip2 \ 12 | ca-certificates \ 13 | coreutils \ 14 | curl \ 15 | gzip \ 16 | libcurl4 \ 17 | libpng16-16 \ 18 | libprotobuf32 \ 19 | tar \ 20 | xz-utils \ 21 | && rm -rf /var/lib/apt/lists/* 22 | 23 | RUN export DEBIAN_FRONTEND=noninteractive \ 24 | && apt-get update \ 25 | && apt-get install --no-install-recommends -y \ 26 | dav1d \ 27 | && rm -rf /var/lib/apt/lists/* 28 | 29 | RUN export DEBIAN_FRONTEND=noninteractive \ 30 | && apt-get update \ 31 | && apt-get install --no-install-recommends -y \ 32 | sqlite3 \ 33 | && rm -rf /var/lib/apt/lists/* 34 | 35 | RUN curl -LsSf https://astral.sh/uv/install.sh | sh \ 36 | && . $HOME/.local/bin/env \ 37 | && uvx fonttools 38 | 39 | ENV PATH="/root/.local/bin:${PATH}" 40 | 41 | #################################################################################################### 42 | FROM base AS build 43 | RUN export DEBIAN_FRONTEND=noninteractive \ 44 | && apt-get update \ 45 | && apt-get install --no-install-recommends -y \ 46 | clang \ 47 | && rm -rf /var/lib/apt/lists/* 48 | RUN export DEBIAN_FRONTEND=noninteractive \ 49 | && apt-get update \ 50 | && apt-get install --no-install-recommends -y \ 51 | autoconf \ 52 | autotools-dev \ 53 | cmake \ 54 | git \ 55 | jq \ 56 | libtool \ 57 | libtool-bin \ 58 | make \ 59 | nasm \ 60 | ninja-build \ 61 | openssh-client \ 62 | patch \ 63 | unzip \ 64 | pkg-config \ 65 | && rm -rf /var/lib/apt/lists/* 66 | RUN export DEBIAN_FRONTEND=noninteractive \ 67 | && apt-get update \ 68 | && apt-get install --no-install-recommends -y \ 69 | libcurl4-openssl-dev \ 70 | libdav1d-dev \ 71 | libgtest-dev \ 72 | libpng-dev \ 73 | libprotobuf-dev \ 74 | libsqlite3-dev \ 75 | && rm -rf /var/lib/apt/lists/* 76 | 77 | RUN curl -fsSL https://bun.sh/install | bash 78 | ENV PATH="/root/.bun/bin:${PATH}" 79 | RUN curl -fsSL https://deb.nodesource.com/setup_current.x | bash - \ 80 | && export DEBIAN_FRONTEND=noninteractive \ 81 | && apt-get install --no-install-recommends -y \ 82 | nodejs \ 83 | && rm -rf /var/lib/apt/lists/* 84 | RUN npm install -g pnpm 85 | 86 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain none 87 | ENV PATH="/root/.cargo/bin:${PATH}" \ 88 | CARGO_PROFILE_RELEASE_DEBUG="line-tables-only" \ 89 | CARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO="packed" \ 90 | CC=clang \ 91 | CXX=clang++ 92 | RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash \ 93 | && cargo-binstall -y cargo-sweep cargo-nextest just cargo-chef cargo-xwin 94 | 95 | RUN set -eux; \ 96 | arch=$([ "$(uname -m)" = "aarch64" ] && echo "arm64" || echo "amd64") && \ 97 | curl -L https://github.com/regclient/regclient/releases/latest/download/regctl-linux-${arch} > /usr/bin/regctl \ 98 | && chmod 755 /usr/bin/regctl \ 99 | && regctl version 100 | 101 | #################################################################################################### 102 | FROM build AS beardist-builder 103 | 104 | WORKDIR /build 105 | 106 | COPY src/ ./src/ 107 | COPY Cargo.toml . 108 | 109 | RUN rustup default stable 110 | 111 | RUN export DEBIAN_FRONTEND=noninteractive \ 112 | && apt-get update \ 113 | && apt-get install --no-install-recommends -y \ 114 | libssl-dev \ 115 | pkg-config \ 116 | && rm -rf /var/lib/apt/lists/* 117 | 118 | RUN cargo build --release 119 | 120 | #################################################################################################### 121 | FROM build AS beardist 122 | 123 | COPY --from=beardist-builder /build/target/release/beardist /usr/bin/beardist 124 | 125 | # Make the binary executable 126 | RUN chmod +x /usr/bin/beardist 127 | -------------------------------------------------------------------------------- /src/system.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | use owo_colors::OwoColorize; 3 | 4 | pub(crate) fn print_sysinfo() { 5 | info!("{}", "🖥️ System Information:".yellow()); 6 | 7 | // Detect if we're in a container 8 | let in_container = match fs_err::read_to_string("/etc/mtab") { 9 | Ok(content) => { 10 | if content.starts_with("overlay /") { 11 | info!("root fs is overlay, we're probably in a container"); 12 | true 13 | } else { 14 | false 15 | } 16 | } 17 | Err(_) => false, 18 | }; 19 | 20 | let mut sys_info = Vec::new(); 21 | 22 | if in_container { 23 | sys_info.push(format!( 24 | "{} {}", 25 | "Environment".dimmed(), 26 | "Container".cyan().underline() 27 | )); 28 | 29 | // Check CPU quota 30 | if let Ok(cpu_max) = fs_err::read_to_string("/sys/fs/cgroup/cpu.max") { 31 | let parts: Vec<&str> = cpu_max.split_whitespace().collect(); 32 | if parts.len() == 2 && parts[0] != "max" { 33 | if let (Ok(quota), Ok(period)) = (parts[0].parse::(), parts[1].parse::()) 34 | { 35 | let cpu_limit = quota / period; 36 | sys_info.push(format!( 37 | "{} {}", 38 | "CPU Quota".dimmed(), 39 | format!("{:.2} CPUs", cpu_limit).cyan().underline() 40 | )); 41 | } 42 | } 43 | } 44 | 45 | // Check Memory limit 46 | if let Ok(memory_max) = fs_err::read_to_string("/sys/fs/cgroup/memory.max") { 47 | let memory_max = memory_max.trim(); 48 | if memory_max != "max" { 49 | if let Ok(memory_limit) = memory_max.parse::() { 50 | sys_info.push(format!( 51 | "{} {}", 52 | "Memory Limit".dimmed(), 53 | crate::format_bytes(memory_limit).cyan().underline() 54 | )); 55 | } 56 | } 57 | } 58 | } 59 | 60 | // Always show host resources 61 | if let Ok(cpu_count) = sys_info::cpu_num() { 62 | sys_info.push(format!( 63 | "{} {}", 64 | "CPU Cores".dimmed(), 65 | cpu_count.to_string().cyan().underline() 66 | )); 67 | } 68 | 69 | // Hostname 70 | if let Ok(hostname) = hostname::get() { 71 | if let Some(hostname_str) = hostname.to_str() { 72 | sys_info.push(format!( 73 | "{} {}", 74 | "Hostname".dimmed(), 75 | hostname_str.cyan().underline() 76 | )); 77 | } 78 | } 79 | 80 | // Memory information 81 | if let Ok(mem_info) = sys_info::mem_info() { 82 | let used_memory = mem_info.total - mem_info.free; 83 | let usage_percentage = (used_memory as f64 / mem_info.total as f64) * 100.0; 84 | sys_info.push(format!( 85 | "{} {} out of {} total ({:.1}% used)", 86 | "Memory".dimmed(), 87 | crate::format_bytes(used_memory * 1024).cyan().underline(), 88 | crate::format_bytes(mem_info.total * 1024) 89 | .cyan() 90 | .underline(), 91 | usage_percentage 92 | )); 93 | } 94 | 95 | // Disk space 96 | if let Ok(disk_info) = sys_info::disk_info() { 97 | let used_space = disk_info.total - disk_info.free; 98 | let usage_percentage = (used_space as f64 / disk_info.total as f64) * 100.0; 99 | sys_info.push(format!( 100 | "{} {} out of {} total ({:.1}% used)", 101 | "Disk Space".dimmed(), 102 | crate::format_bytes(used_space * 1024).cyan().underline(), 103 | crate::format_bytes(disk_info.total * 1024) 104 | .cyan() 105 | .underline(), 106 | usage_percentage 107 | )); 108 | } 109 | 110 | // OS information 111 | let os_info = sys_info::os_type().unwrap_or_else(|_| "Unknown".to_string()); 112 | let os_release = sys_info::os_release().unwrap_or_else(|_| "Unknown".to_string()); 113 | sys_info.push(format!( 114 | "{} {} {}", 115 | "OS".dimmed(), 116 | os_info.cyan().underline(), 117 | os_release.cyan().underline() 118 | )); 119 | 120 | // Print system information 121 | info!("{}", sys_info.join(&" :: ".dimmed().to_string())); 122 | 123 | // Read and print /etc/os-release if it exists 124 | if let Ok(os_release_content) = fs_err::read_to_string("/etc/os-release") { 125 | let mut os_release_info = Vec::new(); 126 | for line in os_release_content.lines() { 127 | if line.starts_with("NAME=") || line.starts_with("VERSION=") || line.starts_with("ID=") 128 | { 129 | let parts: Vec<&str> = line.splitn(2, '=').collect(); 130 | if parts.len() == 2 { 131 | let value = parts[1].trim_matches('"'); 132 | os_release_info.push(format!( 133 | "{} {}", 134 | parts[0].dimmed(), 135 | value.cyan().underline() 136 | )); 137 | } 138 | } 139 | } 140 | if !os_release_info.is_empty() { 141 | info!( 142 | "{}: {}", 143 | "OS Release Details".dimmed(), 144 | os_release_info.join(&" :: ".dimmed().to_string()) 145 | ); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/k8s.rs: -------------------------------------------------------------------------------- 1 | use ignore::WalkBuilder; 2 | use log::info; 3 | use owo_colors::OwoColorize; 4 | use regex::Regex; 5 | use std::path::{Path, PathBuf}; 6 | use std::sync::Arc; 7 | 8 | use crate::github::GitHubClient; 9 | 10 | #[derive(Debug, Clone)] 11 | struct ImageOccurrence { 12 | start: usize, 13 | end: usize, 14 | current_version: String, 15 | context: String, 16 | } 17 | 18 | #[derive(Debug, Clone)] 19 | struct Manifest { 20 | path: PathBuf, 21 | occurrences: Vec, 22 | } 23 | 24 | #[derive(Debug, Clone)] 25 | struct Workspace { 26 | manifests: Vec, 27 | } 28 | 29 | fn collect_workspace(manifest_dir: &Path, image: &str) -> Result { 30 | let search_regex = Regex::new(&format!(r"image:\s*ghcr\.io/{}:v(\S+)", image)).unwrap(); 31 | let manifests = Arc::new(std::sync::Mutex::new(Vec::new())); 32 | 33 | WalkBuilder::new(manifest_dir) 34 | .types( 35 | ignore::types::TypesBuilder::new() 36 | .add_defaults() 37 | .build() 38 | .unwrap(), 39 | ) 40 | .build_parallel() 41 | .run(|| { 42 | let search_regex = search_regex.clone(); 43 | let manifests = Arc::clone(&manifests); 44 | Box::new(move |result| { 45 | if let Ok(entry) = result { 46 | let path = entry.path(); 47 | if path.extension().and_then(|s| s.to_str()) == Some("yaml") 48 | || path.extension().and_then(|s| s.to_str()) == Some("yml") 49 | { 50 | if let Ok(contents) = fs_err::read_to_string(path) { 51 | let mut occurrences = Vec::new(); 52 | for captures in search_regex.captures_iter(&contents) { 53 | let full_match = captures.get(0).unwrap(); 54 | let version = captures.get(1).unwrap(); 55 | let start = full_match.start(); 56 | let end = full_match.end(); 57 | 58 | let lines: Vec<&str> = contents.lines().collect(); 59 | let line_number = contents[..start].lines().count(); 60 | let context_start = line_number.saturating_sub(2); 61 | let context_end = (line_number + 3).min(lines.len()); 62 | let context = lines[context_start..context_end].join("\n"); 63 | 64 | occurrences.push(ImageOccurrence { 65 | start, 66 | end, 67 | current_version: version.as_str().to_string(), 68 | context, 69 | }); 70 | } 71 | if !occurrences.is_empty() { 72 | manifests.lock().unwrap().push(Manifest { 73 | path: path.to_path_buf(), 74 | occurrences, 75 | }); 76 | } 77 | } 78 | } 79 | } 80 | ignore::WalkState::Continue 81 | }) 82 | }); 83 | 84 | Ok(Workspace { 85 | manifests: manifests.lock().unwrap().clone(), 86 | }) 87 | } 88 | 89 | pub(crate) fn k8s(args: crate::DeployArgs) -> eyre::Result<()> { 90 | let manifest_dir = Path::new("manifests"); 91 | info!( 92 | "Searching for manifests in: {}", 93 | manifest_dir.display().bright_cyan() 94 | ); 95 | let workspace = collect_workspace(manifest_dir, &args.image)?; 96 | 97 | let (org, package_name) = match args.image.split_once('/') { 98 | Some((org, name)) if !org.is_empty() && !name.is_empty() => (org, name), 99 | _ => { 100 | return Err(eyre::eyre!("Invalid image format. Expected 'org/name'.")); 101 | } 102 | }; 103 | 104 | info!("YAML files containing '{}' are:", args.image.bright_cyan()); 105 | for manifest in &workspace.manifests { 106 | info!("File: {}", manifest.path.display().bright_green()); 107 | for occurrence in &manifest.occurrences { 108 | info!( 109 | " Version {} at positions {} to {}", 110 | occurrence.current_version.bright_yellow(), 111 | occurrence.start, 112 | occurrence.end 113 | ); 114 | info!(" Context:"); 115 | for (i, line) in occurrence.context.lines().enumerate() { 116 | let prefix = ">>> ".bright_cyan(); 117 | if i == 1 { 118 | info!(" {}{}", prefix, line.bright_yellow()); 119 | } else { 120 | info!(" {}{}", prefix, line); 121 | } 122 | } 123 | info!(""); 124 | } 125 | } 126 | 127 | info!("Initializing GitHub client..."); 128 | let github_client = GitHubClient::from_env()?; 129 | 130 | info!("Checking for new versions..."); 131 | let mut spinner = ['|', '/', '-', '\\'].iter().cycle(); 132 | let mut last_check_time = std::time::Instant::now(); 133 | let new_version = loop { 134 | let latest_version = github_client.get_latest_container_version(org, package_name)?; 135 | 136 | if let Some(version) = latest_version { 137 | // Skip versions that end with -amd64 or -arm64 138 | if version.ends_with("-amd64") || version.ends_with("-arm64") { 139 | info!("Skipping architecture-specific version: {}", version); 140 | std::thread::sleep(std::time::Duration::from_secs(1)); 141 | last_check_time = std::time::Instant::now(); 142 | continue; 143 | } 144 | 145 | let is_new_version = workspace.manifests.iter().any(|manifest| { 146 | manifest 147 | .occurrences 148 | .iter() 149 | .any(|occurrence| occurrence.current_version != version) 150 | }); 151 | 152 | if is_new_version { 153 | eprintln!("\r\x1B[KNew version detected: {}", version.bright_green()); 154 | break version; 155 | } 156 | } 157 | 158 | loop { 159 | std::thread::sleep(std::time::Duration::from_millis(100)); 160 | let elapsed = last_check_time.elapsed(); 161 | eprint!( 162 | "\r\x1B[K{} Checking for new versions... Last checked: {}", 163 | spinner.next().unwrap().bright_cyan(), 164 | format!( 165 | "{:02}:{:02} ago", 166 | elapsed.as_secs() / 60, 167 | elapsed.as_secs() % 60 168 | ) 169 | .bright_yellow() 170 | ); 171 | if elapsed >= std::time::Duration::from_secs(2) { 172 | eprint!( 173 | "\r \r" 174 | ); 175 | break; 176 | } 177 | } 178 | last_check_time = std::time::Instant::now(); // Update last_check_time after each check 179 | }; 180 | 181 | info!("Updating manifests..."); 182 | 183 | for manifest in &workspace.manifests { 184 | let mut contents = fs_err::read_to_string(&manifest.path)?; 185 | for occurrence in &manifest.occurrences { 186 | let before = &contents[..occurrence.start]; 187 | let after = &contents[occurrence.end..]; 188 | let new_image_line = format!("image: ghcr.io/{}:v{}", args.image, new_version); 189 | contents = format!("{}{}{}", before, new_image_line, after); 190 | } 191 | fs_err::write(&manifest.path, contents)?; 192 | info!("Updated {}", manifest.path.display().bright_green()); 193 | } 194 | 195 | info!("Deploying manifests..."); 196 | let mut deploy_cmd = std::process::Command::new("./deploy"); 197 | 198 | // Add all updated manifest paths as arguments 199 | for manifest in &workspace.manifests { 200 | deploy_cmd.arg(manifest.path.as_os_str()); 201 | } 202 | 203 | deploy_cmd 204 | .stdin(std::process::Stdio::inherit()) 205 | .stdout(std::process::Stdio::inherit()) 206 | .stderr(std::process::Stdio::inherit()) 207 | .spawn()? 208 | .wait()?; 209 | 210 | info!("Deployment process completed successfully."); 211 | Ok(()) 212 | } 213 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | https://www.apache.org/licenses/LICENSE-2.0 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | https://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /src/github.rs: -------------------------------------------------------------------------------- 1 | use log::{debug, info}; 2 | use owo_colors::OwoColorize; 3 | use reqwest::blocking::Client; 4 | use semver::Version; 5 | use serde_json::Value; 6 | 7 | use crate::USER_AGENT; 8 | 9 | pub struct GitHubClient { 10 | client: Client, 11 | server_url: String, 12 | token: String, 13 | } 14 | 15 | impl GitHubClient { 16 | pub fn new(server_url: String, token: String) -> Self { 17 | Self { 18 | client: Client::new(), 19 | server_url, 20 | token, 21 | } 22 | } 23 | 24 | pub fn from_env() -> eyre::Result { 25 | let server_url = std::env::var("GITHUB_SERVER_URL") 26 | .unwrap_or_else(|_| "https://api.github.com".to_string()); 27 | let token = std::env::var("GITHUB_TOKEN") 28 | .map_err(|_| eyre::eyre!("GITHUB_TOKEN environment variable not set"))?; 29 | Ok(Self::new(server_url, token)) 30 | } 31 | 32 | /// Get the latest version tag from a GitHub Container Registry (ghcr.io) package 33 | pub fn get_latest_container_version( 34 | &self, 35 | org: &str, 36 | package_name: &str, 37 | ) -> eyre::Result> { 38 | let url = format!( 39 | "{}/orgs/{}/packages/container/{}/versions", 40 | self.server_url, org, package_name 41 | ); 42 | 43 | info!( 44 | "Fetching latest container version for '{}' from '{}'", 45 | package_name, url 46 | ); 47 | 48 | let start_time = std::time::Instant::now(); 49 | let response = self 50 | .client 51 | .get(&url) 52 | .header("Authorization", format!("token {}", self.token)) 53 | .header("Accept", "application/vnd.github+json") 54 | .header("X-GitHub-Api-Version", "2022-11-28") 55 | .header("User-Agent", USER_AGENT) 56 | .send()?; 57 | 58 | let status = response.status(); 59 | let elapsed = start_time.elapsed(); 60 | info!( 61 | "Request completed in {}ms with status {}", 62 | elapsed.as_millis(), 63 | status 64 | ); 65 | 66 | if status != 200 { 67 | let body = response.text()?; 68 | debug!("Error response: {}", body); 69 | return Err(eyre::eyre!( 70 | "Failed to get container versions: HTTP status {status}" 71 | )); 72 | } 73 | 74 | let body = response.text()?; 75 | debug!("Response body size: {} bytes", body.len()); 76 | 77 | let versions: Vec = serde_json::from_str(&body)?; 78 | info!("Received {} versions in response", versions.len()); 79 | 80 | let valid_versions: Vec = versions 81 | .iter() 82 | .filter_map(|version| { 83 | // Look for metadata tags with semver format 84 | version["metadata"]["container"]["tags"] 85 | .as_array() 86 | .and_then(|tags| { 87 | tags.iter() 88 | .filter_map(|tag| tag.as_str()) 89 | .filter_map(|tag| Version::parse(tag.trim_start_matches('v')).ok()) 90 | .max() 91 | }) 92 | }) 93 | .collect(); 94 | 95 | info!("Found {} valid semver tags", valid_versions.len()); 96 | 97 | if valid_versions.is_empty() { 98 | info!("No valid versioned tags found for container"); 99 | return Ok(None); 100 | } 101 | 102 | let latest_version = valid_versions.into_iter().max().unwrap(); 103 | info!("Latest container version found: {}", latest_version); 104 | Ok(Some(latest_version.to_string())) 105 | } 106 | 107 | /// Get the latest release version from a GitHub repository 108 | pub fn get_latest_release_version( 109 | &self, 110 | owner: &str, 111 | repo: &str, 112 | ) -> eyre::Result> { 113 | let url = format!( 114 | "{}/repos/{}/{}/releases/latest", 115 | self.server_url, owner, repo 116 | ); 117 | 118 | info!( 119 | "Fetching latest release for repository '{}/{}' from '{}'", 120 | owner, repo, url 121 | ); 122 | 123 | let start_time = std::time::Instant::now(); 124 | let response = self 125 | .client 126 | .get(&url) 127 | .header("Authorization", format!("token {}", self.token)) 128 | .header("Accept", "application/vnd.github+json") 129 | .header("X-GitHub-Api-Version", "2022-11-28") 130 | .header("User-Agent", USER_AGENT) 131 | .send()?; 132 | 133 | let status = response.status(); 134 | let elapsed = start_time.elapsed(); 135 | info!( 136 | "Request completed in {}ms with status {}", 137 | elapsed.as_millis(), 138 | status 139 | ); 140 | 141 | // 404 means no releases yet 142 | if status == 404 { 143 | info!("No releases found for repository '{}/{}'", owner, repo); 144 | return Ok(None); 145 | } 146 | 147 | if status != 200 { 148 | let body = response.text()?; 149 | debug!("Error response: {}", body); 150 | return Err(eyre::eyre!( 151 | "Failed to get latest release: HTTP status {status}" 152 | )); 153 | } 154 | 155 | let body = response.text()?; 156 | debug!("Response body size: {} bytes", body.len()); 157 | 158 | let release: Value = serde_json::from_str(&body)?; 159 | 160 | if let Some(tag_name) = release["tag_name"].as_str() { 161 | let version_str = tag_name.trim_start_matches('v'); 162 | info!("Latest release tag: {}", tag_name); 163 | 164 | // Try to parse as semver, but return the original tag if not valid 165 | match Version::parse(version_str) { 166 | Ok(version) => Ok(Some(version.to_string())), 167 | Err(_) => Ok(Some(version_str.to_string())), 168 | } 169 | } else { 170 | info!("Release found but no tag_name present"); 171 | Ok(None) 172 | } 173 | } 174 | 175 | /// Create a release if it doesn't exist, and return the release ID 176 | pub fn create_release(&self, org: &str, name: &str, tag: &str) -> eyre::Result { 177 | let github_api_url = format!( 178 | "{}/repos/{}/{}/releases/tags/{}", 179 | self.server_url.replace("github.com", "api.github.com"), 180 | org, 181 | name, 182 | tag 183 | ); 184 | 185 | info!("Checking if release exists at {}...", github_api_url); 186 | 187 | let release_response = self 188 | .client 189 | .get(&github_api_url) 190 | .header("Accept", "application/vnd.github+json") 191 | .header("Authorization", format!("token {}", self.token)) 192 | .header("X-GitHub-Api-Version", "2022-11-28") 193 | .header("User-Agent", USER_AGENT) 194 | .send()?; 195 | 196 | let release_id = if !release_response.status().is_success() { 197 | info!("Release doesn't exist, creating one..."); 198 | 199 | let release_create_url = format!( 200 | "{}/repos/{}/{}/releases", 201 | self.server_url.replace("github.com", "api.github.com"), 202 | org, 203 | name 204 | ); 205 | 206 | let release_create_body = serde_json::json!({ 207 | "tag_name": tag, 208 | "name": tag, 209 | "draft": false, 210 | "prerelease": false 211 | }); 212 | 213 | let create_response = self 214 | .client 215 | .post(&release_create_url) 216 | .header("Accept", "application/vnd.github+json") 217 | .header("Authorization", format!("token {}", self.token)) 218 | .header("X-GitHub-Api-Version", "2022-11-28") 219 | .header("User-Agent", USER_AGENT) 220 | .json(&release_create_body) 221 | .send()?; 222 | 223 | if !create_response.status().is_success() { 224 | return Err(eyre::eyre!( 225 | "Failed to create release: {}", 226 | create_response.text()? 227 | )); 228 | } 229 | 230 | let release_data: serde_json::Value = create_response.json()?; 231 | release_data["id"] 232 | .as_u64() 233 | .ok_or_else(|| eyre::eyre!("Invalid release ID"))? 234 | } else { 235 | let release_data: serde_json::Value = release_response.json()?; 236 | release_data["id"] 237 | .as_u64() 238 | .ok_or_else(|| eyre::eyre!("Invalid release ID"))? 239 | }; 240 | 241 | Ok(release_id) 242 | } 243 | 244 | /// Upload an artifact to a GitHub release as a release asset 245 | pub fn upload_artifact( 246 | &self, 247 | org: &str, 248 | name: &str, 249 | release_id: u64, 250 | package_file_name: &str, 251 | file_content: &[u8], 252 | ) -> eyre::Result<()> { 253 | use log::warn; 254 | 255 | // Assemble the correct uploads.github.com asset endpoint 256 | let upload_url = format!( 257 | "{}/repos/{}/{}/releases/{}/assets?name={}", 258 | self.server_url.replace("github.com", "uploads.github.com"), 259 | org, 260 | name, 261 | release_id, 262 | package_file_name 263 | ); 264 | 265 | info!( 266 | "📤 Uploading package to {} ({})...", 267 | "GitHub".yellow(), 268 | upload_url.cyan() 269 | ); 270 | let upload_start = std::time::Instant::now(); 271 | 272 | // Retry logic for upload attempts 273 | const MAX_RETRIES: usize = 3; 274 | const BASE_RETRY_DELAY_MS: u64 = 2000; // 2 seconds 275 | 276 | let mut attempt = 0; 277 | let mut last_error = None; 278 | 279 | while attempt < MAX_RETRIES { 280 | attempt += 1; 281 | 282 | if attempt > 1 { 283 | info!("🔄 Retry attempt {} of {}...", attempt, MAX_RETRIES); 284 | let jitter = rand::random::() % 1000; // Random jitter between 0-999ms 285 | let delay = BASE_RETRY_DELAY_MS + jitter; 286 | std::thread::sleep(std::time::Duration::from_millis(delay)); 287 | } 288 | 289 | match self 290 | .client 291 | .post(&upload_url) 292 | .header("Accept", "application/vnd.github+json") 293 | .header("Authorization", format!("token {}", self.token)) 294 | .header("X-GitHub-Api-Version", "2022-11-28") 295 | .header("User-Agent", USER_AGENT) 296 | .header("Content-Type", "application/octet-stream") 297 | .body(file_content.to_vec()) 298 | .send() 299 | { 300 | Ok(response) => { 301 | info!( 302 | "🔢 Response status code: {}", 303 | format!("{}", response.status()).blue() 304 | ); 305 | 306 | let status = response.status(); 307 | let response_text = response.text()?; 308 | info!("{}", "----------------------------------------".yellow()); 309 | info!("📄 {}", "Response Data:".yellow()); 310 | info!("{}", "----------------------------------------".yellow()); 311 | info!("{}", response_text); 312 | info!("{}", "----------------------------------------".yellow()); 313 | 314 | // If successful or not a 5xx error, break out of retry loop 315 | if status.is_success() || !status.is_server_error() { 316 | if !status.is_success() { 317 | return Err(eyre::eyre!( 318 | "❌ Upload failed with status code: {}", 319 | status 320 | )); 321 | } 322 | 323 | let upload_time = upload_start.elapsed().as_millis() as u64; 324 | info!( 325 | "✅ Package upload completed ({})", 326 | format!("{}ms", upload_time).green() 327 | ); 328 | return Ok(()); 329 | } 330 | 331 | // If we get here, it's a 5xx error and we'll retry 332 | last_error = Some(eyre::eyre!("Server error with status code: {}", status)); 333 | } 334 | Err(e) => { 335 | last_error = Some(eyre::eyre!("Request error: {}", e)); 336 | } 337 | } 338 | 339 | warn!("📶 Upload attempt {} failed, retrying...", attempt); 340 | } 341 | 342 | // If we get here, all retries failed 343 | Err(last_error 344 | .unwrap_or_else(|| eyre::eyre!("Upload failed after {} attempts", MAX_RETRIES))) 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /src/target_spec.rs: -------------------------------------------------------------------------------- 1 | use eyre::Context; 2 | use log::info; 3 | use owo_colors::OwoColorize; 4 | use serde::Deserialize; 5 | 6 | #[derive(Debug, Deserialize)] 7 | #[allow(dead_code)] 8 | pub(crate) struct TargetSpec { 9 | /// Examples: true, None 10 | #[serde(rename = "abi-return-struct-as-int")] 11 | pub(crate) abi_return_struct_as_int: Option, 12 | /// Examples: "aarch64", "x86_64" 13 | pub(crate) arch: String, 14 | /// Examples: "darwin", None 15 | #[serde(rename = "archive-format")] 16 | pub(crate) archive_format: Option, 17 | /// Examples: "apple-m1", "x86-64" 18 | pub(crate) cpu: Option, 19 | /// Examples: "false" 20 | #[serde(rename = "crt-objects-fallback")] 21 | pub(crate) crt_objects_fallback: Option, 22 | /// Examples: true, None 23 | #[serde(rename = "crt-static-respected")] 24 | pub(crate) crt_static_respected: Option, 25 | /// Examples: "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-n32:64-S128-Fn32", "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128" 26 | #[serde(rename = "data-layout")] 27 | pub(crate) data_layout: String, 28 | /// Examples: "dwarf-dsym", None 29 | #[serde(rename = "debuginfo-kind")] 30 | pub(crate) debuginfo_kind: Option, 31 | /// Examples: ".dylib", None 32 | #[serde(rename = "dll-suffix", default = "default_dll_suffix")] 33 | pub(crate) dll_suffix: String, 34 | /// Examples: true, None 35 | #[serde(rename = "dynamic-linking")] 36 | pub(crate) dynamic_linking: Option, 37 | /// Examples: "gnu", None 38 | pub(crate) env: Option, 39 | /// Examples: false, None 40 | #[serde(rename = "eh-frame-header")] 41 | pub(crate) eh_frame_header: Option, 42 | /// Examples: false, None 43 | #[serde(rename = "emit-debug-gdb-scripts")] 44 | pub(crate) emit_debug_gdb_scripts: Option, 45 | /// Examples: "non-leaf", None 46 | #[serde(rename = "frame-pointer")] 47 | pub(crate) frame_pointer: Option, 48 | /// Examples: false, None 49 | #[serde(rename = "function-sections")] 50 | pub(crate) function_sections: Option, 51 | /// Examples: true, None 52 | #[serde(rename = "has-rpath")] 53 | pub(crate) has_rpath: Option, 54 | /// Examples: true, None 55 | #[serde(rename = "has-thread-local")] 56 | pub(crate) has_thread_local: Option, 57 | /// Examples: true, None 58 | #[serde(rename = "is-like-osx")] 59 | pub(crate) is_like_osx: Option, 60 | /// Examples: ["ZERO_AR_DATE=1"], None 61 | #[serde(rename = "link-env")] 62 | pub(crate) link_env: Option>, 63 | /// Examples: ["IPHONEOS_DEPLOYMENT_TARGET", "TVOS_DEPLOYMENT_TARGET", "XROS_DEPLOYMENT_TARGET"], None 64 | #[serde(rename = "link-env-remove")] 65 | pub(crate) link_env_remove: Option>, 66 | /// Examples: "darwin-cc", "gnu-cc" 67 | #[serde(rename = "linker-flavor")] 68 | pub(crate) linker_flavor: String, 69 | /// Examples: false, None 70 | #[serde(rename = "linker-is-gnu")] 71 | pub(crate) linker_is_gnu: Option, 72 | /// Examples: "darwin", None 73 | #[serde(rename = "lld-flavor")] 74 | pub(crate) lld_flavor: Option, 75 | /// Examples: "hard", None 76 | #[serde(rename = "llvm-floatabi")] 77 | pub(crate) llvm_floatabi: Option, 78 | /// Examples: "arm64-apple-macosx", "x86_64-unknown-linux-gnu" 79 | #[serde(rename = "llvm-target")] 80 | pub(crate) llvm_target: String, 81 | /// Examples: 128, 64 82 | #[serde(rename = "max-atomic-width")] 83 | pub(crate) max_atomic_width: Option, 84 | pub(crate) metadata: Metadata, 85 | /// Examples: "macos", "linux" 86 | pub(crate) os: String, 87 | /// Examples: false, None 88 | #[serde(rename = "plt-by-default")] 89 | pub(crate) plt_by_default: Option, 90 | /// Examples: true, None 91 | #[serde(rename = "position-independent-executables")] 92 | pub(crate) position_independent_executables: Option, 93 | #[serde(rename = "pre-link-args")] 94 | pub(crate) pre_link_args: Option, 95 | /// Examples: "full", None 96 | #[serde(rename = "relro-level")] 97 | pub(crate) relro_level: Option, 98 | /// Examples: "packed", None 99 | #[serde(rename = "split-debuginfo")] 100 | pub(crate) split_debuginfo: Option, 101 | #[serde(rename = "stack-probes")] 102 | pub(crate) stack_probes: Option, 103 | /// Examples: true, None 104 | #[serde(rename = "static-position-independent-executables")] 105 | pub(crate) static_position_independent_executables: Option, 106 | /// Examples: ["address", "thread", "cfi"], ["address", "leak", "memory", "thread", "cfi", "kcfi", "safestack", "dataflow"] 107 | #[serde(rename = "supported-sanitizers")] 108 | pub(crate) supported_sanitizers: Option>, 109 | /// Examples: ["packed", "unpacked", "off"] 110 | #[serde(rename = "supported-split-debuginfo")] 111 | pub(crate) supported_split_debuginfo: Option>, 112 | /// Examples: true, None 113 | #[serde(rename = "supports-xray")] 114 | pub(crate) supports_xray: Option, 115 | /// Examples: ["unix"] 116 | #[serde(rename = "target-family")] 117 | pub(crate) target_family: Option>, 118 | /// Examples: "\u0001mcount", None 119 | #[serde(rename = "target-mcount")] 120 | pub(crate) target_mcount: Option, 121 | /// Examples: "64" 122 | #[serde(rename = "target-pointer-width")] 123 | pub(crate) target_pointer_width: String, 124 | /// Examples: "apple", None 125 | pub(crate) vendor: Option, 126 | } 127 | 128 | #[derive(Debug, Deserialize)] 129 | #[allow(dead_code)] 130 | pub(crate) struct Metadata { 131 | /// Examples: "ARM64 Apple macOS (11.0+, Big Sur+)", "64-bit Linux (kernel 3.2+, glibc 2.17+)" 132 | pub(crate) description: String, 133 | /// Examples: true 134 | pub(crate) host_tools: bool, 135 | /// Examples: true 136 | pub(crate) std: bool, 137 | /// Examples: 1 138 | pub(crate) tier: u8, 139 | } 140 | 141 | #[derive(Debug, Deserialize)] 142 | #[allow(dead_code)] 143 | pub(crate) struct StackProbes { 144 | /// Examples: "inline" 145 | pub(crate) kind: String, 146 | } 147 | 148 | #[derive(Debug, Deserialize)] 149 | #[allow(dead_code)] 150 | pub(crate) struct PreLinkArgs { 151 | /// Examples: ["-m64"] 152 | #[serde(rename = "gnu-cc")] 153 | pub(crate) gnu_cc: Option>, 154 | /// Examples: ["-m64"] 155 | #[serde(rename = "gnu-lld-cc")] 156 | pub(crate) gnu_lld_cc: Option>, 157 | } 158 | 159 | impl TargetSpec { 160 | pub(crate) fn from_json(json_output: &str) -> eyre::Result { 161 | serde_json::from_str(json_output).wrap_err("could not deserialize target spec from JSON payload. '--print target-spec-json' is an unstable Rust flag for a reason, y'know.") 162 | } 163 | 164 | pub(crate) fn full_name(&self) -> String { 165 | let os = if self.os == "macos" { 166 | "darwin" 167 | } else { 168 | self.os.as_str() 169 | }; 170 | let arch = self.arch.as_str(); 171 | let vendor = self.vendor.as_deref().unwrap_or("unknown"); 172 | let env = self.env.as_deref().unwrap_or(""); 173 | 174 | if !env.is_empty() { 175 | format!("{}-{}-{}-{}", arch, vendor, os, env) 176 | } else { 177 | format!("{}-{}-{}", arch, vendor, os) 178 | } 179 | } 180 | 181 | pub(crate) fn print_info(&self) { 182 | // Print relevant information from TargetSpec 183 | info!("{}", "🎯 Target Specification:".yellow()); 184 | let mut target_info = vec![ 185 | format!( 186 | "{} {}", 187 | "Architecture".dimmed(), 188 | self.arch.cyan().underline() 189 | ), 190 | format!("{} {}", "OS".dimmed(), self.os.cyan().underline()), 191 | ]; 192 | if let Some(vendor) = &self.vendor { 193 | target_info.push(format!( 194 | "{} {}", 195 | "Vendor".dimmed(), 196 | vendor.cyan().underline() 197 | )); 198 | } 199 | if let Some(env) = &self.env { 200 | target_info.push(format!( 201 | "{} {}", 202 | "Environment".dimmed(), 203 | env.cyan().underline() 204 | )); 205 | } 206 | // Comment: Basic target information 207 | info!("{}", target_info.join(&" :: ".dimmed().to_string())); 208 | 209 | let target_info = [ 210 | format!("{} {:?}", "CPU".dimmed(), self.cpu.cyan().underline()), 211 | format!( 212 | "{} {}", 213 | "Pointer Width".dimmed(), 214 | self.target_pointer_width.cyan().underline() 215 | ), 216 | format!( 217 | "{} {}", 218 | "Dynamic Linking".dimmed(), 219 | self.dynamic_linking 220 | .map(|b| b.to_string()) 221 | .unwrap_or_else(|| "???".to_string()) 222 | .cyan() 223 | .underline() 224 | ), 225 | format!( 226 | "{} {}", 227 | "DLL Suffix".dimmed(), 228 | self.dll_suffix.cyan().underline() 229 | ), 230 | format!( 231 | "{} {}", 232 | "Max Atomic Width".dimmed(), 233 | self.max_atomic_width 234 | .map(|x| x.to_string()) 235 | .unwrap_or_else(|| "???".to_string()) 236 | .cyan() 237 | .underline() 238 | ), 239 | ]; 240 | // Comment: Detailed target information 241 | info!("{}", target_info.join(&" :: ".dimmed().to_string())); 242 | 243 | let metadata_info = [ 244 | format!("{}:", "Metadata".dimmed()), 245 | format!( 246 | "{} {}", 247 | "Description".dimmed(), 248 | self.metadata.description.cyan().underline() 249 | ), 250 | format!( 251 | "{} {}", 252 | "Host Tools".dimmed(), 253 | self.metadata.host_tools.to_string().cyan().underline() 254 | ), 255 | format!( 256 | "{} {}", 257 | "Standard Library".dimmed(), 258 | self.metadata.std.to_string().cyan().underline() 259 | ), 260 | format!( 261 | "{} {}", 262 | "Tier".dimmed(), 263 | self.metadata.tier.to_string().cyan().underline() 264 | ), 265 | ]; 266 | // Comment: Metadata information 267 | info!("{}", metadata_info.join(&" :: ".dimmed().to_string())); 268 | } 269 | } 270 | 271 | fn default_dll_suffix() -> String { 272 | ".so".into() 273 | } 274 | 275 | /* Sample outputs: 276 | 277 | ## arm64 macOS 278 | 279 | { 280 | "abi-return-struct-as-int": true, 281 | "arch": "aarch64", 282 | "archive-format": "darwin", 283 | "cpu": "apple-m1", 284 | "crt-objects-fallback": "false", 285 | "data-layout": "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-n32:64-S128-Fn32", 286 | "debuginfo-kind": "dwarf-dsym", 287 | "dll-suffix": ".dylib", 288 | "dynamic-linking": true, 289 | "eh-frame-header": false, 290 | "emit-debug-gdb-scripts": false, 291 | "frame-pointer": "non-leaf", 292 | "function-sections": false, 293 | "has-rpath": true, 294 | "has-thread-local": true, 295 | "is-like-osx": true, 296 | "link-env": [ 297 | "ZERO_AR_DATE=1" 298 | ], 299 | "link-env-remove": [ 300 | "IPHONEOS_DEPLOYMENT_TARGET", 301 | "TVOS_DEPLOYMENT_TARGET", 302 | "XROS_DEPLOYMENT_TARGET" 303 | ], 304 | "linker-flavor": "darwin-cc", 305 | "linker-is-gnu": false, 306 | "lld-flavor": "darwin", 307 | "llvm-floatabi": "hard", 308 | "llvm-target": "arm64-apple-macosx", 309 | "max-atomic-width": 128, 310 | "metadata": { 311 | "description": "ARM64 Apple macOS (11.0+, Big Sur+)", 312 | "host_tools": true, 313 | "std": true, 314 | "tier": 1 315 | }, 316 | "os": "macos", 317 | "split-debuginfo": "packed", 318 | "stack-probes": { 319 | "kind": "inline" 320 | }, 321 | "supported-sanitizers": [ 322 | "address", 323 | "thread", 324 | "cfi" 325 | ], 326 | "supported-split-debuginfo": [ 327 | "packed", 328 | "unpacked", 329 | "off" 330 | ], 331 | "target-family": [ 332 | "unix" 333 | ], 334 | "target-mcount": "\u0001mcount", 335 | "target-pointer-width": "64", 336 | "vendor": "apple" 337 | } 338 | 339 | --- 340 | 341 | ## x86_64 linux: 342 | 343 | { 344 | "arch": "x86_64", 345 | "cpu": "x86-64", 346 | "crt-objects-fallback": "false", 347 | "crt-static-respected": true, 348 | "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", 349 | "dynamic-linking": true, 350 | "env": "gnu", 351 | "has-rpath": true, 352 | "has-thread-local": true, 353 | "linker-flavor": "gnu-cc", 354 | "llvm-target": "x86_64-unknown-linux-gnu", 355 | "max-atomic-width": 64, 356 | "metadata": { 357 | "description": "64-bit Linux (kernel 3.2+, glibc 2.17+)", 358 | "host_tools": true, 359 | "std": true, 360 | "tier": 1 361 | }, 362 | "os": "linux", 363 | "plt-by-default": false, 364 | "position-independent-executables": true, 365 | "pre-link-args": { 366 | "gnu-cc": [ 367 | "-m64" 368 | ], 369 | "gnu-lld-cc": [ 370 | "-m64" 371 | ] 372 | }, 373 | "relro-level": "full", 374 | "stack-probes": { 375 | "kind": "inline" 376 | }, 377 | "static-position-independent-executables": true, 378 | "supported-sanitizers": [ 379 | "address", 380 | "leak", 381 | "memory", 382 | "thread", 383 | "cfi", 384 | "kcfi", 385 | "safestack", 386 | "dataflow" 387 | ], 388 | "supported-split-debuginfo": [ 389 | "packed", 390 | "unpacked", 391 | "off" 392 | ], 393 | "supports-xray": true, 394 | "target-family": [ 395 | "unix" 396 | ], 397 | "target-pointer-width": "64" 398 | } 399 | 400 | ## x86_64 linux (docker on orbstack) 401 | 402 | { 403 | "arch": "aarch64", 404 | "crt-objects-fallback": "false", 405 | "crt-static-respected": true, 406 | "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32", 407 | "dynamic-linking": true, 408 | "env": "gnu", 409 | "features": "+v8a,+outline-atomics", 410 | "has-rpath": true, 411 | "has-thread-local": true, 412 | "linker-flavor": "gnu-cc", 413 | "llvm-target": "aarch64-unknown-linux-gnu", 414 | "max-atomic-width": 128, 415 | "metadata": { 416 | "description": "ARM64 Linux (kernel 4.1, glibc 2.17+)", 417 | "host_tools": true, 418 | "std": true, 419 | "tier": 1 420 | }, 421 | "os": "linux", 422 | "position-independent-executables": true, 423 | "relro-level": "full", 424 | "stack-probes": { 425 | "kind": "inline" 426 | }, 427 | "supported-sanitizers": [ 428 | "address", 429 | "leak", 430 | "memory", 431 | "thread", 432 | "hwaddress", 433 | "cfi", 434 | "memtag", 435 | "kcfi" 436 | ], 437 | "supported-split-debuginfo": [ 438 | "packed", 439 | "unpacked", 440 | "off" 441 | ], 442 | "supports-xray": true, 443 | "target-family": [ 444 | "unix" 445 | ], 446 | "target-mcount": "\u0001_mcount", 447 | "target-pointer-width": "64" 448 | } 449 | 450 | */ 451 | -------------------------------------------------------------------------------- /src/homebrew.rs: -------------------------------------------------------------------------------- 1 | use camino::Utf8PathBuf; 2 | use color_eyre::eyre; 3 | use convert_case::{Case, Casing}; 4 | use eyre::Context; 5 | use log::*; 6 | use owo_colors::OwoColorize; 7 | use reqwest::blocking::Client; 8 | use std::{path::PathBuf, sync::Arc}; 9 | use url::Url; 10 | 11 | use crate::{Indented, command::get_trimmed_cmd_stdout, github::GitHubClient, run_command}; 12 | 13 | use serde::Deserialize; 14 | 15 | #[cfg(test)] 16 | mod tests; 17 | 18 | #[derive(Deserialize, Debug, Clone)] 19 | struct TapConfig { 20 | formulas: Vec, 21 | } 22 | 23 | #[derive(Deserialize, Debug, Clone)] 24 | struct Formula { 25 | repo: String, 26 | homepage: String, 27 | desc: String, 28 | license: String, 29 | bins: Vec, 30 | 31 | #[serde(default)] 32 | deps: Vec, 33 | } 34 | 35 | struct Binaries { 36 | mac: Binary, 37 | linux_x86_64: Binary, 38 | linux_aarch64: Binary, 39 | } 40 | 41 | impl Formula { 42 | fn org(&self) -> &str { 43 | self.repo.split('/').next().unwrap() 44 | } 45 | 46 | fn name(&self) -> &str { 47 | self.repo.split('/').nth(1).unwrap() 48 | } 49 | 50 | /// Where the formula is written on disk 51 | fn disk_path(&self) -> Utf8PathBuf { 52 | Utf8PathBuf::from(format!("Formula/{}.rb", self.name())) 53 | } 54 | 55 | fn github_version( 56 | &self, 57 | _config: &TapConfig, 58 | github_token: &str, 59 | ) -> eyre::Result> { 60 | let github_client = GitHubClient::new( 61 | "https://api.github.com".to_string(), 62 | github_token.to_string(), 63 | ); 64 | github_client.get_latest_release_version(self.org(), self.name()) 65 | } 66 | 67 | fn formula_version(&self) -> Option { 68 | let disk_path = self.disk_path(); 69 | if !disk_path.exists() { 70 | return None; 71 | } 72 | let content = fs_err::read_to_string(&disk_path).unwrap(); 73 | let version_line = content 74 | .lines() 75 | .find(|line| line.trim().starts_with("version"))?; 76 | let version = version_line.split('"').nth(1)?; 77 | Some(version.to_string()) 78 | } 79 | } 80 | 81 | struct Binary { 82 | url: String, 83 | sha256: String, 84 | } 85 | 86 | #[derive(Clone)] 87 | struct HomebrewContext { 88 | client: Arc, 89 | dry_run: bool, 90 | formula: Formula, 91 | new_version: String, 92 | } 93 | 94 | impl HomebrewContext { 95 | fn new( 96 | client: Arc, 97 | formula: Formula, 98 | github_version: String, 99 | dry_run: bool, 100 | ) -> eyre::Result> { 101 | let formula_version = formula.formula_version(); 102 | if let Some(formula_version) = formula_version { 103 | if formula_version == github_version { 104 | info!( 105 | "Formula version {} is already up-to-date with GitHub version {}", 106 | formula_version.bright_green(), 107 | github_version.bright_green() 108 | ); 109 | return Ok(None); 110 | } 111 | } 112 | 113 | Ok(Some(Self { 114 | client, 115 | dry_run, 116 | formula, 117 | new_version: github_version, 118 | })) 119 | } 120 | 121 | fn get_binary(&self, url: &str) -> eyre::Result { 122 | Ok(Binary { 123 | url: url.to_string(), 124 | sha256: self.fetch_and_hash(url)?, 125 | }) 126 | } 127 | 128 | fn package_artifact_url(&self, arch: &str) -> String { 129 | format!( 130 | "https://github.com/{}/{}/releases/download/v{}/{}.tar.xz", 131 | self.formula.org(), 132 | self.formula.name(), 133 | self.new_version, 134 | arch 135 | ) 136 | } 137 | 138 | fn update_formula(&self) -> eyre::Result<()> { 139 | info!("Updating Homebrew {}...", "formula".bright_yellow()); 140 | 141 | // Set up URLs for all architectures 142 | let mac_url = self.package_artifact_url("aarch64-apple-darwin"); 143 | let linux_x86_64_url = self.package_artifact_url("x86_64-unknown-linux-gnu"); 144 | let linux_aarch64_url = self.package_artifact_url("aarch64-unknown-linux-gnu"); 145 | 146 | // Use threads to fetch binaries in parallel 147 | let self_clone1 = self.clone(); 148 | let mac = std::thread::spawn(move || self_clone1.get_binary(&mac_url)); 149 | 150 | let self_clone2 = self.clone(); 151 | let linux_x86_64 = std::thread::spawn(move || self_clone2.get_binary(&linux_x86_64_url)); 152 | 153 | let self_clone3 = self.clone(); 154 | let linux_aarch64 = std::thread::spawn(move || self_clone3.get_binary(&linux_aarch64_url)); 155 | 156 | let mac = mac.join().unwrap(); 157 | let linux_x86_64 = linux_x86_64.join().unwrap(); 158 | let linux_aarch64 = linux_aarch64.join().unwrap(); 159 | 160 | let binaries = Binaries { 161 | mac: mac?, 162 | linux_x86_64: linux_x86_64?, 163 | linux_aarch64: linux_aarch64?, 164 | }; 165 | 166 | let formula = self.generate_homebrew_formula(binaries)?; 167 | let formula_path = self.formula.disk_path(); 168 | 169 | if self.dry_run { 170 | info!( 171 | "Dry run: Would write formula to {}", 172 | formula_path.to_string().cyan() 173 | ); 174 | info!("Formula content:\n{}", formula); 175 | } else { 176 | if let Some(parent) = formula_path.parent() { 177 | fs_err::create_dir_all(parent)?; 178 | } 179 | fs_err::write(&formula_path, formula)?; 180 | info!( 181 | "Homebrew formula written to {}", 182 | formula_path.to_string().bright_green() 183 | ); 184 | } 185 | 186 | Ok(()) 187 | } 188 | 189 | fn generate_homebrew_formula(&self, binaries: Binaries) -> eyre::Result { 190 | use std::fmt::Write; 191 | 192 | let mut w = String::new(); 193 | 194 | writeln!(w, "# frozen_string_literal: true")?; 195 | writeln!(w)?; 196 | writeln!(w, "# {}", self.formula.desc)?; 197 | writeln!( 198 | w, 199 | "class {} < Formula", 200 | self.formula.name().to_case(Case::Pascal) 201 | )?; 202 | { 203 | let mut w = w.indented(); 204 | writeln!(w, "desc \"{}\"", self.formula.desc)?; 205 | writeln!(w, "homepage \"{}\"", self.formula.homepage)?; 206 | writeln!(w, "version \"{}\"", self.new_version)?; 207 | writeln!(w, "license \"{}\"", self.formula.license)?; 208 | writeln!(w)?; 209 | for dep in &self.formula.deps { 210 | let parts: Vec<&str> = dep.split('#').collect(); 211 | let (name, keyword) = match parts.as_slice() { 212 | [name] => (name, None), 213 | [name, keyword] => (name, Some(keyword.trim())), 214 | _ => { 215 | return Err(eyre::eyre!( 216 | "Invalid dependency syntax. Use 'name' or 'name#keyword' where keyword is 'recommended' or 'optional'" 217 | )); 218 | } 219 | }; 220 | 221 | match keyword { 222 | None => writeln!(w, "depends_on \"{}\"", name)?, 223 | Some("recommended") => writeln!(w, "depends_on \"{}\" => :recommended", name)?, 224 | Some("optional") => writeln!(w, "depends_on \"{}\" => :optional", name)?, 225 | Some(k) => { 226 | return Err(eyre::eyre!( 227 | "Unknown dependency keyword: '{}'. Use 'recommended' or 'optional'", 228 | k 229 | )); 230 | } 231 | } 232 | } 233 | writeln!(w)?; 234 | writeln!(w, "if OS.mac?")?; 235 | { 236 | let mut w = w.indented(); 237 | writeln!(w, "url \"{}\"", binaries.mac.url)?; 238 | writeln!(w, "sha256 \"{}\"", binaries.mac.sha256)?; 239 | } 240 | writeln!(w, "elsif OS.linux?")?; 241 | { 242 | let mut w = w.indented(); 243 | writeln!(w, "on_intel do")?; 244 | { 245 | let mut w = w.indented(); 246 | writeln!(w, "url \"{}\"", binaries.linux_x86_64.url)?; 247 | writeln!(w, "sha256 \"{}\"", binaries.linux_x86_64.sha256)?; 248 | } 249 | writeln!(w, "end")?; 250 | writeln!(w, "on_arm do")?; 251 | { 252 | let mut w = w.indented(); 253 | writeln!(w, "url \"{}\"", binaries.linux_aarch64.url)?; 254 | writeln!(w, "sha256 \"{}\"", binaries.linux_aarch64.sha256)?; 255 | } 256 | writeln!(w, "end")?; 257 | } 258 | writeln!(w, "end")?; 259 | writeln!(w)?; 260 | writeln!(w, "def install")?; 261 | { 262 | let mut w = w.indented(); 263 | for bin in &self.formula.bins { 264 | writeln!(w, "bin.install \"{}\"", bin)?; 265 | } 266 | writeln!(w, "libexec.install Dir[\"lib*.dylib\"] if OS.mac?")?; 267 | writeln!(w, "libexec.install Dir[\"lib*.so\"] if OS.linux?")?; 268 | } 269 | writeln!(w, "end")?; 270 | } 271 | writeln!(w, "end")?; 272 | 273 | Ok(w) 274 | } 275 | 276 | fn fetch_and_hash(&self, url: &str) -> eyre::Result { 277 | info!("Fetching binary from {}...", url.cyan()); 278 | if self.dry_run { 279 | info!("Dry run: Would fetch {}", "binary".bright_yellow()); 280 | use sha2::{Digest, Sha256}; 281 | let mut hasher = Sha256::new(); 282 | hasher.update(url); 283 | let sha256 = format!("{:x}", hasher.finalize()); 284 | return Ok(sha256); 285 | } 286 | 287 | let response = self.client.get(url).send()?; 288 | let status = response.status(); 289 | if status != 200 { 290 | let error_text = response.text()?; 291 | error!( 292 | "Failed to fetch binary: HTTP status {}, Response: {}", 293 | status.to_string().red(), 294 | error_text.red() 295 | ); 296 | return Err(eyre::eyre!( 297 | "Failed to fetch binary: HTTP status {}", 298 | status 299 | )); 300 | } 301 | let bytes = response.bytes()?; 302 | let byte_count = bytes.len(); 303 | use sha2::{Digest, Sha256}; 304 | let mut hasher = Sha256::new(); 305 | hasher.update(&bytes); 306 | let sha256 = format!("{:x}", hasher.finalize()); 307 | info!( 308 | "Binary fetched ({} bytes) and SHA256 {}", 309 | byte_count.to_string().green(), 310 | "computed".green() 311 | ); 312 | Ok(sha256) 313 | } 314 | } 315 | 316 | fn load_tap_config() -> eyre::Result { 317 | let config_path = fs_err::canonicalize(PathBuf::from(".beardist-tap.json"))?; 318 | let config_str = fs_err::read_to_string(&config_path).wrap_err_with(|| { 319 | format!( 320 | "Failed to read tap config file at {}", 321 | config_path.display().to_string().cyan() 322 | ) 323 | })?; 324 | let config: TapConfig = serde_json::from_str(&config_str).wrap_err_with(|| { 325 | format!( 326 | "Failed to parse config file at {}", 327 | config_path.display().to_string().cyan() 328 | ) 329 | })?; 330 | Ok(config) 331 | } 332 | 333 | pub(crate) fn update_tap() -> eyre::Result<()> { 334 | let dry_run = std::env::var("DRY_RUN").is_ok(); 335 | if dry_run { 336 | info!("Dry run {}", "enabled".bright_yellow()); 337 | } 338 | let github_token = 339 | std::env::var("GITHUB_TOKEN").expect("GITHUB_TOKEN environment variable not set"); 340 | 341 | info!("Loading tap {}...", "configuration".cyan()); 342 | let config = load_tap_config()?; 343 | info!("Tap configuration loaded {}", "successfully".green()); 344 | 345 | let client = Arc::new(Client::new()); 346 | 347 | info!("Processing {}...", "formulas".bright_yellow()); 348 | let mut bumped_formulas = Vec::new(); 349 | for (index, formula) in config.formulas.iter().enumerate() { 350 | info!( 351 | "Processing formula {} of {}: {}", 352 | (index + 1).to_string().cyan(), 353 | config.formulas.len().to_string().cyan(), 354 | formula.name().cyan() 355 | ); 356 | 357 | info!("Fetching GitHub {}...", "version".cyan()); 358 | let github_version = formula.github_version(&config, &github_token)?; 359 | let github_version = match github_version { 360 | Some(version) => version, 361 | None => { 362 | info!("No version found for {}, skipping", formula.name().cyan()); 363 | continue; 364 | } 365 | }; 366 | 367 | info!("GitHub version: {}", github_version.green()); 368 | 369 | let context = HomebrewContext::new( 370 | client.clone(), 371 | formula.clone(), 372 | github_version.clone(), 373 | dry_run, 374 | )?; 375 | 376 | if let Some(context) = context { 377 | info!("Updating formula for {}...", formula.name().bright_yellow()); 378 | context.update_formula()?; 379 | info!( 380 | "Formula update completed for {}", 381 | formula.name().bright_green() 382 | ); 383 | bumped_formulas.push((formula.name().to_string(), github_version)); 384 | } else { 385 | info!("No update needed for {}", formula.name().bright_blue()); 386 | } 387 | } 388 | info!("All formulas {}", "processed".bright_green()); 389 | 390 | if !bumped_formulas.is_empty() { 391 | let commit_message = bumped_formulas 392 | .iter() 393 | .map(|(name, version)| format!("{} to {}", name, version)) 394 | .collect::>() 395 | .join(", "); 396 | 397 | let full_commit_message = format!("Bump formulas: {}", commit_message); 398 | 399 | info!("Committing changes..."); 400 | if !dry_run { 401 | run_command("git", &["add", "."], None)?; 402 | run_command( 403 | "git", 404 | &["commit", "-m", &full_commit_message], 405 | Some(indexmap::indexmap! { 406 | "GIT_AUTHOR_NAME".to_string() => "beardist".to_string(), 407 | "GIT_AUTHOR_EMAIL".to_string() => "amos@bearcove.eu".to_string(), 408 | "GIT_COMMITTER_NAME".to_string() => "beardist".to_string(), 409 | "GIT_COMMITTER_EMAIL".to_string() => "amos@bearcove.eu".to_string(), 410 | }), 411 | )?; 412 | info!("Changes committed successfully"); 413 | } else { 414 | info!( 415 | "Dry run: Would commit changes with message: {}", 416 | full_commit_message.cyan() 417 | ); 418 | } 419 | 420 | info!("Formulas bumped:"); 421 | for (name, version) in bumped_formulas { 422 | info!(" {} to version {}", name.cyan(), version.green()); 423 | } 424 | 425 | info!("Pushing changes..."); 426 | let remote_output = get_trimmed_cmd_stdout("git", &["remote", "-v"], None)?; 427 | let remote_url = remote_output 428 | .lines() 429 | .find(|line| line.contains("(push)")) 430 | .and_then(|line| { 431 | let url = line.split_whitespace().nth(1)?; 432 | // Convert SSH URLs to HTTPS URLs 433 | if url.starts_with("git@github.com:") { 434 | let repo_path = url.trim_start_matches("git@github.com:"); 435 | Some(format!("https://github.com/{}", repo_path)) 436 | } else { 437 | Some(url.to_string()) 438 | } 439 | }) 440 | .ok_or_else(|| eyre::eyre!("Failed to get remote URL"))?; 441 | 442 | let org_repo = remote_url 443 | .trim_start_matches("https://") 444 | .trim_end_matches(".git") 445 | .split('/') 446 | .skip(1) 447 | .take(2) 448 | .collect::>() 449 | .join("/"); 450 | 451 | info!("Remote URL: {}", remote_url.cyan()); 452 | info!("Organization/Repo: {}", org_repo.cyan()); 453 | 454 | let mut push_url = Url::parse(&remote_url)?; 455 | push_url.set_username("token").unwrap(); 456 | push_url.set_password(Some(&github_token)).unwrap(); 457 | 458 | if !dry_run { 459 | run_command("git", &["push", push_url.as_str(), "HEAD:main"], None)?; 460 | info!("Changes pushed successfully"); 461 | } else { 462 | info!("Dry run: Would push changes to remote repository"); 463 | info!("Push command that would be executed:"); 464 | let mut redacted_url = push_url.clone(); 465 | redacted_url.set_password(Some("REDACTED")).unwrap(); 466 | info!("git push {} HEAD:main", redacted_url.to_string().cyan()); 467 | } 468 | } else { 469 | info!("No formulas were bumped"); 470 | } 471 | Ok(()) 472 | } 473 | -------------------------------------------------------------------------------- /src/cargo.rs: -------------------------------------------------------------------------------- 1 | use camino::Utf8PathBuf; 2 | use indexmap::IndexMap; 3 | use log::{debug, error, info, warn}; 4 | use owo_colors::{OwoColorize, Style}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::{BuildContext, PackagedFile, PackagedFileKind, TargetSpec, command}; 8 | 9 | #[derive(Debug, Serialize, Deserialize)] 10 | #[serde(deny_unknown_fields)] 11 | pub(crate) struct CargoConfig { 12 | /// Name of binaries we should pack 13 | pub(crate) bins: Vec, 14 | } 15 | 16 | /// builds values for RUSTUP_HOME, CARGO_HOME, etc. 17 | struct BuildEnv { 18 | /// a cache dir we can use, that persists between builds. 19 | /// we can store rustup and cargo toolchains in there, the timelord cache, etc. 20 | cache_dir: Utf8PathBuf, 21 | } 22 | 23 | impl BuildEnv { 24 | fn cargo_home(&self) -> Utf8PathBuf { 25 | self.cache_dir.join("cargo") 26 | } 27 | 28 | fn rustup_home(&self) -> Utf8PathBuf { 29 | self.cache_dir.join("rustup") 30 | } 31 | 32 | fn pnpm_cache_folder(&self) -> Utf8PathBuf { 33 | self.cache_dir.join("pnpm") 34 | } 35 | 36 | fn get_env(&self) -> IndexMap { 37 | let mut env = IndexMap::new(); 38 | env.insert("CARGO_HOME".to_string(), self.cargo_home().to_string()); 39 | env.insert("RUSTUP_HOME".to_string(), self.rustup_home().to_string()); 40 | env.insert( 41 | "PNPM_CACHE_FOLDER".to_string(), 42 | self.pnpm_cache_folder().to_string(), 43 | ); 44 | env.insert("CLICOLOR_FORCE".to_string(), "1".to_string()); 45 | env.insert("FORCE_COLOR".to_string(), "1".to_string()); 46 | env.insert("RUSTC_BOOTSTRAP".to_string(), "1".to_string()); 47 | env.insert("RUSTFLAGS".to_string(), "-Z remap-cwd-prefix=.".to_string()); 48 | env 49 | } 50 | } 51 | 52 | pub(crate) struct CargoBuildContext<'a> { 53 | /// the build context we're operating in 54 | parent: &'a BuildContext, 55 | 56 | /// the environment we're building in 57 | build_env: BuildEnv, 58 | 59 | /// the target we're building for 60 | target_spec: TargetSpec, 61 | 62 | /// the configuration for this build 63 | config: CargoConfig, 64 | } 65 | 66 | impl<'a> CargoBuildContext<'a> { 67 | pub(crate) fn new(parent: &'a BuildContext, config: CargoConfig) -> eyre::Result { 68 | let build_env = BuildEnv { 69 | cache_dir: parent.cache_dir.clone(), 70 | }; 71 | 72 | info!("{}", "🌍 Environment:".yellow()); 73 | let env = build_env.get_env(); 74 | let max_key_len = env.keys().map(|k| k.len()).max().unwrap_or(0); 75 | for (key, value) in env.iter() { 76 | let formatted_value = 77 | if let Some(relative_path) = value.strip_prefix(build_env.cache_dir.as_str()) { 78 | format!("{}{}", "$CACHE".dimmed(), relative_path) 79 | } else { 80 | value.to_string() 81 | }; 82 | 83 | info!( 84 | " {:>width$}: {}", 85 | key.blue(), 86 | formatted_value, 87 | width = max_key_len + 2 88 | ); 89 | } 90 | 91 | let rustc_version = 92 | command::get_trimmed_cmd_stdout("rustc", &["--version"], Some(env.clone()))?; 93 | let cargo_version = 94 | command::get_trimmed_cmd_stdout("cargo", &["--version"], Some(env.clone()))?; 95 | let cargo_sweep_version = 96 | command::get_trimmed_cmd_stdout("cargo", &["sweep", "--version"], Some(env))?; 97 | info!( 98 | "🔍 Toolchain: {} | {} | {}", 99 | rustc_version.red(), 100 | cargo_version.green(), 101 | cargo_sweep_version.blue() 102 | ); 103 | 104 | let json_output = command::get_trimmed_cmd_stdout( 105 | "rustc", 106 | &["-Z", "unstable-options", "--print", "target-spec-json"], 107 | Some(build_env.get_env()), 108 | )?; 109 | let target_spec = TargetSpec::from_json(&json_output)?; 110 | target_spec.print_info(); 111 | 112 | Ok(Self { 113 | parent, 114 | config, 115 | build_env, 116 | target_spec, 117 | }) 118 | } 119 | 120 | pub(crate) fn build(&self, files_to_package: &mut Vec) -> eyre::Result<()> { 121 | self.run_timelord()?; 122 | self.build_project()?; 123 | 124 | for bin in &self.config.bins { 125 | let binary_path = self.cargo_out_dir().join(bin); 126 | if binary_path.exists() { 127 | let binary_size = fs_err::metadata(&binary_path)?.len(); 128 | info!( 129 | "✅ Produced {} binary at {}", 130 | crate::format_bytes(binary_size).green(), 131 | binary_path.to_string().cyan() 132 | ); 133 | files_to_package.push(PackagedFile { 134 | kind: PackagedFileKind::Bin, 135 | path: binary_path, 136 | }) 137 | } else { 138 | error!( 139 | "❌ Binary file does not exist at path: {}", 140 | binary_path.to_string().red() 141 | ); 142 | panic!(); 143 | } 144 | } 145 | 146 | let mut highlight_patterns = Vec::new(); 147 | highlight_patterns.push(( 148 | regex::Regex::new(r"(?i)(\.dylib|\.so|LC_RPATH|@rpath|@executable_path|\$ORIGIN)") 149 | .unwrap(), 150 | Style::new().blue(), 151 | )); 152 | for bin in &self.config.bins { 153 | highlight_patterns.push(( 154 | regex::Regex::new(&format!(r"(?i)({})", regex::escape(bin))).unwrap(), 155 | Style::new().green(), 156 | )); 157 | } 158 | highlight_patterns.push(( 159 | regex::Regex::new(&format!( 160 | r"(?i)({})", 161 | regex::escape(&self.target_spec.full_name()) 162 | )) 163 | .unwrap(), 164 | Style::new().yellow(), 165 | )); 166 | 167 | debug!("📊 Running {} on rustc...", "target-libdir".dimmed()); 168 | let target_libdir = command::get_trimmed_cmd_stdout( 169 | "rustc", 170 | &["--print", "target-libdir"], 171 | Some(self.get_env()), 172 | )?; 173 | debug!("📊 Target libdir: {}", target_libdir.cyan()); 174 | 175 | let libstd_pattern = 176 | glob::Pattern::new(&format!("libstd-*{}", self.target_spec.dll_suffix))?; 177 | 178 | let libstd_path = fs_err::read_dir(target_libdir)? 179 | .filter_map(|entry| entry.ok()) 180 | .find(|entry| { 181 | entry 182 | .file_name() 183 | .to_str() 184 | .is_some_and(|name| libstd_pattern.matches(name)) 185 | }) 186 | .ok_or_else(|| eyre::eyre!("libstd not found in target libdir"))?; 187 | 188 | let libstd_size = libstd_path.metadata()?.len(); 189 | debug!( 190 | "📊 Found libstd: {} ({})", 191 | libstd_path.file_name().to_str().unwrap().cyan(), 192 | crate::format_bytes(libstd_size).green() 193 | ); 194 | 195 | let libstd_path: Utf8PathBuf = libstd_path.path().try_into().unwrap(); 196 | 197 | let cargo_out_dir = self.cargo_out_dir(); 198 | 199 | // Copy libstd next to the binary 200 | let libstd_copy_path = cargo_out_dir.join(libstd_path.file_name().unwrap()); 201 | if self.target_spec.os == "linux" || self.target_spec.os == "macos" { 202 | // Remove any pre-existing symlinks or files, ignoring any errors 203 | let _ = fs_err::remove_file(&libstd_copy_path); 204 | 205 | // Copy the file 206 | fs_err::copy(&libstd_path, &libstd_copy_path)?; 207 | info!("📄 Copied libstd: {}", libstd_copy_path.to_string().cyan()); 208 | } else { 209 | warn!( 210 | "Skipping libstd copy for unsupported OS: {}", 211 | self.target_spec.os 212 | ); 213 | } 214 | 215 | // Add other libraries 216 | let dll_suffix = self.target_spec.dll_suffix.as_str(); 217 | for entry in fs_err::read_dir(&cargo_out_dir)? { 218 | let entry = entry?; 219 | let file_name = entry.file_name().into_string().unwrap(); 220 | debug!("Examining file: {file_name} (our dll_suffix is {dll_suffix})"); 221 | if file_name.starts_with("lib") && file_name.ends_with(dll_suffix) { 222 | let file_path = entry.path(); 223 | files_to_package.push(PackagedFile { 224 | kind: PackagedFileKind::Lib, 225 | path: file_path.try_into().unwrap(), 226 | }); 227 | } 228 | } 229 | 230 | self.fix_install_names()?; 231 | 232 | if self.target_spec.os == "linux" { 233 | for file in files_to_package 234 | .iter() 235 | .filter(|f| matches!(f.kind, PackagedFileKind::Bin | PackagedFileKind::Lib)) 236 | { 237 | show_fyi( 238 | "ldd", 239 | &[file.path.as_str()], 240 | Some(self.get_env()), 241 | &highlight_patterns, 242 | )?; 243 | 244 | show_fyi( 245 | "readelf", 246 | &["-d", file.path.as_str()], 247 | Some(self.get_env()), 248 | &highlight_patterns, 249 | )?; 250 | } 251 | } else if self.target_spec.os == "macos" { 252 | for file in files_to_package 253 | .iter() 254 | .filter(|f| matches!(f.kind, PackagedFileKind::Bin | PackagedFileKind::Lib)) 255 | { 256 | show_fyi( 257 | "otool", 258 | &["-L", file.path.as_str()], 259 | Some(self.get_env()), 260 | &highlight_patterns, 261 | )?; 262 | } 263 | 264 | show_fyi( 265 | "bash", 266 | &[ 267 | "-c", 268 | &format!( 269 | "otool -l {}", 270 | self.cargo_out_dir().join(&self.config.bins[0]) 271 | ), 272 | ], 273 | Some(self.get_env()), 274 | &highlight_patterns, 275 | )?; 276 | } else { 277 | warn!( 278 | "Skipping binary dependency check for unsupported OS: {}", 279 | self.target_spec.os 280 | ); 281 | } 282 | 283 | info!( 284 | "📊 Running {} on {}...", 285 | "--version".dimmed(), 286 | self.cargo_out_dir() 287 | .join(&self.config.bins[0]) 288 | .to_string() 289 | .cyan() 290 | ); 291 | crate::run_command( 292 | self.cargo_out_dir().join(&self.config.bins[0]).as_str(), 293 | &["--version"], 294 | Some(self.get_env()), 295 | )?; 296 | 297 | Ok(()) 298 | } 299 | 300 | fn run_timelord(&self) -> eyre::Result<()> { 301 | // Detect if we're in CI 302 | if std::env::var("CI").is_ok() { 303 | if std::env::var("SKIP_TIMELORD").is_ok() { 304 | info!("Skipping timelord ($SKIP_TIMELORD is set)"); 305 | } else { 306 | // this manipulates timestamps on files to ensure that incremental builds work correctly 307 | timelord::sync(self.parent.source_dir.clone(), self.cargo_target_dir()); 308 | info!("🕰️ Timelord sync completed in CI environment"); 309 | } 310 | } else { 311 | info!("🏠 Not in CI environment, skipping Timelord sync"); 312 | } 313 | Ok(()) 314 | } 315 | 316 | fn get_env(&self) -> IndexMap { 317 | let mut env = self.build_env.get_env(); 318 | env.insert( 319 | "CARGO_TARGET_DIR".to_string(), 320 | self.cargo_target_dir().to_string(), 321 | ); 322 | env 323 | } 324 | 325 | fn cargo_target_dir(&self) -> Utf8PathBuf { 326 | self.build_env 327 | .cache_dir 328 | .join("target") 329 | .join(&self.parent.config.org) 330 | .join(&self.parent.config.name) 331 | .join(self.target_spec.full_name()) 332 | } 333 | 334 | /// ${TARGET}/${PROFILE} 335 | fn cargo_out_dir(&self) -> Utf8PathBuf { 336 | self.cargo_target_dir().join("release") 337 | } 338 | 339 | fn build_project(&self) -> eyre::Result<()> { 340 | info!("{}", "🔨 Building the project...".yellow()); 341 | let env = self.get_env(); 342 | crate::run_command("cargo", &["build", "--verbose", "--release"], Some(env))?; 343 | Ok(()) 344 | } 345 | 346 | fn fix_install_names(&self) -> eyre::Result<()> { 347 | if self.target_spec.os != "macos" { 348 | return Ok(()); 349 | } 350 | 351 | let deps_dir = self.cargo_out_dir().join("deps"); 352 | 353 | if !deps_dir.exists() { 354 | warn!("deps directory not found: {}", deps_dir); 355 | return Ok(()); 356 | } 357 | 358 | // Collect all dylib files we want to fix 359 | let mut dylibs_to_fix = Vec::new(); 360 | 361 | // Build a hash set of all libraries we own 362 | let mut our_libraries = std::collections::HashSet::new(); 363 | 364 | // Check target directory 365 | for entry in fs_err::read_dir(self.cargo_out_dir())? { 366 | let entry = entry?; 367 | let path = entry.path(); 368 | let file_name = path.file_name().unwrap().to_string_lossy(); 369 | 370 | if file_name.starts_with("lib") && file_name.ends_with(".dylib") { 371 | let is_symlink = path.is_symlink(); 372 | our_libraries.insert(file_name.to_string()); 373 | 374 | if is_symlink { 375 | info!( 376 | "Detected symlink: {}. Adding to our libraries but not fixing.", 377 | path.display().to_string().cyan() 378 | ); 379 | } else { 380 | match path.canonicalize() { 381 | Ok(canonicalized_path) => { 382 | info!( 383 | "Adding {} to dylibs_to_fix", 384 | canonicalized_path.display().to_string().cyan() 385 | ); 386 | dylibs_to_fix.push(canonicalized_path); 387 | } 388 | Err(e) => { 389 | warn!( 390 | "Failed to canonicalize path: {}. Error: {}", 391 | path.display().to_string().red(), 392 | e.to_string().red() 393 | ); 394 | info!( 395 | "Adding original path {} to dylibs_to_fix", 396 | path.display().to_string().cyan() 397 | ); 398 | dylibs_to_fix.push(path); 399 | } 400 | } 401 | } 402 | } 403 | } 404 | 405 | // Add binaries 406 | for binary_name in &self.config.bins { 407 | let binary_path = self.cargo_out_dir().join(binary_name); 408 | dylibs_to_fix.push(binary_path.canonicalize()?); 409 | } 410 | 411 | debug!("Our libraries: {:?}", our_libraries); 412 | 413 | // Fix each dylib 414 | for dylib_path in dylibs_to_fix { 415 | let file_name = dylib_path.file_name().unwrap().to_string_lossy(); 416 | let dylib_path_str = dylib_path.to_string_lossy(); 417 | debug!("🔍 Inspecting dependencies for: {}", file_name.cyan()); 418 | 419 | let dependencies = get_dependencies(dylib_path_str.as_ref())?; 420 | 421 | // Formulate a plan: change only our libraries to use @rpath 422 | for dep in dependencies { 423 | let dep_name = dep.split('/').next_back().unwrap(); 424 | debug!("Examining dependency: {}", dep.cyan()); 425 | if our_libraries.contains(dep_name) { 426 | let new_path = format!("@rpath/{}", dep_name); 427 | debug!( 428 | "🛠️ This is our library. Changing {} to {}", 429 | dep.cyan(), 430 | new_path.blue() 431 | ); 432 | change_dep(dylib_path_str.as_ref(), &dep, &new_path)?; 433 | } else { 434 | debug!("⏭️ This is not our library. Keeping as is: {}", dep.cyan()); 435 | } 436 | } 437 | 438 | // Set the id of the dylib itself (skip for the main binary) 439 | if file_name.starts_with("lib") && file_name.ends_with(".dylib") { 440 | let id = format!("@rpath/{}", file_name); 441 | debug!("🛠️ Setting id for dylib: {}", id.blue()); 442 | command::run_command( 443 | "install_name_tool", 444 | &["-id", &id, dylib_path_str.as_ref()], 445 | None, 446 | )?; 447 | } 448 | 449 | // Verify changes 450 | let verify_deps = get_dependencies(dylib_path_str.as_ref())?; 451 | debug!( 452 | "✅ Verification output for {}:\n{}", 453 | file_name.cyan(), 454 | verify_deps.join("\n") 455 | ); 456 | } 457 | Ok(()) 458 | } 459 | 460 | pub(crate) fn sweep(&self) -> eyre::Result<()> { 461 | debug!("🧹 Running cargo sweep..."); 462 | let env = self.get_env(); 463 | crate::run_command("cargo", &["sweep", "--time", "30"], Some(env))?; 464 | Ok(()) 465 | } 466 | } 467 | 468 | // Helper function to run otool -l and collect all dependencies 469 | fn get_dependencies(path: &str) -> eyre::Result> { 470 | let output = command::get_cmd_stdout("otool", &["-l", path], None)?; 471 | let mut dependencies = Vec::new(); 472 | let mut in_load_dylib = false; 473 | let mut current_dependency; 474 | 475 | for line in output.lines() { 476 | if line.trim().starts_with("cmd LC_LOAD_DYLIB") { 477 | in_load_dylib = true; 478 | } else if in_load_dylib && line.trim().starts_with("name") { 479 | let parts: Vec<&str> = line.split_whitespace().collect(); 480 | if parts.len() > 1 { 481 | current_dependency = parts[1].to_string(); 482 | dependencies.push(current_dependency.clone()); 483 | } 484 | in_load_dylib = false; 485 | } 486 | } 487 | 488 | Ok(dependencies) 489 | } 490 | 491 | // Helper function to run install_name_tool and verify the change 492 | fn change_dep(path: &str, old_dep: &str, new_dep: &str) -> eyre::Result<()> { 493 | command::run_command( 494 | "install_name_tool", 495 | &["-change", old_dep, new_dep, path], 496 | None, 497 | )?; 498 | Ok(()) 499 | } 500 | 501 | fn show_fyi( 502 | command: &str, 503 | args: &[&str], 504 | env: Option>, 505 | highlight_patterns: &[(regex::Regex, Style)], 506 | ) -> eyre::Result<()> { 507 | let cmd_str = format!("{} {}", command, args.join(" ")); 508 | info!("💅 FYI, {}", cmd_str.magenta()); 509 | let output = command::get_cmd_stdout(command, args, env)?; 510 | for line in output.lines() { 511 | let mut highlighted_line = line.to_string(); 512 | for (pattern, style) in highlight_patterns { 513 | highlighted_line = pattern 514 | .replace_all(&highlighted_line, |caps: ®ex::Captures| { 515 | caps[0].to_string().style(*style).to_string() 516 | }) 517 | .to_string(); 518 | } 519 | info!(" {}", highlighted_line); 520 | } 521 | Ok(()) 522 | } 523 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::disallowed_methods)] 2 | 3 | use camino::Utf8PathBuf; 4 | use cargo::{CargoBuildContext, CargoConfig}; 5 | use clap::{Parser, Subcommand}; 6 | use command::run_command; 7 | use eyre::{self, Context, Result}; 8 | use homebrew::update_tap; 9 | use log::*; 10 | use owo_colors::OwoColorize; 11 | use rand::seq::IndexedRandom; 12 | use semver::{BuildMetadata, Prerelease, Version}; 13 | use serde::{Deserialize, Serialize}; 14 | use std::{env, os::unix::fs::PermissionsExt, path::PathBuf}; 15 | use target_spec::TargetSpec; 16 | use tempfile::TempDir; 17 | 18 | pub(crate) mod github; 19 | 20 | mod cargo; 21 | pub(crate) mod command; 22 | mod homebrew; 23 | mod system; 24 | pub(crate) mod target_spec; 25 | 26 | mod utils; 27 | pub use utils::*; 28 | 29 | mod k8s; 30 | 31 | mod indented_writer; 32 | pub(crate) use indented_writer::*; 33 | 34 | /// CLI interface for beardist 35 | #[derive(Parser)] 36 | #[command(author, version, about, long_about = None)] 37 | struct Cli { 38 | /// The subcommand to execute 39 | #[command(subcommand)] 40 | command: Commands, 41 | } 42 | 43 | /// Available subcommands for beardist 44 | #[derive(Subcommand)] 45 | enum Commands { 46 | /// Build the project, create a package, and upload it to github 47 | Build, 48 | /// Bump the version number and create a new git tag 49 | Bump(BumpArgs), 50 | /// Bump k8s manifests and run `./deploy-manifests` 51 | K8s(DeployArgs), 52 | /// Update a Homebrew tap containing a `.beardist-tap.json` 53 | UpdateTap, 54 | } 55 | 56 | /// Arguments for the Bump command 57 | #[derive(Parser)] 58 | struct BumpArgs { 59 | /// Type of version bump (major, minor, or patch) 60 | #[arg(value_enum)] 61 | bump_type: Option, 62 | } 63 | 64 | #[derive(clap::ValueEnum, Clone)] 65 | enum BumpType { 66 | Major, 67 | Minor, 68 | Patch, 69 | } 70 | 71 | /// Arguments for the Deploy command 72 | #[derive(Parser)] 73 | struct DeployArgs { 74 | /// The name of the image to deploy, e.g. "bearcove/home" (`ghcr.io` is implied) 75 | image: String, 76 | } 77 | 78 | pub const CONFIG_VERSION: u64 = 3; 79 | pub const USER_AGENT: &str = "github.com/bearcove/beardist@1.0"; 80 | 81 | #[derive(Debug, Serialize, Deserialize)] 82 | #[serde(deny_unknown_fields)] 83 | struct Config { 84 | /// Version of beardist required 85 | version: u64, 86 | 87 | /// Organization or user name 88 | org: String, 89 | 90 | /// Project name 91 | name: String, 92 | 93 | cargo: Option, 94 | custom: Option, 95 | } 96 | 97 | #[derive(Debug, Serialize, Deserialize)] 98 | #[serde(deny_unknown_fields)] 99 | struct CustomConfig { 100 | /// Any custom build steps to run (`bun build` etc.) 101 | #[serde(default)] 102 | steps: Vec>, 103 | 104 | /// Any other files to include in the archive. You don't need to specify cargo binaries 105 | /// here. This is for data files. 106 | #[serde(default)] 107 | files: Vec, 108 | } 109 | 110 | /// Context for `build` subcommand 111 | struct BuildContext { 112 | /// Configuration for the project (read from .beardist.json) 113 | config: Config, 114 | 115 | /// URL of the github server 116 | github_server_url: String, 117 | 118 | /// github read-write API token 119 | github_rw_token: String, 120 | 121 | /// The git tag we're reacting to (in CI) 122 | tag: String, 123 | 124 | /// Flag indicating whether this is a dry run 125 | is_dry_run: bool, 126 | 127 | /// `$BEARDIST_CACHE_DIR` 128 | cache_dir: Utf8PathBuf, 129 | 130 | /// the source directory we're building (initially, the current directory) 131 | source_dir: Utf8PathBuf, 132 | 133 | /// Temporary directory for package archive 134 | temp_dir: TempDir, 135 | 136 | /// `$BEARDIST_ARTIFACT_NAME` 137 | artifact_name: String, 138 | } 139 | 140 | #[derive(Debug)] 141 | enum PackagedFileKind { 142 | /// Mach-O/PE/ELF, etc. 143 | Bin, 144 | /// .dylib, .so, etc. 145 | Lib, 146 | /// anything else, really 147 | Misc, 148 | } 149 | 150 | struct PackagedFile { 151 | kind: PackagedFileKind, 152 | 153 | /// absolute path on disk — for now the archives are all flat. 154 | path: Utf8PathBuf, 155 | } 156 | 157 | impl BuildContext { 158 | fn new(config: Config) -> Result { 159 | let source_dir = 160 | camino::Utf8PathBuf::from_path_buf(env::current_dir()?.canonicalize()?).unwrap(); 161 | info!( 162 | "🏗️ Building project from: {}", 163 | source_dir.to_string().cyan() 164 | ); 165 | 166 | info!(""); 167 | 168 | // BEARDIST_CACHE_DIR must be set to point to persistent storage 169 | // This tool is meant to be run in CI, and we want the cache 170 | // to use a persistent location for faster builds 171 | // We'll place rustup home, cargo home, target directory, etc. in this cache 172 | let cache_dir = env::var("BEARDIST_CACHE_DIR") 173 | .map(Utf8PathBuf::from) 174 | .map_err(|_| { 175 | eyre::eyre!( 176 | "{} is not set. It should point to persistent storage for CI builds. This is where we'll store rustup home, cargo home, target directory, etc.", 177 | "BEARDIST_CACHE_DIR".cyan() 178 | ) 179 | })?; 180 | 181 | if !cache_dir.try_exists().unwrap_or(false) { 182 | fs_err::create_dir_all(&cache_dir)?; 183 | } 184 | 185 | fs_err::set_permissions(&cache_dir, std::fs::Permissions::from_mode(0o755))?; 186 | 187 | let cache_messages = [ 188 | "🍭 that's where we hide the goodies", 189 | "🕵️ our secret stash of bits and bytes", 190 | "💎 the treasure trove of cached wonders", 191 | "✨ where the magic happens behind the scenes", 192 | "🎡 our digital playground", 193 | "🏞️ the land of cached opportunities", 194 | "💭 where code dreams come true", 195 | "🏰 the fortress of solitude for our builds", 196 | "🏠 our cozy little corner of the disk", 197 | "🐾 where we keep our digital pets", 198 | "🎭 the VIP lounge for our data", 199 | "☕ our code's favorite hangout spot", 200 | "🌼 the secret garden of compilation", 201 | "🏖️ where bits go for vacation", 202 | "🍽️ the all-you-can-cache buffet", 203 | "🕹️ our digital gaming den", 204 | "🚪 the magical wardrobe of our build process", 205 | "📚 where we store our binary collection", 206 | "🏨 the Hotel California of data (you can checkout anytime you like...)", 207 | "⏰ our code's time machine", 208 | "🦇 the Batcave of our build system", 209 | "🍯 where we keep the good stuff", 210 | "🏦 our digital Swiss bank account", 211 | "🏫 the School of Caching", 212 | "🍰 our little slice of binary heaven", 213 | ]; 214 | let cache_message = cache_messages.choose(&mut rand::rng()).unwrap(); 215 | 216 | info!("🔍 Cache {} — {}", cache_dir.cyan(), cache_message.yellow()); 217 | 218 | let mut is_dry_run = false; 219 | 220 | let github_rw_token = match env::var("GH_READWRITE_TOKEN") { 221 | Ok(token) => { 222 | info!( 223 | "{} is set: {}", 224 | "GH_READWRITE_TOKEN".cyan(), 225 | format_secret(&token) 226 | ); 227 | token 228 | } 229 | Err(_) => { 230 | is_dry_run = true; 231 | "placeholder_token".to_string() 232 | } 233 | }; 234 | 235 | let maybe_tag = env::var("GITHUB_REF").ok().and_then(|ref_str| { 236 | info!("{} is set: {}", "GITHUB_REF".cyan(), ref_str); 237 | ref_str.strip_prefix("refs/tags/").map(String::from) 238 | }); 239 | let tag = match maybe_tag { 240 | Some(t) => t, 241 | None => { 242 | is_dry_run = true; 243 | warn!( 244 | "{} is not set or invalid, falling back to placeholder", 245 | "GITHUB_REF".cyan() 246 | ); 247 | "vX.Y.Z".to_string() 248 | } 249 | }; 250 | 251 | let github_server_url = match env::var("GITHUB_SERVER_URL") { 252 | Ok(url) => url, 253 | Err(_) => { 254 | warn!( 255 | "{} is not set, falling back to default", 256 | "GITHUB_SERVER_URL".cyan() 257 | ); 258 | "https://github.com".to_string() 259 | } 260 | }; 261 | 262 | let temp_dir = TempDir::new()?; 263 | 264 | let artifact_name_var = "BEARDIST_ARTIFACT_NAME"; 265 | let artifact_name = std::env::var(artifact_name_var).unwrap_or_else(|_| { 266 | if std::env::var("CI").is_ok() { 267 | error!( 268 | "beardist expects ${} to be set to determine the custom package name to upload", 269 | artifact_name_var 270 | ); 271 | error!( 272 | "example values include 'x86_64-unknown-linux-gnu', 'aarch64-apple-darwin', etc." 273 | ); 274 | error!( 275 | " 276 | you can run the following command to find our triplet easily: 277 | " 278 | ); 279 | error!( 280 | r#" 281 | rustc +stable --print target-libdir | sed -E 's/.*stable-([^/]+).*/\1/' 282 | "# 283 | ); 284 | panic!("${artifact_name_var} must be set in CI environment") 285 | } else { 286 | let output = command::get_trimmed_cmd_stdout( 287 | "rustc", 288 | &["+stable", "--print", "target-libdir"], 289 | None, 290 | ) 291 | .expect("Failed to execute rustc command"); 292 | let triplet = output 293 | .split('/') 294 | .find(|s| s.contains("stable-")) 295 | .and_then(|s| s.strip_prefix("stable-")) 296 | .expect("Failed to extract triplet from rustc output"); 297 | info!( 298 | "Automatically determined artifact name: {}", 299 | triplet.cyan() 300 | ); 301 | triplet.to_string() 302 | } 303 | }); 304 | 305 | let cx = Self { 306 | artifact_name, 307 | config, 308 | cache_dir, 309 | github_server_url, 310 | github_rw_token, 311 | tag, 312 | is_dry_run, 313 | source_dir, 314 | temp_dir, 315 | }; 316 | Ok(cx) 317 | } 318 | 319 | fn create_package_archive( 320 | &self, 321 | files_to_package: &[PackagedFile], 322 | ) -> Result { 323 | let artifact_name = &self.artifact_name; 324 | let package_file = camino::Utf8PathBuf::from_path_buf( 325 | self.temp_dir.path().join(format!("{artifact_name}.tar.xz")), 326 | ) 327 | .unwrap(); 328 | 329 | info!( 330 | "📦 Packaging {} with {} files:", 331 | package_file.cyan(), 332 | files_to_package.len().to_string().yellow() 333 | ); 334 | for file in files_to_package { 335 | let file_size = fs_err::metadata(&file.path)?.len(); 336 | info!( 337 | " - {} {}", 338 | file.path.file_name().unwrap().to_string().blue(), 339 | format!("({})", format_bytes(file_size)).green() 340 | ); 341 | } 342 | 343 | let tar_args = files_to_package 344 | .iter() 345 | .flat_map(|f| { 346 | vec![ 347 | "-C".to_string(), 348 | f.path.parent().unwrap().to_string(), 349 | f.path.file_name().unwrap().to_string(), 350 | ] 351 | }) 352 | .collect::>() 353 | .join(" "); 354 | 355 | let archive_command = format!( 356 | "tar --create --verbose --file=- {} | xz -2 --threads=0 --stdout > {}", 357 | tar_args, package_file 358 | ); 359 | run_command("bash", &["-euo", "pipefail", "-c", &archive_command], None)?; 360 | 361 | Ok(package_file) 362 | } 363 | 364 | fn upload_package( 365 | &self, 366 | package_file: &camino::Utf8Path, 367 | file_content: &[u8], 368 | files_to_package: &[PackagedFile], 369 | ) -> Result<()> { 370 | let org = &self.config.org; 371 | let name = &self.config.name; 372 | let tag = &self.tag; 373 | let package_file_name = package_file.file_name().unwrap(); 374 | assert!(!package_file_name.contains('/')); 375 | 376 | const INSPECT_OUTPUT_DIR: &str = "/tmp/beardist-output"; 377 | let _ = fs_err::remove_dir_all(INSPECT_OUTPUT_DIR); 378 | fs_err::create_dir_all(INSPECT_OUTPUT_DIR)?; 379 | for file in files_to_package { 380 | let dest_path = format!("{}/{}", INSPECT_OUTPUT_DIR, file.path.file_name().unwrap()); 381 | fs_err::copy(&file.path, &dest_path)?; 382 | info!( 383 | "📄 Copied {} to {}", 384 | file.path.to_string().cyan(), 385 | dest_path.bold().underline() 386 | ); 387 | } 388 | info!( 389 | "📁 All files copied to: {}", 390 | INSPECT_OUTPUT_DIR.bold().underline() 391 | ); 392 | 393 | const INSPECT_OUTPUT_PATH: &str = "/tmp/beardist-output.tar.xz"; 394 | fs_err::write(INSPECT_OUTPUT_PATH, file_content)?; 395 | info!( 396 | "📦 {} package written to: {}", 397 | format_bytes(file_content.len() as _).blue(), 398 | INSPECT_OUTPUT_PATH.bold().underline() 399 | ); 400 | if file_content.len() < 10 * 1024 { 401 | return Err(eyre::eyre!( 402 | "Suspiciously small package size ({}). Aborting.", 403 | format_bytes(file_content.len() as _) 404 | )); 405 | } 406 | 407 | if self.is_dry_run { 408 | warn!("Not uploading (dry run)"); 409 | return Ok(()); 410 | } 411 | 412 | // Create a release if it doesn't exist using the github client 413 | let github_client = crate::github::GitHubClient::new( 414 | self.github_server_url.clone(), 415 | self.github_rw_token.clone(), 416 | ); 417 | 418 | let release_id = github_client 419 | .create_release(org, name, tag) 420 | .map_err(|e| eyre::eyre!("Failed to create or get release: {}", e))?; 421 | 422 | // Upload the asset to the release using the GitHub client abstraction 423 | let upload_start = std::time::Instant::now(); 424 | 425 | github_client 426 | .upload_artifact(org, name, release_id, package_file_name, file_content) 427 | .map_err(|e| eyre::eyre!("Failed to upload release artifact: {}", e))?; 428 | 429 | let upload_time = upload_start.elapsed().as_millis() as u64; 430 | info!( 431 | "✅ Package upload completed ({})", 432 | format!("{}ms", upload_time).green() 433 | ); 434 | Ok(()) 435 | } 436 | } 437 | 438 | fn main() -> Result<()> { 439 | if std::env::var("RUST_LOG").is_err() { 440 | unsafe { std::env::set_var("RUST_LOG", "info") } 441 | } 442 | env_logger::builder() 443 | .format_timestamp(None) 444 | .format_target(false) 445 | .format_level(false) // would be nice for non-info, but shrug 446 | .init(); 447 | color_eyre::install()?; 448 | 449 | let cli = Cli::parse(); 450 | 451 | match cli.command { 452 | Commands::Build => build()?, 453 | Commands::Bump(args) => bump(args)?, 454 | Commands::UpdateTap => update_tap()?, 455 | Commands::K8s(args) => k8s::k8s(args)?, 456 | } 457 | 458 | Ok(()) 459 | } 460 | 461 | fn bump(args: BumpArgs) -> Result<()> { 462 | // Check for unstaged changes 463 | let status = command::get_trimmed_cmd_stdout("git", &["status", "--porcelain"], None)?; 464 | if !status.is_empty() { 465 | info!("There are unstaged changes:"); 466 | for line in status.lines() { 467 | info!(" {}", line); 468 | } 469 | info!("Do you want to stage these changes? (y/n)"); 470 | let mut input = String::new(); 471 | std::io::stdin().read_line(&mut input)?; 472 | if input.trim().to_lowercase() == "y" { 473 | run_command("git", &["add", "."], None)?; 474 | info!("Changes staged."); 475 | } 476 | } 477 | 478 | // Check for uncommitted changes 479 | let status = command::get_trimmed_cmd_stdout("git", &["status", "--short"], None)?; 480 | if !status.is_empty() { 481 | info!("There are uncommitted changes:"); 482 | for line in status.lines() { 483 | info!(" {}", line); 484 | } 485 | info!("Do you want to commit these changes? (y/n)"); 486 | let mut input = String::new(); 487 | std::io::stdin().read_line(&mut input)?; 488 | if input.trim().to_lowercase() == "y" { 489 | info!("Enter commit message:"); 490 | let mut message = String::new(); 491 | std::io::stdin().read_line(&mut message)?; 492 | run_command("git", &["commit", "-m", message.trim()], None)?; 493 | info!("Changes committed."); 494 | } 495 | } 496 | 497 | // Check for unpushed commits 498 | let unpushed = command::get_trimmed_cmd_stdout("git", &["log", "@{u}..", "--oneline"], None)?; 499 | if !unpushed.is_empty() { 500 | info!("There are unpushed commits:"); 501 | for line in unpushed.lines() { 502 | info!(" {}", line); 503 | } 504 | info!("Do you want to push these commits? (y/n)"); 505 | let mut input = String::new(); 506 | std::io::stdin().read_line(&mut input)?; 507 | if input.trim().to_lowercase() == "y" { 508 | run_command("git", &["push"], None)?; 509 | info!("Commits pushed."); 510 | } 511 | } 512 | 513 | // Fetch all tags 514 | run_command("git", &["fetch", "--tags"], None)?; 515 | info!("Fetched all tags from remote."); 516 | 517 | // Get all tags sorted by version (newest to oldest) 518 | let output = command::get_trimmed_cmd_stdout("git", &["tag", "--sort=-version:refname"], None)?; 519 | let tags: Vec = output.lines().map(String::from).collect(); 520 | 521 | if tags.is_empty() { 522 | return Err(eyre::eyre!("No tags found")); 523 | } 524 | 525 | let latest_tag = &tags[0]; 526 | info!("Latest tag: {}", latest_tag); 527 | 528 | // Parse the latest tag 529 | let latest_version = semver::Version::parse(latest_tag.trim_start_matches('v'))?; 530 | 531 | let patch_bump = Version { 532 | major: latest_version.major, 533 | minor: latest_version.minor, 534 | patch: latest_version.patch + 1, 535 | pre: Prerelease::EMPTY, 536 | build: BuildMetadata::EMPTY, 537 | }; 538 | let minor_bump = Version { 539 | major: latest_version.major, 540 | minor: latest_version.minor + 1, 541 | patch: 0, 542 | pre: Prerelease::EMPTY, 543 | build: BuildMetadata::EMPTY, 544 | }; 545 | let major_bump = Version { 546 | major: latest_version.major + 1, 547 | minor: 0, 548 | patch: 0, 549 | pre: Prerelease::EMPTY, 550 | build: BuildMetadata::EMPTY, 551 | }; 552 | 553 | let new_version = if let Some(bt) = args.bump_type { 554 | match bt { 555 | BumpType::Patch => patch_bump, 556 | BumpType::Minor => minor_bump, 557 | BumpType::Major => major_bump, 558 | } 559 | } else { 560 | // Ask user for bump type 561 | info!("Choose version bump type:"); 562 | info!( 563 | "1. Patch ({}.{}.{})", 564 | patch_bump.major, 565 | patch_bump.minor, 566 | patch_bump.patch.to_string().green() 567 | ); 568 | info!( 569 | "2. Minor ({}.{}.{})", 570 | minor_bump.major, 571 | minor_bump.minor.to_string().green(), 572 | minor_bump.patch.to_string().green() 573 | ); 574 | info!( 575 | "3. Major ({}.{}.{})", 576 | major_bump.major.to_string().green(), 577 | major_bump.minor.to_string().green(), 578 | major_bump.patch.to_string().green() 579 | ); 580 | 581 | let mut input = String::new(); 582 | std::io::stdin().read_line(&mut input)?; 583 | 584 | match input.trim() { 585 | "1" => patch_bump, 586 | "2" => minor_bump, 587 | "3" => major_bump, 588 | _ => return Err(eyre::eyre!("Invalid choice")), 589 | } 590 | }; 591 | 592 | let new_tag = format!("v{}", new_version); 593 | info!("Creating new tag: {}", new_tag); 594 | 595 | // Create and push the new tag 596 | run_command("git", &["tag", &new_tag], None)?; 597 | run_command("git", &["push", "origin", &new_tag], None)?; 598 | 599 | info!("Tag {} created and pushed successfully", new_tag); 600 | 601 | Ok(()) 602 | } 603 | 604 | fn print_banner() { 605 | let art = r#" __ __ 606 | / \.-"""-./ \ 607 | \ - - / ============ 608 | | o o | beardist 609 | \ .-'''-. / ============ 610 | '-\__Y__/-' 611 | `---`"#; 612 | for line in art.lines() { 613 | info!("{}", line.dimmed()); 614 | } 615 | } 616 | 617 | fn build() -> Result<()> { 618 | print_banner(); 619 | let start_time = std::time::Instant::now(); 620 | let config = load_config()?; 621 | let mut cx = BuildContext::new(config)?; 622 | 623 | info!( 624 | "📦 Building {}/{}", 625 | cx.config.org.blue(), 626 | cx.config.name.green(), 627 | ); 628 | 629 | system::print_sysinfo(); 630 | 631 | let cargo = cx 632 | .config 633 | .cargo 634 | .take() 635 | .map(|cc| CargoBuildContext::new(&cx, cc)) 636 | .transpose()?; 637 | 638 | let mut files_to_package: Vec = Vec::new(); 639 | 640 | let build_start = std::time::Instant::now(); 641 | if let Some(cargo) = cargo.as_ref() { 642 | cargo.build(&mut files_to_package)?; 643 | } 644 | 645 | if let Some(custom) = cx.config.custom.as_ref() { 646 | info!("📋 Executing custom build steps"); 647 | for (index, step) in custom.steps.iter().enumerate() { 648 | let step = step.iter().map(|s| s.as_str()).collect::>(); 649 | info!( 650 | "🔧 Running custom step {}: {}", 651 | index + 1, 652 | step.join(" ").cyan() 653 | ); 654 | run_command(step[0], &step[1..], None)?; 655 | } 656 | 657 | info!("📁 Adding custom files to package"); 658 | for file in &custom.files { 659 | let path = cx.source_dir.join(file); 660 | info!("➕ Adding file: {}", path.to_string().cyan()); 661 | files_to_package.push(PackagedFile { 662 | kind: PackagedFileKind::Misc, 663 | path, 664 | }); 665 | } 666 | } 667 | let build_time = build_start.elapsed().as_millis() as u64; 668 | info!("🔨 Built in {}", format!("{}ms", build_time).green()); 669 | 670 | info!("{}", "----------------------------------------".dimmed()); 671 | 672 | let package_file = cx.create_package_archive(&files_to_package)?; 673 | let archive_time = std::time::Instant::now().elapsed().as_millis() as u64; 674 | let file_content = fs_err::read(&package_file)?; 675 | let upload_start = std::time::Instant::now(); 676 | cx.upload_package(&package_file, &file_content, &files_to_package)?; 677 | let upload_time = upload_start.elapsed().as_millis() as u64; 678 | 679 | if let Some(cargo) = cargo.as_ref() { 680 | cargo.sweep()?; 681 | } 682 | 683 | let total_time = start_time.elapsed().as_millis(); 684 | info!( 685 | "📊 Summary: 🔨 Build: {}ms | 📦 Archive: {}ms{} | ⏱️ Total: {}ms", 686 | build_time.to_string().cyan(), 687 | archive_time.to_string().cyan(), 688 | if !cx.is_dry_run { 689 | format!(" | 📤 Upload: {}ms", upload_time.to_string().cyan()) 690 | } else { 691 | String::new() 692 | }, 693 | total_time.to_string().green() 694 | ); 695 | 696 | Ok(()) 697 | } 698 | 699 | fn load_config() -> Result { 700 | let config_path = fs_err::canonicalize(PathBuf::from(".beardist.json"))?; 701 | let config_str = fs_err::read_to_string(&config_path).wrap_err_with(|| { 702 | format!( 703 | "Failed to read config file at {}", 704 | config_path.display().to_string().cyan() 705 | ) 706 | })?; 707 | let config: Config = serde_json::from_str(&config_str).wrap_err_with(|| { 708 | format!( 709 | "Failed to parse config file at {}", 710 | config_path.display().to_string().cyan() 711 | ) 712 | })?; 713 | if config.version != CONFIG_VERSION { 714 | return Err(eyre::eyre!( 715 | "Invalid beardist config version: {}. Expected: {} (in file {})", 716 | config.version, 717 | CONFIG_VERSION, 718 | config_path.display().to_string().cyan() 719 | )); 720 | } 721 | Ok(config) 722 | } 723 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.18" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "is_terminal_polyfill", 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle" 46 | version = "1.0.10" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.6" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.1.2" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 64 | dependencies = [ 65 | "windows-sys 0.59.0", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "3.0.7" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 73 | dependencies = [ 74 | "anstyle", 75 | "once_cell", 76 | "windows-sys 0.59.0", 77 | ] 78 | 79 | [[package]] 80 | name = "atomic-waker" 81 | version = "1.1.2" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 84 | 85 | [[package]] 86 | name = "autocfg" 87 | version = "1.4.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 90 | 91 | [[package]] 92 | name = "backtrace" 93 | version = "0.3.71" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" 96 | dependencies = [ 97 | "addr2line", 98 | "cc", 99 | "cfg-if", 100 | "libc", 101 | "miniz_oxide", 102 | "object", 103 | "rustc-demangle", 104 | ] 105 | 106 | [[package]] 107 | name = "base64" 108 | version = "0.22.1" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 111 | 112 | [[package]] 113 | name = "beardist" 114 | version = "1.1.0" 115 | dependencies = [ 116 | "camino", 117 | "clap", 118 | "color-eyre", 119 | "convert_case", 120 | "env_logger", 121 | "eyre", 122 | "fs-err", 123 | "glob", 124 | "hostname", 125 | "ignore", 126 | "indexmap", 127 | "log", 128 | "owo-colors 4.2.0", 129 | "rand", 130 | "regex", 131 | "reqwest", 132 | "semver", 133 | "serde", 134 | "serde_json", 135 | "sha2", 136 | "sys-info", 137 | "tempfile", 138 | "timelord", 139 | "url", 140 | ] 141 | 142 | [[package]] 143 | name = "bincode" 144 | version = "2.0.1" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" 147 | dependencies = [ 148 | "bincode_derive", 149 | "serde", 150 | "unty", 151 | ] 152 | 153 | [[package]] 154 | name = "bincode_derive" 155 | version = "2.0.1" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" 158 | dependencies = [ 159 | "virtue", 160 | ] 161 | 162 | [[package]] 163 | name = "bitflags" 164 | version = "2.9.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 167 | 168 | [[package]] 169 | name = "block-buffer" 170 | version = "0.10.4" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 173 | dependencies = [ 174 | "generic-array", 175 | ] 176 | 177 | [[package]] 178 | name = "bstr" 179 | version = "1.11.3" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" 182 | dependencies = [ 183 | "memchr", 184 | "serde", 185 | ] 186 | 187 | [[package]] 188 | name = "bumpalo" 189 | version = "3.17.0" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 192 | 193 | [[package]] 194 | name = "bytes" 195 | version = "1.10.1" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 198 | 199 | [[package]] 200 | name = "camino" 201 | version = "1.1.9" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" 204 | dependencies = [ 205 | "serde", 206 | ] 207 | 208 | [[package]] 209 | name = "cc" 210 | version = "1.2.17" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" 213 | dependencies = [ 214 | "shlex", 215 | ] 216 | 217 | [[package]] 218 | name = "cfg-if" 219 | version = "1.0.0" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 222 | 223 | [[package]] 224 | name = "clap" 225 | version = "4.5.37" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" 228 | dependencies = [ 229 | "clap_builder", 230 | "clap_derive", 231 | ] 232 | 233 | [[package]] 234 | name = "clap_builder" 235 | version = "4.5.37" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" 238 | dependencies = [ 239 | "anstream", 240 | "anstyle", 241 | "clap_lex", 242 | "strsim", 243 | ] 244 | 245 | [[package]] 246 | name = "clap_derive" 247 | version = "4.5.32" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 250 | dependencies = [ 251 | "heck", 252 | "proc-macro2", 253 | "quote", 254 | "syn", 255 | ] 256 | 257 | [[package]] 258 | name = "clap_lex" 259 | version = "0.7.4" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 262 | 263 | [[package]] 264 | name = "color-eyre" 265 | version = "0.6.3" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" 268 | dependencies = [ 269 | "backtrace", 270 | "color-spantrace", 271 | "eyre", 272 | "indenter", 273 | "once_cell", 274 | "owo-colors 3.5.0", 275 | "tracing-error", 276 | ] 277 | 278 | [[package]] 279 | name = "color-spantrace" 280 | version = "0.2.1" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" 283 | dependencies = [ 284 | "once_cell", 285 | "owo-colors 3.5.0", 286 | "tracing-core", 287 | "tracing-error", 288 | ] 289 | 290 | [[package]] 291 | name = "colorchoice" 292 | version = "1.0.3" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 295 | 296 | [[package]] 297 | name = "convert_case" 298 | version = "0.8.0" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" 301 | dependencies = [ 302 | "unicode-segmentation", 303 | ] 304 | 305 | [[package]] 306 | name = "core-foundation" 307 | version = "0.9.4" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 310 | dependencies = [ 311 | "core-foundation-sys", 312 | "libc", 313 | ] 314 | 315 | [[package]] 316 | name = "core-foundation-sys" 317 | version = "0.8.7" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 320 | 321 | [[package]] 322 | name = "cpufeatures" 323 | version = "0.2.17" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 326 | dependencies = [ 327 | "libc", 328 | ] 329 | 330 | [[package]] 331 | name = "crossbeam-deque" 332 | version = "0.8.6" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 335 | dependencies = [ 336 | "crossbeam-epoch", 337 | "crossbeam-utils", 338 | ] 339 | 340 | [[package]] 341 | name = "crossbeam-epoch" 342 | version = "0.9.18" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 345 | dependencies = [ 346 | "crossbeam-utils", 347 | ] 348 | 349 | [[package]] 350 | name = "crossbeam-utils" 351 | version = "0.8.21" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 354 | 355 | [[package]] 356 | name = "crypto-common" 357 | version = "0.1.6" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 360 | dependencies = [ 361 | "generic-array", 362 | "typenum", 363 | ] 364 | 365 | [[package]] 366 | name = "digest" 367 | version = "0.10.7" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 370 | dependencies = [ 371 | "block-buffer", 372 | "crypto-common", 373 | ] 374 | 375 | [[package]] 376 | name = "displaydoc" 377 | version = "0.2.5" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 380 | dependencies = [ 381 | "proc-macro2", 382 | "quote", 383 | "syn", 384 | ] 385 | 386 | [[package]] 387 | name = "either" 388 | version = "1.15.0" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 391 | 392 | [[package]] 393 | name = "encoding_rs" 394 | version = "0.8.35" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 397 | dependencies = [ 398 | "cfg-if", 399 | ] 400 | 401 | [[package]] 402 | name = "env_filter" 403 | version = "0.1.3" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 406 | dependencies = [ 407 | "log", 408 | "regex", 409 | ] 410 | 411 | [[package]] 412 | name = "env_logger" 413 | version = "0.11.8" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 416 | dependencies = [ 417 | "anstream", 418 | "anstyle", 419 | "env_filter", 420 | "jiff", 421 | "log", 422 | ] 423 | 424 | [[package]] 425 | name = "equivalent" 426 | version = "1.0.2" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 429 | 430 | [[package]] 431 | name = "errno" 432 | version = "0.3.10" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 435 | dependencies = [ 436 | "libc", 437 | "windows-sys 0.59.0", 438 | ] 439 | 440 | [[package]] 441 | name = "eyre" 442 | version = "0.6.12" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" 445 | dependencies = [ 446 | "indenter", 447 | "once_cell", 448 | ] 449 | 450 | [[package]] 451 | name = "fastrand" 452 | version = "2.3.0" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 455 | 456 | [[package]] 457 | name = "fnv" 458 | version = "1.0.7" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 461 | 462 | [[package]] 463 | name = "foreign-types" 464 | version = "0.3.2" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 467 | dependencies = [ 468 | "foreign-types-shared", 469 | ] 470 | 471 | [[package]] 472 | name = "foreign-types-shared" 473 | version = "0.1.1" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 476 | 477 | [[package]] 478 | name = "form_urlencoded" 479 | version = "1.2.1" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 482 | dependencies = [ 483 | "percent-encoding", 484 | ] 485 | 486 | [[package]] 487 | name = "fs-err" 488 | version = "3.1.0" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "1f89bda4c2a21204059a977ed3bfe746677dfd137b83c339e702b0ac91d482aa" 491 | dependencies = [ 492 | "autocfg", 493 | ] 494 | 495 | [[package]] 496 | name = "futures-channel" 497 | version = "0.3.31" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 500 | dependencies = [ 501 | "futures-core", 502 | "futures-sink", 503 | ] 504 | 505 | [[package]] 506 | name = "futures-core" 507 | version = "0.3.31" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 510 | 511 | [[package]] 512 | name = "futures-io" 513 | version = "0.3.31" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 516 | 517 | [[package]] 518 | name = "futures-sink" 519 | version = "0.3.31" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 522 | 523 | [[package]] 524 | name = "futures-task" 525 | version = "0.3.31" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 528 | 529 | [[package]] 530 | name = "futures-util" 531 | version = "0.3.31" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 534 | dependencies = [ 535 | "futures-core", 536 | "futures-io", 537 | "futures-sink", 538 | "futures-task", 539 | "memchr", 540 | "pin-project-lite", 541 | "pin-utils", 542 | "slab", 543 | ] 544 | 545 | [[package]] 546 | name = "generic-array" 547 | version = "0.14.7" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 550 | dependencies = [ 551 | "typenum", 552 | "version_check", 553 | ] 554 | 555 | [[package]] 556 | name = "getrandom" 557 | version = "0.2.15" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 560 | dependencies = [ 561 | "cfg-if", 562 | "libc", 563 | "wasi 0.11.0+wasi-snapshot-preview1", 564 | ] 565 | 566 | [[package]] 567 | name = "getrandom" 568 | version = "0.3.2" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 571 | dependencies = [ 572 | "cfg-if", 573 | "libc", 574 | "r-efi", 575 | "wasi 0.14.2+wasi-0.2.4", 576 | ] 577 | 578 | [[package]] 579 | name = "gimli" 580 | version = "0.28.1" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 583 | 584 | [[package]] 585 | name = "glob" 586 | version = "0.3.2" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 589 | 590 | [[package]] 591 | name = "globset" 592 | version = "0.4.16" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" 595 | dependencies = [ 596 | "aho-corasick", 597 | "bstr", 598 | "log", 599 | "regex-automata", 600 | "regex-syntax", 601 | ] 602 | 603 | [[package]] 604 | name = "h2" 605 | version = "0.4.8" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" 608 | dependencies = [ 609 | "atomic-waker", 610 | "bytes", 611 | "fnv", 612 | "futures-core", 613 | "futures-sink", 614 | "http", 615 | "indexmap", 616 | "slab", 617 | "tokio", 618 | "tokio-util", 619 | "tracing", 620 | ] 621 | 622 | [[package]] 623 | name = "hashbrown" 624 | version = "0.15.2" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 627 | 628 | [[package]] 629 | name = "heck" 630 | version = "0.5.0" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 633 | 634 | [[package]] 635 | name = "hostname" 636 | version = "0.4.1" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" 639 | dependencies = [ 640 | "cfg-if", 641 | "libc", 642 | "windows-link", 643 | ] 644 | 645 | [[package]] 646 | name = "http" 647 | version = "1.3.1" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 650 | dependencies = [ 651 | "bytes", 652 | "fnv", 653 | "itoa", 654 | ] 655 | 656 | [[package]] 657 | name = "http-body" 658 | version = "1.0.1" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 661 | dependencies = [ 662 | "bytes", 663 | "http", 664 | ] 665 | 666 | [[package]] 667 | name = "http-body-util" 668 | version = "0.1.3" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 671 | dependencies = [ 672 | "bytes", 673 | "futures-core", 674 | "http", 675 | "http-body", 676 | "pin-project-lite", 677 | ] 678 | 679 | [[package]] 680 | name = "httparse" 681 | version = "1.10.1" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 684 | 685 | [[package]] 686 | name = "human_bytes" 687 | version = "0.4.3" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "91f255a4535024abf7640cb288260811fc14794f62b063652ed349f9a6c2348e" 690 | 691 | [[package]] 692 | name = "humantime" 693 | version = "2.2.0" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" 696 | 697 | [[package]] 698 | name = "hyper" 699 | version = "1.6.0" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 702 | dependencies = [ 703 | "bytes", 704 | "futures-channel", 705 | "futures-util", 706 | "h2", 707 | "http", 708 | "http-body", 709 | "httparse", 710 | "itoa", 711 | "pin-project-lite", 712 | "smallvec", 713 | "tokio", 714 | "want", 715 | ] 716 | 717 | [[package]] 718 | name = "hyper-rustls" 719 | version = "0.27.5" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" 722 | dependencies = [ 723 | "futures-util", 724 | "http", 725 | "hyper", 726 | "hyper-util", 727 | "rustls", 728 | "rustls-pki-types", 729 | "tokio", 730 | "tokio-rustls", 731 | "tower-service", 732 | ] 733 | 734 | [[package]] 735 | name = "hyper-tls" 736 | version = "0.6.0" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 739 | dependencies = [ 740 | "bytes", 741 | "http-body-util", 742 | "hyper", 743 | "hyper-util", 744 | "native-tls", 745 | "tokio", 746 | "tokio-native-tls", 747 | "tower-service", 748 | ] 749 | 750 | [[package]] 751 | name = "hyper-util" 752 | version = "0.1.10" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" 755 | dependencies = [ 756 | "bytes", 757 | "futures-channel", 758 | "futures-util", 759 | "http", 760 | "http-body", 761 | "hyper", 762 | "pin-project-lite", 763 | "socket2", 764 | "tokio", 765 | "tower-service", 766 | "tracing", 767 | ] 768 | 769 | [[package]] 770 | name = "icu_collections" 771 | version = "1.5.0" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 774 | dependencies = [ 775 | "displaydoc", 776 | "yoke", 777 | "zerofrom", 778 | "zerovec", 779 | ] 780 | 781 | [[package]] 782 | name = "icu_locid" 783 | version = "1.5.0" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 786 | dependencies = [ 787 | "displaydoc", 788 | "litemap", 789 | "tinystr", 790 | "writeable", 791 | "zerovec", 792 | ] 793 | 794 | [[package]] 795 | name = "icu_locid_transform" 796 | version = "1.5.0" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 799 | dependencies = [ 800 | "displaydoc", 801 | "icu_locid", 802 | "icu_locid_transform_data", 803 | "icu_provider", 804 | "tinystr", 805 | "zerovec", 806 | ] 807 | 808 | [[package]] 809 | name = "icu_locid_transform_data" 810 | version = "1.5.0" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 813 | 814 | [[package]] 815 | name = "icu_normalizer" 816 | version = "1.5.0" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 819 | dependencies = [ 820 | "displaydoc", 821 | "icu_collections", 822 | "icu_normalizer_data", 823 | "icu_properties", 824 | "icu_provider", 825 | "smallvec", 826 | "utf16_iter", 827 | "utf8_iter", 828 | "write16", 829 | "zerovec", 830 | ] 831 | 832 | [[package]] 833 | name = "icu_normalizer_data" 834 | version = "1.5.0" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 837 | 838 | [[package]] 839 | name = "icu_properties" 840 | version = "1.5.1" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 843 | dependencies = [ 844 | "displaydoc", 845 | "icu_collections", 846 | "icu_locid_transform", 847 | "icu_properties_data", 848 | "icu_provider", 849 | "tinystr", 850 | "zerovec", 851 | ] 852 | 853 | [[package]] 854 | name = "icu_properties_data" 855 | version = "1.5.0" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 858 | 859 | [[package]] 860 | name = "icu_provider" 861 | version = "1.5.0" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 864 | dependencies = [ 865 | "displaydoc", 866 | "icu_locid", 867 | "icu_provider_macros", 868 | "stable_deref_trait", 869 | "tinystr", 870 | "writeable", 871 | "yoke", 872 | "zerofrom", 873 | "zerovec", 874 | ] 875 | 876 | [[package]] 877 | name = "icu_provider_macros" 878 | version = "1.5.0" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 881 | dependencies = [ 882 | "proc-macro2", 883 | "quote", 884 | "syn", 885 | ] 886 | 887 | [[package]] 888 | name = "idna" 889 | version = "1.0.3" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 892 | dependencies = [ 893 | "idna_adapter", 894 | "smallvec", 895 | "utf8_iter", 896 | ] 897 | 898 | [[package]] 899 | name = "idna_adapter" 900 | version = "1.2.0" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 903 | dependencies = [ 904 | "icu_normalizer", 905 | "icu_properties", 906 | ] 907 | 908 | [[package]] 909 | name = "ignore" 910 | version = "0.4.23" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" 913 | dependencies = [ 914 | "crossbeam-deque", 915 | "globset", 916 | "log", 917 | "memchr", 918 | "regex-automata", 919 | "same-file", 920 | "walkdir", 921 | "winapi-util", 922 | ] 923 | 924 | [[package]] 925 | name = "indenter" 926 | version = "0.3.3" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 929 | 930 | [[package]] 931 | name = "indexmap" 932 | version = "2.9.0" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 935 | dependencies = [ 936 | "equivalent", 937 | "hashbrown", 938 | ] 939 | 940 | [[package]] 941 | name = "ipnet" 942 | version = "2.11.0" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 945 | 946 | [[package]] 947 | name = "is_terminal_polyfill" 948 | version = "1.70.1" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 951 | 952 | [[package]] 953 | name = "itoa" 954 | version = "1.0.15" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 957 | 958 | [[package]] 959 | name = "jiff" 960 | version = "0.2.4" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "d699bc6dfc879fb1bf9bdff0d4c56f0884fc6f0d0eb0fba397a6d00cd9a6b85e" 963 | dependencies = [ 964 | "jiff-static", 965 | "jiff-tzdb-platform", 966 | "log", 967 | "portable-atomic", 968 | "portable-atomic-util", 969 | "serde", 970 | "windows-sys 0.59.0", 971 | ] 972 | 973 | [[package]] 974 | name = "jiff-static" 975 | version = "0.2.4" 976 | source = "registry+https://github.com/rust-lang/crates.io-index" 977 | checksum = "8d16e75759ee0aa64c57a56acbf43916987b20c77373cb7e808979e02b93c9f9" 978 | dependencies = [ 979 | "proc-macro2", 980 | "quote", 981 | "syn", 982 | ] 983 | 984 | [[package]] 985 | name = "jiff-tzdb" 986 | version = "0.1.3" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "962e1dfe9b2d75a84536cf5bf5eaaa4319aa7906c7160134a22883ac316d5f31" 989 | 990 | [[package]] 991 | name = "jiff-tzdb-platform" 992 | version = "0.1.2" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "a63c62e404e7b92979d2792352d885a7f8f83fd1d0d31eea582d77b2ceca697e" 995 | dependencies = [ 996 | "jiff-tzdb", 997 | ] 998 | 999 | [[package]] 1000 | name = "js-sys" 1001 | version = "0.3.77" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 1004 | dependencies = [ 1005 | "once_cell", 1006 | "wasm-bindgen", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "lazy_static" 1011 | version = "1.5.0" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1014 | 1015 | [[package]] 1016 | name = "libc" 1017 | version = "0.2.171" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 1020 | 1021 | [[package]] 1022 | name = "linux-raw-sys" 1023 | version = "0.9.3" 1024 | source = "registry+https://github.com/rust-lang/crates.io-index" 1025 | checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" 1026 | 1027 | [[package]] 1028 | name = "litemap" 1029 | version = "0.7.5" 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" 1031 | checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" 1032 | 1033 | [[package]] 1034 | name = "log" 1035 | version = "0.4.27" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 1038 | 1039 | [[package]] 1040 | name = "memchr" 1041 | version = "2.7.4" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 1044 | 1045 | [[package]] 1046 | name = "mime" 1047 | version = "0.3.17" 1048 | source = "registry+https://github.com/rust-lang/crates.io-index" 1049 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1050 | 1051 | [[package]] 1052 | name = "miniz_oxide" 1053 | version = "0.7.4" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 1056 | dependencies = [ 1057 | "adler", 1058 | ] 1059 | 1060 | [[package]] 1061 | name = "mio" 1062 | version = "1.0.3" 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" 1064 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 1065 | dependencies = [ 1066 | "libc", 1067 | "wasi 0.11.0+wasi-snapshot-preview1", 1068 | "windows-sys 0.52.0", 1069 | ] 1070 | 1071 | [[package]] 1072 | name = "native-tls" 1073 | version = "0.2.14" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 1076 | dependencies = [ 1077 | "libc", 1078 | "log", 1079 | "openssl", 1080 | "openssl-probe", 1081 | "openssl-sys", 1082 | "schannel", 1083 | "security-framework", 1084 | "security-framework-sys", 1085 | "tempfile", 1086 | ] 1087 | 1088 | [[package]] 1089 | name = "object" 1090 | version = "0.32.2" 1091 | source = "registry+https://github.com/rust-lang/crates.io-index" 1092 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 1093 | dependencies = [ 1094 | "memchr", 1095 | ] 1096 | 1097 | [[package]] 1098 | name = "once_cell" 1099 | version = "1.21.1" 1100 | source = "registry+https://github.com/rust-lang/crates.io-index" 1101 | checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" 1102 | 1103 | [[package]] 1104 | name = "openssl" 1105 | version = "0.10.71" 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" 1107 | checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" 1108 | dependencies = [ 1109 | "bitflags", 1110 | "cfg-if", 1111 | "foreign-types", 1112 | "libc", 1113 | "once_cell", 1114 | "openssl-macros", 1115 | "openssl-sys", 1116 | ] 1117 | 1118 | [[package]] 1119 | name = "openssl-macros" 1120 | version = "0.1.1" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 1123 | dependencies = [ 1124 | "proc-macro2", 1125 | "quote", 1126 | "syn", 1127 | ] 1128 | 1129 | [[package]] 1130 | name = "openssl-probe" 1131 | version = "0.1.6" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 1134 | 1135 | [[package]] 1136 | name = "openssl-sys" 1137 | version = "0.9.106" 1138 | source = "registry+https://github.com/rust-lang/crates.io-index" 1139 | checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" 1140 | dependencies = [ 1141 | "cc", 1142 | "libc", 1143 | "pkg-config", 1144 | "vcpkg", 1145 | ] 1146 | 1147 | [[package]] 1148 | name = "owo-colors" 1149 | version = "3.5.0" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" 1152 | 1153 | [[package]] 1154 | name = "owo-colors" 1155 | version = "4.2.0" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "1036865bb9422d3300cf723f657c2851d0e9ab12567854b1f4eba3d77decf564" 1158 | 1159 | [[package]] 1160 | name = "percent-encoding" 1161 | version = "2.3.1" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1164 | 1165 | [[package]] 1166 | name = "pin-project-lite" 1167 | version = "0.2.16" 1168 | source = "registry+https://github.com/rust-lang/crates.io-index" 1169 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1170 | 1171 | [[package]] 1172 | name = "pin-utils" 1173 | version = "0.1.0" 1174 | source = "registry+https://github.com/rust-lang/crates.io-index" 1175 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1176 | 1177 | [[package]] 1178 | name = "pkg-config" 1179 | version = "0.3.32" 1180 | source = "registry+https://github.com/rust-lang/crates.io-index" 1181 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1182 | 1183 | [[package]] 1184 | name = "portable-atomic" 1185 | version = "1.11.0" 1186 | source = "registry+https://github.com/rust-lang/crates.io-index" 1187 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 1188 | 1189 | [[package]] 1190 | name = "portable-atomic-util" 1191 | version = "0.2.4" 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" 1193 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 1194 | dependencies = [ 1195 | "portable-atomic", 1196 | ] 1197 | 1198 | [[package]] 1199 | name = "ppv-lite86" 1200 | version = "0.2.21" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1203 | dependencies = [ 1204 | "zerocopy", 1205 | ] 1206 | 1207 | [[package]] 1208 | name = "proc-macro2" 1209 | version = "1.0.94" 1210 | source = "registry+https://github.com/rust-lang/crates.io-index" 1211 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 1212 | dependencies = [ 1213 | "unicode-ident", 1214 | ] 1215 | 1216 | [[package]] 1217 | name = "quote" 1218 | version = "1.0.40" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 1221 | dependencies = [ 1222 | "proc-macro2", 1223 | ] 1224 | 1225 | [[package]] 1226 | name = "r-efi" 1227 | version = "5.2.0" 1228 | source = "registry+https://github.com/rust-lang/crates.io-index" 1229 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 1230 | 1231 | [[package]] 1232 | name = "rand" 1233 | version = "0.9.1" 1234 | source = "registry+https://github.com/rust-lang/crates.io-index" 1235 | checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" 1236 | dependencies = [ 1237 | "rand_chacha", 1238 | "rand_core", 1239 | ] 1240 | 1241 | [[package]] 1242 | name = "rand_chacha" 1243 | version = "0.9.0" 1244 | source = "registry+https://github.com/rust-lang/crates.io-index" 1245 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 1246 | dependencies = [ 1247 | "ppv-lite86", 1248 | "rand_core", 1249 | ] 1250 | 1251 | [[package]] 1252 | name = "rand_core" 1253 | version = "0.9.3" 1254 | source = "registry+https://github.com/rust-lang/crates.io-index" 1255 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 1256 | dependencies = [ 1257 | "getrandom 0.3.2", 1258 | ] 1259 | 1260 | [[package]] 1261 | name = "rayon" 1262 | version = "1.10.0" 1263 | source = "registry+https://github.com/rust-lang/crates.io-index" 1264 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 1265 | dependencies = [ 1266 | "either", 1267 | "rayon-core", 1268 | ] 1269 | 1270 | [[package]] 1271 | name = "rayon-core" 1272 | version = "1.12.1" 1273 | source = "registry+https://github.com/rust-lang/crates.io-index" 1274 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 1275 | dependencies = [ 1276 | "crossbeam-deque", 1277 | "crossbeam-utils", 1278 | ] 1279 | 1280 | [[package]] 1281 | name = "regex" 1282 | version = "1.11.1" 1283 | source = "registry+https://github.com/rust-lang/crates.io-index" 1284 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1285 | dependencies = [ 1286 | "aho-corasick", 1287 | "memchr", 1288 | "regex-automata", 1289 | "regex-syntax", 1290 | ] 1291 | 1292 | [[package]] 1293 | name = "regex-automata" 1294 | version = "0.4.9" 1295 | source = "registry+https://github.com/rust-lang/crates.io-index" 1296 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1297 | dependencies = [ 1298 | "aho-corasick", 1299 | "memchr", 1300 | "regex-syntax", 1301 | ] 1302 | 1303 | [[package]] 1304 | name = "regex-syntax" 1305 | version = "0.8.5" 1306 | source = "registry+https://github.com/rust-lang/crates.io-index" 1307 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1308 | 1309 | [[package]] 1310 | name = "reqwest" 1311 | version = "0.12.15" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" 1314 | dependencies = [ 1315 | "base64", 1316 | "bytes", 1317 | "encoding_rs", 1318 | "futures-channel", 1319 | "futures-core", 1320 | "futures-util", 1321 | "h2", 1322 | "http", 1323 | "http-body", 1324 | "http-body-util", 1325 | "hyper", 1326 | "hyper-rustls", 1327 | "hyper-tls", 1328 | "hyper-util", 1329 | "ipnet", 1330 | "js-sys", 1331 | "log", 1332 | "mime", 1333 | "native-tls", 1334 | "once_cell", 1335 | "percent-encoding", 1336 | "pin-project-lite", 1337 | "rustls-pemfile", 1338 | "serde", 1339 | "serde_json", 1340 | "serde_urlencoded", 1341 | "sync_wrapper", 1342 | "system-configuration", 1343 | "tokio", 1344 | "tokio-native-tls", 1345 | "tower", 1346 | "tower-service", 1347 | "url", 1348 | "wasm-bindgen", 1349 | "wasm-bindgen-futures", 1350 | "web-sys", 1351 | "windows-registry", 1352 | ] 1353 | 1354 | [[package]] 1355 | name = "ring" 1356 | version = "0.17.14" 1357 | source = "registry+https://github.com/rust-lang/crates.io-index" 1358 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 1359 | dependencies = [ 1360 | "cc", 1361 | "cfg-if", 1362 | "getrandom 0.2.15", 1363 | "libc", 1364 | "untrusted", 1365 | "windows-sys 0.52.0", 1366 | ] 1367 | 1368 | [[package]] 1369 | name = "rustc-demangle" 1370 | version = "0.1.24" 1371 | source = "registry+https://github.com/rust-lang/crates.io-index" 1372 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1373 | 1374 | [[package]] 1375 | name = "rustix" 1376 | version = "1.0.3" 1377 | source = "registry+https://github.com/rust-lang/crates.io-index" 1378 | checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" 1379 | dependencies = [ 1380 | "bitflags", 1381 | "errno", 1382 | "libc", 1383 | "linux-raw-sys", 1384 | "windows-sys 0.59.0", 1385 | ] 1386 | 1387 | [[package]] 1388 | name = "rustls" 1389 | version = "0.23.25" 1390 | source = "registry+https://github.com/rust-lang/crates.io-index" 1391 | checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" 1392 | dependencies = [ 1393 | "once_cell", 1394 | "rustls-pki-types", 1395 | "rustls-webpki", 1396 | "subtle", 1397 | "zeroize", 1398 | ] 1399 | 1400 | [[package]] 1401 | name = "rustls-pemfile" 1402 | version = "2.2.0" 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" 1404 | checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" 1405 | dependencies = [ 1406 | "rustls-pki-types", 1407 | ] 1408 | 1409 | [[package]] 1410 | name = "rustls-pki-types" 1411 | version = "1.11.0" 1412 | source = "registry+https://github.com/rust-lang/crates.io-index" 1413 | checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" 1414 | 1415 | [[package]] 1416 | name = "rustls-webpki" 1417 | version = "0.103.0" 1418 | source = "registry+https://github.com/rust-lang/crates.io-index" 1419 | checksum = "0aa4eeac2588ffff23e9d7a7e9b3f971c5fb5b7ebc9452745e0c232c64f83b2f" 1420 | dependencies = [ 1421 | "ring", 1422 | "rustls-pki-types", 1423 | "untrusted", 1424 | ] 1425 | 1426 | [[package]] 1427 | name = "rustversion" 1428 | version = "1.0.20" 1429 | source = "registry+https://github.com/rust-lang/crates.io-index" 1430 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 1431 | 1432 | [[package]] 1433 | name = "ryu" 1434 | version = "1.0.20" 1435 | source = "registry+https://github.com/rust-lang/crates.io-index" 1436 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1437 | 1438 | [[package]] 1439 | name = "same-file" 1440 | version = "1.0.6" 1441 | source = "registry+https://github.com/rust-lang/crates.io-index" 1442 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1443 | dependencies = [ 1444 | "winapi-util", 1445 | ] 1446 | 1447 | [[package]] 1448 | name = "schannel" 1449 | version = "0.1.27" 1450 | source = "registry+https://github.com/rust-lang/crates.io-index" 1451 | checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 1452 | dependencies = [ 1453 | "windows-sys 0.59.0", 1454 | ] 1455 | 1456 | [[package]] 1457 | name = "seahash" 1458 | version = "4.1.0" 1459 | source = "registry+https://github.com/rust-lang/crates.io-index" 1460 | checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" 1461 | 1462 | [[package]] 1463 | name = "security-framework" 1464 | version = "2.11.1" 1465 | source = "registry+https://github.com/rust-lang/crates.io-index" 1466 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1467 | dependencies = [ 1468 | "bitflags", 1469 | "core-foundation", 1470 | "core-foundation-sys", 1471 | "libc", 1472 | "security-framework-sys", 1473 | ] 1474 | 1475 | [[package]] 1476 | name = "security-framework-sys" 1477 | version = "2.14.0" 1478 | source = "registry+https://github.com/rust-lang/crates.io-index" 1479 | checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 1480 | dependencies = [ 1481 | "core-foundation-sys", 1482 | "libc", 1483 | ] 1484 | 1485 | [[package]] 1486 | name = "semver" 1487 | version = "1.0.26" 1488 | source = "registry+https://github.com/rust-lang/crates.io-index" 1489 | checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" 1490 | 1491 | [[package]] 1492 | name = "serde" 1493 | version = "1.0.219" 1494 | source = "registry+https://github.com/rust-lang/crates.io-index" 1495 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1496 | dependencies = [ 1497 | "serde_derive", 1498 | ] 1499 | 1500 | [[package]] 1501 | name = "serde_derive" 1502 | version = "1.0.219" 1503 | source = "registry+https://github.com/rust-lang/crates.io-index" 1504 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1505 | dependencies = [ 1506 | "proc-macro2", 1507 | "quote", 1508 | "syn", 1509 | ] 1510 | 1511 | [[package]] 1512 | name = "serde_json" 1513 | version = "1.0.140" 1514 | source = "registry+https://github.com/rust-lang/crates.io-index" 1515 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 1516 | dependencies = [ 1517 | "itoa", 1518 | "memchr", 1519 | "ryu", 1520 | "serde", 1521 | ] 1522 | 1523 | [[package]] 1524 | name = "serde_urlencoded" 1525 | version = "0.7.1" 1526 | source = "registry+https://github.com/rust-lang/crates.io-index" 1527 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1528 | dependencies = [ 1529 | "form_urlencoded", 1530 | "itoa", 1531 | "ryu", 1532 | "serde", 1533 | ] 1534 | 1535 | [[package]] 1536 | name = "sha2" 1537 | version = "0.10.8" 1538 | source = "registry+https://github.com/rust-lang/crates.io-index" 1539 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 1540 | dependencies = [ 1541 | "cfg-if", 1542 | "cpufeatures", 1543 | "digest", 1544 | ] 1545 | 1546 | [[package]] 1547 | name = "sharded-slab" 1548 | version = "0.1.7" 1549 | source = "registry+https://github.com/rust-lang/crates.io-index" 1550 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 1551 | dependencies = [ 1552 | "lazy_static", 1553 | ] 1554 | 1555 | [[package]] 1556 | name = "shlex" 1557 | version = "1.3.0" 1558 | source = "registry+https://github.com/rust-lang/crates.io-index" 1559 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1560 | 1561 | [[package]] 1562 | name = "slab" 1563 | version = "0.4.9" 1564 | source = "registry+https://github.com/rust-lang/crates.io-index" 1565 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1566 | dependencies = [ 1567 | "autocfg", 1568 | ] 1569 | 1570 | [[package]] 1571 | name = "smallvec" 1572 | version = "1.14.0" 1573 | source = "registry+https://github.com/rust-lang/crates.io-index" 1574 | checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" 1575 | 1576 | [[package]] 1577 | name = "socket2" 1578 | version = "0.5.8" 1579 | source = "registry+https://github.com/rust-lang/crates.io-index" 1580 | checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" 1581 | dependencies = [ 1582 | "libc", 1583 | "windows-sys 0.52.0", 1584 | ] 1585 | 1586 | [[package]] 1587 | name = "stable_deref_trait" 1588 | version = "1.2.0" 1589 | source = "registry+https://github.com/rust-lang/crates.io-index" 1590 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1591 | 1592 | [[package]] 1593 | name = "strsim" 1594 | version = "0.11.1" 1595 | source = "registry+https://github.com/rust-lang/crates.io-index" 1596 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1597 | 1598 | [[package]] 1599 | name = "subtle" 1600 | version = "2.6.1" 1601 | source = "registry+https://github.com/rust-lang/crates.io-index" 1602 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1603 | 1604 | [[package]] 1605 | name = "syn" 1606 | version = "2.0.100" 1607 | source = "registry+https://github.com/rust-lang/crates.io-index" 1608 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 1609 | dependencies = [ 1610 | "proc-macro2", 1611 | "quote", 1612 | "unicode-ident", 1613 | ] 1614 | 1615 | [[package]] 1616 | name = "sync_wrapper" 1617 | version = "1.0.2" 1618 | source = "registry+https://github.com/rust-lang/crates.io-index" 1619 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1620 | dependencies = [ 1621 | "futures-core", 1622 | ] 1623 | 1624 | [[package]] 1625 | name = "synstructure" 1626 | version = "0.13.1" 1627 | source = "registry+https://github.com/rust-lang/crates.io-index" 1628 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1629 | dependencies = [ 1630 | "proc-macro2", 1631 | "quote", 1632 | "syn", 1633 | ] 1634 | 1635 | [[package]] 1636 | name = "sys-info" 1637 | version = "0.9.1" 1638 | source = "registry+https://github.com/rust-lang/crates.io-index" 1639 | checksum = "0b3a0d0aba8bf96a0e1ddfdc352fc53b3df7f39318c71854910c3c4b024ae52c" 1640 | dependencies = [ 1641 | "cc", 1642 | "libc", 1643 | ] 1644 | 1645 | [[package]] 1646 | name = "system-configuration" 1647 | version = "0.6.1" 1648 | source = "registry+https://github.com/rust-lang/crates.io-index" 1649 | checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 1650 | dependencies = [ 1651 | "bitflags", 1652 | "core-foundation", 1653 | "system-configuration-sys", 1654 | ] 1655 | 1656 | [[package]] 1657 | name = "system-configuration-sys" 1658 | version = "0.6.0" 1659 | source = "registry+https://github.com/rust-lang/crates.io-index" 1660 | checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 1661 | dependencies = [ 1662 | "core-foundation-sys", 1663 | "libc", 1664 | ] 1665 | 1666 | [[package]] 1667 | name = "tempfile" 1668 | version = "3.19.1" 1669 | source = "registry+https://github.com/rust-lang/crates.io-index" 1670 | checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" 1671 | dependencies = [ 1672 | "fastrand", 1673 | "getrandom 0.3.2", 1674 | "once_cell", 1675 | "rustix", 1676 | "windows-sys 0.59.0", 1677 | ] 1678 | 1679 | [[package]] 1680 | name = "thread_local" 1681 | version = "1.1.8" 1682 | source = "registry+https://github.com/rust-lang/crates.io-index" 1683 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 1684 | dependencies = [ 1685 | "cfg-if", 1686 | "once_cell", 1687 | ] 1688 | 1689 | [[package]] 1690 | name = "timelord" 1691 | version = "3.0.2" 1692 | source = "registry+https://github.com/rust-lang/crates.io-index" 1693 | checksum = "a7cb2f1346281bcbbf98bedfc3fe752d3f80baafb785aae13209868c6f50dc65" 1694 | dependencies = [ 1695 | "bincode", 1696 | "camino", 1697 | "hostname", 1698 | "human_bytes", 1699 | "humantime", 1700 | "ignore", 1701 | "jiff", 1702 | "log", 1703 | "owo-colors 4.2.0", 1704 | "rayon", 1705 | "seahash", 1706 | "serde", 1707 | ] 1708 | 1709 | [[package]] 1710 | name = "tinystr" 1711 | version = "0.7.6" 1712 | source = "registry+https://github.com/rust-lang/crates.io-index" 1713 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1714 | dependencies = [ 1715 | "displaydoc", 1716 | "zerovec", 1717 | ] 1718 | 1719 | [[package]] 1720 | name = "tokio" 1721 | version = "1.44.1" 1722 | source = "registry+https://github.com/rust-lang/crates.io-index" 1723 | checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" 1724 | dependencies = [ 1725 | "backtrace", 1726 | "bytes", 1727 | "libc", 1728 | "mio", 1729 | "pin-project-lite", 1730 | "socket2", 1731 | "windows-sys 0.52.0", 1732 | ] 1733 | 1734 | [[package]] 1735 | name = "tokio-native-tls" 1736 | version = "0.3.1" 1737 | source = "registry+https://github.com/rust-lang/crates.io-index" 1738 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1739 | dependencies = [ 1740 | "native-tls", 1741 | "tokio", 1742 | ] 1743 | 1744 | [[package]] 1745 | name = "tokio-rustls" 1746 | version = "0.26.2" 1747 | source = "registry+https://github.com/rust-lang/crates.io-index" 1748 | checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 1749 | dependencies = [ 1750 | "rustls", 1751 | "tokio", 1752 | ] 1753 | 1754 | [[package]] 1755 | name = "tokio-util" 1756 | version = "0.7.14" 1757 | source = "registry+https://github.com/rust-lang/crates.io-index" 1758 | checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" 1759 | dependencies = [ 1760 | "bytes", 1761 | "futures-core", 1762 | "futures-sink", 1763 | "pin-project-lite", 1764 | "tokio", 1765 | ] 1766 | 1767 | [[package]] 1768 | name = "tower" 1769 | version = "0.5.2" 1770 | source = "registry+https://github.com/rust-lang/crates.io-index" 1771 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 1772 | dependencies = [ 1773 | "futures-core", 1774 | "futures-util", 1775 | "pin-project-lite", 1776 | "sync_wrapper", 1777 | "tokio", 1778 | "tower-layer", 1779 | "tower-service", 1780 | ] 1781 | 1782 | [[package]] 1783 | name = "tower-layer" 1784 | version = "0.3.3" 1785 | source = "registry+https://github.com/rust-lang/crates.io-index" 1786 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1787 | 1788 | [[package]] 1789 | name = "tower-service" 1790 | version = "0.3.3" 1791 | source = "registry+https://github.com/rust-lang/crates.io-index" 1792 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1793 | 1794 | [[package]] 1795 | name = "tracing" 1796 | version = "0.1.41" 1797 | source = "registry+https://github.com/rust-lang/crates.io-index" 1798 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1799 | dependencies = [ 1800 | "pin-project-lite", 1801 | "tracing-core", 1802 | ] 1803 | 1804 | [[package]] 1805 | name = "tracing-core" 1806 | version = "0.1.33" 1807 | source = "registry+https://github.com/rust-lang/crates.io-index" 1808 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1809 | dependencies = [ 1810 | "once_cell", 1811 | "valuable", 1812 | ] 1813 | 1814 | [[package]] 1815 | name = "tracing-error" 1816 | version = "0.2.1" 1817 | source = "registry+https://github.com/rust-lang/crates.io-index" 1818 | checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" 1819 | dependencies = [ 1820 | "tracing", 1821 | "tracing-subscriber", 1822 | ] 1823 | 1824 | [[package]] 1825 | name = "tracing-subscriber" 1826 | version = "0.3.19" 1827 | source = "registry+https://github.com/rust-lang/crates.io-index" 1828 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 1829 | dependencies = [ 1830 | "sharded-slab", 1831 | "thread_local", 1832 | "tracing-core", 1833 | ] 1834 | 1835 | [[package]] 1836 | name = "try-lock" 1837 | version = "0.2.5" 1838 | source = "registry+https://github.com/rust-lang/crates.io-index" 1839 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1840 | 1841 | [[package]] 1842 | name = "typenum" 1843 | version = "1.18.0" 1844 | source = "registry+https://github.com/rust-lang/crates.io-index" 1845 | checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 1846 | 1847 | [[package]] 1848 | name = "unicode-ident" 1849 | version = "1.0.18" 1850 | source = "registry+https://github.com/rust-lang/crates.io-index" 1851 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1852 | 1853 | [[package]] 1854 | name = "unicode-segmentation" 1855 | version = "1.12.0" 1856 | source = "registry+https://github.com/rust-lang/crates.io-index" 1857 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1858 | 1859 | [[package]] 1860 | name = "untrusted" 1861 | version = "0.9.0" 1862 | source = "registry+https://github.com/rust-lang/crates.io-index" 1863 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1864 | 1865 | [[package]] 1866 | name = "unty" 1867 | version = "0.0.4" 1868 | source = "registry+https://github.com/rust-lang/crates.io-index" 1869 | checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" 1870 | 1871 | [[package]] 1872 | name = "url" 1873 | version = "2.5.4" 1874 | source = "registry+https://github.com/rust-lang/crates.io-index" 1875 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1876 | dependencies = [ 1877 | "form_urlencoded", 1878 | "idna", 1879 | "percent-encoding", 1880 | ] 1881 | 1882 | [[package]] 1883 | name = "utf16_iter" 1884 | version = "1.0.5" 1885 | source = "registry+https://github.com/rust-lang/crates.io-index" 1886 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1887 | 1888 | [[package]] 1889 | name = "utf8_iter" 1890 | version = "1.0.4" 1891 | source = "registry+https://github.com/rust-lang/crates.io-index" 1892 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1893 | 1894 | [[package]] 1895 | name = "utf8parse" 1896 | version = "0.2.2" 1897 | source = "registry+https://github.com/rust-lang/crates.io-index" 1898 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1899 | 1900 | [[package]] 1901 | name = "valuable" 1902 | version = "0.1.1" 1903 | source = "registry+https://github.com/rust-lang/crates.io-index" 1904 | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 1905 | 1906 | [[package]] 1907 | name = "vcpkg" 1908 | version = "0.2.15" 1909 | source = "registry+https://github.com/rust-lang/crates.io-index" 1910 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1911 | 1912 | [[package]] 1913 | name = "version_check" 1914 | version = "0.9.5" 1915 | source = "registry+https://github.com/rust-lang/crates.io-index" 1916 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1917 | 1918 | [[package]] 1919 | name = "virtue" 1920 | version = "0.0.18" 1921 | source = "registry+https://github.com/rust-lang/crates.io-index" 1922 | checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" 1923 | 1924 | [[package]] 1925 | name = "walkdir" 1926 | version = "2.5.0" 1927 | source = "registry+https://github.com/rust-lang/crates.io-index" 1928 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1929 | dependencies = [ 1930 | "same-file", 1931 | "winapi-util", 1932 | ] 1933 | 1934 | [[package]] 1935 | name = "want" 1936 | version = "0.3.1" 1937 | source = "registry+https://github.com/rust-lang/crates.io-index" 1938 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1939 | dependencies = [ 1940 | "try-lock", 1941 | ] 1942 | 1943 | [[package]] 1944 | name = "wasi" 1945 | version = "0.11.0+wasi-snapshot-preview1" 1946 | source = "registry+https://github.com/rust-lang/crates.io-index" 1947 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1948 | 1949 | [[package]] 1950 | name = "wasi" 1951 | version = "0.14.2+wasi-0.2.4" 1952 | source = "registry+https://github.com/rust-lang/crates.io-index" 1953 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 1954 | dependencies = [ 1955 | "wit-bindgen-rt", 1956 | ] 1957 | 1958 | [[package]] 1959 | name = "wasm-bindgen" 1960 | version = "0.2.100" 1961 | source = "registry+https://github.com/rust-lang/crates.io-index" 1962 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1963 | dependencies = [ 1964 | "cfg-if", 1965 | "once_cell", 1966 | "rustversion", 1967 | "wasm-bindgen-macro", 1968 | ] 1969 | 1970 | [[package]] 1971 | name = "wasm-bindgen-backend" 1972 | version = "0.2.100" 1973 | source = "registry+https://github.com/rust-lang/crates.io-index" 1974 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1975 | dependencies = [ 1976 | "bumpalo", 1977 | "log", 1978 | "proc-macro2", 1979 | "quote", 1980 | "syn", 1981 | "wasm-bindgen-shared", 1982 | ] 1983 | 1984 | [[package]] 1985 | name = "wasm-bindgen-futures" 1986 | version = "0.4.50" 1987 | source = "registry+https://github.com/rust-lang/crates.io-index" 1988 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 1989 | dependencies = [ 1990 | "cfg-if", 1991 | "js-sys", 1992 | "once_cell", 1993 | "wasm-bindgen", 1994 | "web-sys", 1995 | ] 1996 | 1997 | [[package]] 1998 | name = "wasm-bindgen-macro" 1999 | version = "0.2.100" 2000 | source = "registry+https://github.com/rust-lang/crates.io-index" 2001 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 2002 | dependencies = [ 2003 | "quote", 2004 | "wasm-bindgen-macro-support", 2005 | ] 2006 | 2007 | [[package]] 2008 | name = "wasm-bindgen-macro-support" 2009 | version = "0.2.100" 2010 | source = "registry+https://github.com/rust-lang/crates.io-index" 2011 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 2012 | dependencies = [ 2013 | "proc-macro2", 2014 | "quote", 2015 | "syn", 2016 | "wasm-bindgen-backend", 2017 | "wasm-bindgen-shared", 2018 | ] 2019 | 2020 | [[package]] 2021 | name = "wasm-bindgen-shared" 2022 | version = "0.2.100" 2023 | source = "registry+https://github.com/rust-lang/crates.io-index" 2024 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 2025 | dependencies = [ 2026 | "unicode-ident", 2027 | ] 2028 | 2029 | [[package]] 2030 | name = "web-sys" 2031 | version = "0.3.77" 2032 | source = "registry+https://github.com/rust-lang/crates.io-index" 2033 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 2034 | dependencies = [ 2035 | "js-sys", 2036 | "wasm-bindgen", 2037 | ] 2038 | 2039 | [[package]] 2040 | name = "winapi-util" 2041 | version = "0.1.9" 2042 | source = "registry+https://github.com/rust-lang/crates.io-index" 2043 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 2044 | dependencies = [ 2045 | "windows-sys 0.59.0", 2046 | ] 2047 | 2048 | [[package]] 2049 | name = "windows-link" 2050 | version = "0.1.1" 2051 | source = "registry+https://github.com/rust-lang/crates.io-index" 2052 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 2053 | 2054 | [[package]] 2055 | name = "windows-registry" 2056 | version = "0.4.0" 2057 | source = "registry+https://github.com/rust-lang/crates.io-index" 2058 | checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" 2059 | dependencies = [ 2060 | "windows-result", 2061 | "windows-strings", 2062 | "windows-targets 0.53.0", 2063 | ] 2064 | 2065 | [[package]] 2066 | name = "windows-result" 2067 | version = "0.3.2" 2068 | source = "registry+https://github.com/rust-lang/crates.io-index" 2069 | checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" 2070 | dependencies = [ 2071 | "windows-link", 2072 | ] 2073 | 2074 | [[package]] 2075 | name = "windows-strings" 2076 | version = "0.3.1" 2077 | source = "registry+https://github.com/rust-lang/crates.io-index" 2078 | checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" 2079 | dependencies = [ 2080 | "windows-link", 2081 | ] 2082 | 2083 | [[package]] 2084 | name = "windows-sys" 2085 | version = "0.52.0" 2086 | source = "registry+https://github.com/rust-lang/crates.io-index" 2087 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2088 | dependencies = [ 2089 | "windows-targets 0.52.6", 2090 | ] 2091 | 2092 | [[package]] 2093 | name = "windows-sys" 2094 | version = "0.59.0" 2095 | source = "registry+https://github.com/rust-lang/crates.io-index" 2096 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 2097 | dependencies = [ 2098 | "windows-targets 0.52.6", 2099 | ] 2100 | 2101 | [[package]] 2102 | name = "windows-targets" 2103 | version = "0.52.6" 2104 | source = "registry+https://github.com/rust-lang/crates.io-index" 2105 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2106 | dependencies = [ 2107 | "windows_aarch64_gnullvm 0.52.6", 2108 | "windows_aarch64_msvc 0.52.6", 2109 | "windows_i686_gnu 0.52.6", 2110 | "windows_i686_gnullvm 0.52.6", 2111 | "windows_i686_msvc 0.52.6", 2112 | "windows_x86_64_gnu 0.52.6", 2113 | "windows_x86_64_gnullvm 0.52.6", 2114 | "windows_x86_64_msvc 0.52.6", 2115 | ] 2116 | 2117 | [[package]] 2118 | name = "windows-targets" 2119 | version = "0.53.0" 2120 | source = "registry+https://github.com/rust-lang/crates.io-index" 2121 | checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" 2122 | dependencies = [ 2123 | "windows_aarch64_gnullvm 0.53.0", 2124 | "windows_aarch64_msvc 0.53.0", 2125 | "windows_i686_gnu 0.53.0", 2126 | "windows_i686_gnullvm 0.53.0", 2127 | "windows_i686_msvc 0.53.0", 2128 | "windows_x86_64_gnu 0.53.0", 2129 | "windows_x86_64_gnullvm 0.53.0", 2130 | "windows_x86_64_msvc 0.53.0", 2131 | ] 2132 | 2133 | [[package]] 2134 | name = "windows_aarch64_gnullvm" 2135 | version = "0.52.6" 2136 | source = "registry+https://github.com/rust-lang/crates.io-index" 2137 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2138 | 2139 | [[package]] 2140 | name = "windows_aarch64_gnullvm" 2141 | version = "0.53.0" 2142 | source = "registry+https://github.com/rust-lang/crates.io-index" 2143 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 2144 | 2145 | [[package]] 2146 | name = "windows_aarch64_msvc" 2147 | version = "0.52.6" 2148 | source = "registry+https://github.com/rust-lang/crates.io-index" 2149 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2150 | 2151 | [[package]] 2152 | name = "windows_aarch64_msvc" 2153 | version = "0.53.0" 2154 | source = "registry+https://github.com/rust-lang/crates.io-index" 2155 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 2156 | 2157 | [[package]] 2158 | name = "windows_i686_gnu" 2159 | version = "0.52.6" 2160 | source = "registry+https://github.com/rust-lang/crates.io-index" 2161 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2162 | 2163 | [[package]] 2164 | name = "windows_i686_gnu" 2165 | version = "0.53.0" 2166 | source = "registry+https://github.com/rust-lang/crates.io-index" 2167 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 2168 | 2169 | [[package]] 2170 | name = "windows_i686_gnullvm" 2171 | version = "0.52.6" 2172 | source = "registry+https://github.com/rust-lang/crates.io-index" 2173 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2174 | 2175 | [[package]] 2176 | name = "windows_i686_gnullvm" 2177 | version = "0.53.0" 2178 | source = "registry+https://github.com/rust-lang/crates.io-index" 2179 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 2180 | 2181 | [[package]] 2182 | name = "windows_i686_msvc" 2183 | version = "0.52.6" 2184 | source = "registry+https://github.com/rust-lang/crates.io-index" 2185 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2186 | 2187 | [[package]] 2188 | name = "windows_i686_msvc" 2189 | version = "0.53.0" 2190 | source = "registry+https://github.com/rust-lang/crates.io-index" 2191 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 2192 | 2193 | [[package]] 2194 | name = "windows_x86_64_gnu" 2195 | version = "0.52.6" 2196 | source = "registry+https://github.com/rust-lang/crates.io-index" 2197 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2198 | 2199 | [[package]] 2200 | name = "windows_x86_64_gnu" 2201 | version = "0.53.0" 2202 | source = "registry+https://github.com/rust-lang/crates.io-index" 2203 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 2204 | 2205 | [[package]] 2206 | name = "windows_x86_64_gnullvm" 2207 | version = "0.52.6" 2208 | source = "registry+https://github.com/rust-lang/crates.io-index" 2209 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2210 | 2211 | [[package]] 2212 | name = "windows_x86_64_gnullvm" 2213 | version = "0.53.0" 2214 | source = "registry+https://github.com/rust-lang/crates.io-index" 2215 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 2216 | 2217 | [[package]] 2218 | name = "windows_x86_64_msvc" 2219 | version = "0.52.6" 2220 | source = "registry+https://github.com/rust-lang/crates.io-index" 2221 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2222 | 2223 | [[package]] 2224 | name = "windows_x86_64_msvc" 2225 | version = "0.53.0" 2226 | source = "registry+https://github.com/rust-lang/crates.io-index" 2227 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 2228 | 2229 | [[package]] 2230 | name = "wit-bindgen-rt" 2231 | version = "0.39.0" 2232 | source = "registry+https://github.com/rust-lang/crates.io-index" 2233 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 2234 | dependencies = [ 2235 | "bitflags", 2236 | ] 2237 | 2238 | [[package]] 2239 | name = "write16" 2240 | version = "1.0.0" 2241 | source = "registry+https://github.com/rust-lang/crates.io-index" 2242 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 2243 | 2244 | [[package]] 2245 | name = "writeable" 2246 | version = "0.5.5" 2247 | source = "registry+https://github.com/rust-lang/crates.io-index" 2248 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 2249 | 2250 | [[package]] 2251 | name = "yoke" 2252 | version = "0.7.5" 2253 | source = "registry+https://github.com/rust-lang/crates.io-index" 2254 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 2255 | dependencies = [ 2256 | "serde", 2257 | "stable_deref_trait", 2258 | "yoke-derive", 2259 | "zerofrom", 2260 | ] 2261 | 2262 | [[package]] 2263 | name = "yoke-derive" 2264 | version = "0.7.5" 2265 | source = "registry+https://github.com/rust-lang/crates.io-index" 2266 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 2267 | dependencies = [ 2268 | "proc-macro2", 2269 | "quote", 2270 | "syn", 2271 | "synstructure", 2272 | ] 2273 | 2274 | [[package]] 2275 | name = "zerocopy" 2276 | version = "0.8.24" 2277 | source = "registry+https://github.com/rust-lang/crates.io-index" 2278 | checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" 2279 | dependencies = [ 2280 | "zerocopy-derive", 2281 | ] 2282 | 2283 | [[package]] 2284 | name = "zerocopy-derive" 2285 | version = "0.8.24" 2286 | source = "registry+https://github.com/rust-lang/crates.io-index" 2287 | checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" 2288 | dependencies = [ 2289 | "proc-macro2", 2290 | "quote", 2291 | "syn", 2292 | ] 2293 | 2294 | [[package]] 2295 | name = "zerofrom" 2296 | version = "0.1.6" 2297 | source = "registry+https://github.com/rust-lang/crates.io-index" 2298 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 2299 | dependencies = [ 2300 | "zerofrom-derive", 2301 | ] 2302 | 2303 | [[package]] 2304 | name = "zerofrom-derive" 2305 | version = "0.1.6" 2306 | source = "registry+https://github.com/rust-lang/crates.io-index" 2307 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 2308 | dependencies = [ 2309 | "proc-macro2", 2310 | "quote", 2311 | "syn", 2312 | "synstructure", 2313 | ] 2314 | 2315 | [[package]] 2316 | name = "zeroize" 2317 | version = "1.8.1" 2318 | source = "registry+https://github.com/rust-lang/crates.io-index" 2319 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 2320 | 2321 | [[package]] 2322 | name = "zerovec" 2323 | version = "0.10.4" 2324 | source = "registry+https://github.com/rust-lang/crates.io-index" 2325 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 2326 | dependencies = [ 2327 | "yoke", 2328 | "zerofrom", 2329 | "zerovec-derive", 2330 | ] 2331 | 2332 | [[package]] 2333 | name = "zerovec-derive" 2334 | version = "0.10.3" 2335 | source = "registry+https://github.com/rust-lang/crates.io-index" 2336 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 2337 | dependencies = [ 2338 | "proc-macro2", 2339 | "quote", 2340 | "syn", 2341 | ] 2342 | --------------------------------------------------------------------------------