ableos/repbuild/src/main.rs

488 lines
15 KiB
Rust

use {
derive_more::Display,
error_stack::{bail, report, Context, Report, Result, ResultExt},
fatfs::{FileSystem, FormatVolumeOptions, FsOptions, ReadWriteSeek},
std::{
fmt::Display,
fs::{self, File},
io::{self, Write},
path::Path,
process::{exit, Command},
},
};
fn main() -> Result<(), Error> {
let mut args = std::env::args();
args.next();
// let disk_meta = fs::metadata("target/disk.img").unwrap();
// let config_meta = fs::metadata("system.toml").unwrap();
// if disk_meta.modified().unwrap() < config_meta.modified().unwrap() {
// // TODO: work on adding in system.toml support
// // TODO: rebuild the disk
// }
match args.next().as_deref() {
Some("build" | "b") => {
let mut release = false;
let mut target = Target::X86_64;
for arg in args {
if arg == "-r" || arg == "--release" {
release = true;
} else if arg == "rv64" || arg == "riscv64" || arg == "riscv64-virt" {
target = Target::Riscv64Virt;
} else if arg == "arm64" || arg == "aarch64" || arg == "aarch64-virt" {
target = Target::Aarch64;
} else {
return Err(report!(Error::InvalidSubCom));
}
}
assemble()?;
build(release, target).change_context(Error::Build)
}
Some("run" | "r") => {
let mut release = false;
let mut target = Target::X86_64;
for arg in args {
if arg == "-r" || arg == "--release" {
release = true;
} else if arg == "rv64" || arg == "riscv64" || arg == "riscv64-virt" {
target = Target::Riscv64Virt;
} else if arg == "arm64" || arg == "aarch64" || arg == "aarch64-virt" {
target = Target::Aarch64;
} else {
return Err(report!(Error::InvalidSubCom));
}
}
assemble()?;
build(release, target)?;
run(release, target)
}
Some("help" | "h") => {
println!(concat!(
"AbleOS RepBuild\n",
"Subcommands:\n",
" build (b): Build a bootable disk image\n",
" help (h): Print this message\n",
" run (r): Build and run AbleOS in QEMU\n\n",
"Options for build and run:\n",
" -r: build in release mode",
" [target]: sets target"
),);
Ok(())
}
_ => Err(report!(Error::InvalidSubCom)),
}
}
fn assemble() -> Result<(), Error> {
match std::fs::create_dir("target/test-programs") {
Ok(_) => (),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => (),
Err(e) => return Err(Report::new(e).change_context(Error::Io)),
}
for entry in std::fs::read_dir("sysdata/test-programs")
.map_err(Report::from)
.change_context(Error::Io)?
{
let entry = entry.map_err(Report::from).change_context(Error::Io)?;
if !entry
.file_type()
.map_err(Report::from)
.change_context(Error::Io)?
.is_file()
{
continue;
}
let name = entry.file_name();
let name = name.to_string_lossy();
let name = name.trim_end_matches(".rhai");
let mut out = File::options()
.write(true)
.create(true)
.open(Path::new("target/test-programs").join(format!("{name}.hbf")))
.map_err(Report::from)
.change_context(Error::Io)?;
out.set_len(0)
.map_err(Report::from)
.change_context(Error::Io)?;
hbasm::assembler(&mut out, |engine| engine.run_file(entry.path()))
.map_err(|e| report!(Error::Assembler).attach_printable(e.to_string()))?;
}
Ok(())
}
fn get_fs() -> Result<FileSystem<impl ReadWriteSeek>, io::Error> {
let filename = "sysdata/system_config.toml";
// Read the contents of the file using a `match` block
// to return the `data: Ok(c)` as a `String`
// or handle any `errors: Err(_)`.
let contents = match fs::read_to_string(filename) {
// If successful return the files text as `contents`.
// `c` is a local variable.
Ok(c) => c,
// Handle the `error` case.
Err(_) => {
// Write `msg` to `stderr`.
eprintln!("Could not read file `{}`", filename);
// Exit the program with exit code `1`.
exit(1);
}
};
use toml::Value;
let mut limine_str = String::new();
let mut data: Value = toml::from_str(&contents).unwrap();
let boot_table = data.get_mut("boot");
let limine_table = boot_table.unwrap().get_mut("limine").unwrap();
let default_entry = limine_table.get("default_entry").unwrap();
let timeout = limine_table.get("timeout").unwrap();
let interface_resolution = limine_table.get("interface_resolution").unwrap();
let verbose = limine_table.get("verbose").unwrap();
let term_wallpaper = limine_table.get("term_wallpaper").unwrap();
let vb_post = match verbose.as_bool().unwrap() {
true => "yes",
false => "no",
};
let term_background = limine_table
.get("term_backdrop")
.unwrap_or(&Value::Integer(0));
let base = format!(
"DEFAULT_ENTRY={}
TIMEOUT={}
VERBOSE={}
INTERFACE_RESOLUTION={}
# Terminal related settings
TERM_WALLPAPER={}
TERM_BACKDROP={}
",
default_entry,
timeout,
vb_post,
interface_resolution,
term_wallpaper.as_str().unwrap(),
term_background
);
limine_str.push_str(&base);
let boot_entries = limine_table.as_table_mut().unwrap();
let mut real_boot_entries = boot_entries.clone();
for (key, value) in boot_entries.into_iter() {
if !value.is_table() {
real_boot_entries.remove(key);
}
}
for (name, mut value) in real_boot_entries {
let comment = value.get("comment").unwrap();
let protocol = value.get("protocol").unwrap();
let resolution = value.get("resolution").unwrap();
let kernel_path = value.get("kernel_path").unwrap();
let kernel_cmdline = value.get("kernel_cmdline").unwrap();
let text_entry = format!(
"
:{}
COMMENT={}
PROTOCOL={}
RESOLUTION={}
KERNEL_PATH={}
KERNEL_CMDLINE={}
",
name,
comment.as_str().unwrap(),
protocol.as_str().unwrap(),
resolution.as_str().unwrap(),
kernel_path.as_str().unwrap(),
kernel_cmdline,
);
limine_str.push_str(&text_entry);
let modules = value.get_mut("modules").unwrap().as_table_mut().unwrap();
// let mut real_modules = modules.clone();
for (key, value) in modules.into_iter() {
if value.is_table() {
let path = value.get("path").expect("You must have `path` as a value");
let default_value = Value::String("".into());
let cmd_line = value.get("cmd_line").unwrap_or(&default_value);
let a = format!(
" MODULE_PATH={}
MODULE_CMDLINE={}\n\n",
path.as_str().unwrap(),
cmd_line
);
limine_str.push_str(&a);
}
}
}
let mut img = File::options()
.read(true)
.write(true)
.create(true)
.open(Path::new("target/disk.img"))?;
img.set_len(1024 * 1024 * 64)?;
fatfs::format_volume(&mut img, FormatVolumeOptions::new())?;
let fs = FileSystem::new(img, FsOptions::new())?;
let bootdir = fs.root_dir().create_dir("efi")?.create_dir("boot")?;
let mut f = fs.root_dir().create_file("limine.cfg")?;
let a = f.write(limine_str.as_bytes())?;
drop(f);
io::copy(
&mut File::open("limine/BOOTX64.EFI")
.map_err(Report::from)
.attach_printable("Copying Limine (x86_64): have you pulled the submodule?")?,
&mut bootdir.create_file("bootx64.efi")?,
)?;
io::copy(
&mut File::open("limine/BOOTAA64.EFI")
.map_err(Report::from)
.attach_printable("Copying Limine (ARM): have you pulled the submodule?")?,
&mut bootdir.create_file("bootaa64.efi")?,
)?;
// TODO: Remove this and replace it with pulling executables from the system_config
for fpath in [
"sysdata/background.bmp",
"target/test-programs/failure.hbf",
"target/test-programs/ecall.hbf",
"target/test-programs/main.hbf",
"target/test-programs/serial_driver.hbf",
"target/test-programs/vfs_test.hbf",
"target/test-programs/sds_test.hbf",
"target/test-programs/limine_framebuffer_driver.hbf",
"target/test-programs/keyboard_driver.hbf",
] {
let path = Path::new(fpath);
io::copy(
&mut File::open(path)?,
&mut fs
.root_dir()
.create_file(&path.file_name().unwrap().to_string_lossy())?,
)?;
}
drop(bootdir);
Ok(fs)
}
fn build(release: bool, target: Target) -> Result<(), Error> {
let fs = get_fs().change_context(Error::Io)?;
let mut com = Command::new("cargo");
com.current_dir("kernel");
com.args(["b"]);
if release {
com.arg("-r");
}
if target == Target::Riscv64Virt {
com.args(["--target", "targets/riscv64-virt-ableos.json"]);
}
if target == Target::Aarch64 {
com.args(["--target", "targets/aarch64-virt-ableos.json"]);
}
match com.status() {
Ok(s) if s.code() != Some(0) => bail!(Error::Build),
Err(e) => bail!(report!(e).change_context(Error::Build)),
_ => (),
}
let mut path: String = "kernel".to_string();
let kernel_dir = match target {
Target::X86_64 => {
path.push_str("_x86-64");
"target/x86_64-ableos"
}
Target::Riscv64Virt => "target/riscv64-virt-ableos",
Target::Aarch64 => {
path.push_str("_aarch64");
"target/aarch64-virt-ableos"
}
};
(|| -> std::io::Result<_> {
io::copy(
&mut File::open(
Path::new(kernel_dir)
.join(if release { "release" } else { "debug" })
.join("kernel"),
)?,
&mut fs.root_dir().create_file(&path)?,
)
.map(|_| ())
})()
.map_err(Report::from)
.change_context(Error::Io)
}
fn run(release: bool, target: Target) -> Result<(), Error> {
let mut com = match target {
Target::X86_64 => Command::new("qemu-system-x86_64"),
Target::Riscv64Virt => Command::new("qemu-system-riscv64"),
Target::Aarch64 => Command::new("qemu-system-aarch64"),
};
let ovmf_path = fetch_ovmf(target);
match target {
Target::X86_64 => {
#[rustfmt::skip]
com.args([
"-bios", &ovmf_path.change_context(Error::OvmfFetch)?,
"-drive", "file=target/disk.img,format=raw",
"-m", "4G",
"-smp", "cores=4",
// "-enable-kvm",
"-cpu", "Broadwell-v4"
]);
}
Target::Riscv64Virt => {
#[rustfmt::skip]
com.args([
"-M", "virt",
"-m", "128M",
"-serial", "stdio",
"-kernel",
if release {
"target/riscv64-virt-ableos/release/kernel"
} else {
"target/riscv64-virt-ableos/debug/kernel"
}
]);
}
Target::Aarch64 => {
#[rustfmt::skip]
com.args([
"-M", "virt",
"-cpu", "cortex-a72",
"-device", "ramfb",
"-device", "qemu-xhci",
"-device", "usb-kbd",
"-m", "2G",
"-bios", &ovmf_path.change_context(Error::OvmfFetch)?,
"-drive", "file=target/disk.img,format=raw",
]);
}
}
match com
.status()
.map_err(Report::from)
.change_context(Error::ProcessSpawn)?
{
s if s.success() => Ok(()),
s => Err(report!(Error::Qemu(s.code()))),
}
}
fn fetch_ovmf(target: Target) -> Result<String, OvmfFetchError> {
let (ovmf_url, ovmf_path) = match target {
Target::X86_64 => (
"https://retrage.github.io/edk2-nightly/bin/RELEASEX64_OVMF.fd",
"target/RELEASEX64_OVMF.fd",
),
Target::Riscv64Virt => return Err(OvmfFetchError::Empty.into()),
Target::Aarch64 => (
"https://retrage.github.io/edk2-nightly/bin/RELEASEAARCH64_QEMU_EFI.fd",
"target/RELEASEAARCH64_QEMU_EFI.fd",
),
};
let mut file = match std::fs::metadata(ovmf_path) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => std::fs::OpenOptions::new()
.create(true)
.write(true)
.read(true)
.open(ovmf_path)
.map_err(Report::from)
.change_context(OvmfFetchError::Io)?,
Ok(_) => return Ok(ovmf_path.to_owned()),
Err(e) => return Err(report!(e).change_context(OvmfFetchError::Io)),
};
let mut bytes = reqwest::blocking::get(ovmf_url)
.map_err(Report::from)
.change_context(OvmfFetchError::Fetch)?;
bytes
.copy_to(&mut file)
.map_err(Report::from)
.change_context(OvmfFetchError::Io)?;
Ok(ovmf_path.to_owned())
}
#[derive(Debug, Display)]
enum OvmfFetchError {
#[display(fmt = "Failed to fetch OVMF package")]
Fetch,
#[display(fmt = "No OVMF package available")]
Empty,
#[display(fmt = "IO Error")]
Io,
}
impl Context for OvmfFetchError {}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Target {
X86_64,
Riscv64Virt,
Aarch64,
}
#[derive(Debug, Display)]
enum Error {
#[display(fmt = "Failed to build the kernel")]
Build,
#[display(fmt = "Missing or invalid subcommand (available: build, run)")]
InvalidSubCom,
#[display(fmt = "IO Error")]
Io,
#[display(fmt = "Failed to spawn a process")]
ProcessSpawn,
#[display(fmt = "Failed to fetch UEFI firmware")]
OvmfFetch,
#[display(fmt = "Failed to assemble Holey Bytes code")]
Assembler,
#[display(fmt = "QEMU Error: {}", "fmt_qemu_err(*_0)")]
Qemu(Option<i32>),
}
impl Context for Error {}
fn fmt_qemu_err(e: Option<i32>) -> impl Display {
struct W(Option<i32>);
impl Display for W {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(c) = self.0 {
c.fmt(f)
} else {
f.write_str("Interrupted by signal")
}
}
}
W(e)
}