Skip to content
This repository has been archived by the owner on Sep 1, 2024. It is now read-only.

Commit

Permalink
xtask
Browse files Browse the repository at this point in the history
  • Loading branch information
memN0ps committed Dec 13, 2023
1 parent 82ef8b5 commit fdd7a6c
Show file tree
Hide file tree
Showing 4 changed files with 376 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ resolver = "2"
members = [
"boot",
"hypervisor",
"xtask",
]

[profile.release]
Expand Down
22 changes: 22 additions & 0 deletions xtask/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "xtask"
version = "0.1.0"
edition = "2021"
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
keywords.workspace = true
categories.workspace = true
readme.workspace = true

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lints]
workspace = true

[dependencies]
cfg-if = "1.0"
clap = { version = "4.3", features = ["derive"] }
ctrlc = "3.4"
wsl = "0.1"
186 changes: 186 additions & 0 deletions xtask/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
//! A build and test assist program. To show the usage, run
//!
//! ```shell
//! cargo xtask
//! ```
use bochs::{Bochs, Cpu};
use clap::{Parser, Subcommand};
use std::{
env, fs,
path::{Path, PathBuf},
process::Command,
};
use vmware::Vmware;


type DynError = Box<dyn std::error::Error>;

#[derive(Parser)]
#[command(author, about, long_about = None)]
struct Cli {
/// Build the hypervisor with the release profile
#[arg(short, long)]
release: bool,

#[command(subcommand)]
command: Commands,
}

#[derive(Subcommand)]
enum Commands {
/// Start a Bochs VM with an Intel processor
BochsIntel,
/// Start a Bochs VM with an AMD processor
BochsAmd,
/// Start a VMware VM
Vmware,
}

fn main() {
let cli = Cli::parse();
let result = match &cli.command {
Commands::BochsIntel => start_vm(&Bochs { cpu: Cpu::Intel }, cli.release),
Commands::BochsAmd => start_vm(&Bochs { cpu: Cpu::Amd }, cli.release),
Commands::Vmware => start_vm(&Vmware {}, cli.release),
};
if let Err(e) = result {
eprintln!("{e}");
std::process::exit(-1);
}
}

trait TestVm {
fn deploy(&self, release: bool) -> Result<(), DynError>;
fn run(&self) -> Result<(), DynError>;
}

fn start_vm<T: TestVm>(vm: &T, release: bool) -> Result<(), DynError> {
build_hypervisor(release)?;
extract_samples()?;
vm.deploy(release)?;
vm.run()
}

fn build_hypervisor(release: bool) -> Result<(), DynError> {
// Building vt-rp only is important because we are running xtask, which cannot
// be overwritten while running.
let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let mut command = Command::new(cargo);
let _ = command.args(["build", "--package", "vt-rp"]);
if release {
let _ = command.arg("--release");
}
let ok = command.current_dir(project_root_dir()).status()?.success();
if !ok {
Err("cargo build failed")?;
}
Ok(())
}

fn project_root_dir() -> PathBuf {
// Get the path to the xtask directory and resolve its parent directory.
let root_dir = Path::new(&env!("CARGO_MANIFEST_DIR"))
.ancestors()
.nth(1)
.unwrap()
.to_path_buf();
fs::canonicalize(root_dir).unwrap()
}

fn extract_samples() -> Result<(), DynError> {
if !Path::new("./tests/samples/").exists() {
println!("Extracting sample files...");
let output = UnixCommand::new("7z")
.args(["x", "-o./tests/", "./tests/samples.7z"])
.output()?;
if !output.status.success() {
Err(format!("7z failed: {output:#?}"))?;
}
}
Ok(())
}

fn copy_artifacts_to(image: &str, release: bool) -> Result<(), DynError> {
fn output_dir(release: bool) -> PathBuf {
let mut out_dir = project_root_dir();
out_dir.extend(&["target", "x86_64-unknown-uefi"]);
out_dir.extend(if release { &["release"] } else { &["debug"] });
fs::canonicalize(&out_dir).unwrap()
}

let vtrp_efi = unix_path(&output_dir(release)) + "/vt-rp.efi";
let startup_nsh = unix_path(&project_root_dir()) + "/tests/startup.nsh";
let files = [vtrp_efi, startup_nsh];
for file in &files {
let output = UnixCommand::new("mcopy")
.args(["-o", "-i", image, file, "::/"])
.output()?;
if !output.status.success() {
Err(format!("mcopy failed: {output:#?}"))?;
}
}
Ok(())
}

fn unix_path(path: &Path) -> String {
if cfg!(target_os = "windows") {
let path_str = path.to_str().unwrap().replace('\\', "\\\\");
let output = UnixCommand::new("wslpath")
.args(["-a", &path_str])
.output()
.unwrap();
std::str::from_utf8(&output.stdout)
.unwrap()
.trim()
.to_string()
} else {
path.to_str().unwrap().to_string()
}
}

// Defines [`UnixCommand`] that wraps [`Command`] with `wsl` command on Windows.
// On non-Windows platforms, it is an alias of [`Command`].
cfg_if::cfg_if! {
if #[cfg(windows)] {
struct UnixCommand {
wsl: Command,
program: String,
}

impl UnixCommand {
fn new(program: &str) -> Self {
Self {
wsl: Command::new("wsl"),
program: program.to_string(),
}
}

pub(crate) fn args<I, S>(&mut self, args: I) -> &mut Command
where
I: IntoIterator<Item = S>,
S: AsRef<std::ffi::OsStr>,
{
self.wsl.arg(self.program.clone()).args(args)
}
}
} else {
type UnixCommand = Command;
}
}

