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() {}