linux observer
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 210 lines 5.7 kB view raw
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3"""Install ownership guard for pipx-managed service installs.""" 4 5from __future__ import annotations 6 7import sys 8from enum import Enum 9from pathlib import Path 10 11MARKER_REL = Path(".config/solstone-linux/.install-source") 12PIPX_BIN_REL = Path(".local/bin/solstone-linux") 13 14 15def marker_path() -> Path: 16 return Path.home() / MARKER_REL 17 18 19def pipx_bin_path() -> Path: 20 return Path.home() / PIPX_BIN_REL 21 22 23class State(str, Enum): 24 ABSENT = "ABSENT" 25 OWNED = "OWNED" 26 CROSS_REPO = "CROSS_REPO" 27 PARTIAL_OWNED = "PARTIAL_OWNED" 28 UNKNOWN = "UNKNOWN" 29 30 31def _parse_marker() -> Path | None: 32 try: 33 raw = marker_path().read_text(encoding="utf-8") 34 except OSError: 35 return None 36 37 stripped = raw.strip() 38 if not stripped: 39 return None 40 41 lines = stripped.splitlines() 42 if len(lines) != 1: 43 return None 44 45 candidate = Path(lines[0].strip()) 46 if not candidate.is_absolute(): 47 return None 48 49 return candidate.resolve() 50 51 52def check(curdir: Path) -> tuple[State, Path | None]: 53 resolved_curdir = curdir.resolve() 54 marker = marker_path() 55 pipx_bin_present = pipx_bin_path().exists() 56 57 if not marker.exists(): 58 if not pipx_bin_present: 59 return (State.ABSENT, None) 60 return (State.UNKNOWN, None) 61 62 owner = _parse_marker() 63 if owner is None: 64 return (State.UNKNOWN, None) 65 if owner != resolved_curdir: 66 return (State.CROSS_REPO, owner) 67 if pipx_bin_present: 68 return (State.OWNED, owner) 69 return (State.PARTIAL_OWNED, owner) 70 71 72def write_marker(curdir: Path) -> None: 73 path = marker_path() 74 path.parent.mkdir(parents=True, exist_ok=True) 75 path.write_text(f"{curdir.resolve()}\n", encoding="utf-8") 76 77 78def remove_marker() -> None: 79 marker_path().unlink(missing_ok=True) 80 81 82def _unknown_reason() -> str: 83 if marker_path().exists(): 84 return ".install-source marker is malformed" 85 return "no .install-source marker — likely pre-hygiene install" 86 87 88def _print_cross_repo_error(curdir: Path, owner: Path | None, uninstall: bool) -> None: 89 lines = [ 90 "error: cross-repo contamination detected", 91 f"current repo: {curdir.resolve()}", 92 f"installed from: {owner}", 93 "", 94 "To recover, run from the installed repo:", 95 " make uninstall-service", 96 "Or manually:", 97 ] 98 if uninstall: 99 lines.extend( 100 [ 101 " systemctl --user stop solstone-linux.service", 102 " systemctl --user disable solstone-linux.service", 103 " rm -f ~/.config/systemd/user/solstone-linux.service", 104 ] 105 ) 106 lines.extend( 107 [ 108 " pipx uninstall solstone-linux", 109 " rm ~/.config/solstone-linux/.install-source", 110 ] 111 ) 112 print("\n".join(lines), file=sys.stderr) 113 114 115def _print_unknown_error(uninstall: bool) -> None: 116 lines = [ 117 f"error: installed: unknown ({_unknown_reason()})", 118 "", 119 "To recover:", 120 ] 121 if uninstall: 122 lines.extend( 123 [ 124 " systemctl --user stop solstone-linux.service", 125 " systemctl --user disable solstone-linux.service", 126 " rm -f ~/.config/systemd/user/solstone-linux.service", 127 ] 128 ) 129 lines.extend( 130 [ 131 " pipx uninstall solstone-linux", 132 " rm -f ~/.config/solstone-linux/.install-source", 133 "Then re-run make install-service.", 134 ] 135 ) 136 print("\n".join(lines), file=sys.stderr) 137 138 139def _preinstall(curdir: Path) -> int: 140 state, owner = check(curdir) 141 if state is State.ABSENT: 142 print("mode: fresh install") 143 return 0 144 if state is State.OWNED: 145 print("mode: upgrade") 146 return 10 147 if state is State.PARTIAL_OWNED: 148 print( 149 "warning: .install-source marker present but pipx binary missing — reinstalling" 150 ) 151 print("mode: upgrade") 152 return 10 153 if state is State.CROSS_REPO: 154 print("mode: aborted — cross-repo contamination") 155 _print_cross_repo_error(curdir, owner, uninstall=False) 156 return 2 157 158 print("mode: aborted — unknown install state") 159 _print_unknown_error(uninstall=False) 160 return 2 161 162 163def _preuninstall(curdir: Path) -> int: 164 state, owner = check(curdir) 165 if state is State.ABSENT: 166 print("no artifacts to remove") 167 return 0 168 if state in {State.OWNED, State.PARTIAL_OWNED}: 169 return 10 170 if state is State.CROSS_REPO: 171 print("mode: aborted — cross-repo contamination") 172 _print_cross_repo_error(curdir, owner, uninstall=True) 173 return 2 174 175 print("mode: aborted — unknown install state") 176 _print_unknown_error(uninstall=True) 177 return 2 178 179 180def main() -> int: 181 if len(sys.argv) < 2: 182 print( 183 "usage: install_guard <preinstall|preuninstall|write|remove> [curdir]", 184 file=sys.stderr, 185 ) 186 return 2 187 188 command = sys.argv[1] 189 if command == "remove": 190 remove_marker() 191 return 0 192 193 if command in {"preinstall", "preuninstall", "write"}: 194 if len(sys.argv) != 3: 195 print(f"usage: install_guard {command} <curdir>", file=sys.stderr) 196 return 2 197 curdir = Path(sys.argv[2]) 198 if command == "preinstall": 199 return _preinstall(curdir) 200 if command == "preuninstall": 201 return _preuninstall(curdir) 202 write_marker(curdir) 203 return 0 204 205 print(f"unknown command: {command}", file=sys.stderr) 206 return 2 207 208 209if __name__ == "__main__": 210 sys.exit(main())