#[cfg(test)]
mod tests {
use crate::unix_path;
use std::path::Path;

#[test]
fn test_unix_path() {
if cfg!(target_os = "windows") {
assert_eq!(unix_path(Path::new(r"C:\")), "/mnt/c/");
assert_eq!(unix_path(Path::new("/tmp")), "/mnt/c/tmp");
} else {
assert_eq!(unix_path(Path::new(r"C:\")), r"C:\");
assert_eq!(unix_path(Path::new("/tmp")), "/tmp");
}
}
}
167 changes: 167 additions & 0 deletions xtask/src/vmware.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
use crate::{copy_artifacts_to, DynError, TestVm, UnixCommand};
use std::{
env,
fs::{self},
io::{BufRead, BufReader},
path::Path,
process::{Command, Stdio},
sync::mpsc::channel,
thread,
time::{Duration, SystemTime},
};

pub(crate) struct Vmware {}

impl TestVm for Vmware {
fn deploy(&self, release: bool) -> Result<(), DynError> {
let output = UnixCommand::new("dd")
.args([
"if=/dev/zero",
"of=/tmp/vmware_cd.img",
"bs=1k",
"count=2880",
])
.output()?;
if !output.status.success() {
Err(format!("dd failed: {output:#?}"))?;
}

let output = UnixCommand::new("mformat")
.args(["-i", "/tmp/vmware_cd.img", "-f", "2880", "::"])
.output()?;
if !output.status.success() {
Err(format!("mformat failed: {output:#?}"))?;
}

copy_artifacts_to("/tmp/vmware_cd.img", release)?;

let output = UnixCommand::new("mkisofs")
.args([
"-eltorito-boot",
"vmware_cd.img",
"-no-emul-boot",
"-o",
"/tmp/vmware_cd.iso",
"/tmp/vmware_cd.img",
])
.output()?;
if !output.status.success() {
Err(format!("mkisofs failed: {output:#?}"))?;
}
Ok(())
}

fn run(&self) -> Result<(), DynError> {
let vmrun = if cfg!(target_os = "windows") {
r"C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe"
} else if wsl::is_wsl() {
"/mnt/c/Program Files (x86)/VMware/VMware Workstation/vmrun.exe"
} else {
"vmrun"
};

let vmx_path = if wsl::is_wsl() {
windows_path("./tests/samples/vmware/NoOS_windows.vmx")
} else {
format!("./tests/samples/vmware/NoOS_{}.vmx", env::consts::OS)
};

// Stop the VM if requested. This is best effort and failures are ignored.
let _unused = Command::new(vmrun)
.args(["stop", vmx_path.as_str(), "nogui"])
.output()?;

// If the serial output file exists, delete it to avoid a popup
let log_file = if cfg!(target_os = "windows") {
r"\\wsl$\Ubuntu-22.04\tmp\serial.log"
} else {
"/tmp/serial.log"
};
if Path::new(log_file).exists() {
fs::remove_file(log_file)?;
}

// Start the VM
println!("🕒 Starting a VMware VM");
let product_type = if cfg!(target_os = "macos") {
"fusion"
} else {
"ws"
};
let output = Command::new(vmrun)
.args(["-T", product_type, "start", vmx_path.as_str()])
.output()?;
if !output.status.success() {
Err(format!("vmrun failed: {output:#?}"))?;
}

// Wait until the serial output file is created. Then, enter loop to read it.
while !Path::new(log_file).exists() {
thread::sleep(Duration::from_secs(1));
}

let _unused = thread::spawn(|| {
let output = UnixCommand::new("tail")
.args(["-f", "/tmp/serial.log"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();

let now = SystemTime::now();

// Read and print stdout as they come in. This does not return.
let reader = BufReader::new(output.stdout.unwrap());
reader
.lines()
.map_while(std::result::Result::ok)
.for_each(|line| {
println!("{:>4}: {line}\r", now.elapsed().unwrap_or_default().as_secs());
});
});

println!("🕒 Please select 'EFI Internal Shell (Unsupported option)' on VMware...");
let (tx, rx) = channel();
ctrlc::set_handler(move || tx.send(()).unwrap())?;
rx.recv()?;

// Stop the VM if requested. This is best effort and failures are ignored.
println!("🕒 Shutting down the VM");
let _unused = Command::new(vmrun)
.args(["stop", vmx_path.as_str(), "nogui"])
.output()?;

Ok(())
}
}

fn windows_path(path: &str) -> String {
if wsl::is_wsl() {
let output = UnixCommand::new("wslpath")
.args(["-a", "-w", path])
.output()
.unwrap();
assert!(output.status.success());
std::str::from_utf8(&output.stdout)
.unwrap()
.trim()
.to_string()
} else {
path.to_string()
}
}

#[cfg(test)]
mod tests {
use crate::vmware::windows_path;

#[test]
fn test_windows_path() {
if cfg!(target_os = "windows") {
assert_eq!(windows_path(r"C:\"), r"C:\");
assert_eq!(windows_path("/mnt/c/tmp"), "/mnt/c/tmp");
} else {
assert_eq!(windows_path("/tmp"), r"\\wsl.localhost\Ubuntu-22.04\tmp");
}
}
}

0 comments on commit fdd7a6c

Please sign in to comment.