Skip to main content

ferroday_cage/provision/
layer.rs

1//! The disposable overlay upper a layered build installs its increment into.
2//!
3//! A build pipeline provisions a base system once, then per component stages
4//! only that component's build dependencies over it — resolving against the base
5//! as already-installed, extracting the increment into an overlay upper, and
6//! configuring it in a cage rooted on an overlay of the pristine base. The base
7//! is never mutated; the increment lives in the upper, which the [`BuildLayer`]
8//! below removes when dropped.
9//!
10//! The handle is here rather than in a userland layer because nothing about it
11//! is one userland's: an upper directory, the work directory beside it, and the
12//! identity map that decides how both are removed. What differs between layers
13//! is how a base's already-installed set is read and how an increment is
14//! configured, and those stay with the layer that knows the archive.
15//!
16//! [`Rooting`] is here for the same reason. Whether a wave's cage is rooted on
17//! the tree being built or on an overlay of a base and that tree is a property
18//! of whether the build is layered, not of the archive it installs from.
19
20use std::path::{Path, PathBuf};
21
22use crate::{Cage, CageBuilder, IdentityMap};
23
24/// Where a wave of a provisioning run is rooted.
25///
26/// A full bootstrap roots its waves on the tree it is building. A layered build
27/// roots them on an overlay of the pristine base and that tree, which is the
28/// increment's writable upper: the command reads the base's own interpreter, its
29/// accounts and its package database through the merged view, and everything it
30/// writes lands in the upper, so the base comes out of the build untouched.
31///
32/// Only a layer that runs something in a cage during a bootstrap needs this —
33/// Debian's `dpkg` waves and Alpine's install scripts. The Gentoo layer runs
34/// nothing: a stage3 is extracted, and a binary package is merged and
35/// registered without its phase functions, so a `gentoo`-only build reaches
36/// [`BuildLayer`] and no further.
37#[cfg_attr(
38    not(any(feature = "debian", feature = "alpine")),
39    allow(
40        dead_code,
41        reason = "the Gentoo layer roots no cage during a bootstrap"
42    )
43)]
44#[derive(Clone, Copy)]
45pub(crate) enum Rooting<'a> {
46    /// The tree being built is the whole root.
47    Whole,
48    /// The tree being built is an increment over the base at this path.
49    Over(&'a Path),
50}
51
52impl<'a> Rooting<'a> {
53    /// The rooting a build with `base` runs under: over it when a base was
54    /// configured, on the tree alone when none was.
55    ///
56    /// The Debian layer names the two cases where it decides them and reaches
57    /// for [`Rooting`] already made; only the Alpine layer carries an
58    /// `Option<&Path>` this far, so a build without that feature has no caller.
59    #[cfg_attr(not(feature = "alpine"), allow(dead_code))]
60    pub(crate) fn over(base: Option<&'a Path>) -> Rooting<'a> {
61        match base {
62            Some(base) => Rooting::Over(base),
63            None => Rooting::Whole,
64        }
65    }
66
67    /// A cage builder rooted for the tree at `staging`, which is the whole root
68    /// or the increment's upper according to which rooting this is.
69    ///
70    /// The type's own allowance covers its variants and not this: an inherent
71    /// method is not the enum. It is dead in the same builds and for the same
72    /// reason — a `gentoo`-only build roots no cage during a bootstrap.
73    #[cfg_attr(
74        not(any(feature = "debian", feature = "alpine")),
75        allow(
76            dead_code,
77            reason = "the Gentoo layer roots no cage during a bootstrap"
78        )
79    )]
80    pub(crate) fn builder(self, staging: &Path) -> CageBuilder {
81        match self {
82            Rooting::Whole => Cage::builder().rootfs(staging),
83            // The base is the read-only lower and the tree being built is the
84            // writable upper, so the command sees the merged root and writes
85            // only into the upper.
86            Rooting::Over(base) => Cage::builder().overlay_rootfs(base, staging),
87        }
88    }
89}
90
91/// A staged build layer: the disposable overlay upper an increment was installed
92/// into, removed when the handle is dropped.
93///
94// Which provisioners return one depends on the userland features built, and a
95// link to a layer that is not compiled does not resolve. The sentence is
96// therefore written per feature combination, as is each link's definition below.
97#[cfg_attr(feature = "debian", doc = "Returned by [`Debian::stage_layer`].")]
98#[cfg_attr(feature = "alpine", doc = "Returned by [`Alpine::stage_layer`].")]
99#[cfg_attr(feature = "gentoo", doc = "Returned by [`Gentoo::stage_layer`].")]
100///
101/// The upper it owns is the writable layer of a build root — the caller roots a
102/// cage on
103/// [`overlay_rootfs(base, layer.path())`](crate::CageBuilder::overlay_rootfs)
104/// to build against the merged `base + increment` view. Dropping the handle
105/// removes the upper and the overlay's work directory beside it, reverting the
106/// increment and leaving the base pristine; the base is a separate directory
107/// the handle never touches.
108///
109/// Keep the handle alive for as long as the build root is in use, and drop it to
110/// dispose of the increment. The removal is best-effort at drop time — a drop
111/// cannot report an error — so a caller that must observe a disposal failure
112/// removes the upper directory explicitly through
113/// [`provision::remove`](crate::provision::remove) before dropping the handle,
114/// which then finds nothing to remove.
115///
116#[cfg_attr(
117    feature = "debian",
118    doc = "[`Debian::stage_layer`]: crate::provision::debian::Debian::stage_layer"
119)]
120#[cfg_attr(
121    feature = "alpine",
122    doc = "[`Alpine::stage_layer`]: crate::provision::alpine::Alpine::stage_layer"
123)]
124#[cfg_attr(
125    feature = "gentoo",
126    doc = "[`Gentoo::stage_layer`]: crate::provision::gentoo::Gentoo::stage_layer"
127)]
128#[derive(Debug)]
129pub struct BuildLayer {
130    /// The overlay upper the increment was installed into: the build root's
131    /// writable layer.
132    upper: PathBuf,
133    /// The overlay work directory beside the upper. The kernel leaves a mode-`0`
134    /// directory here that a plain recursive delete cannot descend, so disposal
135    /// treats it specially.
136    work: PathBuf,
137    /// The identity map the increment was configured under, which decides how the
138    /// upper and work directory are removed.
139    identity_map: IdentityMap,
140}
141
142impl BuildLayer {
143    /// Builds a layer handle over a staged `upper`, computing the sibling work
144    /// directory the overlay-rooted cage manages so disposal removes both.
145    pub(crate) fn new(upper: &Path, identity_map: IdentityMap) -> BuildLayer {
146        // The work directory is the same sibling an overlay-rooted cage derives
147        // from this upper; a canonical upper always has a parent and file name,
148        // so the fallback to the upper itself is unreachable in practice and
149        // simply makes disposal a no-op rather than panicking.
150        let work = crate::spec::overlay_work_dir(upper).unwrap_or_else(|| upper.to_path_buf());
151        BuildLayer {
152            upper: upper.to_path_buf(),
153            work,
154            identity_map,
155        }
156    }
157
158    /// The overlay upper directory, the build root's writable layer.
159    ///
160    /// Pass it as the upper of
161    /// [`CageBuilder::overlay_rootfs`](crate::CageBuilder::overlay_rootfs), with
162    /// the base as the lower, to root a build cage on the merged view.
163    pub fn path(&self) -> &Path {
164        &self.upper
165    }
166}
167
168impl Drop for BuildLayer {
169    fn drop(&mut self) {
170        // Under the single-identity map every entry is the caller's own, but the
171        // overlay leaves a mode-`0` work directory a plain delete cannot descend;
172        // a chmod-aware removal restores traversable permissions as it goes.
173        // Under a range map the increment and the work directory hold
174        // subordinate-owned entries the caller cannot chmod, so the removal
175        // escalates through the identity map, where root inside it owns them.
176        if matches!(self.identity_map, IdentityMap::Single) {
177            crate::host::force_remove_dir_all(&self.upper);
178            crate::host::force_remove_dir_all(&self.work);
179        } else {
180            // The map the increment was staged under, rather than the
181            // subordinate default: an explicit range map allocates ids the
182            // bundled chain would not, and the removal has to re-enter the map
183            // the tree's ownership was written in.
184            //
185            // And no lock: an upper is a directory a caller named as a build
186            // layer, not a published rootfs, so `<upper>.lock` is a path this
187            // never created and may not delete. The work directory is the
188            // layer's own and has no lock either.
189            for tree in [&self.upper, &self.work] {
190                let _ = crate::provision::Remove::new(tree)
191                    .map(self.identity_map.clone())
192                    .remove_lock(false)
193                    .run();
194            }
195        }
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::scratch::Scratch;
203
204    #[test]
205    fn a_single_map_layer_drop_removes_the_upper_and_work_tree() {
206        // A single-identity layer disposes of its upper and the overlay work
207        // directory, including a mode-`0` work/work directory a plain recursive
208        // delete cannot descend — the artifact an overlay mount leaves behind.
209        use std::os::unix::fs::PermissionsExt;
210
211        let dir = Scratch::for_test("layer", "dispose-single");
212        let upper = dir.join("upper");
213        std::fs::create_dir_all(upper.join("usr/bin")).unwrap();
214        std::fs::write(upper.join("usr/bin/tool"), b"x").unwrap();
215
216        let work = crate::spec::overlay_work_dir(&upper).unwrap();
217        std::fs::create_dir_all(work.join("work")).unwrap();
218        std::fs::write(work.join("index"), b"i").unwrap();
219        // The overlay artifact: a directory with no permissions at all.
220        std::fs::set_permissions(work.join("work"), std::fs::Permissions::from_mode(0o000))
221            .unwrap();
222
223        let layer = BuildLayer::new(&upper, IdentityMap::Single);
224        assert_eq!(layer.path(), upper.as_path());
225        drop(layer);
226
227        assert!(!upper.exists(), "the upper is removed");
228        assert!(
229            !work.exists(),
230            "the work directory is removed despite mode-0"
231        );
232    }
233
234    #[test]
235    fn a_layer_drop_leaves_a_lock_sibling_it_never_created() {
236        // An upper is a directory a caller named as a build layer, not a
237        // published rootfs -- `ensure` wrote no lock beside it, so `<upper>.lock`
238        // is an unrelated path, and deleting it is a removal outside the tree
239        // the layer owns. A build root and its layers routinely sit beside the
240        // published trees a cache holds.
241        let dir = Scratch::for_test("layer", "dispose-lock");
242        let upper = dir.join("upper");
243        std::fs::create_dir_all(&upper).unwrap();
244        let sibling = dir.join("upper.lock");
245        std::fs::write(&sibling, b"someone else's").unwrap();
246
247        // Asserted for the single-identity map, whose removal never touched the
248        // sibling, and for the escalating one, which did.
249        for map in [IdentityMap::Single, IdentityMap::Subordinate] {
250            std::fs::create_dir_all(&upper).unwrap();
251            drop(BuildLayer::new(&upper, map));
252            assert!(!upper.exists(), "the upper is removed");
253            assert!(
254                sibling.exists(),
255                "a lock the layer never created went with it",
256            );
257        }
258        assert_eq!(std::fs::read(&sibling).unwrap(), b"someone else's");
259    }
260}