aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat
-rw-r--r--.cargo/config.toml5+5 −0
-rw-r--r--.dockerignore2+1 −1
-rw-r--r--.github/workflows/build.yml35+11 −24
-rw-r--r--.gitignore2+1 −1
-rw-r--r--CLA.md4+2 −2
-rw-r--r--Cargo.lock219+174 −45
-rw-r--r--Cargo.toml4+2 −2
-rw-r--r--Dockerfile18+9 −9
-rw-r--r--README.md38+19 −19
-rw-r--r--REUSE.toml16+8 −8
-rw-r--r--charts/recluse/Chart.yaml (renamed from charts/tesor/Chart.yaml)4+2 −2
-rw-r--r--charts/recluse/templates/_helpers.tpl (renamed from charts/tesor/templates/_helpers.tpl)16+8 −8
-rw-r--r--charts/recluse/templates/configmap.yaml (renamed from charts/tesor/templates/configmap.yaml)8+4 −4
-rw-r--r--charts/recluse/templates/deployment.yaml (renamed from charts/tesor/templates/deployment.yaml)24+12 −12
-rw-r--r--charts/recluse/templates/httproute.yaml (renamed from charts/tesor/templates/httproute.yaml)6+3 −3
-rw-r--r--charts/recluse/templates/pvc.yaml (renamed from charts/tesor/templates/pvc.yaml)4+2 −2
-rw-r--r--charts/recluse/templates/service.yaml (renamed from charts/tesor/templates/service.yaml)6+3 −3
-rw-r--r--charts/recluse/values.yaml (renamed from charts/tesor/values.yaml)4+2 −2
-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.lock18+0 −18
-rw-r--r--mise.toml4+1 −3
-rw-r--r--nfpm.yaml24+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.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
55 files changed, 1043 insertions, 291 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,5 @@
+# SPDX-FileCopyrightText: 2026 Nikolay Govorov
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+[alias]
+xtask = "run --release --package xtask --"
diff --git a/.dockerignore b/.dockerignore
index 373529b..90a5968 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -3,7 +3,7 @@
.git
.github
-.tesor-state
+.recluse-state
dist
target
*.lcov
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 4078821..a8ba779 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
@@ -37,15 +30,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,13 +57,13 @@ 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
run: |
- PKG_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "tesor") | .version')
+ PKG_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "recluse") | .version')
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
VERSION="${GITHUB_REF_NAME#v}"
if [[ "$VERSION" != "$PKG_VERSION" ]]; then
@@ -114,7 +101,7 @@ 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
@@ -138,7 +125,7 @@ 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:
@@ -146,7 +133,7 @@ jobs:
runs-on: ubuntu-latest
needs: [build, smoke]
concurrency:
- group: package-publish-tesor-nightly
+ group: package-publish-recluse-nightly
cancel-in-progress: false
if: github.ref == 'refs/heads/main'
permissions:
@@ -199,7 +186,7 @@ jobs:
- name: Publish package repositories
run: |
mise run publish -- \
- --service tesor \
+ --service recluse \
--channel nightly \
--input dist \
deb rpm
@@ -275,7 +262,7 @@ jobs:
shell: bash
run: |
version="${{ steps.artifact.outputs.version }}"
- image="ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/tesor"
+ image="ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/recluse"
tags=(
--tag "$image:sha-$GITHUB_SHA"
--tag "$image:$version"
@@ -302,11 +289,11 @@ jobs:
--context . \
--file Dockerfile \
--platform linux/amd64,linux/arm64 \
- --cache-scope tesor \
+ --cache-scope recluse \
"${tags[@]}" "${labels[@]}" "${image_output[@]}"
mise run chart -- \
- --chart charts/tesor \
+ --chart charts/recluse \
--version "$version" \
--app-version "$version" \
"${chart_output[@]}"
diff --git a/.gitignore b/.gitignore
index 2c7fb07..ef36af8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,4 @@ mise.local.toml
mise.*.local.toml
/.container
-/.tesor-state
+/.recluse-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..2dda4d8 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"
@@ -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"
@@ -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"
@@ -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"
@@ -2691,51 +2853,6 @@ dependencies = [
]
[[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",
-]
-
-[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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
index d82aa9e..e4f4141 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -7,23 +7,23 @@ ARG ALPINE_DIGEST=sha256:fd791d74b68913cbb027c6546007b3f0d3bc45125f797758156952b
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
+RUN install -d -m 0750 /rootfs/var/lib/recluse
FROM gcr.io/distroless/cc-debian13:nonroot@${DISTROLESS_DIGEST}
ARG TARGETARCH
-LABEL org.opencontainers.image.source="https://git.dimidiumlabs.io/tesor" \
+LABEL org.opencontainers.image.source="https://github.com/dimidiumlabs/recluse" \
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/
+COPY --chown=root:root --chmod=0755 .container/binary-${TARGETARCH}/recluse /usr/local/bin/recluse
+COPY --from=rootfs --chown=10000:10000 /rootfs/var/lib/recluse /var/lib/recluse
+COPY --chown=root:root pkg/recluse.toml /etc/recluse.toml
+COPY --chown=root:root LICENSE README.md /usr/share/doc/recluse/
USER 10000:10000
EXPOSE 2000
-VOLUME ["/var/lib/tesor"]
+VOLUME ["/var/lib/recluse"]
-ENTRYPOINT ["/usr/local/bin/tesor"]
-CMD ["--config=/etc/tesor.toml"]
+ENTRYPOINT ["/usr/local/bin/recluse"]
+CMD ["--config=/etc/recluse.toml"]
diff --git a/README.md b/README.md
index 2c51ae4..fa4cfee 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.
@@ -14,52 +14,52 @@ is **not** intended for production use.
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
+echo "deb [signed-by=/usr/share/keyrings/dimidiumlabs.gpg] https://pkg.dimidiumlabs.io/recluse/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://pkg.dimidiumlabs.io/recluse/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://pkg.dimidiumlabs.io/recluse/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 zypper addrepo https://pkg.dimidiumlabs.io/recluse/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..717f144 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -10,13 +10,13 @@ path = [
"mise.lock",
"pkg/*.svg",
- "charts/tesor/templates/*",
+ "charts/recluse/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 +37,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/recluse/Chart.yaml
index 9a70371..ff40446 100644
--- a/charts/tesor/Chart.yaml
+++ b/charts/recluse/Chart.yaml
@@ -5,6 +5,6 @@ apiVersion: v2
type: application
version: 0.1.0
-name: tesor
-home: https://git.dimidiumlabs.io/tesor
+name: recluse
+home: https://github.com/dimidiumlabs/recluse
description: Tiny package caching proxy
diff --git a/charts/tesor/templates/_helpers.tpl b/charts/recluse/templates/_helpers.tpl
index 41b478a..4da058b 100644
--- a/charts/tesor/templates/_helpers.tpl
+++ b/charts/recluse/templates/_helpers.tpl
@@ -1,32 +1,32 @@
# SPDX-FileCopyrightText: 2026 Nikolay Govorov
# SPDX-License-Identifier: AGPL-3.0-or-later
-{{- define "tesor.name" -}}
+{{- define "recluse.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
-{{- define "tesor.fullname" -}}
+{{- define "recluse.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
-{{- printf "%s-%s" .Release.Name (include "tesor.name" .) | trunc 63 | trimSuffix "-" -}}
+{{- printf "%s-%s" .Release.Name (include "recluse.name" .) | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
-{{- define "tesor.labels" -}}
+{{- define "recluse.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
-app.kubernetes.io/name: {{ include "tesor.name" . }}
+app.kubernetes.io/name: {{ include "recluse.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" . }}
+{{- define "recluse.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "recluse.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
-{{- define "tesor.image" -}}
+{{- define "recluse.image" -}}
{{- if .Values.image.digest -}}
{{- printf "%s@%s" .Values.image.repository .Values.image.digest -}}
{{- else -}}
diff --git a/charts/tesor/templates/configmap.yaml b/charts/recluse/templates/configmap.yaml
index 5dd823c..d5dbf18 100644
--- a/charts/tesor/templates/configmap.yaml
+++ b/charts/recluse/templates/configmap.yaml
@@ -5,13 +5,13 @@
apiVersion: v1
kind: ConfigMap
metadata:
- name: {{ include "tesor.fullname" . }}
+ name: {{ include "recluse.fullname" . }}
labels:
- {{- include "tesor.labels" . | nindent 4 }}
+ {{- include "recluse.labels" . | nindent 4 }}
data:
- tesor.toml: |
+ recluse.toml: |
appname = {{ .Values.config.appname | quote }}
- dirname = "/var/lib/tesor"
+ dirname = "/var/lib/recluse"
[server]
max_body_size = {{ .Values.config.server.maxBodySize | quote }}
diff --git a/charts/tesor/templates/deployment.yaml b/charts/recluse/templates/deployment.yaml
index 952927e..f82648c 100644
--- a/charts/tesor/templates/deployment.yaml
+++ b/charts/recluse/templates/deployment.yaml
@@ -4,16 +4,16 @@
apiVersion: apps/v1
kind: Deployment
metadata:
- name: {{ include "tesor.fullname" . }}
+ name: {{ include "recluse.fullname" . }}
labels:
- {{- include "tesor.labels" . | nindent 4 }}
+ {{- include "recluse.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
strategy:
type: Recreate
selector:
matchLabels:
- {{- include "tesor.selectorLabels" . | nindent 6 }}
+ {{- include "recluse.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
@@ -24,7 +24,7 @@ spec:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
- {{- include "tesor.selectorLabels" . | nindent 8 }}
+ {{- include "recluse.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
@@ -38,12 +38,12 @@ spec:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- - name: tesor
- image: {{ include "tesor.image" . | quote }}
+ - name: recluse
+ image: {{ include "recluse.image" . | quote }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
- args: ["--config=/etc/tesor/tesor.toml"]
+ args: ["--config=/etc/recluse/recluse.toml"]
ports:
- name: http
containerPort: 2000
@@ -65,19 +65,19 @@ spec:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: config
- mountPath: /etc/tesor/tesor.toml
- subPath: tesor.toml
+ mountPath: /etc/recluse/recluse.toml
+ subPath: recluse.toml
readOnly: true
- name: state
- mountPath: /var/lib/tesor
+ mountPath: /var/lib/recluse
volumes:
- name: config
configMap:
- name: {{ default (include "tesor.fullname" .) .Values.config.existingConfigMap }}
+ name: {{ default (include "recluse.fullname" .) .Values.config.existingConfigMap }}
- name: state
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
- claimName: {{ default (include "tesor.fullname" .) .Values.persistence.existingClaim }}
+ claimName: {{ default (include "recluse.fullname" .) .Values.persistence.existingClaim }}
{{- else }}
emptyDir: {}
{{- end }}
diff --git a/charts/tesor/templates/httproute.yaml b/charts/recluse/templates/httproute.yaml
index 63e0159..5b7d341 100644
--- a/charts/tesor/templates/httproute.yaml
+++ b/charts/recluse/templates/httproute.yaml
@@ -5,9 +5,9 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
- name: {{ include "tesor.fullname" . }}
+ name: {{ include "recluse.fullname" . }}
labels:
- {{- include "tesor.labels" . | nindent 4 }}
+ {{- include "recluse.labels" . | nindent 4 }}
spec:
{{- with .Values.route.parentRefs }}
parentRefs:
@@ -19,7 +19,7 @@ spec:
{{- end }}
rules:
- backendRefs:
- - name: {{ include "tesor.fullname" . }}
+ - name: {{ include "recluse.fullname" . }}
port: {{ .Values.service.port }}
{{- end }}
diff --git a/charts/tesor/templates/pvc.yaml b/charts/recluse/templates/pvc.yaml
index 32a4d7d..2c563d1 100644
--- a/charts/tesor/templates/pvc.yaml
+++ b/charts/recluse/templates/pvc.yaml
@@ -5,9 +5,9 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
- name: {{ include "tesor.fullname" . }}
+ name: {{ include "recluse.fullname" . }}
labels:
- {{- include "tesor.labels" . | nindent 4 }}
+ {{- include "recluse.labels" . | nindent 4 }}
spec:
accessModes:
{{- toYaml .Values.persistence.accessModes | nindent 4 }}
diff --git a/charts/tesor/templates/service.yaml b/charts/recluse/templates/service.yaml
index 1c173a0..0367f03 100644
--- a/charts/tesor/templates/service.yaml
+++ b/charts/recluse/templates/service.yaml
@@ -4,12 +4,12 @@
apiVersion: v1
kind: Service
metadata:
- name: {{ include "tesor.fullname" . }}
+ name: {{ include "recluse.fullname" . }}
labels:
- {{- include "tesor.labels" . | nindent 4 }}
+ {{- include "recluse.labels" . | nindent 4 }}
spec:
selector:
- {{- include "tesor.selectorLabels" . | nindent 4 }}
+ {{- include "recluse.selectorLabels" . | nindent 4 }}
ports:
- name: http
port: {{ .Values.service.port }}
diff --git a/charts/tesor/values.yaml b/charts/recluse/values.yaml
index a88ae89..d6762d8 100644
--- a/charts/tesor/values.yaml
+++ b/charts/recluse/values.yaml
@@ -4,7 +4,7 @@
replicaCount: 1
image:
- repository: ghcr.io/dimidiumlabs/tesor
+ repository: ghcr.io/dimidiumlabs/recluse
tag: ""
digest: ""
pullPolicy: IfNotPresent
@@ -36,7 +36,7 @@ service:
config:
existingConfigMap: ""
- appname: tesor
+ appname: recluse
hostnames: []
server:
shutdownTimeout: 60
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..4a2058b 100644
--- a/mise.lock
+++ b/mise.lock
@@ -14,10 +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"
@@ -57,17 +53,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..f616adc 100644
--- a/mise.toml
+++ b/mise.toml
@@ -8,11 +8,9 @@ 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
@@ -36,7 +34,7 @@ shellcheck = "0.11.0"
[task_config]
dir = "{{cwd}}"
includes = [
- "git::https://git.dimidiumlabs.io/platform.git//tasks?ref=159ce265b95d655342c378d9aaa7dc14dbf1dfa4",
+ "git::https://github.com/dimidiumlabs/platform.git//tasks?ref=8bc35fe8be889c50db2d1fb4425cc1b4097dc6b8",
]
[tasks.fmt]
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(&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(),
+ ),
+ }
+}