use cargo_metadata::{Message, PackageId};
use clap::Args;
use object::{Object, ObjectSection, ObjectSegment};
use std::{
ffi::OsStr,
path::{Path, PathBuf},
process::{Stdio, exit},
};
use tokio::{process::Command, task::block_in_place};
use crate::errors::CliError;
#[derive(Args, Debug)]
pub struct CargoOpts {
#[arg(
trailing_var_arg = true,
allow_hyphen_values = true,
value_name = "CARGO-OPTIONS"
)]
args: Vec<String>,
}
pub fn cargo_bin() -> std::ffi::OsString {
std::env::var_os("CARGO").unwrap_or_else(|| "cargo".to_owned().into())
}
async fn is_supported_release_channel(cargo_bin: &OsStr) -> bool {
let rustc = Command::new(cargo_bin)
.arg("--version")
.output()
.await
.unwrap();
let rustc = String::from_utf8(rustc.stdout).unwrap();
rustc.contains("nightly") || rustc.contains("-dev")
}
pub struct BuildOutput {
pub elf_artifact: PathBuf,
pub bin_artifact: PathBuf,
pub package_id: PackageId,
}
pub async fn build(path: &Path, opts: CargoOpts) -> Result<Option<BuildOutput>, CliError> {
let cargo = cargo_bin();
if !is_supported_release_channel(&cargo).await {
return Err(CliError::UnsupportedReleaseChannel)?;
}
let mut build_cmd = std::process::Command::new(cargo);
build_cmd
.current_dir(path)
.stdout(Stdio::piped())
.arg("build")
.arg("--message-format")
.arg("json-render-diagnostics");
let mut explicit_target_specified = false;
for arg in &opts.args {
if arg == "--target" || arg.starts_with("--target=") {
explicit_target_specified = true;
break;
}
}
if !explicit_target_specified {
build_cmd.arg("--target").arg("armv7a-vex-v5");
}
build_cmd.args(opts.args);
block_in_place::<_, Result<Option<BuildOutput>, CliError>>(|| {
let mut out = build_cmd.spawn()?;
let reader = std::io::BufReader::new(out.stdout.take().unwrap());
let mut output = None;
for message in Message::parse_stream(reader) {
if let Message::CompilerArtifact(artifact) = message?
&& let Some(elf_artifact_path) = artifact.executable
{
let binary = objcopy(&std::fs::read(&elf_artifact_path)?)?;
let binary_path = elf_artifact_path.with_extension("bin");
std::fs::write(&binary_path, binary)?;
eprintln!(" \x1b[1;92mObjcopy\x1b[0m {binary_path}");
output = Some(BuildOutput {
bin_artifact: binary_path.into_std_path_buf(),
elf_artifact: elf_artifact_path.into_std_path_buf(),
package_id: artifact.package_id,
});
}
}
let status = out.wait()?;
if !status.success() {
exit(status.code().unwrap_or(1));
}
Ok(output)
})
}
pub fn objcopy(elf: &[u8]) -> Result<Vec<u8>, CliError> {
let elf = object::File::parse(elf)?;
let mut loadable_sections = elf
.sections() .filter(|section| {
let Some((section_offset, section_size)) = section.file_range() else {
return false;
};
for segment in elf.segments() {
let (segment_offset, segment_size) = segment.file_range();
if segment_offset <= section_offset
&& segment_offset + segment_size >= section_offset + section_size
{
return true;
}
}
false
})
.collect::<Vec<_>>();
if loadable_sections.is_empty() {
return Ok(Vec::new());
}
loadable_sections.sort_by_key(|section| section.address());
let start_address = loadable_sections.first().unwrap().address();
let end_address = {
let last_section = loadable_sections.last().unwrap();
last_section.address() + last_section.size()
};
let mut binary = vec![0; (end_address - start_address) as usize];
for section in loadable_sections {
let address = section.address();
let start = address - start_address;
let end = (address - start_address) + section.size();
binary[(start as usize)..(end as usize)].copy_from_slice(section.data()?);
}
Ok(binary)
}