Compare commits
9
Commits
86825e7327
...
f42b3cfe0e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f42b3cfe0e | ||
|
|
51ceb19480 | ||
|
|
25e2328658 | ||
|
|
9f563c8f94 | ||
|
|
7d633eb06e | ||
|
|
6d1f3f3763 | ||
|
|
de923cb4d5 | ||
|
|
bd9a55c8eb | ||
|
|
cab7699711 |
@@ -1,5 +1,7 @@
|
|||||||
# Ignore bash environment variables.
|
# Ignore bash environment variables.
|
||||||
/config/bash/env
|
/config/bash/env
|
||||||
|
# Personal espanso triggers (name, email), seeded per-machine by setup-identity.
|
||||||
|
/config/dot/espanso/match/identity.yml
|
||||||
# Ignore backups of old config files
|
# Ignore backups of old config files
|
||||||
/config/old
|
/config/old
|
||||||
# Ignore Wireguard config of course!
|
# Ignore Wireguard config of course!
|
||||||
|
|||||||
@@ -3,11 +3,23 @@
|
|||||||
Formerly Sunhat. A personal config for Fedora, with the intention of helping a
|
Formerly Sunhat. A personal config for Fedora, with the intention of helping a
|
||||||
user set up their Fedora system with one command.
|
user set up their Fedora system with one command.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bash <(curl -fsSL https://git.gbrown.org/gib/Panama/raw/branch/main/boot)
|
||||||
|
```
|
||||||
|
|
||||||
|
`boot` installs git if the machine lacks it, clones this repository to
|
||||||
|
`~/.local/share/Panama` (or `$PANAMA_PATH`), and hands off to `install`. It is
|
||||||
|
deliberately small enough to read first, and the same two steps by hand work
|
||||||
|
identically:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://git.gbrown.org/gib/Panama.git ~/.local/share/Panama
|
git clone https://git.gbrown.org/gib/Panama.git ~/.local/share/Panama
|
||||||
~/.local/share/Panama/install
|
~/.local/share/Panama/install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Both are safe to run again: an existing clone is fast-forwarded rather than
|
||||||
|
replaced, and `install` is the upgrade path.
|
||||||
|
|
||||||
`install` asks its questions first and then runs the stages in `setup/scripts/`
|
`install` asks its questions first and then runs the stages in `setup/scripts/`
|
||||||
in order, without stopping again:
|
in order, without stopping again:
|
||||||
|
|
||||||
@@ -101,7 +113,7 @@ docs/ Settings reference, and the design specs behind the work
|
|||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
131 of them, under `tests/`. Run the lot, or a subset by pattern:
|
133 of them, under `tests/`. Run the lot, or a subset by pattern:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
panama test # everything
|
panama test # everything
|
||||||
@@ -159,6 +171,12 @@ package, a `flatpak:` line is a Flathub id, `| Name` gives the menu something
|
|||||||
readable, and an indented line belongs to the entry above it — which is how OBS
|
readable, and an indented line belongs to the entry above it — which is how OBS
|
||||||
carries its sixteen plugin extensions as one thing to tick.
|
carries its sixteen plugin extensions as one thing to tick.
|
||||||
|
|
||||||
|
`panama-sudo` is pkexec with a stated reason: `panama-sudo --reason "why" --
|
||||||
|
command` shows the reason on Panama's password prompt, clearly labeled as an
|
||||||
|
unverified claim beside polkitd's own action text — meant for agents and
|
||||||
|
scripts, so the person typing the password learns why before they do. Without
|
||||||
|
a reason, a running shell, or `qs` it behaves exactly like pkexec.
|
||||||
|
|
||||||
`panama app` is deliberately not part of `./install`. Everything else Panama
|
`panama app` is deliberately not part of `./install`. Everything else Panama
|
||||||
installs comes from dnf or Flathub; these are built from source because no
|
installs comes from dnf or Flathub; these are built from source because no
|
||||||
packaged form exists, and a source build is slow, wants the network throughout,
|
packaged form exists, and a source build is slow, wants the network throughout,
|
||||||
|
|||||||
Executable
+34
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# pkexec, with a stated reason on the password prompt.
|
||||||
|
#
|
||||||
|
# panama-sudo --reason "Installing gamemode hooks" -- dnf install gamemode
|
||||||
|
#
|
||||||
|
# The reason travels to the shell over Quickshell IPC before pkexec runs, and
|
||||||
|
# Panama's prompt shows it clearly labeled beside polkitd's own action message
|
||||||
|
# -- beside, never instead of: anything can claim any reason, so the real
|
||||||
|
# action text stays the trust anchor. Meant for agents and scripts, so the
|
||||||
|
# person at the keyboard learns WHY before typing their password.
|
||||||
|
#
|
||||||
|
# Degrades to plain pkexec: no --reason, no running shell, or no qs on PATH
|
||||||
|
# all behave identically to calling pkexec yourself.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
reason=""
|
||||||
|
if [[ "${1:-}" == "--reason" ]]; then
|
||||||
|
reason="${2:?panama-sudo: --reason needs a value}"
|
||||||
|
shift 2
|
||||||
|
fi
|
||||||
|
[[ "${1:-}" == "--" ]] && shift
|
||||||
|
|
||||||
|
if (( $# == 0 )); then
|
||||||
|
echo 'usage: panama-sudo [--reason "why"] -- command [args...]' >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$reason" ]] && command -v qs >/dev/null 2>&1; then
|
||||||
|
qs ipc call polkit reason "$reason" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec pkexec "$@"
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Panama's front door: the one command a fresh Fedora machine needs.
|
||||||
|
#
|
||||||
|
# bash <(curl -fsSL https://git.gbrown.org/gib/Panama/raw/branch/main/boot)
|
||||||
|
#
|
||||||
|
# Deliberately dumb, because a copy of this script leaves the repository the
|
||||||
|
# moment somebody curls it -- nothing here can be fixed by re-running
|
||||||
|
# ./install, so there is as little here as possible: get git, get the clone,
|
||||||
|
# hand off. Everything with judgment in it lives in `install`, which is also
|
||||||
|
# where re-runs and upgrades already work.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_URL="https://git.gbrown.org/gib/Panama.git"
|
||||||
|
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||||
|
export PANAMA_PATH
|
||||||
|
|
||||||
|
# Root would put the clone and every dotfile in root's home and run the
|
||||||
|
# desktop setup for the wrong user. sudo is used inside where it is needed.
|
||||||
|
if [[ "$(id -u)" -eq 0 ]]; then
|
||||||
|
echo "Run this as your own user, not root: the install configures YOUR desktop." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Panama assumes Fedora's repositories, package names, and GNOME base install.
|
||||||
|
if ! grep -qi '^ID=fedora' /etc/os-release 2>/dev/null; then
|
||||||
|
echo "This looks like something other than Fedora; Panama only supports Fedora Workstation." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# git is the one dependency the clone itself needs. Everything else -- gum
|
||||||
|
# included -- is bootstrapped by `install`.
|
||||||
|
if ! command -v git >/dev/null 2>&1; then
|
||||||
|
echo "Installing git, which the clone needs"
|
||||||
|
sudo dnf install -y git
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$PANAMA_PATH/.git" ]]; then
|
||||||
|
# An existing clone makes this the recovery command too. Only a fast-forward:
|
||||||
|
# local work is never rewritten, and a diverged clone still installs from
|
||||||
|
# what it has rather than stopping someone mid-repair.
|
||||||
|
echo "Panama is already cloned at $PANAMA_PATH; updating"
|
||||||
|
git -C "$PANAMA_PATH" pull --ff-only \
|
||||||
|
|| echo "Could not fast-forward; installing from the clone as it is" >&2
|
||||||
|
else
|
||||||
|
git clone "$REPO_URL" "$PANAMA_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# `curl | bash` and `bash <(curl ...)` can leave stdin as the pipe, and the
|
||||||
|
# first thing install runs is the interview, which has to be able to ask.
|
||||||
|
# Reattach the terminal when there is one; without one the interview will say
|
||||||
|
# so itself.
|
||||||
|
# The probe actually opens /dev/tty rather than testing -r: a process with no
|
||||||
|
# controlling terminal passes -r and then fails the redirect.
|
||||||
|
if [[ ! -t 0 ]] && (exec </dev/tty) 2>/dev/null; then
|
||||||
|
exec "$PANAMA_PATH/install" </dev/tty
|
||||||
|
fi
|
||||||
|
exec "$PANAMA_PATH/install"
|
||||||
+2
-2
@@ -3,7 +3,6 @@
|
|||||||
# Aliases I like
|
# Aliases I like
|
||||||
alias :q="exit"
|
alias :q="exit"
|
||||||
alias :wq="exit"
|
alias :wq="exit"
|
||||||
alias startsunshine="systemctl --user restart sunshine.service"
|
|
||||||
alias sourcerc="source ~/.bashrc"
|
alias sourcerc="source ~/.bashrc"
|
||||||
alias c="clear"
|
alias c="clear"
|
||||||
alias shutdown="systemctl poweroff"
|
alias shutdown="systemctl poweroff"
|
||||||
@@ -32,7 +31,8 @@ alias ls='eza -lh --group-directories-first --icons'
|
|||||||
alias lsa='ls -a'
|
alias lsa='ls -a'
|
||||||
alias lt='eza --tree --level=2 --long --icons --git'
|
alias lt='eza --tree --level=2 --long --icons --git'
|
||||||
alias lta='lt -a'
|
alias lta='lt -a'
|
||||||
alias ff="fzf --preview 'batcat --style=numbers --color=always {}'"
|
# Fedora's bat installs /usr/bin/bat; batcat is the Debian name.
|
||||||
|
alias ff="fzf --preview 'bat --style=numbers --color=always {}'"
|
||||||
|
|
||||||
# Directories
|
# Directories
|
||||||
alias ..='cd ..'
|
alias ..='cd ..'
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
# espanso match file
|
# espanso match file
|
||||||
# https://espanso.org/docs/
|
# https://espanso.org/docs/
|
||||||
|
# Name and email triggers (:fn, :em, ...) are NOT here: they are personal, and
|
||||||
|
# this file is shared. setup-identity seeds them into match/identity.yml --
|
||||||
|
# per-machine, gitignored, yours to edit -- from the install interview.
|
||||||
matches:
|
matches:
|
||||||
# Name
|
|
||||||
- trigger: ":fn"
|
|
||||||
replace: "Gabriel Brown"
|
|
||||||
- trigger: ":fin"
|
|
||||||
replace: "Gabriel A Brown"
|
|
||||||
# Email
|
|
||||||
- trigger: ":em"
|
|
||||||
replace: "[email protected]"
|
|
||||||
- trigger: ":empro"
|
|
||||||
replace: "[email protected]"
|
|
||||||
|
|
||||||
# Date
|
# Date
|
||||||
- trigger: ":date"
|
- trigger: ":date"
|
||||||
replace: "{{mydate}}"
|
replace: "{{mydate}}"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ Don't "fix" them.
|
|||||||
| `hyprland.lua` | Entry point. Each `require()` is its own error scope |
|
| `hyprland.lua` | Entry point. Each `require()` is its own error scope |
|
||||||
| `prefs.lua` | Reads the settings file the Settings app writes. See below |
|
| `prefs.lua` | Reads the settings file the Settings app writes. See below |
|
||||||
| `env.lua` | Environment. Note the uwsm caveat below |
|
| `env.lua` | Environment. Note the uwsm caveat below |
|
||||||
| `monitors.lua` | DP-2 geometry, scaling, and the HDR decision |
|
| `monitors.lua` | Monitor geometry and scaling (the Kuycon by description), and the HDR decision |
|
||||||
| `looks.lua` | Colors, blur, glow, shadows, animations, VRR, scanout |
|
| `looks.lua` | Colors, blur, glow, shadows, animations, VRR, scanout |
|
||||||
| `input.lua` | Keyboard/mouse. Click-to-focus, like GNOME |
|
| `input.lua` | Keyboard/mouse. Click-to-focus, like GNOME |
|
||||||
| `rules.lua` | Window rules, gaming rules, layer rules for the shell |
|
| `rules.lua` | Window rules, gaming rules, layer rules for the shell |
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ hl.on("hyprland.start", function()
|
|||||||
-- prompt if Panama's ever fails to come up.
|
-- prompt if Panama's ever fails to come up.
|
||||||
hl.exec_cmd("systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP && systemctl --user start panama-polkit-agent.service hyprpaper.service vicinae.service hypridle.service")
|
hl.exec_cmd("systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP && systemctl --user start panama-polkit-agent.service hyprpaper.service vicinae.service hypridle.service")
|
||||||
|
|
||||||
|
-- Text expansion. change-settings runs `espanso service register`, which
|
||||||
|
-- writes and enables espanso's own user unit; the explicit start makes the
|
||||||
|
-- first Hyprland login after a fresh install work rather than the second.
|
||||||
|
-- Started on its own line: unlike the units above it carries no
|
||||||
|
-- ConditionEnvironment, and a missing unit (espanso not yet registered)
|
||||||
|
-- must not muddy the start of the four that lock and wallpaper depend on.
|
||||||
|
hl.exec_cmd("systemctl --user start espanso.service")
|
||||||
|
|
||||||
-- The shell: bar, dock, overview, quick settings, notifications, capture.
|
-- The shell: bar, dock, overview, quick settings, notifications, capture.
|
||||||
-- No systemd unit ships with quickshell, so it runs as a compositor child.
|
-- No systemd unit ships with quickshell, so it runs as a compositor child.
|
||||||
hl.exec_cmd("quickshell --daemonize")
|
hl.exec_cmd("quickshell --daemonize")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
-- Monitors
|
-- Monitors
|
||||||
--
|
--
|
||||||
-- Kuycon P20 on DP-2: 4500x3000 @ 60Hz, 1.5x fractional scale.
|
-- Kuycon P20 (matched by description): 4500x3000 @ 60Hz, 1.5x fractional scale.
|
||||||
-- 4500/1.5 = 3000 and 3000/1.5 = 2000, both integers, so this is a "clean"
|
-- 4500/1.5 = 3000 and 3000/1.5 = 2000, both integers, so this is a "clean"
|
||||||
-- fractional scale and Hyprland will not complain.
|
-- fractional scale and Hyprland will not complain.
|
||||||
--
|
--
|
||||||
@@ -125,32 +125,10 @@ local function display_position(entry, fallback)
|
|||||||
return fallback
|
return fallback
|
||||||
end
|
end
|
||||||
|
|
||||||
local shipped_mode = "4500x3000@60"
|
-- Every connected output uses the same validated per-output store. Automatic
|
||||||
local shipped_scale = 1.5
|
-- placement and the compositor's normal color policy unless the entry says
|
||||||
local shipped_transform = 0
|
-- otherwise.
|
||||||
local dp2 = display_entry("DP-2")
|
|
||||||
|
|
||||||
hl.monitor({
|
|
||||||
output = "DP-2",
|
|
||||||
mode = dp2 and dp2.mode or shipped_mode,
|
|
||||||
position = display_position(dp2, "0x0"),
|
|
||||||
scale = dp2 and dp2.scale or shipped_scale,
|
|
||||||
transform = dp2 and dp2.transform or shipped_transform,
|
|
||||||
|
|
||||||
-- 10-bit output. 4500x3000@60 at 10bpc is ~24 Gbps, right at the edge of
|
|
||||||
-- DP 1.4 HBR3, so this relies on DSC. If the display fails to light up or
|
|
||||||
-- falls back to a lower mode, drop this line first.
|
|
||||||
bitdepth = 10,
|
|
||||||
|
|
||||||
-- "auto" = sRGB at 8bpc, wide gamut at 10bpc. Not HDR; see header.
|
|
||||||
cm = "auto",
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Other connected outputs use the same validated per-output store. They keep
|
|
||||||
-- automatic placement and the compositor's normal color policy; DP-2 alone
|
|
||||||
-- carries the panel-specific 10-bit policy documented above.
|
|
||||||
for output, _ in pairs(displays) do
|
for output, _ in pairs(displays) do
|
||||||
if output ~= "DP-2" then
|
|
||||||
local entry = display_entry(output)
|
local entry = display_entry(output)
|
||||||
if entry ~= nil then
|
if entry ~= nil then
|
||||||
hl.monitor({
|
hl.monitor({
|
||||||
@@ -161,9 +139,34 @@ for output, _ in pairs(displays) do
|
|||||||
transform = entry.transform,
|
transform = entry.transform,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- The Kuycon P20, matched by what it is rather than where it is plugged in.
|
||||||
|
-- This used to be a rule for connector DP-2 outright, which handed the panel's
|
||||||
|
-- 4500x3000 mode and 1.5 scale to whatever monitor a stranger's machine had on
|
||||||
|
-- its most common DisplayPort connector. Emitted after the prefs loop so a
|
||||||
|
-- saved entry for its connector still carries the mode/scale/position, while
|
||||||
|
-- this rule holds the shipped defaults and the panel-specific color policy.
|
||||||
|
local shipped_mode = "4500x3000@60"
|
||||||
|
local shipped_scale = 1.5
|
||||||
|
local shipped_transform = 0
|
||||||
|
local kuycon = display_entry("DP-2")
|
||||||
|
hl.monitor({
|
||||||
|
output = "desc:GVT Kuycon P20",
|
||||||
|
mode = kuycon and kuycon.mode or shipped_mode,
|
||||||
|
position = display_position(kuycon, "0x0"),
|
||||||
|
scale = kuycon and kuycon.scale or shipped_scale,
|
||||||
|
transform = kuycon and kuycon.transform or shipped_transform,
|
||||||
|
|
||||||
|
-- 10-bit output. 4500x3000@60 at 10bpc is ~24 Gbps, right at the edge of
|
||||||
|
-- DP 1.4 HBR3, so this relies on DSC. If the display fails to light up or
|
||||||
|
-- falls back to a lower mode, drop this line first.
|
||||||
|
bitdepth = 10,
|
||||||
|
|
||||||
|
-- "auto" = sRGB at 8bpc, wide gamut at 10bpc. Not HDR; see header.
|
||||||
|
cm = "auto",
|
||||||
|
})
|
||||||
|
|
||||||
-- Any monitor not named above: sane defaults rather than nothing.
|
-- Any monitor not named above: sane defaults rather than nothing.
|
||||||
hl.monitor({
|
hl.monitor({
|
||||||
output = "",
|
output = "",
|
||||||
|
|||||||
@@ -94,6 +94,34 @@ PanelWindow {
|
|||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The caller's stated reason, when panama-sudo passed one.
|
||||||
|
// Untrusted commentary from an unprivileged process, so it is
|
||||||
|
// labeled as a claim and drawn beside polkitd's message above --
|
||||||
|
// never in place of it. The real action text is the trust anchor.
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
visible: Polkit.statedReason !== ""
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Stated reason (unverified)"
|
||||||
|
color: Theme.fgMuted
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: Polkit.statedReason
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
font.italic: true
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
visible: Polkit.users.length > 1
|
visible: Polkit.users.length > 1
|
||||||
|
|||||||
@@ -38,12 +38,31 @@ Singleton {
|
|||||||
// How many times a wrong password has been offered for this request.
|
// How many times a wrong password has been offered for this request.
|
||||||
property int attempts: 0
|
property int attempts: 0
|
||||||
|
|
||||||
|
// A caller's stated reason for the NEXT request, and the one attached to
|
||||||
|
// the request on screen. Untrusted by design -- any process can state one
|
||||||
|
// -- so the prompt shows it clearly labeled beside polkitd's real action
|
||||||
|
// message, never in place of it. See stateReason().
|
||||||
|
property var pendingReason: null
|
||||||
|
property string statedReason: ""
|
||||||
|
|
||||||
readonly property bool active: root.request !== null
|
readonly property bool active: root.request !== null
|
||||||
|
|
||||||
// Held only between pressing Enter and the helper accepting it on stdin.
|
// Held only between pressing Enter and the helper accepting it on stdin.
|
||||||
property string pendingSecret: ""
|
property string pendingSecret: ""
|
||||||
|
|
||||||
|
|
||||||
|
// panama-sudo's side channel: state WHY the authentication request about
|
||||||
|
// to arrive is being made, so the prompt can say more than the generic
|
||||||
|
// action text. Single-shot and short-lived -- it attaches only to the next
|
||||||
|
// request, and only if that request arrives within ten seconds -- so a
|
||||||
|
// stale reason can never dress up an unrelated prompt.
|
||||||
|
function stateReason(text: string): void {
|
||||||
|
const trimmed = String(text).trim().slice(0, 200);
|
||||||
|
if (trimmed === "")
|
||||||
|
return;
|
||||||
|
root.pendingReason = { text: trimmed, at: Date.now() };
|
||||||
|
}
|
||||||
|
|
||||||
function begin(path: string): void {
|
function begin(path: string): void {
|
||||||
// A second request while one is open would leave the first
|
// A second request while one is open would leave the first
|
||||||
// unanswerable; polkit serializes these in practice, and refusing is
|
// unanswerable; polkit serializes these in practice, and refusing is
|
||||||
@@ -69,6 +88,13 @@ Singleton {
|
|||||||
: (root.users.length > 0 ? String(root.users[0]) : "");
|
: (root.users.length > 0 ? String(root.users[0]) : "");
|
||||||
root.attempts = 0;
|
root.attempts = 0;
|
||||||
root.failureText = "";
|
root.failureText = "";
|
||||||
|
// Consume the stated reason whether or not it is still fresh:
|
||||||
|
// either way it must not survive to a later request.
|
||||||
|
const pending = root.pendingReason;
|
||||||
|
root.pendingReason = null;
|
||||||
|
root.statedReason = (pending !== null && Date.now() - pending.at <= 10000)
|
||||||
|
? pending.text
|
||||||
|
: "";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Polkit: could not read the request:", error);
|
console.warn("Polkit: could not read the request:", error);
|
||||||
root.dismiss("failed");
|
root.dismiss("failed");
|
||||||
@@ -114,6 +140,7 @@ Singleton {
|
|||||||
root.failureText = "";
|
root.failureText = "";
|
||||||
root.pendingSecret = "";
|
root.pendingSecret = "";
|
||||||
root.authenticating = false;
|
root.authenticating = false;
|
||||||
|
root.statedReason = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function responsePathFor(path: string): string {
|
function responsePathFor(path: string): string {
|
||||||
|
|||||||
@@ -424,8 +424,15 @@ ShellRoot {
|
|||||||
target: "polkit"
|
target: "polkit"
|
||||||
function begin(path: string): void { Polkit.begin(path); }
|
function begin(path: string): void { Polkit.begin(path); }
|
||||||
function cancel(): void { Polkit.cancel(); }
|
function cancel(): void { Polkit.cancel(); }
|
||||||
|
// panama-sudo's side channel: the reason a privileged command is about
|
||||||
|
// to run, shown labeled on the prompt beside polkitd's own message.
|
||||||
|
function reason(text: string): void { Polkit.stateReason(text); }
|
||||||
function status(): string {
|
function status(): string {
|
||||||
return JSON.stringify({ active: Polkit.active, action: Polkit.actionId });
|
return JSON.stringify({
|
||||||
|
active: Polkit.active,
|
||||||
|
action: Polkit.actionId,
|
||||||
|
statedReason: Polkit.statedReason
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,13 @@ cleanup() {
|
|||||||
# design, and one of them is an email address.
|
# design, and one of them is an email address.
|
||||||
[[ -n "${PANAMA_ANSWERS:-}" ]] && rm -f "$PANAMA_ANSWERS"
|
[[ -n "${PANAMA_ANSWERS:-}" ]] && rm -f "$PANAMA_ANSWERS"
|
||||||
}
|
}
|
||||||
trap cleanup EXIT INT TERM
|
trap cleanup EXIT
|
||||||
|
# A bare `trap cleanup INT` is not an abort: bash runs the handler and then
|
||||||
|
# carries on with the script, so Ctrl-C would kill only the current stage and
|
||||||
|
# the remaining ones -- MOK enrollment, firmware -- would still run. Exit
|
||||||
|
# explicitly instead; the EXIT trap above does the actual cleanup.
|
||||||
|
trap 'exit 130' INT
|
||||||
|
trap 'exit 143' TERM
|
||||||
|
|
||||||
gsettings set org.gnome.desktop.screensaver lock-enabled false 2>/dev/null || true
|
gsettings set org.gnome.desktop.screensaver lock-enabled false 2>/dev/null || true
|
||||||
gsettings set org.gnome.desktop.session idle-delay 0 2>/dev/null || true
|
gsettings set org.gnome.desktop.session idle-delay 0 2>/dev/null || true
|
||||||
|
|||||||
@@ -30,3 +30,6 @@ python3-virtualenv
|
|||||||
ripgrep
|
ripgrep
|
||||||
ruby
|
ruby
|
||||||
sqlite3
|
sqlite3
|
||||||
|
# vimx: clipboard-capable vim, which the shipped `vim` alias and the vimrc
|
||||||
|
# that link-dotfiles installs both assume. Fedora carries it as vim-X11.
|
||||||
|
vim-X11
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Strict: the copy over / below is the one thing this stage exists to do, and
|
||||||
|
# without set -e its failure fell through to guarded no-ops and exited 0 --
|
||||||
|
# a failed stage the installer's summary could never see.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
# --- Helper functions ---
|
# --- Helper functions ---
|
||||||
log() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
|
log() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
|
||||||
exists() { command -v "$1" >/dev/null 2>&1; }
|
exists() { command -v "$1" >/dev/null 2>&1; }
|
||||||
|
|
||||||
# --- Defined Paths ---
|
# --- Defined Paths ---
|
||||||
PANAMA_PATH="$HOME/.local/share/Panama"
|
# The default, not an assignment: an exported PANAMA_PATH from a clone at
|
||||||
|
# another location must win, or the copy over / below reads the wrong tree.
|
||||||
|
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||||
|
|
||||||
echo -e "\n--- Copying System & User files ---"
|
echo -e "\n--- Copying System & User files ---"
|
||||||
log "Changing DNF Settings"
|
log "Changing DNF Settings"
|
||||||
@@ -61,6 +68,18 @@ echo -e "\n--- Hyprland session services ---"
|
|||||||
# PartOf=graphical-session.target.
|
# PartOf=graphical-session.target.
|
||||||
log "Hyprland services are started per-session by hypr/autostart.lua, not enabled globally"
|
log "Hyprland services are started per-session by hypr/autostart.lua, not enabled globally"
|
||||||
|
|
||||||
|
# espanso is the exception to the rule above: text expansion is wanted under
|
||||||
|
# any compositor and conflicts with nothing GNOME runs, and upstream's own
|
||||||
|
# mechanism is a registered user unit. The RPM ships no unit -- without this,
|
||||||
|
# the package installs, the config links, and no trigger ever fires.
|
||||||
|
# `service register` writes and enables the unit and is idempotent on re-runs;
|
||||||
|
# autostart.lua also starts it so the first Hyprland login after a fresh
|
||||||
|
# install expands text without waiting for the next one.
|
||||||
|
if exists espanso && ! systemctl --user cat espanso.service >/dev/null 2>&1; then
|
||||||
|
log "Registering the espanso text-expansion service"
|
||||||
|
espanso service register >/dev/null 2>&1 || log "Could not register espanso"
|
||||||
|
fi
|
||||||
|
|
||||||
# Notification daemons must NOT be installed: mako, dunst and swaync all
|
# Notification daemons must NOT be installed: mako, dunst and swaync all
|
||||||
# register Name=org.freedesktop.Notifications for D-Bus activation, which races
|
# register Name=org.freedesktop.Notifications for D-Bus activation, which races
|
||||||
# Quickshell's own notification server at login. Whoever wins keeps the name.
|
# Quickshell's own notification server at login. Whoever wins keeps the name.
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ packages_in() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Defined Paths ---
|
# --- Defined Paths ---
|
||||||
PANAMA_PATH="$HOME/.local/share/Panama"
|
# The default, not an assignment: ./install and link-dotfiles honor an exported
|
||||||
|
# PANAMA_PATH, and clobbering it here made a clone anywhere else source the
|
||||||
|
# extras catalog from a path that does not exist.
|
||||||
|
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||||
|
|
||||||
# Reading the extras catalog, shared with `panama apps` so the two front doors
|
# Reading the extras catalog, shared with `panama apps` so the two front doors
|
||||||
# cannot disagree about what a category contains.
|
# cannot disagree about what a category contains.
|
||||||
@@ -214,9 +217,12 @@ if rpm -q rustdesk >/dev/null 2>&1; then
|
|||||||
log "RustDesk already installed"
|
log "RustDesk already installed"
|
||||||
else
|
else
|
||||||
log "Resolving the latest RustDesk release..."
|
log "Resolving the latest RustDesk release..."
|
||||||
|
# `|| true` because a failed curl -- unauthenticated GitHub API calls get
|
||||||
|
# rate-limited -- would otherwise trip set -e and kill the stage before the
|
||||||
|
# empty-result fallback below could do its job.
|
||||||
rustdesk_url="$(curl -fsSL https://api.github.com/repos/rustdesk/rustdesk/releases/latest 2>/dev/null \
|
rustdesk_url="$(curl -fsSL https://api.github.com/repos/rustdesk/rustdesk/releases/latest 2>/dev/null \
|
||||||
| jq -r '.assets[].browser_download_url | select(test("x86_64\\.rpm$")) | select(test("suse") | not)' \
|
| jq -r '.assets[].browser_download_url | select(test("x86_64\\.rpm$")) | select(test("suse") | not)' \
|
||||||
| head -1)"
|
| head -1 || true)"
|
||||||
if [[ -n "$rustdesk_url" ]]; then
|
if [[ -n "$rustdesk_url" ]]; then
|
||||||
log "Installing RustDesk from $rustdesk_url"
|
log "Installing RustDesk from $rustdesk_url"
|
||||||
# The RPM ships rustdesk.service already enabled, which is what provides
|
# The RPM ships rustdesk.service already enabled, which is what provides
|
||||||
@@ -271,7 +277,9 @@ install_extra_category() {
|
|||||||
name="$(basename "$file")"
|
name="$(basename "$file")"
|
||||||
|
|
||||||
local dnf_packages flatpak_ids
|
local dnf_packages flatpak_ids
|
||||||
dnf_packages=$(catalog_all_targets "$file" | grep -v '^flatpak:' | tr "\n" " ")
|
# sed rather than grep -v: most categories are flatpak-only, and grep exits 1
|
||||||
|
# when it selects nothing, which set -e above turns into a dead stage.
|
||||||
|
dnf_packages=$(catalog_all_targets "$file" | sed '/^flatpak:/d' | tr "\n" " ")
|
||||||
flatpak_ids=$(catalog_all_targets "$file" | sed -n 's/^flatpak://p' | tr "\n" " ")
|
flatpak_ids=$(catalog_all_targets "$file" | sed -n 's/^flatpak://p' | tr "\n" " ")
|
||||||
|
|
||||||
if [[ -n "${dnf_packages// /}" ]]; then
|
if [[ -n "${dnf_packages// /}" ]]; then
|
||||||
|
|||||||
@@ -122,7 +122,12 @@ if [[ -n "$nvidia_card" ]]; then
|
|||||||
fi
|
fi
|
||||||
second="$(gum input --password --header "MOK password again")"
|
second="$(gum input --password --header "MOK password again")"
|
||||||
if [[ "$first" == "$second" ]]; then
|
if [[ "$first" == "$second" ]]; then
|
||||||
mok_hash="$(mokutil --generate-hash="$first")"
|
# Fed on stdin, never as an argument: an argument sits
|
||||||
|
# in /proc/<pid>/cmdline for any local process to read
|
||||||
|
# while mokutil runs. mokutil prints its two prompts on
|
||||||
|
# stdout too, so the hash is the last line.
|
||||||
|
mok_hash="$(printf '%s\n%s\n' "$first" "$first" \
|
||||||
|
| mokutil --generate-hash | tail -n 1)"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
printf 'Those did not match.\n'
|
printf 'Those did not match.\n'
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Strict: a failed mv or ln here used to fall through and exit 0, so a machine
|
||||||
|
# could end up half-linked while the installer's summary reported the stage as
|
||||||
|
# fine. Anything genuinely optional below carries its own `|| true`.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
# --- Helper functions ---
|
# --- Helper functions ---
|
||||||
log() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
|
log() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
|
||||||
|
|
||||||
@@ -10,8 +15,10 @@ PANAMA_DOT="$PANAMA_PATH/config/dot"
|
|||||||
PANAMA_OLD="$PANAMA_PATH/config/old"
|
PANAMA_OLD="$PANAMA_PATH/config/old"
|
||||||
CONFIG="$HOME/.config"
|
CONFIG="$HOME/.config"
|
||||||
|
|
||||||
# Make backup folder if it doesn't exist
|
# Make backup folder if it doesn't exist -- and ~/.config itself, which a
|
||||||
mkdir -p "$PANAMA_OLD"
|
# truly fresh HOME does not have yet. Before set -e above, its absence made
|
||||||
|
# every symlink below fail silently while the stage still reported success.
|
||||||
|
mkdir -p "$PANAMA_OLD" "$CONFIG"
|
||||||
|
|
||||||
# --- Bashrc ---
|
# --- Bashrc ---
|
||||||
echo -e "\n--- Replacing .bashrc ---"
|
echo -e "\n--- Replacing .bashrc ---"
|
||||||
|
|||||||
@@ -41,6 +41,26 @@ git config --global alias.st status
|
|||||||
git config --global pull.rebase true
|
git config --global pull.rebase true
|
||||||
log "git aliases and pull.rebase applied"
|
log "git aliases and pull.rebase applied"
|
||||||
|
|
||||||
|
# Text-expansion identity. The :fn/:em triggers used to hardcode the repository
|
||||||
|
# owner's name and email in the shared match file; each machine now writes its
|
||||||
|
# own from the interview's answers. Per-machine and gitignored -- espanso loads
|
||||||
|
# every file under match/, so it sits beside base.yml without being shared.
|
||||||
|
# Kept when it already exists: the file is the user's to edit, and a re-run
|
||||||
|
# must not erase what they added.
|
||||||
|
espanso_identity="${XDG_CONFIG_HOME:-$HOME/.config}/espanso/match/identity.yml"
|
||||||
|
if [[ -d "$(dirname "$espanso_identity")" && ! -e "$espanso_identity" ]] \
|
||||||
|
&& [[ -n "$git_name" || -n "$git_email" ]]; then
|
||||||
|
{
|
||||||
|
printf '# Personal expansion triggers, seeded from the install interview.\n'
|
||||||
|
printf '# Per-machine and untracked: add your own freely.\n'
|
||||||
|
printf 'matches:\n'
|
||||||
|
[[ -n "$git_name" ]] && printf ' - trigger: ":fn"\n replace: "%s"\n' "$git_name"
|
||||||
|
[[ -n "$git_email" ]] && printf ' - trigger: ":em"\n replace: "%s"\n' "$git_email"
|
||||||
|
:
|
||||||
|
} > "$espanso_identity"
|
||||||
|
log "Seeded espanso identity triggers at $espanso_identity"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "${PANAMA_GH_LOGIN:-no}" == yes ]]; then
|
if [[ "${PANAMA_GH_LOGIN:-no}" == yes ]]; then
|
||||||
if command -v gh >/dev/null 2>&1; then
|
if command -v gh >/dev/null 2>&1; then
|
||||||
log "Signing in to GitHub"
|
log "Signing in to GitHub"
|
||||||
|
|||||||
@@ -122,8 +122,14 @@ assert(by_output["DP-2"].mode == "4500x3000@60")
|
|||||||
assert(by_output["DP-2"].scale == 1.5)
|
assert(by_output["DP-2"].scale == 1.5)
|
||||||
assert(by_output["DP-2"].transform == 0)
|
assert(by_output["DP-2"].transform == 0)
|
||||||
assert(by_output["DP-2"].position == "0x0")
|
assert(by_output["DP-2"].position == "0x0")
|
||||||
assert(by_output["DP-2"].bitdepth == 10)
|
-- The panel-specific color policy rides the description-matched rule, not the
|
||||||
assert(by_output["DP-2"].cm == "auto")
|
-- connector: a stranger's monitor on DP-2 must not inherit the Kuycon's mode
|
||||||
|
-- or its 10-bit request.
|
||||||
|
local kuycon = by_output["desc:GVT Kuycon P20"]
|
||||||
|
assert(kuycon ~= nil)
|
||||||
|
assert(kuycon.mode == "4500x3000@60")
|
||||||
|
assert(kuycon.bitdepth == 10)
|
||||||
|
assert(kuycon.cm == "auto")
|
||||||
assert(by_output["HDMI-A-1"].mode == "2560x1440@60")
|
assert(by_output["HDMI-A-1"].mode == "2560x1440@60")
|
||||||
assert(by_output["HDMI-A-1"].scale == 1)
|
assert(by_output["HDMI-A-1"].scale == 1)
|
||||||
assert(by_output["HDMI-A-1"].transform == 1)
|
assert(by_output["HDMI-A-1"].transform == 1)
|
||||||
|
|||||||
Executable
+117
@@ -0,0 +1,117 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# The stated reason on the authentication prompt.
|
||||||
|
#
|
||||||
|
# panama-sudo lets a caller say WHY it is about to trigger a password prompt.
|
||||||
|
# The reason is untrusted text from an unprivileged process, so the properties
|
||||||
|
# worth pinning are the ones that keep it honest:
|
||||||
|
#
|
||||||
|
# 1. The prompt renders the reason BESIDE polkitd's real action message,
|
||||||
|
# labeled as unverified -- never in place of it. Any process can claim
|
||||||
|
# "Updating your system" while requesting something else; the action text
|
||||||
|
# is the trust anchor and must survive.
|
||||||
|
# 2. Single-shot and short-lived: a reason attaches to the next request
|
||||||
|
# only, is consumed whether or not it was fresh, and expires rather than
|
||||||
|
# dressing up an unrelated prompt minutes later.
|
||||||
|
# 3. panama-sudo degrades to plain pkexec: no --reason, no qs, or a dead
|
||||||
|
# shell must all still run the command.
|
||||||
|
#
|
||||||
|
# The wrapper is exercised for real against stub qs and pkexec; the QML side
|
||||||
|
# is pinned statically, the way the polkit-agent contract pins its rules.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
wrapper="$repo_dir/bin/panama-sudo"
|
||||||
|
service="$repo_dir/config/dot/quickshell/services/Polkit.qml"
|
||||||
|
prompt="$repo_dir/config/dot/quickshell/modules/polkit/PolkitPrompt.qml"
|
||||||
|
shell_qml="$repo_dir/config/dot/quickshell/shell.qml"
|
||||||
|
|
||||||
|
findings=()
|
||||||
|
note() { findings+=("$1"); }
|
||||||
|
|
||||||
|
[[ -x "$wrapper" ]] || { printf 'polkit reason contract: %s is not executable\n' "$wrapper" >&2; exit 1; }
|
||||||
|
|
||||||
|
# ── The wrapper, for real ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
work="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$work"' EXIT
|
||||||
|
calls="$work/calls"
|
||||||
|
stub_dir="$work/bin"
|
||||||
|
mkdir -p "$stub_dir"
|
||||||
|
|
||||||
|
for command in qs pkexec; do
|
||||||
|
cat >"$stub_dir/$command" <<STUB
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf '%s %s\n' "$command" "\$*" >>"$calls"
|
||||||
|
STUB
|
||||||
|
chmod +x "$stub_dir/$command"
|
||||||
|
done
|
||||||
|
|
||||||
|
run_wrapper() { PATH="$stub_dir:$PATH" "$wrapper" "$@" >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
# A reason reaches the shell first, then the command runs unchanged.
|
||||||
|
: >"$calls"
|
||||||
|
run_wrapper --reason "Test reason" -- some-command --with args \
|
||||||
|
|| note 'the wrapper failed with a reason and a command'
|
||||||
|
grep -q 'qs ipc call polkit reason Test reason' "$calls" \
|
||||||
|
|| note 'the reason never reaches the shell over IPC'
|
||||||
|
grep -q 'pkexec some-command --with args' "$calls" \
|
||||||
|
|| note 'the command does not reach pkexec unchanged'
|
||||||
|
[[ "$(head -1 "$calls")" == qs* ]] \
|
||||||
|
|| note 'the reason is sent after pkexec instead of before the prompt can appear'
|
||||||
|
|
||||||
|
# No reason means no IPC chatter, and still pkexec.
|
||||||
|
: >"$calls"
|
||||||
|
run_wrapper -- some-command || note 'the wrapper failed without a reason'
|
||||||
|
grep -q 'qs' "$calls" && note 'the wrapper calls qs even when no reason was given'
|
||||||
|
grep -q 'pkexec some-command' "$calls" || note 'a reasonless call does not reach pkexec'
|
||||||
|
|
||||||
|
# A dead shell must not cost the command: qs failing is stepped over.
|
||||||
|
cat >"$stub_dir/qs" <<'STUB'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
exit 1
|
||||||
|
STUB
|
||||||
|
chmod +x "$stub_dir/qs"
|
||||||
|
: >"$calls"
|
||||||
|
run_wrapper --reason "Doomed" -- some-command \
|
||||||
|
|| note 'a failing qs stops the command instead of degrading to plain pkexec'
|
||||||
|
grep -q 'pkexec some-command' "$calls" \
|
||||||
|
|| note 'the command is lost when the shell is not answering'
|
||||||
|
|
||||||
|
# No command is a usage error, not a bare pkexec prompt for nothing.
|
||||||
|
run_wrapper --reason "Aimless" -- && note 'the wrapper accepts a reason with no command'
|
||||||
|
|
||||||
|
# ── The QML side, statically ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# The IPC door exists and feeds the service.
|
||||||
|
rg -Fq 'target: "polkit"' "$shell_qml" \
|
||||||
|
|| note 'shell.qml has no polkit IPC target'
|
||||||
|
rg -Fq 'Polkit.stateReason(text)' "$shell_qml" \
|
||||||
|
|| note 'the polkit IPC target does not feed Polkit.stateReason'
|
||||||
|
|
||||||
|
# Single-shot, bounded, and cleared: consumed on adopt even when stale, aged
|
||||||
|
# against a ten-second window, and wiped with the rest of the request state.
|
||||||
|
rg -Fq 'root.pendingReason = null' "$service" \
|
||||||
|
|| note 'a stated reason is not consumed when a request arrives'
|
||||||
|
rg -Fq 'pending.at <= 10000' "$service" \
|
||||||
|
|| note 'a stated reason never expires, so it can dress up a later prompt'
|
||||||
|
rg -Fq 'root.statedReason = ""' "$service" \
|
||||||
|
|| note 'the stated reason survives dismissal'
|
||||||
|
|
||||||
|
# The prompt shows the real message AND the labeled reason -- both, in that
|
||||||
|
# trust order.
|
||||||
|
rg -Fq 'text: Polkit.message' "$prompt" \
|
||||||
|
|| note "polkitd's own action message is no longer rendered"
|
||||||
|
rg -Fq 'text: Polkit.statedReason' "$prompt" \
|
||||||
|
|| note 'the stated reason is never rendered'
|
||||||
|
rg -Fq 'Stated reason (unverified)' "$prompt" \
|
||||||
|
|| note 'the stated reason is not labeled as an unverified claim'
|
||||||
|
|
||||||
|
if (( ${#findings[@]} > 0 )); then
|
||||||
|
printf 'polkit reason contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||||
|
printf ' - %s\n' "${findings[@]}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'polkit reason contract: PASS\n'
|
||||||
Executable
+104
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# The front door: `boot` is the script the README tells a fresh machine to
|
||||||
|
# curl, so it runs before anything else Panama ships -- including its own
|
||||||
|
# tests. What it must get right is small and worth pinning:
|
||||||
|
#
|
||||||
|
# * a machine without the clone gets one, from the documented URL, at
|
||||||
|
# PANAMA_PATH, and the install runs
|
||||||
|
# * a machine with the clone is not re-cloned -- the same command is the
|
||||||
|
# recovery command -- and a fast-forward failure does not stop the install
|
||||||
|
# * boot hands off to the clone's own install, with PANAMA_PATH exported,
|
||||||
|
# so a clone at a chosen location installs from that location
|
||||||
|
#
|
||||||
|
# Run against stub git and install in a throwaway PANAMA_PATH; nothing here
|
||||||
|
# touches the real clone or the network.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
boot="$repo_dir/boot"
|
||||||
|
|
||||||
|
findings=()
|
||||||
|
note() { findings+=("$1"); }
|
||||||
|
|
||||||
|
[[ -x "$boot" ]] || { printf 'boot contract: %s is not executable\n' "$boot" >&2; exit 1; }
|
||||||
|
|
||||||
|
work="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$work"' EXIT
|
||||||
|
|
||||||
|
calls="$work/calls"
|
||||||
|
stub_dir="$work/bin"
|
||||||
|
clone_dir="$work/Panama"
|
||||||
|
mkdir -p "$stub_dir"
|
||||||
|
|
||||||
|
# The stub install records that it ran and what PANAMA_PATH it saw. The stub
|
||||||
|
# git records its arguments, and materializes a clone the way the real one
|
||||||
|
# would -- boot execs the clone's install, so the clone has to contain one.
|
||||||
|
cat >"$work/fake-install" <<STUB
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf 'install PANAMA_PATH=%s\n' "\${PANAMA_PATH:-unset}" >>"$calls"
|
||||||
|
STUB
|
||||||
|
chmod +x "$work/fake-install"
|
||||||
|
|
||||||
|
cat >"$stub_dir/git" <<STUB
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf 'git %s\n' "\$*" >>"$calls"
|
||||||
|
if [[ "\$1" == "clone" ]]; then
|
||||||
|
mkdir -p "\$3/.git"
|
||||||
|
cp "$work/fake-install" "\$3/install"
|
||||||
|
fi
|
||||||
|
STUB
|
||||||
|
chmod +x "$stub_dir/git"
|
||||||
|
|
||||||
|
run_boot() {
|
||||||
|
: >"$calls"
|
||||||
|
PATH="$stub_dir:$PATH" PANAMA_PATH="$clone_dir" bash "$boot" </dev/null >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── A machine without the clone ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
run_boot || note 'boot failed on a machine without the clone'
|
||||||
|
|
||||||
|
grep -q "git clone https://git.gbrown.org/gib/Panama.git $clone_dir" "$calls" \
|
||||||
|
|| note 'boot does not clone the documented repository to PANAMA_PATH'
|
||||||
|
grep -q "install PANAMA_PATH=$clone_dir" "$calls" \
|
||||||
|
|| note 'boot does not hand off to the clone'\''s install with PANAMA_PATH exported'
|
||||||
|
|
||||||
|
# ── A machine that already has it ────────────────────────────────────────────
|
||||||
|
|
||||||
|
run_boot || note 'boot failed on a machine that already has the clone'
|
||||||
|
|
||||||
|
grep -q 'git clone' "$calls" \
|
||||||
|
&& note 'boot re-clones over an existing checkout'
|
||||||
|
grep -q 'git -C .* pull --ff-only' "$calls" \
|
||||||
|
|| note 'boot does not fast-forward an existing clone'
|
||||||
|
grep -q "install PANAMA_PATH=$clone_dir" "$calls" \
|
||||||
|
|| note 'boot does not run the install from an existing clone'
|
||||||
|
|
||||||
|
# ── A diverged clone still installs ──────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# pull --ff-only refusing is normal life -- local commits, a rebase upstream.
|
||||||
|
# The command doubles as the repair path, so a refusal must be stepped over.
|
||||||
|
|
||||||
|
cat >"$stub_dir/git" <<'STUB'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
[[ "$*" == *pull* ]] && exit 1
|
||||||
|
exit 0
|
||||||
|
STUB
|
||||||
|
chmod +x "$stub_dir/git"
|
||||||
|
|
||||||
|
: >"$calls"
|
||||||
|
if ! PATH="$stub_dir:$PATH" PANAMA_PATH="$clone_dir" bash "$boot" </dev/null >/dev/null 2>&1; then
|
||||||
|
note 'a clone that cannot fast-forward stops the install instead of proceeding'
|
||||||
|
fi
|
||||||
|
grep -q "install PANAMA_PATH=$clone_dir" "$calls" \
|
||||||
|
|| note 'the install does not run when the fast-forward is refused'
|
||||||
|
|
||||||
|
if (( ${#findings[@]} > 0 )); then
|
||||||
|
printf 'boot contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||||
|
printf ' - %s\n' "${findings[@]}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'boot contract: PASS\n'
|
||||||
@@ -115,6 +115,31 @@ if grep 'flatpak install' <<<"$recorded" | grep -q 'from-dnf'; then
|
|||||||
note 'a dnf package is passed to flatpak'
|
note 'a dnf package is passed to flatpak'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# A flatpak-only category -- which most of the real ones are -- must survive
|
||||||
|
# the installer's own strict options. Filtering the dnf half with `grep -v`
|
||||||
|
# once left an exit 1 for zero matches, and set -e killed the stage before its
|
||||||
|
# flatpak half ran. Run under those options, not the contract's laxer ones;
|
||||||
|
# the status is captured rather than `||`-guarded because a condition context
|
||||||
|
# would switch errexit off inside the subshell and hide the very failure this
|
||||||
|
# pins.
|
||||||
|
: >"$calls"
|
||||||
|
flatpak_only="$work/flatpak-only"
|
||||||
|
printf 'flatpak:org.example.OnlyFlatpak\n' >"$flatpak_only"
|
||||||
|
(
|
||||||
|
set -euo pipefail
|
||||||
|
PATH="$stub_dir:$PATH"
|
||||||
|
log() { :; }
|
||||||
|
source "$catalog"
|
||||||
|
eval "$filter"
|
||||||
|
eval "$loop"
|
||||||
|
install_extra_category "$flatpak_only"
|
||||||
|
)
|
||||||
|
flatpak_only_status=$?
|
||||||
|
(( flatpak_only_status == 0 )) \
|
||||||
|
|| note 'a flatpak-only category aborts the installer under set -euo pipefail'
|
||||||
|
grep -q 'flatpak install -y flathub org.example.OnlyFlatpak' <<<"$(cat "$calls" 2>/dev/null)" \
|
||||||
|
|| note 'a flatpak-only category installs nothing'
|
||||||
|
|
||||||
# Choosing nothing installs nothing.
|
# Choosing nothing installs nothing.
|
||||||
: >"$calls"
|
: >"$calls"
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -52,8 +52,15 @@ done <<<"$consumed"
|
|||||||
|
|
||||||
# ── 3. Nothing is left behind ────────────────────────────────────────────────
|
# ── 3. Nothing is left behind ────────────────────────────────────────────────
|
||||||
|
|
||||||
grep -q 'trap cleanup EXIT INT TERM' "$install_script" \
|
# Cleanup rides the EXIT trap; INT and TERM must exit explicitly, because a
|
||||||
|| note 'install does not arm a cleanup trap on EXIT INT TERM'
|
# trap handler that merely cleans up lets bash carry on with the remaining
|
||||||
|
# stages after a Ctrl-C -- MOK enrollment and firmware included.
|
||||||
|
grep -q 'trap cleanup EXIT' "$install_script" \
|
||||||
|
|| note 'install does not arm a cleanup trap on EXIT'
|
||||||
|
grep -qE "trap 'exit [0-9]+' INT" "$install_script" \
|
||||||
|
|| note 'install does not exit on SIGINT, so Ctrl-C would keep installing'
|
||||||
|
grep -qE "trap 'exit [0-9]+' TERM" "$install_script" \
|
||||||
|
|| note 'install does not exit on SIGTERM, so a kill would keep installing'
|
||||||
grep -q 'rm -f "$PANAMA_ANSWERS"' "$install_script" \
|
grep -q 'rm -f "$PANAMA_ANSWERS"' "$install_script" \
|
||||||
|| note 'the cleanup trap does not delete the answers file'
|
|| note 'the cleanup trap does not delete the answers file'
|
||||||
grep -qE 'mktemp' "$install_script" \
|
grep -qE 'mktemp' "$install_script" \
|
||||||
|
|||||||
Reference in New Issue
Block a user