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