Skip to main content

snix_build/sandbox/
mod.rs

1use std::path::{Path, PathBuf};
2
3use typed_builder::TypedBuilder;
4
5use crate::buildservice::{AdditionalFile, EnvVar};
6
7pub mod event;
8
9/// A handle returned by sandboxes that allows finding build outputs relative to logical sandbox root.
10///
11/// Since different build sandboxes have their own internal directory layout, this trait
12/// encapsulates those implementation details and allows callers to query paths in a consistent way.
13pub(crate) trait SandboxOutputs {
14    /// Resolves to an absolute filesystem host path that corresponds to `path`.
15    fn find_path(
16        &self,
17        path: impl AsRef<Path> + Send,
18    ) -> impl Future<Output = Option<PathBuf>> + Send;
19}
20
21/// A sandbox builder.
22///
23/// Its API is tailored to the needs of Snix builds, namely running sandboxed commands
24/// with optional build input paths, files, network access. And allow for such commands
25/// to produce outputs that stay available after the sandbox has stopped.
26#[derive(TypedBuilder)]
27pub struct SandboxSpec {
28    /// Working directory on the host, where the sandbox is assembled.
29    #[builder(setter(into))]
30    host_workdir: PathBuf,
31
32    /// Command to execute inside the sandbox
33    #[builder(setter(fn transform<I, P>(value: I) -> Vec<String>
34            where
35                I: IntoIterator<Item = P>,
36                P: AsRef<str>,
37            {
38                value.into_iter().map(|p| p.as_ref().into()).collect()
39            }))]
40    command: Vec<String>,
41
42    /// Workdir inside the sandbox, in which the [Self::command] will be executed.
43    #[builder(setter(into))]
44    sandbox_workdir: PathBuf,
45
46    /// A list of scratch paths to make available inside the sandbox.
47    ///
48    /// These directories are read+writable inside the sandbox and their contents is preserved
49    /// after the sandbox has stopped.
50    #[builder(setter(fn transform<I, P>(value: I) -> Vec<PathBuf>
51            where
52                I: IntoIterator<Item = P>,
53                P: AsRef<Path>,
54            {
55                value.into_iter().map(|p| p.as_ref().into()).collect()
56            }))]
57    scratches: Vec<PathBuf>,
58
59    /// Any additional files to rw-mount inside the sandbox.
60    #[builder(default, setter(into))]
61    additional_files: Vec<AdditionalFile>,
62
63    /// Env vars to set before running [Self::command].
64    #[builder(default, setter(into))]
65    env_vars: Vec<EnvVar>,
66
67    /// Optionally read-only mount build inputs.
68    ///
69    /// # Example
70    /// Mount some host path at "/nix/store" inside the sandbox.
71    ///
72    /// ```rust
73    /// use snix_build::sandbox::SandboxSpec;
74    /// let _  = SandboxSpec::builder()
75    ///     .host_workdir("/tmp/sandbox1")
76    ///     .command(["echo", "Hello"])
77    ///     .sandbox_workdir("build")
78    ///     .scratches(["foo"])
79    ///     .with_inputs("nix/store", |path| {
80    ///         // mount dir at `path`
81    ///         // return an RAII guard that will unmount the dir
82    ///         Ok(())
83    ///     })
84    ///     .build();
85    /// ```
86    #[builder(default, setter(
87        fn transform<TResult: InputsGuard + 'static>(
88             inputs_dir: impl AsRef<Path>,
89             provider: impl Fn(&Path) -> std::io::Result<TResult> + Send + 'static,
90        ) -> InputsProvider {
91            InputsProvider::new(inputs_dir, provider)
92        }
93    ))]
94    with_inputs: InputsProvider,
95
96    /// Absolute path to the shell that will be mounted at /bin/sh inside the sandbox.
97    ///
98    /// It must static binary, otherwise it will likely fail to start.
99    #[builder(default)]
100    provide_shell: Option<PathBuf>,
101
102    /// Whether to allow network access inside the sandbox.
103    #[builder(default)]
104    allow_network: bool,
105}
106
107impl SandboxSpec {
108    pub fn host_workdir(&self) -> &Path {
109        &self.host_workdir
110    }
111
112    pub fn command(&self) -> impl IntoIterator<Item = &String> {
113        &self.command
114    }
115
116    pub fn sandbox_workdir(&self) -> &Path {
117        &self.sandbox_workdir
118    }
119
120    pub fn scratches(&self) -> impl IntoIterator<Item = &PathBuf> {
121        &self.scratches
122    }
123
124    pub fn additional_files(&self) -> impl IntoIterator<Item = &AdditionalFile> {
125        &self.additional_files
126    }
127
128    pub fn env_vars(&self) -> impl IntoIterator<Item = &EnvVar> {
129        &self.env_vars
130    }
131
132    pub fn provide_shell(&self) -> Option<&Path> {
133        self.provide_shell.as_deref()
134    }
135
136    pub fn allow_network(&self) -> bool {
137        self.allow_network
138    }
139
140    pub fn inputs_provider(&self) -> &InputsProvider {
141        &self.with_inputs
142    }
143}
144
145/// Inputs provider.
146pub struct InputsProvider {
147    inputs_dir: PathBuf,
148    provider: ProviderFn,
149}
150
151impl InputsProvider {
152    fn new<TResult: Send + 'static>(
153        inputs_dir: impl AsRef<Path>,
154        mut provider: impl FnMut(&Path) -> std::io::Result<TResult> + Send + 'static,
155    ) -> Self {
156        Self {
157            inputs_dir: inputs_dir.as_ref().into(),
158            provider: Box::new(move |p| provider(p).map(|r| Box::new(r) as Box<dyn InputsGuard>)),
159        }
160    }
161
162    /// Consumes the inputs provider and provides the inputs at `path`.
163    ///
164    /// The returned guard cleans up or unmounts inputs when dropped.
165    pub fn provide_inputs(
166        mut self,
167        path: impl AsRef<Path>,
168    ) -> std::io::Result<Box<dyn InputsGuard>> {
169        (self.provider)(path.as_ref())
170    }
171
172    pub fn inputs_dir(&self) -> &Path {
173        &self.inputs_dir
174    }
175}
176
177impl Default for InputsProvider {
178    fn default() -> Self {
179        Self {
180            inputs_dir: Default::default(),
181            provider: Box::new(|_| Ok(Box::new(()))),
182        }
183    }
184}
185
186impl From<SandboxSpec> for InputsProvider {
187    fn from(value: SandboxSpec) -> Self {
188        value.with_inputs
189    }
190}
191
192/// RAII token for the inputs.
193///
194/// When this guard is dropped, the inputs may be unmounted/deleted.
195pub trait InputsGuard: Send {}
196
197/// Blanket implementation for all types.
198///
199/// It's up to the inputs provider whether it wants to unmount/delete inputs.
200impl<T: Send> InputsGuard for T {}
201
202/// Type erased closure providing sandbox inputs.
203///
204/// Returns an guard that has a chance to clean up/unmount inputs after the sandbox has stopped.
205type ProviderFn = Box<dyn FnMut(&Path) -> std::io::Result<Box<dyn InputsGuard>> + Send>;
206
207#[doc(hidden)]
208/// When nothing is set, you can't call build():
209///
210/// ```compile_fail
211/// use snix_build::sandbox::SandboxSpec;
212/// let _ = SandboxSpec::builder().build();
213///
214/// ```
215///
216/// When all required fields are set, can build():
217/// ```rust
218/// use snix_build::sandbox::SandboxSpec;
219/// let _ = SandboxSpec::builder()
220///     .host_workdir("/tmp/foo")
221///     .command(["/bin/sh", "-c", "echo Hello"])
222///     .sandbox_workdir("/build")
223///     .scratches(["build", "nix/store"])
224///     .build();
225/// ```
226///
227/// Can't call provide_inputs twice:
228///
229/// Compile fails
230/// ```compile_fail
231/// use snix_build::sandbox::InputsProvider;
232///
233/// fn test_inputs_provider(p: InputsProvider) {
234///   let guard1 = p.provide_inputs("/tmp");
235///   let guard2 = p.provide_inputs("/tmp");
236/// }
237/// ```
238fn _compile_tests() {}