Repository navigation
Expand file tree
/
Copy pathtopo
More file actions
executable file
·89 lines (78 loc) · 3.69 KB
/
Copy pathtopo
File metadata and controls
executable file
·89 lines (78 loc) · 3.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#!/usr/bin/env python3
import sys
import os
# The floor the code actually requires: PEP 604 annotations like `int | None` are
# evaluated at runtime inside dataclasses, and the CI test matrix starts at 3.10.
# Checked before importing anything from src/ and written in a dialect old
# interpreters can still parse, because that is the whole point -- on Debian 11
# (3.9) or RHEL 8 (3.6) those imports fail with SyntaxError or TypeError, and the
# ImportError handler below would blame a perfectly present source tree.
MIN_PYTHON = (3, 10)
if sys.version_info < MIN_PYTHON:
sys.stderr.write(
"Error: Topo requires Python {}.{} or newer, but this is Python {}.\n".format(
MIN_PYTHON[0],
MIN_PYTHON[1],
".".join(str(part) for part in sys.version_info[:3]),
)
)
sys.exit(1)
# Get the real physical path of the script
REAL_PATH = os.path.realpath(__file__)
REAL_DIR = os.path.dirname(REAL_PATH)
# A system package must not grow files under /usr that its own file list does not
# mention. Most of what topo does needs root, and the first `sudo topo clean`
# would otherwise have CPython write __pycache__/*.pyc all through
# /usr/lib/topo/src/ -- files dpkg never recorded, so `apt remove topo` cannot
# delete them and cannot rmdir the directories holding them either, leaving a
# shell of empty directories and stale bytecode behind after an uninstall.
#
# The marker is read here, by hand, rather than through get_install_source() in
# src/core/install_source.py, which owns both the filename and the meaning of its
# contents: this has to be decided *before* the first import from src/, or that
# import is itself the thing that writes a .pyc. Same reason MIN_PYTHON above is
# spelled out here instead of imported. A script install keeps its bytecode cache
# and its faster startup; it writes into a directory that is the user's own.
try:
with open(os.path.join(REAL_DIR, ".topo-install-source")) as marker:
if marker.read().strip().lower() == "package":
sys.dont_write_bytecode = True
except OSError:
pass
# Add project root to Python search path so src is recognized as a package
sys.path.insert(0, REAL_DIR)
def restore_terminal_after_interrupt():
try:
from src.core import terminal_state
terminal_state.reset_terminal(force=True)
except Exception:
pass
try:
sys.stdout.write("\r\033[K")
sys.stdout.flush()
except Exception:
pass
# Only the import is allowed to mean "source modules are missing". main() used to
# sit in this same try, so any lazy import failing *inside* a feature module --
# `import termios` in remove.py, say -- was reported as a broken installation and
# pointed the user at a directory that was fine all along.
try:
try:
from src.main import main
except ImportError as e:
print(f"Error: Could not find topo source modules in {REAL_DIR}", file=sys.stderr)
print(f"Debug info: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
except KeyboardInterrupt:
# Only a Ctrl-C during import above, or in the instant after main() is called
# but before it installs its own signal handler, reaches here: once main() is
# running it catches KeyboardInterrupt itself and converts it to
# SystemExit(130), which this clause does not intercept. The message is a
# verbatim copy of src.main.INTERRUPTED_MESSAGE -- it cannot be imported,
# because the import is one of the things this very handler is covering -- and
# 130 is the same 128 + SIGINT main() exits with. Keep both in sync.
restore_terminal_after_interrupt()
print("\n🚫 Process interrupted by user.", file=sys.stderr)
sys.exit(130)