snix_build/buildservice/
bwrap.rs

1use std::path::PathBuf;
2
3use bstr::BStr;
4use snix_castore::{
5    blobservice::BlobService,
6    directoryservice::DirectoryService,
7    fs::fuse::FuseDaemon,
8    import::fs::ingest_path,
9    refscan::{ReferencePattern, ReferenceScanner},
10};
11use tonic::async_trait;
12use tracing::{Span, debug, info, instrument, warn};
13use uuid::Uuid;
14
15use super::BuildService;
16use crate::{
17    buildservice::{BuildConstraints, BuildOutput, BuildRequest, BuildResult},
18    bwrap::Bwrap,
19    sandbox::SandboxSpec,
20};
21const SANDBOX_SHELL: &str = env!("SNIX_BUILD_SANDBOX_SHELL");
22
23pub struct BubblewrapBuildService<BS, DS> {
24    /// Root path in which all builds run
25    workdir: PathBuf,
26
27    /// Handle to a [BlobService], used by filesystems spawned during builds.
28    blob_service: BS,
29    /// Handle to a [DirectoryService], used by filesystems spawned during builds.
30    directory_service: DS,
31
32    // semaphore to track number of concurrently running builds.
33    // this is necessary, as otherwise we very quickly run out of open file handles.
34    concurrent_builds: tokio::sync::Semaphore,
35}
36impl<BS, DS> BubblewrapBuildService<BS, DS> {
37    pub fn new(workdir: PathBuf, blob_service: BS, directory_service: DS) -> Self {
38        // We map root inside the container to the uid/gid this is running at,
39        // and allocate one for uid 1000 into the container from the range we
40        // got in /etc/sub{u,g}id.
41        // FUTUREWORK: use different uids?
42        Self {
43            workdir,
44            blob_service,
45            directory_service,
46            concurrent_builds: tokio::sync::Semaphore::new(2),
47        }
48    }
49}
50
51#[async_trait]
52impl<BS, DS> BuildService for BubblewrapBuildService<BS, DS>
53where
54    BS: BlobService + Clone + 'static,
55    DS: DirectoryService + Clone + 'static,
56{
57    #[instrument(skip_all, err)]
58    async fn do_build(&self, request: BuildRequest) -> std::io::Result<BuildResult> {
59        let _permit = self.concurrent_builds.acquire().await.unwrap();
60
61        let build_name = Uuid::new_v4();
62        let sandbox_path = self.workdir.join(build_name.to_string());
63        info!(%build_name, "Starting bwrap build");
64
65        let span = Span::current();
66        span.record("build_name", build_name.to_string());
67
68        let blob_service = self.blob_service.clone();
69        let directory_service = self.directory_service.clone();
70
71        let spec = SandboxSpec::builder()
72            .host_workdir(sandbox_path)
73            .sandbox_workdir(request.working_dir)
74            .scratches(request.scratch_paths)
75            .command(request.command_args)
76            .env_vars(request.environment_vars)
77            .additional_files(request.additional_files)
78            .with_inputs(request.inputs_dir, move |path| {
79                let root_nodes = Box::new(request.inputs.clone());
80                let fs = snix_castore::fs::SnixStoreFs::new(
81                    blob_service.clone(),
82                    directory_service.clone(),
83                    root_nodes,
84                    true,
85                    None,
86                    false,
87                );
88                // FUTUREWORK: make fuse daemon threads configurable?
89                FuseDaemon::new(fs, path, 4, false)
90            })
91            .allow_network(
92                request
93                    .constraints
94                    .contains(&BuildConstraints::NetworkAccess),
95            )
96            .provide_shell(
97                request
98                    .constraints
99                    .contains(&BuildConstraints::ProvideBinSh)
100                    .then_some(SANDBOX_SHELL.into()),
101            )
102            .build();
103
104        let outcome = Bwrap::initialize(spec)?.run().await?;
105
106        if !outcome.output().status.success() {
107            let stdout = BStr::new(&outcome.output().stdout);
108            let stderr = BStr::new(&outcome.output().stderr);
109
110            warn!(stdout=%stdout, stderr=%stderr, exit_code=%outcome.output().status, "build failed");
111
112            return Err(std::io::Error::other("nonzero exit code".to_string()));
113        }
114
115        let outputs: Vec<_> = request
116            .outputs
117            .iter()
118            .filter_map(|o| outcome.find_path(o))
119            .collect();
120        if outputs.len() != request.outputs.len() {
121            warn!("Not all outputs produced");
122            return Err(std::io::Error::other(
123                "Not all outputs produced".to_string(),
124            ));
125        }
126        let patterns = ReferencePattern::new(request.refscan_needles);
127        let outputs = futures::future::try_join_all(outputs.into_iter().enumerate().map(
128            |(i, host_output_path)| {
129                let output_path = &request.outputs[i];
130                debug!(host.path=?host_output_path, output.path=?output_path, "ingesting path");
131                let patterns = patterns.clone();
132                async move {
133                    let scanner = ReferenceScanner::new(patterns);
134                    Ok::<_, std::io::Error>(BuildOutput {
135                        node: ingest_path(
136                            &self.blob_service,
137                            &self.directory_service,
138                            host_output_path,
139                            Some(&scanner),
140                        )
141                        .await
142                        .map_err(|e| {
143                            std::io::Error::new(
144                                std::io::ErrorKind::InvalidData,
145                                format!("Unable to ingest output: {e}"),
146                            )
147                        })?,
148
149                        output_needles: scanner
150                            .matches()
151                            .into_iter()
152                            .enumerate()
153                            .filter(|(_, val)| *val)
154                            .map(|(idx, _)| idx as u64)
155                            .collect(),
156                    })
157                }
158            },
159        ))
160        .await?;
161        Ok(BuildResult { outputs })
162    }
163}