use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use crate::commands::upgrade_details::{UpgradeDetail, UpgradeDetails, UpgradeDetailsManager};
use crate::runner::Runner;
fn write_debug_log(tmpdir: &Path, message: &str) {
if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(debug_file, "{}", message);
}
}
#[derive(Debug, Deserialize, Serialize)]
struct OutdatedPackage {
name: String,
installed_versions: Vec<String>,
current_version: String,
pinned: bool,
pinned_version: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
struct OutdatedPackages {
formulae: Vec<OutdatedPackage>,
casks: Vec<OutdatedPackage>,
}
#[derive(Debug, Deserialize, Serialize)]
struct SimpleOutdatedPackage {
name: String,
installed_version: String,
current_version: String,
}
fn get_outdated_packages(runner: &dyn Runner, tmpdir: &Path) -> Result<Vec<SimpleOutdatedPackage>> {
let logfile = tmpdir.join("brew_outdated.log");
write_debug_log(tmpdir, "=== get_outdated_packages 主函数开始 ===");
write_debug_log(
tmpdir,
&format!("时间: {}", chrono::Utc::now().format("%Y-%m-%d %H:%M:%S")),
);
write_debug_log(tmpdir, &format!("临时目录: {}", tmpdir.display()));
write_debug_log(tmpdir, "=== 尝试 JSON 方法 ===");
match get_outdated_packages_json(runner, tmpdir, &logfile) {
Ok(packages) => {
if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(
debug_file,
"JSON 方法成功,发现 {} 个过时软件包",
packages.len()
);
}
if !packages.is_empty() {
if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(debug_file, "返回 JSON 方法结果");
}
return Ok(packages);
} else if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log"))
{
let _ = writeln!(debug_file, "JSON 方法返回空列表,尝试备用方法");
}
}
Err(e) => {
if let Ok(mut file) = File::create(tmpdir.join("brew_errors.log")) {
let _ = writeln!(file, "JSON method failed: {}", e);
}
if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(debug_file, "JSON 方法失败: {},尝试备用方法", e);
}
}
}
if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(debug_file, "=== 尝试文本方法 ===");
}
match get_outdated_packages_text(runner, tmpdir, &logfile) {
Ok(packages) => {
if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(
debug_file,
"文本方法成功,发现 {} 个过时软件包",
packages.len()
);
}
Ok(packages)
}
Err(e) => {
if let Ok(mut file) = File::create(tmpdir.join("brew_errors.log")) {
let _ = writeln!(file, "All methods failed: {}", e);
}
if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(debug_file, "所有方法都失败: {},返回空列表", e);
}
Ok(Vec::new())
}
}
}
fn get_outdated_packages_json(
runner: &dyn Runner,
tmpdir: &Path,
logfile: &Path,
) -> Result<Vec<SimpleOutdatedPackage>> {
if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(debug_file, "=== Homebrew 过时软件包检测调试信息 ===");
let _ = writeln!(
debug_file,
"时间: {}",
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S")
);
let _ = writeln!(debug_file, "执行命令: brew outdated --json");
}
let (rc, out) = runner.run("brew outdated --json", logfile, false)?;
if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(debug_file, "命令退出码: {}", rc);
let _ = writeln!(debug_file, "输出长度: {} 字符", out.len());
let _ = writeln!(debug_file, "原始输出:");
let _ = writeln!(debug_file, "{}", out);
let _ = writeln!(debug_file, "=== JSON 解析开始 ===");
}
if out.trim().is_empty() {
if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(debug_file, "警告: brew outdated --json 输出为空");
}
return Ok(Vec::new());
}
let outdated: OutdatedPackages = match serde_json::from_str::<OutdatedPackages>(&out) {
Ok(parsed) => {
if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(debug_file, "JSON 解析成功");
let _ = writeln!(debug_file, "Formulae 数量: {}", parsed.formulae.len());
let _ = writeln!(debug_file, "Casks 数量: {}", parsed.casks.len());
}
parsed
}
Err(e) => {
if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(debug_file, "JSON 解析失败: {}", e);
let _ = writeln!(debug_file, "原始输出内容:");
let _ = writeln!(debug_file, "{}", out);
}
return Err(e.into());
}
};
let mut all_outdated = Vec::new();
if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(debug_file, "=== 处理 Formulae ===");
}
for (index, package) in outdated.formulae.iter().enumerate() {
if let Some(installed_version) = package.installed_versions.first() {
let simple_package = SimpleOutdatedPackage {
name: package.name.clone(),
installed_version: installed_version.clone(),
current_version: package.current_version.clone(),
};
all_outdated.push(simple_package);
if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(
debug_file,
"Formulae[{}]: {} {} -> {}",
index, package.name, installed_version, package.current_version
);
}
} else if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(
debug_file,
"警告: Formulae[{}] {} 没有安装版本信息",
index, package.name
);
}
}
if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(debug_file, "=== 处理 Casks ===");
}
for (index, package) in outdated.casks.iter().enumerate() {
if let Some(installed_version) = package.installed_versions.first() {
let simple_package = SimpleOutdatedPackage {
name: package.name.clone(),
installed_version: installed_version.clone(),
current_version: package.current_version.clone(),
};
all_outdated.push(simple_package);
if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(
debug_file,
"Cask[{}]: {} {} -> {}",
index, package.name, installed_version, package.current_version
);
}
} else if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(
debug_file,
"警告: Cask[{}] {} 没有安装版本信息",
index, package.name
);
}
}
if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(debug_file, "=== 检测结果汇总 ===");
let _ = writeln!(debug_file, "总共发现 {} 个过时软件包", all_outdated.len());
if all_outdated.is_empty() {
let _ = writeln!(debug_file, "所有软件包都是最新版本");
} else {
for (index, package) in all_outdated.iter().enumerate() {
let _ = writeln!(
debug_file,
" [{}] {}: {} -> {}",
index, package.name, package.installed_version, package.current_version
);
}
}
}
let json_file = tmpdir.join("outdated_packages.json");
if let Ok(mut file) = File::create(&json_file) {
let _ = writeln!(file, "{}", serde_json::to_string_pretty(&all_outdated)?);
}
Ok(all_outdated)
}
fn get_outdated_packages_text(
runner: &dyn Runner,
_tmpdir: &Path,
logfile: &Path,
) -> Result<Vec<SimpleOutdatedPackage>> {
let (_, out) = runner.run("brew outdated", logfile, false)?;
let mut packages = Vec::new();
for line in out.lines() {
if let Some((name, version_info)) = line.split_once(' ') {
if let Some((installed, current)) = version_info.split_once(" -> ") {
packages.push(SimpleOutdatedPackage {
name: name.to_string(),
installed_version: installed.to_string(),
current_version: current.to_string(),
});
}
}
}
Ok(packages)
}
pub fn brew_update(
runner: &dyn Runner,
tmpdir: &Path,
verbose: bool,
) -> Result<(String, i32, PathBuf)> {
let logfile = tmpdir.join("brew_update.log");
let (_, commit_before) = runner.run(
"cd $(brew --repository) && git log -1 --format='%H' 2>/dev/null || echo 'unknown'",
&logfile,
verbose,
)?;
let (rc_update, out_update) = runner.run(
"HOMEBREW_NO_PROGRESS=1 HOMEBREW_NO_ANALYTICS=1 HOMEBREW_NO_INSECURE_REDIRECT=1 HOMEBREW_NO_EMOJI=1 HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_ENV_HINTS=1 brew update --quiet 2>&1",
&logfile,
verbose,
)?;
if rc_update != 0 {
return Ok(("failed".to_string(), rc_update, logfile));
}
let (_, commit_after) = runner.run(
"cd $(brew --repository) && git log -1 --format='%H' 2>/dev/null || echo 'unknown'",
&logfile,
verbose,
)?;
let state = if (commit_before.trim() == commit_after.trim()
&& commit_before.trim() != "unknown")
|| out_update.contains("Already up-to-date.")
{
"unchanged"
} else {
"changed"
};
Ok((state.to_string(), rc_update, logfile))
}
pub fn brew_upgrade(
runner: &dyn Runner,
tmpdir: &Path,
verbose: bool,
) -> Result<(String, i32, PathBuf)> {
let logfile = tmpdir.join("brew_upgrade.log");
let outdated_packages = get_outdated_packages(runner, tmpdir)?;
if let Ok(mut file) = File::create(tmpdir.join("brew_upgrade_debug.log")) {
let _ = writeln!(
file,
"Debug: 升级前过时软件包数量: {}",
outdated_packages.len()
);
for pkg in &outdated_packages {
let _ = writeln!(
file,
" - {}: {} -> {}",
pkg.name, pkg.installed_version, pkg.current_version
);
}
}
if outdated_packages.is_empty() {
if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(debug_file, "没有过时软件包,但仍执行 brew upgrade 命令");
}
}
let (rc_upgrade, _out_upgrade) = runner.run(
"HOMEBREW_NO_PROGRESS=1 HOMEBREW_NO_ANALYTICS=1 HOMEBREW_NO_INSECURE_REDIRECT=1 HOMEBREW_NO_EMOJI=1 HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_ENV_HINTS=1 brew upgrade --quiet 2>&1",
&logfile,
verbose,
)?;
let actual_output = std::fs::read_to_string(&logfile).unwrap_or_default();
if rc_upgrade != 0 {
return Ok(("failed".to_string(), rc_upgrade, logfile));
}
let has_actual_upgrades =
actual_output.contains("==> Upgrading") || actual_output.contains("==> Installing");
let mut upgrade_details = Vec::new();
if has_actual_upgrades || !outdated_packages.is_empty() {
std::thread::sleep(std::time::Duration::from_millis(1000));
let updated_outdated = get_outdated_packages(runner, tmpdir)?;
for outdated in &outdated_packages {
let still_outdated = updated_outdated.iter().any(|p| p.name == outdated.name);
if !still_outdated {
upgrade_details.push(UpgradeDetail::version_upgrade(
outdated.name.clone(),
outdated.installed_version.clone(),
outdated.current_version.clone(),
));
}
}
}
let mut details = UpgradeDetails::new("Homebrew".to_string());
details.add_details(upgrade_details);
if details.has_upgrades() {
let _ = UpgradeDetailsManager::save_upgrade_details(&details, tmpdir, "brew");
}
let state = if details.has_upgrades() {
"changed"
} else if has_actual_upgrades {
"changed"
} else if !outdated_packages.is_empty() {
"changed"
} else {
if actual_output.contains("All formulae and casks are up to date")
|| actual_output.contains("Already up-to-date")
{
"unchanged"
} else {
"unchanged"
}
};
if let Ok(mut debug_file) = File::create(tmpdir.join("brew_detailed_debug.log")) {
let _ = writeln!(debug_file, "=== 状态判断调试 ===");
let _ = writeln!(
debug_file,
"details.has_upgrades(): {}",
details.has_upgrades()
);
let _ = writeln!(debug_file, "has_actual_upgrades: {}", has_actual_upgrades);
let _ = writeln!(
debug_file,
"outdated_packages.len(): {}",
outdated_packages.len()
);
let _ = writeln!(
debug_file,
"actual_output 长度: {} 字符",
actual_output.len()
);
let _ = writeln!(debug_file, "actual_output 内容: '{}'", actual_output);
let _ = writeln!(
debug_file,
"actual_output 包含 'All formulae and casks are up to date': {}",
actual_output.contains("All formulae and casks are up to date")
);
let _ = writeln!(
debug_file,
"actual_output 包含 'Already up-to-date': {}",
actual_output.contains("Already up-to-date")
);
let _ = writeln!(
debug_file,
"actual_output 包含 'up to date': {}",
actual_output.contains("up to date")
);
let _ = writeln!(debug_file, "最终状态: {}", state);
}
Ok((state.to_string(), rc_upgrade, logfile))
}
pub fn brew_cleanup(
runner: &dyn Runner,
tmpdir: &Path,
verbose: bool,
) -> Result<(String, i32, PathBuf)> {
let logfile = tmpdir.join("brew_cleanup.log");
let (rc_cleanup, out_cleanup) = runner.run(
"HOMEBREW_NO_PROGRESS=1 HOMEBREW_NO_ANALYTICS=1 HOMEBREW_NO_INSECURE_REDIRECT=1 HOMEBREW_NO_EMOJI=1 HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_ENV_HINTS=1 brew cleanup --quiet 2>&1",
&logfile,
verbose,
)?;
if rc_cleanup != 0 {
return Ok(("failed".to_string(), rc_cleanup, logfile));
}
let state = if out_cleanup.contains("Nothing to clean up") {
"unchanged"
} else {
"changed"
};
Ok((state.to_string(), rc_cleanup, logfile))
}