Skip to main content

libc_abi_tools/provider/
prepare.rs

1//! Prepares a provider into the cache in one of the delivery modes. Every mode
2//! produces the same layout; see docs/PROVIDER-DELIVERY.md.
3use super::{
4    build::Tools,
5    cache::{self, Entry, Mode},
6    json::Value,
7    pins,
8};
9use crate::{glibc::Snapshot, libgcc, sha256, stub};
10use std::{
11    fs,
12    path::{Path, PathBuf},
13    process::{Command, Stdio},
14};
15
16/// Options shared by every mode.
17#[derive(Clone, Debug)]
18pub struct Options {
19    /// The cache root ([`cache::default_root`]).
20    pub root: PathBuf,
21    /// The host tools: GCC (whose `crtbegin.o`/`crtend.o` the provider uses), and
22    /// in `source` mode the binutils that build and verify the objects.
23    pub tools: Tools,
24    /// Prepare again even if the cache has this provider.
25    pub force: bool,
26    /// `container`: podman or docker; found on PATH when `None`.
27    pub engine: Option<PathBuf>,
28    /// `container`: a directory of previously fetched `glibc-X.Y.tar.xz`
29    /// archives, used before downloading (also `ABI_GLIBC_SOURCE_DIR`).
30    pub archives: Option<PathBuf>,
31    /// `container`: move the build's work tree (sources, the installed glibc,
32    /// logs) here instead of deleting it. The directory must not exist.
33    pub keep_work: Option<PathBuf>,
34}
35
36impl Options {
37    /// Options for the cache at ROOT with the default tools found on PATH (`gcc`,
38    /// `ar`, `readelf`, `as`, `ld`), no forced preparation, and the default
39    /// container settings.
40    pub fn new(root: PathBuf) -> Self {
41        Self {
42            root,
43            tools: Tools::default(),
44            force: false,
45            engine: None,
46            archives: None,
47            keep_work: None,
48        }
49    }
50}
51
52/// A prepared provider.
53#[derive(Clone, Debug)]
54pub struct Prepared {
55    /// Where the provider is in the cache.
56    pub entry: Entry,
57    /// Whether the cache already had it.
58    pub reused: bool,
59    /// What the preparation found worth reporting.
60    pub notes: Vec<String>,
61}
62
63/// Prepares RELEASE in MODE, or returns the cached provider with the same inputs.
64/// SNAPSHOT is the release's ABI lists, from which the stubs are generated.
65pub fn prepare(
66    release: &str,
67    mode: Mode,
68    snapshot: &Snapshot,
69    options: &Options,
70) -> Result<Prepared, String> {
71    let release = cache::release(release)?;
72    if snapshot.release.to_string() != release {
73        return Err("the ABI snapshot is for another release".into());
74    }
75    if !cfg!(all(target_os = "linux", target_arch = "x86_64")) {
76        return Err("providers are prepared on x86-64 Linux".into());
77    }
78    match mode {
79        Mode::Source => source(release, snapshot, options),
80        Mode::Prebuilt => Err(
81            "prebuilt mode is not available yet: shipping CI-built glibc objects awaits \
82             a legal review; use source (the default) or container"
83                .into(),
84        ),
85        Mode::Container => container(release, snapshot, options),
86    }
87}
88
89/// The content hash RELEASE would be prepared under in MODE now, without
90/// preparing it; `None` if MODE is unavailable.
91pub fn current_hash(
92    release: &str,
93    mode: Mode,
94    snapshot: &Snapshot,
95    tools: &Tools,
96) -> Result<Option<String>, String> {
97    let host = Host::probe(tools)?;
98    match mode {
99        Mode::Source => source_inputs(release, snapshot, &host),
100        Mode::Prebuilt => Ok(None),
101        Mode::Container => Ok(Some(container_hash(release, snapshot, &host)?)),
102    }
103}
104
105/// The host toolchain facts every mode depends on.
106struct Host {
107    versions: Vec<(&'static str, String)>,
108    gcc_major: u32,
109    startup: PathBuf,
110    startup_hashes: String,
111}
112
113impl Host {
114    fn probe(tools: &Tools) -> Result<Self, String> {
115        let versions = tools.versions()?;
116        let startup = tools.gcc_startup()?;
117        let mut startup_hashes = String::new();
118        for name in ["crtbegin.o", "crtend.o"] {
119            let bytes = fs::read(startup.join(name)).map_err(|e| e.to_string())?;
120            startup_hashes.push_str(&format!(
121                "{name} {}\n",
122                sha256::hex(&sha256::sha256(&bytes))
123            ));
124        }
125        Ok(Self {
126            versions,
127            gcc_major: tools.gcc_major()?,
128            startup,
129            startup_hashes,
130        })
131    }
132
133    fn inputs(&self) -> Vec<(String, Vec<u8>)> {
134        let tools: String = self
135            .versions
136            .iter()
137            .map(|(name, version)| format!("{name} {version}\n"))
138            .collect();
139        vec![
140            ("tools".into(), tools.into_bytes()),
141            (
142                "gcc-startup".into(),
143                format!("{}\n{}", self.startup.display(), self.startup_hashes).into_bytes(),
144            ),
145        ]
146    }
147
148    fn manifest(&self) -> Vec<(String, Value)> {
149        vec![
150            (
151                "tools".into(),
152                Value::Object(
153                    self.versions
154                        .iter()
155                        .map(|(name, version)| (name.to_string(), Value::String(version.clone())))
156                        .collect(),
157                ),
158            ),
159            ("gcc_major".into(), Value::Integer(self.gcc_major.into())),
160            (
161                "gcc_startup".into(),
162                Value::String(self.startup.to_string_lossy().into_owned()),
163            ),
164        ]
165    }
166}
167
168fn common_inputs(release: &str, mode: Mode, snapshot: &Snapshot) -> Vec<(String, Vec<u8>)> {
169    vec![
170        ("libc-abi-tools".into(), env!("CARGO_PKG_VERSION").into()),
171        ("release".into(), release.into()),
172        ("mode".into(), mode.name().into()),
173        ("abi-lists".into(), snapshot.to_tsv(None).into_bytes()),
174        ("libgcc".into(), libgcc::INTERFACE.into()),
175    ]
176}
177
178fn hash_of(inputs: &[(String, Vec<u8>)]) -> String {
179    let borrowed: Vec<(&str, &[u8])> = inputs
180        .iter()
181        .map(|(name, bytes)| (name.as_str(), bytes.as_slice()))
182        .collect();
183    cache::content_hash(&borrowed)
184}
185
186/// A new temporary directory next to ENTRY, removed again on failure.
187struct Stage {
188    dir: PathBuf,
189}
190
191impl Stage {
192    fn new(entry: &Entry) -> Result<Self, String> {
193        let parent = entry.dir.parent().expect("entries have a parent");
194        fs::create_dir_all(parent).map_err(|e| format!("{}: {e}", parent.display()))?;
195        let name = entry.dir.file_name().expect("named").to_string_lossy();
196        let dir = parent.join(format!(".tmp-{name}-{}", std::process::id()));
197        cache::remove_tree(&dir)?;
198        fs::create_dir(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
199        Ok(Self { dir })
200    }
201
202    /// Writes the manifest and descriptor and moves the stage to ENTRY.
203    fn finish(
204        self,
205        entry: &Entry,
206        startup: &Path,
207        mut members: Vec<(String, Value)>,
208        force: bool,
209    ) -> Result<(), String> {
210        let mut manifest = vec![
211            (
212                "format".into(),
213                Value::String(cache::MANIFEST_FORMAT.into()),
214            ),
215            ("target".into(), Value::String(cache::TARGET.into())),
216            ("glibc".into(), Value::String(entry.release.clone())),
217            ("mode".into(), Value::String(entry.mode.name().into())),
218            ("hash".into(), Value::String(entry.hash.clone())),
219            (
220                "libc_abi_tools".into(),
221                Value::String(env!("CARGO_PKG_VERSION").into()),
222            ),
223        ];
224        manifest.append(&mut members);
225        manifest.push(("files".into(), cache::inventory(&self.dir)?));
226        fs::write(
227            self.dir.join("manifest.json"),
228            Value::Object(manifest).to_text(),
229        )
230        .map_err(|e| e.to_string())?;
231        let text = cache::descriptor_text(&entry.dir, &entry.release, startup)?;
232        fs::write(self.dir.join("provider.txt"), text).map_err(|e| e.to_string())?;
233        if entry.dir.exists() {
234            if !force {
235                // Prepared concurrently with the same inputs; keep the first.
236                return Ok(());
237            }
238            cache::remove_tree(&entry.dir)?;
239        }
240        fs::rename(&self.dir, &entry.dir).map_err(|e| {
241            format!(
242                "cannot move {} to {}: {e}",
243                self.dir.display(),
244                entry.dir.display()
245            )
246        })
247    }
248}
249
250impl Drop for Stage {
251    fn drop(&mut self) {
252        let _ = cache::remove_tree(&self.dir);
253    }
254}
255
256fn reuse(entry: &Entry, force: bool) -> Option<Prepared> {
257    (!force && entry.dir.join("manifest.json").is_file()).then(|| Prepared {
258        entry: entry.clone(),
259        reused: true,
260        notes: Vec::new(),
261    })
262}
263
264/// Writes the stubs, the loader's link stub and the `libgcc_s` stub.
265fn write_stubs(stage: &Path, tools: &Tools, stubs: &[(&str, stub::Sources)]) -> Result<(), String> {
266    cache::write_glibc_stubs(tools, &stage.join("stubs"), stubs)?;
267    cache::write_libgcc_stub(tools, &stage.join("compiler-runtime"))
268}
269
270fn copy(from: &Path, to: &Path) -> Result<(), String> {
271    if let Some(parent) = to.parent() {
272        fs::create_dir_all(parent).map_err(|e| e.to_string())?;
273    }
274    fs::copy(from, to)
275        .map(|_| ())
276        .map_err(|e| format!("cannot copy {}: {e}", from.display()))
277}
278
279#[cfg(feature = "provider-sources")]
280fn source_inputs(
281    release: &str,
282    snapshot: &Snapshot,
283    host: &Host,
284) -> Result<Option<String>, String> {
285    let sources = glibc_provider_sources::Sources::bundled()?;
286    let data = sources.release(release)?;
287    let mut inputs = common_inputs(release, Mode::Source, snapshot);
288    for table in glibc_provider_sources::TABLES {
289        inputs.push((table.into(), data.table(table)?.as_bytes().to_vec()));
290    }
291    inputs.push((
292        "accepted-differences".into(),
293        glibc_provider_sources::ACCEPTED_DIFFERENCES.into(),
294    ));
295    inputs.extend(host.inputs());
296    Ok(Some(hash_of(&inputs)))
297}
298
299#[cfg(not(feature = "provider-sources"))]
300fn source_inputs(_: &str, _: &Snapshot, _: &Host) -> Result<Option<String>, String> {
301    Err(NO_SOURCES.into())
302}
303
304#[cfg(not(feature = "provider-sources"))]
305const NO_SOURCES: &str =
306    "built without the provider-sources feature, so source mode is unavailable; use container";
307
308#[cfg(not(feature = "provider-sources"))]
309fn source(_: &str, _: &Snapshot, _: &Options) -> Result<Prepared, String> {
310    Err(NO_SOURCES.into())
311}
312
313/// `source` mode: compile the recorded glibc sources with the host's GCC and
314/// verify every object against the container build's recorded interface.
315#[cfg(feature = "provider-sources")]
316fn source(release: &str, snapshot: &Snapshot, options: &Options) -> Result<Prepared, String> {
317    let tools = &options.tools;
318    let host = Host::probe(tools)?;
319    let hash = source_inputs(release, snapshot, &host)?;
320    let entry = cache::entry(&options.root, release, Mode::Source, &hash.expect("source"));
321    if let Some(prepared) = reuse(&entry, options.force) {
322        return Ok(prepared);
323    }
324    let sources = glibc_provider_sources::Sources::bundled()?;
325    let data = sources.release(release)?;
326    let stage = Stage::new(&entry)?;
327    let work = stage.dir.join("work");
328    fs::create_dir(&work).map_err(|e| e.to_string())?;
329    let inputs = work.join("inputs");
330    data.write_sources(&inputs)?;
331    let values = data
332        .table("release.tsv")?
333        .lines()
334        .skip(1)
335        .filter_map(|line| line.split_once('\t'))
336        .map(|(k, v)| (k.to_string(), v.to_string()))
337        .collect();
338    let objects = data
339        .objects()?
340        .into_iter()
341        .map(|o| super::build::Object {
342            target: o.target.into(),
343            member: o.member,
344            object: o.object.into(),
345            kind: o.kind.into(),
346            directory: o.directory.into(),
347            command: o.command.into(),
348        })
349        .collect();
350    let recorded = super::build::Recorded {
351        release,
352        values,
353        objects,
354        interfaces: data.table("interfaces.tsv")?,
355        accepted: glibc_provider_sources::ACCEPTED_DIFFERENCES,
356    };
357    let built = super::build::build(&recorded, &inputs, tools)?;
358    let mut report = String::from("object\tstatus\tdetail\n");
359    for verdict in &built.verdicts {
360        report.push_str(&format!(
361            "{}\t{}\t{}\n",
362            verdict.object,
363            verdict.status,
364            verdict.detail.replace(['\t', '\n'], " ")
365        ));
366    }
367    if !built.verified() {
368        let failed = entry
369            .dir
370            .with_file_name(format!("source-{}.differences.tsv", entry.hash));
371        let _ = fs::write(&failed, &report);
372        let differing: Vec<&str> = built
373            .verdicts
374            .iter()
375            .filter(|v| v.status == "differs")
376            .map(|v| v.object.as_str())
377            .collect();
378        return Err(format!(
379            "glibc {release}: GCC {} built objects that differ from the qualified container \
380             build ({}; details in {}). Not using them: use --mode container, or a GCC in the \
381             qualified range {}-{}",
382            host.gcc_major,
383            differing.join(", "),
384            failed.display(),
385            super::build::QUALIFIED_GCC.start(),
386            super::build::QUALIFIED_GCC.end()
387        ));
388    }
389    let sysroot = stage.dir.join("sysroot");
390    fs::create_dir_all(sysroot.join("usr")).map_err(|e| e.to_string())?;
391    data.write_headers(&sysroot.join("usr/include"))?;
392    for (target, path) in &built.targets {
393        copy(path, &sysroot.join("usr/lib64").join(target))?;
394    }
395    let table = data.table("libraries.tsv")?;
396    let stubs = stub::glibc_from_table(snapshot, table)?;
397    write_stubs(&stage.dir, tools, &stubs)?;
398    fs::write(stage.dir.join("libraries.tsv"), table).map_err(|e| e.to_string())?;
399    // No real loader exists in this mode; the descriptor's sysroot holds the
400    // loader's link stub, which is never run.
401    copy(
402        &stage.dir.join("stubs/ld-linux-x86-64.so.2"),
403        &sysroot.join("lib64/ld-linux-x86-64.so.2"),
404    )?;
405    fs::write(stage.dir.join("verification.tsv"), &report).map_err(|e| e.to_string())?;
406    cache::remove_tree(&work)?;
407    let mut notes = vec![format!(
408        "verified every object against the container build ({})",
409        built.summary()
410    )];
411    if !super::build::QUALIFIED_GCC.contains(&host.gcc_major) {
412        notes.push(format!(
413            "GCC {} is outside the range qualified in CI ({}-{}); the local verification passed",
414            host.gcc_major,
415            super::build::QUALIFIED_GCC.start(),
416            super::build::QUALIFIED_GCC.end()
417        ));
418    }
419    let mut members = vec![(
420        "origin".into(),
421        Value::Object(vec![
422            (
423                "data".into(),
424                Value::String("glibc-provider-sources".into()),
425            ),
426            (
427                "archive".into(),
428                Value::String(data.value("archive").unwrap_or("").into()),
429            ),
430            (
431                "archive_sha256".into(),
432                Value::String(data.value("archive_sha256").unwrap_or("").into()),
433            ),
434            (
435                "loader".into(),
436                Value::String("sysroot/lib64/ld-linux-x86-64.so.2 is the link stub".into()),
437            ),
438        ]),
439    )];
440    members.extend(host.manifest());
441    members.push((
442        "gcc_qualified".into(),
443        Value::Bool(super::build::QUALIFIED_GCC.contains(&host.gcc_major)),
444    ));
445    members.push(("verification".into(), Value::String(built.summary())));
446    stage.finish(&entry, &host.startup, members, options.force)?;
447    Ok(Prepared {
448        entry,
449        reused: false,
450        notes,
451    })
452}
453
454fn container_hash(release: &str, snapshot: &Snapshot, host: &Host) -> Result<String, String> {
455    let pins = pins::pins(release)?;
456    let mut inputs = common_inputs(release, Mode::Container, snapshot);
457    inputs.push(("archive".into(), pins.archive_sha256()?.into()));
458    inputs.push(("image".into(), pins.image()?.into()));
459    inputs.push(("script".into(), pins.script.into()));
460    inputs.extend(host.inputs());
461    Ok(hash_of(&inputs))
462}
463
464/// podman, else docker, from PATH.
465fn find_engine() -> Result<PathBuf, String> {
466    for engine in ["podman", "docker"] {
467        let found = Command::new(engine)
468            .arg("--version")
469            .stdout(Stdio::null())
470            .stderr(Stdio::null())
471            .status()
472            .is_ok_and(|s| s.success());
473        if found {
474            return Ok(engine.into());
475        }
476    }
477    Err("container mode needs podman or docker on PATH (or --engine)".into())
478}
479
480/// Runs a command, sending its output to LOG.
481fn logged(command: &mut Command, log: &Path) -> Result<(), String> {
482    let file = fs::OpenOptions::new()
483        .create(true)
484        .append(true)
485        .open(log)
486        .map_err(|e| e.to_string())?;
487    let error = file.try_clone().map_err(|e| e.to_string())?;
488    let status = command
489        .stdout(file)
490        .stderr(error)
491        .status()
492        .map_err(|e| format!("cannot run {:?}: {e}", command.get_program()))?;
493    if !status.success() {
494        return Err(format!(
495            "{:?} failed; see {}",
496            command.get_program(),
497            log.display()
498        ));
499    }
500    Ok(())
501}
502
503/// Places `glibc-X.Y.tar.xz` in WORK, from ARCHIVES or the GNU mirrors, and
504/// checks its pinned SHA-256.
505fn fetch(
506    pins: &pins::Pins,
507    work: &Path,
508    archives: Option<&Path>,
509    log: &Path,
510) -> Result<(), String> {
511    let name = pins.archive();
512    let target = work.join(&name);
513    let local = archives
514        .map(Path::to_path_buf)
515        .or_else(|| std::env::var_os("ABI_GLIBC_SOURCE_DIR").map(PathBuf::from))
516        .map(|dir| dir.join(&name))
517        .filter(|path| path.is_file());
518    if let Some(path) = local {
519        copy(&path, &target)?;
520    } else {
521        let mut fetched = false;
522        for base in [
523            "https://ftp.gnu.org/gnu/glibc",
524            "https://mirrors.kernel.org/gnu/glibc",
525        ] {
526            let mut curl = Command::new("curl");
527            curl.args(["-fL", "--retry", "2", "--connect-timeout", "30", "-o"])
528                .arg(&target)
529                .arg(format!("{base}/{name}"));
530            if logged(&mut curl, log).is_ok() {
531                fetched = true;
532                break;
533            }
534            let _ = fs::remove_file(&target);
535        }
536        if !fetched {
537            return Err(format!("cannot download {name}; see {}", log.display()));
538        }
539    }
540    let bytes = fs::read(&target).map_err(|e| e.to_string())?;
541    if sha256::hex(&sha256::sha256(&bytes)) != pins.archive_sha256()? {
542        return Err(format!("{name} does not match its pinned SHA-256"));
543    }
544    Ok(())
545}
546
547/// `container` mode: run the pinned container build that CI runs, and take the
548/// provider's glibc files from its installed tree.
549fn container(release: &str, snapshot: &Snapshot, options: &Options) -> Result<Prepared, String> {
550    let tools = &options.tools;
551    let host = Host::probe(tools)?;
552    let pins = pins::pins(release)?;
553    let hash = container_hash(release, snapshot, &host)?;
554    let entry = cache::entry(&options.root, release, Mode::Container, &hash);
555    if let Some(keep) = &options.keep_work {
556        if keep.exists() {
557            return Err(format!("--keep-work {} already exists", keep.display()));
558        }
559    }
560    if let Some(prepared) = reuse(&entry, options.force) {
561        if options.keep_work.is_some() {
562            return Err(format!(
563                "the glibc {release} container provider is cached, so there is no build to keep; add --force"
564            ));
565        }
566        return Ok(prepared);
567    }
568    let engine = match &options.engine {
569        Some(engine) => engine.clone(),
570        None => find_engine()?,
571    };
572    let stage = Stage::new(&entry)?;
573    let work = stage.dir.join("work");
574    fs::create_dir(&work).map_err(|e| e.to_string())?;
575    let log = entry.dir.with_file_name(format!("container-{hash}.log"));
576    let _ = fs::remove_file(&log);
577    fetch(pins, &work, options.archives.as_deref(), &log)?;
578    fs::write(work.join("build-inputs.sh"), pins.script).map_err(|e| e.to_string())?;
579    let image = pins.image()?;
580    logged(Command::new(&engine).args(["pull", image]), &log)?;
581    let mount = format!("{}:/work", work.to_str().ok_or("paths must be UTF-8")?);
582    let built = logged(
583        Command::new(&engine).args([
584            "run",
585            "--rm",
586            "-v",
587            &mount,
588            image,
589            "bash",
590            "/work/build-inputs.sh",
591        ]),
592        &log,
593    );
594    // Docker leaves the build's files owned by root; hand them back.
595    let docker = engine
596        .file_name()
597        .is_some_and(|n| n.to_string_lossy().contains("docker"));
598    if docker {
599        let id = |flag: &str| -> Result<String, String> {
600            let out = Command::new("id")
601                .arg(flag)
602                .output()
603                .map_err(|e| e.to_string())?;
604            Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
605        };
606        let owner = format!("{}:{}", id("-u")?, id("-g")?);
607        logged(
608            Command::new(&engine).args([
609                "run", "--rm", "-v", &mount, image, "chown", "-R", &owner, "/work",
610            ]),
611            &log,
612        )?;
613    }
614    built.map_err(|e| format!("the glibc {release} container build failed: {e}"))?;
615    let installed = work.join("source");
616    let sysroot = stage.dir.join("sysroot");
617    fs::create_dir_all(sysroot.join("usr")).map_err(|e| e.to_string())?;
618    cache::copy_tree(&installed.join("usr/include"), &sysroot.join("usr/include"))?;
619    let mut targets = vec!["crt1.o", "crti.o", "crtn.o", "libc_nonshared.a"];
620    if installed.join("usr/lib64/libpthread_nonshared.a").is_file() {
621        targets.push("libpthread_nonshared.a");
622    }
623    for target in targets {
624        let path = format!("usr/lib64/{target}");
625        copy(&installed.join(&path), &sysroot.join(&path))?;
626    }
627    // The real libraries, with the loader: a runtime for tests.
628    cache::copy_tree(&installed.join("lib64"), &sysroot.join("lib64"))?;
629    let table = stub::glibc_table(snapshot, &installed.join("lib64"))?;
630    let stubs = stub::glibc_from_table(snapshot, &table)?;
631    write_stubs(&stage.dir, tools, &stubs)?;
632    fs::write(stage.dir.join("libraries.tsv"), &table).map_err(|e| e.to_string())?;
633    // The builder's own compiler runtime, to run tests with (never to link).
634    for dir in ["prebuilt/usr/lib64", "prebuilt/lib64"] {
635        let runtime = work.join(dir).join(libgcc::SONAME);
636        if runtime.exists() {
637            copy(
638                &runtime,
639                &stage.dir.join("run-runtime").join(libgcc::SONAME),
640            )?;
641            break;
642        }
643    }
644    match &options.keep_work {
645        Some(keep) => {
646            if fs::rename(&work, keep).is_err() {
647                cache::copy_tree(&work, keep)?;
648                cache::remove_tree(&work)?;
649            }
650        }
651        None => cache::remove_tree(&work)?,
652    }
653    let mut members = vec![(
654        "origin".into(),
655        Value::Object(vec![
656            ("archive".into(), Value::String(pins.archive())),
657            (
658                "archive_sha256".into(),
659                Value::String(pins.archive_sha256()?.into()),
660            ),
661            ("image".into(), Value::String(image.into())),
662            (
663                "engine".into(),
664                Value::String(engine.to_string_lossy().into_owned()),
665            ),
666        ]),
667    )];
668    members.extend(host.manifest());
669    stage.finish(&entry, &host.startup, members, options.force)?;
670    let _ = fs::remove_file(&log);
671    Ok(Prepared {
672        entry,
673        reused: false,
674        notes: vec![format!("built by the pinned container build in {image}")],
675    })
676}