pub use crate::artefact::target::TargetTriple;
pub use crate::version::Version;
use crate::binstall_metadata::{DEFAULT_PKG_FMT, WINDOWS_PKG_FMT};
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use thiserror::Error;
const CRATE_NAME: &str = "whitaker-installer";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArchiveFormat {
Tgz,
Zip,
}
#[derive(Debug)]
pub struct InstallerPackageParams {
pub version: Version,
pub target: TargetTriple,
pub binary_path: PathBuf,
pub output_dir: PathBuf,
}
#[derive(Debug)]
pub struct InstallerPackageOutput {
pub archive_path: PathBuf,
pub archive_name: String,
}
#[derive(Debug, Error)]
pub enum InstallerPackagingError {
#[error("I/O error during packaging: {0}")]
Io(#[from] std::io::Error),
#[error("binary file not found: {0}")]
BinaryNotFound(PathBuf),
#[error("ZIP archive error: {0}")]
Zip(#[from] zip::result::ZipError),
}
#[must_use]
pub fn archive_filename(version: &Version, target: &TargetTriple) -> String {
let ext = if target.is_windows() {
WINDOWS_PKG_FMT
} else {
DEFAULT_PKG_FMT
};
format!(
"{CRATE_NAME}-{}-v{}.{ext}",
target.as_str(),
version.as_str()
)
}
#[must_use]
pub fn inner_dir_name(version: &Version, target: &TargetTriple) -> String {
format!("{CRATE_NAME}-{}-v{}", target.as_str(), version.as_str())
}
#[must_use]
pub fn binary_filename(target: &TargetTriple) -> String {
if target.is_windows() {
format!("{CRATE_NAME}.exe")
} else {
CRATE_NAME.to_owned()
}
}
#[must_use]
pub fn archive_format(target: &TargetTriple) -> ArchiveFormat {
if target.is_windows() {
ArchiveFormat::Zip
} else {
ArchiveFormat::Tgz
}
}
pub fn package_installer(
params: InstallerPackageParams,
) -> Result<InstallerPackageOutput, InstallerPackagingError> {
if !params.binary_path.is_file() {
return Err(InstallerPackagingError::BinaryNotFound(
params.binary_path.clone(),
));
}
fs::create_dir_all(¶ms.output_dir)?;
let name = archive_filename(¶ms.version, ¶ms.target);
let output_path = params.output_dir.join(&name);
let inner_dir = inner_dir_name(¶ms.version, ¶ms.target);
let bin_name = binary_filename(¶ms.target);
match archive_format(¶ms.target) {
ArchiveFormat::Tgz => {
create_tgz_archive(&output_path, &inner_dir, ¶ms.binary_path, &bin_name)?;
}
ArchiveFormat::Zip => {
create_zip_archive(&output_path, &inner_dir, ¶ms.binary_path, &bin_name)?;
}
}
Ok(InstallerPackageOutput {
archive_path: output_path,
archive_name: name,
})
}
fn create_tgz_archive(
output_path: &Path,
inner_dir: &str,
binary_path: &Path,
bin_name: &str,
) -> Result<(), InstallerPackagingError> {
let output_file = fs::File::create(output_path)?;
let gz_encoder = flate2::write::GzEncoder::new(output_file, flate2::Compression::default());
let mut archive = tar::Builder::new(gz_encoder);
let archive_entry_path = format!("{inner_dir}/{bin_name}");
archive.append_path_with_name(binary_path, &archive_entry_path)?;
let gz_encoder = archive.into_inner()?;
gz_encoder.finish()?;
Ok(())
}
fn create_zip_archive(
output_path: &Path,
inner_dir: &str,
binary_path: &Path,
bin_name: &str,
) -> Result<(), InstallerPackagingError> {
let output_file = fs::File::create(output_path)?;
let mut zip_writer = zip::ZipWriter::new(output_file);
let archive_entry_path = format!("{inner_dir}/{bin_name}");
let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
zip_writer.start_file(&archive_entry_path, options)?;
let mut binary_file = fs::File::open(binary_path)?;
let mut buffer = [0u8; 8192];
loop {
let bytes_read = binary_file.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
zip_writer.write_all(&buffer[..bytes_read])?;
}
zip_writer.finish()?;
Ok(())
}
#[cfg(test)]
#[path = "installer_packaging_tests.rs"]
mod tests;