1use 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#[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 let scratch_root = path.join("scratch");
37 fs::create_dir_all(&scratch_root).context("failed to create scratch/ dir")?;
38
39 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 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 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
76pub(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 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
101fn 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}