use anyhow::{anyhow, bail, Context};
use argh::FromArgs;
use bootloader::disk_image::create_disk_image;
use std::{
convert::TryFrom,
fs::{self, File},
io::{self, Seek},
path::{Path, PathBuf},
process::Command,
str::FromStr,
};
type ExitCode = i32;
#[derive(FromArgs)]
struct BuildArguments {
#[argh(option)]
kernel_manifest: PathBuf,
#[argh(option)]
kernel_binary: PathBuf,
#[argh(option, default = "Firmware::All")]
firmware: Firmware,
#[argh(switch)]
run: bool,
#[argh(switch)]
quiet: bool,
#[argh(option)]
features: Vec<String>,
#[argh(option)]
target_dir: Option<PathBuf>,
#[argh(option)]
out_dir: Option<PathBuf>,
}
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
enum Firmware {
Bios,
Uefi,
All,
}
impl FromStr for Firmware {
type Err = FirmwareParseError;
fn from_str(s: &str) -> Result<Self, FirmwareParseError> {
match s.to_ascii_lowercase().as_str() {
"bios" => Ok(Firmware::Bios),
"uefi" => Ok(Firmware::Uefi),
"all" => Ok(Firmware::All),
_other => Err(FirmwareParseError),
}
}
}
impl Firmware {
fn uefi(&self) -> bool {
match self {
Firmware::Bios => false,
Firmware::Uefi | Firmware::All => true,
}
}
fn bios(&self) -> bool {
match self {
Firmware::Bios | Firmware::All => true,
Firmware::Uefi => false,
}
}
}
#[derive(Debug, displaydoc::Display, Eq, PartialEq, Copy, Clone)]
struct FirmwareParseError;
fn main() -> anyhow::Result<()> {
let args: BuildArguments = argh::from_env();
if args.firmware.uefi() {
let build_or_run = if args.run { "run" } else { "build" };
let mut cmd = Command::new(env!("CARGO"));
cmd.arg(build_or_run).arg("--bin").arg("uefi");
cmd.arg("--release");
cmd.arg("--target").arg("x86_64-unknown-uefi");
cmd.arg("--features")
.arg(args.features.join(" ") + " uefi_bin");
cmd.arg("-Zbuild-std=core");
cmd.arg("-Zbuild-std-features=compiler-builtins-mem");
if let Some(target_dir) = &args.target_dir {
cmd.arg("--target-dir").arg(target_dir);
}
if args.quiet {
cmd.arg("--quiet");
}
cmd.env("KERNEL", &args.kernel_binary);
cmd.env("KERNEL_MANIFEST", &args.kernel_manifest);
assert!(cmd.status()?.success());
cmd.arg("--message-format").arg("json");
let output = cmd
.output()
.context("failed to execute kernel build with json output")?;
if !output.status.success() {
return Err(anyhow!("{}", String::from_utf8_lossy(&output.stderr)));
}
let mut executables = Vec::new();
for line in String::from_utf8(output.stdout)
.context("build JSON output is not valid UTF-8")?
.lines()
{
let mut artifact = json::parse(line).context("build JSON output is not valid JSON")?;
if let Some(executable) = artifact["executable"].take_string() {
executables.push(PathBuf::from(executable));
}
}
assert_eq!(executables.len(), 1);
let executable_path = executables.pop().unwrap();
let executable_name = executable_path
.file_stem()
.and_then(|stem| stem.to_str())
.ok_or_else(|| {
anyhow!(
"executable path `{}` has invalid file stem",
executable_path.display()
)
})?;
let kernel_name = args
.kernel_binary
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| {
anyhow!(
"kernel binary path `{}` has invalid file name",
args.kernel_binary.display()
)
})?;
if let Some(out_dir) = &args.out_dir {
let efi_file = out_dir.join(format!("boot-{}-{}.efi", executable_name, kernel_name));
create_uefi_disk_image(&executable_path, &efi_file)
.context("failed to create UEFI disk image")?;
}
}
if args.firmware.bios() {
let mut cmd = Command::new(env!("CARGO"));
cmd.arg("build").arg("--bin").arg("bios");
cmd.arg("--profile").arg("release");
cmd.arg("-Z").arg("unstable-options");
cmd.arg("--target").arg("x86_64-bootloader.json");
cmd.arg("--features")
.arg(args.features.join(" ") + " bios_bin");
cmd.arg("-Zbuild-std=core");
cmd.arg("-Zbuild-std-features=compiler-builtins-mem");
if let Some(target_dir) = &args.target_dir {
cmd.arg("--target-dir").arg(target_dir);
}
if args.quiet {
cmd.arg("--quiet");
}
cmd.env("KERNEL", &args.kernel_binary);
cmd.env("KERNEL_MANIFEST", &args.kernel_manifest);
cmd.env("RUSTFLAGS", "-C opt-level=s");
assert!(cmd.status()?.success());
cmd.arg("--message-format").arg("json");
let output = cmd
.output()
.context("failed to execute kernel build with json output")?;
if !output.status.success() {
return Err(anyhow!("{}", String::from_utf8_lossy(&output.stderr)));
}
let mut executables = Vec::new();
for line in String::from_utf8(output.stdout)
.context("build JSON output is not valid UTF-8")?
.lines()
{
let mut artifact = json::parse(line).context("build JSON output is not valid JSON")?;
if let Some(executable) = artifact["executable"].take_string() {
executables.push(PathBuf::from(executable));
}
}
assert_eq!(executables.len(), 1);
let executable_path = executables.pop().unwrap();
let executable_name = executable_path.file_name().unwrap().to_str().unwrap();
let kernel_name = args.kernel_binary.file_name().unwrap().to_str().unwrap();
let mut output_bin_path = executable_path
.parent()
.unwrap()
.join(format!("boot-{}-{}.img", executable_name, kernel_name));
create_disk_image(&executable_path, &output_bin_path)
.context("Failed to create bootable disk image")?;
if let Some(out_dir) = &args.out_dir {
let file = out_dir.join(output_bin_path.file_name().unwrap());
fs::copy(output_bin_path, &file)?;
output_bin_path = file;
}
if !args.quiet {
println!(
"Created bootable disk image at {}",
output_bin_path.display()
);
}
if args.run {
bios_run(&output_bin_path)?;
}
}
Ok(())
}
fn create_uefi_disk_image(executable_path: &Path, efi_file: &Path) -> anyhow::Result<()> {
fs::copy(&executable_path, &efi_file).context("failed to copy efi file to out dir")?;
let efi_size = fs::metadata(&efi_file)
.context("failed to read metadata of efi file")?
.len();
let fat_file_path = {
const MB: u64 = 1024 * 1024;
let fat_path = efi_file.with_extension("fat");
let fat_file = fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&fat_path)
.context("Failed to create UEFI FAT file")?;
let efi_size_rounded = ((efi_size - 1) / MB + 1) * MB;
fat_file
.set_len(efi_size_rounded)
.context("failed to set UEFI FAT file length")?;
let format_options = fatfs::FormatVolumeOptions::new().volume_label(*b"FOOO ");
fatfs::format_volume(&fat_file, format_options)
.context("Failed to format UEFI FAT file")?;
let partition = fatfs::FileSystem::new(&fat_file, fatfs::FsOptions::new())
.context("Failed to open FAT file system of UEFI FAT file")?;
let root_dir = partition.root_dir();
root_dir.create_dir("efi")?;
root_dir.create_dir("efi/boot")?;
let mut bootx64 = root_dir.create_file("efi/boot/bootx64.efi")?;
bootx64.truncate()?;
io::copy(&mut fs::File::open(&executable_path)?, &mut bootx64)?;
fat_path
};
{
let image_path = efi_file.with_extension("img");
let mut image = fs::OpenOptions::new()
.create(true)
.truncate(true)
.read(true)
.write(true)
.open(&image_path)
.context("failed to create UEFI disk image")?;
let partition_size: u64 = fs::metadata(&fat_file_path)
.context("failed to read metadata of UEFI FAT partition")?
.len();
let image_size = partition_size + 1024 * 64;
image
.set_len(image_size)
.context("failed to set length of UEFI disk image")?;
let mbr = gpt::mbr::ProtectiveMBR::with_lb_size(
u32::try_from((image_size / 512) - 1).unwrap_or(0xFF_FF_FF_FF),
);
mbr.overwrite_lba0(&mut image)
.context("failed to write protective MBR")?;
let block_size = gpt::disk::LogicalBlockSize::Lb512;
let block_size_bytes: u64 = block_size.into();
let mut disk = gpt::GptConfig::new()
.writable(true)
.initialized(false)
.logical_block_size(block_size)
.create_from_device(Box::new(&mut image), None)
.context("failed to open UEFI disk image")?;
disk.update_partitions(Default::default())
.context("failed to initialize GPT partition table")?;
let partition_id = disk
.add_partition("boot", partition_size, gpt::partition_types::EFI, 0)
.context("failed to add boot partition")?;
let partition = disk
.partitions()
.get(&partition_id)
.ok_or_else(|| anyhow!("Partition doesn't exist after adding it"))?;
let created_partition_size: u64 =
(partition.last_lba - partition.first_lba + 1u64) * block_size_bytes;
if created_partition_size != partition_size {
bail!(
"Created partition has invalid size (size is {:?}, expected {})",
created_partition_size,
partition_size
);
}
let start_offset = partition
.bytes_start(block_size)
.context("failed to retrieve partition start offset")?;
disk.write()
.context("failed to write GPT partition table to UEFI image file")?;
image
.seek(io::SeekFrom::Start(start_offset))
.context("failed to seek to boot partiiton start")?;
let bytes_written = io::copy(
&mut File::open(&fat_file_path).context("failed to open fat image")?,
&mut image,
)
.context("failed to write boot partition content")?;
if bytes_written != partition_size {
bail!(
"Invalid number of partition bytes written (expected {}, got {})",
partition_size,
bytes_written
);
}
}
Ok(())
}
fn bios_run(bin_path: &Path) -> anyhow::Result<Option<ExitCode>> {
let mut qemu = Command::new("qemu-system-x86_64");
qemu.arg("-drive")
.arg(format!("format=raw,file={}", bin_path.display()));
qemu.arg("-s");
qemu.arg("--no-reboot");
println!("{:?}", qemu);
let exit_status = qemu.status()?;
let ret = if exit_status.success() {
None
} else {
exit_status.code()
};
Ok(ret)
}