[go: up one dir, main page]

blob: 193fa109d2572e0d39f1af7f5acfe2d2822c7cb7 [file] [log] [blame]
mod arch;
mod build_mode;
mod config;
mod gen_disk;
mod package;
mod qemu;
mod secure_boot;
mod shim;
mod vboot;
use anyhow::{anyhow, Result};
use arch::Arch;
use argh::FromArgs;
use camino::{Utf8Path, Utf8PathBuf};
use command_run::Command;
use config::Config;
use fs_err as fs;
use package::Package;
use qemu::{Display, Qemu, VarAccess};
use std::{env, process};
/// Tools for crdyboot.
#[derive(FromArgs, PartialEq, Debug)]
pub struct Opt {
/// action to run
#[argh(subcommand)]
action: Action,
}
#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand)]
enum Action {
Setup(SetupAction),
Check(CheckAction),
Format(FormatAction),
Lint(LintAction),
Test(TestAction),
Build(BuildAction),
PrepDisk(PrepDiskAction),
UpdateDisk(UpdateDiskAction),
Qemu(QemuAction),
BuildEnroller(BuildEnrollerAction),
Writedisk(WritediskAction),
GenVbootReturnCodeStrings(GenVbootReturnCodeStringsAction),
}
/// Build crdyboot.
#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "build")]
struct BuildAction {}
/// Build enroller.
#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "build-enroller")]
struct BuildEnrollerAction {}
/// Check formating, lint, test, and build.
#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "check")]
struct CheckAction {}
/// Run "cargo fmt" on all the code.
#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "fmt")]
struct FormatAction {
/// don't format the code, just check if it's already formatted
#[argh(switch)]
check: bool,
}
/// Modify an existing CloudReady build to insert crdyboot.
#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "update-disk")]
struct UpdateDiskAction {}
/// Run "cargo clippy" on all the code.
#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "lint")]
struct LintAction {}
/// Sign shim and the kernel partitions.
#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "prep-disk")]
struct PrepDiskAction {}
/// Initialize the workspace.
#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "setup")]
struct SetupAction {
/// path of the reven disk image to copy.
#[argh(positional)]
disk_image: Option<Utf8PathBuf>,
}
struct Miri(bool);
/// Run "cargo test" in the vboot project.
#[derive(FromArgs, PartialEq, Debug, Default)]
#[argh(subcommand, name = "test")]
struct TestAction {
/// disable miri tests
#[argh(switch)]
no_miri: bool,
}
/// Run crdyboot under qemu.
#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "qemu")]
struct QemuAction {
/// use 32-bit UEFI instead of 64-bit
#[argh(switch)]
ia32: bool,
/// enable secure boot
#[argh(switch)]
secure_boot: bool,
/// type of qemu display to use none, gtk, sdl (default=sdl)
#[argh(option, default = "Display::Sdl")]
display: Display,
}
/// Write the disk binary to a USB with `writedisk`.
#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "writedisk")]
struct WritediskAction {}
/// Regenerate vboot/src/return_codes.rs.
#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "gen-vboot-return-code-strings")]
struct GenVbootReturnCodeStringsAction {}
fn run_cargo_deny() -> Result<()> {
// Check if cargo-deny is installed, and install it if not.
if Command::with_args("cargo", &["deny", "--version"])
.enable_capture()
.run()
.is_err()
{
Command::with_args("cargo", &["install", "--locked", "cargo-deny"])
.run()?;
}
// Run cargo-deny. This uses the config in `.deny.toml`.
Command::with_args("cargo", &["deny", "check"]).run()?;
Ok(())
}
fn run_check(conf: &Config) -> Result<()> {
run_cargo_deny()?;
run_rustfmt(&FormatAction { check: true })?;
run_tests(&Default::default())?;
run_crdyboot_build(conf)?;
run_clippy(conf)
}
/// Add cargo features to a command. Does nothing if `features` is empty.
fn add_cargo_features_args(cmd: &mut Command, features: &[&str]) {
if !features.is_empty() {
cmd.add_args(&["--features", &features.join(",")]);
}
}
fn run_uefi_build(conf: &Config, package: Package) -> Result<()> {
let features = conf.get_package_features(package);
let build_mode = conf.build_mode();
for target in Arch::all_targets() {
let mut cmd = Command::with_args(
"cargo",
&[
"build",
"--package",
package.name(),
"-Zbuild-std=core,compiler_builtins,alloc",
"-Zbuild-std-features=compiler-builtins-mem",
"--target",
target,
],
);
add_cargo_features_args(&mut cmd, &features);
cmd.add_args(build_mode.cargo_args());
cmd.run()?;
}
Ok(())
}
fn run_crdyboot_build(conf: &Config) -> Result<()> {
run_uefi_build(conf, Package::Crdyboot)
}
pub fn update_local_repo(path: &Utf8Path, url: &str, rev: &str) -> Result<()> {
// Clone repo if not already cloned, otherwise just fetch.
if path.exists() {
Command::with_args("git", &["-C", path.as_str(), "fetch"]).run()?;
} else {
Command::with_args("git", &["clone", url, path.as_str()]).run()?;
}
// Check out a known-working commit.
Command::with_args("git", &["-C", path.as_str(), "checkout", rev]).run()?;
// Init/update submodules.
Command::with_args(
"git",
&["-C", path.as_str(), "submodule", "update", "--init"],
)
.run()?;
Ok(())
}
fn run_build_enroller(conf: &Config) -> Result<()> {
run_uefi_build(conf, Package::Enroller)?;
gen_disk::gen_enroller_disk(conf)
}
fn copy_file<S, D>(src: S, dst: D) -> Result<()>
where
S: AsRef<Utf8Path>,
D: AsRef<Utf8Path>,
{
let src = src.as_ref();
let dst = dst.as_ref();
println!("copy {} to {}", src, dst);
fs::copy(src, dst)?;
Ok(())
}
fn run_rustfmt(action: &FormatAction) -> Result<()> {
let mut cmd = Command::with_args("cargo", &["fmt", "--all"]);
if action.check {
cmd.add_args(&["--", "--check"]);
}
cmd.run()?;
Ok(())
}
fn run_prep_disk(conf: &Config) -> Result<()> {
shim::update_shim(conf)?;
// Sign both kernel partitions.
gen_disk::sign_kernel_partition(conf, "KERN-A")?;
gen_disk::sign_kernel_partition(conf, "KERN-B")
}
fn run_clippy_for_package(conf: &Config, package: Package) -> Result<()> {
let mut cmd =
Command::with_args("cargo", &["clippy", "--package", package.name()]);
// Use a UEFI target for everything but xtask. This gives slightly
// better coverage (for example, third_party/malloc.rs is not
// included on the host target), and is required in newer versions
// of uefi-rs due to `eh_personality` no longer being set.
if package != Package::Xtask {
cmd.add_args(&[
"-Zbuild-std=core,compiler_builtins,alloc",
"-Zbuild-std-features=compiler-builtins-mem",
"--target",
// Arbitrarily choose the 64-bit target.
Arch::X64.uefi_target(),
]);
}
add_cargo_features_args(&mut cmd, &conf.get_package_features(package));
cmd.run()?;
Ok(())
}
fn run_clippy(conf: &Config) -> Result<()> {
for package in Package::all() {
run_clippy_for_package(conf, package)?;
}
Ok(())
}
fn run_tests_for_package(package: Package, miri: Miri) -> Result<()> {
let mut cmd = Command::new("cargo");
if miri.0 {
cmd.add_arg("miri");
cmd.env
.insert("MIRIFLAGS".into(), "-Zmiri-tag-raw-pointers".into());
}
cmd.add_args(&["test", "--package", package.name()]);
cmd.run()?;
Ok(())
}
fn run_tests(action: &TestAction) -> Result<()> {
run_tests_for_package(Package::Xtask, Miri(false))?;
run_tests_for_package(Package::Vboot, Miri(false))?;
run_tests_for_package(Package::Libcrdy, Miri(false))?;
if !action.no_miri {
run_tests_for_package(Package::Vboot, Miri(true))?;
run_tests_for_package(Package::Libcrdy, Miri(true))?;
}
Ok(())
}
fn generate_secure_boot_keys(conf: &Config) -> Result<()> {
secure_boot::generate_key(
&conf.secure_boot_root_key_paths(),
"SecureBootRootTestKey",
)?;
secure_boot::generate_key(
&conf.secure_boot_shim_key_paths(),
"SecureBootShimTestKey",
)?;
let root_key_paths = conf.secure_boot_root_key_paths();
// Generate the PK/KEK and db vars for use with the enroller.
secure_boot::generate_signed_vars(&root_key_paths, "PK")?;
secure_boot::generate_signed_vars(&root_key_paths, "db")
}
fn init_submodules(conf: &Config) -> Result<()> {
Command::with_args(
"git",
&[
"-C",
conf.repo_path().as_str(),
"submodule",
"update",
"--init",
],
)
.run()?;
Ok(())
}
/// Run the enroller in a VM to set up UEFI variables for secure boot.
fn enroll_secure_boot_keys(conf: &Config) -> Result<()> {
for arch in Arch::all() {
let ovmf = conf.ovmf_paths(arch);
// Copy the system OVMF files to a local directory.
// TODO: move these hardcoded paths to the config.
let system_ovmf_dir = Utf8Path::new("/usr/share/OVMF/");
let (system_code, system_vars) = match arch {
Arch::Ia32 => ("OVMF32_CODE_4M.secboot.fd", "OVMF32_VARS_4M.fd"),
Arch::X64 => ("OVMF_CODE_4M.secboot.fd", "OVMF_VARS_4M.fd"),
};
copy_file(system_ovmf_dir.join(system_code), ovmf.code())?;
copy_file(system_ovmf_dir.join(system_vars), ovmf.original_vars())?;
// Keep a copy of the original vars for running QEMU in
// non-secure-boot mode.
copy_file(ovmf.original_vars(), ovmf.secure_boot_vars())?;
// Run the enroller in QEMU to set up secure boot UEFI variables.
let qemu = Qemu::new(ovmf);
qemu.run_disk_image(
&conf.enroller_disk_path(),
VarAccess::ReadWrite,
Display::None,
)?;
}
Ok(())
}
/// Fix build errors caused by a vboot upgrade.
fn clean_futility_build(conf: &Config) -> Result<()> {
Command::with_args(
"make",
&["-C", conf.vboot_reference_path().as_str(), "clean"],
)
.run()?;
Ok(())
}
/// Build futility, the firmware utility executable that is part of
/// vboot_reference.
fn build_futility(conf: &Config) -> Result<()> {
let mut cmd = Command::with_args(
"make",
&[
"-C",
conf.vboot_reference_path().as_str(),
"USE_FLASHROM=0",
conf.futility_executable_path().as_str(),
],
);
// For compatiblity with openssl3, allow use of deprecated
// functions.
cmd.env
.insert("CFLAGS".into(), "-Wno-deprecated-declarations".into());
cmd.run()?;
Ok(())
}
// Run various setup operations. This must be run once before running
// any other xtask commands.
fn run_setup(conf: &Config, action: &SetupAction) -> Result<()> {
init_submodules(conf)?;
if let Some(disk_image) = &action.disk_image {
copy_file(disk_image, conf.disk_path())?;
}
if !conf.disk_path().exists() {
println!("A disk image is needed to continue. Rerun this command");
println!("with the path of a reven disk image, which will be copied");
println!("to a local path.");
process::exit(1);
}
build_futility(conf)?;
generate_secure_boot_keys(conf)?;
run_build_enroller(conf)?;
enroll_secure_boot_keys(conf)?;
// Build and install shim, and sign the kernel partitions with a
// local key.
run_prep_disk(conf)?;
// Generate a disk image used by the `test_load_kernel` vboot test.
gen_disk::gen_vboot_test_disk(conf)
}
fn run_qemu(conf: &Config, action: &QemuAction) -> Result<()> {
let disk = conf.disk_path();
let ovmf = if action.ia32 {
conf.ovmf_paths(Arch::Ia32)
} else {
conf.ovmf_paths(Arch::X64)
};
let mut qemu = Qemu::new(ovmf);
qemu.secure_boot = action.secure_boot;
qemu.run_disk_image(disk, VarAccess::ReadOnly, action.display)
}
fn run_writedisk(conf: &Config) -> Result<()> {
Command::with_args("writedisk", &[conf.disk_path()]).run()?;
Ok(())
}
fn rerun_setup_if_needed(action: &Action, conf: &Config) -> Result<()> {
// Bump this version any time the setup step needs to be re-run.
let current_version = 4;
// Don't run setup if the user is already doing it.
if matches!(action, Action::Setup(_)) {
return Ok(());
}
// Don't try to run setup if the workspace doesn't exist yet.
if !conf.workspace_path().exists() {
return Ok(());
}
// Nothing to do if the version is already high enough.
let existing_version = conf.read_setup_version();
if existing_version >= current_version {
return Ok(());
}
println!(
"Re-running setup: upgrading from {} to {}",
existing_version, current_version
);
// Put any version-specific cleanup operations here.
if conf.read_setup_version() < 4 {
clean_futility_build(conf)?;
}
// End version-specific cleanup operations.
run_setup(conf, &SetupAction { disk_image: None })?;
conf.write_setup_version(current_version)
}
/// Get the repo root path. This assumes this executable is located at
/// <repo>/target/<buildmode>/<exe>.
fn get_repo_path() -> Result<Utf8PathBuf> {
let exe_path = env::current_exe()?;
let repo_path = exe_path
.parent()
.and_then(|path| path.parent())
.and_then(|path| path.parent())
.ok_or_else(|| anyhow!("repo path: not enough parents"))?;
Ok(Utf8Path::from_path(repo_path)
.ok_or_else(|| anyhow!("repo path: not utf-8"))?
.to_path_buf())
}
fn main() -> Result<()> {
let opt: Opt = argh::from_env();
let repo_root = get_repo_path()?;
// Create the config file from the default if it doesn't already exist.
let conf_path = config::config_path(&repo_root);
let default_conf_path = repo_root.join("xtask/default.toml");
if !conf_path.exists() {
copy_file(&default_conf_path, &conf_path)?;
}
let conf = Config::load(&repo_root)?;
// Re-run setup if something has changed that requires it.
rerun_setup_if_needed(&opt.action, &conf)?;
match &opt.action {
Action::Build(_) => run_crdyboot_build(&conf),
Action::BuildEnroller(_) => run_build_enroller(&conf),
Action::Check(_) => run_check(&conf),
Action::Format(action) => run_rustfmt(action),
Action::UpdateDisk(_) => gen_disk::copy_in_crdyboot(&conf),
Action::Lint(_) => run_clippy(&conf),
Action::PrepDisk(_) => run_prep_disk(&conf),
Action::Setup(action) => run_setup(&conf, action),
Action::Test(action) => run_tests(action),
Action::Qemu(action) => run_qemu(&conf, action),
Action::Writedisk(_) => run_writedisk(&conf),
Action::GenVbootReturnCodeStrings(_) => {
vboot::gen_return_code_strings(&conf)
}
}
}