aboutsummaryrefslogtreecommitdiff
path: root/mingling_cli/src/bin/wrapper.rs
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-08-16 01:59:20 +0800
committer魏曹先生 <1992414357@qq.com>2026-08-16 02:17:35 +0800
commitd85834037e0ad73fb3ce62a2a1ff8666865d4357 (patch)
treebbf167e2b02799110dd202710e1c8cd1de08c819 /mingling_cli/src/bin/wrapper.rs
parent2a5704fb9e438a9844469bf5c85b525d037f6663 (diff)
feat: add self-update command for mling
Add `mling update` to fetch the latest GitHub Actions artifact for the current platform, stage it as a tarball in the data directory, and apply it on the next wrapper invocation. The wrapper extracts the staged package over the installation directory while skipping the running executable, then removes the staged file.
Diffstat (limited to 'mingling_cli/src/bin/wrapper.rs')
-rw-r--r--mingling_cli/src/bin/wrapper.rs92
1 files changed, 91 insertions, 1 deletions
diff --git a/mingling_cli/src/bin/wrapper.rs b/mingling_cli/src/bin/wrapper.rs
index b1aecf6..90bbc53 100644
--- a/mingling_cli/src/bin/wrapper.rs
+++ b/mingling_cli/src/bin/wrapper.rs
@@ -1,9 +1,14 @@
use std::env;
use std::ffi::OsString;
-use std::path::PathBuf;
+use std::fs;
+use std::path::{Component, Path, PathBuf};
use std::process::{self, Command};
+use flate2::read::GzDecoder;
+use tar::Archive;
+
fn main() {
+ apply_update_if_present();
exec();
}
@@ -45,3 +50,88 @@ fn exec() {
}
}
}
+
+/// The update package staged by `mling update` at `{data_dir}/mingling/update.tar.gz`.
+fn update_package_path() -> Option<PathBuf> {
+ dirs::data_dir().map(|data_dir| data_dir.join("mingling").join("update.tar.gz"))
+}
+
+/// Apply a staged update, if any: unpack it over the installation directory
+/// (never replacing the running wrapper itself), then remove the staged file.
+/// This runs before forwarding to `mingling-cli`, so the new version is loaded
+/// by this very invocation.
+fn apply_update_if_present() {
+ let Some(update_path) = update_package_path() else {
+ return;
+ };
+ if !update_path.is_file() {
+ return;
+ }
+ let Ok(current_exe) = env::current_exe() else {
+ return;
+ };
+
+ // The package mirrors the install layout: the wrapper lives at
+ // `<root>/bin/mling` and the archive root maps onto `<root>`, so entries
+ // like `bin/mingling-cli` replace the files next to this wrapper.
+ let Some(exe_dir) = current_exe.parent() else {
+ return;
+ };
+ let Some(install_root) = exe_dir.parent() else {
+ return;
+ };
+
+ match unpack_update(&update_path, &current_exe, install_root) {
+ Ok(()) => {
+ let _ = fs::remove_file(update_path);
+ }
+ Err(e) => eprintln!("mling: failed to apply update: {e}"),
+ }
+}
+
+/// Extract `update.tar.gz` into `install_root`, skipping the running wrapper.
+fn unpack_update(
+ update_path: &Path,
+ current_exe: &Path,
+ install_root: &Path,
+) -> std::io::Result<()> {
+ let file = fs::File::open(update_path)?;
+ let mut archive = Archive::new(GzDecoder::new(file));
+
+ for entry in archive.entries()? {
+ let mut entry = entry?;
+ let entry_type = entry.header().entry_type();
+ let rel_path = sanitize_relative_path(&entry.path()?);
+ if rel_path.as_os_str().is_empty() {
+ continue;
+ }
+ let dest = install_root.join(rel_path);
+ // The running executable cannot (and must not) replace itself.
+ if dest == current_exe {
+ continue;
+ }
+ if entry_type.is_dir() {
+ fs::create_dir_all(dest)?;
+ continue;
+ }
+ if !entry_type.is_file() {
+ continue;
+ }
+ if let Some(parent) = dest.parent() {
+ fs::create_dir_all(parent)?;
+ }
+ entry.unpack(dest)?;
+ }
+ Ok(())
+}
+
+/// Keep only normal path components so entries cannot escape `install_root`.
+fn sanitize_relative_path(path: &Path) -> PathBuf {
+ let mut out = PathBuf::new();
+ for component in path.components() {
+ if let Component::Normal(part) = component {
+ out.push(part);
+ }
+ }
+ out
+}