// DCR — Cargo-like C/C++ project manager. // // Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . use crate::utils::log::{error, warn}; use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc}; use serde::Deserialize; use std::fs; use std::io::Read; use std::path::{Path, PathBuf}; #[cfg(target_os = "linux")] use std::process::Command; use std::time::{SystemTime, UNIX_EPOCH}; const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/dexoron/dcr/releases/latest"; /// Metadata for the latest GitHub release of DCR. #[derive(Debug, Deserialize)] struct Release { /// Release tag, typically including a leading `v` (e.g. `v1.2.3`). tag_name: String, /// Downloadable assets attached to the release. assets: Vec, } /// A single downloadable asset from a GitHub release. #[derive(Debug, Deserialize)] struct ReleaseAsset { /// Asset file name as published on GitHub. name: String, /// Direct URL used to download the asset. browser_download_url: String, } /// Handles the `dcr --update` flag: checks GitHub for a newer release and /// replaces the running binary when an update is available. /// /// # Parameters /// - `args`: Remaining tokens after `--update` (only `--help` is accepted). /// /// # Returns /// `0` on success / already up to date / pacman-managed refuse; `1` on error. pub fn flag_update(args: &[String]) -> i32 { if args.first().is_some_and(|a| a == "--help") { printc("USAGE:", BOLD_GREEN); printc(" dcr --update", BOLD_CYAN); println!(); printc("DESCRIPTION:", BOLD_GREEN); println!(" Updates DCR to the latest version from GitHub Releases."); return 0; } if !args.is_empty() { warn("Command does not support additional arguments"); return 1; } let current_exe = match std::env::current_exe() { Ok(path) => path, Err(_) => { error("Failed to resolve current binary path"); return 1; } }; // Refuse self-update when the binary is owned by pacman/AUR. if let Some(package_name) = pacman_owned_package(¤t_exe) { warn("This dcr binary is managed by pacman/AUR"); println!( "Update via package manager: yay/paru -Syu {package_name} or sudo pacman -Syu {package_name}" ); return 0; } let current_version = env!("CARGO_PKG_VERSION"); let target = env!("DCR_TARGET"); let release = match fetch_latest_release() { Ok(release) => release, Err(err) => { error(&format!("Failed to check for updates: {err}")); return 1; } }; // Normalize the tag by stripping a leading `v` before comparison. let latest_version = release.tag_name.trim_start_matches('v'); if latest_version == current_version { println!("Latest version is already installed: {current_version}"); return 0; } let candidate_names = asset_candidates(target); let Some(asset) = release .assets .iter() .find(|asset| candidate_names.iter().any(|name| name == &asset.name)) else { error(&format!("Binary for target {target} not found")); return 1; }; let bytes = match download_asset(&asset.browser_download_url) { Ok(bytes) => bytes, Err(err) => { error(&format!("Failed to download update: {err}")); return 1; } }; let temp_path = temp_binary_path(¤t_exe); if fs::write(&temp_path, &bytes).is_err() { error("Failed to write temporary binary"); return 1; } set_executable_permissions(&temp_path); // Replace the running executable in place, then clean up the temp file. if self_replace::self_replace(&temp_path).is_err() { let _ = fs::remove_file(&temp_path); error("Failed to replace current binary"); return 1; } let _ = fs::remove_file(&temp_path); println!("Update completed: {current_version} -> {latest_version}"); 0 } /// Fetches the latest DCR release metadata from the GitHub Releases API. fn fetch_latest_release() -> Result { let response = ureq::get(LATEST_RELEASE_URL) .set("User-Agent", "dcr-updater") .call() .map_err(|e| match e { ureq::Error::Status(code, _) => format!("GitHub API returned status {code}"), ureq::Error::Transport(e) => format!("GitHub API request failed: {e}"), })?; response .into_json::() .map_err(|_| "GitHub API response has an unexpected format".to_string()) } /// Downloads a release asset from the given URL into memory. fn download_asset(url: &str) -> Result, String> { let response = ureq::get(url) .set("User-Agent", "dcr-updater") .call() .map_err(|e| match e { ureq::Error::Status(code, _) => format!("Download returned status {code}"), ureq::Error::Transport(e) => format!("Download request failed: {e}"), })?; let mut reader = response.into_reader(); let mut data = Vec::new(); reader .read_to_end(&mut data) .map_err(|_| "Failed to read downloaded data".to_string())?; Ok(data) } /// Builds the list of expected asset file names for the given build target. fn asset_candidates(target: &str) -> Vec { let mut names = vec![format!("dcr-{target}")]; // Windows releases may publish the binary with an `.exe` suffix. if target.contains("-windows-") || target.ends_with("-windows") { names.push(format!("dcr-{target}.exe")); } names } /// Creates a unique temporary path next to the current executable for the download. fn temp_binary_path(current_exe: &Path) -> PathBuf { let stamp = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|value| value.as_millis()) .unwrap_or(0); let mut extension = format!("new-{stamp}"); if cfg!(windows) { extension.push_str(".exe"); } current_exe.with_extension(extension) } /// Sets executable permissions on Unix; no-op on other platforms. fn set_executable_permissions(_path: &Path) { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; if let Ok(meta) = fs::metadata(_path) { let mut perms = meta.permissions(); perms.set_mode(0o755); let _ = fs::set_permissions(_path, perms); } } } /// On Linux, returns the pacman package that owns `path`, if any. #[cfg(target_os = "linux")] fn pacman_owned_package(path: &Path) -> Option { let output = Command::new("pacman").arg("-Qoq").arg(path).output().ok()?; if !output.status.success() { return None; } let package_name = String::from_utf8_lossy(&output.stdout).trim().to_string(); if package_name.is_empty() { return None; } Some(package_name) } /// Non-Linux stub: package-manager ownership is not checked. #[cfg(not(target_os = "linux"))] fn pacman_owned_package(_path: &Path) -> Option { None }