diff --git a/bin/omarchy-install-hermes-cli b/bin/omarchy-install-hermes-cli index c45adca4..a5acd0fe 100755 --- a/bin/omarchy-install-hermes-cli +++ b/bin/omarchy-install-hermes-cli @@ -368,19 +368,24 @@ if runtime_ready; then runtime_present=true fi +# A seeded app has to be whole, whatever state the runtime beside it is in. +refuse_unless_app_whole() { + if [[ -e $native_app || -L $native_app ]] && ! native_app_complete; then + echo "The Hermes desktop app at $native_app is incomplete. Repair it with 'hermes desktop --build-only' before trying again." >&2 + return 1 + fi +} + # The packaged app can be seeded only beside a runtime at the release it was -# built from, with its desktop sources untouched, and a seeded app has to be -# whole. Asked before a command is replaced, so that a refusal leaves the -# user's Hermes exactly as it was, and again before seeding. +# built from, with its desktop sources untouched and the Linux runtime patch +# still able to land. Asked before a command is replaced, so that a refusal +# leaves the user's Hermes exactly as it was, and again before seeding. # Asked through ||, so nothing in here stops on its own: every git answer is # checked, and one that cannot be read refuses rather than reading as clean. refuse_unless_seedable() { local runtime_commit changes + refuse_unless_app_whole || return 1 if [[ -e $native_app || -L $native_app ]]; then - if ! native_app_complete; then - echo "The Hermes desktop app at $native_app is incomplete. Repair it with 'hermes desktop --build-only' before trying again." >&2 - return 1 - fi return 0 fi if ! runtime_commit=$(git -C "$runtime" rev-parse HEAD 2>/dev/null) || [[ -z $runtime_commit ]]; then @@ -399,26 +404,31 @@ refuse_unless_seedable() { echo "Hermes desktop sources have local changes. Run 'hermes desktop --build-only', then try again; existing files have been kept." >&2 return 1 fi + if ! git -C "$runtime" apply --check /usr/share/hermes-desktop/runtime.patch >/dev/null 2>&1 && + ! git -C "$runtime" apply --reverse --check /usr/share/hermes-desktop/runtime.patch >/dev/null 2>&1; then + echo "The Hermes Linux runtime patch conflicts with local changes. Existing files have been kept." >&2 + return 1 + fi } if [[ $runtime_present == "true" ]]; then refuse_unless_seedable || exit 1 +elif [[ -e $runtime || -L $runtime ]]; then + # The upstream installer can reset an existing checkout. Do not pin a newer + # or modified runtime back to the package release while repairing setup, + # and a git state that cannot be read is not a clean one. + refuse_unless_app_whole || exit 1 + if ! runtime_commit=$(git -C "$runtime" rev-parse HEAD 2>/dev/null) || [[ $runtime_commit != "$release_commit" ]] || + ! changes=$(git -C "$runtime" status --porcelain --untracked-files=all 2>/dev/null) || [[ -n $changes ]]; then + echo "Hermes setup is incomplete at $runtime. Repair that installation before trying again; existing files have been kept." >&2 + exit 1 + fi fi # Whether this run gave Hermes something new to show the theme to: a runtime # it set up, or the app it seeded. Putting a command back is neither. set_up=false -if [[ $runtime_present == "false" && ( -e $runtime || -L $runtime ) ]]; then - # The upstream installer can reset an existing checkout. Do not pin a newer - # or modified runtime back to the package release while repairing setup. - if [[ $(git -C "$runtime" rev-parse HEAD 2>/dev/null) != "$release_commit" ]] || - [[ -n $(git -C "$runtime" status --porcelain --untracked-files=all) ]]; then - echo "Hermes setup is incomplete at $runtime. Repair that installation before trying again; existing files have been kept." >&2 - exit 1 - fi -fi - # The command is the runtime's own and runs; anything else at the path gets # replaced below. The retired wrapper is never run to find out. command_ready=false diff --git a/test/shell.d/hermes-desktop-install-test.sh b/test/shell.d/hermes-desktop-install-test.sh index 39f0591b..49c332aa 100644 --- a/test/shell.d/hermes-desktop-install-test.sh +++ b/test/shell.d/hermes-desktop-install-test.sh @@ -384,6 +384,50 @@ run_installer && fail "incomplete modified runtime cannot be reset by upstream i ! grep -qx bootstrap "$test_tmp/events" || fail "modified runtime never reaches upstream installer" pass "patch conflicts and incomplete modified runtimes retain local changes and stop safely" +# Every refusal comes before anything of the user's is touched, whatever state +# the runtime is in: a launcher of their own is neither saved aside nor +# replaced, and no bootstrap runs, by a run that is going to stop anyway. +own_launcher="#!/bin/bash +exec \"$test_home/tools/hermes\" \"\$@\"" +assert_untouched() { + [[ $(cat "$test_home/.local/bin/hermes") == "$own_launcher" ]] || fail "$1: the user's launcher is not as it was" + [[ -z $(find "$test_home/.local/bin" -maxdepth 1 -name '.hermes-before-desktop.*' -print) ]] || fail "$1: something was saved aside" + [[ ! -s $test_tmp/events ]] || fail "$1: setup ran" "$(cat "$test_tmp/events")" +} +# A finished runtime at the release whose edit the runtime patch cannot land on. +new_home patch-conflict-own-launcher +touch "$test_tmp/package-installed" +HOME="$test_home" HERMES_HOME="$hermes_home" bash "$test_tmp/share/install.sh" --dir "$runtime" --hermes-home "$hermes_home" +printf 'local edit\n' >"$runtime/runtime.txt" +printf '%s\n' "$own_launcher" >"$test_home/.local/bin/hermes" +: >"$test_tmp/events" +run_cli --now && fail "a conflicting edit still stops setup" +grep -q 'patch conflicts' "$test_tmp/output" || fail "a conflicting edit is named" "$(cat "$test_tmp/output")" +assert_untouched "a patch conflict" +# An unfinished runtime beside a half-built app. +new_home incomplete-app +touch "$test_tmp/package-installed" +OMARCHY_TEST_NO_MARKER=1 HOME="$test_home" HERMES_HOME="$hermes_home" bash "$test_tmp/share/install.sh" --dir "$runtime" --hermes-home "$hermes_home" +mkdir -p "$native/resources" +printf 'half\n' >"$native/resources/app.asar" +printf '%s\n' "$own_launcher" >"$test_home/.local/bin/hermes" +: >"$test_tmp/events" +run_cli --now && fail "a half-built app beside an unfinished runtime still stops setup" +grep -q 'is incomplete' "$test_tmp/output" || fail "a half-built app is named" "$(cat "$test_tmp/output")" +assert_untouched "a half-built app" +# An unfinished runtime whose git state cannot be read is not a clean one. +new_home unreadable-incomplete +touch "$test_tmp/package-installed" +OMARCHY_TEST_NO_MARKER=1 HOME="$test_home" HERMES_HOME="$hermes_home" bash "$test_tmp/share/install.sh" --dir "$runtime" --hermes-home "$hermes_home" +printf '%s\n' "$own_launcher" >"$test_home/.local/bin/hermes" +chmod 000 "$runtime/.git/index" +: >"$test_tmp/events" +run_cli --now && { chmod 644 "$runtime/.git/index"; fail "an unreadable git state beside an unfinished runtime is not a clean tree"; } +chmod 644 "$runtime/.git/index" +grep -q 'incomplete at' "$test_tmp/output" || fail "an unreadable git state is refused as incomplete" "$(cat "$test_tmp/output")" +assert_untouched "an unreadable git state" +pass "a refusal leaves the user's launcher and runtime as they were, whatever state the runtime is in" + # Once set up, a runtime is the user's to edit; a finished install is not # re-patched or re-verified, only opened. new_home finished-edit @@ -543,9 +587,8 @@ run_cli --now || fail "a finished install is accepted by the default agent path" run_cli --check || fail "--check follows the installed runtime" pass "choosing Hermes as the default agent installs the app's runtime without opening the app" -# A Hermes the user set up themselves is what the default agent runs, and -# nothing is installed beside it. Asked for the app by name, Omarchy installs -# the package first, and then the runtime supersedes it with the command saved. +# A Hermes from elsewhere is not the app's: choosing Hermes installs the app +# over it, with the previous command saved aside. new_home own-hermes mkdir -p "$test_home/.local/bin" cat >"$test_home/.local/bin/hermes" <<'SH'