feat: implement selfupdate command
Release / create-release (push) Failing after 31s
Release / build-linux-x86_64 (push) Has been skipped
Release / build-linux-aarch64 (push) Has been skipped
Release / build-macos (aarch64-apple-darwin) (push) Has been skipped
Release / build-macos (x86_64-apple-darwin) (push) Has been skipped
Release / build-windows (push) Has been skipped

- Fetch latest release from Gitea API using existing reqwest client
- Match release asset by BUILD_TARGET triple (supports .tar.gz and .zip)
- Compare versions; show confirmation prompt (skippable with -y/--yes)
- Download archive, extract binary, atomically replace self via self-replace
- Support private repositories via GITEA_TOKEN environment variable
- Expose BUILD_TARGET in build.rs for compile-time target triple detection
- Add .gitea/workflows/release.yml for multi-platform release builds on tag push

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-17 20:15:19 +09:00
parent 7947c3bae9
commit 3578a39d27
7 changed files with 832 additions and 1 deletions
+139
View File
@@ -0,0 +1,139 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
create-release:
runs-on: ubuntu-latest
outputs:
release_id: ${{ steps.create.outputs.release_id }}
steps:
- name: Create Gitea release
id: create
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
RESPONSE=$(curl -sf -X POST \
-H "Authorization: Bearer ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases" \
-d "{\"tag_name\": \"${GITHUB_REF_NAME}\", \"name\": \"${GITHUB_REF_NAME}\"}")
echo "release_id=$(echo "$RESPONSE" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')" >> "$GITHUB_OUTPUT"
build-linux-x86_64:
needs: create-release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust stable
run: rustup update stable && rustup default stable
- name: Add musl target
run: rustup target add x86_64-unknown-linux-musl
- name: Install musl tools
run: sudo apt-get update && sudo apt-get install -y musl-tools
- name: Build
run: cargo build --release --target x86_64-unknown-linux-musl
- name: Create archive
run: |
VERSION=${GITHUB_REF_NAME#v}
TARGET=x86_64-unknown-linux-musl
ARCHIVE="mdrs-${VERSION}-${TARGET}.tar.gz"
tar -czf "${ARCHIVE}" -C target/${TARGET}/release mdrs
echo "ARCHIVE=${ARCHIVE}" >> "$GITHUB_ENV"
- name: Upload asset
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
curl -sf -X POST \
-H "Authorization: Bearer ${GITEA_TOKEN}" \
-F "attachment=@${ARCHIVE}" \
"${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${{ needs.create-release.outputs.release_id }}/assets"
build-linux-aarch64:
needs: create-release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust stable
run: rustup update stable && rustup default stable
- name: Install cross
run: cargo install cross --locked
- name: Build
run: cross build --release --target aarch64-unknown-linux-musl
- name: Create archive
run: |
VERSION=${GITHUB_REF_NAME#v}
TARGET=aarch64-unknown-linux-musl
ARCHIVE="mdrs-${VERSION}-${TARGET}.tar.gz"
tar -czf "${ARCHIVE}" -C target/${TARGET}/release mdrs
echo "ARCHIVE=${ARCHIVE}" >> "$GITHUB_ENV"
- name: Upload asset
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
curl -sf -X POST \
-H "Authorization: Bearer ${GITEA_TOKEN}" \
-F "attachment=@${ARCHIVE}" \
"${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${{ needs.create-release.outputs.release_id }}/assets"
build-macos:
needs: create-release
runs-on: macos-latest
strategy:
matrix:
target:
- x86_64-apple-darwin
- aarch64-apple-darwin
steps:
- uses: actions/checkout@v4
- name: Install Rust stable
run: rustup update stable && rustup default stable
- name: Add target
run: rustup target add ${{ matrix.target }}
- name: Build
run: cargo build --release --target ${{ matrix.target }}
- name: Create archive
run: |
VERSION=${GITHUB_REF_NAME#v}
TARGET=${{ matrix.target }}
ARCHIVE="mdrs-${VERSION}-${TARGET}.tar.gz"
tar -czf "${ARCHIVE}" -C target/${TARGET}/release mdrs
echo "ARCHIVE=${ARCHIVE}" >> "$GITHUB_ENV"
- name: Upload asset
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
curl -sf -X POST \
-H "Authorization: Bearer ${GITEA_TOKEN}" \
-F "attachment=@${ARCHIVE}" \
"${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${{ needs.create-release.outputs.release_id }}/assets"
build-windows:
needs: create-release
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust stable
run: rustup update stable && rustup default stable
- name: Build
run: cargo build --release --target x86_64-pc-windows-msvc
- name: Create archive
shell: pwsh
run: |
$VERSION = $env:GITHUB_REF_NAME -replace '^v', ''
$TARGET = "x86_64-pc-windows-msvc"
$ARCHIVE = "mdrs-${VERSION}-${TARGET}.zip"
Compress-Archive -Path "target/${TARGET}/release/mdrs.exe" -DestinationPath $ARCHIVE
echo "ARCHIVE=$ARCHIVE" >> $env:GITHUB_ENV
- name: Upload asset
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
shell: bash
run: |
curl -sf -X POST \
-H "Authorization: Bearer ${GITEA_TOKEN}" \
-F "attachment=@${ARCHIVE}" \
"${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${{ needs.create-release.outputs.release_id }}/assets"
Generated
+451 -1
View File
@@ -2,6 +2,23 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "ahash"
version = "0.7.8"
@@ -87,6 +104,15 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "arrayvec"
version = "0.7.6"
@@ -232,6 +258,25 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "bzip2"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
dependencies = [
"bzip2-sys",
]
[[package]]
name = "bzip2-sys"
version = "0.1.13+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "cc"
version = "1.2.60"
@@ -239,6 +284,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
@@ -254,6 +301,16 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "clap"
version = "4.6.1"
@@ -315,6 +372,12 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e57e3272f0190c3f1584272d613719ba5fc7df7f4942fe542e63d949cf3a649b"
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "cow-utils"
version = "0.1.3"
@@ -330,12 +393,42 @@ dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]]
name = "critical-section"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crypto-common"
version = "0.1.7"
@@ -363,6 +456,32 @@ version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
[[package]]
name = "deflate64"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"powerfmt",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "digest"
version = "0.10.7"
@@ -371,6 +490,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
@@ -495,12 +615,39 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "fastrand"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
[[package]]
name = "filetime"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
dependencies = [
"cfg-if",
"libc",
"libredox",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "flate2"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "fnv"
version = "1.0.7"
@@ -701,6 +848,15 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "http"
version = "1.4.0"
@@ -922,6 +1078,15 @@ dependencies = [
"hashbrown 0.17.0",
]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]]
name = "ipnet"
version = "2.12.0"
@@ -950,6 +1115,16 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.4",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.95"
@@ -974,7 +1149,10 @@ version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
dependencies = [
"bitflags",
"libc",
"plain",
"redox_syscall 0.7.4",
]
[[package]]
@@ -983,6 +1161,12 @@ version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
version = "0.8.2"
@@ -1019,6 +1203,27 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "lzma-rs"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
dependencies = [
"byteorder",
"crc",
]
[[package]]
name = "lzma-sys"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "mdrs-client-rust"
version = "0.1.1"
@@ -1030,17 +1235,22 @@ dependencies = [
"ctrlc",
"dirs",
"dotenvy",
"flate2",
"fs2",
"futures",
"os_info",
"reqwest",
"rpassword",
"self-replace",
"serde",
"serde_json",
"sha2",
"tar",
"tempfile",
"tokio",
"unicode-normalization",
"validators",
"zip",
]
[[package]]
@@ -1071,6 +1281,16 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
name = "mio"
version = "1.2.0"
@@ -1116,6 +1336,12 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "num-conv"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -1342,11 +1568,21 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"redox_syscall 0.5.18",
"smallvec",
"windows-link",
]
[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest",
"hmac",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -1379,6 +1615,18 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkg-config"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "plain"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "postcard"
version = "1.1.3"
@@ -1401,6 +1649,12 @@ dependencies = [
"zerovec",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -1601,6 +1855,15 @@ dependencies = [
"bitflags",
]
[[package]]
name = "redox_syscall"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a"
dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.4.6"
@@ -1824,6 +2087,19 @@ dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.38"
@@ -1895,6 +2171,17 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "self-replace"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03ec815b5eab420ab893f63393878d89c90fdd94c0bcc44c07abb8ad95552fb7"
dependencies = [
"fastrand",
"tempfile",
"windows-sys 0.52.0",
]
[[package]]
name = "semver"
version = "1.0.28"
@@ -1960,6 +2247,17 @@ dependencies = [
"serde",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha2"
version = "0.10.9"
@@ -1987,6 +2285,12 @@ dependencies = [
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "simdutf8"
version = "0.1.5"
@@ -2122,6 +2426,30 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tar"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.61.2",
]
[[package]]
name = "thiserror"
version = "1.0.69"
@@ -2162,6 +2490,25 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "time"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
]
[[package]]
name = "time-core"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "tinystr"
version = "0.8.3"
@@ -2842,6 +3189,25 @@ dependencies = [
"tap",
]
[[package]]
name = "xattr"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
dependencies = [
"libc",
"rustix",
]
[[package]]
name = "xz2"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
dependencies = [
"lzma-sys",
]
[[package]]
name = "yoke"
version = "0.8.2"
@@ -2911,6 +3277,20 @@ name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "zerotrie"
@@ -2945,8 +3325,78 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "zip"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
dependencies = [
"aes",
"arbitrary",
"bzip2",
"constant_time_eq",
"crc32fast",
"crossbeam-utils",
"deflate64",
"displaydoc",
"flate2",
"getrandom 0.3.4",
"hmac",
"indexmap",
"lzma-rs",
"memchr",
"pbkdf2",
"sha1",
"thiserror 2.0.18",
"time",
"xz2",
"zeroize",
"zopfli",
"zstd",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zopfli"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
dependencies = [
"bumpalo",
"crc32fast",
"log",
"simd-adler32",
]
[[package]]
name = "zstd"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.16+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
dependencies = [
"cc",
"pkg-config",
]
+5
View File
@@ -29,3 +29,8 @@ ctrlc = "3"
os_info = "3"
dotenvy = "0.15"
unicode-normalization = "0.1"
self-replace = "1"
tar = "0.4"
flate2 = "1"
zip = "2"
tempfile = "3"
+5
View File
@@ -13,5 +13,10 @@ fn main() {
.unwrap_or("unknown")
.to_string();
println!("cargo:rustc-env=RUSTC_VERSION={}", version);
// Expose the build target triple so selfupdate can match release assets.
println!(
"cargo:rustc-env=BUILD_TARGET={}",
std::env::var("TARGET").unwrap_or_default()
);
println!("cargo:rerun-if-changed=build.rs");
}
+1
View File
@@ -18,3 +18,4 @@ pub mod shared;
pub mod upload;
pub mod version;
pub mod whoami;
pub mod selfupdate;
+218
View File
@@ -0,0 +1,218 @@
use anyhow::{anyhow, bail};
use reqwest::header::{AUTHORIZATION, USER_AGENT};
use serde::Deserialize;
use std::env;
use std::io::{self, Write};
use std::path::Path;
const GITEA_HOST: &str = "https://git.ni.riken.jp";
const REPO_OWNER: &str = "niu";
const REPO_NAME: &str = "mdrs-client-rust";
/// Current build target triple, captured at compile time via build.rs.
const BUILD_TARGET: &str = env!("BUILD_TARGET");
#[derive(Deserialize)]
struct GiteaRelease {
tag_name: String,
assets: Vec<GiteaAsset>,
}
#[derive(Deserialize)]
struct GiteaAsset {
name: String,
browser_download_url: String,
}
/// Returns true if `latest` is strictly greater than `current` (semver-like comparison).
fn is_newer(current: &str, latest: &str) -> bool {
let parse = |s: &str| -> Vec<u64> {
s.trim_start_matches('v')
.split('.')
.map(|p| p.parse::<u64>().unwrap_or(0))
.collect()
};
let cur = parse(current);
let lat = parse(latest);
let len = cur.len().max(lat.len());
for i in 0..len {
let c = cur.get(i).copied().unwrap_or(0);
let l = lat.get(i).copied().unwrap_or(0);
if l > c {
return true;
}
if l < c {
return false;
}
}
false
}
/// Extract the binary named `bin_name` from a `.tar.gz` archive at `archive_path`
/// and write it to `dest_path`.
fn extract_from_tar_gz(archive_path: &Path, bin_name: &str, dest_path: &Path) -> anyhow::Result<()> {
use flate2::read::GzDecoder;
use tar::Archive;
let file = std::fs::File::open(archive_path)?;
let gz = GzDecoder::new(file);
let mut archive = Archive::new(gz);
for entry in archive.entries()? {
let mut entry = entry?;
let path = entry.path()?;
// Match by file name only (ignore directory prefix in archive).
if path.file_name().and_then(|n| n.to_str()) == Some(bin_name) {
entry.unpack(dest_path)?;
return Ok(());
}
}
bail!("Binary '{}' not found in archive", bin_name)
}
/// Extract the binary named `bin_name` from a `.zip` archive at `archive_path`
/// and write it to `dest_path`.
fn extract_from_zip(archive_path: &Path, bin_name: &str, dest_path: &Path) -> anyhow::Result<()> {
use std::io::Read;
let file = std::fs::File::open(archive_path)?;
let mut archive = zip::ZipArchive::new(file)?;
for i in 0..archive.len() {
let mut entry = archive.by_index(i)?;
let entry_name = entry.name().to_owned();
let file_name = Path::new(&entry_name)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
if file_name == bin_name {
let mut buf = Vec::new();
entry.read_to_end(&mut buf)?;
std::fs::write(dest_path, &buf)?;
return Ok(());
}
}
bail!("Binary '{}' not found in archive", bin_name)
}
pub async fn selfupdate(yes: bool) -> anyhow::Result<()> {
let current_version = env!("CARGO_PKG_VERSION");
println!("Checking for updates (current version: {current_version}, target: {BUILD_TARGET})...");
let api_url = format!(
"{GITEA_HOST}/api/v1/repos/{REPO_OWNER}/{REPO_NAME}/releases?limit=1"
);
let client = reqwest::Client::new();
let mut req = client
.get(&api_url)
.header(USER_AGENT, format!("mdrs/{current_version}"));
if let Ok(token) = env::var("GITEA_TOKEN") {
req = req.header(AUTHORIZATION, format!("Bearer {token}"));
}
let resp = req.send().await?;
if !resp.status().is_success() {
bail!("Failed to fetch release info: HTTP {}", resp.status());
}
let releases: Vec<GiteaRelease> = resp.json().await?;
let release = releases
.into_iter()
.next()
.ok_or_else(|| anyhow!("No releases found"))?;
let latest_version = release.tag_name.trim_start_matches('v');
if !is_newer(current_version, latest_version) {
println!("Already up-to-date ({current_version}).");
return Ok(());
}
println!("New version available: {latest_version}");
// Find the asset matching the current build target.
let asset = release
.assets
.iter()
.find(|a| a.name.contains(BUILD_TARGET))
.ok_or_else(|| {
let names: Vec<&str> = release.assets.iter().map(|a| a.name.as_str()).collect();
anyhow!(
"No release asset found for target '{BUILD_TARGET}'. \
Available assets: {}",
names.join(", ")
)
})?;
println!("Asset: {}", asset.name);
if !yes {
print!("Update to version {latest_version}? [y/N] ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
if !input.trim().eq_ignore_ascii_case("y") {
println!("Update cancelled.");
return Ok(());
}
}
// Download the asset to a temporary directory.
let tmp_dir = tempfile::Builder::new()
.prefix("mdrs-selfupdate-")
.tempdir()?;
let archive_path = tmp_dir.path().join(&asset.name);
println!("Downloading {}...", asset.browser_download_url);
let mut download_req = client
.get(&asset.browser_download_url)
.header(USER_AGENT, format!("mdrs/{current_version}"));
if let Ok(token) = env::var("GITEA_TOKEN") {
download_req = download_req.header(AUTHORIZATION, format!("Bearer {token}"));
}
let download_resp = download_req.send().await?;
if !download_resp.status().is_success() {
bail!(
"Failed to download asset: HTTP {}",
download_resp.status()
);
}
let bytes = download_resp.bytes().await?;
std::fs::write(&archive_path, &bytes)?;
// Extract the binary from the archive.
let bin_name = if cfg!(windows) { "mdrs.exe" } else { "mdrs" };
let new_bin = tmp_dir.path().join(bin_name);
let name = asset.name.as_str();
if name.ends_with(".tar.gz") || name.ends_with(".tgz") {
extract_from_tar_gz(&archive_path, bin_name, &new_bin)?;
} else if name.ends_with(".zip") {
extract_from_zip(&archive_path, bin_name, &new_bin)?;
} else {
bail!("Unsupported archive format: {}", asset.name);
}
// Make the extracted binary executable on Unix.
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&new_bin)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&new_bin, perms)?;
}
// Atomically replace the current executable.
self_replace::self_replace(&new_bin)?;
println!("Successfully updated to version {latest_version}.");
Ok(())
}
+13
View File
@@ -111,6 +111,13 @@ enum Commands {
},
/// Show the version of this tool
Version,
/// Update this binary to the latest release
#[command(name = "selfupdate")]
SelfUpdate {
/// Skip the confirmation prompt
#[arg(short = 'y', long)]
yes: bool,
},
}
/// Print the error message in Python-compatible format and exit with code 2.
@@ -349,5 +356,11 @@ fn main() {
Commands::Version => {
commands::version::version();
}
Commands::SelfUpdate { yes } => {
let rt = tokio::runtime::Runtime::new().unwrap();
if let Err(e) = rt.block_on(commands::selfupdate::selfupdate(*yes)) {
handle_error(e.into());
}
}
}
}