diff options
author | Lars Wirzenius <liw@liw.fi> | 2023-08-31 09:04:27 +0300 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2023-09-01 07:01:32 +0300 |
commit | ad1663f151988e3d01df85bc9f796fd3d5fd3b01 (patch) | |
tree | 6c1c4150edb3c8de67f161553b7a7f819a17dc8f | |
parent | 60636d04447a7d03ca4cef8b24c761c7a9030000 (diff) | |
download | ambient-run-ad1663f151988e3d01df85bc9f796fd3d5fd3b01.tar.gz |
feat: actually run a (dummy) build in a VM
Sponsored-by: author
-rw-r--r-- | Cargo.lock | 5 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | ambient-run.md | 27 | ||||
-rw-r--r-- | src/bin/ambient-run.rs | 25 | ||||
-rw-r--r-- | src/error.rs | 3 | ||||
-rw-r--r-- | src/lib.rs | 1 | ||||
-rw-r--r-- | src/project.rs | 13 | ||||
-rw-r--r-- | src/qemu.rs | 220 | ||||
-rw-r--r-- | subplot/ambient-run.rs | 18 | ||||
-rw-r--r-- | subplot/ambient-run.yaml | 4 |
10 files changed, 311 insertions, 6 deletions
@@ -32,6 +32,7 @@ dependencies = [ "subplot-build", "subplotlib", "tar", + "tempfile", "thiserror", ] @@ -1235,9 +1236,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.7.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", "fastrand", @@ -10,6 +10,7 @@ serde = { version = "1.0.182", features = ["derive"] } serde_yaml = "0.9.25" subplotlib = "0.8.0" tar = "0.4.40" +tempfile = "3.8.0" thiserror = "1.0.44" [build-dependencies] diff --git a/ambient-run.md b/ambient-run.md index c5dc188..0387d0e 100644 --- a/ambient-run.md +++ b/ambient-run.md @@ -172,6 +172,33 @@ image: /my/image.qcow2 ## Building with ambient-run ### Smoke test build works + +_Requirement:_ I can do a simple build that just runs the shell +command `echo hello, world` using `ambient-run`. + +_Justification:_ This is the simplest possible project build, and if +it works, then at least the very fundamental parts of `ambient-run` +work, and vice versa. + +_Stakeholder:_ Lars. + +~~~scenario +given an installed ambient-run +given file hello-project.yaml +given image file image.qcow2 specified for test suite +when I run ambient-run build hello-project.yaml --log hello.log +then file hello.log contains "hello, world" +~~~ + +~~~{#hello-project.yaml .file .yaml} +source: . +shell: | + #!/bin/bash + set -xeuo pipefail + echo hello, world +image: image.qcow2 +~~~ + ### Build is given dependencies ### Cache is persistent between builds ### Build gets the resources is demands diff --git a/src/bin/ambient-run.rs b/src/bin/ambient-run.rs index a2d3809..d0ca22b 100644 --- a/src/bin/ambient-run.rs +++ b/src/bin/ambient-run.rs @@ -2,6 +2,7 @@ use ambient_run::{ config::{canonical, default_config_file, Config}, error::AmbientRunError, project::Project, + qemu::Qemu, vdrive::VirtualDriveBuilder, }; use clap::{Parser, Subcommand}; @@ -28,6 +29,7 @@ fn fallible_main() -> Result<(), AmbientRunError> { Command::Config(x) => x.run(&args, &config), Command::Project(x) => x.run(&args, &config), Command::VDrive(x) => x.run(&args, &config), + Command::Build(x) => x.run(&args, &config), } } @@ -53,6 +55,7 @@ enum Command { Project(ProjectCommand), #[clap(name = "vdrive")] VDrive(VDriveCommand), + Build(BuildCommand), } #[derive(Debug, Parser)] @@ -196,3 +199,25 @@ impl VDriveExtractCommand { Ok(()) } } + +#[derive(Debug, Parser)] +struct BuildCommand { + /// Project build specification. + filename: PathBuf, + + /// Build log. + #[clap(long)] + log: Option<PathBuf>, +} + +impl BuildCommand { + fn run(&self, _global: &Args, config: &Config) -> Result<(), AmbientRunError> { + let project = Project::load(&self.filename, config)?; + let mut qemu = Qemu::new(project.image()).with_shell(project.shell()); + if let Some(log) = &self.log { + qemu = qemu.with_log(log); + } + qemu.run()?; + Ok(()) + } +} diff --git a/src/error.rs b/src/error.rs index d3a03ed..0958d35 100644 --- a/src/error.rs +++ b/src/error.rs @@ -10,5 +10,8 @@ pub enum AmbientRunError { Project(#[from] crate::project::ProjectError), #[error(transparent)] + Qemu(#[from] crate::qemu::QemuError), + + #[error(transparent)] VDrive(#[from] crate::vdrive::VirtualDriveError), } @@ -5,4 +5,5 @@ pub mod config; pub mod error; pub mod project; +pub mod qemu; pub mod vdrive; diff --git a/src/project.rs b/src/project.rs index b672867..341b5ad 100644 --- a/src/project.rs +++ b/src/project.rs @@ -38,13 +38,10 @@ impl Project { /// Load named project file, update self with values. pub fn add_from(&mut self, filename: &Path) -> Result<(), ProjectError> { - eprintln!("adding from {}", filename.display()); let bytes = std::fs::read(filename).map_err(|e| ProjectError::Open(filename.into(), e))?; let text = String::from_utf8_lossy(&bytes); - eprintln!("text: {:?}", text); let snippet: ProjectSnippet = serde_yaml::from_str(&text).map_err(|e| ProjectError::Yaml(filename.into(), e))?; - eprintln!("snippet: {:#?}", snippet); if let Some(x) = snippet.source { self.source = x; } @@ -61,6 +58,16 @@ impl Project { pub fn as_yaml(&self) -> Result<String, ProjectError> { serde_yaml::to_string(self).map_err(ProjectError::AsYaml) } + + /// VM image to use for this build. + pub fn image(&self) -> &Path { + self.image.as_ref() + } + + /// Shell snippet to run to build the project. + pub fn shell(&self) -> &str { + self.shell.as_ref() + } } #[derive(Debug, Deserialize)] diff --git a/src/qemu.rs b/src/qemu.rs new file mode 100644 index 0000000..c0aa1b1 --- /dev/null +++ b/src/qemu.rs @@ -0,0 +1,220 @@ +//! Run QEMU. + +use crate::vdrive::{VirtualDrive, VirtualDriveBuilder, VirtualDriveError}; + +use std::{ + fs::{copy, write, File}, + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; + +const OVMF_FD: &str = "/usr/share/ovmf/OVMF.fd"; + +/// A QEMU runner. +#[derive(Debug, Default)] +pub struct Qemu { + image: PathBuf, + log: Option<PathBuf>, + source: Option<PathBuf>, + shell: Option<String>, +} + +impl Qemu { + /// Create a new runner. + pub fn new(image: &Path) -> Self { + Self { + image: image.into(), + ..Default::default() + } + } + + /// Set log file. + pub fn with_log(mut self, log: &Path) -> Self { + self.log = Some(log.into()); + self + } + + /// Set source directory. + pub fn with_source(mut self, source: &Path) -> Self { + self.source = Some(source.into()); + self + } + + /// Set script to run. + pub fn with_shell(mut self, shell: &str) -> Self { + self.shell = Some(shell.into()); + self + } + + /// Run QEMU in the specified way. + pub fn run(&self) -> Result<(), QemuError> { + let tmp = tempfile::tempdir().map_err(QemuError::TempDir)?; + let empty = tempfile::tempdir().map_err(QemuError::TempDir)?; + + let image = tmp.path().join("vm.qcow2"); + let vars = tmp.path().join("vars.fd"); + + copy(&self.image, &image).map_err(|e| QemuError::Copy(self.image.clone(), e))?; + copy(OVMF_FD, &vars).map_err(|e| QemuError::Copy(OVMF_FD.into(), e))?; + + let output_drive = Self::create_tar(tmp.path().join("output"), empty.path())?; + let cache_drive = Self::create_tar(tmp.path().join("cache"), empty.path())?; + let deps_drive = Self::create_tar(tmp.path().join("deps"), empty.path())?; + + if let Some(shell) = &self.shell { + write(empty.path().join(".ambient-script"), shell).map_err(QemuError::Write)?; + } + let source_drive = Self::create_tar(tmp.path().join("src"), empty.path())?; + + let args = QemuArgs::default() + .with_valued_arg("-m", "16384") + .with_valued_arg("-smp", "cpus=4") + .with_valued_arg("-display", "none") + .with_valued_arg("-chardev", "stdio,id=serial0") + .with_valued_arg("-serial", "chardev:serial0") + .with_ipflash(0, OVMF_FD, true) + .with_ipflash(1, vars.to_str().unwrap(), false) + .with_qcow2(image.to_str().unwrap()) + .with_raw(source_drive.filename()) + .with_raw(output_drive.filename()) + .with_raw(cache_drive.filename()) + .with_raw(deps_drive.filename()) + .with_arg("-nodefaults"); + + let log = Self::create_file(&self.log)?; + + let child = Command::new("kvm") + .args(args.iter()) + .stdin(Stdio::null()) + .stdout(Stdio::from(log)) + .stderr(Stdio::null()) + .spawn() + .map_err(QemuError::Run)?; + + let output = child.wait_with_output().map_err(QemuError::Run)?; + if output.status.success() { + eprintln!("kvm OK"); + Ok(()) + } else { + let out = String::from_utf8_lossy(&output.stdout); + let err = String::from_utf8_lossy(&output.stderr); + eprintln!("kvm ERROR\n{}\n{}", out, err); + Err(QemuError::Kvm) + } + } + + fn create_file(filename: &Option<PathBuf>) -> Result<File, QemuError> { + let filename = if let Some(filename) = filename { + filename + } else { + Path::new("/dev/null") + }; + let file = File::create(filename).map_err(|e| QemuError::Log(filename.into(), e))?; + Ok(file) + } + + fn create_tar(tar_filename: PathBuf, dirname: &Path) -> Result<VirtualDrive, QemuError> { + let tar = VirtualDriveBuilder::default() + .filename(&tar_filename) + .root_directory(dirname) + .create() + .map_err(|e| QemuError::Tar(dirname.into(), e))?; + Ok(tar) + } +} + +#[derive(Debug, Default)] +struct QemuArgs { + args: Vec<String>, +} + +impl QemuArgs { + fn with_arg(mut self, arg: &str) -> Self { + self.args.push(arg.into()); + self + } + + fn with_valued_arg(mut self, arg: &str, value: &str) -> Self { + self.args.push(arg.into()); + self.args.push(value.into()); + self + } + + fn with_ipflash(mut self, unit: usize, path: &str, readonly: bool) -> Self { + self.args.push("-drive".into()); + self.args.push(format!( + "if=pflash,format=raw,unit={},file={}{}", + unit, + path, + if readonly { ",readonly=on" } else { "" }, + )); + self + } + + fn with_qcow2(mut self, path: &str) -> Self { + self.args.push("-drive".into()); + self.args + .push(format!("format=qcow2,if=virtio,file={}", path)); + self + } + + fn with_raw(mut self, path: &Path) -> Self { + self.args.push("-drive".into()); + self.args.push(format!( + "format=raw,if=virtio,file={},readonly=on", + path.display() + )); + self + } + + fn iter(&self) -> impl Iterator<Item = &str> { + self.args.iter().map(|s| s.as_str()) + } +} + +/// Possible errors from running Qemu. +#[allow(missing_docs)] +#[derive(Debug, thiserror::Error)] +pub enum QemuError { + #[error("failed to run QEMU")] + Run(#[source] std::io::Error), + + #[error("failed to run QEMU")] + Kvm, + + #[error("failed to create a temporary directory")] + TempDir(#[source] std::io::Error), + + #[error("failed to copy to temporary directory: {0}")] + Copy(PathBuf, #[source] std::io::Error), + + #[error("failed to write temporary file")] + Write(#[source] std::io::Error), + + #[error("failed to open log file for writing")] + Log(PathBuf, #[source] std::io::Error), + + #[error("failed to create a tar archive from {0}")] + Tar(PathBuf, #[source] VirtualDriveError), +} + +#[cfg(test)] +mod test { + use super::Qemu; + use std::path::Path; + + #[test] + fn sets_image() { + let path = Path::new("/my/image.qcow2"); + let qemu = Qemu::new(path); + assert_eq!(&qemu.image, &path); + } + + #[test] + fn sets_log() { + let path = Path::new("/my/image.qcow2"); + let log = Path::new("/my/log"); + let qemu = Qemu::new(path).with_log(log); + assert_eq!(qemu.log, Some(log.into())); + } +} diff --git a/subplot/ambient-run.rs b/subplot/ambient-run.rs index 508588c..0b5f097 100644 --- a/subplot/ambient-run.rs +++ b/subplot/ambient-run.rs @@ -5,7 +5,7 @@ use subplotlib::steplibrary::datadir::Datadir; use subplotlib::steplibrary::files::Files; use subplotlib::steplibrary::runcmd::Runcmd; -use std::path::Path; +use std::path::{Path, PathBuf}; #[derive(Debug, Default)] struct SubplotContext {} @@ -30,6 +30,22 @@ fn install_ambient_run(context: &ScenarioContext) { #[step] #[context(SubplotContext)] +#[context(Datadir)] +fn copy_image_file(context: &ScenarioContext, filename: &Path) { + let image = PathBuf::from(std::env::var_os("IMAGE").expect("exected IMAGE to be set")); + let filename = context.with_mut( + |context: &mut Datadir| { + Ok(context + .canonicalise_filename(filename) + .expect("canonical filename for image file copy")) + }, + false, + )?; + std::os::unix::fs::symlink(image, filename).expect("symlink $IMAGE to test dir"); +} + +#[step] +#[context(SubplotContext)] #[context(Runcmd)] #[context(Files)] fn stdout_matches_yaml(context: &ScenarioContext, file: SubplotDataFile) { diff --git a/subplot/ambient-run.yaml b/subplot/ambient-run.yaml index 50d04f9..f44eb29 100644 --- a/subplot/ambient-run.yaml +++ b/subplot/ambient-run.yaml @@ -2,6 +2,10 @@ impl: rust: function: install_ambient_run +- given: image file {filename} specified for test suite + impl: + rust: + function: copy_image_file - then: stdout, as YAML, matches file {file:file} impl: rust: |