Skip to content

Commit

Permalink
fix: use self-replace to update maa-cli self (#363)
Browse files Browse the repository at this point in the history
On windows, the maa-cli binary is locked by the system when it is
running. This makes it impossible to update the binary directly. To
solve this problem, we can use the self-replace crate to update the
binary.

Besides, constcat crate is added to create some platform-specific file
names. The extract function is slightly modified to accept a closure
that accepts a Cow<Path> instead of a &Path.
  • Loading branch information
wangl-cc authored Jan 1, 2025
1 parent e4e1cbc commit d57c81b
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 83 deletions.
40 changes: 40 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ clap = "4.4"
clap_complete = "4.4"
clap_mangen = "0.2.20"
color-print = "0.3.6"
constcat = "0.5.1"
digest = "0.10.7"
directories = "5"
dunce = "1.0.4"
Expand All @@ -31,6 +32,7 @@ libloading = "0.8"
log = "0.4.20"
prettytable = { version = "0.10.0", default-features = false }
regex = "1.10.2"
self-replace = "1.5.0"
semver = "1.0.19"
serde = "1"
serde_json = "1"
Expand All @@ -39,6 +41,7 @@ serde_yaml = "0.9.25"
sha2 = "0.10.7"
signal-hook = "0.3.17"
tar = "0.4.40"
tempfile = "3.14.0"
thiserror = "2"
tokio = "1.31"
toml = "0.8"
Expand Down
3 changes: 3 additions & 0 deletions crates/maa-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ clap = { workspace = true, features = ["derive"] }
clap_complete = { workspace = true }
clap_mangen = { workspace = true }
color-print = { workspace = true }
constcat = { workspace = true }
digest = { workspace = true, optional = true }
dunce = { workspace = true }
env_logger = { workspace = true, features = ["auto-color"] }
Expand All @@ -69,6 +70,8 @@ tar = { workspace = true, optional = true }
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
toml = { workspace = true }
zip = { workspace = true, optional = true, features = ["deflate"] }
self-replace = { workspace = true }
tempfile = { workspace = true }

# Windows specific dependencies
[target.'cfg(windows)'.dependencies]
Expand Down
66 changes: 37 additions & 29 deletions crates/maa-cli/src/installer/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ impl<'f> Archive<'f> {
/// If the output path exists, the file will be skipped if the file size matches.
/// Otherwise, the file will be overwritten.
/// The file permissions will be preserved.
pub fn extract(&self, mapper: impl Fn(&Path) -> Option<PathBuf>) -> Result<()> {
pub fn extract<F>(&self, mapper: F) -> Result<()>
where
F: FnMut(Cow<Path>) -> Option<PathBuf>,
{
println!("Extracting archive file...");
match self.archive_type {
ArchiveType::Zip => extract_zip(&self.file, mapper),
Expand All @@ -78,24 +81,29 @@ impl<'f> Archive<'f> {
}
}

fn extract_zip(file: &Path, mapper: impl Fn(&Path) -> Option<PathBuf>) -> Result<()> {
fn extract_zip<F>(file: &Path, mut mapper: F) -> Result<()>
where
F: FnMut(Cow<Path>) -> Option<PathBuf>,
{
let mut archive = zip::ZipArchive::new(File::open(file)?)?;

for i in 0..archive.len() {
let mut file = archive.by_index(i).unwrap();

let outpath = match file.enclosed_name() {
Some(path) => match mapper(&path) {
Some(path) => path,
None => continue,
},
let src_path = file
.enclosed_name()
.context("Bad file path in zip archive")?
.into();
let dst = match mapper(src_path) {
Some(path) => path,
None => continue,
};
let dst = dst.as_path();

if file.is_dir() {
continue;
} else {
if let Some(p) = outpath.parent() {
if let Some(p) = dst.parent() {
p.ensure()?;
}

Expand All @@ -114,23 +122,23 @@ fn extract_zip(file: &Path, mapper: impl Fn(&Path) -> Option<PathBuf>) -> Result
let mut contents = Vec::new();
file.read_to_end(&mut contents)?;
let link_target = std::ffi::OsString::from_vec(contents);
if outpath.exists() {
remove_file(&outpath).with_context(|| {
format!("Failed to remove existing file: {}", outpath.display())
if dst.exists() {
remove_file(dst).with_context(|| {
format!("Failed to remove existing file: {}", dst.display())
})?;
}
symlink(link_target, &outpath).with_context(|| {
format!("Failed to extract file: {}", outpath.display())
symlink(link_target, dst).with_context(|| {
format!("Failed to extract file: {}", dst.display())
})?;
continue;
}
}
}

let mut outfile = File::create(&outpath)
.with_context(|| format!("Failed to create file: {}", outpath.display()))?;
let mut outfile = File::create(dst)
.with_context(|| format!("Failed to create file: {}", dst.display()))?;
copy(&mut file, &mut outfile)
.with_context(|| format!("Failed to extract file: {}", outpath.display()))?;
.with_context(|| format!("Failed to extract file: {}", dst.display()))?;
}

#[cfg(unix)]
Expand All @@ -141,35 +149,35 @@ fn extract_zip(file: &Path, mapper: impl Fn(&Path) -> Option<PathBuf>) -> Result
};

if let Some(mode) = file.unix_mode() {
set_permissions(&outpath, Permissions::from_mode(mode))
.with_context(|| format!("Failed to set permissions: {}", outpath.display()))?;
set_permissions(dst, Permissions::from_mode(mode))
.with_context(|| format!("Failed to set permissions: {}", dst.display()))?;
}
}
}

Ok(())
}

fn extract_tar_gz(file: &Path, mapper: impl Fn(&Path) -> Option<PathBuf>) -> Result<()> {
fn extract_tar_gz<F>(file: &Path, mut mapper: F) -> Result<()>
where
F: FnMut(Cow<Path>) -> Option<PathBuf>,
{
let gz_decoder = flate2::read::GzDecoder::new(File::open(file)?);
let mut archive = tar::Archive::new(gz_decoder);

for entry in archive.entries()? {
let mut file = entry?;

let outpath = match &file.path() {
Ok(path) => match mapper(path) {
Some(path) => path,
None => continue,
},
Err(e) => return Err(anyhow!("Error while reading tar entry: {}", e)),
let mut entry = entry?;
let entry_path = entry.path().context("Bad file path in tar.gz archive")?;
let dst = match mapper(entry_path) {
Some(path) => path,
None => continue,
};

if let Some(p) = outpath.parent() {
if let Some(p) = dst.parent() {
p.ensure()?;
}

file.unpack(&outpath)?;
entry.unpack(&dst)?;
}

println!("Done!");
Expand Down
21 changes: 11 additions & 10 deletions crates/maa-cli/src/installer/maa_cli.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
use std::{
collections::BTreeMap,
env::{consts, current_exe},
time::Duration,
};
use std::{collections::BTreeMap, time::Duration};

use anyhow::{anyhow, Context, Result};
use dunce::canonicalize;
use semver::Version;
use serde::Deserialize;
use tokio::runtime::Runtime;
Expand All @@ -20,6 +15,9 @@ use crate::{
dirs::{self, Ensure},
};

const MAA_CLI_NAME: &str = "maa";
const MAA_CLI_EXE: &str = constcat::concat!(MAA_CLI_NAME, std::env::consts::EXE_SUFFIX);

pub fn update(args: &CommonArgs) -> Result<()> {
let config = CLI_CONFIG.cli_config().with_args(args);

Expand All @@ -33,7 +31,6 @@ pub fn update(args: &CommonArgs) -> Result<()> {
return Ok(());
}

let bin_path = canonicalize(current_exe()?)?;
let details = version_json.details();
let asset = details.asset()?;
let asset_name = asset.name();
Expand Down Expand Up @@ -61,15 +58,19 @@ pub fn update(args: &CommonArgs) -> Result<()> {
.context("Failed to download maa-cli")?;
};

let cli_exe = format!("maa{}", consts::EXE_SUFFIX);
let tmp_dir = tempfile::tempdir()?;
let tmp_exe = tmp_dir.path().join(MAA_CLI_EXE);

Archive::new(cache_path.into())?.extract(|path| {
if config.components().binary && path.ends_with(&cli_exe) {
Some(bin_path.clone())
if config.components().binary && path.ends_with(MAA_CLI_EXE) {
Some(tmp_exe.clone())
} else {
None
}
})?;

self_replace::self_replace(tmp_exe)?;

Ok(())
}

Expand Down
Loading

0 comments on commit d57c81b

Please sign in to comment.