-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmain.py
More file actions
361 lines (291 loc) · 13.7 KB
/
Copy pathmain.py
File metadata and controls
361 lines (291 loc) · 13.7 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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
"""LinuxSetupPro entry point: CLI parsing and full install orchestration."""
import argparse
import os
import shutil
import subprocess
import sys
import time
from rich.console import Console
from core import detector, installer, report, shellConfig
from core.bannerGenerator import BannerGenerator, FONTS, COLOR_SCHEMES, default_settings
from core.errorClassifier import ErrorClassifier
from core.packageManager import get_backend, OperationResult, INSTALLED, FAILED, SKIPPED
from core.report import PackageRecord, Report
from ui import banner, menu
from ui import theme
VERSION = "1.0.0"
# Exact upgrade command shown when the interpreter is too old, per platform.
_PYTHON_UPGRADE = {
detector.TERMUX: "pkg upgrade python",
detector.DEBIAN: "sudo apt update && sudo apt install python3.10",
detector.FEDORA: "sudo dnf install python3.11",
detector.ARCH: "sudo pacman -Syu python",
}
def build_parser():
parser = argparse.ArgumentParser(
prog="linux-setup-pro",
description="Cross-platform setup and hardening tool for Termux and Linux.",
)
parser.add_argument("--dry-run", action="store_true",
help="show what would change without executing anything")
parser.add_argument("--only", choices=["primary", "secondary"],
help="run only the primary categories or only the secondary menu")
parser.add_argument("--set-editor", action="store_true",
help="reopen the editor picker, update EDITOR, then exit")
parser.add_argument("--report", action="store_true",
help="print the saved installation reports and exit")
parser.add_argument("--version", action="store_true", help="print the version and exit")
parser.add_argument("--no-banner", action="store_true",
help="suppress the startup and end-of-run banners")
parser.add_argument("--verbose", action="store_true",
help="show raw subprocess output instead of progress widgets")
parser.add_argument("--remove-banner", action="store_true",
help="remove the persistent shell banner block and exit")
parser.add_argument("--public-ip", action="store_true",
help="show public IP in the startup session block (makes an outbound request)")
return parser
def check_python_version(platform_info, console):
if sys.version_info >= (3, 8):
return
command = _PYTHON_UPGRADE.get(platform_info.platform, "install Python 3.8 or newer")
console.print(f"Python 3.8+ is required (found {sys.version.split()[0]}).", style=theme.ERROR)
console.print(f"Upgrade with: {command}", style=theme.WARN)
sys.exit(1)
def resolve_platform(console):
platform_info = detector.detect()
if platform_info is None:
console.print("Unsupported or unrecognized platform. Aborting.", style=theme.ERROR)
sys.exit(1)
return platform_info
def run_primary(inst, catalog, plat, console, dry_run, verbose):
records = []
started = time.monotonic()
for block in catalog.get("primary", []):
category = block.get("category", "")
entries = []
for entry in block.get("packages", []):
entry = dict(entry)
entry["_category"] = category
entries.append(entry)
if not entries:
continue
console.print()
console.print(category, style=theme.HEADING)
records.extend(inst.install_sequence(entries))
custom_records, notes = customization_flow(inst, catalog, plat, console, dry_run)
records.extend(custom_records)
elapsed = time.monotonic() - started
rpt = Report("primary", plat.pretty_name, plat.backend_key)
rpt.write(records, elapsed, notes)
return records
def customization_flow(inst, catalog, plat, console, dry_run):
console.print()
console.print("Customization", style=theme.HEADING)
custom = catalog.get("customization", {})
editors = custom.get("editors", [])
shells = custom.get("shells", {})
notes = []
selected = menu.editor_multiselect(editors)
entries = []
for editor in editors:
if editor["name"] in selected:
e = dict(editor)
e["_category"] = "Customization"
entries.append(e)
theme_choice = menu.select(
"Choose a shell prompt theme:",
[("starship", "starship - lightweight, fast, no shell switch required"),
("oh-my-zsh", "oh-my-zsh - heavier, popular, requires switching to zsh")],
default="starship",
)
if theme_choice == "starship" and "starship" in shells:
entries.append(_shell_entry("starship", shells["starship"]))
elif theme_choice == "oh-my-zsh" and "zsh" in shells:
entries.append(_shell_entry("zsh", shells["zsh"]))
want_fish = menu.confirm("Install the fish shell as an optional extra?", default=False)
if want_fish and "fish" in shells:
entries.append(_shell_entry("fish", shells["fish"]))
records = inst.install_sequence(entries) if entries else []
if theme_choice == "oh-my-zsh":
records.append(_install_oh_my_zsh(console, dry_run))
if not dry_run and menu.confirm("Switch default shell to zsh?", default=False):
_switch_shell_to_zsh(console)
default_editor = _choose_default_editor(selected)
if default_editor and not dry_run:
path = shellConfig.set_editor(default_editor)
notes.append(f"Default EDITOR set to {default_editor} in {path}. "
f"Change it later with: python3 main.py --set-editor")
elif default_editor:
notes.append(f"Default EDITOR would be set to {default_editor} (dry-run).")
return records, notes
def _shell_entry(name, spec):
entry = dict(spec)
entry["name"] = name
entry["_category"] = "Customization"
return entry
def _choose_default_editor(selected):
if not selected:
return None
if "nano" in selected:
return "nano"
if "micro" in selected:
return "micro"
return selected[0]
def _install_oh_my_zsh(console, dry_run):
url = "https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh"
console.print(f"oh-my-zsh installs by running its official installer from {url}", style=theme.WARN)
if dry_run:
return PackageRecord("Customization", "oh-my-zsh", "Community zsh configuration framework.",
SKIPPED, result=OperationResult("oh-my-zsh", SKIPPED, stdout="dry-run"))
if shutil.which("zsh") is None:
result = OperationResult("oh-my-zsh", FAILED, stderr="zsh is not installed")
return PackageRecord("Customization", "oh-my-zsh", "Community zsh configuration framework.",
FAILED, result=result, cause="zsh missing",
suggestion="Select oh-my-zsh again after zsh installs successfully.")
command = f'sh -c "$(curl -fsSL {url})" "" --unattended'
env = dict(os.environ, RUNZSH="no", CHSH="no")
try:
proc = subprocess.run(["bash", "-c", command], env=env, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, text=True, timeout=300, check=False)
status = INSTALLED if proc.returncode == 0 else FAILED
result = OperationResult("oh-my-zsh", status, ["bash", "-c", command],
proc.stdout or "", proc.stderr or "", proc.returncode)
except (OSError, subprocess.SubprocessError) as exc:
result = OperationResult("oh-my-zsh", FAILED, stderr=str(exc))
return PackageRecord("Customization", "oh-my-zsh", "Community zsh configuration framework.",
result.status, result=result)
def _switch_shell_to_zsh(console):
zsh = shutil.which("zsh")
if not zsh:
console.print("zsh not found; cannot switch shell.", style=theme.WARN)
return
try:
subprocess.run(["chsh", "-s", zsh], check=False)
console.print(f"Default shell set to {zsh}. Restart your terminal to apply.", style=theme.DIM)
except OSError as exc:
console.print(f"Could not switch shell: {exc}", style=theme.WARN)
def run_secondary(inst, catalog, plat, console):
console.print()
console.print("Secondary tools marked [LOCKED] require a proot Linux chroot or root access "
"and may not fully function on stock or unrooted devices.", style=theme.WARN)
entries = []
for block in catalog.get("secondary", []):
for entry in block.get("packages", []):
e = dict(entry)
e["_category"] = block.get("category", "")
entries.append(e)
selected = menu.secondary_menu(entries)
if not selected:
console.print("No tools selected.", style=theme.DIM)
return []
chosen = [e for e in entries if e["name"] in selected]
console.print()
console.print("You are about to install: " + ", ".join(e["name"] for e in chosen), style=theme.HEADING)
if not menu.confirm("Proceed with installation?", default=False):
console.print("Cancelled.", style=theme.DIM)
return []
started = time.monotonic()
records = inst.install_sequence(chosen)
elapsed = time.monotonic() - started
rpt = Report("secondary", plat.pretty_name, plat.backend_key)
rpt.write(records, elapsed)
return records
def banner_generation_flow(backend, plat, console, dry_run):
if not menu.confirm("Generate a persistent terminal banner?", default=False):
return
generator = BannerGenerator(backend, console)
if not dry_run:
generator.ensure_tools()
name = menu.text("Name to display:", default="hacker")
handle = menu.text("Handle or tagline (optional):", default="")
text_value = f"{name} {handle}".strip() if handle else name
if menu.confirm("Customize font and color scheme?", default=False):
font = menu.select("Font:", [(f, f) for f in FONTS], default=FONTS[0])
scheme = menu.select("Color scheme:", [(c, c) for c in COLOR_SCHEMES], default=COLOR_SCHEMES[0])
else:
font, scheme = default_settings()
if dry_run:
console.print(f"Would render banner '{text_value}' with {font}/{scheme} and persist it.",
style=theme.DIM)
return
command = generator.render_preview(text_value, font, scheme)
if command and menu.confirm("Add this banner to your shell startup?", default=True):
path = generator.persist(command)
console.print(f"Banner added to {path}. It appears on your next shell start.", style=theme.DIM)
def action_report(console):
content = report.read_reports()
if content is None:
console.print("No reports found. Run an install first to generate reports/.", style=theme.WARN)
return
# Reports are plain text with no markup; print without style interpretation.
console.print(content, markup=False, highlight=False)
def action_remove_banner(console):
removed_any = False
for shell in ("bash", "zsh"):
path, removed = shellConfig.remove_banner_block(shell)
if removed:
console.print(f"Removed banner block from {path}.", style=theme.DIM)
removed_any = True
if removed_any:
console.print("Restart your terminal for the change to take effect.", style=theme.DIM)
else:
console.print("No banner block found.", style=theme.WARN)
def action_set_editor(catalog, console, dry_run):
editors = catalog.get("customization", {}).get("editors", [])
selected = menu.editor_multiselect(editors)
editor = _choose_default_editor(selected)
if not editor:
console.print("No editor selected.", style=theme.WARN)
return
if dry_run:
console.print(f"Would set EDITOR to {editor} (dry-run).", style=theme.DIM)
return
path = shellConfig.set_editor(editor)
console.print(f"EDITOR set to {editor} in {path}. Restart your terminal to apply.", style=theme.DIM)
def main(argv=None):
args = build_parser().parse_args(argv)
console = Console()
if args.version:
console.print(f"LinuxSetupPro v{VERSION}")
return 0
if args.report:
action_report(console)
return 0
if args.remove_banner:
action_remove_banner(console)
return 0
if not args.no_banner:
banner.show_startup(VERSION, console, public_ip=args.public_ip)
plat = resolve_platform(console)
check_python_version(plat, console)
console.print(f"Detected platform: {plat.pretty_name} (backend: {plat.backend_key})", style=theme.DIM)
backend = get_backend(plat.backend_key, dry_run=args.dry_run)
if backend is None:
console.print("No package backend available for this platform.", style=theme.ERROR)
return 1
catalog = installer.load_catalog()
if args.set_editor:
action_set_editor(catalog, console, args.dry_run)
return 0
classifier = ErrorClassifier(plat.backend_key)
inst = installer.Installer(backend, classifier, console, verbose=args.verbose)
if args.only == "secondary":
run_secondary(inst, catalog, plat, console)
banner_generation_flow(backend, plat, console, args.dry_run)
if not args.no_banner:
banner.show_end(VERSION, plat, console)
return 0
run_primary(inst, catalog, plat, console, args.dry_run, args.verbose)
if args.only == "primary":
banner_generation_flow(backend, plat, console, args.dry_run)
if not args.no_banner:
banner.show_end(VERSION, plat, console)
return 0
if menu.confirm("Proceed to the secondary security tools menu?", default=False):
run_secondary(inst, catalog, plat, console)
banner_generation_flow(backend, plat, console, args.dry_run)
if not args.no_banner:
banner.show_end(VERSION, plat, console)
return 0
if __name__ == "__main__":
sys.exit(main())