aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat
-rw-r--r--.cargo/config.toml (renamed from CODEOWNERS)3+2 −1
-rw-r--r--.dockerignore9+0 −9
-rw-r--r--.editorconfig26+0 −26
-rw-r--r--.github/workflows/build.yml202+34 −168
-rw-r--r--.gitignore6+2 −4
-rw-r--r--CLA.md4+2 −2
-rw-r--r--Cargo.lock229+179 −50
-rw-r--r--Cargo.toml4+2 −2
-rw-r--r--Dockerfile29+0 −29
-rw-r--r--Makefile49+49 −0
-rw-r--r--README.md42+21 −21
-rw-r--r--REUSE.toml15+7 −8
-rw-r--r--charts/tesor/Chart.yaml10+0 −10
-rw-r--r--charts/tesor/templates/_helpers.tpl37+0 −37
-rw-r--r--charts/tesor/templates/configmap.yaml46+0 −46
-rw-r--r--charts/tesor/templates/deployment.yaml97+0 −97
-rw-r--r--charts/tesor/templates/httproute.yaml26+0 −26
-rw-r--r--charts/tesor/templates/pvc.yaml22+0 −22
-rw-r--r--charts/tesor/templates/service.yaml19+0 −19
-rw-r--r--charts/tesor/values.yaml78+0 −78
-rw-r--r--crates/recluse/Cargo.toml (renamed from crates/tesor/Cargo.toml)8+6 −2
-rw-r--r--crates/recluse/build.rs13+13 −0
-rw-r--r--crates/recluse/src/assets/apple-touch-icon.png (renamed from crates/tesor/src/assets/apple-touch-icon.png)bin715 -> 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)bin715 -> 715 bytes
-rw-r--r--crates/recluse/src/assets/favicon-512.png (renamed from crates/tesor/src/assets/favicon-512.png)bin2329 -> 2329 bytes
-rw-r--r--crates/recluse/src/assets/favicon.ico (renamed from crates/tesor/src/assets/favicon.ico)bin32038 -> 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)bin122452 -> 122452 bytes
-rw-r--r--crates/recluse/src/assets/jetbrainsmono/JetBrainsMono[wght].woff2 (renamed from crates/tesor/src/assets/jetbrainsmono/JetBrainsMono[wght].woff2)bin113700 -> 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.rs4+2 −2
-rw-r--r--crates/tesor/build.rs30+0 −30
-rw-r--r--deny.toml3+2 −1
-rw-r--r--mise.lock34+0 −34
-rw-r--r--mise.toml39+6 −33
-rw-r--r--nfpm.yaml26+14 −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.sh4+2 −2
-rw-r--r--pkg/scripts/preinstall.sh13+7 −6
-rw-r--r--pkg/scripts/preremove.sh6+3 −3
-rwxr-xr-xtests/smoke/run-local.sh24+12 −12
-rw-r--r--tests/smoke/src/main.rs36+18 −18
-rw-r--r--xtask/Cargo.toml16+16 −0
-rw-r--r--xtask/build.rs9+9 −0
-rw-r--r--xtask/src/lib.rs4+4 −0
-rw-r--r--xtask/src/licenses.rs614+614 −0
-rw-r--r--xtask/src/main.rs19+19 −0
57 files changed, 1080 insertions, 845 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..f6c2256 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -10,16 +10,8 @@ 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
jobs:
lint:
@@ -37,15 +29,9 @@ jobs:
run: mise bootstrap --locked --yes --update
- 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 }}
@@ -70,28 +56,29 @@ jobs:
run: mise bootstrap --locked --yes --update
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
- - run: cargo clippy --workspace --locked --all-targets --all-features --release -- -D warnings
+ - 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: Build packages
+ if: github.event_name == 'push'
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
-
+ base=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "recluse") | .version')
+ case "$GITHUB_REF" in
+ refs/tags/v*)
+ version=${GITHUB_REF#refs/tags/v}
+ [ "$version" = "$base" ]
+ ;;
+ *)
+ version="$base-nightly.${{ github.run_number }}.g$(git rev-parse --short=12 HEAD)"
+ ;;
+ esac
mise run package -- \
- --version "$VERSION" \
+ --version "$version" \
--arch "${{ matrix.arch }}" \
--output dist \
deb rpm
+ printf 'VERSION=%s\n' "$version" > dist/build.env
env:
GPG_KEY_ID: ${{ vars.GPG_KEY_ID }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
@@ -107,6 +94,7 @@ jobs:
path: coverage.lcov
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ if: github.event_name == 'push'
with:
name: packages-${{ matrix.arch }}
path: dist/*
@@ -114,12 +102,13 @@ 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.event_name == 'push'
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
@@ -138,17 +127,14 @@ 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
+ name: Publish packages
runs-on: ubuntu-latest
needs: [build, smoke]
- concurrency:
- group: package-publish-tesor-nightly
- cancel-in-progress: false
- if: github.ref == 'refs/heads/main'
+ if: github.event_name == 'push'
permissions:
contents: write
steps:
@@ -168,145 +154,25 @@ jobs:
path: dist
merge-multiple: true
- # Update Github Release
- - name: Update 'nightly' tag
+ - name: Publish
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
-
- git tag -f ${{ env.RELEASE_TAG }}
- git push origin --force tag ${{ env.RELEASE_TAG }}
- - name: Delete old release assets
- run: |
- for asset in $(gh release view ${{ env.RELEASE_TAG }} --json assets --jq '.assets[].name' 2>/dev/null || true); do
- gh release delete-asset ${{ env.RELEASE_TAG }} "$asset" --yes
- done
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - name: Create GitHub Release
- uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
- with:
- name: ${{ env.RELEASE_TAG }}
- tag_name: ${{ env.RELEASE_TAG }}
- files: dist/*
- body: |
- Last successful build from `main` branch.
-
- **Release checks**: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Publish package repositories
- run: |
+ . dist/build.env
mise run publish -- \
- --service tesor \
- --channel nightly \
+ --name recluse \
+ --repository "${{ github.server_url }}/${{ github.repository }}" \
+ --version "$VERSION" \
--input dist \
- deb rpm
+ --s3-bucket "${{ vars.S3_BUCKET }}" \
+ --s3-public-url "https://${{ vars.S3_BUCKET }}.${{ vars.S3_ENDPOINT }}" \
+ --s3-provider Other \
+ --s3-endpoint "${{ vars.S3_ENDPOINT }}" \
+ --github-release
env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GPG_KEY_ID: ${{ vars.GPG_KEY_ID }}
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
- 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 }}
- 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
- 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[@]}"
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,49 @@
+# SPDX-FileCopyrightText: 2026 Nikolay Govorov
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+all: legal fmt clippy test
+
+.PHONY: legal
+legal: signoff licenses
+
+.PHONY: signoff
+signoff:
+ mise run signoff
+
+.PHONY: setup
+setup:
+ mise bootstrap
+
+.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..41dbb9b 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,53 @@ 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://dimidiumlabs.fsn1.your-objectstorage.com/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/nightly/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/nightly/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://dimidiumlabs.fsn1.your-objectstorage.com/public.gpg
+sudo zypper addrepo https://dimidiumlabs.fsn1.your-objectstorage.com/rpm/nightly/ 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. Clone repo: `git clone https://github.com/dimidiumlabs/recluse.git && cd recluse`
1. Provision the toolchain and system build dependencies: `mise bootstrap`
1. Build from source: `mise exec -- 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 +95,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..5b14da9 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -10,13 +10,12 @@ path = [
"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 +36,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
index 0aded9e..025fcd6 100644
--- a/mise.lock
+++ b/mise.lock
@@ -14,26 +14,6 @@ checksum = "sha256:967b5cc996c29d8baa52bbb4595ef1f53af35255af8e2036ddbc6468d7b52
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"
@@ -57,17 +37,3 @@ 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..d1d8759 100644
--- a/mise.toml
+++ b/mise.toml
@@ -8,22 +8,21 @@ 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"
+rust = { version = "1.97.1", profile = "minimal", components = [
+ "clippy",
+ "llvm-tools-preview",
+ "rustfmt",
+] }
[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"
@@ -31,35 +30,9 @@ shellcheck = "0.11.0"
"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..0a9ab25 100644
--- a/nfpm.yaml
+++ b/nfpm.yaml
@@ -4,9 +4,11 @@
# 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}
+release: ${RELEASE}
+version_schema: none
license: AGPL-3.0-or-later
platform: linux
maintainer: Nikolay Govorov <me@govorov.online>
@@ -14,34 +16,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(&current) {
+ 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(),
+ ),
+ }
+}