aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml2
-rw-r--r--README.md7
-rw-r--r--dist/index.html539
-rw-r--r--mingling_cli/Cargo.lock56
-rw-r--r--mingling_cli/Cargo.toml3
-rw-r--r--mingling_cli/src/bin/wrapper.rs23
-rw-r--r--mingling_cli/src/updater/cmd_update.rs287
7 files changed, 701 insertions, 216 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 90e7d57..4ef1bbb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -86,7 +86,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: mling-${{ steps.metadata.outputs.os }}-pkg
- path: dist/
+ path: dist/mling-*
if-no-files-found: error
Move-Unreleased-Tag:
diff --git a/README.md b/README.md
index 4310891..a5d73e7 100644
--- a/README.md
+++ b/README.md
@@ -112,8 +112,10 @@ tag = "unreleased"
features = []
```
-> [!NOTE]
-> To learn more, check out [Writing with Mingling](https://github.com/mingling-rs/mingling/blob/main/GETTING-STARTED.md)
+To learn more, check out [Writing with Mingling](https://github.com/mingling-rs/mingling/blob/main/GETTING-STARTED.md)
+
+> [!Note]
+> You can also use the `mling` scaffolding tool to build, check, and manage your project [Download](https://mingling-rs.github.io/mingling/dist) | [About](https://github.com/mingling-rs/mingling/tree/main/mingling_cli)
## Roadmap
@@ -160,6 +162,7 @@ See [LICENSE-MIT](LICENSE-MIT) or [LICENSE-APACHE](LICENSE-APACHE) file for deta
**To learn more, check out the following links:**
+- ⚡ Mingling CLI - [About Mling](https://mingling-rs.github.io/mingling/dist)
- 📦 Repo - [Github](https://github.com/mingling-rs/mingling) | [Gitee](https://gitee.com/mingling-rs/mingling) | [Origin](https://catilgrass.cn/mingling.git)
- 🚪 Mainpage - [Github](https://mingling-rs.github.io/mingling/) | [crates.io](https://crates.io/crates/mingling)
- 💡 Examples - [Github](https://mingling-rs.github.io/mingling/docs/examples.html)
diff --git a/dist/index.html b/dist/index.html
new file mode 100644
index 0000000..6fcdb0b
--- /dev/null
+++ b/dist/index.html
@@ -0,0 +1,539 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Mling CLI — Download</title>
+ <link
+ rel="icon"
+ type="image/png"
+ href="../docs/res/favicon_small.png"
+ />
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+ <link
+ href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700&family=JetBrains+Mono:wght@400;600&display=swap"
+ rel="stylesheet"
+ />
+ <style>
+ *,
+ *::before,
+ *::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ }
+
+ html {
+ scroll-behavior: smooth;
+ }
+
+ body {
+ font-family: "Noto Serif SC", Georgia, "Times New Roman", serif;
+ background-color: #1a1410;
+ color: #e8ddd0;
+ line-height: 1.6;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ overflow-x: hidden;
+ }
+
+ a {
+ color: #d4a84b;
+ text-decoration: none;
+ transition: color 0.2s;
+ }
+ a:hover {
+ color: #e8c46a;
+ }
+
+ .container {
+ max-width: 1080px;
+ margin: 0 auto;
+ padding: 0 1.5rem;
+ }
+
+ nav {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.8rem 2rem;
+ background: rgba(26, 20, 16, 0.88);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ border-bottom: 1px solid rgba(212, 168, 75, 0.12);
+ }
+
+ nav .logo {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-weight: 700;
+ font-size: 1.1rem;
+ color: #e8ddd0;
+ }
+
+ nav .logo img {
+ width: 28px;
+ height: 28px;
+ filter: brightness(0) invert(1);
+ }
+
+ nav .nav-links {
+ display: flex;
+ gap: 1.5rem;
+ align-items: center;
+ font-size: 0.9rem;
+ }
+
+ nav .nav-links a {
+ color: #9a8a7a;
+ transition: color 0.2s;
+ }
+
+ nav .nav-links a:hover {
+ color: #d4a84b;
+ }
+
+ nav .nav-links .btn-nav {
+ display: inline-block;
+ padding: 0.35rem 1rem;
+ border: 1px solid #d4a84b;
+ border-radius: 20px;
+ color: #d4a84b;
+ font-weight: 600;
+ font-size: 0.85rem;
+ transition:
+ background 0.2s,
+ color 0.2s;
+ }
+
+ nav .nav-links .btn-nav:hover {
+ background: #d4a84b;
+ color: #1a1410;
+ }
+
+ .hero {
+ padding: 5rem 1.5rem 3rem;
+ text-align: center;
+ }
+
+ .hero h1 {
+ font-size: 2.75rem;
+ font-weight: 700;
+ color: #e8ddd0;
+ margin-bottom: 0.75rem;
+ }
+
+ .hero .subtitle {
+ font-size: 1.1rem;
+ color: #9a8a7a;
+ max-width: 640px;
+ margin: 0 auto 2.5rem;
+ }
+
+ .download-row {
+ display: flex;
+ gap: 1.25rem;
+ justify-content: center;
+ flex-wrap: wrap;
+ }
+
+ .download-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.4rem;
+ min-width: 220px;
+ padding: 1.75rem 1.5rem;
+ background: #241c16;
+ border: 1px solid #3a2e24;
+ border-radius: 2px;
+ color: #e8ddd0;
+ transition:
+ border-color 0.25s,
+ transform 0.2s;
+ }
+
+ .download-card:hover {
+ border-color: #d4a84b;
+ color: #e8ddd0;
+ transform: translateY(-4px);
+ }
+
+ .download-card .platform {
+ width: 44px;
+ height: 44px;
+ margin-bottom: 0.25rem;
+ }
+
+ .download-card .platform.invert {
+ filter: brightness(0) invert(1);
+ }
+
+ .download-card .os-name {
+ font-size: 1.05rem;
+ font-weight: 700;
+ }
+
+ .download-card .file-name {
+ font-family: "JetBrains Mono", monospace;
+ font-size: 0.78rem;
+ color: #d4a84b;
+ text-decoration: underline;
+ transition: color 0.2s;
+ }
+
+ .download-card .file-name:hover {
+ color: #e8c46a;
+ }
+
+ .download-card .checksum {
+ font-size: 0.78rem;
+ color: #7a6a5a;
+ }
+
+ .download-card .checksum a {
+ color: #7a6a5a;
+ text-decoration: underline;
+ }
+
+ .download-card .checksum a:hover {
+ color: #d4a84b;
+ }
+
+ .checksum-note {
+ margin: 1.25rem auto 0;
+ width: fit-content;
+ font-size: 0.8rem;
+ color: #6a5a4a;
+ text-align: center;
+ }
+
+ .section {
+ padding: 3.5rem 1.5rem;
+ border-top: 1px solid #2a1e14;
+ }
+
+ .section.alt {
+ background: #241c16;
+ }
+
+ .section h2 {
+ font-size: 1.6rem;
+ font-weight: 700;
+ color: #e8ddd0;
+ margin-bottom: 1rem;
+ }
+
+ .section h2::before {
+ content: "✦";
+ color: #c43931;
+ margin-right: 0.5rem;
+ }
+
+ .section h2 span {
+ color: #c43931;
+ }
+
+ .section p {
+ color: #9a8a7a;
+ margin-bottom: 1.25rem;
+ max-width: 720px;
+ }
+
+ .code-block {
+ background: #1a1410;
+ border: 1px solid #3a2e24;
+ border-radius: 2px;
+ padding: 1.5rem 1.5rem 1.5rem;
+ overflow-x: auto;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
+ }
+
+ .code-block pre {
+ margin: 0;
+ padding: 0;
+ background: transparent;
+ }
+
+ .code-block code {
+ font-family: "JetBrains Mono", "Noto Serif SC", monospace;
+ font-variant-ligatures: contextual;
+ font-size: 0.85rem;
+ line-height: 1.65;
+ color: #c0b0a0;
+ }
+
+ .code-block code .cm {
+ color: #6a5a4a;
+ }
+
+ .cmd-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 1rem;
+ margin-top: 1rem;
+ }
+
+ .cmd-card {
+ background: #1a1410;
+ border: 1px solid #3a2e24;
+ border-radius: 2px;
+ padding: 1.25rem 1.25rem;
+ }
+
+ .cmd-card code {
+ display: inline-block;
+ font-family: "JetBrains Mono", monospace;
+ font-size: 0.85rem;
+ color: #d4a84b;
+ margin-bottom: 0.4rem;
+ }
+
+ .cmd-card p {
+ font-size: 0.85rem;
+ color: #9a8a7a;
+ margin-bottom: 0;
+ }
+
+ footer {
+ padding: 2rem 1.5rem;
+ text-align: center;
+ color: #6a5a4a;
+ font-size: 0.85rem;
+ border-top: 1px solid #2a1e14;
+ }
+
+ footer a {
+ color: #7a6a5a;
+ }
+
+ footer a:hover {
+ color: #d4a84b;
+ }
+
+ @media (max-width: 768px) {
+ nav {
+ padding: 0.8rem 1rem;
+ }
+
+ nav .nav-links {
+ gap: 0.8rem;
+ }
+
+ .hero h1 {
+ font-size: 2rem;
+ }
+
+ .download-card {
+ min-width: 100%;
+ }
+ }
+ </style>
+ </head>
+
+ <body>
+ <nav>
+ <a href="../" class="logo">
+ <img
+ src="../docs/res/icon.png"
+ alt="Mingling icon"
+ width="28"
+ height="28"
+ />
+ Mìng Lìng
+ </a>
+ <div class="nav-links">
+ <a
+ href="https://github.com/mingling-rs/mingling/tree/main/mingling_cli"
+ target="_blank"
+ >GitHub</a
+ >
+ <a
+ href="https://crates.io/crates/mingling-cli"
+ target="_blank"
+ class="btn-nav"
+ >cargo install mingling-cli</a
+ >
+ </div>
+ </nav>
+
+ <section class="hero">
+ <h1>Mingling CLI</h1>
+ <p class="subtitle">
+ The command-line companion for scaffolding, managing, and
+ updating Mingling projects. Install it once, then let
+ <code>mling update</code> keep it fresh.
+ </p>
+ </section>
+
+ <section class="section alt">
+ <div class="container">
+ <h2>How to use</h2>
+ <p>Some commands you will use every day:</p>
+ <div class="cmd-grid">
+ <div class="cmd-card">
+ <code>mling proj-init 0.4@basic</code>
+ <p>Initialize a project</p>
+ </div>
+ <div class="cmd-card">
+ <code>mling install</code>
+ <p>Install the project to the Mingling package list</p>
+ </div>
+ <div class="cmd-card">
+ <code>mling pkg-enable your-cli@0.1.0</code>
+ <p>Enable the specified package</p>
+ </div>
+ <div class="cmd-card">
+ <code>mling lint</code>
+ <p>Mingling Linter</p>
+ </div>
+ <div class="cmd-card">
+ <code>mling -h</code>
+ <p>Show help messages</p>
+ </div>
+ <div class="cmd-card">
+ <code>mling update</code>
+ <p>Update mling to the latest version</p>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <h2>Download</h2>
+ <p>
+ Choose the package for your platform and download the latest
+ build.
+ </p>
+ <div class="download-row">
+ <div class="download-card">
+ <svg
+ class="platform invert"
+ viewBox="0 0 128 128"
+ xmlns="http://www.w3.org/2000/svg"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="M113.823 104.595c-1.795-1.478-3.629-2.921-5.308-4.525-1.87-1.785-3.045-3.944-2.789-6.678.147-1.573-.216-2.926-2.113-3.452.446-1.154.864-1.928 1.033-2.753.188-.92.178-1.887.204-2.834.264-9.96-3.334-18.691-8.663-26.835-2.454-3.748-5.017-7.429-7.633-11.066-4.092-5.688-5.559-12.078-5.633-18.981a47.564 47.564 0 00-1.081-9.475C80.527 11.956 77.291 7.233 71.422 4.7c-4.497-1.942-9.152-2.327-13.901-1.084-6.901 1.805-11.074 6.934-10.996 14.088.074 6.885.417 13.779.922 20.648.288 3.893-.312 7.252-2.895 10.34-2.484 2.969-4.706 6.172-6.858 9.397-1.229 1.844-2.317 3.853-3.077 5.931-2.07 5.663-3.973 11.373-7.276 16.5-1.224 1.9-1.363 4.026-.494 6.199.225.563.363 1.429.089 1.882-2.354 3.907-5.011 7.345-10.066 8.095-3.976.591-4.172 1.314-4.051 5.413.1 3.337.061 6.705-.28 10.021-.363 3.555.008 4.521 3.442 5.373 7.924 1.968 15.913 3.647 23.492 6.854 3.227 1.365 6.465.891 9.064-1.763 2.713-2.771 6.141-3.855 9.844-3.859 6.285-.005 12.572.298 18.86.369 1.702.02 2.679.653 3.364 2.199.84 1.893 2.26 3.284 4.445 3.526 4.193.462 8.013-.16 11.19-3.359 3.918-3.948 8.436-7.066 13.615-9.227 1.482-.619 2.878-1.592 4.103-2.648 2.231-1.922 2.113-3.146-.135-5zM62.426 24.12c.758-2.601 2.537-4.289 5.243-4.801 2.276-.43 4.203.688 5.639 3.246 1.546 2.758 2.054 5.64.734 8.658-1.083 2.474-1.591 2.707-4.123 1.868-.474-.157-.937-.343-1.777-.652.708-.594 1.154-1.035 1.664-1.382 1.134-.772 1.452-1.858 1.346-3.148-.139-1.694-1.471-3.194-2.837-3.175-1.225.017-2.262 1.167-2.4 2.915-.086 1.089.095 2.199.173 3.589-3.446-1.023-4.711-3.525-3.662-7.118zm-12.75-2.251c1.274-1.928 3.197-2.314 5.101-1.024 2.029 1.376 3.547 5.256 2.763 7.576-.285.844-1.127 1.5-1.716 2.241l-.604-.374c-.23-1.253-.276-2.585-.757-3.733-.304-.728-1.257-1.184-1.919-1.762-.622.739-1.693 1.443-1.757 2.228-.088 1.084.477 2.28.969 3.331.311.661 1.001 1.145 1.713 1.916l-1.922 1.51c-3.018-2.7-3.915-8.82-1.871-11.909zM87.34 86.075c-.203 2.604-.5 2.713-3.118 3.098-1.859.272-2.359.756-2.453 2.964a101.744 101.744 0 00-.012 7.753c.061 1.77-.537 3.158-1.755 4.393-6.764 6.856-14.845 10.105-24.512 8.926-4.17-.509-6.896-3.047-9.097-6.639.98-.363 1.705-.607 2.412-.894 3.122-1.27 3.706-3.955 1.213-6.277-1.884-1.757-3.986-3.283-6.007-4.892-1.954-1.555-3.934-3.078-5.891-4.629-1.668-1.323-2.305-3.028-2.345-5.188-.094-5.182.972-10.03 3.138-14.747 1.932-4.209 3.429-8.617 5.239-12.885.935-2.202 1.906-4.455 3.278-6.388 1.319-1.854 2.134-3.669 1.988-5.94-.084-1.276-.016-2.562-.016-3.843l.707-.352c1.141.985 2.302 1.949 3.423 2.959 4.045 3.646 7.892 3.813 12.319.67 1.888-1.341 3.93-2.47 5.927-3.652.497-.294 1.092-.423 1.934-.738 2.151 5.066 4.262 10.033 6.375 15 1.072 2.524 1.932 5.167 3.264 7.547 2.671 4.775 4.092 9.813 4.07 15.272-.012 2.83.137 5.67-.081 8.482z"
+ />
+ </svg>
+ <span class="os-name">Linux</span>
+ <a class="file-name" href="./mling-linux.tar.gz"
+ >mling-linux.tar.gz</a
+ >
+ <span class="checksum"
+ ><a href="./mling-linux.tar.gz.sha256"
+ >SHA256</a
+ ></span
+ >
+ </div>
+ <div class="download-card">
+ <svg
+ class="platform invert"
+ viewBox="0 0 128 128"
+ xmlns="http://www.w3.org/2000/svg"
+ aria-hidden="true"
+ >
+ <path
+ d="M126 1.637l-67 9.834v49.831l67-.534zM1.647 66.709l.003 42.404 50.791 6.983-.04-49.057zm56.82.68l.094 49.465 67.376 9.509.016-58.863zM1.61 19.297l.047 42.383 50.791-.289-.023-49.016z"
+ />
+ </svg>
+ <span class="os-name">Windows</span>
+ <a class="file-name" href="./mling-win.tar.gz"
+ >mling-win.tar.gz</a
+ >
+ <span class="checksum"
+ ><a href="./mling-win.tar.gz.sha256"
+ >SHA256</a
+ ></span
+ >
+ </div>
+ <div class="download-card">
+ <svg
+ class="platform invert"
+ viewBox="0 0 128 128"
+ xmlns="http://www.w3.org/2000/svg"
+ aria-hidden="true"
+ >
+ <path
+ d="M97.905 67.885c.174 18.8 16.494 25.057 16.674 25.137-.138.44-2.607 8.916-8.597 17.669-5.178 7.568-10.553 15.108-19.018 15.266-8.318.152-10.993-4.934-20.504-4.934-9.508 0-12.479 4.776-20.354 5.086-8.172.31-14.395-8.185-19.616-15.724C15.822 94.961 7.669 66.8 18.616 47.791c5.438-9.44 15.158-15.417 25.707-15.571 8.024-.153 15.598 5.398 20.503 5.398 4.902 0 14.106-6.676 23.782-5.696 4.051.169 15.421 1.636 22.722 12.324-.587.365-13.566 7.921-13.425 23.639M82.272 21.719c4.338-5.251 7.258-12.563 6.462-19.836-6.254.251-13.816 4.167-18.301 9.416-4.02 4.647-7.54 12.087-6.591 19.216 6.971.54 14.091-3.542 18.43-8.796"
+ />
+ </svg>
+ <span class="os-name">macOS</span>
+ <a class="file-name" href="./mling-mac.tar.gz"
+ >mling-mac.tar.gz</a
+ >
+ <span class="checksum"
+ ><a href="./mling-mac.tar.gz.sha256"
+ >SHA256</a
+ ></span
+ >
+ </div>
+ </div>
+
+ <p class="checksum-note">
+ Packages are built on every push to
+ <a
+ href="https://github.com/mingling-rs/mingling"
+ target="_blank"
+ >main</a
+ >
+ and published here automatically.
+ </p>
+ </div>
+ </section>
+
+ <section class="section alt">
+ <div class="container">
+ <h2>Install</h2>
+ <p>
+ Extract the package and load the bundled environment script
+ — it adds <code>bin/</code> to <code>PATH</code> and enables
+ shell completion.
+ </p>
+ <div class="code-block">
+ <pre><code><span class="cm"># Linux / macOS (zsh, bash, fish)</span>
+tar -xzf mling-linux.tar.gz
+source load_mling.sh
+
+<span class="cm"># Windows (PowerShell)</span>
+tar -xzf mling-win.tar.gz
+.\load_mling.ps1</code></pre>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <h2>Update</h2>
+ <p>
+ <code>mling</code> can update itself. It downloads from this
+ page by default, verifies the sha256 checksum, and applies
+ the new version on your next invocation.
+ </p>
+ <div class="code-block">
+ <pre><code><span class="cm"># Update to the latest build</span>
+mling update
+
+<span class="cm"># Point it at a custom mirror if needed</span>
+mling cfg update-url https://mingling-rs.github.io/mingling/dist
+mling update</code></pre>
+ </div>
+ </div>
+ </section>
+
+ <footer>
+ <a href="../">Mìng Lìng</a> ·
+ <a href="https://github.com/mingling-rs/mingling" target="_blank"
+ >GitHub</a
+ >
+ ·
+ <a href="../docs/doc.html">Docs</a>
+ </footer>
+ </body>
+</html>
diff --git a/mingling_cli/Cargo.lock b/mingling_cli/Cargo.lock
index 40724d2..fd3fb1d 100644
--- a/mingling_cli/Cargo.lock
+++ b/mingling_cli/Cargo.lock
@@ -26,15 +26,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
-name = "arbitrary"
-version = "1.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
-dependencies = [
- "derive_arbitrary",
-]
-
-[[package]]
name = "arg-picker"
version = "0.2.0"
dependencies = [
@@ -223,12 +214,6 @@ dependencies = [
]
[[package]]
-name = "crossbeam-utils"
-version = "0.8.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
-
-[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -274,17 +259,6 @@ dependencies = [
]
[[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.119",
-]
-
-[[package]]
name = "derive_builder"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -868,7 +842,6 @@ dependencies = [
"tar",
"tokio",
"toml_edit 0.25.13+spec-1.1.0",
- "zip",
]
[[package]]
@@ -2029,36 +2002,7 @@ dependencies = [
]
[[package]]
-name = "zip"
-version = "2.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
-dependencies = [
- "arbitrary",
- "crc32fast",
- "crossbeam-utils",
- "displaydoc",
- "flate2",
- "indexmap",
- "memchr",
- "thiserror",
- "zopfli",
-]
-
-[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
-
-[[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",
-]
diff --git a/mingling_cli/Cargo.toml b/mingling_cli/Cargo.toml
index 4388fea..009b334 100644
--- a/mingling_cli/Cargo.toml
+++ b/mingling_cli/Cargo.toml
@@ -69,8 +69,7 @@ just_template = "0.2.1"
sha2 = "0.10"
# Self-update
-reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json", "gzip"] }
-zip = { version = "2", default-features = false, features = ["deflate"] }
+reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "gzip"] }
tar = "0.4"
flate2 = "1"
diff --git a/mingling_cli/src/bin/wrapper.rs b/mingling_cli/src/bin/wrapper.rs
index 90bbc53..a47b926 100644
--- a/mingling_cli/src/bin/wrapper.rs
+++ b/mingling_cli/src/bin/wrapper.rs
@@ -1,10 +1,12 @@
use std::env;
use std::ffi::OsString;
use std::fs;
+use std::io;
use std::path::{Component, Path, PathBuf};
use std::process::{self, Command};
use flate2::read::GzDecoder;
+use sha2::{Digest, Sha256};
use tar::Archive;
fn main() {
@@ -83,6 +85,13 @@ fn apply_update_if_present() {
match unpack_update(&update_path, &current_exe, install_root) {
Ok(()) => {
+ // Record the applied package's checksum so `mling update` can tell
+ // that this installation is already up to date.
+ if let Some(checksum_path) = last_update_checksum_path()
+ && let Ok(checksum) = sha256_file(&update_path)
+ {
+ let _ = fs::write(checksum_path, checksum);
+ }
let _ = fs::remove_file(update_path);
}
Err(e) => eprintln!("mling: failed to apply update: {e}"),
@@ -135,3 +144,17 @@ fn sanitize_relative_path(path: &Path) -> PathBuf {
}
out
}
+
+/// `{data_dir}/mingling/last-update.sha256`, where the wrapper records the
+/// checksum of the update it applied.
+fn last_update_checksum_path() -> Option<PathBuf> {
+ dirs::data_dir().map(|data_dir| data_dir.join("mingling").join("last-update.sha256"))
+}
+
+/// The sha256 hex digest of a file.
+fn sha256_file(path: &Path) -> io::Result<String> {
+ let mut file = fs::File::open(path)?;
+ let mut hasher = Sha256::new();
+ io::copy(&mut file, &mut hasher)?;
+ Ok(format!("{:x}", hasher.finalize()))
+}
diff --git a/mingling_cli/src/updater/cmd_update.rs b/mingling_cli/src/updater/cmd_update.rs
index b75f4e9..545361f 100644
--- a/mingling_cli/src/updater/cmd_update.rs
+++ b/mingling_cli/src/updater/cmd_update.rs
@@ -1,34 +1,40 @@
-use std::{fs, io, path::Path, path::PathBuf};
+use std::{fs, path::Path, path::PathBuf};
use mingling::{
Grouped, LazyRes, RenderResult, Routable,
macros::{chain, command, metadata, renderer, routeify},
metadata::Description,
};
+use sha2::{Digest, Sha256};
use crate::{Next, config::ResMlingConfig, eprintln_cargo, println_cargo};
-/// Config key holding the GitHub repository that publishes the mling packages.
-const CONFIG_KEY_GITHUB: &str = "mingling-github";
+/// Config key holding the base URL that hosts the mling release packages.
+const CONFIG_KEY_UPDATE_URL: &str = "update-url";
/// Default update source, used when the config key is unset.
-const DEFAULT_GITHUB: &str = "https://github.com/mingling-rs/mingling";
+const DEFAULT_UPDATE_URL: &str = "https://mingling-rs.github.io/mingling/dist";
/// Name of the staged update package inside `{data_dir}/mingling`.
const UPDATE_FILE_NAME: &str = "update.tar.gz";
-/// The resolved download task: fetch the newest artifact and stage it.
+/// Records the sha256 of the last applied update, written by the wrapper.
+const LAST_UPDATE_FILE_NAME: &str = "last-update.sha256";
+
+/// The resolved download task: check the remote checksum and stage the package.
#[derive(Debug, Default, Grouped)]
pub struct StateUpdateDownload {
- pub owner: String,
- pub repo: String,
+ pub base_url: String,
pub update_path: PathBuf,
}
-/// The staged update package, ready to be applied by the `mling` wrapper.
+/// The latest package is already installed.
+#[derive(Debug, Default, Grouped)]
+pub struct ResultUpdateUpToDate;
+
+/// The latest package was downloaded, verified, and staged for the wrapper.
#[derive(Debug, Default, Grouped)]
-pub struct ResultUpdate {
- pub artifact_name: String,
+pub struct ResultUpdateStaged {
pub update_path: PathBuf,
}
@@ -37,22 +43,16 @@ pub struct ResultUpdate {
pub enum UpdateError {
/// The data directory could not be determined.
NoDataDirectory,
- /// The configured update source is not a GitHub repository URL.
- InvalidRepo(String),
- /// A network or API request failed.
+ /// The configured update URL is not a valid `http(s)://` URL.
+ InvalidUrl(String),
+ /// A network request failed, or the remote responded with an error.
Network(String),
- /// No matching artifact was found.
- NoArtifact(String),
+ /// The downloaded package failed its sha256 verification.
+ ChecksumMismatch(String),
/// Writing the staged update package failed.
Io(String),
}
-/// A downloaded artifact with its inner `*.tar.gz` extracted.
-struct Artifact {
- name: String,
- tar_gz: Vec<u8>,
-}
-
#[metadata(EntryUpdate)]
pub fn desc_update() -> Description {
"Update mling to the latest version".into()
@@ -61,45 +61,46 @@ pub fn desc_update() -> Description {
#[command(routeify)]
pub fn update(config: &mut LazyRes<ResMlingConfig>) -> Next {
let config = config.get_ref();
- let source = config.get_or(CONFIG_KEY_GITHUB, DEFAULT_GITHUB);
+ let source = config.get_or(CONFIG_KEY_UPDATE_URL, DEFAULT_UPDATE_URL);
let Some(update_path) = update_package_path() else {
return UpdateError::NoDataDirectory.to_chain();
};
- match parse_github_repo(source) {
- Some((owner, repo)) => StateUpdateDownload {
- owner,
- repo,
- update_path,
- }
- .to_chain(),
- None => UpdateError::InvalidRepo(source.to_string()).to_chain(),
+ if !is_http_url(source) {
+ return UpdateError::InvalidUrl(source.to_string()).to_chain();
+ }
+ StateUpdateDownload {
+ base_url: source.to_string(),
+ update_path,
}
+ .to_chain()
}
-/// Fetch the newest artifact for the current platform and stage it at
+/// Check the remote checksum against the installed version; if they differ,
+/// download the package, verify its checksum, and stage it at
/// `{data_dir}/mingling/update.tar.gz`.
#[chain(routeify)]
pub async fn handle_state_update_download(state: StateUpdateDownload) -> Next {
- match fetch_latest_artifact(&state.owner, &state.repo).await {
- Ok(artifact) => {
- if let Err(e) = write_update_package(&artifact.tar_gz, &state.update_path) {
- return UpdateError::Io(e).to_chain();
- }
- ResultUpdate {
- artifact_name: artifact.name,
- update_path: state.update_path,
- }
- .to_chain()
+ match check_and_fetch(&state.base_url, &state.update_path).await {
+ Ok(FetchOutcome::UpToDate) => ResultUpdateUpToDate.to_chain(),
+ Ok(FetchOutcome::Staged) => ResultUpdateStaged {
+ update_path: state.update_path,
}
+ .to_chain(),
Err(e) => e.to_chain(),
}
}
#[renderer]
-pub fn render_result_update(r: ResultUpdate) -> RenderResult {
+pub fn render_result_update_up_to_date(_: ResultUpdateUpToDate) -> RenderResult {
+ let mut result = RenderResult::new();
+ println_cargo!(result, "mling is already up to date");
+ result
+}
+
+#[renderer]
+pub fn render_result_update_staged(r: ResultUpdateStaged) -> RenderResult {
let mut result = RenderResult::new();
- println_cargo!(result, "Downloaded: {}", r.artifact_name);
- println_cargo!(result, "Staged: {}", r.update_path.display());
+ println_cargo!(result, "Downloaded: {}", r.update_path.display());
println_cargo!(result, "Run `mling` again to apply the update");
result
}
@@ -111,14 +112,14 @@ pub fn render_error_update(err: UpdateError) -> RenderResult {
UpdateError::NoDataDirectory => {
eprintln_cargo!(result, "failed to determine the data directory");
}
- UpdateError::InvalidRepo(source) => {
+ UpdateError::InvalidUrl(source) => {
eprintln_cargo!(
result,
- "invalid update source `{}`, expected a GitHub repository URL like `https://github.com/mingling-rs/mingling`",
+ "invalid update URL `{}`, expected an `http(s)://` URL such as `https://mingling-rs.github.io/mingling/dist`",
source
);
}
- UpdateError::Network(msg) | UpdateError::NoArtifact(msg) | UpdateError::Io(msg) => {
+ UpdateError::Network(msg) | UpdateError::ChecksumMismatch(msg) | UpdateError::Io(msg) => {
eprintln_cargo!(result, "{}", msg);
}
}
@@ -130,27 +131,17 @@ pub fn update_package_path() -> Option<PathBuf> {
dirs::data_dir().map(|dir| dir.join("mingling").join(UPDATE_FILE_NAME))
}
-/// Extract `owner` / `repo` from a GitHub URL such as
-/// `https://github.com/mingling-rs/mingling`. Trailing slashes and `.git`
-/// suffixes are tolerated, and a bare `owner/repo` is accepted as well.
-fn parse_github_repo(source: &str) -> Option<(String, String)> {
- let trimmed = source.trim().trim_end_matches('/');
- if trimmed.is_empty() {
- return None;
- }
- let path = match trimmed.rfind("://") {
- Some(idx) => &trimmed[idx + 3..],
- None => trimmed,
- };
- let mut segments = path.split('/').filter(|s| !s.is_empty());
- // The first segment is the host; take the two path segments after it.
- segments.next()?;
- let owner = segments.next()?;
- let repo = segments.next()?.trim_end_matches(".git");
- Some((owner.to_string(), repo.to_string()))
+/// `{data_dir}/mingling/last-update.sha256`, the checksum of the last applied update.
+fn last_update_checksum_path() -> Option<PathBuf> {
+ dirs::data_dir().map(|dir| dir.join("mingling").join(LAST_UPDATE_FILE_NAME))
}
-/// The platform suffix used by the CI artifact names (`mling-{os}-...`).
+fn is_http_url(source: &str) -> bool {
+ let source = source.trim();
+ source.starts_with("http://") || source.starts_with("https://")
+}
+
+/// The platform suffix used by the package names (`mling-{os}.tar.gz`).
fn update_os_name() -> &'static str {
if cfg!(windows) {
"win"
@@ -163,127 +154,113 @@ fn update_os_name() -> &'static str {
}
}
-/// Query the GitHub Actions API, pick the newest non-expired artifact for the
-/// current platform, download it, and extract the inner `*.tar.gz`.
-async fn fetch_latest_artifact(owner: &str, repo: &str) -> Result<Artifact, UpdateError> {
+enum FetchOutcome {
+ UpToDate,
+ Staged,
+}
+
+/// Fetch `mling-{os}.tar.gz.sha256`, skip the download when the installed
+/// version already matches, then download and verify the package before
+/// staging it.
+async fn check_and_fetch(base_url: &str, update_path: &Path) -> Result<FetchOutcome, UpdateError> {
+ let os = update_os_name();
+ let base = base_url.trim_end_matches('/');
+ let checksum_url = format!("{base}/mling-{os}.tar.gz.sha256");
+ let package_url = format!("{base}/mling-{os}.tar.gz");
+
let client = reqwest::Client::builder()
.user_agent(format!("mling-updater/{}", env!("CARGO_PKG_VERSION")))
.build()
.map_err(|e| UpdateError::Network(format!("failed to build HTTP client: {e}")))?;
- let list_url =
- format!("https://api.github.com/repos/{owner}/{repo}/actions/artifacts?per_page=100");
- let mut request = client
- .get(&list_url)
- .header("Accept", "application/vnd.github+json");
- if let Ok(token) = std::env::var("GITHUB_TOKEN")
- && !token.is_empty()
- {
- request = request.header("Authorization", format!("Bearer {token}"));
- }
-
- let response = request.send().await.map_err(|e| {
- UpdateError::Network(format!("failed to query GitHub Actions artifacts: {e}"))
+ // 1. Fetch the remote checksum first.
+ let response = client.get(&checksum_url).send().await.map_err(|e| {
+ UpdateError::Network(format!(
+ "failed to fetch checksum from `{checksum_url}`: {e}"
+ ))
})?;
if !response.status().is_success() {
return Err(UpdateError::Network(format!(
- "GitHub Actions API returned {} for `{list_url}`",
+ "failed to fetch checksum from `{checksum_url}`: HTTP {}",
response.status()
)));
}
- let json: serde_json::Value = response
- .json()
+ let checksum_text = response
+ .text()
.await
- .map_err(|e| UpdateError::Network(format!("failed to parse GitHub response: {e}")))?;
-
- let os = update_os_name();
- let prefix = format!("mling-{os}-");
- let artifact = json
- .get("artifacts")
- .and_then(serde_json::Value::as_array)
- .into_iter()
- .flatten()
- .filter(|a| {
- a.get("expired").and_then(serde_json::Value::as_bool) != Some(true)
- && a.get("name")
- .and_then(serde_json::Value::as_str)
- .is_some_and(|name| name.starts_with(&prefix))
- })
- .max_by_key(|a| {
- a.get("created_at")
- .and_then(serde_json::Value::as_str)
- .unwrap_or("")
- })
- .ok_or_else(|| {
- UpdateError::NoArtifact(format!("no `{prefix}*` artifact found in {owner}/{repo}"))
- })?;
+ .map_err(|e| UpdateError::Network(format!("failed to read checksum: {e}")))?;
+ let remote_sha = parse_sha256(&checksum_text).ok_or_else(|| {
+ UpdateError::Network(format!("invalid checksum file at `{checksum_url}`"))
+ })?;
- let name = artifact
- .get("name")
- .and_then(serde_json::Value::as_str)
- .unwrap_or("mling")
- .to_string();
- let download_url = artifact
- .get("archive_download_url")
- .and_then(serde_json::Value::as_str)
- .unwrap_or(&format!(
- "https://api.github.com/repos/{owner}/{repo}/actions/artifacts/{}/zip",
- artifact
- .get("id")
- .and_then(serde_json::Value::as_u64)
- .unwrap_or(0)
- ))
- .to_string();
+ // 2. Skip the download when the installed version already matches.
+ if let Some(local_sha) = read_last_update_checksum()
+ && local_sha == remote_sha
+ {
+ return Ok(FetchOutcome::UpToDate);
+ }
+ // 3. Download the package.
let response =
- client.get(&download_url).send().await.map_err(|e| {
- UpdateError::Network(format!("failed to download artifact `{name}`: {e}"))
+ client.get(&package_url).send().await.map_err(|e| {
+ UpdateError::Network(format!("failed to download `{package_url}`: {e}"))
})?;
if !response.status().is_success() {
return Err(UpdateError::Network(format!(
- "failed to download artifact `{name}`: HTTP {}",
+ "failed to download `{package_url}`: HTTP {}",
response.status()
)));
}
- let zip_bytes = response
+ let bytes = response
.bytes()
.await
- .map_err(|e| UpdateError::Network(format!("failed to read artifact `{name}`: {e}")))?;
+ .map_err(|e| UpdateError::Network(format!("failed to read package body: {e}")))?;
- let tar_gz = extract_tar_gz_from_zip(&zip_bytes)
- .map_err(|e| UpdateError::Network(format!("invalid artifact `{name}`: {e}")))?;
- Ok(Artifact { name, tar_gz })
+ // 4. Verify the package before staging it.
+ let actual_sha = sha256_hex(&bytes);
+ if actual_sha != remote_sha {
+ return Err(UpdateError::ChecksumMismatch(format!(
+ "checksum mismatch for `{package_url}`: expected {remote_sha}, got {actual_sha}"
+ )));
+ }
+
+ // 5. Stage it for the wrapper.
+ write_update_package(&bytes, update_path)?;
+ Ok(FetchOutcome::Staged)
}
-/// The GitHub artifact is a zip containing the `mling-{os}-{sha}-{date}.tar.gz`
-/// built by CI; extract that inner file.
-fn extract_tar_gz_from_zip(zip_bytes: &[u8]) -> Result<Vec<u8>, String> {
- let reader = io::Cursor::new(zip_bytes);
- let mut archive = zip::ZipArchive::new(reader).map_err(|e| e.to_string())?;
- for index in 0..archive.len() {
- let mut file = archive.by_index(index).map_err(|e| e.to_string())?;
- let file_name = file.name().to_string();
- if file_name.ends_with(".tar.gz") {
- let mut tar_gz = Vec::with_capacity(file.size() as usize);
- io::copy(&mut file, &mut tar_gz).map_err(|e| e.to_string())?;
- return Ok(tar_gz);
- }
- }
- Err("artifact contains no `*.tar.gz` file".to_string())
+/// Parse the sha256 hex digest from a `sha256sum`-style line (`<hash> <file>`).
+fn parse_sha256(line: &str) -> Option<String> {
+ let hash = line.split_whitespace().next()?;
+ (hash.len() == 64 && hash.chars().all(|c| c.is_ascii_hexdigit())).then(|| hash.to_string())
+}
+
+/// The checksum of the last update applied by the wrapper, if recorded.
+fn read_last_update_checksum() -> Option<String> {
+ let path = last_update_checksum_path()?;
+ let content = fs::read_to_string(path).ok()?;
+ let sha = content.trim();
+ (!sha.is_empty()).then(|| sha.to_string())
+}
+
+fn sha256_hex(bytes: &[u8]) -> String {
+ let mut hasher = Sha256::new();
+ hasher.update(bytes);
+ format!("{:x}", hasher.finalize())
}
/// Stage the update package at the wrapper's well-known location. The bytes are
/// written to a temporary file first so a failed download never corrupts a
/// previously staged update.
-fn write_update_package(tar_gz: &[u8], update_path: &Path) -> Result<(), String> {
- let parent = update_path
- .parent()
- .ok_or_else(|| format!("no parent directory for {}", update_path.display()))?;
- fs::create_dir_all(parent).map_err(|e| e.to_string())?;
+fn write_update_package(package: &[u8], update_path: &Path) -> Result<(), UpdateError> {
+ let parent = update_path.parent().ok_or_else(|| {
+ UpdateError::Io(format!("no parent directory for {}", update_path.display()))
+ })?;
+ fs::create_dir_all(parent).map_err(|e| UpdateError::Io(e.to_string()))?;
let tmp_path = parent.join("update.tar.gz.tmp");
- fs::write(&tmp_path, tar_gz).map_err(|e| e.to_string())?;
+ fs::write(&tmp_path, package).map_err(|e| UpdateError::Io(e.to_string()))?;
if update_path.exists() {
- fs::remove_file(update_path).map_err(|e| e.to_string())?;
+ fs::remove_file(update_path).map_err(|e| UpdateError::Io(e.to_string()))?;
}
- fs::rename(&tmp_path, update_path).map_err(|e| e.to_string())
+ fs::rename(&tmp_path, update_path).map_err(|e| UpdateError::Io(e.to_string()))
}