Skip to main content

ambient_ci/
vdrive.rs

1//! Virtual drive handling for ambient-run.
2
3use std::{
4    fs::{File, OpenOptions},
5    path::{Path, PathBuf},
6    process::Command,
7};
8
9/// A virtual drive.
10#[derive(Debug, Clone)]
11pub struct VirtualDrive {
12    filename: PathBuf,
13}
14
15impl VirtualDrive {
16    /// Path to file containing virtual drive.
17    pub fn filename(&self) -> &Path {
18        self.filename.as_path()
19    }
20
21    /// Extract files in the virtual drive into a directory. Create
22    /// the directory if it doesn't exist.
23    pub fn extract_to(&self, dirname: &Path) -> Result<(), VirtualDriveError> {
24        if !dirname.exists() {
25            std::fs::create_dir(dirname)
26                .map_err(|e| VirtualDriveError::Extract(dirname.into(), e))?;
27        }
28        tar_extract(&self.filename, dirname)?;
29        Ok(())
30    }
31}
32
33/// Builder for [`VirtualDrive`].
34#[derive(Debug, Default)]
35pub struct VirtualDriveBuilder {
36    filename: Option<PathBuf>,
37    root: Option<PathBuf>,
38    size: Option<u64>,
39}
40
41impl VirtualDriveBuilder {
42    /// Set filename for virtual drive.
43    pub fn filename(mut self, filename: &Path) -> Self {
44        self.filename = Some(filename.into());
45        self
46    }
47
48    /// Set directory of tree to copy into virtual drive.
49    pub fn root_directory(mut self, dirname: &Path) -> Self {
50        self.root = Some(dirname.into());
51        self
52    }
53
54    /// Set size of new drive. This is important when the build VM
55    /// writes to the drive.
56    pub fn size(mut self, size: u64) -> Self {
57        self.size = Some(size);
58        self
59    }
60
61    /// Create a virtual drive.
62    pub fn create(self) -> Result<VirtualDrive, VirtualDriveError> {
63        let filename = self.filename.expect("filename has been set");
64
65        // Create the file, either empty or to the desired size. If we
66        // don't have self.root set, the file created (and maybe
67        // truncated) will be.
68        {
69            let archive = File::create(&filename)
70                .map_err(|e| VirtualDriveError::Create(filename.clone(), e))?;
71            if let Some(size) = self.size {
72                archive
73                    .set_len(size)
74                    .map_err(|e| VirtualDriveError::Create(filename.clone(), e))?;
75            }
76        }
77
78        if let Some(root) = self.root {
79            match tar_create(&filename, &root) {
80                Ok(_) => (),
81                Err(VirtualDriveError::TarFailed(cmd, filename, exit, stderr)) => {
82                    Err(VirtualDriveError::TarFailed(cmd, filename, exit, stderr))?;
83                }
84                Err(err) => {
85                    Err(err)?;
86                }
87            }
88        }
89
90        Ok(VirtualDrive { filename })
91    }
92
93    /// Open an existing virtual drive.
94    pub fn open(self) -> Result<VirtualDrive, VirtualDriveError> {
95        let filename = self.filename.expect("filename has been set");
96        Ok(VirtualDrive { filename })
97    }
98}
99
100/// Create a tar archive out of a directory.
101pub fn create_tar(
102    tar_filename: PathBuf,
103    dirname: &Path,
104) -> Result<VirtualDrive, VirtualDriveError> {
105    assert!(!tar_filename.starts_with(dirname));
106    VirtualDriveBuilder::default()
107        .filename(&tar_filename)
108        .root_directory(dirname)
109        .create()
110}
111
112/// Create a tar archive with a fixed length, out of a directory.
113pub fn create_tar_with_size(
114    tar_filename: PathBuf,
115    dirname: &Path,
116    size: u64,
117) -> Result<VirtualDrive, VirtualDriveError> {
118    let tar = VirtualDriveBuilder::default()
119        .filename(&tar_filename)
120        .root_directory(dirname)
121        .size(size)
122        .create()?;
123
124    let metadata = std::fs::metadata(&tar_filename)
125        .map_err(|err| VirtualDriveError::Metadata(tar_filename.clone(), err))?;
126    if metadata.len() < size {
127        let file = OpenOptions::new()
128            .write(true)
129            .truncate(false)
130            .open(&tar_filename)
131            .map_err(|err| VirtualDriveError::CreateTar(tar_filename.clone(), err))?;
132        file.set_len(size)
133            .map_err(|err| VirtualDriveError::SetLen(size, tar_filename.clone(), err))?;
134    }
135
136    let metadata = std::fs::metadata(&tar_filename)
137        .map_err(|err| VirtualDriveError::Metadata(tar_filename.clone(), err))?;
138    if metadata.len() > size {
139        return Err(VirtualDriveError::DriveTooBig(metadata.len(), size));
140    }
141
142    Ok(tar)
143}
144
145fn tar_create(tar_filename: &Path, dirname: &Path) -> Result<(), VirtualDriveError> {
146    let output = Command::new("tar")
147        .arg("-cvf")
148        .arg(tar_filename)
149        .arg("-C")
150        .arg(dirname)
151        .arg(".")
152        .output()
153        .map_err(|err| VirtualDriveError::Tar("create", dirname.into(), err))?;
154
155    if let Some(exit) = output.status.code() {
156        if exit != 0 {
157            let stderr = String::from_utf8_lossy(&output.stderr).to_string();
158            return Err(VirtualDriveError::TarFailed(
159                "create",
160                dirname.into(),
161                exit,
162                stderr,
163            ));
164        }
165    }
166
167    Ok(())
168}
169
170fn tar_extract(tar_filename: &Path, dirname: &Path) -> Result<(), VirtualDriveError> {
171    let output = Command::new("tar")
172        .arg("-xvvvf")
173        .arg(tar_filename)
174        .arg("-C")
175        .arg(dirname)
176        .arg("--no-same-owner")
177        .output()
178        .map_err(|err| VirtualDriveError::Tar("extract", dirname.into(), err))?;
179
180    if let Some(exit) = output.status.code() {
181        if exit != 0 {
182            let stderr = String::from_utf8_lossy(&output.stderr).to_string();
183            return Err(VirtualDriveError::TarFailed(
184                "extract",
185                dirname.into(),
186                exit,
187                stderr,
188            ));
189        }
190    }
191
192    Ok(())
193}
194
195/// Errors that may be returned from [`VirtualDrive`] use.
196#[allow(missing_docs)]
197#[derive(Debug, thiserror::Error)]
198pub enum VirtualDriveError {
199    #[error("failed to create virtual drive {0}")]
200    Create(PathBuf, #[source] std::io::Error),
201
202    #[error("failed to create tar archive for virtual drive from {0}")]
203    CreateTar(PathBuf, #[source] std::io::Error),
204
205    #[error("failed to open virtual drive {0}")]
206    Open(PathBuf, #[source] std::io::Error),
207
208    #[error("failed to list files in virtual drive {0}")]
209    List(PathBuf, #[source] std::io::Error),
210
211    #[error("failed to create directory {0}")]
212    Extract(PathBuf, #[source] std::io::Error),
213
214    #[error("failed to extract {0} to {1}")]
215    ExtractEntry(PathBuf, PathBuf, #[source] std::io::Error),
216
217    #[error(transparent)]
218    Util(#[from] crate::util::UtilError),
219
220    #[error("failed to get length of file {0}")]
221    Metadata(PathBuf, #[source] std::io::Error),
222
223    #[error("failed to set length of file to {0}: {1}")]
224    SetLen(u64, PathBuf, #[source] std::io::Error),
225
226    #[error("virtual drive is too big: {0} > {1}")]
227    DriveTooBig(u64, u64),
228
229    /// Can't run tar.
230    #[error("failed to run system tar command: {0}: {1}")]
231    Tar(&'static str, PathBuf, #[source] std::io::Error),
232
233    /// Tar failed.
234    #[error("failed to run system tar command: {0}: {1}")]
235    TarFailed(&'static str, PathBuf, i32, String),
236}