Skip to main content

stout_install/
extract.rs

1//! Bottle extraction
2
3use crate::error::{Error, Result};
4use flate2::read::GzDecoder;
5use std::fs::File;
6use std::path::{Path, PathBuf};
7use tar::Archive;
8use tracing::{debug, info};
9
10/// Extract a bottle tarball to the Cellar
11///
12/// Bottles are tarballs with structure: `<name>/<version>/...`
13/// We extract to: `<cellar>/<name>/<version>/...`
14pub fn extract_bottle(
15    bottle_path: impl AsRef<Path>,
16    cellar: impl AsRef<Path>,
17) -> Result<PathBuf> {
18    let bottle_path = bottle_path.as_ref();
19    let cellar = cellar.as_ref();
20
21    debug!("Extracting {} to {}", bottle_path.display(), cellar.display());
22
23    let file = File::open(bottle_path)?;
24    let decoder = GzDecoder::new(file);
25    let mut archive = Archive::new(decoder);
26
27    // Create cellar if it doesn't exist
28    std::fs::create_dir_all(cellar)?;
29
30    // Extract all entries
31    let mut install_path: Option<PathBuf> = None;
32
33    for entry in archive.entries()? {
34        let mut entry = entry?;
35        let path = entry.path()?;
36
37        // Get the top-level directory (package name)
38        if install_path.is_none() {
39            if let Some(component) = path.components().next() {
40                let pkg_name = component.as_os_str().to_string_lossy();
41                // The path inside the tarball is like `wget/1.24.5/...`
42                // We want to extract to `<cellar>/wget/1.24.5/...`
43                if let Some(second) = path.components().nth(1) {
44                    let version = second.as_os_str().to_string_lossy();
45                    install_path = Some(cellar.join(&*pkg_name).join(&*version));
46                }
47            }
48        }
49
50        // Compute full destination path
51        let dest = cellar.join(&path);
52
53        // Create parent directories
54        if let Some(parent) = dest.parent() {
55            std::fs::create_dir_all(parent)?;
56        }
57
58        // Extract the entry
59        entry.unpack(&dest)?;
60    }
61
62    let install_path = install_path.ok_or_else(|| {
63        Error::InvalidBottle("Could not determine install path from bottle".to_string())
64    })?;
65
66    info!("Extracted to {}", install_path.display());
67    Ok(install_path)
68}
69
70/// Remove an installed package from the Cellar
71pub fn remove_package(cellar: impl AsRef<Path>, name: &str, version: &str) -> Result<()> {
72    let package_path = cellar.as_ref().join(name).join(version);
73
74    if !package_path.exists() {
75        return Err(Error::PackageNotFound(format!("{}/{}", name, version)));
76    }
77
78    debug!("Removing {}", package_path.display());
79    std::fs::remove_dir_all(&package_path)?;
80
81    // Remove parent directory if empty
82    let parent = cellar.as_ref().join(name);
83    if parent.read_dir()?.next().is_none() {
84        std::fs::remove_dir(&parent)?;
85    }
86
87    info!("Removed {}-{}", name, version);
88    Ok(())
89}