use crate::bottle::{detect_platform, BottleDownloader};
use crate::cache::Cache;
use crate::cask::CaskState;
use crate::discovery::{discover_linux_system_packages, discover_manually_installed_casks};
use crate::error::{Result, WaxError};
use crate::install::{create_symlinks, InstallMode, InstallState, InstalledPackage};
use crate::lockfile::Lockfile;
use crate::signal::{check_cancelled, CriticalSection};
use crate::ui::{PROGRESS_BAR_CHARS, PROGRESS_BAR_TEMPLATE};
use console::style;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use std::collections::HashMap;
use std::sync::Arc;
use tempfile::TempDir;
use tokio::sync::Semaphore;
use tracing::instrument;
#[instrument(skip(cache))]
pub async fn sync(cache: &Cache) -> Result<()> {
let start = std::time::Instant::now();
let lockfile_path = Lockfile::default_path();
let lockfile = Lockfile::load(&lockfile_path).await?;
let package_count = lockfile.packages.len();
let cask_count = lockfile.casks.len();
if package_count == 0 && cask_count == 0 {
println!("no packages or casks in lockfile");
return Ok(());
}
let formulae = cache.load_formulae().await?;
let state = InstallState::new()?;
let installed_packages = load_installed_packages(&state, &formulae).await?;
let casks = cache.load_casks().await?;
let cask_state = CaskState::new()?;
let installed_casks = load_installed_casks(&cask_state, &casks).await?;
let current_platform = detect_platform();
let actions = compute_sync_actions(&lockfile, &installed_packages, &installed_casks);
if print_sync_preview(&actions) {
return Ok(());
}
let sync_package_count = actions.packages_to_install.len();
if sync_package_count > 0 {
let entries =
build_sync_entries(actions.packages_to_install, &formulae, ¤t_platform)?;
let temp_dir = Arc::new(TempDir::new()?);
let extracted_packages =
download_and_extract_packages(entries, Arc::clone(&temp_dir)).await?;
install_extracted_packages(extracted_packages, &state).await?;
}
if !actions.casks_to_install.is_empty() {
println!();
crate::commands::install::install_impl(
cache,
&actions.casks_to_install,
crate::commands::install::InstallArgs {
dry_run: false,
ask: false,
cask: true,
user: false,
global: false,
build_from_source: false,
head: false,
run_scripts: true,
quiet: true,
force_reinstall: false,
external_pb: None,
},
)
.await?;
}
let elapsed = start.elapsed();
let total_synced = sync_package_count + actions.casks_to_install.len();
println!();
println!(
"{} {} synced{}",
total_synced,
if total_synced == 1 {
"package/cask"
} else {
"packages/casks"
},
crate::ui::elapsed_suffix(elapsed)
);
Ok(())
}
async fn load_installed_packages(
state: &InstallState,
formulae: &[crate::api::Formula],
) -> Result<HashMap<String, InstalledPackage>> {
let mut installed_packages = state.load().await?;
if cfg!(target_os = "linux") {
for (name, package) in discover_linux_system_packages(formulae).await? {
installed_packages.entry(name).or_insert(package);
}
}
if !installed_packages.is_empty() {
state.save(&installed_packages).await?;
}
Ok(installed_packages)
}
struct SyncActions {
packages_to_install: Vec<(String, crate::lockfile::LockfilePackage)>,
casks_to_install: Vec<String>,
up_to_date: Vec<String>,
upgrades: Vec<(String, String, String)>,
casks_up_to_date: Vec<String>,
cask_upgrades: Vec<(String, String, String)>,
}
fn compute_sync_actions(
lockfile: &Lockfile,
installed_packages: &HashMap<String, InstalledPackage>,
installed_casks: &HashMap<String, crate::cask::InstalledCask>,
) -> SyncActions {
let mut packages_to_install = Vec::new();
let mut casks_to_install = Vec::new();
let mut up_to_date = Vec::new();
let mut upgrades = Vec::new();
for (name, lock_pkg) in &lockfile.packages {
match installed_packages.get(name) {
Some(installed) if installed.version != lock_pkg.version => {
upgrades.push((
name.clone(),
installed.version.clone(),
lock_pkg.version.clone(),
));
packages_to_install.push((name.clone(), lock_pkg.clone()));
}
Some(installed) if installed.platform != lock_pkg.bottle => {
packages_to_install.push((name.clone(), lock_pkg.clone()));
}
Some(_) => {
up_to_date.push(name.clone());
}
None => {
packages_to_install.push((name.clone(), lock_pkg.clone()));
}
}
}
let mut casks_up_to_date = Vec::new();
let mut cask_upgrades = Vec::new();
for (name, lock_cask) in &lockfile.casks {
match installed_casks.get(name) {
Some(installed) if installed.version != lock_cask.version => {
cask_upgrades.push((
name.clone(),
installed.version.clone(),
lock_cask.version.clone(),
));
casks_to_install.push(name.clone());
}
Some(_) => {
casks_up_to_date.push(name.clone());
}
None => {
casks_to_install.push(name.clone());
}
}
}
SyncActions {
packages_to_install,
casks_to_install,
up_to_date,
upgrades,
casks_up_to_date,
cask_upgrades,
}
}
fn print_sync_preview(actions: &SyncActions) -> bool {
if !actions.packages_to_install.is_empty() || !actions.upgrades.is_empty() {
let upgrade_index: HashMap<_, _> = actions
.upgrades
.iter()
.map(|(name, old_ver, new_ver)| (name.as_str(), (old_ver.as_str(), new_ver.as_str())))
.collect();
for (name, lock_pkg) in &actions.packages_to_install {
if let Some((old_ver, new_ver)) = upgrade_index.get(name.as_str()) {
println!(
" {} {} {} → {}",
style("↑").cyan(),
style(name).magenta(),
style(*old_ver).dim(),
style(*new_ver).green()
);
} else {
println!(
" {} {} {}",
style("+").green(),
style(name).magenta(),
style(format!("@{}", lock_pkg.version)).dim()
);
}
}
}
if !actions.casks_to_install.is_empty() || !actions.cask_upgrades.is_empty() {
let cask_upgrade_index: HashMap<_, _> = actions
.cask_upgrades
.iter()
.map(|(name, old_ver, new_ver)| (name.as_str(), (old_ver.as_str(), new_ver.as_str())))
.collect();
for name in &actions.casks_to_install {
if let Some((old_ver, new_ver)) = cask_upgrade_index.get(name.as_str()) {
println!(
" {} {} {} {} → {}",
style("↑").cyan(),
style(name).magenta(),
style("(cask)").yellow(),
style(*old_ver).dim(),
style(*new_ver).green()
);
} else {
println!(
" {} {} {}",
style("+").green(),
style(name).magenta(),
style("(cask)").yellow()
);
}
}
}
if actions.packages_to_install.is_empty() && actions.casks_to_install.is_empty() {
if !actions.up_to_date.is_empty() || !actions.casks_up_to_date.is_empty() {
let total_up_to_date = actions.up_to_date.len() + actions.casks_up_to_date.len();
println!(
"{} {} packages/casks up to date",
style("✓").green(),
total_up_to_date
);
}
return true;
}
false
}
struct SyncEntry {
name: String,
version: String,
platform: String,
url: String,
sha256: String,
}
fn build_sync_entries(
packages_to_install: Vec<(String, crate::lockfile::LockfilePackage)>,
formulae: &[crate::api::Formula],
current_platform: &str,
) -> Result<Vec<SyncEntry>> {
let mut entries = Vec::new();
for (name, lock_pkg) in packages_to_install {
let formula = formulae
.iter()
.find(|f| f.name == name)
.ok_or_else(|| WaxError::FormulaNotFound(name.clone()))?;
if formula.versions.stable != lock_pkg.version {
return Err(WaxError::LockfileError(format!(
"Package {} version mismatch: lockfile specifies {} but latest available is {}. The locked version may no longer be available.",
name, lock_pkg.version, formula.versions.stable
)));
}
if lock_pkg.bottle != current_platform {
println!(
"platform mismatch for {}: {} → {}",
name, lock_pkg.bottle, current_platform
);
}
let bottle_info = formula
.bottle
.as_ref()
.and_then(|b| b.stable.as_ref())
.ok_or_else(|| WaxError::BottleNotAvailable(format!("{} (no bottle info)", name)))?;
let bottle_file = bottle_info
.file_for_platform(&lock_pkg.bottle)
.ok_or_else(|| {
WaxError::BottleNotAvailable(format!("{} for platform {}", name, lock_pkg.bottle))
})?;
entries.push(SyncEntry {
name: name.clone(),
version: lock_pkg.version.clone(),
platform: lock_pkg.bottle.clone(),
url: bottle_file.url.clone(),
sha256: bottle_file.sha256.clone(),
});
}
Ok(entries)
}
async fn download_and_extract_packages(
entries: Vec<SyncEntry>,
temp_dir: Arc<TempDir>,
) -> Result<Vec<(String, String, String, std::path::PathBuf)>> {
let multi = MultiProgress::new();
let downloader = Arc::new(BottleDownloader::new());
let concurrent_limit = entries.len().clamp(1, 32);
let semaphore = Arc::new(Semaphore::new(concurrent_limit));
let probe_tasks: Vec<_> = entries
.iter()
.map(|e| {
let dl = Arc::clone(&downloader);
let url = e.url.clone();
tokio::spawn(async move { dl.probe_size(&url).await })
})
.collect();
let mut sizes: Vec<u64> = Vec::with_capacity(entries.len());
for task in probe_tasks {
sizes.push(task.await.unwrap_or(0));
}
let mut tasks = Vec::new();
for (entry, size) in entries.into_iter().zip(sizes) {
let conns =
BottleDownloader::num_connections(size, BottleDownloader::MAX_CONNECTIONS_PER_DOWNLOAD);
let downloader = Arc::clone(&downloader);
let semaphore = Arc::clone(&semaphore);
let temp_dir = Arc::clone(&temp_dir);
let pb = multi.add(ProgressBar::new(0));
let style = ProgressStyle::default_bar()
.template(PROGRESS_BAR_TEMPLATE)
.unwrap()
.progress_chars(PROGRESS_BAR_CHARS);
pb.set_style(style);
pb.set_message(entry.name.clone());
let task = tokio::spawn(async move {
let permit = match semaphore.acquire().await {
Ok(p) => p,
Err(e) => {
return Err(WaxError::InstallError(format!(
"Failed to acquire semaphore permit: {}",
e
)));
}
};
let tarball_path = temp_dir
.path()
.join(format!("{}-{}.tar.gz", entry.name, entry.version));
downloader
.download(&entry.url, &tarball_path, Some(&pb), conns, None)
.await?;
pb.finish_and_clear();
drop(permit);
crate::digest::verify_sha256_file(&tarball_path, &entry.sha256)?;
let extract_dir = temp_dir.path().join(&entry.name);
BottleDownloader::extract(&tarball_path, &extract_dir)?;
Ok::<_, WaxError>((entry.name, entry.version, entry.platform, extract_dir))
});
tasks.push(task);
}
let mut extracted_packages = Vec::new();
for result in tasks {
let result = result.await;
match result {
Ok(Ok(data)) => extracted_packages.push(data),
Ok(Err(e)) => return Err(e),
Err(e) => {
return Err(WaxError::InstallError(format!(
"Download task failed: {}",
e
)))
}
}
}
Ok(extracted_packages)
}
async fn install_extracted_packages(
extracted_packages: Vec<(String, String, String, std::path::PathBuf)>,
state: &InstallState,
) -> Result<()> {
let install_mode = InstallMode::detect();
install_mode.validate()?;
let cellar = install_mode.cellar_path()?;
check_cancelled()?;
println!();
for (name, version, platform, extract_dir) in extracted_packages {
let _critical = CriticalSection::new();
let formula_cellar = cellar.join(&name).join(&version);
tokio::fs::create_dir_all(&formula_cellar).await?;
crate::bottle::copy_extracted_bottle_to_cellar(
&extract_dir,
&name,
&version,
&formula_cellar,
)?;
create_symlinks(
&name,
&version,
&cellar,
false,
install_mode,
)
.await?;
let package = InstalledPackage {
name: name.clone(),
version: version.clone(),
platform: platform.clone(),
install_date: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64,
install_mode,
from_source: false,
bottle_rebuild: 0,
bottle_sha256: None,
pinned: false,
};
state.add(package).await?;
println!("+ {}", style(&name).magenta());
}
Ok(())
}
async fn load_installed_casks(
cask_state: &CaskState,
casks: &[crate::api::Cask],
) -> Result<HashMap<String, crate::cask::InstalledCask>> {
let mut installed_casks = cask_state.load().await?;
if cfg!(target_os = "macos") {
for (name, cask) in discover_manually_installed_casks(casks).await? {
installed_casks.entry(name).or_insert(cask);
}
cask_state.save(&installed_casks).await?;
}
Ok(installed_casks)
}