mirror of
https://github.com/encounter/objdiff.git
synced 2025-12-09 21:47:42 +00:00
Experimental objdiff-cli diff auto-rebuild
This commit is contained in:
50
objdiff-core/src/jobs/check_update.rs
Normal file
50
objdiff-core/src/jobs/check_update.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use std::{sync::mpsc::Receiver, task::Waker};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use self_update::{
|
||||
cargo_crate_version,
|
||||
update::{Release, ReleaseUpdate},
|
||||
};
|
||||
|
||||
use crate::jobs::{start_job, update_status, Job, JobContext, JobResult, JobState};
|
||||
|
||||
pub struct CheckUpdateConfig {
|
||||
pub build_updater: fn() -> Result<Box<dyn ReleaseUpdate>>,
|
||||
pub bin_names: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct CheckUpdateResult {
|
||||
pub update_available: bool,
|
||||
pub latest_release: Release,
|
||||
pub found_binary: Option<String>,
|
||||
}
|
||||
|
||||
fn run_check_update(
|
||||
context: &JobContext,
|
||||
cancel: Receiver<()>,
|
||||
config: CheckUpdateConfig,
|
||||
) -> Result<Box<CheckUpdateResult>> {
|
||||
update_status(context, "Fetching latest release".to_string(), 0, 1, &cancel)?;
|
||||
let updater = (config.build_updater)().context("Failed to create release updater")?;
|
||||
let latest_release = updater.get_latest_release()?;
|
||||
let update_available =
|
||||
self_update::version::bump_is_greater(cargo_crate_version!(), &latest_release.version)?;
|
||||
// Find the binary name in the release assets
|
||||
let mut found_binary = None;
|
||||
for bin_name in &config.bin_names {
|
||||
if latest_release.assets.iter().any(|a| &a.name == bin_name) {
|
||||
found_binary = Some(bin_name.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
update_status(context, "Complete".to_string(), 1, 1, &cancel)?;
|
||||
Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary }))
|
||||
}
|
||||
|
||||
pub fn start_check_update(waker: Waker, config: CheckUpdateConfig) -> JobState {
|
||||
start_job(waker, "Check for updates", Job::CheckUpdate, move |context, cancel| {
|
||||
run_check_update(&context, cancel, config)
|
||||
.map(|result| JobResult::CheckUpdate(Some(result)))
|
||||
})
|
||||
}
|
||||
103
objdiff-core/src/jobs/create_scratch.rs
Normal file
103
objdiff-core/src/jobs/create_scratch.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use std::{fs, path::PathBuf, sync::mpsc::Receiver, task::Waker};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
|
||||
use crate::{
|
||||
build::{run_make, BuildConfig, BuildStatus},
|
||||
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateScratchConfig {
|
||||
pub build_config: BuildConfig,
|
||||
pub context_path: Option<PathBuf>,
|
||||
pub build_context: bool,
|
||||
|
||||
// Scratch fields
|
||||
pub compiler: String,
|
||||
pub platform: String,
|
||||
pub compiler_flags: String,
|
||||
pub function_name: String,
|
||||
pub target_obj: PathBuf,
|
||||
pub preset_id: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct CreateScratchResult {
|
||||
pub scratch_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, serde::Deserialize)]
|
||||
struct CreateScratchResponse {
|
||||
pub slug: String,
|
||||
pub claim_token: String,
|
||||
}
|
||||
|
||||
const API_HOST: &str = "https://decomp.me";
|
||||
|
||||
fn run_create_scratch(
|
||||
status: &JobContext,
|
||||
cancel: Receiver<()>,
|
||||
config: CreateScratchConfig,
|
||||
) -> Result<Box<CreateScratchResult>> {
|
||||
let project_dir =
|
||||
config.build_config.project_dir.as_ref().ok_or_else(|| anyhow!("Missing project dir"))?;
|
||||
|
||||
let mut context = None;
|
||||
if let Some(context_path) = &config.context_path {
|
||||
if config.build_context {
|
||||
update_status(status, "Building context".to_string(), 0, 2, &cancel)?;
|
||||
match run_make(&config.build_config, context_path) {
|
||||
BuildStatus { success: true, .. } => {}
|
||||
BuildStatus { success: false, stdout, stderr, .. } => {
|
||||
bail!("Failed to build context:\n{stdout}\n{stderr}")
|
||||
}
|
||||
}
|
||||
}
|
||||
let context_path = project_dir.join(context_path);
|
||||
context = Some(
|
||||
fs::read_to_string(&context_path)
|
||||
.map_err(|e| anyhow!("Failed to read {}: {}", context_path.display(), e))?,
|
||||
);
|
||||
}
|
||||
|
||||
update_status(status, "Creating scratch".to_string(), 1, 2, &cancel)?;
|
||||
let diff_flags = [format!("--disassemble={}", config.function_name)];
|
||||
let diff_flags = serde_json::to_string(&diff_flags)?;
|
||||
let obj_path = project_dir.join(&config.target_obj);
|
||||
let file = reqwest::blocking::multipart::Part::file(&obj_path)
|
||||
.with_context(|| format!("Failed to open {}", obj_path.display()))?;
|
||||
let mut form = reqwest::blocking::multipart::Form::new()
|
||||
.text("compiler", config.compiler.clone())
|
||||
.text("platform", config.platform.clone())
|
||||
.text("compiler_flags", config.compiler_flags.clone())
|
||||
.text("diff_label", config.function_name.clone())
|
||||
.text("diff_flags", diff_flags)
|
||||
.text("context", context.unwrap_or_default())
|
||||
.text("source_code", "// Move related code from Context tab to here");
|
||||
if let Some(preset) = config.preset_id {
|
||||
form = form.text("preset", preset.to_string());
|
||||
}
|
||||
form = form.part("target_obj", file);
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client
|
||||
.post(format!("{API_HOST}/api/scratch"))
|
||||
.multipart(form)
|
||||
.send()
|
||||
.map_err(|e| anyhow!("Failed to send request: {}", e))?;
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow!("Failed to create scratch: {}", response.text()?));
|
||||
}
|
||||
let body: CreateScratchResponse = response.json().context("Failed to parse response")?;
|
||||
let scratch_url = format!("{API_HOST}/scratch/{}/claim?token={}", body.slug, body.claim_token);
|
||||
|
||||
update_status(status, "Complete".to_string(), 2, 2, &cancel)?;
|
||||
Ok(Box::from(CreateScratchResult { scratch_url }))
|
||||
}
|
||||
|
||||
pub fn start_create_scratch(waker: Waker, config: CreateScratchConfig) -> JobState {
|
||||
start_job(waker, "Create scratch", Job::CreateScratch, move |context, cancel| {
|
||||
run_create_scratch(&context, cancel, config)
|
||||
.map(|result| JobResult::CreateScratch(Some(result)))
|
||||
})
|
||||
}
|
||||
230
objdiff-core/src/jobs/mod.rs
Normal file
230
objdiff-core/src/jobs/mod.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
mpsc::{Receiver, Sender, TryRecvError},
|
||||
Arc, RwLock,
|
||||
},
|
||||
task::Waker,
|
||||
thread::JoinHandle,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::jobs::{
|
||||
check_update::CheckUpdateResult, create_scratch::CreateScratchResult, objdiff::ObjDiffResult,
|
||||
update::UpdateResult,
|
||||
};
|
||||
|
||||
pub mod check_update;
|
||||
pub mod create_scratch;
|
||||
pub mod objdiff;
|
||||
pub mod update;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
pub enum Job {
|
||||
ObjDiff,
|
||||
CheckUpdate,
|
||||
Update,
|
||||
CreateScratch,
|
||||
}
|
||||
pub static JOB_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct JobQueue {
|
||||
pub jobs: Vec<JobState>,
|
||||
pub results: Vec<JobResult>,
|
||||
}
|
||||
|
||||
impl JobQueue {
|
||||
/// Adds a job to the queue.
|
||||
#[inline]
|
||||
pub fn push(&mut self, state: JobState) { self.jobs.push(state); }
|
||||
|
||||
/// Adds a job to the queue if a job of the given kind is not already running.
|
||||
#[inline]
|
||||
pub fn push_once(&mut self, job: Job, func: impl FnOnce() -> JobState) {
|
||||
if !self.is_running(job) {
|
||||
self.push(func());
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether a job of the given kind is running.
|
||||
pub fn is_running(&self, kind: Job) -> bool {
|
||||
self.jobs.iter().any(|j| j.kind == kind && j.handle.is_some())
|
||||
}
|
||||
|
||||
/// Returns whether any job is running.
|
||||
pub fn any_running(&self) -> bool {
|
||||
self.jobs.iter().any(|job| {
|
||||
if let Some(handle) = &job.handle {
|
||||
return !handle.is_finished();
|
||||
}
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterates over all jobs mutably.
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut JobState> + '_ { self.jobs.iter_mut() }
|
||||
|
||||
/// Iterates over all finished jobs, returning the job state and the result.
|
||||
pub fn iter_finished(
|
||||
&mut self,
|
||||
) -> impl Iterator<Item = (&mut JobState, std::thread::Result<JobResult>)> + '_ {
|
||||
self.jobs.iter_mut().filter_map(|job| {
|
||||
if let Some(handle) = &job.handle {
|
||||
if !handle.is_finished() {
|
||||
return None;
|
||||
}
|
||||
let result = job.handle.take().unwrap().join();
|
||||
return Some((job, result));
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Clears all finished jobs.
|
||||
pub fn clear_finished(&mut self) {
|
||||
self.jobs.retain(|job| {
|
||||
!(job.handle.is_none() && job.context.status.read().unwrap().error.is_none())
|
||||
});
|
||||
}
|
||||
|
||||
/// Clears all errored jobs.
|
||||
pub fn clear_errored(&mut self) {
|
||||
self.jobs.retain(|job| job.context.status.read().unwrap().error.is_none());
|
||||
}
|
||||
|
||||
/// Removes a job from the queue given its ID.
|
||||
pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); }
|
||||
|
||||
/// Collects the results of all finished jobs and handles any errors.
|
||||
pub fn collect_results(&mut self) {
|
||||
let mut results = vec![];
|
||||
for (job, result) in self.iter_finished() {
|
||||
match result {
|
||||
Ok(result) => {
|
||||
match result {
|
||||
JobResult::None => {
|
||||
// Job context contains the error
|
||||
}
|
||||
_ => results.push(result),
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let err = if let Some(msg) = err.downcast_ref::<&'static str>() {
|
||||
anyhow::Error::msg(*msg)
|
||||
} else if let Some(msg) = err.downcast_ref::<String>() {
|
||||
anyhow::Error::msg(msg.clone())
|
||||
} else {
|
||||
anyhow::Error::msg("Thread panicked")
|
||||
};
|
||||
let result = job.context.status.write();
|
||||
if let Ok(mut guard) = result {
|
||||
guard.error = Some(err);
|
||||
} else {
|
||||
drop(result);
|
||||
job.context.status = Arc::new(RwLock::new(JobStatus {
|
||||
title: "Error".to_string(),
|
||||
progress_percent: 0.0,
|
||||
progress_items: None,
|
||||
status: String::new(),
|
||||
error: Some(err),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.results.append(&mut results);
|
||||
self.clear_finished();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JobContext {
|
||||
pub status: Arc<RwLock<JobStatus>>,
|
||||
pub waker: Waker,
|
||||
}
|
||||
|
||||
pub struct JobState {
|
||||
pub id: usize,
|
||||
pub kind: Job,
|
||||
pub handle: Option<JoinHandle<JobResult>>,
|
||||
pub context: JobContext,
|
||||
pub cancel: Sender<()>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct JobStatus {
|
||||
pub title: String,
|
||||
pub progress_percent: f32,
|
||||
pub progress_items: Option<[u32; 2]>,
|
||||
pub status: String,
|
||||
pub error: Option<anyhow::Error>,
|
||||
}
|
||||
|
||||
pub enum JobResult {
|
||||
None,
|
||||
ObjDiff(Option<Box<ObjDiffResult>>),
|
||||
CheckUpdate(Option<Box<CheckUpdateResult>>),
|
||||
Update(Box<UpdateResult>),
|
||||
CreateScratch(Option<Box<CreateScratchResult>>),
|
||||
}
|
||||
|
||||
fn should_cancel(rx: &Receiver<()>) -> bool {
|
||||
match rx.try_recv() {
|
||||
Ok(_) | Err(TryRecvError::Disconnected) => true,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn start_job(
|
||||
waker: Waker,
|
||||
title: &str,
|
||||
kind: Job,
|
||||
run: impl FnOnce(JobContext, Receiver<()>) -> Result<JobResult> + Send + 'static,
|
||||
) -> JobState {
|
||||
let status = Arc::new(RwLock::new(JobStatus {
|
||||
title: title.to_string(),
|
||||
progress_percent: 0.0,
|
||||
progress_items: None,
|
||||
status: String::new(),
|
||||
error: None,
|
||||
}));
|
||||
let context = JobContext { status: status.clone(), waker: waker.clone() };
|
||||
let context_inner = JobContext { status: status.clone(), waker };
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let handle = std::thread::spawn(move || match run(context_inner, rx) {
|
||||
Ok(state) => state,
|
||||
Err(e) => {
|
||||
if let Ok(mut w) = status.write() {
|
||||
w.error = Some(e);
|
||||
}
|
||||
JobResult::None
|
||||
}
|
||||
});
|
||||
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
|
||||
// log::info!("Started job {}", id); TODO
|
||||
JobState { id, kind, handle: Some(handle), context, cancel: tx }
|
||||
}
|
||||
|
||||
fn update_status(
|
||||
context: &JobContext,
|
||||
str: String,
|
||||
count: u32,
|
||||
total: u32,
|
||||
cancel: &Receiver<()>,
|
||||
) -> Result<()> {
|
||||
let mut w =
|
||||
context.status.write().map_err(|_| anyhow::Error::msg("Failed to lock job status"))?;
|
||||
w.progress_items = Some([count, total]);
|
||||
w.progress_percent = count as f32 / total as f32;
|
||||
if should_cancel(cancel) {
|
||||
w.status = "Cancelled".to_string();
|
||||
return Err(anyhow::Error::msg("Cancelled"));
|
||||
} else {
|
||||
w.status = str;
|
||||
}
|
||||
drop(w);
|
||||
context.waker.wake_by_ref();
|
||||
Ok(())
|
||||
}
|
||||
199
objdiff-core/src/jobs/objdiff.rs
Normal file
199
objdiff-core/src/jobs/objdiff.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
use std::{path::PathBuf, sync::mpsc::Receiver, task::Waker};
|
||||
|
||||
use anyhow::{anyhow, Error, Result};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::{
|
||||
build::{run_make, BuildConfig, BuildStatus},
|
||||
config::SymbolMappings,
|
||||
diff::{diff_objs, DiffObjConfig, MappingConfig, ObjDiff},
|
||||
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
|
||||
obj::{read, ObjInfo},
|
||||
};
|
||||
|
||||
pub struct ObjDiffConfig {
|
||||
pub build_config: BuildConfig,
|
||||
pub build_base: bool,
|
||||
pub build_target: bool,
|
||||
pub target_path: Option<PathBuf>,
|
||||
pub base_path: Option<PathBuf>,
|
||||
pub diff_obj_config: DiffObjConfig,
|
||||
pub symbol_mappings: SymbolMappings,
|
||||
pub selecting_left: Option<String>,
|
||||
pub selecting_right: Option<String>,
|
||||
}
|
||||
|
||||
pub struct ObjDiffResult {
|
||||
pub first_status: BuildStatus,
|
||||
pub second_status: BuildStatus,
|
||||
pub first_obj: Option<(ObjInfo, ObjDiff)>,
|
||||
pub second_obj: Option<(ObjInfo, ObjDiff)>,
|
||||
pub time: OffsetDateTime,
|
||||
}
|
||||
|
||||
fn run_build(
|
||||
context: &JobContext,
|
||||
cancel: Receiver<()>,
|
||||
mut config: ObjDiffConfig,
|
||||
) -> Result<Box<ObjDiffResult>> {
|
||||
// Use the per-object symbol mappings, we don't set mappings globally
|
||||
config.diff_obj_config.symbol_mappings = MappingConfig {
|
||||
mappings: config.symbol_mappings,
|
||||
selecting_left: config.selecting_left,
|
||||
selecting_right: config.selecting_right,
|
||||
};
|
||||
|
||||
let mut target_path_rel = None;
|
||||
let mut base_path_rel = None;
|
||||
if config.build_target || config.build_base {
|
||||
let project_dir = config
|
||||
.build_config
|
||||
.project_dir
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::msg("Missing project dir"))?;
|
||||
if let Some(target_path) = &config.target_path {
|
||||
target_path_rel = Some(target_path.strip_prefix(project_dir).map_err(|_| {
|
||||
anyhow!(
|
||||
"Target path '{}' doesn't begin with '{}'",
|
||||
target_path.display(),
|
||||
project_dir.display()
|
||||
)
|
||||
})?);
|
||||
}
|
||||
if let Some(base_path) = &config.base_path {
|
||||
base_path_rel = Some(base_path.strip_prefix(project_dir).map_err(|_| {
|
||||
anyhow!(
|
||||
"Base path '{}' doesn't begin with '{}'",
|
||||
base_path.display(),
|
||||
project_dir.display()
|
||||
)
|
||||
})?);
|
||||
};
|
||||
}
|
||||
|
||||
let mut total = 1;
|
||||
if config.build_target && target_path_rel.is_some() {
|
||||
total += 1;
|
||||
}
|
||||
if config.build_base && base_path_rel.is_some() {
|
||||
total += 1;
|
||||
}
|
||||
if config.target_path.is_some() {
|
||||
total += 1;
|
||||
}
|
||||
if config.base_path.is_some() {
|
||||
total += 1;
|
||||
}
|
||||
|
||||
let mut step_idx = 0;
|
||||
let mut first_status = match target_path_rel {
|
||||
Some(target_path_rel) if config.build_target => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Building target {}", target_path_rel.display()),
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
run_make(&config.build_config, target_path_rel)
|
||||
}
|
||||
_ => BuildStatus::default(),
|
||||
};
|
||||
|
||||
let mut second_status = match base_path_rel {
|
||||
Some(base_path_rel) if config.build_base => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Building base {}", base_path_rel.display()),
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
run_make(&config.build_config, base_path_rel)
|
||||
}
|
||||
_ => BuildStatus::default(),
|
||||
};
|
||||
|
||||
let time = OffsetDateTime::now_utc();
|
||||
|
||||
let first_obj = match &config.target_path {
|
||||
Some(target_path) if first_status.success => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Loading target {}", target_path.display()),
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
match read::read(target_path, &config.diff_obj_config) {
|
||||
Ok(obj) => Some(obj),
|
||||
Err(e) => {
|
||||
first_status = BuildStatus {
|
||||
success: false,
|
||||
stdout: format!("Loading object '{}'", target_path.display()),
|
||||
stderr: format!("{:#}", e),
|
||||
..Default::default()
|
||||
};
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
step_idx += 1;
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let second_obj = match &config.base_path {
|
||||
Some(base_path) if second_status.success => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Loading base {}", base_path.display()),
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
match read::read(base_path, &config.diff_obj_config) {
|
||||
Ok(obj) => Some(obj),
|
||||
Err(e) => {
|
||||
second_status = BuildStatus {
|
||||
success: false,
|
||||
stdout: format!("Loading object '{}'", base_path.display()),
|
||||
stderr: format!("{:#}", e),
|
||||
..Default::default()
|
||||
};
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
step_idx += 1;
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
update_status(context, "Performing diff".to_string(), step_idx, total, &cancel)?;
|
||||
step_idx += 1;
|
||||
let result = diff_objs(&config.diff_obj_config, first_obj.as_ref(), second_obj.as_ref(), None)?;
|
||||
|
||||
update_status(context, "Complete".to_string(), step_idx, total, &cancel)?;
|
||||
Ok(Box::new(ObjDiffResult {
|
||||
first_status,
|
||||
second_status,
|
||||
first_obj: first_obj.and_then(|o| result.left.map(|d| (o, d))),
|
||||
second_obj: second_obj.and_then(|o| result.right.map(|d| (o, d))),
|
||||
time,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn start_build(waker: Waker, config: ObjDiffConfig) -> JobState {
|
||||
start_job(waker, "Build", Job::ObjDiff, move |context, cancel| {
|
||||
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
|
||||
})
|
||||
}
|
||||
66
objdiff-core/src/jobs/update.rs
Normal file
66
objdiff-core/src/jobs/update.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use std::{
|
||||
env::{current_dir, current_exe},
|
||||
fs::File,
|
||||
path::PathBuf,
|
||||
sync::mpsc::Receiver,
|
||||
task::Waker,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
pub use self_update; // Re-export self_update crate
|
||||
use self_update::update::ReleaseUpdate;
|
||||
|
||||
use crate::jobs::{start_job, update_status, Job, JobContext, JobResult, JobState};
|
||||
|
||||
pub struct UpdateConfig {
|
||||
pub build_updater: fn() -> Result<Box<dyn ReleaseUpdate>>,
|
||||
pub bin_name: String,
|
||||
}
|
||||
|
||||
pub struct UpdateResult {
|
||||
pub exe_path: PathBuf,
|
||||
}
|
||||
|
||||
fn run_update(
|
||||
status: &JobContext,
|
||||
cancel: Receiver<()>,
|
||||
config: UpdateConfig,
|
||||
) -> Result<Box<UpdateResult>> {
|
||||
update_status(status, "Fetching latest release".to_string(), 0, 3, &cancel)?;
|
||||
let updater = (config.build_updater)().context("Failed to create release updater")?;
|
||||
let latest_release = updater.get_latest_release()?;
|
||||
let asset =
|
||||
latest_release.assets.iter().find(|a| a.name == config.bin_name).ok_or_else(|| {
|
||||
anyhow::Error::msg(format!("No release asset for {}", config.bin_name))
|
||||
})?;
|
||||
|
||||
update_status(status, "Downloading release".to_string(), 1, 3, &cancel)?;
|
||||
let tmp_dir = tempfile::Builder::new().prefix("update").tempdir_in(current_dir()?)?;
|
||||
let tmp_path = tmp_dir.path().join(&asset.name);
|
||||
let tmp_file = File::create(&tmp_path)?;
|
||||
self_update::Download::from_url(&asset.download_url)
|
||||
.set_header(reqwest::header::ACCEPT, "application/octet-stream".parse()?)
|
||||
.download_to(tmp_file)?;
|
||||
|
||||
update_status(status, "Extracting release".to_string(), 2, 3, &cancel)?;
|
||||
let tmp_file = tmp_dir.path().join("replacement_tmp");
|
||||
let target_file = current_exe()?;
|
||||
self_update::Move::from_source(&tmp_path)
|
||||
.replace_using_temp(&tmp_file)
|
||||
.to_dest(&target_file)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::{fs, os::unix::fs::PermissionsExt};
|
||||
fs::set_permissions(&target_file, fs::Permissions::from_mode(0o755))?;
|
||||
}
|
||||
tmp_dir.close()?;
|
||||
|
||||
update_status(status, "Complete".to_string(), 3, 3, &cancel)?;
|
||||
Ok(Box::from(UpdateResult { exe_path: target_file }))
|
||||
}
|
||||
|
||||
pub fn start_update(waker: Waker, config: UpdateConfig) -> JobState {
|
||||
start_job(waker, "Update app", Job::Update, move |context, cancel| {
|
||||
run_update(&context, cancel, config).map(JobResult::Update)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user