You've already forked pages
forked from didirus/AstralRinth
Update Rust & Java dependencies (#4540)
* Update Java dependencies * Baselint lint fixes * Update Rust version * Update actix-files 0.6.6 -> 0.6.8 * Update actix-http 3.11.0 -> 3.11.2 * Update actix-rt 2.10.0 -> 2.11.0 * Update async_zip 0.0.17 -> 0.0.18 * Update async-compression 0.4.27 -> 0.4.32 * Update async-trait 0.1.88 -> 0.1.89 * Update async-tungstenite 0.30.0 -> 0.31.0 * Update const_format 0.2.34 -> 0.2.35 * Update bitflags 2.9.1 -> 2.9.4 * Update bytemuck 1.23.1 -> 1.24.0 * Update typed-path 0.11.0 -> 0.12.0 * Update chrono 0.4.41 -> 0.4.42 * Update cidre 0.11.2 -> 0.11.3 * Update clap 4.5.43 -> 4.5.48 * Update data-url 0.3.1 -> 0.3.2 * Update discord-rich-presence 0.2.5 -> 1.0.0 * Update enumset 1.1.7 -> 1.1.10 * Update flate2 1.1.2 -> 1.1.4 * Update hyper 1.6.0 -> 1.7.0 * Update hyper-util 0.1.16 -> 0.1.17 * Update iana-time-zone 0.1.63 -> 0.1.64 * Update image 0.25.6 -> 0.25.8 * Update indexmap 2.10.0 -> 2.11.4 * Update json-patch 4.0.0 -> 4.1.0 * Update meilisearch-sdk 0.29.1 -> 0.30.0 * Update clickhouse 0.13.3 -> 0.14.0 * Fix some prettier things * Update lettre 0.11.18 -> 0.11.19 * Update phf 0.12.1 -> 0.13.1 * Update png 0.17.16 -> 0.18.0 * Update quick-xml 0.38.1 -> 0.38.3 * Update redis 0.32.4 -> 0.32.7 * Update regex 1.11.1 -> 1.11.3 * Update reqwest 0.12.22 -> 0.12.23 * Update rust_decimal 1.37.2 -> 1.38.0 * Update rust-s3 0.35.1 -> 0.37.0 * Update serde 1.0.219 -> 1.0.228 * Update serde_bytes 0.11.17 -> 0.11.19 * Update serde_json 1.0.142 -> 1.0.145 * Update serde_with 3.14.0 -> 3.15.0 * Update sentry 0.42.0 -> 0.45.0 and sentry-actix 0.42.0 -> 0.45.0 * Update spdx 0.10.9 -> 0.12.0 * Update sysinfo 0.36.1 -> 0.37.2 * Update tauri 2.7.0 -> 2.8.5 * Update tauri-build 2.3.1 -> 2.4.1 * Update tauri-plugin-deep-link 2.4.1 -> 2.4.3 * Update tauri-plugin-dialog 2.3.2 -> 2.4.0 * Update tauri-plugin-http 2.5.1 -> 2.5.2 * Update tauri-plugin-opener 2.4.0 -> 2.5.0 * Update tauri-plugin-os 2.3.0 -> 2.3.1 * Update tauri-plugin-single-instance 2.3.2 -> 2.3.4 * Update tempfile 3.20.0 -> 3.23.0 * Update thiserror 2.0.12 -> 2.0.17 * Update tracing-subscriber 0.3.19 -> 0.3.20 * Update url 2.5.4 -> 2.5.7 * Update uuid 1.17.0 -> 1.18.1 * Update webp 0.3.0 -> 0.3.1 * Update whoami 1.6.0 -> 1.6.1 * Note that windows and windows-core can't be updated yet * Update zbus 5.9.0 -> 5.11.0 * Update zip 4.3.0 -> 6.0.0 * Fix build * Enforce rustls crypto provider * Refresh Cargo.lock * Update transitive dependencies * Bump Gradle usage to Java 17 * Use ubuntu-latest consistently across workflows * Fix lint * Fix lint in Rust * Update native-dialog 0.9.0 -> 0.9.2 * Update regex 1.11.3 -> 1.12.2 * Update reqwest 0.12.23 -> 0.12.24 * Update rust_decimal 1.38.0 -> 1.39.0 * Remaining lock-only updates * chore: move TLS impl of some other dependencies to aws-lc-rs The AWS bloatware "virus" expands by sheer force of widespread adoption by the ecosystem... 🫣 * chore(fmt): run Tombi --------- Co-authored-by: Alejandro González <me@alegon.dev>
This commit is contained in:
2
.github/workflows/i18n-pull.yml
vendored
2
.github/workflows/i18n-pull.yml
vendored
@@ -11,7 +11,7 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
pull_translations:
|
pull_translations:
|
||||||
name: 'Pull translations from Crowdin'
|
name: 'Pull translations from Crowdin'
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
concurrency:
|
concurrency:
|
||||||
group: i18n-pull:${{ github.ref }}
|
group: i18n-pull:${{ github.ref }}
|
||||||
|
|||||||
2
.github/workflows/i18n-push.yml
vendored
2
.github/workflows/i18n-push.yml
vendored
@@ -18,7 +18,7 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
push_translations:
|
push_translations:
|
||||||
name: Push sources to Crowdin
|
name: Push sources to Crowdin
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
concurrency:
|
concurrency:
|
||||||
group: i18n-push:${{ github.ref }}
|
group: i18n-push:${{ github.ref }}
|
||||||
|
|||||||
6
.github/workflows/theseus-build.yml
vendored
6
.github/workflows/theseus-build.yml
vendored
@@ -28,13 +28,13 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
platform: [macos-latest, windows-latest, ubuntu-22.04]
|
platform: [macos-latest, windows-latest, ubuntu-latest]
|
||||||
include:
|
include:
|
||||||
- platform: macos-latest
|
- platform: macos-latest
|
||||||
artifact-target-name: universal-apple-darwin
|
artifact-target-name: universal-apple-darwin
|
||||||
- platform: windows-latest
|
- platform: windows-latest
|
||||||
artifact-target-name: x86_64-pc-windows-msvc
|
artifact-target-name: x86_64-pc-windows-msvc
|
||||||
- platform: ubuntu-22.04
|
- platform: ubuntu-latest
|
||||||
artifact-target-name: x86_64-unknown-linux-gnu
|
artifact-target-name: x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
@@ -127,7 +127,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
[System.Convert]::FromBase64String("$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64") | Set-Content -Path signer-client-cert.p12 -AsByteStream
|
[System.Convert]::FromBase64String("$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64") | Set-Content -Path signer-client-cert.p12 -AsByteStream
|
||||||
$env:DIGICERT_ONE_SIGNER_CREDENTIALS = "$env:DIGICERT_ONE_SIGNER_API_KEY|$PWD\signer-client-cert.p12|$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD"
|
$env:DIGICERT_ONE_SIGNER_CREDENTIALS = "$env:DIGICERT_ONE_SIGNER_API_KEY|$PWD\signer-client-cert.p12|$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD"
|
||||||
$env:JAVA_HOME = "$env:JAVA_HOME_11_X64"
|
$env:JAVA_HOME = "$env:JAVA_HOME_17_X64"
|
||||||
pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis,updater'
|
pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis,updater'
|
||||||
Remove-Item -Path signer-client-cert.p12 -ErrorAction SilentlyContinue
|
Remove-Item -Path signer-client-cert.p12 -ErrorAction SilentlyContinue
|
||||||
if: startsWith(matrix.platform, 'windows')
|
if: startsWith(matrix.platform, 'windows')
|
||||||
|
|||||||
2
.github/workflows/turbo-ci.yml
vendored
2
.github/workflows/turbo-ci.yml
vendored
@@ -11,7 +11,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Lint and Test
|
name: Lint and Test
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Ensure pnpm output is colored in GitHub Actions logs
|
# Ensure pnpm output is colored in GitHub Actions logs
|
||||||
|
|||||||
691
Cargo.lock
generated
691
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
132
Cargo.toml
132
Cargo.toml
@@ -16,84 +16,84 @@ edition = "2024"
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
actix-cors = "0.7.1"
|
actix-cors = "0.7.1"
|
||||||
actix-files = "0.6.6"
|
actix-files = "0.6.8"
|
||||||
actix-http = "3.11.0"
|
actix-http = "3.11.2"
|
||||||
actix-multipart = "0.7.2"
|
actix-multipart = "0.7.2"
|
||||||
actix-rt = "2.10.0"
|
actix-rt = "2.11.0"
|
||||||
actix-web = "4.11.0"
|
actix-web = "4.11.0"
|
||||||
actix-web-prom = "0.10.0"
|
actix-web-prom = "0.10.0"
|
||||||
actix-ws = "0.3.0"
|
actix-ws = "0.3.0"
|
||||||
argon2 = { version = "0.5.3", features = ["std"] }
|
argon2 = { version = "0.5.3", features = ["std"] }
|
||||||
ariadne = { path = "packages/ariadne" }
|
ariadne = { path = "packages/ariadne" }
|
||||||
async-compression = { version = "0.4.27", default-features = false }
|
async-compression = { version = "0.4.32", default-features = false }
|
||||||
async-recursion = "1.1.1"
|
async-recursion = "1.1.1"
|
||||||
async-stripe = { version = "0.41.0", default-features = false, features = [
|
async-stripe = { version = "0.41.0", default-features = false, features = [
|
||||||
"runtime-tokio-hyper-rustls",
|
"runtime-tokio-hyper-rustls",
|
||||||
] }
|
] }
|
||||||
async-trait = "0.1.88"
|
async-trait = "0.1.89"
|
||||||
async-tungstenite = { version = "0.30.0", default-features = false, features = [
|
async-tungstenite = { version = "0.31.0", default-features = false, features = [
|
||||||
"futures-03-sink",
|
"futures-03-sink",
|
||||||
] }
|
] }
|
||||||
async-walkdir = "2.1.0"
|
async-walkdir = "2.1.0"
|
||||||
async_zip = "0.0.17"
|
async_zip = "0.0.18"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
bitflags = "2.9.1"
|
bitflags = "2.9.4"
|
||||||
bytemuck = "1.23.1"
|
bytemuck = "1.24.0"
|
||||||
bytes = "1.10.1"
|
bytes = "1.10.1"
|
||||||
censor = "0.3.0"
|
censor = "0.3.0"
|
||||||
chardetng = "0.1.17"
|
chardetng = "0.1.17"
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.42"
|
||||||
cidre = { version = "0.11.2", default-features = false, features = [
|
cidre = { version = "0.11.3", default-features = false, features = [
|
||||||
"macos_15_0",
|
"macos_15_0",
|
||||||
] }
|
] }
|
||||||
clap = "4.5.43"
|
clap = "4.5.48"
|
||||||
clickhouse = "0.13.3"
|
clickhouse = "0.14.0"
|
||||||
color-eyre = "0.6.5"
|
color-eyre = "0.6.5"
|
||||||
color-thief = "0.2.2"
|
color-thief = "0.2.2"
|
||||||
console-subscriber = "0.4.1"
|
console-subscriber = "0.4.1"
|
||||||
const_format = "0.2.34"
|
const_format = "0.2.34"
|
||||||
daedalus = { path = "packages/daedalus" }
|
daedalus = { path = "packages/daedalus" }
|
||||||
dashmap = "6.1.0"
|
dashmap = "6.1.0"
|
||||||
data-url = "0.3.1"
|
data-url = "0.3.2"
|
||||||
deadpool-redis = "0.22.0"
|
deadpool-redis = "0.22.0"
|
||||||
derive_more = "2.0.1"
|
derive_more = "2.0.1"
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
discord-rich-presence = "0.2.5"
|
discord-rich-presence = "1.0.0"
|
||||||
dotenv-build = "0.1.1"
|
dotenv-build = "0.1.1"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
dunce = "1.0.5"
|
dunce = "1.0.5"
|
||||||
either = "1.15.0"
|
either = "1.15.0"
|
||||||
encoding_rs = "0.8.35"
|
encoding_rs = "0.8.35"
|
||||||
enumset = "1.1.7"
|
enumset = "1.1.10"
|
||||||
eyre = "0.6.12"
|
eyre = "0.6.12"
|
||||||
flate2 = "1.1.2"
|
flate2 = "1.1.4"
|
||||||
fs4 = { version = "0.13.1", default-features = false }
|
fs4 = { version = "0.13.1", default-features = false }
|
||||||
futures = { version = "0.3.31", default-features = false }
|
futures = "0.3.31"
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
heck = "0.5.0"
|
heck = "0.5.0"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
hickory-resolver = "0.25.2"
|
hickory-resolver = "0.25.2"
|
||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
hyper = "1.6.0"
|
hyper = "1.7.0"
|
||||||
hyper-rustls = { version = "0.27.7", default-features = false, features = [
|
hyper-rustls = { version = "0.27.7", default-features = false, features = [
|
||||||
|
"aws-lc-rs",
|
||||||
"http1",
|
"http1",
|
||||||
"native-tokio",
|
"native-tokio",
|
||||||
"ring",
|
|
||||||
"tls12",
|
"tls12",
|
||||||
] }
|
] }
|
||||||
hyper-util = "0.1.16"
|
hyper-util = "0.1.17"
|
||||||
iana-time-zone = "0.1.63"
|
iana-time-zone = "0.1.64"
|
||||||
image = { version = "0.25.6", default-features = false, features = ["rayon"] }
|
image = { version = "0.25.8", default-features = false, features = ["rayon"] }
|
||||||
indexmap = "2.10.0"
|
indexmap = "2.11.4"
|
||||||
indicatif = "0.18.0"
|
indicatif = "0.18.0"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
jemalloc_pprof = "0.8.1"
|
jemalloc_pprof = "0.8.1"
|
||||||
json-patch = { version = "4.0.0", default-features = false }
|
json-patch = { version = "4.1.0", default-features = false }
|
||||||
lettre = { version = "0.11.18", default-features = false, features = [
|
lettre = { version = "0.11.19", default-features = false, features = [
|
||||||
|
"aws-lc-rs",
|
||||||
"builder",
|
"builder",
|
||||||
"hostname",
|
"hostname",
|
||||||
"pool",
|
"pool",
|
||||||
"ring",
|
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-native-certs",
|
"rustls-native-certs",
|
||||||
"smtp-transport",
|
"smtp-transport",
|
||||||
@@ -101,37 +101,38 @@ lettre = { version = "0.11.18", default-features = false, features = [
|
|||||||
"tokio1-rustls",
|
"tokio1-rustls",
|
||||||
] }
|
] }
|
||||||
maxminddb = "0.26.0"
|
maxminddb = "0.26.0"
|
||||||
meilisearch-sdk = { version = "0.29.1", default-features = false }
|
meilisearch-sdk = { version = "0.30.0", default-features = false }
|
||||||
murmur2 = "0.1.0"
|
murmur2 = "0.1.0"
|
||||||
native-dialog = "0.9.0"
|
native-dialog = "0.9.2"
|
||||||
notify = { version = "8.2.0", default-features = false }
|
notify = { version = "8.2.0", default-features = false }
|
||||||
notify-debouncer-mini = { version = "0.7.0", default-features = false }
|
notify-debouncer-mini = { version = "0.7.0", default-features = false }
|
||||||
p256 = "0.13.2"
|
p256 = "0.13.2"
|
||||||
paste = "1.0.15"
|
paste = "1.0.15"
|
||||||
path-util = { path = "packages/path-util" }
|
path-util = { path = "packages/path-util" }
|
||||||
phf = { version = "0.12.1", features = ["macros"] }
|
phf = { version = "0.13.1", features = ["macros"] }
|
||||||
png = "0.17.16"
|
png = "0.18.0"
|
||||||
prometheus = "0.14.0"
|
prometheus = "0.14.0"
|
||||||
quartz_nbt = "0.2.9"
|
quartz_nbt = "0.2.9"
|
||||||
quick-xml = "0.38.1"
|
quick-xml = "0.38.3"
|
||||||
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
||||||
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
||||||
redis = "0.32.4"
|
redis = "0.32.7"
|
||||||
regex = "1.11.1"
|
regex = "1.12.2"
|
||||||
reqwest = { version = "0.12.22", default-features = false }
|
reqwest = { version = "0.12.24", default-features = false }
|
||||||
rgb = "0.8.52"
|
rgb = "0.8.52"
|
||||||
rust_decimal = { version = "1.37.2", features = [
|
rust_decimal = { version = "1.39.0", features = [
|
||||||
"serde-with-float",
|
"serde-with-float",
|
||||||
"serde-with-str",
|
"serde-with-str",
|
||||||
] }
|
] }
|
||||||
rust_iso3166 = "0.1.14"
|
rust_iso3166 = "0.1.14"
|
||||||
rust-s3 = { version = "0.35.1", default-features = false, features = [
|
rust-s3 = { version = "0.37.0", default-features = false, features = [
|
||||||
"fail-on-err",
|
"fail-on-err",
|
||||||
"tags",
|
"tags",
|
||||||
"tokio-rustls-tls",
|
"tokio-rustls-tls",
|
||||||
] }
|
] }
|
||||||
|
rustls = "0.23.32"
|
||||||
rusty-money = "0.4.1"
|
rusty-money = "0.4.1"
|
||||||
sentry = { version = "0.42.0", default-features = false, features = [
|
sentry = { version = "0.45.0", default-features = false, features = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"contexts",
|
"contexts",
|
||||||
"debug-images",
|
"debug-images",
|
||||||
@@ -139,37 +140,37 @@ sentry = { version = "0.42.0", default-features = false, features = [
|
|||||||
"reqwest",
|
"reqwest",
|
||||||
"rustls",
|
"rustls",
|
||||||
] }
|
] }
|
||||||
sentry-actix = "0.42.0"
|
sentry-actix = "0.45.0"
|
||||||
serde = "1.0.219"
|
serde = "1.0.228"
|
||||||
serde_bytes = "0.11.17"
|
serde_bytes = "0.11.19"
|
||||||
serde_cbor = "0.11.2"
|
serde_cbor = "0.11.2"
|
||||||
serde_ini = "0.2.0"
|
serde_ini = "0.2.0"
|
||||||
serde_json = "1.0.142"
|
serde_json = "1.0.145"
|
||||||
serde_with = "3.14.0"
|
serde_with = "3.15.0"
|
||||||
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
||||||
sha1 = "0.10.6"
|
sha1 = "0.10.6"
|
||||||
sha1_smol = { version = "1.0.1", features = ["std"] }
|
sha1_smol = { version = "1.0.1", features = ["std"] }
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
spdx = "0.10.9"
|
spdx = "0.12.0"
|
||||||
sqlx = { version = "0.8.6", default-features = false }
|
sqlx = { version = "0.8.6", default-features = false }
|
||||||
sysinfo = { version = "0.36.1", default-features = false }
|
sysinfo = { version = "0.37.2", default-features = false }
|
||||||
tar = "0.4.44"
|
tar = "0.4.44"
|
||||||
tauri = "2.7.0"
|
tauri = "2.8.5"
|
||||||
tauri-build = "2.3.1"
|
tauri-build = "2.4.1"
|
||||||
tauri-plugin-deep-link = "2.4.1"
|
tauri-plugin-deep-link = "2.4.3"
|
||||||
tauri-plugin-dialog = "2.3.2"
|
tauri-plugin-dialog = "2.4.0"
|
||||||
tauri-plugin-http = "2.5.1"
|
tauri-plugin-http = "2.5.2"
|
||||||
tauri-plugin-opener = "2.4.0"
|
tauri-plugin-opener = "2.5.0"
|
||||||
tauri-plugin-os = "2.3.0"
|
tauri-plugin-os = "2.3.1"
|
||||||
tauri-plugin-single-instance = "2.3.2"
|
tauri-plugin-single-instance = "2.3.4"
|
||||||
tauri-plugin-updater = { version = "2.9.0", default-features = false, features = [
|
tauri-plugin-updater = { version = "2.9.0", default-features = false, features = [
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
"zip",
|
"zip",
|
||||||
] }
|
] }
|
||||||
tauri-plugin-window-state = "2.4.0"
|
tauri-plugin-window-state = "2.4.0"
|
||||||
tempfile = "3.20.0"
|
tempfile = "3.23.0"
|
||||||
theseus = { path = "packages/app-lib" }
|
theseus = { path = "packages/app-lib" }
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.17"
|
||||||
tikv-jemalloc-ctl = "0.6.0"
|
tikv-jemalloc-ctl = "0.6.0"
|
||||||
tikv-jemallocator = "0.6.0"
|
tikv-jemallocator = "0.6.0"
|
||||||
tokio = "1.47.1"
|
tokio = "1.47.1"
|
||||||
@@ -180,22 +181,22 @@ tracing = "0.1.41"
|
|||||||
tracing-actix-web = { version = "0.7.19", default-features = false }
|
tracing-actix-web = { version = "0.7.19", default-features = false }
|
||||||
tracing-ecs = "0.5.0"
|
tracing-ecs = "0.5.0"
|
||||||
tracing-error = "0.2.1"
|
tracing-error = "0.2.1"
|
||||||
tracing-subscriber = "0.3.19"
|
tracing-subscriber = "0.3.20"
|
||||||
typed-path = "0.11.0"
|
typed-path = "0.12.0"
|
||||||
url = "2.5.4"
|
url = "2.5.7"
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
uuid = "1.17.0"
|
uuid = "1.18.1"
|
||||||
validator = "0.20.0"
|
validator = "0.20.0"
|
||||||
webp = { version = "0.3.0", default-features = false }
|
webp = { version = "0.3.1", default-features = false }
|
||||||
webview2-com = "0.38.0" # Should be updated in lockstep with wry
|
webview2-com = "0.38.0" # Should be updated in lockstep with wry
|
||||||
whoami = "1.6.0"
|
whoami = "1.6.1"
|
||||||
windows = "0.61.3"
|
windows = "=0.61.3" # Locked on 0.61 until we can update windows-core to 0.62
|
||||||
windows-core = "0.61.2"
|
windows-core = "=0.61.2" # Locked on 0.61 until webview2-com updates to 0.62
|
||||||
winreg = "0.55.0"
|
winreg = "0.55.0"
|
||||||
woothee = "0.13.0"
|
woothee = "0.13.0"
|
||||||
yaserde = "0.12.0"
|
yaserde = "0.12.0"
|
||||||
zbus = "5.9.0"
|
zbus = "5.11.0"
|
||||||
zip = { version = "4.3.0", default-features = false, features = [
|
zip = { version = "6.0.0", default-features = false, features = [
|
||||||
"bzip2",
|
"bzip2",
|
||||||
"deflate",
|
"deflate",
|
||||||
"deflate64",
|
"deflate64",
|
||||||
@@ -234,6 +235,7 @@ redundant_clone = "warn"
|
|||||||
redundant_feature_names = "warn"
|
redundant_feature_names = "warn"
|
||||||
redundant_type_annotations = "warn"
|
redundant_type_annotations = "warn"
|
||||||
todo = "warn"
|
todo = "warn"
|
||||||
|
uninlined_format_args = "warn"
|
||||||
unnested_or_patterns = "warn"
|
unnested_or_patterns = "warn"
|
||||||
wildcard_dependencies = "warn"
|
wildcard_dependencies = "warn"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
FROM rust:1.89.0 AS build
|
FROM rust:1.90.0 AS build
|
||||||
|
|
||||||
WORKDIR /usr/src/daedalus
|
WORKDIR /usr/src/daedalus
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ description: Guide for contributing to Modrinth's backend
|
|||||||
|
|
||||||
This project is part of our [monorepo](https://github.com/modrinth/code). You can find it in the `apps/labrinth` directory. The instructions below assume that you have switched your working directory to the `apps/labrinth` subdirectory.
|
This project is part of our [monorepo](https://github.com/modrinth/code). You can find it in the `apps/labrinth` directory. The instructions below assume that you have switched your working directory to the `apps/labrinth` subdirectory.
|
||||||
|
|
||||||
[labrinth] is the Rust-based backend serving Modrinth's API with the help of the [Actix](https://actix.rs) framework. To get started with a labrinth instance, install docker, docker-compose (which comes with Docker), and [Rust]. The initial startup can be done simply with the command `docker-compose up`, or with `docker compose up` (Compose V2 and later). That will deploy a PostgreSQL database on port 5432, a MeiliSearch instance on port 7700, and a [Mailpit](https://mailpit.axllent.org/) SMTP server on port 1025, with a web UI to inspect sent emails on port 8025. To run the API itself, you'll need to use the `cargo run` command, this will deploy the API on port 8000.
|
[labrinth] is the Rust-based backend serving Modrinth's API with the help of the [Actix](https://actix.rs) framework. To get started with a labrinth instance, install docker, docker-compose (which comes with Docker), cmake, and [Rust]. The initial startup can be done simply with the command `docker-compose up`, or with `docker compose up` (Compose V2 and later). That will deploy a PostgreSQL database on port 5432, a MeiliSearch instance on port 7700, and a [Mailpit](https://mailpit.axllent.org/) SMTP server on port 1025, with a web UI to inspect sent emails on port 8025. To run the API itself, you'll need to use the `cargo run` command, this will deploy the API on port 8000.
|
||||||
|
|
||||||
To get a basic configuration, copy the `.env.local` file to `.env`. Now, you'll have to install the sqlx CLI, which can be done with cargo:
|
To get a basic configuration, copy the `.env.local` file to `.env`. Now, you'll have to install the sqlx CLI, which can be done with cargo:
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,8 @@
|
|||||||
::backdrop,
|
::backdrop,
|
||||||
:root[data-theme='light'],
|
:root[data-theme='light'],
|
||||||
[data-theme='light'] ::backdrop {
|
[data-theme='light'] ::backdrop {
|
||||||
--sl-font-system:
|
--sl-font-system: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto,
|
||||||
Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell,
|
Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||||
Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
|
||||||
|
|
||||||
--sl-color-white: var(--color-contrast); /* “white” */
|
--sl-color-white: var(--color-contrast); /* “white” */
|
||||||
--sl-color-gray-1: var(--color-base);
|
--sl-color-gray-1: var(--color-base);
|
||||||
|
|||||||
@@ -113,6 +113,10 @@ migrations/20250725230041_reports-closed-status-index.sql
|
|||||||
migrations/20250727184120_user-newsletter-subscription-column.sql
|
migrations/20250727184120_user-newsletter-subscription-column.sql
|
||||||
migrations/20250804221014_users-redeemals.sql
|
migrations/20250804221014_users-redeemals.sql
|
||||||
migrations/20250805001654_product-prices-public.sql
|
migrations/20250805001654_product-prices-public.sql
|
||||||
|
migrations/20250823233518_user-compliance.sql
|
||||||
|
migrations/20250902133943_notification-extension.sql
|
||||||
|
migrations/20250914190749_affiliate_codes.sql
|
||||||
|
migrations/20250927120742_user_limits.sql
|
||||||
|
|
||||||
# Prettier reformats some of the PostgreSQL-specific COPY syntax here,
|
# Prettier reformats some of the PostgreSQL-specific COPY syntax here,
|
||||||
# which is very likely to break things
|
# which is very likely to break things
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ rust_decimal = { workspace = true, features = [
|
|||||||
] }
|
] }
|
||||||
rust_iso3166 = { workspace = true }
|
rust_iso3166 = { workspace = true }
|
||||||
rust-s3 = { workspace = true }
|
rust-s3 = { workspace = true }
|
||||||
|
rustls.workspace = true
|
||||||
rusty-money = { workspace = true }
|
rusty-money = { workspace = true }
|
||||||
sentry = { workspace = true }
|
sentry = { workspace = true }
|
||||||
sentry-actix = { workspace = true }
|
sentry-actix = { workspace = true }
|
||||||
@@ -108,7 +109,7 @@ sqlx = { workspace = true, features = [
|
|||||||
"postgres",
|
"postgres",
|
||||||
"runtime-tokio",
|
"runtime-tokio",
|
||||||
"rust_decimal",
|
"rust_decimal",
|
||||||
"tls-rustls-ring",
|
"tls-rustls-aws-lc-rs",
|
||||||
] }
|
] }
|
||||||
tar = { workspace = true }
|
tar = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
FROM rust:1.89.0 AS build
|
FROM rust:1.90.0 AS build
|
||||||
|
|
||||||
WORKDIR /usr/src/labrinth
|
WORKDIR /usr/src/labrinth
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -113,6 +113,10 @@ async fn main() -> std::io::Result<()> {
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rustls::crypto::aws_lc_rs::default_provider()
|
||||||
|
.install_default()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// DSN is from SENTRY_DSN env variable.
|
// DSN is from SENTRY_DSN env variable.
|
||||||
// Has no effect if not set.
|
// Has no effect if not set.
|
||||||
let sentry = sentry::init(sentry::ClientOptions {
|
let sentry = sentry::init(sentry::ClientOptions {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ impl AnalyticsQueue {
|
|||||||
self.playtime_queue.clear();
|
self.playtime_queue.clear();
|
||||||
|
|
||||||
if !playtime_queue.is_empty() {
|
if !playtime_queue.is_empty() {
|
||||||
let mut playtimes = client.insert("playtime")?;
|
let mut playtimes = client.insert::<Playtime>("playtime").await?;
|
||||||
|
|
||||||
for playtime in playtime_queue {
|
for playtime in playtime_queue {
|
||||||
playtimes.write(&playtime).await?;
|
playtimes.write(&playtime).await?;
|
||||||
@@ -132,7 +132,7 @@ impl AnalyticsQueue {
|
|||||||
.await
|
.await
|
||||||
.map_err(DatabaseError::CacheError)?;
|
.map_err(DatabaseError::CacheError)?;
|
||||||
|
|
||||||
let mut views = client.insert("views")?;
|
let mut views = client.insert::<PageView>("views").await?;
|
||||||
|
|
||||||
for (all_views, monetized) in raw_views {
|
for (all_views, monetized) in raw_views {
|
||||||
for (idx, mut view) in all_views.into_iter().enumerate() {
|
for (idx, mut view) in all_views.into_iter().enumerate() {
|
||||||
@@ -200,7 +200,7 @@ impl AnalyticsQueue {
|
|||||||
.map_err(DatabaseError::CacheError)?;
|
.map_err(DatabaseError::CacheError)?;
|
||||||
|
|
||||||
let mut transaction = pool.begin().await?;
|
let mut transaction = pool.begin().await?;
|
||||||
let mut downloads = client.insert("downloads")?;
|
let mut downloads = client.insert::<Download>("downloads").await?;
|
||||||
|
|
||||||
let mut version_downloads: HashMap<i64, i32> = HashMap::new();
|
let mut version_downloads: HashMap<i64, i32> = HashMap::new();
|
||||||
let mut project_downloads: HashMap<i64, i32> = HashMap::new();
|
let mut project_downloads: HashMap<i64, i32> = HashMap::new();
|
||||||
|
|||||||
@@ -704,7 +704,7 @@ async fn dynamic_email_body(
|
|||||||
.wrap_internal_err("SITE_URL is not set")?;
|
.wrap_internal_err("SITE_URL is not set")?;
|
||||||
let site_url = site_url.trim_end_matches('/');
|
let site_url = site_url.trim_end_matches('/');
|
||||||
|
|
||||||
let url = format!("{}/_internal/templates/email/dynamic", site_url);
|
let url = format!("{site_url}/_internal/templates/email/dynamic");
|
||||||
|
|
||||||
std::str::from_utf8(
|
std::str::from_utf8(
|
||||||
reqwest::Client::new()
|
reqwest::Client::new()
|
||||||
|
|||||||
@@ -686,11 +686,12 @@ async fn query_clickhouse<Row>(
|
|||||||
cx: &mut QueryClickhouseContext<'_>,
|
cx: &mut QueryClickhouseContext<'_>,
|
||||||
query: &str,
|
query: &str,
|
||||||
use_columns: &[(&str, bool)],
|
use_columns: &[(&str, bool)],
|
||||||
row_get_bucket: impl Fn(&Row) -> u64,
|
// I hate using the hidden type Row::Value here, but it's what next() returns, so I see no other option
|
||||||
row_to_analytics: impl Fn(Row) -> AnalyticsData,
|
row_get_bucket: impl Fn(&Row::Value<'_>) -> u64,
|
||||||
|
row_to_analytics: impl Fn(Row::Value<'_>) -> AnalyticsData,
|
||||||
) -> Result<(), ApiError>
|
) -> Result<(), ApiError>
|
||||||
where
|
where
|
||||||
Row: clickhouse::Row + serde::de::DeserializeOwned + std::fmt::Debug,
|
Row: clickhouse::RowRead + serde::de::DeserializeOwned + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let mut query = cx
|
let mut query = cx
|
||||||
.clickhouse
|
.clickhouse
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ pub async fn swap_index(
|
|||||||
let index_name = config.get_index_name(index_name, false);
|
let index_name = config.get_index_name(index_name, false);
|
||||||
let swap_indices = SwapIndexes {
|
let swap_indices = SwapIndexes {
|
||||||
indexes: (index_name_next, index_name),
|
indexes: (index_name_next, index_name),
|
||||||
|
rename: None,
|
||||||
};
|
};
|
||||||
client
|
client
|
||||||
.swap_indexes([&swap_indices])
|
.swap_indexes([&swap_indices])
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ impl super::Validator for DataPackValidator {
|
|||||||
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
||||||
// Time since release of 17w43a, 2017-10-25, which introduced datapacks
|
// Time since release of 17w43a, 2017-10-25, which introduced datapacks
|
||||||
SupportedGameVersions::PastDate(
|
SupportedGameVersions::PastDate(
|
||||||
DateTime::from_timestamp(1508889600, 0).unwrap(),
|
DateTime::from_timestamp_secs(1508889600).unwrap(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ impl super::Validator for ForgeValidator {
|
|||||||
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
||||||
// Time since release of 1.13, the first forge version which uses the new TOML system
|
// Time since release of 1.13, the first forge version which uses the new TOML system
|
||||||
SupportedGameVersions::PastDate(
|
SupportedGameVersions::PastDate(
|
||||||
DateTime::from_timestamp(1540122067, 0).unwrap(),
|
DateTime::from_timestamp_secs(1540122067).unwrap(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,8 +56,8 @@ impl super::Validator for LegacyForgeValidator {
|
|||||||
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
||||||
// Times between versions 1.5.2 to 1.12.2, which all use the legacy way of defining mods
|
// Times between versions 1.5.2 to 1.12.2, which all use the legacy way of defining mods
|
||||||
SupportedGameVersions::Range(
|
SupportedGameVersions::Range(
|
||||||
DateTime::from_timestamp(0, 0).unwrap(),
|
DateTime::from_timestamp_secs(0).unwrap(),
|
||||||
DateTime::from_timestamp(1540122066, 0).unwrap(),
|
DateTime::from_timestamp_secs(1540122066).unwrap(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ impl super::Validator for QuiltValidator {
|
|||||||
|
|
||||||
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
||||||
SupportedGameVersions::PastDate(
|
SupportedGameVersions::PastDate(
|
||||||
DateTime::from_timestamp(1646070100, 0).unwrap(),
|
DateTime::from_timestamp_secs(1646070100).unwrap(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ impl super::Validator for PackValidator {
|
|||||||
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
||||||
// Time since release of 13w24a which replaced texture packs with resource packs
|
// Time since release of 13w24a which replaced texture packs with resource packs
|
||||||
SupportedGameVersions::PastDate(
|
SupportedGameVersions::PastDate(
|
||||||
DateTime::from_timestamp(1371137542, 0).unwrap(),
|
DateTime::from_timestamp_secs(1371137542).unwrap(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,8 +59,8 @@ impl super::Validator for TexturePackValidator {
|
|||||||
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
||||||
// a1.2.2a to 13w23b
|
// a1.2.2a to 13w23b
|
||||||
SupportedGameVersions::Range(
|
SupportedGameVersions::Range(
|
||||||
DateTime::from_timestamp(1289339999, 0).unwrap(),
|
DateTime::from_timestamp_secs(1289339999).unwrap(),
|
||||||
DateTime::from_timestamp(1370651522, 0).unwrap(),
|
DateTime::from_timestamp_secs(1370651522).unwrap(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ pub async fn setup(db: &database::TemporaryDatabase) -> LabrinthConfig {
|
|||||||
println!("Some environment variables are missing!");
|
println!("Some environment variables are missing!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||||
|
|
||||||
let pool = db.pool.clone();
|
let pool = db.pool.clone();
|
||||||
let ro_pool = db.ro_pool.clone();
|
let ro_pool = db.ro_pool.clone();
|
||||||
let redis_pool = db.redis_pool.clone();
|
let redis_pool = db.redis_pool.clone();
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
allow-dbg-in-tests = true
|
allow-dbg-in-tests = true
|
||||||
msrv = "1.89.0"
|
msrv = "1.90.0"
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
**/*.rs
|
**/*.rs
|
||||||
.sqlx
|
.sqlx
|
||||||
|
|
||||||
|
java/build
|
||||||
|
|
||||||
# Migrations existing before Prettier formatted them shall always be ignored,
|
# Migrations existing before Prettier formatted them shall always be ignored,
|
||||||
# as any changes to them will break existing deployments
|
# as any changes to them will break existing deployments
|
||||||
migrations/20240711194701_init.sql
|
migrations/20240711194701_init.sql
|
||||||
|
|||||||
@@ -1,18 +1,7 @@
|
|||||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowCopyAction
|
|
||||||
import com.github.jengelman.gradle.plugins.shadow.transformers.CacheableTransformer
|
|
||||||
import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransformer
|
|
||||||
import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext
|
|
||||||
import org.apache.tools.zip.ZipEntry
|
|
||||||
import org.apache.tools.zip.ZipOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.jar.JarFile
|
|
||||||
import java.util.jar.Attributes as JarAttributes
|
|
||||||
import java.util.jar.Manifest as JarManifest
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
id("com.diffplug.spotless") version "7.0.4"
|
id("com.diffplug.spotless") version "8.0.0"
|
||||||
id("com.gradleup.shadow") version "9.0.0-rc2"
|
id("com.gradleup.shadow") version "9.2.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
@@ -20,9 +9,9 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.ow2.asm:asm:9.8")
|
implementation("org.ow2.asm:asm:9.9")
|
||||||
implementation("org.ow2.asm:asm-tree:9.8")
|
implementation("org.ow2.asm:asm-tree:9.9")
|
||||||
implementation("com.google.code.gson:gson:2.13.1")
|
implementation("com.google.code.gson:gson:2.13.2")
|
||||||
|
|
||||||
testImplementation(libs.junit.jupiter)
|
testImplementation(libs.junit.jupiter)
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
@@ -30,7 +19,7 @@ dependencies {
|
|||||||
|
|
||||||
java {
|
java {
|
||||||
toolchain {
|
toolchain {
|
||||||
languageVersion = JavaLanguageVersion.of(11)
|
languageVersion = JavaLanguageVersion.of(17)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,52 +45,9 @@ tasks.shadowJar {
|
|||||||
attributes["Premain-Class"] = "com.modrinth.theseus.agent.TheseusAgent"
|
attributes["Premain-Class"] = "com.modrinth.theseus.agent.TheseusAgent"
|
||||||
}
|
}
|
||||||
|
|
||||||
enableRelocation = true
|
addMultiReleaseAttribute = false
|
||||||
|
enableAutoRelocation = true
|
||||||
relocationPrefix = "com.modrinth.theseus.shadow"
|
relocationPrefix = "com.modrinth.theseus.shadow"
|
||||||
|
|
||||||
// Adapted from ManifestResourceTransformer to do one thing: remove Multi-Release.
|
|
||||||
// Multi-Release gets added by shadow because gson has Multi-Release set to true, however
|
|
||||||
// shadow strips the actual versions directory, as gson only has a module-info.class in there.
|
|
||||||
// However, older versions of SecureJarHandler crash if Multi-Release is set to true but the
|
|
||||||
// versions directory is missing.
|
|
||||||
transform(@CacheableTransformer object : ResourceTransformer {
|
|
||||||
private var manifestDiscovered = false
|
|
||||||
private var manifest: JarManifest? = null
|
|
||||||
|
|
||||||
override fun canTransformResource(element: FileTreeElement): Boolean {
|
|
||||||
return JarFile.MANIFEST_NAME.equals(element.path, ignoreCase = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun transform(context: TransformerContext) {
|
|
||||||
if (!manifestDiscovered) {
|
|
||||||
try {
|
|
||||||
manifest = JarManifest(context.inputStream)
|
|
||||||
manifestDiscovered = true
|
|
||||||
} catch (e: IOException) {
|
|
||||||
logger.warn("Failed to read MANIFEST.MF", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hasTransformedResource(): Boolean = true
|
|
||||||
|
|
||||||
override fun modifyOutputStream(
|
|
||||||
os: ZipOutputStream,
|
|
||||||
preserveFileTimestamps: Boolean
|
|
||||||
) {
|
|
||||||
// If we didn't find a manifest, then let's create one.
|
|
||||||
if (manifest == null) {
|
|
||||||
manifest = JarManifest()
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest!!.mainAttributes.remove(JarAttributes.Name.MULTI_RELEASE)
|
|
||||||
|
|
||||||
os.putNextEntry(ZipEntry(JarFile.MANIFEST_NAME).apply {
|
|
||||||
time = ShadowCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES
|
|
||||||
})
|
|
||||||
manifest!!.write(os)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named<Test>("test") {
|
tasks.named<Test>("test") {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[versions]
|
[versions]
|
||||||
junit-jupiter = "5.12.1"
|
junit-jupiter = "5.14.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }
|
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
// Apply the foojay-resolver plugin to allow automatic download of JDKs
|
// Apply the foojay-resolver plugin to allow automatic download of JDKs
|
||||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0"
|
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "theseus"
|
rootProject.name = "theseus"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! Miscellaneous PNG utilities for Minecraft skins.
|
//! Miscellaneous PNG utilities for Minecraft skins.
|
||||||
|
|
||||||
use std::io::Read;
|
use std::io::{BufRead, Cursor, Seek};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
@@ -9,7 +9,8 @@ use data_url::DataUrl;
|
|||||||
use futures::{Stream, TryStreamExt, future::Either, stream};
|
use futures::{Stream, TryStreamExt, future::Either, stream};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use rgb::Rgba;
|
use rgb::Rgba;
|
||||||
use tokio_util::{compat::FuturesAsyncReadCompatExt, io::SyncIoBridge};
|
use tokio::io::AsyncReadExt;
|
||||||
|
use tokio_util::compat::FuturesAsyncReadCompatExt;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -95,7 +96,8 @@ pub fn dimensions(png_data: &[u8]) -> crate::Result<(u32, u32)> {
|
|||||||
pub async fn normalize_skin_texture(
|
pub async fn normalize_skin_texture(
|
||||||
texture: &UrlOrBlob,
|
texture: &UrlOrBlob,
|
||||||
) -> crate::Result<Bytes> {
|
) -> crate::Result<Bytes> {
|
||||||
let texture_stream = SyncIoBridge::new(Box::pin(
|
let mut texture_data = Vec::with_capacity(8192);
|
||||||
|
Box::pin(
|
||||||
match texture {
|
match texture {
|
||||||
UrlOrBlob::Url(url) => Either::Left(
|
UrlOrBlob::Url(url) => Either::Left(
|
||||||
url_to_data_stream(url)
|
url_to_data_stream(url)
|
||||||
@@ -112,84 +114,84 @@ pub async fn normalize_skin_texture(
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
.compat(),
|
.compat(),
|
||||||
));
|
)
|
||||||
|
.read_to_end(&mut texture_data)
|
||||||
|
.await?;
|
||||||
|
|
||||||
tokio::task::spawn_blocking(|| {
|
let mut png_reader = {
|
||||||
let mut png_reader = {
|
let mut decoder = png::Decoder::new(Cursor::new(texture_data));
|
||||||
let mut decoder = png::Decoder::new(texture_stream);
|
decoder
|
||||||
decoder.set_transformations(
|
.set_transformations(png::Transformations::normalize_to_color8());
|
||||||
png::Transformations::normalize_to_color8(),
|
decoder.read_info()
|
||||||
);
|
}?;
|
||||||
decoder.read_info()
|
|
||||||
}?;
|
|
||||||
|
|
||||||
// The code below assumes that the skin texture has valid dimensions.
|
// The code below assumes that the skin texture has valid dimensions.
|
||||||
// This also serves as a way to bail out early for obviously invalid or
|
// This also serves as a way to bail out early for obviously invalid or
|
||||||
// adversarial textures
|
// adversarial textures
|
||||||
if png_reader.info().width != 64
|
if png_reader.info().width != 64
|
||||||
|| ![64, 32].contains(&png_reader.info().height)
|
|| ![64, 32].contains(&png_reader.info().height)
|
||||||
{
|
{
|
||||||
Err(ErrorKind::InvalidSkinTexture)?;
|
Err(ErrorKind::InvalidSkinTexture)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_legacy_skin = png_reader.info().height == 32;
|
let is_legacy_skin = png_reader.info().height == 32;
|
||||||
let mut texture_buf =
|
let mut texture_buf =
|
||||||
get_skin_texture_buffer(&mut png_reader, is_legacy_skin)?;
|
get_skin_texture_buffer(&mut png_reader, is_legacy_skin)?;
|
||||||
if is_legacy_skin {
|
if is_legacy_skin {
|
||||||
convert_legacy_skin_texture(&mut texture_buf, png_reader.info());
|
convert_legacy_skin_texture(&mut texture_buf, png_reader.info());
|
||||||
do_notch_transparency_hack(&mut texture_buf, png_reader.info());
|
do_notch_transparency_hack(&mut texture_buf, png_reader.info());
|
||||||
}
|
}
|
||||||
make_inner_parts_opaque(&mut texture_buf, png_reader.info());
|
make_inner_parts_opaque(&mut texture_buf, png_reader.info());
|
||||||
|
|
||||||
let mut encoded_png = vec![];
|
let mut encoded_png = vec![];
|
||||||
|
|
||||||
let mut png_encoder = png::Encoder::new(&mut encoded_png, 64, 64);
|
let mut png_encoder = png::Encoder::new(&mut encoded_png, 64, 64);
|
||||||
png_encoder.set_color(png::ColorType::Rgba);
|
png_encoder.set_color(png::ColorType::Rgba);
|
||||||
png_encoder.set_depth(png::BitDepth::Eight);
|
png_encoder.set_depth(png::BitDepth::Eight);
|
||||||
png_encoder.set_filter(png::FilterType::NoFilter);
|
png_encoder.set_filter(png::Filter::NoFilter);
|
||||||
png_encoder.set_compression(png::Compression::Fast);
|
png_encoder.set_compression(png::Compression::Fast);
|
||||||
|
|
||||||
// Keeping color space information properly set, to handle the occasional
|
// Keeping color space information properly set, to handle the occasional
|
||||||
// strange PNG with non-sRGB chromaticities and/or different grayscale spaces
|
// strange PNG with non-sRGB chromaticities and/or different grayscale spaces
|
||||||
// that keeps most people wondering, is what sets a carefully crafted image
|
// that keeps most people wondering, is what sets a carefully crafted image
|
||||||
// manipulation routine apart :)
|
// manipulation routine apart :)
|
||||||
if let Some(source_chromaticities) =
|
if let Some(source_chromaticities) =
|
||||||
png_reader.info().source_chromaticities.as_ref().copied()
|
png_reader.info().source_chromaticities.as_ref().copied()
|
||||||
{
|
{
|
||||||
png_encoder.set_source_chromaticities(source_chromaticities);
|
png_encoder.set_source_chromaticities(source_chromaticities);
|
||||||
}
|
}
|
||||||
if let Some(source_gamma) =
|
if let Some(source_gamma) = png_reader.info().source_gamma.as_ref().copied()
|
||||||
png_reader.info().source_gamma.as_ref().copied()
|
{
|
||||||
{
|
png_encoder.set_source_gamma(source_gamma);
|
||||||
png_encoder.set_source_gamma(source_gamma);
|
}
|
||||||
}
|
if let Some(source_srgb) = png_reader.info().srgb.as_ref().copied() {
|
||||||
if let Some(source_srgb) = png_reader.info().srgb.as_ref().copied() {
|
png_encoder.set_source_srgb(source_srgb);
|
||||||
png_encoder.set_source_srgb(source_srgb);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let png_buf = bytemuck::try_cast_slice(&texture_buf)
|
let png_buf = bytemuck::try_cast_slice(&texture_buf)
|
||||||
.map_err(|_| ErrorKind::InvalidPng)?;
|
.map_err(|_| ErrorKind::InvalidPng)?;
|
||||||
let mut png_writer = png_encoder.write_header()?;
|
let mut png_writer = png_encoder.write_header()?;
|
||||||
png_writer.write_image_data(png_buf)?;
|
png_writer.write_image_data(png_buf)?;
|
||||||
png_writer.finish()?;
|
png_writer.finish()?;
|
||||||
|
|
||||||
Ok(encoded_png.into())
|
Ok(encoded_png.into())
|
||||||
})
|
|
||||||
.await?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads a skin texture and returns a 64x64 buffer in RGBA format.
|
/// Reads a skin texture and returns a 64x64 buffer in RGBA format.
|
||||||
fn get_skin_texture_buffer<R: Read>(
|
fn get_skin_texture_buffer<R: BufRead + Seek>(
|
||||||
png_reader: &mut png::Reader<R>,
|
png_reader: &mut png::Reader<R>,
|
||||||
is_legacy_skin: bool,
|
is_legacy_skin: bool,
|
||||||
) -> crate::Result<Vec<Rgba<u8>>> {
|
) -> crate::Result<Vec<Rgba<u8>>> {
|
||||||
|
let output_buffer_size = png_reader
|
||||||
|
.output_buffer_size()
|
||||||
|
.expect("Reasonable skin texture size verified already");
|
||||||
let mut png_buf = if is_legacy_skin {
|
let mut png_buf = if is_legacy_skin {
|
||||||
// Legacy skins have half the height, so duplicate the rows to
|
// Legacy skins have half the height, so duplicate the rows to
|
||||||
// turn them into a 64x64 texture
|
// turn them into a 64x64 texture
|
||||||
vec![0; png_reader.output_buffer_size() * 2]
|
vec![0; output_buffer_size * 2]
|
||||||
} else {
|
} else {
|
||||||
// Modern skins are left as-is
|
// Modern skins are left as-is
|
||||||
vec![0; png_reader.output_buffer_size()]
|
vec![0; output_buffer_size]
|
||||||
};
|
};
|
||||||
png_reader.next_frame(&mut png_buf)?;
|
png_reader.next_frame(&mut png_buf)?;
|
||||||
|
|
||||||
@@ -373,9 +375,10 @@ fn set_alpha(
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn normalize_skin_texture_works() {
|
async fn normalize_skin_texture_works() {
|
||||||
let decode_to_pixels = |png_data: &[u8]| {
|
let decode_to_pixels = |png_data: &[u8]| {
|
||||||
let decoder = png::Decoder::new(png_data);
|
let decoder = png::Decoder::new(Cursor::new(png_data));
|
||||||
let mut reader = decoder.read_info().expect("Failed to read PNG info");
|
let mut reader = decoder.read_info().expect("Failed to read PNG info");
|
||||||
let mut buffer = vec![0; reader.output_buffer_size()];
|
let mut buffer =
|
||||||
|
vec![0; reader.output_buffer_size().expect("Skin size too large")];
|
||||||
reader
|
reader
|
||||||
.next_frame(&mut buffer)
|
.next_frame(&mut buffer)
|
||||||
.expect("Failed to decode PNG");
|
.expect("Failed to decode PNG");
|
||||||
|
|||||||
@@ -176,6 +176,9 @@ pub enum ErrorKind {
|
|||||||
|
|
||||||
#[error("Deserialization error: {0}")]
|
#[error("Deserialization error: {0}")]
|
||||||
DeserializationError(#[from] serde::de::value::Error),
|
DeserializationError(#[from] serde::de::value::Error),
|
||||||
|
|
||||||
|
#[error("Discord IPC error: {0}")]
|
||||||
|
DiscordRichPresenceError(#[from] discord_rich_presence::error::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|||||||
@@ -18,12 +18,7 @@ impl DiscordGuard {
|
|||||||
/// Initialize discord IPC client, and attempt to connect to it
|
/// Initialize discord IPC client, and attempt to connect to it
|
||||||
/// If it fails, it will still return a DiscordGuard, but the client will be unconnected
|
/// If it fails, it will still return a DiscordGuard, but the client will be unconnected
|
||||||
pub fn init() -> crate::Result<DiscordGuard> {
|
pub fn init() -> crate::Result<DiscordGuard> {
|
||||||
let dipc =
|
let dipc = DiscordIpcClient::new("1123683254248148992");
|
||||||
DiscordIpcClient::new("1123683254248148992").map_err(|e| {
|
|
||||||
crate::ErrorKind::OtherError(format!(
|
|
||||||
"Could not create Discord client {e}",
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(DiscordGuard {
|
Ok(DiscordGuard {
|
||||||
client: Arc::new(RwLock::new(dipc)),
|
client: Arc::new(RwLock::new(dipc)),
|
||||||
@@ -87,25 +82,14 @@ impl DiscordGuard {
|
|||||||
let mut client: tokio::sync::RwLockWriteGuard<'_, DiscordIpcClient> =
|
let mut client: tokio::sync::RwLockWriteGuard<'_, DiscordIpcClient> =
|
||||||
self.client.write().await;
|
self.client.write().await;
|
||||||
let res = client.set_activity(activity.clone());
|
let res = client.set_activity(activity.clone());
|
||||||
let could_not_set_err = |e: Box<dyn serde::ser::StdError>| {
|
|
||||||
crate::ErrorKind::OtherError(format!(
|
|
||||||
"Could not update Discord activity {e}",
|
|
||||||
))
|
|
||||||
};
|
|
||||||
|
|
||||||
if reconnect_if_fail {
|
if reconnect_if_fail {
|
||||||
if let Err(_e) = res {
|
if let Err(_e) = res {
|
||||||
client.reconnect().map_err(|e| {
|
client.reconnect()?;
|
||||||
crate::ErrorKind::OtherError(format!(
|
return Ok(client.set_activity(activity)?); // try again, but don't reconnect if it fails again
|
||||||
"Could not reconnect to Discord IPC {e}",
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
return Ok(client
|
|
||||||
.set_activity(activity)
|
|
||||||
.map_err(could_not_set_err)?); // try again, but don't reconnect if it fails again
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.map_err(could_not_set_err)?;
|
res?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -126,25 +110,13 @@ impl DiscordGuard {
|
|||||||
let mut client = self.client.write().await;
|
let mut client = self.client.write().await;
|
||||||
let res = client.clear_activity();
|
let res = client.clear_activity();
|
||||||
|
|
||||||
let could_not_clear_err = |e: Box<dyn serde::ser::StdError>| {
|
|
||||||
crate::ErrorKind::OtherError(format!(
|
|
||||||
"Could not clear Discord activity {e}",
|
|
||||||
))
|
|
||||||
};
|
|
||||||
|
|
||||||
if reconnect_if_fail {
|
if reconnect_if_fail {
|
||||||
if res.is_err() {
|
if res.is_err() {
|
||||||
client.reconnect().map_err(|e| {
|
client.reconnect()?;
|
||||||
crate::ErrorKind::OtherError(format!(
|
return Ok(client.clear_activity()?); // try again, but don't reconnect if it fails again
|
||||||
"Could not reconnect to Discord IPC {e}",
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
return Ok(client
|
|
||||||
.clear_activity()
|
|
||||||
.map_err(could_not_clear_err)?); // try again, but don't reconnect if it fails again
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.map_err(could_not_clear_err)?;
|
res?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ impl FriendsSocket {
|
|||||||
pub async fn disconnect(&self) -> crate::Result<()> {
|
pub async fn disconnect(&self) -> crate::Result<()> {
|
||||||
let mut write_lock = self.write.write().await;
|
let mut write_lock = self.write.write().await;
|
||||||
if let Some(ref mut write_half) = *write_lock {
|
if let Some(ref mut write_half) = *write_lock {
|
||||||
write_half.close().await?;
|
SinkExt::close(write_half).await?;
|
||||||
*write_lock = None;
|
*write_lock = None;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -516,7 +516,7 @@ impl Process {
|
|||||||
chrono::DateTime::<Utc>::from_timestamp(secs, nsecs)
|
chrono::DateTime::<Utc>::from_timestamp(secs, nsecs)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
chrono::DateTime::<Utc>::from_timestamp(timestamp_val, 0)
|
chrono::DateTime::<Utc>::from_timestamp_secs(timestamp_val)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,36 @@
|
|||||||
// AUTO-GENERATED FILE - DO NOT EDIT
|
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||||
import { article as a_new_chapter_for_modrinth_servers } from "./a_new_chapter_for_modrinth_servers";
|
import { article as windows_borderless_malware_disclosure } from "./windows_borderless_malware_disclosure";
|
||||||
import { article as accelerating_development } from "./accelerating_development";
|
import { article as whats_modrinth } from "./whats_modrinth";
|
||||||
import { article as becoming_sustainable } from "./becoming_sustainable";
|
|
||||||
import { article as capital_return } from "./capital_return";
|
|
||||||
import { article as carbon_ads } from "./carbon_ads";
|
|
||||||
import { article as creator_monetization } from "./creator_monetization";
|
|
||||||
import { article as creator_update } from "./creator_update";
|
|
||||||
import { article as creator_updates_july_2025 } from "./creator_updates_july_2025";
|
|
||||||
import { article as design_refresh } from "./design_refresh";
|
|
||||||
import { article as download_adjustment } from "./download_adjustment";
|
|
||||||
import { article as free_server_medal } from "./free_server_medal";
|
|
||||||
import { article as knossos_v2_1_0 } from "./knossos_v2_1_0";
|
|
||||||
import { article as licensing_guide } from "./licensing_guide";
|
|
||||||
import { article as modpack_changes } from "./modpack_changes";
|
|
||||||
import { article as modpacks_alpha } from "./modpacks_alpha";
|
|
||||||
import { article as modrinth_app_beta } from "./modrinth_app_beta";
|
|
||||||
import { article as modrinth_beta } from "./modrinth_beta";
|
|
||||||
import { article as modrinth_servers_asia } from "./modrinth_servers_asia";
|
|
||||||
import { article as modrinth_servers_beta } from "./modrinth_servers_beta";
|
|
||||||
import { article as new_environments } from "./new_environments";
|
|
||||||
import { article as new_site_beta } from "./new_site_beta";
|
|
||||||
import { article as plugins_resource_packs } from "./plugins_resource_packs";
|
|
||||||
import { article as pride_campaign_2025 } from "./pride_campaign_2025";
|
|
||||||
import { article as redesign } from "./redesign";
|
|
||||||
import { article as russian_censorship } from "./russian_censorship";
|
|
||||||
import { article as skins_now_in_modrinth_app } from "./skins_now_in_modrinth_app";
|
|
||||||
import { article as standing_by_our_values } from "./standing_by_our_values";
|
|
||||||
import { article as standing_by_our_values_russian } from "./standing_by_our_values_russian";
|
|
||||||
import { article as two_years_of_modrinth } from "./two_years_of_modrinth";
|
import { article as two_years_of_modrinth } from "./two_years_of_modrinth";
|
||||||
import { article as two_years_of_modrinth_history } from "./two_years_of_modrinth_history";
|
import { article as two_years_of_modrinth_history } from "./two_years_of_modrinth_history";
|
||||||
import { article as whats_modrinth } from "./whats_modrinth";
|
import { article as standing_by_our_values } from "./standing_by_our_values";
|
||||||
import { article as windows_borderless_malware_disclosure } from "./windows_borderless_malware_disclosure";
|
import { article as standing_by_our_values_russian } from "./standing_by_our_values_russian";
|
||||||
|
import { article as skins_now_in_modrinth_app } from "./skins_now_in_modrinth_app";
|
||||||
|
import { article as russian_censorship } from "./russian_censorship";
|
||||||
|
import { article as redesign } from "./redesign";
|
||||||
|
import { article as pride_campaign_2025 } from "./pride_campaign_2025";
|
||||||
|
import { article as plugins_resource_packs } from "./plugins_resource_packs";
|
||||||
|
import { article as new_site_beta } from "./new_site_beta";
|
||||||
|
import { article as new_environments } from "./new_environments";
|
||||||
|
import { article as modrinth_servers_beta } from "./modrinth_servers_beta";
|
||||||
|
import { article as modrinth_servers_asia } from "./modrinth_servers_asia";
|
||||||
|
import { article as modrinth_beta } from "./modrinth_beta";
|
||||||
|
import { article as modrinth_app_beta } from "./modrinth_app_beta";
|
||||||
|
import { article as modpacks_alpha } from "./modpacks_alpha";
|
||||||
|
import { article as modpack_changes } from "./modpack_changes";
|
||||||
|
import { article as licensing_guide } from "./licensing_guide";
|
||||||
|
import { article as knossos_v2_1_0 } from "./knossos_v2_1_0";
|
||||||
|
import { article as free_server_medal } from "./free_server_medal";
|
||||||
|
import { article as download_adjustment } from "./download_adjustment";
|
||||||
|
import { article as design_refresh } from "./design_refresh";
|
||||||
|
import { article as creator_updates_july_2025 } from "./creator_updates_july_2025";
|
||||||
|
import { article as creator_update } from "./creator_update";
|
||||||
|
import { article as creator_monetization } from "./creator_monetization";
|
||||||
|
import { article as carbon_ads } from "./carbon_ads";
|
||||||
|
import { article as capital_return } from "./capital_return";
|
||||||
|
import { article as becoming_sustainable } from "./becoming_sustainable";
|
||||||
|
import { article as accelerating_development } from "./accelerating_development";
|
||||||
|
import { article as a_new_chapter_for_modrinth_servers } from "./a_new_chapter_for_modrinth_servers";
|
||||||
|
|
||||||
export const articles = [
|
export const articles = [
|
||||||
windows_borderless_malware_disclosure,
|
windows_borderless_malware_disclosure,
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.89.0"
|
channel = "1.90.0"
|
||||||
profile = "default"
|
profile = "default"
|
||||||
|
|||||||
Reference in New Issue
Block a user