use std::path::{Path, PathBuf};
use arcbox_vz::{
MacAuxiliaryStorage, MacGraphicsDeviceConfiguration, MacMachineIdentifier, MacOSBootLoader,
MacOSInstaller, MacOSRestoreImage, MacPlatform, StorageDeviceConfiguration,
VirtualMachineConfiguration, min_cpu_count, min_memory_size,
};
use chrono::Utc;
use super::download::download_ipsw;
use super::image::{MacImage, MacImageManager, MacImageMeta};
use crate::error::{CoreError, Result};
#[derive(Debug, Clone)]
pub enum PullSource {
LocalIpsw(PathBuf),
Latest,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PullPhase {
Download,
Install,
}
const INSTALL_MIN_MEMORY: u64 = 8 * GIB;
const GIB: u64 = 1024 * 1024 * 1024;
impl MacImageManager {
#[allow(
clippy::future_not_send,
reason = "drives Virtualization.framework through ObjC pointers and the VM's dispatch queue, which are inherently !Send and held across await; the caller drives it on a single thread"
)]
pub async fn install_from_ipsw(
&self,
ipsw: &Path,
name: &str,
disk_gb: u64,
on_progress: impl FnMut(f64),
) -> Result<MacImage> {
let dir = self.image_dir(name)?;
if dir.join("meta.json").exists() {
return Err(CoreError::already_exists(format!("macOS image '{name}'")));
}
let restore = MacOSRestoreImage::load_from_url(ipsw).await?;
let reqs = restore.requirements()?;
if !reqs.hardware_model.is_supported() {
return Err(CoreError::macos(
"restore image hardware model is not supported on this host",
));
}
std::fs::create_dir_all(&dir)?;
let disk_path = dir.join("disk.img");
let aux_path = dir.join("aux.img");
std::fs::write(
dir.join("hwmodel.bin"),
reqs.hardware_model.data_representation(),
)?;
let machine_id = MacMachineIdentifier::new()?;
std::fs::write(dir.join("machine-id.bin"), machine_id.data_representation())?;
let disk = std::fs::File::create(&disk_path)?;
disk.set_len(disk_gb * GIB)?;
drop(disk);
let _ = std::fs::remove_file(&aux_path);
let aux = MacAuxiliaryStorage::create(&aux_path, &reqs.hardware_model, true)?;
let platform = MacPlatform::new(&reqs.hardware_model, &machine_id, &aux)?;
let cpus = usize::try_from(reqs.minimum_cpu_count.max(min_cpu_count())).unwrap_or(1);
let memory = reqs
.minimum_memory_size
.max(INSTALL_MIN_MEMORY)
.max(min_memory_size());
let mut config = VirtualMachineConfiguration::new()?;
config
.set_cpu_count(cpus)
.set_memory_size(memory)
.set_platform(platform)
.set_boot_loader(MacOSBootLoader::new()?)
.add_storage_device(StorageDeviceConfiguration::disk_image(&disk_path, false)?)
.add_graphics_device(MacGraphicsDeviceConfiguration::new(1920, 1080, 80)?);
config.validate()?;
let vm = config.build()?;
let installer = MacOSInstaller::new(&vm, ipsw)?;
installer.install(&vm, on_progress).await?;
let _ = vm.stop().await;
let meta = MacImageMeta {
name: name.to_string(),
source: Some(ipsw.display().to_string()),
stream: None,
version: None,
os_version: None,
os_build: None,
runner_version: None,
minimum_cpu_count: reqs.minimum_cpu_count,
minimum_memory_mib: memory / (1024 * 1024),
disk_gb,
created_at: Utc::now(),
};
self.write_meta(&meta)?;
self.get(name)
}
#[allow(
clippy::future_not_send,
reason = "drives Virtualization.framework through !Send ObjC pointers held across await; the caller drives it on a single thread"
)]
pub async fn pull(
&self,
source: PullSource,
name: &str,
disk_gb: u64,
mut on_progress: impl FnMut(PullPhase, f64),
) -> Result<MacImage> {
let ipsw = match source {
PullSource::LocalIpsw(path) => path,
PullSource::Latest => {
let restore = MacOSRestoreImage::latest_supported().await?;
let url = restore
.url()
.ok_or_else(|| CoreError::macos("latest restore image has no download URL"))?;
drop(restore);
download_ipsw(&url, &self.cache_dir(), None, |frac| {
on_progress(PullPhase::Download, frac);
})
.await?
}
};
self.install_from_ipsw(&ipsw, name, disk_gb, |frac| {
on_progress(PullPhase::Install, frac);
})
.await
}
}