snix_build/oci/
bundle.rs

1//! Module to create an OCI runtime bundle for a given [BuildRequest].
2use std::{
3    ffi::OsStr,
4    fs,
5    io::Write,
6    path::{Component, Path, PathBuf},
7};
8
9use super::scratch_name;
10use crate::buildservice::BuildRequest;
11use anyhow::{Context, bail};
12use tracing::{debug, instrument};
13
14/// Produce an OCI bundle in a given path.
15/// Check [super::spec::make_spec] for a description about the paths produced.
16#[instrument(err)]
17pub(crate) fn make_bundle<'a>(
18    request: &BuildRequest,
19    runtime_spec: &oci_spec::runtime::Spec,
20    path: &Path,
21) -> anyhow::Result<()> {
22    fs::create_dir_all(path).context("failed to create bundle path")?;
23
24    let spec_json = serde_json::to_string(runtime_spec).context("failed to render spec to json")?;
25    fs::write(path.join("config.json"), spec_json).context("failed to write config.json")?;
26
27    fs::create_dir_all(path.join("inputs")).context("failed to create inputs dir")?;
28
29    let root_path = path.join("root");
30
31    fs::create_dir_all(&root_path).context("failed to create root path dir")?;
32    fs::create_dir_all(root_path.join("etc")).context("failed to create root/etc dir")?;
33
34    // TODO: populate /etc/{group,passwd}. It's a mess?
35
36    let scratch_root = path.join("scratch");
37    fs::create_dir_all(&scratch_root).context("failed to create scratch/ dir")?;
38
39    // for each scratch path, calculate its name inside scratch, and ensure the
40    // directory exists.
41    for p in request.scratch_paths.iter() {
42        let scratch_path = scratch_root.join(scratch_name(p));
43        debug!(scratch_path=?scratch_path, path=?p, "about to create scratch dir");
44        fs::create_dir_all(scratch_path.clone()).context("Unable to create scratch dir")?;
45
46        // TODO(#152): this is a hack, in the general case we may not have the "build" directory and additional files
47        // may not have /build prefix. But in practice today snix_build.rs is the only user of the builder and
48        // it always sets up a /build scratch and populates all additional_files with the /build prefix.
49        // For now this unblocks builds, but worth improving in the future.
50        if p == Path::new("build") {
51            for file in request.additional_files.iter() {
52                if file.path.components().count() < 2
53                    || file.path.components().next() != Some(Component::Normal(OsStr::new("build")))
54                {
55                    Err(std::io::Error::other(
56                        "Additional files must start with build/",
57                    ))?
58                }
59
60                // remove build/ prefix
61                let p = file.path.components().skip(1).collect::<PathBuf>();
62                if let Some(parent) = p.parent() {
63                    fs::create_dir_all(scratch_path.clone().join(parent))
64                        .context("Failed to create dir for additional file")?;
65                }
66                let p = scratch_path.join(p);
67                let mut out = std::fs::File::create(p).context("could not create file")?;
68                out.write_all(&file.contents)?;
69            }
70        }
71    }
72
73    Ok(())
74}
75
76/// Determine the path of all outputs specified in a [BuildRequest]
77/// as seen from the host, for post-build ingestion.
78/// This lookup needs to take scratch paths into consideration, as the build
79/// root is not writable on its own.
80/// If a path can't be determined, an error is returned.
81pub(crate) fn get_host_output_paths(
82    request: &BuildRequest,
83    bundle_path: &Path,
84) -> anyhow::Result<Vec<PathBuf>> {
85    let scratch_root = bundle_path.join("scratch");
86
87    let mut host_output_paths: Vec<PathBuf> = Vec::with_capacity(request.outputs.len());
88
89    for output_path in request.outputs.iter() {
90        // calculate the location of the path.
91        if let Some((mp, relpath)) = find_path_in_scratchs(output_path, &request.scratch_paths) {
92            host_output_paths.push(scratch_root.join(scratch_name(mp)).join(relpath));
93        } else {
94            bail!("unable to find path {output_path:?}");
95        }
96    }
97
98    Ok(host_output_paths)
99}
100
101/// For a given list of mountpoints (sorted) and a search_path, find the
102/// specific mountpoint parenting that search_path and return it, as well as the
103/// relative path from there to the search_path.
104/// mountpoints must be sorted, so we can iterate over the list from the back
105/// and match on the prefix.
106fn find_path_in_scratchs<'a, 'b, I>(
107    search_path: &'a Path,
108    mountpoints: I,
109) -> Option<(&'b Path, &'a Path)>
110where
111    I: IntoIterator<Item = &'b PathBuf>,
112    I::IntoIter: DoubleEndedIterator,
113{
114    mountpoints
115        .into_iter()
116        .rev()
117        .find_map(|mp| Some((mp.as_path(), search_path.strip_prefix(mp).ok()?)))
118}
119
120#[cfg(test)]
121mod tests {
122    use std::path::{Path, PathBuf};
123
124    use rstest::rstest;
125
126    use crate::{buildservice::BuildRequest, oci::scratch_name};
127
128    use super::{find_path_in_scratchs, get_host_output_paths};
129
130    #[rstest]
131    #[case::simple("nix/store/aaaa", &["nix/store".into()], Some(("nix/store", "aaaa")))]
132    #[case::prefix_no_sep("nix/store/aaaa", &["nix/sto".into()], None)]
133    #[case::not_found("nix/store/aaaa", &["build".into()], None)]
134    fn test_test_find_path_in_scratchs(
135        #[case] search_path: &str,
136        #[case] mountpoints: &[String],
137        #[case] expected: Option<(&str, &str)>,
138    ) {
139        let expected = expected.map(|e| (Path::new(e.0), Path::new(e.1)));
140        assert_eq!(
141            find_path_in_scratchs(
142                Path::new(search_path),
143                mountpoints
144                    .iter()
145                    .map(PathBuf::from)
146                    .collect::<Vec<_>>()
147                    .as_slice()
148            ),
149            expected
150        );
151    }
152
153    #[test]
154    fn test_get_host_output_paths_simple() {
155        let request = BuildRequest {
156            outputs: vec!["nix/store/fhaj6gmwns62s6ypkcldbaj2ybvkhx3p-foo".into()],
157            scratch_paths: vec!["build".into(), "nix/store".into()],
158            ..Default::default()
159        };
160
161        let paths =
162            get_host_output_paths(&request, Path::new("bundle-root")).expect("must succeed");
163
164        let mut expected_path = PathBuf::new();
165        expected_path.push("bundle-root");
166        expected_path.push("scratch");
167        expected_path.push(scratch_name(Path::new("nix/store")));
168        expected_path.push("fhaj6gmwns62s6ypkcldbaj2ybvkhx3p-foo");
169
170        assert_eq!(vec![expected_path], paths)
171    }
172}