summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2023-08-19 14:55:31 +0300
committerLars Wirzenius <liw@liw.fi>2023-08-19 17:40:50 +0300
commit4ff4a3a62ee39b3ec3ce2121741a1b148c620525 (patch)
tree6c4ed2bd841ff73791cda524d383b984416a0c90
parentfad14cfe28273c48c5031fbd781d553efaabf100 (diff)
downloadambient-run-4ff4a3a62ee39b3ec3ce2121741a1b148c620525.tar.gz
feat: subcommands to manipulate virtual drives
Sponsored-by: author
-rw-r--r--Cargo.lock21
-rw-r--r--Cargo.toml1
-rw-r--r--ambient-run.md70
-rw-r--r--src/bin/ambient-run.rs97
-rw-r--r--src/error.rs3
-rw-r--r--src/lib.rs1
-rw-r--r--src/vdrive.rs124
-rw-r--r--subplot/ambient-run.rs16
-rw-r--r--subplot/ambient-run.yaml4
9 files changed, 335 insertions, 2 deletions
diff --git a/Cargo.lock b/Cargo.lock
index f5acb35..a8cefda 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 8bab92f..4e6243a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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),
}
diff --git a/src/lib.rs b/src/lib.rs
index a57c977..3e7428d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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