1use 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#[derive(Clone, Debug)]
18pub struct Options {
19 pub root: PathBuf,
21 pub tools: Tools,
24 pub force: bool,
26 pub engine: Option<PathBuf>,
28 pub archives: Option<PathBuf>,
31 pub keep_work: Option<PathBuf>,
34}
35
36impl Options {
37 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#[derive(Clone, Debug)]
54pub struct Prepared {
55 pub entry: Entry,
57 pub reused: bool,
59 pub notes: Vec<String>,
61}
62
63pub 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
89pub 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
105struct 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
186struct 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 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 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
264fn 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#[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 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
464fn 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
480fn 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
503fn 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
547fn 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 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 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 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}