diff options
Diffstat
| -rw-r--r-- | .cargo/config.toml (renamed from CODEOWNERS) | 3 | +2 −1 |
| -rw-r--r-- | .dockerignore | 9 | +0 −9 |
| -rw-r--r-- | .editorconfig | 26 | +0 −26 |
| -rw-r--r-- | .github/workflows/build.yml | 263 | +85 −178 |
| -rw-r--r-- | .github/workflows/legal.yml | 1 | +0 −1 |
| -rw-r--r-- | .gitignore | 6 | +2 −4 |
| -rw-r--r-- | CLA.md | 4 | +2 −2 |
| -rw-r--r-- | Cargo.lock | 229 | +179 −50 |
| -rw-r--r-- | Cargo.toml | 4 | +2 −2 |
| -rw-r--r-- | Dockerfile | 29 | +0 −29 |
| -rw-r--r-- | Makefile | 51 | +51 −0 |
| -rw-r--r-- | README.md | 48 | +25 −23 |
| -rw-r--r-- | REUSE.toml | 16 | +7 −9 |
| -rw-r--r-- | charts/tesor/Chart.yaml | 10 | +0 −10 |
| -rw-r--r-- | charts/tesor/templates/_helpers.tpl | 37 | +0 −37 |
| -rw-r--r-- | charts/tesor/templates/configmap.yaml | 46 | +0 −46 |
| -rw-r--r-- | charts/tesor/templates/deployment.yaml | 97 | +0 −97 |
| -rw-r--r-- | charts/tesor/templates/httproute.yaml | 26 | +0 −26 |
| -rw-r--r-- | charts/tesor/templates/pvc.yaml | 22 | +0 −22 |
| -rw-r--r-- | charts/tesor/templates/service.yaml | 19 | +0 −19 |
| -rw-r--r-- | charts/tesor/values.yaml | 78 | +0 −78 |
| -rw-r--r-- | crates/recluse/Cargo.toml (renamed from crates/tesor/Cargo.toml) | 8 | +6 −2 |
| -rw-r--r-- | crates/recluse/build.rs | 13 | +13 −0 |
| -rw-r--r-- | crates/recluse/src/assets/apple-touch-icon.png (renamed from crates/tesor/src/assets/apple-touch-icon.png) | bin | 715 -> 715 bytes |
| -rw-r--r-- | crates/recluse/src/assets/base.css (renamed from crates/tesor/src/assets/base.css) | 0 | +0 −0 |
| -rw-r--r-- | crates/recluse/src/assets/favicon-192.png (renamed from crates/tesor/src/assets/favicon-192.png) | bin | 715 -> 715 bytes |
| -rw-r--r-- | crates/recluse/src/assets/favicon-512.png (renamed from crates/tesor/src/assets/favicon-512.png) | bin | 2329 -> 2329 bytes |
| -rw-r--r-- | crates/recluse/src/assets/favicon.ico (renamed from crates/tesor/src/assets/favicon.ico) | bin | 32038 -> 32038 bytes |
| -rw-r--r-- | crates/recluse/src/assets/favicon.svg (renamed from crates/tesor/src/assets/favicon.svg) | 0 | +0 −0 |
| -rw-r--r-- | crates/recluse/src/assets/jetbrainsmono/JetBrainsMono-Italic[wght].woff2 (renamed from crates/tesor/src/assets/jetbrainsmono/JetBrainsMono-Italic[wght].woff2) | bin | 122452 -> 122452 bytes |
| -rw-r--r-- | crates/recluse/src/assets/jetbrainsmono/JetBrainsMono[wght].woff2 (renamed from crates/tesor/src/assets/jetbrainsmono/JetBrainsMono[wght].woff2) | bin | 113700 -> 113700 bytes |
| -rw-r--r-- | crates/recluse/src/assets/manifest.webmanifest (renamed from crates/tesor/src/assets/manifest.webmanifest) | 2 | +1 −1 |
| -rw-r--r-- | crates/recluse/src/assets/robots.txt (renamed from crates/tesor/src/assets/robots.txt) | 0 | +0 −0 |
| -rw-r--r-- | crates/recluse/src/config.rs (renamed from crates/tesor/src/config.rs) | 8 | +4 −4 |
| -rw-r--r-- | crates/recluse/src/controller_backend.rs (renamed from crates/tesor/src/controller_backend.rs) | 0 | +0 −0 |
| -rw-r--r-- | crates/recluse/src/controller_web.rs (renamed from crates/tesor/src/controller_web.rs) | 16 | +8 −8 |
| -rw-r--r-- | crates/recluse/src/main.rs (renamed from crates/tesor/src/main.rs) | 8 | +4 −4 |
| -rw-r--r-- | crates/recluse/src/proxy.rs (renamed from crates/tesor/src/proxy.rs) | 2 | +1 −1 |
| -rw-r--r-- | crates/recluse/src/storage.rs (renamed from crates/tesor/src/storage.rs) | 0 | +0 −0 |
| -rw-r--r-- | crates/recluse/src/telemetry.rs (renamed from crates/tesor/src/telemetry.rs) | 4 | +2 −2 |
| -rw-r--r-- | crates/repos/src/go.rs | 4 | +2 −2 |
| -rw-r--r-- | crates/tesor/build.rs | 30 | +0 −30 |
| -rw-r--r-- | deny.toml | 3 | +2 −1 |
| -rw-r--r-- | mise.lock | 73 | +0 −73 |
| -rw-r--r-- | mise.toml | 54 | +1 −53 |
| -rw-r--r-- | nfpm.yaml | 24 | +12 −12 |
| -rw-r--r-- | pkg/recluse.service (renamed from pkg/tesor.service) | 10 | +5 −5 |
| -rw-r--r-- | pkg/recluse.toml (renamed from pkg/tesor.toml) | 20 | +10 −10 |
| -rw-r--r-- | pkg/scripts/postinstall.sh | 4 | +2 −2 |
| -rw-r--r-- | pkg/scripts/preinstall.sh | 13 | +7 −6 |
| -rw-r--r-- | pkg/scripts/preremove.sh | 6 | +3 −3 |
| -rwxr-xr-x | tests/smoke/run-local.sh | 24 | +12 −12 |
| -rw-r--r-- | tests/smoke/src/main.rs | 36 | +18 −18 |
| -rw-r--r-- | xtask/Cargo.toml | 16 | +16 −0 |
| -rw-r--r-- | xtask/build.rs | 9 | +9 −0 |
| -rw-r--r-- | xtask/src/lib.rs | 4 | +4 −0 |
| -rw-r--r-- | xtask/src/licenses.rs | 614 | +614 −0 |
| -rw-r--r-- | xtask/src/main.rs | 19 | +19 −0 |
58 files changed, 1130 insertions, 918 deletions
diff --git a/CODEOWNERS b/.cargo/config.toml index faf8e2e..6133d93 100644 --- a/CODEOWNERS +++ b/.cargo/config.toml @@ -1,4 +1,5 @@ # SPDX-FileCopyrightText: 2026 Nikolay Govorov # SPDX-License-Identifier: AGPL-3.0-or-later -* @mrdimidium +[alias] +xtask = "run --release --package xtask --" diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 --- a/.dockerignore +++ /dev/null @@ -1,9 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nikolay Govorov -# SPDX-License-Identifier: AGPL-3.0-or-later - -.git -.github -.tesor-state -dist -target -*.lcov diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 --- a/.editorconfig +++ /dev/null @@ -1,26 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nikolay Govorov -# SPDX-License-Identifier: AGPL-3.0-or-later - -root = true - -[*] -charset = utf-8 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true -indent_style = space -indent_size = 4 - -[*.md] -# double whitespace at end of line -# denotes a line break in Markdown -trim_trailing_whitespace = false - -[*.rs] -max_line_length = 100 - -[*.yml] -indent_size = 2 - -[{Makefile,*.mk}] -indent_style = tab diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4078821..2471a92 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,13 +10,6 @@ on: pull_request: branches: [main] -permissions: - contents: read - -concurrency: - group: tesor-build-${{ github.ref }} - cancel-in-progress: true - env: CARGO_TERM_COLOR: always RELEASE_TAG: nightly @@ -28,24 +21,12 @@ jobs: steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3 - with: - version: 2026.7.5 - experimental: true - install: false - - name: Install dependencies - run: mise bootstrap --locked --yes --update + - uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - - name: formatting + - name: Check formatting run: cargo fmt --all --check - - name: shellcheck - run: shellcheck pkg/scripts/*.sh tests/smoke/*.sh - - - name: clippy - run: cargo clippy --workspace --all-targets --all-features --release --locked -- -D warnings - build: name: Build (${{ matrix.arch }}) runs-on: ${{ matrix.runner }} @@ -61,41 +42,47 @@ jobs: steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3 + - uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable with: - version: 2026.7.5 - experimental: true - install: false - - name: Install dependencies - run: mise bootstrap --locked --yes --update + components: llvm-tools-preview - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - - run: cargo clippy --workspace --locked --all-targets --all-features --release -- -D warnings + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec #v6.3.0 + with: + passphrase: ${{ secrets.GPG_PASSPHRASE }} + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + + - run: cargo clippy --all-targets --all-features --release -- -D warnings + - run: cargo build --release - run: cargo test --all-features --release --locked - - run: cargo build --package tesor --bin tesor --release --locked + + - name: Install nfpm + run: | + echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | sudo tee /etc/apt/sources.list.d/goreleaser.list + sudo apt update && sudo apt install nfpm - name: Build packages run: | - PKG_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "tesor") | .version') - if [[ "$GITHUB_REF" == refs/tags/v* ]]; then - VERSION="${GITHUB_REF_NAME#v}" - if [[ "$VERSION" != "$PKG_VERSION" ]]; then - echo "Tag version $VERSION does not match Cargo.toml version $PKG_VERSION" >&2 - exit 1 - fi - else - VERSION="$PKG_VERSION~nightly.$(git log -1 --format=%ct)" - fi - - mise run package -- \ - --version "$VERSION" \ - --arch "${{ matrix.arch }}" \ - --output dist \ - deb rpm + export SIGNING_PRIVATE_KEY="/tmp/private.asc" + printf '%s' "$GPG_PRIVATE_KEY" > $SIGNING_PRIVATE_KEY + chmod 600 $SIGNING_PRIVATE_KEY + + PKG_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "recluse") | .version') + export VERSION="${PKG_VERSION}~nightly.$(git log -1 --format=%ct)" + export ARCH=${{ matrix.arch }} + + mkdir -p dist/ + for pkg in deb rpm; do + nfpm package --packager $pkg --target dist/ + done env: GPG_KEY_ID: ${{ vars.GPG_KEY_ID }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + + - name: Install tools + run: make setup - name: Generate coverage (lcov) run: cargo llvm-cov --all-features --workspace --lcov --output-path coverage.lcov @@ -114,23 +101,16 @@ jobs: - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: binary-${{ matrix.arch }} - path: target/release/tesor + path: target/release/recluse smoke: name: Smoke tests runs-on: ubuntu-24.04 needs: [build] + if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3 - with: - version: 2026.7.5 - experimental: true - install: false - - name: Install dependencies - run: mise bootstrap --locked --yes --update - - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: binary-amd64 @@ -138,29 +118,22 @@ jobs: - name: Run smoke tests against local instance run: | - chmod +x target/release/tesor + chmod +x target/release/recluse ./tests/smoke/run-local.sh publish: name: Publish nightly build runs-on: ubuntu-latest needs: [build, smoke] - concurrency: - group: package-publish-tesor-nightly - cancel-in-progress: false if: github.ref == 'refs/heads/main' permissions: contents: write steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3 - with: - version: 2026.7.5 - experimental: true - install: false - - name: Install dependencies - run: mise bootstrap --locked --yes --update + - name: Install tools + run: | + sudo apt-get update + sudo apt-get install -y aptly rclone createrepo-c - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: @@ -168,6 +141,17 @@ jobs: path: dist merge-multiple: true + # Configure GPG keys + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec #v6.3.0 + with: + passphrase: ${{ secrets.GPG_PASSPHRASE }} + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + - name: Export GPG public key + run: printf '%s' "$GPG_PUBLIC_KEY" > dist/public.gpg + env: + GPG_PUBLIC_KEY: ${{ secrets.GPG_PUBLIC_KEY }} + # Update Github Release - name: Update 'nightly' tag run: | @@ -196,117 +180,40 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Publish package repositories - run: | - mise run publish -- \ - --service tesor \ - --channel nightly \ - --input dist \ - deb rpm - env: - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - GPG_KEY_ID: ${{ vars.GPG_KEY_ID }} - PACKAGE_KEY_VERSION: ${{ vars.PACKAGE_KEY_VERSION }} - S3_BUCKET: ${{ vars.S3_BUCKET }} - S3_ENDPOINT: ${{ vars.S3_ENDPOINT }} - S3_PUBLIC_URL: ${{ vars.S3_PUBLIC_URL }} - S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }} - S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }} - - oci: - name: OCI artifacts - needs: [smoke] - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3 - with: - version: 2026.7.5 - experimental: true - install: false - - - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 - with: - pattern: binary-* - path: .container - - - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 - - - name: Determine artifact version - id: artifact - shell: bash + # Update APT/RPM repos on s3 + - name: Create APT repository run: | - base_version=$(python3 -c 'import tomllib; print(tomllib.load(open("Cargo.toml", "rb"))["workspace"]["package"]["version"])') - if [[ "$GITHUB_REF" == refs/heads/main ]]; then - version="$base_version-nightly.$(git log -1 --format=%ct)" - elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then - version="${GITHUB_REF_NAME#v}" - if [[ "$version" != "$base_version" ]]; then - echo "Tag version $version does not match Cargo.toml version $base_version" >&2 - exit 1 - fi - else - version="$base_version-pr.$GITHUB_RUN_NUMBER" - fi - echo "version=$version" >> "$GITHUB_OUTPUT" - - - name: Log in to GHCR - if: github.event_name != 'pull_request' - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Log Helm in to GHCR - if: github.event_name != 'pull_request' - env: - GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} + aptly repo create -distribution="${{ env.RELEASE_TAG }}" -component=main recluse + aptly repo add recluse dist/*.deb + aptly publish repo -architectures=amd64,arm64 -gpg-key="${{vars.GPG_KEY_ID}}" recluse + - name: Create RPM repository run: | - printf '%s' "$GHCR_TOKEN" | mise x helm@4.1.1 -- \ - helm registry login ghcr.io --username "$GITHUB_ACTOR" --password-stdin - - - name: Build and publish OCI artifacts - shell: bash + mkdir -p rpm-repo + cp dist/*.rpm rpm-repo/ + + cat > rpm-repo/recluse-nightly.repo << EOF + [recluse-nightly] + name=Recluse Nightly + gpgkey=${{github.server_url}}/${{github.repository}}/releases/download/${{env.RELEASE_TAG}}/public.gpg + baseurl=https://${{vars.S3_BUCKET}}.${{vars.S3_ENDPOINT}}/rpm/ + enabled=1 + gpgcheck=1 + EOF + + createrepo_c rpm-repo/ + gpg --default-key="${{vars.GPG_KEY_ID}}" --detach-sign --armor rpm-repo/repodata/repomd.xml + - name: Upload RPM to S3 run: | - version="${{ steps.artifact.outputs.version }}" - image="ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/tesor" - tags=( - --tag "$image:sha-$GITHUB_SHA" - --tag "$image:$version" - ) - labels=( - --label "org.opencontainers.image.revision=$GITHUB_SHA" - --label "org.opencontainers.image.source=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" - --label "org.opencontainers.image.version=$version" - ) - image_output=() - chart_output=() - - if [[ "$GITHUB_REF" == refs/heads/main ]]; then - tags+=(--tag "$image:nightly") - elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then - tags+=(--tag "$image:latest") - fi - if [[ "${{ github.event_name }}" != pull_request ]]; then - image_output+=(--push) - chart_output+=(--push "oci://ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/charts") - fi - - mise run container -- \ - --context . \ - --file Dockerfile \ - --platform linux/amd64,linux/arm64 \ - --cache-scope tesor \ - "${tags[@]}" "${labels[@]}" "${image_output[@]}" - - mise run chart -- \ - --chart charts/tesor \ - --version "$version" \ - --app-version "$version" \ - "${chart_output[@]}" + mkdir -p ~/.config/rclone + cat > ~/.config/rclone/rclone.conf << EOF + [hetzner] + type = s3 + provider = Other + acl = public-read + endpoint = ${{vars.S3_ENDPOINT}} + access_key_id = ${{secrets.S3_ACCESS_KEY_ID}} + secret_access_key = ${{secrets.S3_SECRET_ACCESS_KEY}} + EOF + + rclone sync rpm-repo/ "hetzner:${{vars.S3_BUCKET}}/rpm/" --progress + rclone sync ~/.aptly/public/ "hetzner:${{vars.S3_BUCKET}}/apt/" --progress diff --git a/.github/workflows/legal.yml b/.github/workflows/legal.yml index 0336344..3d6aa54 100644 --- a/.github/workflows/legal.yml +++ b/.github/workflows/legal.yml @@ -26,7 +26,6 @@ jobs: with: version: 2026.7.5 experimental: true - install: false - name: Check contribution sign-off run: mise run signoff diff --git a/.gitignore b/.gitignore index 2c7fb07..8b1a3f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,8 @@ # SPDX-FileCopyrightText: 2026 Nikolay Govorov # SPDX-License-Identifier: AGPL-3.0-or-later -/dist /target +/dist +/.recluse-state mise.local.toml mise.*.local.toml - -/.container -/.tesor-state diff --git a/CLA.md b/CLA.md index c0418c8..f8240f7 100644 --- a/CLA.md +++ b/CLA.md @@ -1,4 +1,4 @@ -# Tesor Individual Contributor License Agreement +# Recluse Individual Contributor License Agreement Version 1.0 @@ -6,7 +6,7 @@ Version 1.0 > Agreement Version 1.0 licensed under a > [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/). -Thank you for your interest in contributing to Tesor (the "Project"). In this +Thank you for your interest in contributing to Recluse (the "Project"). In this Agreement, "We" and "Us" mean Nikolay Govorov. This contributor agreement ("Agreement") documents the rights granted by diff --git a/Cargo.lock b/Cargo.lock index d2e2df0..d178bd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -294,6 +294,15 @@ dependencies = [ ] [[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] name = "cc" version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -306,6 +315,15 @@ dependencies = [ ] [[package]] +name = "cfg-expr" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" +dependencies = [ + "smallvec", +] + +[[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -410,6 +428,25 @@ dependencies = [ ] [[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] name = "crossbeam-queue" version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -510,7 +547,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -548,6 +585,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] name = "flate2" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -792,9 +835,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.16" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -1201,6 +1244,20 @@ dependencies = [ ] [[package]] +name = "krates" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c432bb7d7a968a338f6c7cbd8782f38ffd87a793ff06c3a20257f45e1d003f6d" +dependencies = [ + "camino", + "cfg-expr", + "petgraph", + "semver", + "serde", + "serde_json", +] + +[[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1632,6 +1689,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + +[[package]] name = "pin-project" version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1913,6 +1981,72 @@ dependencies = [ ] [[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "recluse" +version = "0.1.0" +dependencies = [ + "async-trait", + "axum", + "axum-server", + "base", + "bytes", + "bytesize", + "chrono", + "crc32fast", + "hex", + "http-body-util", + "hyper", + "hyper-tls", + "hyper-util", + "maud", + "opentelemetry", + "opentelemetry-appender-tracing", + "opentelemetry-otlp", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk", + "repos", + "rustls", + "sd-notify", + "serde", + "serde_json", + "sqlx", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "toml", + "tonic", + "tower", + "tower-http", + "tower_governor", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber", + "url", + "uuid", + "xtask", +] + +[[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1931,6 +2065,18 @@ dependencies = [ ] [[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] name = "regex-automata" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2077,7 +2223,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2203,6 +2349,10 @@ name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -2383,6 +2533,18 @@ dependencies = [ ] [[package]] +name = "spdx" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8da593e30beb790fc9424502eb898320b44e5eb30367dbda1c1edde8e2f32d7" +dependencies = [ + "regex", + "smallvec", + "unicode-normalization", + "zstd", +] + +[[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2687,52 +2849,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.52.0", -] - -[[package]] -name = "tesor" -version = "0.1.0" -dependencies = [ - "async-trait", - "axum", - "axum-server", - "base", - "bytes", - "bytesize", - "chrono", - "crc32fast", - "hex", - "http-body-util", - "hyper", - "hyper-tls", - "hyper-util", - "maud", - "opentelemetry", - "opentelemetry-appender-tracing", - "opentelemetry-otlp", - "opentelemetry-semantic-conventions", - "opentelemetry_sdk", - "repos", - "rustls", - "sd-notify", - "serde", - "serde_json", - "sqlx", - "tempfile", - "thiserror 2.0.18", - "tokio", - "tokio-util", - "toml", - "tonic", - "tower", - "tower-http", - "tower_governor", - "tracing", - "tracing-opentelemetry", - "tracing-subscriber", - "url", - "uuid", + "windows-sys 0.61.2", ] [[package]] @@ -3738,6 +3855,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] +name = "xtask" +version = "0.0.0" +dependencies = [ + "krates", + "rayon", + "serde", + "serde_json", + "spdx", + "tracing", +] + +[[package]] name = "yoke" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml index 19cca99..475ca1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later [workspace] -members = ["crates/*", "tests/smoke"] +members = ["crates/*", "tests/smoke", "xtask"] resolver = "3" [workspace.package] @@ -12,7 +12,7 @@ version = "0.1.0" license = "AGPL-3.0-or-later" authors = ["Nikolay Govorov <me@govorov.online>"] homepage = "https://pkg.earth" -repository = "https://git.dimidiumlabs.io/tesor" +repository = "https://github.com/dimidiumlabs/recluse" [workspace.dependencies] # local dependencies diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 --- a/Dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -# syntax=docker/dockerfile:1.7@sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e -# SPDX-FileCopyrightText: 2026 Nikolay Govorov -# SPDX-License-Identifier: AGPL-3.0-or-later - -ARG ALPINE_VERSION=3.23.5 -ARG ALPINE_DIGEST=sha256:fd791d74b68913cbb027c6546007b3f0d3bc45125f797758156952bc2d6daf40 -ARG DISTROLESS_DIGEST=sha256:a77defd6fedbb3392b175ba8ea3d1c22be963c1597c248c3ba987ddd80bfb512 - -FROM --platform=$BUILDPLATFORM docker.io/library/alpine:${ALPINE_VERSION}@${ALPINE_DIGEST} AS rootfs -RUN install -d -m 0750 /rootfs/var/lib/tesor - -FROM gcr.io/distroless/cc-debian13:nonroot@${DISTROLESS_DIGEST} - -ARG TARGETARCH - -LABEL org.opencontainers.image.source="https://git.dimidiumlabs.io/tesor" \ - org.opencontainers.image.licenses="AGPL-3.0-or-later" - -COPY --chown=root:root --chmod=0755 .container/binary-${TARGETARCH}/tesor /usr/local/bin/tesor -COPY --from=rootfs --chown=10000:10000 /rootfs/var/lib/tesor /var/lib/tesor -COPY --chown=root:root pkg/tesor.toml /etc/tesor.toml -COPY --chown=root:root LICENSE README.md /usr/share/doc/tesor/ - -USER 10000:10000 -EXPOSE 2000 -VOLUME ["/var/lib/tesor"] - -ENTRYPOINT ["/usr/local/bin/tesor"] -CMD ["--config=/etc/tesor.toml"] diff --git a/Makefile b/Makefile new file mode 100644 --- /dev/null +++ b/Makefile @@ -0,0 +1,51 @@ +# SPDX-FileCopyrightText: 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +CARGO_LLVM_COV_VERSION = ^0.8 + +all: legal fmt clippy test + +.PHONY: legal +legal: signoff licenses + +.PHONY: signoff +signoff: + mise run signoff + +.PHONY: setup +setup: + cargo install cargo-llvm-cov@$(CARGO_LLVM_COV_VERSION) + +.PHONY: licenses +licenses: + mise run licenses + +.PHONY: fmt +fmt: + cargo fmt --all --check + +.PHONY: deps +deps: licenses + +.PHONY: clippy +clippy: + cargo clippy --all-targets --all-features -- -D warnings + +.PHONY: test +test: + cargo build --release + cargo test --all-features --release --locked + +.PHONY: smoke +smoke: + cargo run -p smoke + +.PHONY: smoke-local +smoke-local: + ./tests/smoke/run-local.sh + +.PHONY: coverage +coverage: + mkdir -p target/coverage/ + cargo llvm-cov --all-features --workspace --lcov --output-path target/coverage/lcov.info + cargo llvm-cov --all-features --workspace --html --output-dir target/coverage/html diff --git a/README.md b/README.md index 2c51ae4..1968712 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Tesor — tiny packages caching proxy +# Recluse — tiny packages caching proxy Soon humanity will go to Mars and in order for the colonists to be able to program, we will need a local mirror of packages. @@ -13,53 +13,55 @@ is **not** intended for production use. ```bash sudo apt install curl gnupg -curl -fsSL https://pkg.dimidiumlabs.io/packages.gpg | sudo gpg --dearmor -o /usr/share/keyrings/dimidiumlabs.gpg -echo "deb [signed-by=/usr/share/keyrings/dimidiumlabs.gpg] https://pkg.dimidiumlabs.io/tesor/apt/ nightly main" | sudo tee /etc/apt/sources.list.d/tesor.list -sudo apt update && sudo apt install tesor +curl -fsSL https://github.com/dimidiumlabs/recluse/releases/download/nightly/public.gpg | sudo gpg --dearmor -o /usr/share/keyrings/recluse.gpg +echo "deb [signed-by=/usr/share/keyrings/recluse.gpg] https://dimidiumlabs.fsn1.your-objectstorage.com/apt/ nightly main" | sudo tee /etc/apt/sources.list.d/recluse.list +sudo apt update && sudo apt install recluse -sudo systemctl enable --now tesor +sudo systemctl enable --now recluse ``` **Fedora/RHEL:** ```bash # DNF5 (Fedora 41+, RHEL 10+) -sudo dnf config-manager addrepo --from-repofile=https://pkg.dimidiumlabs.io/tesor/rpm/nightly/tesor-nightly.repo +sudo dnf config-manager addrepo --from-repofile=https://dimidiumlabs.fsn1.your-objectstorage.com/rpm/recluse-nightly.repo # DNF4 (Fedora 40 and older, RHEL 8/9) -sudo curl -o /etc/yum.repos.d/tesor-nightly.repo https://pkg.dimidiumlabs.io/tesor/rpm/nightly/tesor-nightly.repo +sudo curl -o /etc/yum.repos.d/recluse-nightly.repo https://dimidiumlabs.fsn1.your-objectstorage.com/rpm/recluse-nightly.repo -sudo dnf install tesor -sudo systemctl enable --now tesor +sudo dnf install recluse +sudo systemctl enable --now recluse ``` **openSUSE:** ```bash -sudo rpm --import https://pkg.dimidiumlabs.io/packages.gpg -sudo zypper addrepo https://pkg.dimidiumlabs.io/tesor/rpm/nightly/ tesor-nightly +sudo rpm --import https://github.com/dimidiumlabs/recluse/releases/download/nightly/public.gpg +sudo zypper addrepo https://dimidiumlabs.fsn1.your-objectstorage.com/rpm/ recluse-nightly sudo zypper refresh -sudo zypper install tesor +sudo zypper install recluse -sudo systemctl enable --now tesor +sudo systemctl enable --now recluse ``` ## Build from source -1. Clone repo: `git clone https://git.dimidiumlabs.io/tesor.git && cd tesor` -1. Provision the toolchain and system build dependencies: `mise bootstrap` -1. Build from source: `mise exec -- cargo build --release` +1. Install build dependencies: + - Fedora/RedHat: `sudo dnf install gcc openssl-devel pkg-config` + - Debian/Ubuntu: `sudo apt install build-essential libssl-dev pkg-config` +1. Clone repo: `git clone https://github.com/dimidiumlabs/recluse.git && cd recluse` +1. Build from source: `cargo build --release` 1. Install manually ```bash - sudo groupadd --system tesor - sudo useradd --system --gid tesor --no-create-home --shell /usr/sbin/nologin tesor - sudo install -m 700 -o tesor ./pkg/tesor.toml /etc/ - sudo install -m 755 -o root ./pkg/tesor.service /usr/lib/systemd/system - sudo install -m 755 -o root target/release/tesor /usr/local/bin/ + sudo groupadd --system recluse + sudo useradd --system --gid recluse --no-create-home --shell /usr/sbin/nologin recluse + sudo install -m 700 -o recluse ./pkg/recluse.toml /etc/ + sudo install -m 755 -o root ./pkg/recluse.service /usr/lib/systemd/system + sudo install -m 755 -o root target/release/recluse /usr/local/bin/ ``` -1. start systemd service: `sudo systemctl enable --now tesor` +1. start systemd service: `sudo systemctl enable --now recluse` ## Contributing @@ -95,7 +97,7 @@ Remember, AI agents should make software better, not worse. ## Licensing -Tesor source code is licensed under AGPL-3.0-or-later. Documentation is +Recluse source code is licensed under AGPL-3.0-or-later. Documentation is licensed under CC-BY-4.0. The bundled JetBrains Mono font files are licensed under OFL-1.1. diff --git a/REUSE.toml b/REUSE.toml index ca825f7..e0a4b90 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -7,16 +7,14 @@ version = 1 path = [ "Cargo.toml", "Cargo.lock", - "mise.lock", "pkg/*.svg", - "charts/tesor/templates/*", - "crates/tesor/src/assets/*.svg", - "crates/tesor/src/assets/*.ico", - "crates/tesor/src/assets/*.png", - "crates/tesor/src/assets/robots.txt", - "crates/tesor/src/assets/manifest.webmanifest", + "crates/recluse/src/assets/*.svg", + "crates/recluse/src/assets/*.ico", + "crates/recluse/src/assets/*.png", + "crates/recluse/src/assets/robots.txt", + "crates/recluse/src/assets/manifest.webmanifest", ] SPDX-FileCopyrightText = "2026 Nikolay Govorov" SPDX-License-Identifier = "AGPL-3.0-or-later" @@ -37,8 +35,8 @@ SPDX-License-Identifier = "CC-BY-3.0" [[annotations]] path = [ - "crates/tesor/src/assets/jetbrainsmono/OFL.txt", - "crates/tesor/src/assets/jetbrainsmono/*.woff2", + "crates/recluse/src/assets/jetbrainsmono/OFL.txt", + "crates/recluse/src/assets/jetbrainsmono/*.woff2", ] SPDX-FileCopyrightText = "Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)" SPDX-License-Identifier = "OFL-1.1" diff --git a/charts/tesor/Chart.yaml b/charts/tesor/Chart.yaml deleted file mode 100644 --- a/charts/tesor/Chart.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nikolay Govorov -# SPDX-License-Identifier: AGPL-3.0-or-later - -apiVersion: v2 -type: application -version: 0.1.0 - -name: tesor -home: https://git.dimidiumlabs.io/tesor -description: Tiny package caching proxy diff --git a/charts/tesor/templates/_helpers.tpl b/charts/tesor/templates/_helpers.tpl deleted file mode 100644 --- a/charts/tesor/templates/_helpers.tpl +++ /dev/null @@ -1,37 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nikolay Govorov -# SPDX-License-Identifier: AGPL-3.0-or-later - -{{- define "tesor.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{- define "tesor.fullname" -}} -{{- if .Values.fullnameOverride -}} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- printf "%s-%s" .Release.Name (include "tesor.name" .) | trunc 63 | trimSuffix "-" -}} -{{- end -}} -{{- end -}} - -{{- define "tesor.labels" -}} -helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }} -app.kubernetes.io/name: {{ include "tesor.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end -}} - -{{- define "tesor.selectorLabels" -}} -app.kubernetes.io/name: {{ include "tesor.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end -}} - -{{- define "tesor.image" -}} -{{- if .Values.image.digest -}} -{{- printf "%s@%s" .Values.image.repository .Values.image.digest -}} -{{- else -}} -{{- printf "%s:%s" .Values.image.repository (default .Chart.AppVersion .Values.image.tag) -}} -{{- end -}} -{{- end -}} - -# vim: set filetype=helm: diff --git a/charts/tesor/templates/configmap.yaml b/charts/tesor/templates/configmap.yaml deleted file mode 100644 --- a/charts/tesor/templates/configmap.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nikolay Govorov -# SPDX-License-Identifier: AGPL-3.0-or-later - -{{- if not .Values.config.existingConfigMap }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ include "tesor.fullname" . }} - labels: - {{- include "tesor.labels" . | nindent 4 }} -data: - tesor.toml: | - appname = {{ .Values.config.appname | quote }} - dirname = "/var/lib/tesor" - - [server] - max_body_size = {{ .Values.config.server.maxBodySize | quote }} - max_concurrent_requests = {{ .Values.config.server.maxConcurrentRequests }} - - rate_limit_period = {{ .Values.config.server.rateLimitPeriod }} - rate_limit_burst_size = {{ .Values.config.server.rateLimitBurstSize }} - - request_timeout = {{ .Values.config.server.requestTimeout }} - shutdown_timeout = {{ .Values.config.server.shutdownTimeout }} - - [[listen]] - addr = "0.0.0.0:2000" - hostnames = {{ .Values.config.hostnames | toJson }} - - {{- range $name, $backend := .Values.config.backends }} - [backends.{{ $name }}] - enabled = {{ $backend.enabled }} - refresh_interval = {{ $backend.refreshInterval }} - - {{- end }} - [telemetry.stdout] - enabled = {{ .Values.config.log.enabled }} - log_level = {{ .Values.config.log.level | quote }} - log_format = {{ .Values.config.log.format | quote }} - {{- with .Values.config.extra }} - -{{ . | indent 4 }} - {{- end }} -{{- end }} - -# vim: set filetype=helm: diff --git a/charts/tesor/templates/deployment.yaml b/charts/tesor/templates/deployment.yaml deleted file mode 100644 --- a/charts/tesor/templates/deployment.yaml +++ /dev/null @@ -1,97 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nikolay Govorov -# SPDX-License-Identifier: AGPL-3.0-or-later - -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "tesor.fullname" . }} - labels: - {{- include "tesor.labels" . | nindent 4 }} -spec: - replicas: {{ .Values.replicaCount }} - strategy: - type: Recreate - selector: - matchLabels: - {{- include "tesor.selectorLabels" . | nindent 6 }} - template: - metadata: - annotations: - {{- if not .Values.config.existingConfigMap }} - checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} - {{- end }} - {{- with .Values.podAnnotations }} - {{- toYaml . | nindent 8 }} - {{- end }} - labels: - {{- include "tesor.selectorLabels" . | nindent 8 }} - {{- with .Values.podLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - automountServiceAccountToken: false - terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} - securityContext: - {{- toYaml .Values.podSecurityContext | nindent 8 }} - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - containers: - - name: tesor - image: {{ include "tesor.image" . | quote }} - imagePullPolicy: {{ .Values.image.pullPolicy }} - securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} - args: ["--config=/etc/tesor/tesor.toml"] - ports: - - name: http - containerPort: 2000 - protocol: TCP - startupProbe: - tcpSocket: - port: http - failureThreshold: 60 - periodSeconds: 5 - readinessProbe: - tcpSocket: - port: http - periodSeconds: 10 - livenessProbe: - tcpSocket: - port: http - periodSeconds: 20 - resources: - {{- toYaml .Values.resources | nindent 12 }} - volumeMounts: - - name: config - mountPath: /etc/tesor/tesor.toml - subPath: tesor.toml - readOnly: true - - name: state - mountPath: /var/lib/tesor - volumes: - - name: config - configMap: - name: {{ default (include "tesor.fullname" .) .Values.config.existingConfigMap }} - - name: state - {{- if .Values.persistence.enabled }} - persistentVolumeClaim: - claimName: {{ default (include "tesor.fullname" .) .Values.persistence.existingClaim }} - {{- else }} - emptyDir: {} - {{- end }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} - -# vim: set filetype=helm: diff --git a/charts/tesor/templates/httproute.yaml b/charts/tesor/templates/httproute.yaml deleted file mode 100644 --- a/charts/tesor/templates/httproute.yaml +++ /dev/null @@ -1,26 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nikolay Govorov -# SPDX-License-Identifier: AGPL-3.0-or-later - -{{- if .Values.route.enabled }} -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: {{ include "tesor.fullname" . }} - labels: - {{- include "tesor.labels" . | nindent 4 }} -spec: - {{- with .Values.route.parentRefs }} - parentRefs: - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.route.hostnames }} - hostnames: - {{- toYaml . | nindent 4 }} - {{- end }} - rules: - - backendRefs: - - name: {{ include "tesor.fullname" . }} - port: {{ .Values.service.port }} -{{- end }} - -# vim: set filetype=helm: diff --git a/charts/tesor/templates/pvc.yaml b/charts/tesor/templates/pvc.yaml deleted file mode 100644 --- a/charts/tesor/templates/pvc.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nikolay Govorov -# SPDX-License-Identifier: AGPL-3.0-or-later - -{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: {{ include "tesor.fullname" . }} - labels: - {{- include "tesor.labels" . | nindent 4 }} -spec: - accessModes: - {{- toYaml .Values.persistence.accessModes | nindent 4 }} - resources: - requests: - storage: {{ .Values.persistence.size }} - {{- with .Values.persistence.storageClass }} - storageClassName: {{ . | quote }} - {{- end }} -{{- end }} - -# vim: set filetype=helm: diff --git a/charts/tesor/templates/service.yaml b/charts/tesor/templates/service.yaml deleted file mode 100644 --- a/charts/tesor/templates/service.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nikolay Govorov -# SPDX-License-Identifier: AGPL-3.0-or-later - -apiVersion: v1 -kind: Service -metadata: - name: {{ include "tesor.fullname" . }} - labels: - {{- include "tesor.labels" . | nindent 4 }} -spec: - selector: - {{- include "tesor.selectorLabels" . | nindent 4 }} - ports: - - name: http - port: {{ .Values.service.port }} - targetPort: http - protocol: TCP - -# vim: set filetype=helm: diff --git a/charts/tesor/values.yaml b/charts/tesor/values.yaml deleted file mode 100644 --- a/charts/tesor/values.yaml +++ /dev/null @@ -1,78 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Nikolay Govorov -# SPDX-License-Identifier: AGPL-3.0-or-later - -replicaCount: 1 - -image: - repository: ghcr.io/dimidiumlabs/tesor - tag: "" - digest: "" - pullPolicy: IfNotPresent - -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" - -podAnnotations: {} -podLabels: {} - -podSecurityContext: - runAsNonRoot: true - - runAsUser: 10000 - runAsGroup: 10000 - - fsGroup: 10000 - fsGroupChangePolicy: OnRootMismatch - -securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: [ALL] - readOnlyRootFilesystem: true - -service: - port: 2000 - -config: - existingConfigMap: "" - appname: tesor - hostnames: [] - server: - shutdownTimeout: 60 - requestTimeout: 30 - maxBodySize: 64 MiB - maxConcurrentRequests: 512 - rateLimitPeriod: 10 - rateLimitBurstSize: 50 - backends: - go: - enabled: true - refreshInterval: 3600 - zig: - enabled: true - refreshInterval: 3600 - log: - enabled: true - level: info - format: json - extra: "" - -persistence: - enabled: true - existingClaim: "" - accessModes: [ReadWriteOnce] - size: 20Gi - storageClass: "" - -route: - enabled: false - hostnames: [] - parentRefs: [] - -resources: {} -nodeSelector: {} -tolerations: [] -affinity: {} - -terminationGracePeriodSeconds: 70 diff --git a/crates/tesor/Cargo.toml b/crates/recluse/Cargo.toml index 908295d..cdf8c42 100644 --- a/crates/tesor/Cargo.toml +++ b/crates/recluse/Cargo.toml @@ -2,8 +2,8 @@ # SPDX-License-Identifier: AGPL-3.0-or-later [package] -name = "tesor" -description = "tesor web-server and daemon" +name = "recluse" +description = "recluse web-server and daemon" version.workspace = true authors.workspace = true @@ -58,5 +58,9 @@ opentelemetry_sdk.workspace = true [target.'cfg(target_os = "linux")'.dependencies] sd-notify.workspace = true +[build-dependencies] +xtask = { path = "../../xtask" } +serde_json.workspace = true + [dev-dependencies] tempfile.workspace = true diff --git a/crates/recluse/build.rs b/crates/recluse/build.rs new file mode 100644 --- /dev/null +++ b/crates/recluse/build.rs @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2026 Nikolay Govorov +// SPDX-License-Identifier: AGPL-3.0-or-later + +fn main() { + println!("cargo::rerun-if-changed=../../Cargo.lock"); + + let json = + xtask::licenses::generate_json("Cargo.toml").expect("failed to generate licenses JSON"); + + let out_dir = std::env::var("OUT_DIR").unwrap(); + std::fs::write(format!("{out_dir}/licenses.json"), json) + .expect("failed to write licenses.json"); +} diff --git a/crates/tesor/src/assets/apple-touch-icon.png b/crates/recluse/src/assets/apple-touch-icon.png index 0f6bb61..0f6bb61 100644 --- a/crates/tesor/src/assets/apple-touch-icon.png +++ b/crates/recluse/src/assets/apple-touch-icon.png Binary files differ diff --git a/crates/tesor/src/assets/base.css b/crates/recluse/src/assets/base.css index 66503e7..66503e7 100644 --- a/crates/tesor/src/assets/base.css +++ b/crates/recluse/src/assets/base.css diff --git a/crates/tesor/src/assets/favicon-192.png b/crates/recluse/src/assets/favicon-192.png index 0f6bb61..0f6bb61 100644 --- a/crates/tesor/src/assets/favicon-192.png +++ b/crates/recluse/src/assets/favicon-192.png Binary files differ diff --git a/crates/tesor/src/assets/favicon-512.png b/crates/recluse/src/assets/favicon-512.png index 49cd3cd..49cd3cd 100644 --- a/crates/tesor/src/assets/favicon-512.png +++ b/crates/recluse/src/assets/favicon-512.png Binary files differ diff --git a/crates/tesor/src/assets/favicon.ico b/crates/recluse/src/assets/favicon.ico index 093e50b..093e50b 100644 --- a/crates/tesor/src/assets/favicon.ico +++ b/crates/recluse/src/assets/favicon.ico Binary files differ diff --git a/crates/tesor/src/assets/favicon.svg b/crates/recluse/src/assets/favicon.svg index ed7cc17..ed7cc17 100644 --- a/crates/tesor/src/assets/favicon.svg +++ b/crates/recluse/src/assets/favicon.svg diff --git a/crates/tesor/src/assets/jetbrainsmono/JetBrainsMono-Italic[wght].woff2 b/crates/recluse/src/assets/jetbrainsmono/JetBrainsMono-Italic[wght].woff2 index 98adc80..98adc80 100644 --- a/crates/tesor/src/assets/jetbrainsmono/JetBrainsMono-Italic[wght].woff2 +++ b/crates/recluse/src/assets/jetbrainsmono/JetBrainsMono-Italic[wght].woff2 Binary files differ diff --git a/crates/tesor/src/assets/jetbrainsmono/JetBrainsMono[wght].woff2 b/crates/recluse/src/assets/jetbrainsmono/JetBrainsMono[wght].woff2 index 4cd1413..4cd1413 100644 --- a/crates/tesor/src/assets/jetbrainsmono/JetBrainsMono[wght].woff2 +++ b/crates/recluse/src/assets/jetbrainsmono/JetBrainsMono[wght].woff2 Binary files differ diff --git a/crates/tesor/src/assets/manifest.webmanifest b/crates/recluse/src/assets/manifest.webmanifest index 6d7fa0a..2277253 100644 --- a/crates/tesor/src/assets/manifest.webmanifest +++ b/crates/recluse/src/assets/manifest.webmanifest @@ -1,5 +1,5 @@ { - "name": "tesor", + "name": "recluse", "icons": [ { "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" }, { "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" }, diff --git a/crates/tesor/src/assets/robots.txt b/crates/recluse/src/assets/robots.txt index 2fe8da2..2fe8da2 100644 --- a/crates/tesor/src/assets/robots.txt +++ b/crates/recluse/src/assets/robots.txt diff --git a/crates/tesor/src/config.rs b/crates/recluse/src/config.rs index e9e148a..b729729 100644 --- a/crates/tesor/src/config.rs +++ b/crates/recluse/src/config.rs @@ -240,8 +240,8 @@ pub struct ConfigService { impl Default for ConfigService { fn default() -> Self { Self { - appname: "tesor".to_string(), - dirname: PathBuf::from("./.tesor-state"), + appname: "recluse".to_string(), + dirname: PathBuf::from("./.recluse-state"), listen: vec![ListenerConfig::default()], server: ServerConfig::default(), telemetry: TelemetryConfig::default(), @@ -410,13 +410,13 @@ mod tests { #[test] fn test_load_none_defaults() { let temp = TempDir::new().unwrap(); - let state = temp.path().join(".tesor-state"); + let state = temp.path().join(".recluse-state"); std::fs::create_dir_all(&state).unwrap(); let cwd = std::env::current_dir().unwrap(); std::env::set_current_dir(temp.path()).unwrap(); let cfg = ConfigService::load(None).unwrap(); - assert_eq!(cfg.appname(), "tesor"); + assert_eq!(cfg.appname(), "recluse"); std::env::set_current_dir(cwd).unwrap(); } diff --git a/crates/tesor/src/controller_backend.rs b/crates/recluse/src/controller_backend.rs index f97caa1..f97caa1 100644 --- a/crates/tesor/src/controller_backend.rs +++ b/crates/recluse/src/controller_backend.rs diff --git a/crates/tesor/src/controller_web.rs b/crates/recluse/src/controller_web.rs index 125295e..fef09ee 100644 --- a/crates/tesor/src/controller_web.rs +++ b/crates/recluse/src/controller_web.rs @@ -167,9 +167,9 @@ impl WebController { }; WebController::layout( - "Tesor — tiny & opinionated packages mirror", + "Recluse — tiny & opinionated packages mirror", html! { - h1 { "Tesor — tiny & opinionated packages mirror." } + h1 { "Recluse — tiny & opinionated packages mirror." } p { r#"This site provides a caching proxy for downloading Zig and Go installation files. @@ -177,8 +177,8 @@ impl WebController { } p { - "Tesor is open source software licensed under " a href="https://www.gnu.org/licenses/agpl-3.0.html" { "AGPL-3.0" } ". " - "Source code is available on " a href="https://git.dimidiumlabs.io/tesor" { "Dimidium Labs Git" } ". " + "Recluse is open source software licensed under " a href="https://www.gnu.org/licenses/agpl-3.0.html" { "AGPL-3.0" } ". " + "Source code is available on " a href="https://github.com/dimidiumlabs/recluse" { "GitHub" } ". " "A list of dependency licenses " a href="/about/licenses" { "is available" } "." } @@ -323,19 +323,19 @@ impl WebController { html! { h1 { "Licenses" } - h2 { "Tesor" } + h2 { "Recluse" } p { - "Tesor is licensed under the " b { "GNU Affero General Public License v3.0 (AGPL-3.0)" } ". " + "Recluse is licensed under the " b { "GNU Affero General Public License v3.0 (AGPL-3.0)" } ". " "This means you are free to use, modify, and distribute the software. " - "If you run a modified version of Tesor as a network service, " + "If you run a modified version of Recluse as a network service, " "you must make the source code of your modifications available to its users. " "The software is provided as-is, without warranty of any kind." } p { "Source code is available on " - a href="https://git.dimidiumlabs.io/tesor" { "Dimidium Labs Git" } + a href="https://github.com/dimidiumlabs/recluse" { "GitHub" } ". The full license text is included below." } diff --git a/crates/tesor/src/main.rs b/crates/recluse/src/main.rs index 1029eca..a42eaee 100644 --- a/crates/tesor/src/main.rs +++ b/crates/recluse/src/main.rs @@ -82,7 +82,7 @@ async fn run_index_refresh<S: BackendSpec>( const VERSION: &str = env!("CARGO_PKG_VERSION"); const HELP: &str = "\ -Usage: tesor [--config=<path>] +Usage: recluse [--config=<path>] Options: --config=<path> Path to config file (optional) @@ -134,7 +134,7 @@ async fn main() { return; } if arg == "--version" || arg == "-V" { - println!("tesor {VERSION}"); + println!("recluse {VERSION}"); return; } if let Some(path) = arg.strip_prefix("--config=") { @@ -155,7 +155,7 @@ async fn main() { let storage = Arc::new(storage::StorageService::new(config.clone()).await.unwrap()); let network = Arc::new(proxy::ProxyService::new()); - let source = format!("tesor:{}", config.appname()); + let source = format!("recluse:{}", config.appname()); let backends = config.backends(); const REQUEST_ID_HEADER: http::HeaderName = http::HeaderName::from_static("x-request-id"); @@ -331,7 +331,7 @@ async fn main() { )) .layer(tower_http::set_header::SetResponseHeaderLayer::overriding( http::header::SERVER, - http::HeaderValue::from_static(concat!("tesor/", env!("CARGO_PKG_VERSION"))), + http::HeaderValue::from_static(concat!("recluse/", env!("CARGO_PKG_VERSION"))), )); let mut tasks = tokio::task::JoinSet::new(); diff --git a/crates/tesor/src/proxy.rs b/crates/recluse/src/proxy.rs index 5e98d23..16a322b 100644 --- a/crates/tesor/src/proxy.rs +++ b/crates/recluse/src/proxy.rs @@ -40,7 +40,7 @@ impl ProxyService { let request = Request::builder() .method(http::Method::GET) .uri(request.url.as_str()) - .header(http::header::USER_AGENT, "tesor/0.1") + .header(http::header::USER_AGENT, "recluse/0.1") .body(Empty::<Bytes>::new()) .unwrap(); diff --git a/crates/tesor/src/storage.rs b/crates/recluse/src/storage.rs index bc751a7..bc751a7 100644 --- a/crates/tesor/src/storage.rs +++ b/crates/recluse/src/storage.rs diff --git a/crates/tesor/src/telemetry.rs b/crates/recluse/src/telemetry.rs index 9036924..df39fa0 100644 --- a/crates/tesor/src/telemetry.rs +++ b/crates/recluse/src/telemetry.rs @@ -102,9 +102,9 @@ impl Drop for TelemetryService { } impl TelemetryService { pub fn init(config: &TelemetryConfig, service_name: &str, service_version: &str) -> Self { - let env_filter = match std::env::var_os("TESOR_LOG") { + let env_filter = match std::env::var_os("RECLUSE_LOG") { Some(val) => tracing_subscriber::EnvFilter::try_new(val.to_string_lossy()) - .expect("Invalid TESOR_LOG"), + .expect("Invalid RECLUSE_LOG"), None => tracing_subscriber::EnvFilter::new(Self::log_level_to_filter( config.stdout.log_level, )), diff --git a/crates/repos/src/go.rs b/crates/repos/src/go.rs index 8b08f78..eb1a928 100644 --- a/crates/repos/src/go.rs +++ b/crates/repos/src/go.rs @@ -487,10 +487,10 @@ mod tests_go_filename { upstream: Url::parse("https://dl.google.com/go/").unwrap(), ..Default::default() }; - let url = t.upstream_url(&config, "tesor:test").unwrap(); + let url = t.upstream_url(&config, "recluse:test").unwrap(); assert_eq!( url.as_str(), - "https://dl.google.com/go/go1.25.6.linux-amd64.tar.gz?source=tesor%3Atest" + "https://dl.google.com/go/go1.25.6.linux-amd64.tar.gz?source=recluse%3Atest" ); } diff --git a/crates/tesor/build.rs b/crates/tesor/build.rs deleted file mode 100644 --- a/crates/tesor/build.rs +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Nikolay Govorov -// SPDX-License-Identifier: AGPL-3.0-or-later - -fn main() { - println!("cargo::rerun-if-changed=../../Cargo.lock"); - println!("cargo::rerun-if-changed=../../mise.toml"); - println!("cargo::rerun-if-changed=../../mise.lock"); - - let out_dir = std::env::var("OUT_DIR").unwrap(); - let target = std::env::var("TARGET").unwrap(); - let output = format!("{out_dir}/licenses.json"); - - let status = std::process::Command::new("mise") - .current_dir(env!("CARGO_MANIFEST_DIR")) - .args([ - "run", - "licenses-json", - "--", - "--manifest-path", - "Cargo.toml", - "--output", - &output, - "--target", - &target, - ]) - .status() - .expect("failed to run licenses-json task"); - - assert!(status.success(), "failed to generate licenses.json"); -} diff --git a/deny.toml b/deny.toml index 3cd37da..cc39732 100644 --- a/deny.toml +++ b/deny.toml @@ -22,10 +22,11 @@ allow = [ ] exceptions = [ # The project itself is distributed under the AGPL, but avoid it in dependencies + { crate = "xtask", allow = ["AGPL-3.0-or-later"] }, { crate = "base", allow = ["AGPL-3.0-or-later"] }, { crate = "repos", allow = ["AGPL-3.0-or-later"] }, { crate = "smoke", allow = ["AGPL-3.0-or-later"] }, - { crate = "tesor", allow = ["AGPL-3.0-or-later"] }, + { crate = "recluse", allow = ["AGPL-3.0-or-later"] }, ] unused-allowed-license = "allow" diff --git a/mise.lock b/mise.lock deleted file mode 100644 --- a/mise.lock +++ /dev/null @@ -1,73 +0,0 @@ -# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html - -[[tools."aqua:taiki-e/cargo-llvm-cov"]] -version = "0.8.7" -backend = "aqua:taiki-e/cargo-llvm-cov" - -[tools."aqua:taiki-e/cargo-llvm-cov"."platforms.linux-arm64"] -checksum = "sha256:b7bb2ad514166f3b19fc06874c577eefde0010e5ac571f9ae33ff45cd19a4785" -url = "https://github.com/taiki-e/cargo-llvm-cov/releases/download/v0.8.7/cargo-llvm-cov-aarch64-unknown-linux-musl.tar.gz" -url_api = "https://api.github.com/repos/taiki-e/cargo-llvm-cov/releases/assets/418861713" - -[tools."aqua:taiki-e/cargo-llvm-cov"."platforms.linux-x64"] -checksum = "sha256:967b5cc996c29d8baa52bbb4595ef1f53af35255af8e2036ddbc6468d7b523c7" -url = "https://github.com/taiki-e/cargo-llvm-cov/releases/download/v0.8.7/cargo-llvm-cov-x86_64-unknown-linux-musl.tar.gz" -url_api = "https://api.github.com/repos/taiki-e/cargo-llvm-cov/releases/assets/418861752" - -[[tools."cargo:cargo-about"]] -version = "0.8.4" -backend = "cargo:cargo-about" - -[[tools.gh]] -version = "2.96.0" -backend = "aqua:cli/cli" - -[tools.gh."platforms.linux-arm64"] -checksum = "sha256:06f86ec7103d41993b76cd78072f43595c34aaa56506d971d9860e67140bf909" -url = "https://github.com/cli/cli/releases/download/v2.96.0/gh_2.96.0_linux_arm64.tar.gz" -url_api = "https://api.github.com/repos/cli/cli/releases/assets/464728549" -provenance = "github-attestations" - -[tools.gh."platforms.linux-x64"] -checksum = "sha256:83d5c2ccad5498f58bf6368acb1ab32588cf43ab3a4b1c301bf36328b1c8bd60" -url = "https://github.com/cli/cli/releases/download/v2.96.0/gh_2.96.0_linux_amd64.tar.gz" -url_api = "https://api.github.com/repos/cli/cli/releases/assets/464728543" -provenance = "github-attestations" - -[[tools.jq]] -version = "1.8.2" -backend = "aqua:jqlang/jq" - -[tools.jq."platforms.linux-arm64"] -checksum = "sha256:8b85c817833814ddca00a144c33705546355afccf0cf39b188f3cdb48b852309" -url = "https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-arm64" -url_api = "https://api.github.com/repos/jqlang/jq/releases/assets/453012756" -provenance = "github-attestations" - -[tools.jq."platforms.linux-x64"] -checksum = "sha256:b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f" -url = "https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64" -url_api = "https://api.github.com/repos/jqlang/jq/releases/assets/453012752" -provenance = "github-attestations" - -[[tools.rust]] -version = "1.97.1" -backend = "core:rust" - -[tools.rust.options] -components = "clippy,llvm-tools-preview,rustfmt" -profile = "minimal" - -[[tools.shellcheck]] -version = "0.11.0" -backend = "aqua:koalaman/shellcheck" - -[tools.shellcheck."platforms.linux-arm64"] -checksum = "sha256:12b331c1d2db6b9eb13cfca64306b1b157a86eb69db83023e261eaa7e7c14588" -url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.aarch64.tar.xz" -url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056934" - -[tools.shellcheck."platforms.linux-x64"] -checksum = "sha256:8c3be12b05d5c177a04c29e3c78ce89ac86f1595681cab149b65b97c4e227198" -url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.x86_64.tar.xz" -url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056942" diff --git a/mise.toml b/mise.toml index 142594c..e6867f4 100644 --- a/mise.toml +++ b/mise.toml @@ -6,60 +6,8 @@ min_version = "2026.7.5" [settings] experimental = true -[tools] -"aqua:taiki-e/cargo-llvm-cov" = "0.8.7" -"cargo:cargo-about" = "0.8.4" -gh = "2.96.0" -jq = "1.8.2" -rust = { version = "1.97.1", profile = "minimal", components = [ "clippy", "llvm-tools-preview", "rustfmt" ] } -shellcheck = "0.11.0" - -[bootstrap.packages] -# APT -"apt:apt-utils" = "latest" -"apt:build-essential" = "latest" -"apt:debsigs" = "latest" -"apt:createrepo-c" = "latest" -"apt:gnupg" = "latest" -"apt:libssl-dev" = "latest" -"apt:pkg-config" = "latest" -"apt:rpm" = "latest" - -# DNF -"dnf:createrepo_c" = "latest" -"dnf:gcc" = "latest" -"dnf:gnupg2" = "latest" -"dnf:openssl-devel" = "latest" -"dnf:pkgconf-pkg-config" = "latest" -"dnf:rpm" = "latest" - [task_config] dir = "{{cwd}}" includes = [ - "git::https://git.dimidiumlabs.io/platform.git//tasks?ref=159ce265b95d655342c378d9aaa7dc14dbf1dfa4", + "git::https://github.com/dimidiumlabs/infra.git//tasks?ref=1934f5fb264ab046031d5d1b61227de2e1f8dde9", ] - -[tasks.fmt] -run = "cargo fmt --all --check" - -[tasks.clippy] -run = "cargo clippy --all-targets --all-features -- -D warnings" - -[tasks.test] -run = """ -cargo build --release -cargo test --all-features --release --locked -""" - -[tasks.smoke] -run = "cargo run -p smoke" - -[tasks.smoke-local] -run = "./tests/smoke/run-local.sh" - -[tasks.coverage] -run = """ -mkdir -p target/coverage/ -cargo llvm-cov --all-features --workspace --lcov --output-path target/coverage/lcov.info -cargo llvm-cov --all-features --workspace --html --output-dir target/coverage/html -""" diff --git a/nfpm.yaml b/nfpm.yaml index 5704fcb..c53abb5 100644 --- a/nfpm.yaml +++ b/nfpm.yaml @@ -4,7 +4,7 @@ # yaml-language-server: $schema=https://nfpm.goreleaser.com/schema.json # vim: set ts=2 sw=2 tw=0 fo=cnqoj -name: tesor +name: recluse arch: ${ARCH} version: ${VERSION} license: AGPL-3.0-or-later @@ -14,34 +14,34 @@ description: Tiny packages caching proxy contents: - src: ./LICENSE - dst: /usr/share/doc/tesor/LICENSE + dst: /usr/share/doc/recluse/LICENSE - src: ./README.md - dst: /usr/share/doc/tesor/README.md + dst: /usr/share/doc/recluse/README.md - - src: target/release/tesor - dst: /usr/local/bin/tesor + - src: target/release/recluse + dst: /usr/local/bin/recluse file_info: mode: 0755 - - src: pkg/tesor.toml - dst: /etc/tesor.toml + - src: pkg/recluse.toml + dst: /etc/recluse.toml type: config|noreplace file_info: mode: 0640 owner: root - group: tesor + group: recluse - - src: pkg/tesor.service - dst: /usr/lib/systemd/system/tesor.service + - src: pkg/recluse.service + dst: /usr/lib/systemd/system/recluse.service file_info: mode: 0644 - - dst: /var/lib/tesor + - dst: /var/lib/recluse type: dir file_info: mode: 0755 owner: root - group: tesor + group: recluse scripts: diff --git a/pkg/tesor.service b/pkg/recluse.service index c2d463d..f3ab476 100644 --- a/pkg/tesor.service +++ b/pkg/recluse.service @@ -2,21 +2,21 @@ # SPDX-License-Identifier: AGPL-3.0-or-later [Unit] -Description=tesor server (tiny packages caching proxy) +Description=recluse server (tiny packages caching proxy) Requires=network-online.target After=time-sync.target network-online.target remote-fs.target nss-lookup.target Wants=time-sync.target [Service] Type=notify -User=tesor -Group=tesor +User=recluse +Group=recluse Restart=always RestartSec=30 WatchdogSec=30 NotifyAccess=main -ExecPaths=/usr/local/bin/tesor /usr/lib -ExecStart=/usr/local/bin/tesor --config=/etc/tesor.toml +ExecPaths=/usr/local/bin/recluse /usr/lib +ExecStart=/usr/local/bin/recluse --config=/etc/recluse.toml LimitCORE=infinity LimitNOFILE=500000 AmbientCapabilities=CAP_NET_BIND_SERVICE diff --git a/pkg/tesor.toml b/pkg/recluse.toml index 7296086..751672b 100644 --- a/pkg/tesor.toml +++ b/pkg/recluse.toml @@ -1,13 +1,13 @@ # SPDX-FileCopyrightText: 2026 Nikolay Govorov # SPDX-License-Identifier: AGPL-3.0-or-later -# tesor will dutifully indicate its user agent and protocols (for example, source for zig). +# recluse will dutifully indicate its user agent and protocols (for example, source for zig). # Specify your name if you want your instance to be identified. -appname="tesor" +appname="recluse" # Path to the directory for storing state (indexes, caches, statistics). # Must be a writable directory. -dirname="/var/lib/tesor" +dirname="/var/lib/recluse" # Common server settings [server] @@ -25,8 +25,8 @@ addr = "0.0.0.0:2025" hostnames = ["localhost", "127.0.0.1", "::1"] # For HTTPS, set tls_cert and tls_key to PEM file paths. -# tls_crt = "/etc/tesor/cert.pem" -# tls_key = "/etc/tesor/key.pem" +# tls_crt = "/etc/recluse/cert.pem" +# tls_key = "/etc/recluse/key.pem" # Telemetry configuration # Logs to stdout (pretty or json format) @@ -47,13 +47,13 @@ log_format = "pretty" # pretty, json # log_level = "info" # # # For gRPC with TLS, specify certificates: -# # tls_ca = "/etc/tesor/otel-ca.crt" -# # tls_crt = "/etc/tesor/otel-client.crt" # for mTLS -# # tls_key = "/etc/tesor/otel-client.key" # for mTLS +# # tls_ca = "/etc/recluse/otel-ca.crt" +# # tls_crt = "/etc/recluse/otel-client.crt" # for mTLS +# # tls_key = "/etc/recluse/otel-client.key" # for mTLS # # # HTTP headers for authentication: # # [telemetry.otelcol.headers] # # Authorization = "Bearer <token>" -# Environment variable TESOR_LOG overrides log_level settings. -# Example: TESOR_LOG=debug,sqlx=warn +# Environment variable RECLUSE_LOG overrides log_level settings. +# Example: RECLUSE_LOG=debug,sqlx=warn diff --git a/pkg/scripts/postinstall.sh b/pkg/scripts/postinstall.sh index 6e391fc..204f44a 100644 --- a/pkg/scripts/postinstall.sh +++ b/pkg/scripts/postinstall.sh @@ -4,7 +4,7 @@ set -e -if [ -x "/bin/systemctl" ] && [ -d /run/systemd/system ] && [ -f /usr/lib/systemd/system/tesor.service ]; then +if [ -x "/bin/systemctl" ] && [ -d /run/systemd/system ] && [ -f /usr/lib/systemd/system/recluse.service ]; then /bin/systemctl daemon-reload - /bin/systemctl enable tesor + /bin/systemctl enable recluse fi diff --git a/pkg/scripts/preinstall.sh b/pkg/scripts/preinstall.sh index 175eaf1..95c374d 100644 --- a/pkg/scripts/preinstall.sh +++ b/pkg/scripts/preinstall.sh @@ -4,13 +4,14 @@ set -e -TESOR_USER=${TESOR_USER:-tesor} -TESOR_GROUP=${TESOR_GROUP:-${TESOR_USER}} +PROGRAM=recluse +RECLUSE_USER=${RECLUSE_USER:-recluse} +RECLUSE_GROUP=${RECLUSE_GROUP:-${RECLUSE_USER}} -if ! getent group "$TESOR_GROUP" >/dev/null; then - groupadd --system "$TESOR_GROUP" +if ! getent group $RECLUSE_GROUP >/dev/null; then + groupadd --system $RECLUSE_GROUP fi -if ! getent passwd "$TESOR_USER" >/dev/null; then - useradd --system --gid "$TESOR_GROUP" --no-create-home --shell /usr/sbin/nologin "$TESOR_USER" +if ! getent passwd $RECLUSE_USER >/dev/null; then + useradd --system --gid $RECLUSE_GROUP --no-create-home --shell /usr/sbin/nologin $RECLUSE_USER fi diff --git a/pkg/scripts/preremove.sh b/pkg/scripts/preremove.sh index 9cd6599..3c16f62 100644 --- a/pkg/scripts/preremove.sh +++ b/pkg/scripts/preremove.sh @@ -4,7 +4,7 @@ set -e -if [ -x "/bin/systemctl" ] && [ -d /run/systemd/system ] && [ -f /usr/lib/systemd/system/tesor.service ]; then - /bin/systemctl stop tesor.service || true - /bin/systemctl disable tesor.service || true +if [ -x "/bin/systemctl" ] && [ -d /run/systemd/system ] && [ -f /usr/lib/systemd/system/recluse.service ]; then + /bin/systemctl stop recluse.service || true + /bin/systemctl disable recluse.service || true fi diff --git a/tests/smoke/run-local.sh b/tests/smoke/run-local.sh index 4b72441..22b5a72 100755 --- a/tests/smoke/run-local.sh +++ b/tests/smoke/run-local.sh @@ -2,25 +2,25 @@ # SPDX-FileCopyrightText: 2026 Nikolay Govorov # SPDX-License-Identifier: AGPL-3.0-or-later -# Starts tesor in a temporary directory, runs smoke tests, cleans up. -# Expects a prebuilt binary at target/release/tesor (run `cargo build --release` first). +# Starts recluse in a temporary directory, runs smoke tests, cleans up. +# Expects a prebuilt binary at target/release/recluse (run `cargo build --release` first). # # Usage: # ./tests/smoke/run-local.sh # default port 2025 -# TESOR_PORT=9999 ./tests/smoke/run-local.sh +# RECLUSE_PORT=9999 ./tests/smoke/run-local.sh set -euo pipefail -BIN="target/release/tesor" -PORT="${TESOR_PORT:-2025}" +BIN="target/release/recluse" +PORT="${RECLUSE_PORT:-2025}" BASE_URL="http://127.0.0.1:${PORT}" TMPDIR="$(mktemp -d)" -LOGFILE="$TMPDIR/tesor.log" +LOGFILE="$TMPDIR/recluse.log" trap 'kill "$PID" 2>/dev/null; wait "$PID" 2>/dev/null; rm -rf "$TMPDIR"' EXIT # Write minimal config -cat > "$TMPDIR/tesor.toml" <<EOF +cat > "$TMPDIR/recluse.toml" <<EOF appname = "smoke" dirname = "$TMPDIR/state" @@ -44,10 +44,10 @@ EOF mkdir -p "$TMPDIR/state" -# Start tesor -echo "Starting tesor on ${BASE_URL}..." +# Start recluse +echo "Starting recluse on ${BASE_URL}..." echo "Server log: ${LOGFILE}" -"$BIN" --config="$TMPDIR/tesor.toml" >"$LOGFILE" 2>&1 & +"$BIN" --config="$TMPDIR/recluse.toml" >"$LOGFILE" 2>&1 & PID=$! # Wait for index to load (log shows "index refreshed" for each backend) @@ -58,7 +58,7 @@ for i in $(seq 1 120); do break fi if ! kill -0 "$PID" 2>/dev/null; then - echo "tesor exited unexpectedly. Server log:" + echo "recluse exited unexpectedly. Server log:" cat "$LOGFILE" exit 1 fi @@ -72,4 +72,4 @@ if ! grep -q "index refreshed" "$LOGFILE" 2>/dev/null; then fi # Run smoke tests -TESOR_URL="${BASE_URL}" cargo run -p smoke +RECLUSE_URL="${BASE_URL}" cargo run -p smoke diff --git a/tests/smoke/src/main.rs b/tests/smoke/src/main.rs index ca109f9..f23a233 100644 --- a/tests/smoke/src/main.rs +++ b/tests/smoke/src/main.rs @@ -1,13 +1,13 @@ // SPDX-FileCopyrightText: 2026 Nikolay Govorov // SPDX-License-Identifier: AGPL-3.0-or-later -// Black-box smoke tests for Tesor. +// Black-box smoke tests for Recluse. // -// Downloads archives from Tesor and upstream, compares sha256 hashes, +// Downloads archives from Recluse and upstream, compares sha256 hashes, // verifies that minisig signatures (Zig) and sha256 checksums (Go) match upstream. // // Usage: -// TESOR_URL=https://pkg.earth cargo run -p smoke +// RECLUSE_URL=https://pkg.earth cargo run -p smoke use sha2::{Digest, Sha256}; @@ -198,8 +198,8 @@ fn test_web(r: &mut Runner, base_url: &str) { r.fail(name, &format!("status {status}, expected 200")); } else if !ct.contains("text/html") { r.fail(name, &format!("content-type: {ct}")); - } else if !body.contains("Tesor") { - r.fail(name, "body missing \"Tesor\""); + } else if !body.contains("Recluse") { + r.fail(name, "body missing \"Recluse\""); } else { r.ok(name); } @@ -274,11 +274,11 @@ fn test_go(r: &mut Runner, base_url: &str) { r.fetch_bytes(&format!("{base_url}/go/{file}")), r.fetch_bytes(&format!("https://go.dev/dl/{file}")), ) { - (Ok(tesor), Ok(upstream)) => { - let zh = sha256hex(&tesor); + (Ok(recluse), Ok(upstream)) => { + let zh = sha256hex(&recluse); let uh = sha256hex(&upstream); if zh != uh { - r.fail(&name, &format!("tesor={zh} upstream={uh}")); + r.fail(&name, &format!("recluse={zh} upstream={uh}")); } else { r.ok(&name); } @@ -332,11 +332,11 @@ fn test_zig(r: &mut Runner, base_url: &str) { r.fetch_bytes(&format!("{base_url}/zig/{}", entry.file)), r.fetch_bytes(&zig_upstream_url(entry.file, entry.version)), ) { - (Ok(tesor), Ok(upstream)) => { - let zh = sha256hex(&tesor); + (Ok(recluse), Ok(upstream)) => { + let zh = sha256hex(&recluse); let uh = sha256hex(&upstream); if zh != uh { - r.fail(&name, &format!("tesor={zh} upstream={uh}")); + r.fail(&name, &format!("recluse={zh} upstream={uh}")); } else { r.ok(&name); } @@ -348,11 +348,11 @@ fn test_zig(r: &mut Runner, base_url: &str) { // Signature: compare .minisig with upstream { let name = format!("zig: {}.minisig matches upstream", entry.file); - let tesor_url = format!("{base_url}/zig/{}.minisig", entry.file); + let recluse_url = format!("{base_url}/zig/{}.minisig", entry.file); let upstream_url = format!("{}.minisig", zig_upstream_url(entry.file, entry.version)); - match (r.fetch_bytes(&tesor_url), r.fetch_bytes(&upstream_url)) { - (Ok(tesor), Ok(upstream)) => { - if tesor != upstream { + match (r.fetch_bytes(&recluse_url), r.fetch_bytes(&upstream_url)) { + (Ok(recluse), Ok(upstream)) => { + if recluse != upstream { r.fail(&name, "minisig content differs from upstream"); } else { r.ok(&name); @@ -374,11 +374,11 @@ fn test_zig(r: &mut Runner, base_url: &str) { } fn main() { - let base_url = match std::env::var("TESOR_URL") { + let base_url = match std::env::var("RECLUSE_URL") { Ok(url) => url, Err(_) => { - eprintln!("Error: TESOR_URL environment variable is not set."); - eprintln!("Usage: TESOR_URL=https://pkg.earth cargo run -p smoke"); + eprintln!("Error: RECLUSE_URL environment variable is not set."); + eprintln!("Usage: RECLUSE_URL=https://pkg.earth cargo run -p smoke"); std::process::exit(1); } }; diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2026 Nikolay Govorov +# SPDX-License-Identifier: AGPL-3.0-or-later + +[package] +name = "xtask" +edition.workspace = true +publish.workspace = true +license.workspace = true + +[dependencies] +krates = "0.20" +rayon = "1.10" +tracing.workspace = true +serde.workspace = true +serde_json.workspace = true +spdx = { version = "0.13", features = ["detection-inline-cache", "text"] } diff --git a/xtask/build.rs b/xtask/build.rs new file mode 100644 --- /dev/null +++ b/xtask/build.rs @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2026 Nikolay Govorov +// SPDX-License-Identifier: AGPL-3.0-or-later + +fn main() { + println!( + "cargo:rustc-env=TARGET={}", + std::env::var("TARGET").unwrap() + ); +} diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs new file mode 100644 --- /dev/null +++ b/xtask/src/lib.rs @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: 2026 Nikolay Govorov +// SPDX-License-Identifier: AGPL-3.0-or-later + +pub mod licenses; diff --git a/xtask/src/licenses.rs b/xtask/src/licenses.rs new file mode 100644 --- /dev/null +++ b/xtask/src/licenses.rs @@ -0,0 +1,614 @@ +// SPDX-FileCopyrightText: 2026 Nikolay Govorov +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// The approach to gathering licenses — resolving the dependency graph via +// krates, scanning crate sources for license texts with spdx detection, +// and deduplicating results — is based on cargo-about +// (https://github.com/EmbarkStudios/cargo-about) by Embark Studios, +// licensed under MIT OR Apache-2.0. + +use std::collections::BTreeMap; +use std::{cmp, fmt}; + +use krates::cm; +use krates::{Utf8Path, Utf8PathBuf}; +use serde::Serialize; +use spdx::detection as sd; +use spdx::{Expression, LicenseReq, Licensee}; + +const CONFIDENCE_THRESHOLD: f32 = 0.8; + +const IGNORE_PRIVATE: bool = true; +const IGNORE_DEV_DEPENDENCIES: bool = true; +const IGNORE_BUILD_DEPENDENCIES: bool = true; +const IGNORE_TRANSITIVE_DEPENDENCIES: bool = false; + +struct Crate(cm::Package); + +impl Crate { + fn get_license_expression(&self) -> LicenseInfo { + if let Some(license_field) = &self.0.license { + match Crate::parse_license_expression(license_field) { + Ok(validated) => LicenseInfo::Expr(validated), + Err(err) => { + tracing::error!("unable to parse license expression for '{self}': {err}"); + LicenseInfo::Unknown + } + } + } else { + tracing::warn!("crate '{self}' doesn't have a license field"); + LicenseInfo::Unknown + } + } + + fn parse_license_expression(license: &str) -> Result<Expression, spdx::ParseError> { + Expression::parse_mode( + license, + spdx::ParseMode { + allow_deprecated: true, + allow_imprecise_license_names: true, + allow_slash_as_or_operator: false, + allow_postfix_plus_on_gpl: true, + allow_unknown: false, + }, + ) + } +} + +impl Ord for Crate { + fn cmp(&self, o: &Self) -> cmp::Ordering { + match self.0.name.cmp(&o.0.name) { + cmp::Ordering::Equal => self.0.version.cmp(&o.0.version), + o => o, + } + } +} + +impl PartialOrd for Crate { + fn partial_cmp(&self, o: &Self) -> Option<cmp::Ordering> { + Some(self.cmp(o)) + } +} + +impl Eq for Crate {} + +impl PartialEq for Crate { + fn eq(&self, o: &Self) -> bool { + self.cmp(o) == cmp::Ordering::Equal + } +} + +impl From<cm::Package> for Crate { + fn from(mut pkg: cm::Package) -> Self { + // Fix the license field as cargo used to allow the invalid / separator + if let Some(lf) = &mut pkg.license { + *lf = lf.replace('/', " OR "); + } + + Self(pkg) + } +} + +impl krates::KrateDetails for Crate { + fn name(&self) -> &str { + &self.0.name + } + + fn version(&self) -> &krates::semver::Version { + &self.0.version + } +} + +impl fmt::Display for Crate { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.0.name, self.0.version) + } +} + +impl std::ops::Deref for Crate { + type Target = cm::Package; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +type Krates = krates::Krates<Crate>; + +fn get_all_crates(cargo_toml: &Utf8Path) -> Result<Krates, krates::Error> { + let mut mdc = krates::Cmd::new(); + mdc.manifest_path(cargo_toml); + + let mut builder = krates::Builder::new(); + + if IGNORE_BUILD_DEPENDENCIES { + builder.ignore_kind(krates::DepKind::Build, krates::Scope::All); + } + + if IGNORE_DEV_DEPENDENCIES { + builder.ignore_kind(krates::DepKind::Dev, krates::Scope::All); + } + + if IGNORE_TRANSITIVE_DEPENDENCIES { + builder.ignore_kind(krates::DepKind::Normal, krates::Scope::NonWorkspace); + builder.ignore_kind(krates::DepKind::Dev, krates::Scope::NonWorkspace); + builder.ignore_kind(krates::DepKind::Build, krates::Scope::NonWorkspace); + } + + builder.include_targets(std::iter::once((env!("TARGET"), vec![]))); + + let graph = builder.build(mdc, |filtered: cm::Package| { + tracing::debug!("filtered {} {}", filtered.name, filtered.version); + })?; + + Ok(graph) +} + +type LicenseStore = sd::Store; + +fn load_license_store() -> Result<LicenseStore, Box<dyn std::error::Error>> { + Ok(sd::Store::load_inline()?) +} + +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +enum LicenseInfo { + Expr(Expression), + Unknown, +} + +enum LicenseFileKind { + /// The license file is the canonical text of the license + Text(String), + /// The file just has a license header + Header, +} + +struct LicenseFile { + license_expr: Expression, + confidence: f32, + kind: LicenseFileKind, +} + +impl Ord for LicenseFile { + fn cmp(&self, o: &Self) -> cmp::Ordering { + match self.license_expr.as_ref().cmp(o.license_expr.as_ref()) { + cmp::Ordering::Equal => o + .confidence + .partial_cmp(&self.confidence) + .expect("NaN encountered comparing license confidences"), + ord => ord, + } + } +} + +impl PartialOrd for LicenseFile { + fn partial_cmp(&self, o: &Self) -> Option<cmp::Ordering> { + Some(self.cmp(o)) + } +} + +impl PartialEq for LicenseFile { + fn eq(&self, o: &Self) -> bool { + self.cmp(o) == cmp::Ordering::Equal + } +} + +impl Eq for LicenseFile {} + +struct KrateLicense<'krate> { + krate: &'krate Crate, + lic_info: LicenseInfo, + license_files: Vec<LicenseFile>, +} + +fn walk_files(dir: &Utf8Path) -> Vec<Utf8PathBuf> { + let mut files = Vec::new(); + let mut stack = vec![dir.to_path_buf()]; + + while let Some(current) = stack.pop() { + let entries = match std::fs::read_dir(¤t) { + Ok(entries) => entries, + Err(e) => { + tracing::warn!("failed to read directory '{current}': {e}"); + continue; + } + }; + + for entry in entries.filter_map(|e| e.ok()) { + let ft = match entry.file_type() { + Ok(ft) => ft, + Err(_) => continue, + }; + + let path = match Utf8PathBuf::from_path_buf(entry.path()) { + Ok(pb) => pb, + Err(e) => { + tracing::warn!("skipping path {}, not a valid utf-8 path", e.display()); + continue; + } + }; + + if ft.is_dir() { + stack.push(path); + } else if ft.is_file() { + files.push(path); + } + } + } + + files +} + +fn scan_files( + root_dir: &Utf8Path, + scanner: &sd::scan::Scanner<'_>, + threshold: f32, +) -> Vec<LicenseFile> { + walk_files(root_dir) + .into_iter() + .filter_map(|path| { + let contents = read_file(&path)?; + check_is_license_file(path, contents, scanner, threshold) + }) + .collect() +} + +fn read_file(path: &Utf8Path) -> Option<String> { + match std::fs::read_to_string(path) { + Err(ref e) if e.kind() == std::io::ErrorKind::InvalidData => { + tracing::debug!("binary file '{path}' detected"); + None + } + Err(e) => { + tracing::error!("failed to read '{path}': {e}"); + None + } + Ok(c) => Some(c), + } +} + +fn check_is_license_file( + path: Utf8PathBuf, + contents: String, + scanner: &sd::scan::Scanner<'_>, + threshold: f32, +) -> Option<LicenseFile> { + match scan_text(&contents, scanner, threshold) { + ScanResult::Header(ided) => { + let license_expr = match Expression::parse(ided.id.name) { + Ok(expr) => expr, + Err(err) => { + tracing::error!( + "failed to parse license '{}' at {path:?} into a valid expression: {err}", + ided.id.name + ); + return None; + } + }; + + Some(LicenseFile { + license_expr, + confidence: ided.confidence, + kind: LicenseFileKind::Header, + }) + } + ScanResult::Text(ided) => { + let license_expr = match Expression::parse(ided.id.name) { + Ok(expr) => expr, + Err(err) => { + tracing::error!( + "failed to parse license '{}' at {path:?} into a valid expression: {err}", + ided.id.name + ); + return None; + } + }; + + Some(LicenseFile { + license_expr, + confidence: ided.confidence, + kind: LicenseFileKind::Text(contents), + }) + } + ScanResult::UnknownId(id_str) => { + tracing::error!("found unknown SPDX identifier '{id_str}' scanning '{path}'"); + None + } + ScanResult::LowLicenseChance(ided) => { + tracing::debug!( + "found '{}' scanning '{path}' but it only has a confidence score of {}", + ided.id.name, + ided.confidence, + ); + None + } + ScanResult::NoLicense => None, + } +} + +struct Identified { + confidence: f32, + id: spdx::LicenseId, +} + +enum ScanResult { + Header(Identified), + Text(Identified), + UnknownId(String), + LowLicenseChance(Identified), + NoLicense, +} + +fn scan_text(contents: &str, strat: &sd::scan::Scanner<'_>, threshold: f32) -> ScanResult { + let text = spdx::detection::TextData::new(contents); + let lic_match = strat.scan(&text); + + let Some(identified) = lic_match.license else { + return ScanResult::NoLicense; + }; + + let lic_id = match spdx::license_id(identified.name) { + Some(id) => Identified { + confidence: lic_match.score, + id, + }, + None => return ScanResult::UnknownId(identified.name.to_owned()), + }; + + use spdx::detection::LicenseType; + + if lic_match.score >= threshold { + match identified.kind { + LicenseType::Header => ScanResult::Header(lic_id), + LicenseType::Original => ScanResult::Text(lic_id), + LicenseType::Alternate => { + panic!("Alternate license detected") + } + } + } else { + ScanResult::LowLicenseChance(lic_id) + } +} + +fn gather_licenses<'k>(krates: &'k Krates, store: &LicenseStore) -> Vec<KrateLicense<'k>> { + use rayon::prelude::*; + + let min_threshold = (CONFIDENCE_THRESHOLD - 0.5).max(0.1); + + let scanner = sd::scan::Scanner::new(store) + .confidence_threshold(min_threshold) + .optimize(false) + .max_passes(1); + + let mut licensed_krates: Vec<_> = krates + .krates() + .par_bridge() + .filter_map(|krate| { + // Skip private/workspace crates + if IGNORE_PRIVATE + && let Some(publish) = &krate.publish + && publish.is_empty() + { + tracing::debug!("ignoring private crate '{krate}'"); + return None; + } + + let lic_info = krate.get_license_expression(); + let root_path = krate.manifest_path.parent().unwrap(); + + let mut license_files = scan_files(root_path, &scanner, CONFIDENCE_THRESHOLD); + + // Condense each license down to the best candidate if + // multiple are found + license_files.sort(); + let mut last_expr = None; + license_files.retain(|lf| { + let dominated = last_expr.as_ref() == Some(&lf.license_expr); + last_expr = Some(lf.license_expr.clone()); + !dominated + }); + + Some(KrateLicense { + krate, + lic_info, + license_files, + }) + }) + .collect(); + + licensed_krates.sort_by(|a, b| a.krate.cmp(b.krate)); + licensed_krates +} + +/// For an OR expression like "MIT OR Apache-2.0", pick the minimal set of +/// licenses to satisfy the expression. Actual license policy validation +/// is handled by `cargo deny`. +fn pick_licenses(expr: &Expression) -> Vec<LicenseReq> { + let accepted: Vec<Licensee> = expr + .requirements() + .filter_map(|r| { + r.req + .license + .id() + .map(|id| Licensee::parse(id.name).unwrap()) + }) + .collect(); + + expr.minimized_requirements(&accepted).unwrap_or_default() +} + +/// For crates without a `license` field, synthesize requirements from +/// scanned license files. +fn synthesize_from_files(files: &[LicenseFile]) -> Vec<LicenseReq> { + let mut reqs = Vec::new(); + for lf in files { + for req in lf.license_expr.requirements() { + if !reqs.contains(&req.req) { + reqs.push(req.req.clone()); + } + } + } + reqs +} + +fn effective_licenses(kl: &KrateLicense<'_>) -> Vec<LicenseReq> { + match &kl.lic_info { + LicenseInfo::Expr(expr) => pick_licenses(expr), + LicenseInfo::Unknown => { + if kl.license_files.is_empty() { + tracing::warn!( + "unable to determine license for '{}': no `license` specified, and no license files were found", + kl.krate + ); + Vec::new() + } else { + synthesize_from_files(&kl.license_files) + } + } + } +} + +#[derive(Clone, Serialize)] +struct UsedBy { + #[serde(rename = "crate")] + krate: UsedByCrate, +} + +#[derive(Clone, Serialize)] +struct UsedByCrate { + name: String, + version: String, + repository: Option<String>, +} + +#[derive(Clone, Serialize)] +struct License { + name: String, + id: String, + first_of_kind: bool, + text: String, + used_by: Vec<UsedBy>, +} + +#[derive(Serialize)] +struct LicenseSet { + count: usize, + name: String, + id: String, +} + +#[derive(Serialize)] +struct LicenseList { + overview: Vec<LicenseSet>, + licenses: Vec<License>, +} + +fn generate(nfos: &[KrateLicense<'_>]) -> LicenseList { + let mut licenses_map: BTreeMap<String, BTreeMap<String, License>> = BTreeMap::new(); + + for krate_license in nfos { + let reqs = effective_licenses(krate_license); + + for license_req in &reqs { + let spdx::LicenseItem::Spdx { id, .. } = license_req.license else { + tracing::warn!( + "{license_req} has no license file for crate '{}'", + krate_license.krate + ); + continue; + }; + + // Try to find actual license text from scanned files + let license_text = krate_license + .license_files + .iter() + .find_map(|lf| { + if !lf + .license_expr + .evaluate(|ereq| ereq.license.id() == Some(id)) + { + return None; + } + + match &lf.kind { + LicenseFileKind::Text(text) => Some(text.clone()), + LicenseFileKind::Header => None, + } + }) + .unwrap_or_else(|| { + tracing::debug!( + "unable to find text for license '{license_req}' for crate '{}', falling back to canonical text", + krate_license.krate + ); + id.text().to_owned() + }); + + let used_by = UsedBy { + krate: UsedByCrate { + name: krate_license.krate.name.clone(), + version: krate_license.krate.version.to_string(), + repository: krate_license.krate.repository.clone(), + }, + }; + + let entry = licenses_map.entry(id.full_name.to_owned()).or_default(); + + let lic = entry + .entry(license_text.clone()) + .or_insert_with(|| License { + name: id.full_name.to_owned(), + id: id.name.to_owned(), + text: license_text, + used_by: Vec::new(), + first_of_kind: false, + }); + lic.used_by.push(used_by); + } + } + + let mut licenses: Vec<_> = licenses_map + .into_values() + .flat_map(|v| v.into_values()) + .collect(); + + for lic in &mut licenses { + lic.used_by.sort_by_key(|used| used.krate.name.len()); + } + + licenses.sort_by(|a, b| a.id.cmp(&b.id)); + + let mut overview_map: BTreeMap<&str, LicenseSet> = BTreeMap::new(); + + for lic in &mut licenses { + let ls = overview_map.entry(&lic.id).or_insert_with(|| { + lic.first_of_kind = true; + LicenseSet { + count: 0, + name: lic.name.clone(), + id: lic.id.clone(), + } + }); + ls.count += lic.used_by.len(); + } + + let mut overview: Vec<_> = overview_map.into_values().collect(); + overview.sort_by(|a, b| a.name.cmp(&b.name)); + + LicenseList { overview, licenses } +} + +/// Gathers all dependency licenses and returns a JSON string. +pub fn generate_json(manifest_path: &str) -> Result<String, Box<dyn std::error::Error>> { + let manifest_path = Utf8PathBuf::from(manifest_path); + if !manifest_path.exists() { + return Err(format!("manifest path '{manifest_path}' does not exist").into()); + } + + let krates = get_all_crates(&manifest_path)?; + let store = load_license_store()?; + let summary = gather_licenses(&krates, &store); + let list = generate(&summary); + + Ok(serde_json::to_string_pretty(&list)?) +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2026 Nikolay Govorov +// SPDX-License-Identifier: AGPL-3.0-or-later + +fn main() -> Result<(), Box<dyn std::error::Error>> { + let args: Vec<String> = std::env::args().skip(1).collect(); + + match args.first().map(|s| s.as_str()) { + Some("licenses") => { + let json = xtask::licenses::generate_json("crates/recluse/Cargo.toml")?; + println!("{json}"); + Ok(()) + } + Some(cmd) => Err(format!("unknown command: {cmd}").into()), + None => Err( + "usage: cargo xtask <command>\n\ncommands:\n licenses Generate dependency license JSON" + .into(), + ), + } +} |
