use anyhow::{Context, Result};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
fn main() -> Result<()> {
let args = parse_args()?;
bundle(&args)
}
struct Args {
archive: PathBuf,
output: PathBuf,
fuselage_args: Vec<String>,
build_dir: Option<PathBuf>,
exist_ok: bool,
keep: bool,
}
fn parse_args() -> Result<Args> {
let raw: Vec<String> = std::env::args().skip(1).collect();
let mut archive: Option<PathBuf> = None;
let mut output: Option<PathBuf> = None;
let mut build_dir: Option<PathBuf> = None;
let mut exist_ok = false;
let mut keep = false;
let mut fuselage_args: Vec<String> = Vec::new();
let mut after_dashdash = false;
let mut i = 0;
while i < raw.len() {
let arg = &raw[i];
if after_dashdash {
fuselage_args.push(arg.clone());
} else if arg == "--" {
after_dashdash = true;
} else if let Some(val) = arg.strip_prefix("--archive=") {
archive = Some(PathBuf::from(val));
} else if arg == "--archive" {
i += 1;
let val = raw.get(i).context("--archive requires a value")?;
archive = Some(PathBuf::from(val));
} else if let Some(val) = arg.strip_prefix("--output=") {
output = Some(PathBuf::from(val));
} else if arg == "--output" {
i += 1;
let val = raw.get(i).context("--output requires a value")?;
output = Some(PathBuf::from(val));
} else if let Some(val) = arg.strip_prefix("--build-dir=") {
build_dir = Some(PathBuf::from(val));
} else if arg == "--build-dir" {
i += 1;
let val = raw.get(i).context("--build-dir requires a value")?;
build_dir = Some(PathBuf::from(val));
} else if arg == "--exist-ok" {
exist_ok = true;
} else if arg == "--keep" {
keep = true;
} else {
anyhow::bail!("unrecognised argument: {arg:?}");
}
i += 1;
}
let archive = archive.context("--archive is required")?;
let output = output.context("--output is required")?;
if !archive.is_file() {
anyhow::bail!("archive file not found: {}", archive.display());
}
Ok(Args {
archive,
output,
fuselage_args,
build_dir,
exist_ok,
keep,
})
}
fn bundle(args: &Args) -> Result<()> {
let build_dir = match &args.build_dir {
Some(p) => p.clone(),
None => std::env::temp_dir().join(format!("_fuselage_bundle_{}", std::process::id())),
};
let pre_existed = build_dir.exists();
if pre_existed && !args.exist_ok {
anyhow::bail!(
"build dir already exists: {} (use --exist-ok to allow)",
build_dir.display()
);
}
let we_created = !pre_existed;
if we_created && !args.keep && args.output.starts_with(&build_dir) {
anyhow::bail!(
"--output is inside --build-dir and will be deleted on cleanup; \
use --keep to preserve the build directory, or choose an output path outside it"
);
}
if we_created {
std::fs::create_dir_all(&build_dir)
.with_context(|| format!("failed to create build dir {}", build_dir.display()))?;
}
let result = bundle_in(&build_dir, &args.archive, &args.output, &args.fuselage_args);
if we_created && !args.keep {
if let Err(e) = std::fs::remove_dir_all(&build_dir) {
eprintln!(
"warning: failed to remove build dir {}: {e}",
build_dir.display()
);
}
}
result
}
fn bundle_in(
build_dir: &Path,
archive: &Path,
output: &Path,
fuselage_args: &[String],
) -> Result<()> {
let stub_c = build_dir.join("stub.c");
generate_stub_c(&stub_c, fuselage_args)?;
let stub_bin = build_dir.join("stub");
compile_stub(&stub_c, &stub_bin)?;
assemble(output, &stub_bin, archive)?;
set_executable(output)?;
println!(
"fuselage-bundle: wrote {} ({} bytes)",
output.display(),
std::fs::metadata(output)?.len()
);
Ok(())
}
fn generate_stub_c(dest: &Path, fuselage_args: &[String]) -> Result<()> {
let template = include_str!("stub_template.c");
let args_literal: String = fuselage_args
.iter()
.map(|a| format!(" {},\n", c_string_literal(a)))
.collect();
let source = template.replace(" FUSELAGE_BUNDLE_ARGS\n", &args_literal);
std::fs::write(dest, source).with_context(|| format!("failed to write {}", dest.display()))?;
Ok(())
}
fn c_string_literal(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
out.push_str(&format!("\\x{:02x}", c as u32));
}
c => out.push(c),
}
}
out.push('"');
out
}
fn compile_stub(src: &Path, dest: &Path) -> Result<()> {
let compiler = if which_compiler("musl-gcc") {
"musl-gcc"
} else {
"gcc"
};
let status = Command::new(compiler)
.args(["-static", "-O2", "-s", "-o"])
.arg(dest)
.arg(src)
.status()
.with_context(|| format!("failed to run {compiler} — is it installed?"))?;
if !status.success() {
anyhow::bail!("{compiler} failed to compile {}", src.display());
}
Ok(())
}
fn which_compiler(name: &str) -> bool {
use std::os::unix::fs::PermissionsExt;
std::env::var_os("PATH")
.map(|path| {
std::env::split_paths(&path).any(|dir| {
let candidate = dir.join(name);
std::fs::metadata(&candidate)
.map(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
})
})
.unwrap_or(false)
}
fn assemble(output: &Path, stub: &Path, squashfs: &Path) -> Result<()> {
use std::io::{Read, Seek, SeekFrom};
let stub_bytes =
std::fs::read(stub).with_context(|| format!("failed to read {}", stub.display()))?;
let mut sfs_file = std::fs::File::open(squashfs)
.with_context(|| format!("failed to open {}", squashfs.display()))?;
let mut magic = [0u8; 4];
if sfs_file.read_exact(&mut magic).is_err() || (&magic != b"hsqs" && &magic != b"sqsh") {
anyhow::bail!(
"{}: does not look like a squashfs image (bad magic)",
squashfs.display()
);
}
sfs_file
.seek(SeekFrom::Start(0))
.with_context(|| format!("failed to seek {}", squashfs.display()))?;
let stub_len = stub_bytes.len() as u64;
let pad_len = align_up(stub_len, 4096) - stub_len;
let mut f = std::fs::File::create(output)
.with_context(|| format!("failed to create {}", output.display()))?;
f.write_all(&stub_bytes)
.with_context(|| format!("failed to write stub to {}", output.display()))?;
let padding = vec![0u8; pad_len as usize];
f.write_all(&padding)
.with_context(|| format!("failed to write padding to {}", output.display()))?;
std::io::copy(&mut sfs_file, &mut f)
.with_context(|| format!("failed to write squashfs to {}", output.display()))?;
Ok(())
}
fn align_up(value: u64, align: u64) -> u64 {
(value + align - 1) & !(align - 1)
}
fn set_executable(path: &Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(path)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(path, perms)
.with_context(|| format!("failed to set permissions on {}", path.display()))?;
Ok(())
}