snix_build/sandbox/
mod.rs

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