use super::helpers::*;
use std::io::Write;
use std::path::Path;
pub fn cmd_image_android(
file: &Path,
machine_name: Option<&str>,
output: &Path,
json: bool,
) -> Result<(), String> {
let config = parse_and_validate(file)?;
let (name, machine) = super::image_cmd::resolve_machine(&config, machine_name)?;
let zip_data = build_magisk_module(&name, machine, file)?;
std::fs::write(output, &zip_data)
.map_err(|e| format!("write magisk module to {}: {e}", output.display()))?;
if json {
println!(
"{}",
serde_json::json!({
"machine": name,
"output": output.display().to_string(),
"size": zip_data.len(),
"format": "magisk-module",
})
);
} else {
println!(
"Magisk module: {} ({} bytes)",
output.display(),
zip_data.len()
);
println!(" machine: {name}");
println!(" arch: {}", machine.arch);
println!("\nInstall: flash via Magisk Manager or TWRP");
}
Ok(())
}
pub fn generate_init_rc(hostname: &str) -> String {
format!(
r#"# Forjar convergence service for {hostname}
service forjar /system/bin/forjar apply --yes -f /system/etc/forjar/forjar.yaml
class late_start
user root
group root
oneshot
disabled
on property:sys.boot_completed=1
start forjar
"#
)
}
fn generate_module_prop(name: &str) -> String {
format!(
r#"id=forjar-{name}
name=Forjar IaC Agent ({name})
version=1.0.0
versionCode=1
author=forjar
description=Infrastructure convergence agent for {name}
minMagisk=25000
"#
)
}
fn generate_post_fs_data() -> &'static str {
r#"#!/system/bin/sh
# Forjar Magisk module — post-fs-data hook
# Ensure forjar config directory exists
mkdir -p /data/forjar
# Copy marker if first boot
if [ ! -f /data/forjar/.firstboot-done ]; then
log -t forjar "First boot detected, convergence will run at boot_completed"
fi
"#
}
fn generate_service_sh() -> &'static str {
r#"#!/system/bin/sh
# Forjar Magisk module — boot service
MODDIR="${0%/*}"
FORJAR="$MODDIR/system/bin/forjar"
CONFIG="$MODDIR/system/etc/forjar/forjar.yaml"
if [ ! -f /data/forjar/.firstboot-done ]; then
log -t forjar "Running first-boot convergence"
"$FORJAR" apply --yes -f "$CONFIG" 2>&1 | while read -r line; do
log -t forjar "$line"
done
touch /data/forjar/.firstboot-done
log -t forjar "First-boot convergence complete"
fi
"#
}
fn build_magisk_module(
name: &str,
machine: &crate::core::types::Machine,
config_file: &Path,
) -> Result<Vec<u8>, String> {
let mut buf = Vec::new();
{
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut buf));
let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
zip.start_file("module.prop", options)
.map_err(|e| format!("zip module.prop: {e}"))?;
zip.write_all(generate_module_prop(name).as_bytes())
.map_err(|e| format!("write module.prop: {e}"))?;
zip.start_file("post-fs-data.sh", options)
.map_err(|e| format!("zip post-fs-data.sh: {e}"))?;
zip.write_all(generate_post_fs_data().as_bytes())
.map_err(|e| format!("write post-fs-data.sh: {e}"))?;
zip.start_file("service.sh", options)
.map_err(|e| format!("zip service.sh: {e}"))?;
zip.write_all(generate_service_sh().as_bytes())
.map_err(|e| format!("write service.sh: {e}"))?;
zip.start_file("system/etc/init/forjar.rc", options)
.map_err(|e| format!("zip init.rc: {e}"))?;
zip.write_all(generate_init_rc(&machine.hostname).as_bytes())
.map_err(|e| format!("write init.rc: {e}"))?;
let config_content =
std::fs::read_to_string(config_file).map_err(|e| format!("read config: {e}"))?;
zip.start_file("system/etc/forjar/forjar.yaml", options)
.map_err(|e| format!("zip config: {e}"))?;
zip.write_all(config_content.as_bytes())
.map_err(|e| format!("write config: {e}"))?;
let stub = format!(
"#!/system/bin/sh\necho 'forjar stub — cross-compile for {} and replace this file'\nexit 1\n",
machine.arch
);
zip.start_file("system/bin/forjar", options)
.map_err(|e| format!("zip binary stub: {e}"))?;
zip.write_all(stub.as_bytes())
.map_err(|e| format!("write binary stub: {e}"))?;
zip.start_file("customize.sh", options)
.map_err(|e| format!("zip customize.sh: {e}"))?;
zip.write_all(generate_customize_sh().as_bytes())
.map_err(|e| format!("write customize.sh: {e}"))?;
zip.finish().map_err(|e| format!("finalize zip: {e}"))?;
}
Ok(buf)
}
fn generate_customize_sh() -> &'static str {
r#"#!/system/bin/sh
# Forjar Magisk module installer
ui_print "Installing Forjar IaC agent..."
set_perm "$MODPATH/system/bin/forjar" 0 0 0755
set_perm_recursive "$MODPATH/system/etc/forjar" 0 0 0755 0644
mkdir -p /data/forjar
ui_print "Forjar will run convergence on next boot"
"#
}