diff options
author | Lars Wirzenius <liw@noreply.codeberg.org> | 2023-08-19 14:43:38 +0000 |
---|---|---|
committer | Lars Wirzenius <liw@noreply.codeberg.org> | 2023-08-19 14:43:38 +0000 |
commit | 60636d04447a7d03ca4cef8b24c761c7a9030000 (patch) | |
tree | 6c4ed2bd841ff73791cda524d383b984416a0c90 | |
parent | fad14cfe28273c48c5031fbd781d553efaabf100 (diff) | |
parent | 4ff4a3a62ee39b3ec3ce2121741a1b148c620525 (diff) | |
download | ambient-run-60636d04447a7d03ca4cef8b24c761c7a9030000.tar.gz |
Merge pull request 'feat: subcommands to manipulate virtual drives' (#3) from liw/vdrive into main
Reviewed-on: https://codeberg.org/ambient/ambient-run/pulls/3
-rw-r--r-- | Cargo.lock | 21 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | ambient-run.md | 70 | ||||
-rw-r--r-- | src/bin/ambient-run.rs | 97 | ||||
-rw-r--r-- | src/error.rs | 3 | ||||
-rw-r--r-- | src/lib.rs | 1 | ||||
-rw-r--r-- | src/vdrive.rs | 124 | ||||
-rw-r--r-- | subplot/ambient-run.rs | 16 | ||||
-rw-r--r-- | subplot/ambient-run.yaml | 4 |
9 files changed, 335 insertions, 2 deletions
@@ -31,6 +31,7 @@ dependencies = [ "serde_yaml 0.9.25", "subplot-build", "subplotlib", + "tar", "thiserror", ] @@ -1222,6 +1223,17 @@ dependencies = [ ] [[package]] +name = "tar" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] name = "tempfile" version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1719,6 +1731,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] +name = "xattr" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985" +dependencies = [ + "libc", +] + +[[package]] name = "yaml-rust" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -9,6 +9,7 @@ directories-next = "2.0.0" serde = { version = "1.0.182", features = ["derive"] } serde_yaml = "0.9.25" subplotlib = "0.8.0" +tar = "0.4.40" thiserror = "1.0.44" [build-dependencies] diff --git a/ambient-run.md b/ambient-run.md index e290364..c5dc188 100644 --- a/ambient-run.md +++ b/ambient-run.md @@ -176,6 +176,76 @@ image: /my/image.qcow2 ### Cache is persistent between builds ### Build gets the resources is demands +## Utility functionality + +The `ambient-run` command has some subcommands that are meant to be +useful for debugging and testing, but rarely used by normal users. +This section is for verifying those. + +### Create a virtual drive from directory + +_Requirement:_ I can create a virtual drive from the contents of a +directory. + +_Justification:_ This is useful for, at least, verifying that the +functionality dealing with virtual drives works, independently of the +rest of the program. + +_Stakeholder:_ Lars. + +~~~scenario +given an installed ambient-run +when I create directory src +when I write "my project!" to file src/README.md +when I run ambient-run vdrive create --root src my.drive +then file my.drive exists +when I run ambient-run vdrive list my.drive +then stdout contains "README.md" +when I run ambient-run vdrive extract my.drive --to extracted +then file extracted/README.md contains "my project!" +~~~ + +### Create an empty virtual drive + +_Requirement:_ I can create an virtual drive with a desired size. + +_Justification:_ This is useful for, at least, verifying that the +functionality dealing with virtual drives works, independently of the +rest of the program. + +_Stakeholder:_ Lars. + +~~~scenario +given an installed ambient-run +when I run ambient-run vdrive create my.drive --size 1024 +then file my.drive exists and is 1024 bytes long +when I run ambient-run vdrive list my.drive +then stdout is exactly "" +~~~ + +### Create a virtual drive of wanted size + +_Requirement:_ I can create an virtual drive with a desired size with +contents of a directory. + +_Justification:_ This is useful for, at least, verifying that the +functionality dealing with virtual drives works, independently of the +rest of the program. + +_Stakeholder:_ Lars. + +~~~scenario +given an installed ambient-run +when I create directory src +when I write "my project!" to file src/README.md +when I run ambient-run vdrive create my.drive --size 10000 --root src +then file my.drive exists and is 10000 bytes long +when I run ambient-run vdrive list my.drive +then stdout contains "README.md" +when I run ambient-run vdrive extract my.drive --to extracted +then file extracted/README.md contains "my project!" +~~~ + ## Errors during build ### Build produces too large an artifact ### Build produces too large a cache diff --git a/src/bin/ambient-run.rs b/src/bin/ambient-run.rs index da74b15..a2d3809 100644 --- a/src/bin/ambient-run.rs +++ b/src/bin/ambient-run.rs @@ -2,8 +2,9 @@ use ambient_run::{ config::{canonical, default_config_file, Config}, error::AmbientRunError, project::Project, + vdrive::VirtualDriveBuilder, }; -use clap::Parser; +use clap::{Parser, Subcommand}; use std::{ error::Error, path::{Path, PathBuf}, @@ -26,6 +27,7 @@ fn fallible_main() -> Result<(), AmbientRunError> { match &args.cmd { Command::Config(x) => x.run(&args, &config), Command::Project(x) => x.run(&args, &config), + Command::VDrive(x) => x.run(&args, &config), } } @@ -45,10 +47,12 @@ impl Args { } } -#[derive(Debug, Parser)] +#[derive(Debug, Subcommand)] enum Command { Config(ConfigCommand), Project(ProjectCommand), + #[clap(name = "vdrive")] + VDrive(VDriveCommand), } #[derive(Debug, Parser)] @@ -103,3 +107,92 @@ impl ProjectCommand { Ok(()) } } + +#[derive(Debug, Parser)] +struct VDriveCommand { + #[clap(subcommand)] + cmd: VDriveSubcommand, +} + +impl VDriveCommand { + fn run(&self, global: &Args, config: &Config) -> Result<(), AmbientRunError> { + match &self.cmd { + VDriveSubcommand::Create(x) => x.run(global, config), + VDriveSubcommand::List(x) => x.run(global, config), + VDriveSubcommand::Extract(x) => x.run(global, config), + } + } +} + +#[derive(Debug, Subcommand)] +enum VDriveSubcommand { + Create(VDriveCreateCommand), + List(VDriveListCommand), + Extract(VDriveExtractCommand), +} + +#[derive(Debug, Parser)] +struct VDriveCreateCommand { + /// Name of output file in which to write virtual drive. + output: PathBuf, + + /// Path to root directory of files to put into virtual drive. + #[clap(long, short)] + root: Option<PathBuf>, + + /// Size of drive to create. + #[clap(long, short)] + size: Option<u64>, +} + +impl VDriveCreateCommand { + fn run(&self, _global: &Args, _config: &Config) -> Result<(), AmbientRunError> { + let mut builder = VirtualDriveBuilder::default().filename(&self.output); + if let Some(root) = &self.root { + builder = builder.root_directory(root); + } + if let Some(size) = self.size { + builder = builder.size(size); + } + builder.create()?; + Ok(()) + } +} + +#[derive(Debug, Parser)] +struct VDriveListCommand { + /// Name of file containing virtual drive. + filename: PathBuf, +} + +impl VDriveListCommand { + fn run(&self, _global: &Args, _config: &Config) -> Result<(), AmbientRunError> { + let v = VirtualDriveBuilder::default() + .filename(&self.filename) + .open()?; + for filename in v.list()? { + println!("{}", filename.display()); + } + Ok(()) + } +} + +#[derive(Debug, Parser)] +struct VDriveExtractCommand { + /// Name of file containing virtual drive. + filename: PathBuf, + + /// Directory to create and extract virtual drive files to. + #[clap(long, short)] + to: PathBuf, +} + +impl VDriveExtractCommand { + fn run(&self, _global: &Args, _config: &Config) -> Result<(), AmbientRunError> { + let v = VirtualDriveBuilder::default() + .filename(&self.filename) + .open()?; + v.extract_to(&self.to)?; + Ok(()) + } +} diff --git a/src/error.rs b/src/error.rs index 35adc8f..d3a03ed 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,4 +8,7 @@ pub enum AmbientRunError { #[error(transparent)] Project(#[from] crate::project::ProjectError), + + #[error(transparent)] + VDrive(#[from] crate::vdrive::VirtualDriveError), } @@ -5,3 +5,4 @@ pub mod config; pub mod error; pub mod project; +pub mod vdrive; diff --git a/src/vdrive.rs b/src/vdrive.rs new file mode 100644 index 0000000..e86eb9d --- /dev/null +++ b/src/vdrive.rs @@ -0,0 +1,124 @@ +//! Virtual drive handling for ambient-run. + +use std::{ + fs::File, + path::{Path, PathBuf}, +}; + +/// A virtual drive. +pub struct VirtualDrive { + filename: PathBuf, +} + +impl VirtualDrive { + /// Path to file containing virtual drive. + pub fn filename(&self) -> &Path { + self.filename.as_path() + } + + /// List files in the virtual drive. + pub fn list(&self) -> Result<Vec<PathBuf>, VirtualDriveError> { + let file = File::open(&self.filename) + .map_err(|e| VirtualDriveError::Open(self.filename.clone(), e))?; + let mut archive = tar::Archive::new(file); + let entries = archive + .entries() + .map_err(|e| VirtualDriveError::List(self.filename.clone(), e))?; + let mut filenames = vec![]; + for maybe_entry in entries { + let entry = + maybe_entry.map_err(|e| VirtualDriveError::List(self.filename.clone(), e))?; + let path = entry + .path() + .map_err(|e| VirtualDriveError::List(self.filename.clone(), e))?; + let path = path.to_path_buf(); + filenames.push(path); + } + Ok(filenames) + } + + /// Extract files in the virtual drive into a new directory + pub fn extract_to(&self, dirname: &Path) -> Result<(), VirtualDriveError> { + std::fs::create_dir(dirname).map_err(|e| VirtualDriveError::Extract(dirname.into(), e))?; + let file = File::open(&self.filename) + .map_err(|e| VirtualDriveError::Open(self.filename.clone(), e))?; + let mut archive = tar::Archive::new(file); + archive + .unpack(dirname) + .map_err(|e| VirtualDriveError::Extract(self.filename.clone(), e))?; + Ok(()) + } +} + +/// Builder for [`VirtualDrive`]. +#[derive(Default)] +pub struct VirtualDriveBuilder { + filename: Option<PathBuf>, + root: Option<PathBuf>, + size: Option<u64>, +} + +impl VirtualDriveBuilder { + /// Set filename for virtual drive. + pub fn filename(mut self, filename: &Path) -> Self { + self.filename = Some(filename.into()); + self + } + + /// Set directory of tree to copy into virtual drive. + pub fn root_directory(mut self, dirname: &Path) -> Self { + self.root = Some(dirname.into()); + self + } + + /// Set size of new drive. This is important when the build VM + /// writes to the drive. + pub fn size(mut self, size: u64) -> Self { + self.size = Some(size); + self + } + + /// Create a virtual drive. + pub fn create(self) -> Result<VirtualDrive, VirtualDriveError> { + let filename = self.filename.expect("filename has been set"); + let file = + File::create(&filename).map_err(|e| VirtualDriveError::Create(filename.clone(), e))?; + if let Some(size) = self.size { + file.set_len(size) + .map_err(|e| VirtualDriveError::Create(filename.clone(), e))?; + } + let mut builder = tar::Builder::new(file); + if let Some(root) = self.root { + builder + .append_dir_all(".", &root) + .map_err(VirtualDriveError::CreateTar)?; + } + Ok(VirtualDrive { filename }) + } + + /// Open an existing virtual drive. + pub fn open(self) -> Result<VirtualDrive, VirtualDriveError> { + let filename = self.filename.expect("filename has been set"); + Ok(VirtualDrive { filename }) + } +} + +/// Errors that may be returned from [`VirtualDrive`] use. +#[allow(missing_docs)] +#[derive(Debug, thiserror::Error)] +pub enum VirtualDriveError { + #[error("failed to create virtual drive {0}")] + Create(PathBuf, #[source] std::io::Error), + + #[error("failed to create tar archive for virtual drive")] + CreateTar(#[source] std::io::Error), + + #[error("failed to open virtual drive {0}")] + Open(PathBuf, #[source] std::io::Error), + + #[error("failed to list files in virtual drive {0}")] + List(PathBuf, #[source] std::io::Error), + + #[error("failed to create directory {0}")] + Extract(PathBuf, #[source] std::io::Error), +} diff --git a/subplot/ambient-run.rs b/subplot/ambient-run.rs index 37c2a8c..508588c 100644 --- a/subplot/ambient-run.rs +++ b/subplot/ambient-run.rs @@ -1,6 +1,7 @@ // Implementations of Subplot scenario steps for ambient-run. use serde_yaml::Value; +use subplotlib::steplibrary::datadir::Datadir; use subplotlib::steplibrary::files::Files; use subplotlib::steplibrary::runcmd::Runcmd; @@ -40,3 +41,18 @@ fn stdout_matches_yaml(context: &ScenarioContext, file: SubplotDataFile) { println!("Expected:\n{expected:#?}"); assert_eq!(actual, expected); } + +#[step] +#[context(SubplotContext)] +#[context(Datadir)] +fn file_length(context: &ScenarioContext, filename: &Path, bytes: i64) { + println!("filename: {}", filename.display()); + println!("cwd: {}", std::env::current_dir().unwrap().display()); + + let file = context.with(|datadir: &Datadir| datadir.open_read(filename), false)?; + + let metadata = file.metadata().expect("expected to get open file metadata"); + let len = metadata.len(); + println!("len: {}", len); + assert_eq!(len, bytes as u64); +} diff --git a/subplot/ambient-run.yaml b/subplot/ambient-run.yaml index 8a8c1d6..50d04f9 100644 --- a/subplot/ambient-run.yaml +++ b/subplot/ambient-run.yaml @@ -6,3 +6,7 @@ impl: rust: function: stdout_matches_yaml +- then: file {filename:path} exists and is {bytes:int} bytes long + impl: + rust: + function: file_length |