use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{Context, Result};
use arcbox_api::{SetupPhase, SetupState};
use arcbox_core::BootAssetProvider;
use tracing::info;
pub fn find_bundle_contents() -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?;
let mut dir = exe.parent()?;
loop {
let contents = dir;
if contents.join("Resources").is_dir() && contents.join("Info.plist").exists() {
if contents.join("MacOS").join("ArcBox").exists() {
return Some(contents.to_path_buf());
}
}
dir = contents.parent()?;
}
}
pub(super) fn seed_from_bundle(data_dir: &Path) {
let Some(contents) = find_bundle_contents() else {
return;
};
tracing::info!("App bundle detected: {}", contents.display());
let version = arcbox_core::boot_asset_version();
let boot_dst = data_dir.join(format!("boot/{version}"));
let boot_src = contents.join(format!("Resources/assets/{version}"));
if boot_src.join("manifest.json").exists() {
seed_dir_files(
&boot_src,
&boot_dst,
&["manifest.json", "kernel", "rootfs.erofs"],
"boot assets",
);
}
let runtime_src = contents.join("Resources/runtime");
let runtime_dst = data_dir.join("runtime");
if runtime_src.is_dir() {
seed_dir_recursive(&runtime_src, &runtime_dst, "runtime binaries");
}
let agent_src = contents.join("Resources/bin/arcbox-agent");
let agent_dst = data_dir.join("bin/arcbox-agent");
if agent_src.exists() && !agent_dst.exists() {
if let Err(e) = (|| -> std::io::Result<()> {
std::fs::create_dir_all(agent_dst.parent().unwrap())?;
std::fs::copy(&agent_src, &agent_dst)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&agent_dst, std::fs::Permissions::from_mode(0o755))?;
}
Ok(())
})() {
tracing::warn!("Failed to seed agent from bundle: {e}");
} else {
tracing::info!("Seeded arcbox-agent from bundle");
}
}
}
fn seed_dir_files(src: &Path, dst: &Path, files: &[&str], label: &str) {
if let Err(e) = (|| -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
for name in files {
let s = src.join(name);
let d = dst.join(name);
if s.exists() && !d.exists() {
std::fs::copy(&s, &d)?;
}
}
Ok(())
})() {
tracing::warn!("Failed to seed {label} from bundle: {e}");
} else {
tracing::info!("Seeded {label} from bundle");
}
}
fn seed_dir_recursive(src: &Path, dst: &Path, label: &str) {
let result = (|| -> std::io::Result<u32> {
let mut count = 0u32;
for entry in walkdir(src) {
let (rel, is_dir) = entry?;
let d = dst.join(&rel);
if is_dir {
std::fs::create_dir_all(&d)?;
} else if !d.exists() {
if let Some(parent) = d.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::copy(src.join(&rel), &d)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let src_mode = std::fs::metadata(src.join(&rel))?.permissions().mode();
if src_mode & 0o111 != 0 {
let dst_mode = std::fs::metadata(&d)?.permissions().mode();
let new_mode = (dst_mode & !0o111) | (src_mode & 0o111);
std::fs::set_permissions(&d, std::fs::Permissions::from_mode(new_mode))?;
}
}
count += 1;
}
}
Ok(count)
})();
match result {
Ok(0) => tracing::debug!("{label}: already seeded"),
Ok(n) => tracing::info!("Seeded {n} {label} from bundle"),
Err(e) => tracing::warn!("Failed to seed {label} from bundle: {e}"),
}
}
fn walkdir(root: &Path) -> impl Iterator<Item = std::io::Result<(PathBuf, bool)>> {
let mut stack = vec![PathBuf::new()];
let root = root.to_path_buf();
std::iter::from_fn(move || {
while let Some(rel) = stack.pop() {
let abs = root.join(&rel);
let Ok(meta) = std::fs::metadata(&abs) else {
continue;
};
if meta.is_dir() {
match std::fs::read_dir(&abs) {
Ok(entries) => {
for entry in entries.flatten() {
if let Ok(name) = entry.file_name().into_string() {
stack.push(rel.join(name));
}
}
}
Err(e) => return Some(Err(e)),
}
if !rel.as_os_str().is_empty() {
return Some(Ok((rel, true)));
}
} else {
return Some(Ok((rel, false)));
}
}
None
})
}
pub(super) async fn ensure_boot_assets(
data_dir: &Path,
setup_state: &Arc<SetupState>,
) -> Result<()> {
let cache_dir = data_dir.join("boot");
let provider =
BootAssetProvider::new(cache_dir).context("Failed to create boot asset provider")?;
if provider.is_cached() {
tracing::debug!("Boot assets already cached");
return Ok(());
}
setup_state.set_phase(SetupPhase::DownloadingAssets, "Downloading boot assets...");
provider
.get_assets_with_progress(Some(Box::new(|p| {
tracing::info!(
name = %p.name,
current = p.current,
total = p.total,
"Boot asset progress: {:?}",
p.phase
);
})))
.await
.context("Failed to download boot assets")?;
info!("Boot assets downloaded");
Ok(())
}
pub(super) fn ensure_agent_binary(data_dir: &Path) -> Result<()> {
let agent_dest = data_dir.join("bin/arcbox-agent");
if agent_dest.exists() {
return Ok(());
}
let version = arcbox_core::boot_asset_version();
let agent_src = data_dir.join(format!("boot/{version}/arcbox-agent"));
if !agent_src.exists() {
tracing::debug!(
"Agent binary not found in boot cache at {}",
agent_src.display()
);
return Ok(());
}
std::fs::create_dir_all(agent_dest.parent().unwrap())
.context("Failed to create agent bin directory")?;
std::fs::copy(&agent_src, &agent_dest).context("Failed to copy agent binary")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&agent_dest, std::fs::Permissions::from_mode(0o755))
.context("Failed to set agent binary permissions")?;
}
info!("Agent binary installed from boot cache");
Ok(())
}