max / alloy
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
4 files changed,
+191 insertions,
-0 deletions
| @@ -1581,6 +1581,15 @@ | |||
| 1581 | 1581 | # Cursor, GTK theme | |
| 1582 | 1582 | bibata-cursor-theme \ | |
| 1583 | 1583 | adw-gtk3-theme \ | |
| 1584 | + | # dconf, named rather than left to a transitive pull. It is what | |
| 1585 | + | # compiles and reads /etc/dconf/db/local, which is where Alloy's | |
| 1586 | + | # GTK schema defaults live (the file chooser's hidden files, so | |
| 1587 | + | # far). GTK reads that database through gsettings whether or not | |
| 1588 | + | # the CLI is present, but `dconf update` at build time and the | |
| 1589 | + | # assertion after it both need the binary, and a defaults | |
| 1590 | + | # mechanism that depends on someone else's dependency graph is | |
| 1591 | + | # one release from silently not applying. | |
| 1592 | + | dconf \ | |
| 1584 | 1593 | # Screenshot capture + region-select (sway has no built-in grab) | |
| 1585 | 1594 | grim slurp \ | |
| 1586 | 1595 | # The browser is not a line here any more: it is the `BROWSER` ARG | |
| @@ -2316,6 +2325,46 @@ | |||
| 2316 | 2325 | COPY usr/ /usr/ | |
| 2317 | 2326 | COPY --from=rust-build /staged-skel/ / | |
| 2318 | 2327 | ||
| 2328 | + | # ===================================================================== | |
| 2329 | + | # dconf: the GTK schema defaults, compiled and proven to answer. | |
| 2330 | + | # ===================================================================== | |
| 2331 | + | # Alloy's configuration story is files in etc/skel a person can read and edit. | |
| 2332 | + | # A compiled binary database is a second kind of thing, and this is the whole | |
| 2333 | + | # of it: GTK/GNOME schema defaults have no file form a user could edit | |
| 2334 | + | # instead, so a machine-wide default has to be a dconf keyfile or it cannot | |
| 2335 | + | # exist. What it holds is argued in `etc/dconf/db/local.d/`; wiki | |
| 2336 | + | # `alloy-packaging-policy` says what may go in and what may not. | |
| 2337 | + | # | |
| 2338 | + | # The profile puts `user-db:user` above `system-db:local`, so a person who | |
| 2339 | + | # sets a key the other way wins and stays winning. That is why a system | |
| 2340 | + | # default is the correct shape here and why writing into the user's own dconf | |
| 2341 | + | # from `alloy-session` was refused: this can be overridden, that would | |
| 2342 | + | # override. | |
| 2343 | + | # | |
| 2344 | + | # THE ASSERTION IS THE POINT. `dconf update` exits 0 when it compiles nothing, | |
| 2345 | + | # so a keyfile in the wrong directory, a bad group name, or a missing profile | |
| 2346 | + | # all fail silently and leave a defaults mechanism that quietly stopped | |
| 2347 | + | # applying. So the check is a real read through the profile rather than a test | |
| 2348 | + | # for the file: `dconf read` consults the databases directly and needs no | |
| 2349 | + | # session bus, which is what makes it available at build time. | |
| 2350 | + | RUN set -eu; \ | |
| 2351 | + | if [ "$PROFILE" = client ]; then \ | |
| 2352 | + | command -v dconf >/dev/null \ | |
| 2353 | + | || { echo "dconf is not in the image; /etc/dconf/db/local cannot be compiled and the GTK defaults would be inert" >&2; exit 1; }; \ | |
| 2354 | + | test -f /etc/dconf/profile/user \ | |
| 2355 | + | || { echo "/etc/dconf/profile/user did not land; with no profile dconf reads the user database alone and db/local is never consulted" >&2; exit 1; }; \ | |
| 2356 | + | dconf update; \ | |
| 2357 | + | test -s /etc/dconf/db/local \ | |
| 2358 | + | || { echo "dconf update compiled nothing; db/local.d is empty or unreadable" >&2; exit 1; }; \ | |
| 2359 | + | for key in /org/gtk/settings/file-chooser/show-hidden /org/gtk/gtk4/settings/file-chooser/show-hidden; do \ | |
| 2360 | + | [ "$(dconf read "$key")" = true ] \ | |
| 2361 | + | || { echo "$key does not read true out of the compiled database; the GTK file chooser would hide dotfiles" >&2; exit 1; }; \ | |
| 2362 | + | done; \ | |
| 2363 | + | echo "dconf: $(ls /etc/dconf/db/local.d | wc -l) keyfile(s) compiled, GTK3 and GTK4 file choosers show dotfiles"; \ | |
| 2364 | + | else \ | |
| 2365 | + | echo "dconf: keyfiles ride along inert on a profile with no session"; \ | |
| 2366 | + | fi | |
| 2367 | + | ||
| 2319 | 2368 | # ===================================================================== | |
| 2320 | 2369 | # What has to reach a machine that UPGRADED into this image, not only | |
| 2321 | 2370 | # one installed from it: the /var declarations and the greeter account. |
| @@ -1,0 +1,117 @@ | |||
| 1 | + | //! Alloy's one compiled-database default, asserted from the tree. | |
| 2 | + | //! | |
| 3 | + | //! Ruled 2026-08-27 (GoingsOn alloy `533d8165`): Alloy adopts dconf as a | |
| 4 | + | //! defaults mechanism, narrowly. The GTK file chooser hid dotfiles while yazi | |
| 5 | + | //! and helix's picker showed them, and a GTK schema default has no file form a | |
| 6 | + | //! user could edit instead, so a keyfile is the only shape the fix has. | |
| 7 | + | //! | |
| 8 | + | //! Three parts have to agree or the mechanism is inert, and the failure is | |
| 9 | + | //! silent in every direction: `dconf update` exits 0 having compiled nothing, | |
| 10 | + | //! and GTK falls back to upstream's value with nothing on screen to say so. | |
| 11 | + | //! | |
| 12 | + | //! - The profile, or dconf reads the user database alone and never consults | |
| 13 | + | //! `db/local`. | |
| 14 | + | //! - The keyfile, under `db/local.d/`, in the directory the profile's | |
| 15 | + | //! `system-db:local` names. | |
| 16 | + | //! - The Containerfile's `dconf update` and the read that proves it took. | |
| 17 | + | //! | |
| 18 | + | //! Read rather than executed: compiling the database here would assert this | |
| 19 | + | //! machine's dconf, and the build does the real check against the image's. | |
| 20 | + | ||
| 21 | + | use std::path::PathBuf; | |
| 22 | + | ||
| 23 | + | fn tree(relative: &str) -> String { | |
| 24 | + | let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) | |
| 25 | + | .join("../..") | |
| 26 | + | .join(relative); | |
| 27 | + | std::fs::read_to_string(&path) | |
| 28 | + | .unwrap_or_else(|error| panic!("reading {}: {error}", path.display())) | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | fn lines(relative: &str) -> Vec<String> { | |
| 32 | + | tree(relative) | |
| 33 | + | .lines() | |
| 34 | + | .map(str::trim) | |
| 35 | + | .filter(|line| !line.is_empty() && !line.starts_with('#')) | |
| 36 | + | .map(str::to_string) | |
| 37 | + | .collect() | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | /// The user database is read first, so a person's own setting wins. | |
| 41 | + | /// | |
| 42 | + | /// The order is the whole difference between a default and a policy. Reversed, | |
| 43 | + | /// Alloy would be overriding a user who chose the other way, which | |
| 44 | + | /// `alloy-byo-principle` forbids and which is why setting this from | |
| 45 | + | /// `alloy-session` was refused. | |
| 46 | + | #[test] | |
| 47 | + | fn the_profile_puts_the_user_above_the_system_default() { | |
| 48 | + | assert_eq!( | |
| 49 | + | lines("etc/dconf/profile/user"), | |
| 50 | + | ["user-db:user", "system-db:local"] | |
| 51 | + | ); | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | /// Both schemas, because GTK3 and GTK4 keep separate ones. | |
| 55 | + | /// | |
| 56 | + | /// An app is covered by the version it links, so declaring one and not the | |
| 57 | + | /// other leaves half the file choosers on upstream's value — the same | |
| 58 | + | /// inconsistency this closed, one layer down. | |
| 59 | + | #[test] | |
| 60 | + | fn both_gtk_file_choosers_show_hidden_files() { | |
| 61 | + | let keyfile = lines("etc/dconf/db/local.d/00-alloy-file-chooser"); | |
| 62 | + | assert_eq!( | |
| 63 | + | keyfile, | |
| 64 | + | [ | |
| 65 | + | "[org/gtk/settings/file-chooser]", | |
| 66 | + | "show-hidden=true", | |
| 67 | + | "[org/gtk/gtk4/settings/file-chooser]", | |
| 68 | + | "show-hidden=true", | |
| 69 | + | ] | |
| 70 | + | ); | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | /// The keyfile lives where the profile says to look. | |
| 74 | + | /// | |
| 75 | + | /// `system-db:local` means `/etc/dconf/db/local`, compiled from | |
| 76 | + | /// `/etc/dconf/db/local.d/`. A keyfile one directory out compiles into | |
| 77 | + | /// nothing and reports success. | |
| 78 | + | #[test] | |
| 79 | + | fn the_keyfile_sits_in_the_directory_the_profile_names() { | |
| 80 | + | let profile = lines("etc/dconf/profile/user"); | |
| 81 | + | let db = profile | |
| 82 | + | .iter() | |
| 83 | + | .find_map(|line| line.strip_prefix("system-db:")) | |
| 84 | + | .expect("the profile names a system database"); | |
| 85 | + | let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) | |
| 86 | + | .join("../..") | |
| 87 | + | .join("etc/dconf/db") | |
| 88 | + | .join(format!("{db}.d")); | |
| 89 | + | let entries: Vec<_> = std::fs::read_dir(&dir) | |
| 90 | + | .unwrap_or_else(|error| panic!("reading {}: {error}", dir.display())) | |
| 91 | + | .map(|entry| entry.expect("a readable entry").file_name()) | |
| 92 | + | .collect(); | |
| 93 | + | assert!(!entries.is_empty(), "{} is empty", dir.display()); | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | /// The build compiles the database and proves it answers. | |
| 97 | + | /// | |
| 98 | + | /// `dconf update` succeeding is not evidence: it exits 0 having compiled | |
| 99 | + | /// nothing at all. Only a read through the profile distinguishes a database | |
| 100 | + | /// that carries the key from one that was never written. | |
| 101 | + | #[test] | |
| 102 | + | fn the_containerfile_compiles_the_database_and_reads_the_key_back() { | |
| 103 | + | let containerfile = tree("Containerfile"); | |
| 104 | + | assert!( | |
| 105 | + | containerfile.contains("dconf update"), | |
| 106 | + | "the build never compiles the database" | |
| 107 | + | ); | |
| 108 | + | assert!( | |
| 109 | + | containerfile.contains(r#"dconf read "$key""#), | |
| 110 | + | "the build does not read the key back, so a silent no-op would pass" | |
| 111 | + | ); | |
| 112 | + | assert!( | |
| 113 | + | containerfile.contains("/org/gtk/settings/file-chooser/show-hidden") | |
| 114 | + | && containerfile.contains("/org/gtk/gtk4/settings/file-chooser/show-hidden"), | |
| 115 | + | "the build checks only one of the two schemas" | |
| 116 | + | ); | |
| 117 | + | } |
| @@ -1,0 +1,14 @@ | |||
| 1 | + | # The GTK file chooser shows dotfiles. | |
| 2 | + | # | |
| 3 | + | # The rest of Alloy already does: yazi shows hidden files, and helix's picker | |
| 4 | + | # shows them. The GTK chooser was the one place a developer workstation hid | |
| 5 | + | # them and waited to be told Ctrl-H, which is the inconsistency this closes. | |
| 6 | + | # | |
| 7 | + | # Both schemas, because GTK3 and GTK4 keep separate ones and an app is only | |
| 8 | + | # covered by the version it links. | |
| 9 | + | ||
| 10 | + | [org/gtk/settings/file-chooser] | |
| 11 | + | show-hidden=true | |
| 12 | + | ||
| 13 | + | [org/gtk/gtk4/settings/file-chooser] | |
| 14 | + | show-hidden=true |
| @@ -1,0 +1,11 @@ | |||
| 1 | + | # Alloy's dconf profile. | |
| 2 | + | # | |
| 3 | + | # Two lines, and the order is the whole point: the user's own database is read | |
| 4 | + | # first, so anything a person sets wins over anything Alloy declares. The | |
| 5 | + | # system database under it is a default, not a policy — `system-db` rather | |
| 6 | + | # than a lockdown entry, so nothing here is enforced. | |
| 7 | + | # | |
| 8 | + | # Fedora ships no /etc/dconf/profile/user, and with no profile dconf reads the | |
| 9 | + | # user database alone. So this file is what makes db/local.d get read at all. | |
| 10 | + | user-db:user | |
| 11 | + | system-db:local |