snix_build/buildservice/
oci.rs1use anyhow::Context;
2use bstr::BStr;
3use snix_castore::{
4 blobservice::BlobService,
5 directoryservice::DirectoryService,
6 fs::fuse::FuseDaemon,
7 import::fs::ingest_path,
8 refscan::{ReferencePattern, ReferenceScanner},
9};
10use tokio::process::{Child, Command};
11use tonic::async_trait;
12use tracing::{Span, debug, instrument, warn};
13use uuid::Uuid;
14
15use crate::buildservice::{BuildOutput, BuildRequest, BuildResult};
16use crate::oci::{get_host_output_paths, make_bundle, make_spec};
17use std::{ffi::OsStr, path::PathBuf, process::Stdio};
18
19use super::BuildService;
20
21const SANDBOX_SHELL: &str = env!("SNIX_BUILD_SANDBOX_SHELL");
22const MAX_CONCURRENT_BUILDS: usize = 2; pub struct OCIBuildService<BS, DS> {
25 bundle_root: PathBuf,
27
28 blob_service: BS,
30 directory_service: DS,
32
33 concurrent_builds: tokio::sync::Semaphore,
36}
37
38impl<BS, DS> OCIBuildService<BS, DS> {
39 pub fn new(bundle_root: PathBuf, blob_service: BS, directory_service: DS) -> Self {
40 Self {
45 bundle_root,
46 blob_service,
47 directory_service,
48 concurrent_builds: tokio::sync::Semaphore::new(MAX_CONCURRENT_BUILDS),
49 }
50 }
51}
52
53#[async_trait]
54impl<BS, DS> BuildService for OCIBuildService<BS, DS>
55where
56 BS: BlobService + Clone + 'static,
57 DS: DirectoryService + Clone + 'static,
58{
59 #[instrument(skip_all, err)]
60 async fn do_build(&self, request: BuildRequest) -> std::io::Result<BuildResult> {
61 let _permit = self.concurrent_builds.acquire().await.unwrap();
62
63 let bundle_name = Uuid::new_v4();
64 let bundle_path = self.bundle_root.join(bundle_name.to_string());
65
66 let span = Span::current();
67 span.record("bundle_name", bundle_name.to_string());
68
69 let mut runtime_spec = make_spec(&request, true, SANDBOX_SHELL)
70 .context("failed to create spec")
71 .map_err(std::io::Error::other)?;
72
73 let linux = runtime_spec.linux().clone().unwrap();
74
75 runtime_spec.set_linux(Some(linux));
76
77 make_bundle(&request, &runtime_spec, &bundle_path)
78 .context("failed to produce bundle")
79 .map_err(std::io::Error::other)?;
80
81 let host_output_paths = get_host_output_paths(&request, &bundle_path)
85 .context("failed to calculate host output paths")
86 .map_err(std::io::Error::other)?;
87
88 let patterns = ReferencePattern::new(request.refscan_needles);
90 let _fuse_daemon = tokio::task::spawn_blocking({
92 let blob_service = self.blob_service.clone();
93 let directory_service = self.directory_service.clone();
94
95 let dest = bundle_path.join("inputs");
96
97 let root_nodes = Box::new(request.inputs);
98 move || {
99 let fs = snix_castore::fs::SnixStoreFs::new(
100 blob_service,
101 directory_service,
102 root_nodes,
103 true,
104 false,
105 );
106 FuseDaemon::new(fs, dest, 4, true).context("failed to start fuse daemon")
109 }
110 })
111 .await?
112 .context("mounting")
113 .map_err(std::io::Error::other)?;
114
115 debug!(bundle.path=?bundle_path, bundle.name=%bundle_name, "about to spawn bundle");
116
117 let child = spawn_bundle(bundle_path, &bundle_name.to_string())?;
119
120 let child_output = child
123 .wait_with_output()
124 .await
125 .context("failed to run process")
126 .map_err(std::io::Error::other)?;
127
128 if !child_output.status.success() {
130 let stdout = BStr::new(&child_output.stdout);
131 let stderr = BStr::new(&child_output.stderr);
132
133 warn!(stdout=%stdout, stderr=%stderr, exit_code=%child_output.status, "build failed");
134
135 return Err(std::io::Error::other("nonzero exit code".to_string()));
136 }
137
138 let outputs = futures::future::try_join_all(host_output_paths.into_iter().enumerate().map(
142 |(i, host_output_path)| {
143 let output_path = &request.outputs[i];
144 let patterns = patterns.clone();
145 async move {
146 debug!(host.path=?host_output_path, output.path=?output_path, "ingesting path");
147
148 let scanner = ReferenceScanner::new(patterns);
149
150 Ok::<_, std::io::Error>(BuildOutput {
151 node: ingest_path(
152 self.blob_service.clone(),
153 &self.directory_service,
154 host_output_path,
155 Some(&scanner),
156 )
157 .await
158 .map_err(|e| {
159 std::io::Error::new(
160 std::io::ErrorKind::InvalidData,
161 format!("Unable to ingest output: {e}"),
162 )
163 })?,
164
165 output_needles: scanner
166 .matches()
167 .into_iter()
168 .enumerate()
169 .filter(|(_, val)| *val)
170 .map(|(idx, _)| idx as u64)
171 .collect(),
172 })
173 }
174 },
175 ))
176 .await?;
177
178 Ok(BuildResult { outputs })
179 }
180}
181
182#[instrument(err)]
185fn spawn_bundle(
186 bundle_path: impl AsRef<OsStr> + std::fmt::Debug,
187 bundle_name: &str,
188) -> std::io::Result<Child> {
189 let mut command = Command::new("runc");
190
191 command
192 .args(&[
193 "run".into(),
194 "--bundle".into(),
195 bundle_path.as_ref().to_os_string(),
196 bundle_name.into(),
197 ])
198 .stderr(Stdio::piped())
199 .stdout(Stdio::piped())
200 .stdin(Stdio::null());
201
202 command.spawn()
203}