| 138 |
138 |
|
)
|
| 139 |
139 |
|
}
|
| 140 |
140 |
|
|
|
141 |
+ |
/// The three arms of the font guard, which is `if PROFILE / elif GUI / else`.
|
|
142 |
+ |
///
|
|
143 |
+ |
/// [`if_branches`] splits on the one branch-joining `else` and is right for
|
|
144 |
+ |
/// every two-armed conditional in this file. The guard at the end of the
|
|
145 |
+ |
/// Containerfile is not one: its server half splits again on `$GUI`, because
|
|
146 |
+ |
/// `PROFILE=server GUI=tauri` is a real machine (astra, which builds the arm64
|
|
147 |
+ |
/// Tauri releases natively) and it carries fontconfig underneath GTK and WebKit
|
|
148 |
+ |
/// while carrying no face at all. Handing that block to `if_branches` returns a
|
|
149 |
+ |
/// "server" half that is only the `GUI=tauri` arm, and the absence claims would
|
|
150 |
+ |
/// then be asserted against the branch that deliberately does not make them.
|
|
151 |
+ |
///
|
|
152 |
+ |
/// Returns (client, server with GUI=none, server with any other GUI).
|
|
153 |
+ |
fn three_arms(block: &str) -> (String, String, String) {
|
|
154 |
+ |
let elif = block
|
|
155 |
+ |
.find("elif ")
|
|
156 |
+ |
.unwrap_or_else(|| panic!("expected an `elif` arm in this block:\n{block}"));
|
|
157 |
+ |
let client = block[..elif].to_string();
|
|
158 |
+ |
let rest = &block[elif..];
|
|
159 |
+ |
let (headless, gui) = if_branches(rest);
|
|
160 |
+ |
(client, headless, gui)
|
|
161 |
+ |
}
|
|
162 |
+ |
|
| 141 |
163 |
|
/// Whether the conditional the branches came from tests `$PROFILE`.
|
| 142 |
164 |
|
///
|
| 143 |
165 |
|
/// [`if_branches`] splits on the `else` and says nothing about what was
|
| 282 |
304 |
|
);
|
| 283 |
305 |
|
}
|
| 284 |
306 |
|
|
| 285 |
|
- |
/// Run an extracted block against a fake root, with `$PROFILE` set and only the
|
| 286 |
|
- |
/// stubs on `PATH`.
|
|
307 |
+ |
/// Run an extracted block against a fake root, with `$PROFILE` and `$GUI` set
|
|
308 |
+ |
/// and only the stubs on `PATH`.
|
| 287 |
309 |
|
///
|
| 288 |
310 |
|
/// `PATH` is the fake root's bin directory and nothing else, which is the point
|
| 289 |
311 |
|
/// on `server`: the branch asserts `fc-list` is not reachable, and that claim
|
| 290 |
312 |
|
/// can only be tested where the host's own fontconfig cannot answer it.
|
| 291 |
|
- |
fn run_block(script: &str, profile: &str, bin: &Path) -> std::process::Output {
|
|
313 |
+ |
///
|
|
314 |
+ |
/// `$GUI` is passed rather than defaulted because the server branch splits on
|
|
315 |
+ |
/// it, and a default here would decide which half of that split every test in
|
|
316 |
+ |
/// this file exercises without saying so.
|
|
317 |
+ |
fn run_block(script: &str, profile: &str, gui: &str, bin: &Path) -> std::process::Output {
|
| 292 |
318 |
|
Command::new(shell())
|
| 293 |
319 |
|
.arg("-c")
|
| 294 |
320 |
|
.arg(script)
|
| 295 |
321 |
|
.env("PROFILE", profile)
|
|
322 |
+ |
.env("GUI", gui)
|
| 296 |
323 |
|
.env("PATH", format!("{}", bin.display()))
|
| 297 |
324 |
|
.output()
|
| 298 |
325 |
|
.expect("running the extracted block")
|
| 469 |
496 |
|
#[test]
|
| 470 |
497 |
|
fn the_server_branch_asserts_absence_rather_than_presence() {
|
| 471 |
498 |
|
let guard = block_with("no font covers");
|
| 472 |
|
- |
let (_, server) = if_branches(&guard);
|
|
499 |
+ |
let (_, headless, gui) = three_arms(&guard);
|
|
500 |
+ |
|
|
501 |
+ |
for (label, arm) in [("GUI=none", &headless), ("GUI=tauri", &gui)] {
|
|
502 |
+ |
assert!(
|
|
503 |
+ |
arm.contains("test ! -e") && arm.contains("/usr/share/fonts/quasi"),
|
|
504 |
+ |
"the {label} server arm does not prove the house faces are absent:\n{arm}",
|
|
505 |
+ |
);
|
|
506 |
+ |
assert!(
|
|
507 |
+ |
!arm.contains("fc-list ':family=Quasi"),
|
|
508 |
+ |
"the {label} server arm asks fontconfig about the house faces, on a profile \
|
|
509 |
+ |
that installs no face whatever it carries to read one with:\n{arm}",
|
|
510 |
+ |
);
|
|
511 |
+ |
}
|
|
512 |
+ |
assert!(
|
|
513 |
+ |
headless.contains("! command -v fc-list"),
|
|
514 |
+ |
"the headless server arm does not prove fontconfig itself is absent, so a \
|
|
515 |
+ |
package dragging it back in would go unreported:\n{headless}",
|
|
516 |
+ |
);
|
|
517 |
+ |
}
|
|
518 |
+ |
|
|
519 |
+ |
/// The other direction, which is the half that is easy to get wrong.
|
|
520 |
+ |
///
|
|
521 |
+ |
/// A server that builds Tauri apps has to HAVE fontconfig: gtk3-devel and
|
|
522 |
+ |
/// webkit2gtk4.1-devel bring the GTK and WebKit runtime and fontconfig sits
|
|
523 |
+ |
/// under it, and a build host missing it fails at link time rather than at boot.
|
|
524 |
+ |
/// So that arm asserts presence, and asserting absence there is what the first
|
|
525 |
+ |
/// astra mint hit at step 101 of 103 on 2026-09-04.
|
|
526 |
+ |
///
|
|
527 |
+ |
/// What must NOT weaken with it is the claim that actually mattered: no face.
|
|
528 |
+ |
/// Tools that answer questions about faces are the price of building the apps;
|
|
529 |
+ |
/// a face on a machine that rasterises nothing is payload with no reader, and
|
|
530 |
+ |
/// that is still refused. The loop above covers the face half for both arms;
|
|
531 |
+ |
/// this covers the package half, which is the one a careless edit would drop.
|
|
532 |
+ |
#[test]
|
|
533 |
+ |
fn the_tauri_server_arm_asserts_presence_and_still_refuses_every_face() {
|
|
534 |
+ |
let guard = block_with("no font covers");
|
|
535 |
+ |
let (_, _, gui) = three_arms(&guard);
|
| 473 |
536 |
|
|
| 474 |
537 |
|
assert!(
|
| 475 |
|
- |
server.contains("test ! -e") && server.contains("/usr/share/fonts/quasi"),
|
| 476 |
|
- |
"the server branch does not prove the house faces are absent:\n{server}",
|
|
538 |
+ |
gui.contains("command -v fc-list") && !gui.contains("! command -v fc-list"),
|
|
539 |
+ |
"the GUI-bearing server arm does not require fontconfig, so a mint that lost \
|
|
540 |
+ |
the toolkit packages would report nothing and fail later at a link:\n{gui}",
|
| 477 |
541 |
|
);
|
|
542 |
+ |
for pkg in [
|
|
543 |
+ |
"default-fonts-cjk-sans",
|
|
544 |
+ |
"default-fonts-other-sans",
|
|
545 |
+ |
"google-noto-sans-mono-cjk-vf-fonts",
|
|
546 |
+ |
] {
|
|
547 |
+ |
assert!(
|
|
548 |
+ |
gui.contains(pkg),
|
|
549 |
+ |
"the GUI-bearing server arm stopped denying {pkg}; carrying fontconfig is \
|
|
550 |
+ |
not a reason to carry a font:\n{gui}",
|
|
551 |
+ |
);
|
|
552 |
+ |
}
|
| 478 |
553 |
|
assert!(
|
| 479 |
|
- |
!server.contains("fc-list ':family=Quasi"),
|
| 480 |
|
- |
"the server branch still asks fontconfig about the house faces, on a profile \
|
| 481 |
|
- |
that installs neither the faces nor fontconfig:\n{server}",
|
| 482 |
|
- |
);
|
| 483 |
|
- |
assert!(
|
| 484 |
|
- |
server.contains("! command -v fc-list"),
|
| 485 |
|
- |
"the server branch does not prove fontconfig itself is absent, so a package \
|
| 486 |
|
- |
dragging it back in would go unreported:\n{server}",
|
|
554 |
+ |
gui.contains("emoji"),
|
|
555 |
+ |
"the GUI-bearing server arm stopped denying an emoji font:\n{gui}",
|
| 487 |
556 |
|
);
|
| 488 |
557 |
|
}
|
| 489 |
558 |
|
|
| 509 |
578 |
|
std::fs::write(root.join("faces-staged").join(face), "face").expect("staged face");
|
| 510 |
579 |
|
}
|
| 511 |
580 |
|
|
| 512 |
|
- |
let output = run_block(&face_install_script(root), "client", &bin);
|
|
581 |
+ |
let output = run_block(&face_install_script(root), "client", "tauri", &bin);
|
| 513 |
582 |
|
assert!(
|
| 514 |
583 |
|
output.status.success(),
|
| 515 |
584 |
|
"the client branch failed:\n{}",
|
| 538 |
607 |
|
std::fs::create_dir_all(root.join("faces-staged")).expect("staged faces");
|
| 539 |
608 |
|
std::fs::write(root.join("faces-staged/QuasiMono[wght].ttf"), "face").expect("staged face");
|
| 540 |
609 |
|
|
| 541 |
|
- |
let output = run_block(&face_install_script(root), "server", &bin);
|
|
610 |
+ |
let output = run_block(&face_install_script(root), "server", "none", &bin);
|
| 542 |
611 |
|
assert!(
|
| 543 |
612 |
|
output.status.success(),
|
| 544 |
613 |
|
"the server branch failed:\n{}",
|
| 564 |
633 |
|
std::fs::create_dir_all(root.join("faces-staged")).expect("staged faces");
|
| 565 |
634 |
|
std::fs::create_dir_all(root.join("usr/share/fonts/quasi")).expect("an installed face dir");
|
| 566 |
635 |
|
|
| 567 |
|
- |
let output = run_block(&face_install_script(root), "server", &bin);
|
|
636 |
+ |
let output = run_block(&face_install_script(root), "server", "none", &bin);
|
| 568 |
637 |
|
assert!(
|
| 569 |
638 |
|
!output.status.success(),
|
| 570 |
639 |
|
"a server root carrying the house faces passed the install block",
|
| 595 |
664 |
|
\nesac\nexit 0\n",
|
| 596 |
665 |
|
);
|
| 597 |
666 |
|
|
| 598 |
|
- |
let output = run_block(&block_with("dnf install -y fontconfig"), profile, &bin);
|
|
667 |
+ |
let output = run_block(
|
|
668 |
+ |
&block_with("dnf install -y fontconfig"),
|
|
669 |
+ |
profile,
|
|
670 |
+ |
"none",
|
|
671 |
+ |
&bin,
|
|
672 |
+ |
);
|
| 599 |
673 |
|
assert!(
|
| 600 |
674 |
|
output.status.success(),
|
| 601 |
675 |
|
"the fontconfig install block failed on {profile}:\n{}",
|
| 619 |
693 |
|
write_executable(&bin.join("dnf"), "#!/bin/sh\nexit 0\n");
|
| 620 |
694 |
|
write_executable(&bin.join("fc-cache"), "#!/bin/sh\nexit 0\n");
|
| 621 |
695 |
|
|
| 622 |
|
- |
let output = run_block(&block_with("dnf install -y fontconfig"), "server", &bin);
|
|
696 |
+ |
let output = run_block(
|
|
697 |
+ |
&block_with("dnf install -y fontconfig"),
|
|
698 |
+ |
"server",
|
|
699 |
+ |
"none",
|
|
700 |
+ |
&bin,
|
|
701 |
+ |
);
|
| 623 |
702 |
|
assert!(
|
| 624 |
703 |
|
!output.status.success(),
|
| 625 |
704 |
|
"a server carrying fontconfig from its base passed the install block",
|
| 639 |
718 |
|
let bin = fake_bin(&scratch);
|
| 640 |
719 |
|
rpm_tool(&bin, "bash\nsystemd\npodman\nadwaita-mono-fonts");
|
| 641 |
720 |
|
|
| 642 |
|
- |
let output = run_block(&font_guard_script(root), "server", &bin);
|
|
721 |
+ |
let output = run_block(&font_guard_script(root), "server", "none", &bin);
|
| 643 |
722 |
|
assert!(
|
| 644 |
723 |
|
output.status.success(),
|
| 645 |
724 |
|
"the guard failed on a correctly headless root:\n{}",
|
| 647 |
726 |
|
);
|
| 648 |
727 |
|
}
|
| 649 |
728 |
|
|
|
729 |
+ |
/// The same guard on a build host: `PROFILE=server GUI=tauri`, which is astra.
|
|
730 |
+ |
///
|
|
731 |
+ |
/// Both directions, because each is a real failure. With the font tools present
|
|
732 |
+ |
/// the root is correct and the guard has to pass -- asserting absence here is
|
|
733 |
+ |
/// what the first astra mint hit. With them missing the toolkit packages did not
|
|
734 |
+ |
/// land, and a mint that says nothing about that produces a build host that
|
|
735 |
+ |
/// fails at a link months later.
|
|
736 |
+ |
#[test]
|
|
737 |
+ |
fn the_font_guard_reads_a_tauri_build_host_both_ways() {
|
|
738 |
+ |
let scratch = Scratch::new("guard-server-tauri");
|
|
739 |
+ |
let root = scratch.path();
|
|
740 |
+ |
let bin = fake_bin(&scratch);
|
|
741 |
+ |
rpm_tool(
|
|
742 |
+ |
&bin,
|
|
743 |
+ |
"bash\nsystemd\npodman\ngtk3-devel\nwebkit2gtk4.1-devel",
|
|
744 |
+ |
);
|
|
745 |
+ |
font_tools(&bin, &root.join("usr/share/fonts/quasi"));
|
|
746 |
+ |
|
|
747 |
+ |
let output = run_block(&font_guard_script(root), "server", "tauri", &bin);
|
|
748 |
+ |
assert!(
|
|
749 |
+ |
output.status.success(),
|
|
750 |
+ |
"the guard failed on a build host that correctly carries fontconfig and no face:\n{}",
|
|
751 |
+ |
String::from_utf8_lossy(&output.stderr),
|
|
752 |
+ |
);
|
|
753 |
+ |
|
|
754 |
+ |
let bare = Scratch::new("guard-server-tauri-bare");
|
|
755 |
+ |
let bare_root = bare.path();
|
|
756 |
+ |
let bare_bin = fake_bin(&bare);
|
|
757 |
+ |
rpm_tool(&bare_bin, "bash\nsystemd");
|
|
758 |
+ |
|
|
759 |
+ |
let output = run_block(&font_guard_script(bare_root), "server", "tauri", &bare_bin);
|
|
760 |
+ |
assert!(
|
|
761 |
+ |
!output.status.success(),
|
|
762 |
+ |
"the guard passed a GUI=tauri host with no fontconfig, so a mint that lost the \
|
|
763 |
+ |
toolkit packages would report nothing",
|
|
764 |
+ |
);
|
|
765 |
+ |
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
766 |
+ |
assert!(
|
|
767 |
+ |
stderr.contains("no fontconfig"),
|
|
768 |
+ |
"the failure does not say fontconfig is what is missing:\n{stderr}",
|
|
769 |
+ |
);
|
|
770 |
+ |
}
|
|
771 |
+ |
|
| 650 |
772 |
|
/// And fails on each way that root could be wrong: fontconfig back on the
|
| 651 |
773 |
|
/// PATH, the faces installed, the browser's coverage installed, an emoji font.
|
| 652 |
774 |
|
#[test]
|
| 685 |
807 |
|
std::fs::create_dir_all(root.join("usr/share/fonts/quasi")).expect("faces");
|
| 686 |
808 |
|
}
|
| 687 |
809 |
|
|
| 688 |
|
- |
let output = run_block(&font_guard_script(root), "server", &bin);
|
|
810 |
+ |
let output = run_block(&font_guard_script(root), "server", "none", &bin);
|
| 689 |
811 |
|
assert!(
|
| 690 |
812 |
|
!output.status.success(),
|
| 691 |
813 |
|
"the guard passed a server root with {label}",
|
| 712 |
834 |
|
font_tools(&bin, &fonts);
|
| 713 |
835 |
|
rpm_tool(&bin, "bash\ndefault-fonts-cjk-sans");
|
| 714 |
836 |
|
|
| 715 |
|
- |
let output = run_block(&font_guard_script(root), "client", &bin);
|
|
837 |
+ |
let output = run_block(&font_guard_script(root), "client", "tauri", &bin);
|
| 716 |
838 |
|
assert!(
|
| 717 |
839 |
|
output.status.success(),
|
| 718 |
840 |
|
"the guard failed on a client root that has everything it asks for:\n{}",
|