-
Notifications
You must be signed in to change notification settings - Fork 236
/
Copy pathinstall-eaf.py
executable file
·546 lines (483 loc) · 22.2 KB
/
install-eaf.py
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
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
#!/usr/bin/env python3
import argparse
import datetime
import json
import os
import subprocess
import sys
import sysconfig
from shutil import rmtree, which
parser = argparse.ArgumentParser()
parser.add_argument("--install-all-apps", action="store_true",
help='install/update all available applications')
parser.add_argument("--install-core-deps", action="store_true",
help='only install/update core dependencies')
parser.add_argument("-i", "--install", nargs='+', default=[],
help='only install/update apps listed here')
parser.add_argument("--install-new-apps", action="store_true",
help='also install previously uninstalled or new applications')
parser.add_argument("-f", "--force", action="store_true",
help="force install/update app dependencies even if apps are already up-to-date")
parser.add_argument("--ignore-core-deps", action="store_true",
help='ignore core dependencies')
parser.add_argument("--ignore-sys-deps", action="store_true",
help='ignore system dependencies')
parser.add_argument("--ignore-py-deps", action="store_true",
help='ignore python dependencies')
parser.add_argument("--ignore-node-deps", action="store_true",
help='ignore node dependencies')
parser.add_argument("--git-full-clone", action="store_true",
help='during installation, conduct a full clone to preserve git logs')
parser.add_argument("--app-drop-local-edit", action="store_true",
help='during installation, local changes to app repos will be hard reset')
parser.add_argument("--app-save-local-edit", action="store_true",
help='compared with --app-drop-local-edit, this option will stash your changes')
args = parser.parse_args()
NPM_CMD = "npm.cmd" if sys.platform == "win32" else "npm"
PIP_CMD = "pip3" if which("pip3") else "pip" # mac only have pip3, so we need use pip3 instead pip
class bcolors:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
script_path = os.path.dirname(os.path.realpath(__file__))
def get_available_apps_dict():
with open(os.path.join(script_path, 'applications.json')) as f:
info = json.load(f)
apps_dict = {}
for app_name, app_spec_dict in info.items():
if app_spec_dict["type"] == "app":
apps_dict[app_name] = app_spec_dict
return apps_dict
available_apps_dict = get_available_apps_dict()
install_failed_sys = []
install_failed_pys = []
install_failed_npm_globals = []
install_failed_apps = []
important_messages = [
"[EAF] Please run 'git pull && ./install-eaf.py' (M-x eaf-install-and-update) to update EAF and their dependencies.",
"[EAF] Refer to dependencies.json for the list of dependencies installed on your system."
]
def run_command(command, path=script_path, ensure_pass=True, get_result=False):
print("[EAF] Running", ' '.join(command), "@", path)
# Throw exception if command not found,
# We found install-eaf.py still can work even it not found npm in system.
if not which(command[0]):
return Exception(f"Not found command: {command[0]}")
# Use LC_ALL=C to make sure command output use English.
# Then we can use English keyword to check command output.
english_env = os.environ.copy()
english_env['LC_ALL'] = 'C'
if get_result:
process = subprocess.Popen(command, env = english_env, stdin = subprocess.PIPE,
universal_newlines=True, text=True, cwd=path,
stdout = subprocess.PIPE)
else:
process = subprocess.Popen(command, env = english_env, stdin = subprocess.PIPE,
universal_newlines=True, text=True, cwd=path)
process.wait()
if process.returncode != 0 and ensure_pass:
raise Exception(process.returncode)
if get_result:
return process.stdout.readlines()
else:
return None
def prune_existing_sys_deps(deps_list):
remove_deps = []
for dep in deps_list:
if "node" in dep and which("node"):
remove_deps.append(dep)
elif "npm" in dep and which("npm"):
remove_deps.append(dep)
return list(set(deps_list) - set(remove_deps))
def get_archlinux_aur_helper():
command = None
for helper in ["paru", "pacaur", "yay", "yaourt", "aura"]:
if which(helper):
command = helper
break
if command:
return command
else:
print("Please install one of AUR's helper, such as 'pacaur', 'yay', 'yaourt', 'paru', etc.", file=sys.stderr)
sys.exit(1)
def install_sys_deps(distro: str, deps_list):
deps_list = prune_existing_sys_deps(deps_list)
command = []
if distro == 'dnf':
command = ['sudo', 'dnf', '-y', 'install']
elif distro == 'emerge':
command = ['sudo', 'emerge', "--update"]
elif distro == 'apt':
command = ['sudo', 'apt', '-y', 'install']
elif distro == 'pacman':
aur_helper = get_archlinux_aur_helper()
command = [aur_helper, '-Sy', '--noconfirm', '--needed']
elif which("pkg"):
command = ['doas', 'pkg', '-y', 'install']
elif which("guix"):
command = ['guix', 'install']
elif which("nix"):
command = ['nix', 'profile', 'install']
elif which("zypper"):
command = ['sudo', 'zypper', 'install','-y']
command.extend(deps_list)
try:
run_command(command)
except Exception as e:
print("Error: {}".format(e))
install_failed_sys.append(' '.join(command))
def install_py_deps(deps_list):
if sys.prefix == sys.base_prefix:
# pass --break-system-packages to permit installing packages into EXTERNALLY-MANAGED Python installations. see https://github.com/pypa/pip/issues/11780
if get_distro() != "guix" and os.path.exists(os.path.join(sysconfig.get_path("stdlib", sysconfig.get_default_scheme() if hasattr(sysconfig, "get_default_scheme") else sysconfig._get_default_scheme()),"EXTERNALLY-MANAGED")):
command = [PIP_CMD, 'install', '--user', '--break-system-packages', '-U']
else:
command = [PIP_CMD, 'install', '--user', '-U']
else:
# if running on a virtual env, --user option is not valid.
command = [PIP_CMD, 'install', '-U']
command.extend(deps_list)
try:
run_command(command)
except Exception as e:
print("Error:", e)
install_failed_pys.append(' '.join(command))
def install_npm_gloal_deps(deps_list):
command = ["sudo", NPM_CMD, "install", "-g"]
command.extend(deps_list)
try:
run_command(command)
except Exception as e:
print("Error:", e)
install_failed_npm_globals.append(' '.join(command))
def remove_node_modules_path(app_path_list):
for app_path in app_path_list:
node_modules_path = os.path.join(app_path, "node_modules")
if os.path.isdir(node_modules_path):
rmtree(node_modules_path)
print("[EAF] WARN: removing {}".format(node_modules_path))
def install_npm_install(app_path_list):
for app_path in app_path_list:
command = [NPM_CMD, "install", "--force"]
try:
run_command(command, path=app_path)
except Exception as e:
print("Error:", e)
install_failed_apps.append(app_path)
def install_npm_rebuild(app_path_list):
for app_path in app_path_list:
command = [NPM_CMD, "rebuild"]
try:
run_command(command, path=app_path)
except Exception as e:
print("Error:", e)
install_failed_apps.append(app_path)
def install_vue_install(app_path_list):
for app_path in app_path_list:
command = [NPM_CMD, "install", "--force"]
try:
run_command(command, path=app_path)
except Exception as e:
print("Error:", e)
install_failed_apps.append(app_path)
command = [NPM_CMD, "run", "build"]
try:
run_command(command, path=app_path)
except Exception as e:
print("Error:", e)
install_failed_apps.append(app_path)
def add_or_update_app(app: str, app_spec_dict):
url = ""
path = os.path.join("app", app)
if os.path.exists(path):
print("\n[EAF] Updating", app, "to newest version...")
else:
print("\n[EAF] Adding", app, "application to EAF...")
url = app_spec_dict['url']
branch = app_spec_dict['branch']
time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M')
updated = True
if os.path.exists(path):
if args.app_drop_local_edit:
print("[EAF] Clean {}'s local changed for pull code automatically.".format(app))
run_command(["git", "clean", "-df"], path=path, ensure_pass=False)
run_command(["git", "reset", "--hard", "origin"], path=path, ensure_pass=False)
elif args.app_save_local_edit:
print("[EAF] Clean {}'s local changed for pull code automatically.".format(app))
run_command(["git", "clean", "-df"], path=path, ensure_pass=False)
run_command(["git", "stash", "save", "[{}] Auto stashed by install-eaf.py".format(time)], path=path, ensure_pass=False)
run_command(["git", "reset", "--hard"], path=path, ensure_pass=False)
run_command(["git", "checkout", branch], path=path, ensure_pass=False)
run_command(["git", "reset", "--hard", "origin"], path=path, ensure_pass=False)
branch_outputs = run_command(["git", "branch"], path=path, get_result=True)
if branch_outputs is None:
raise Exception("Not in git app!")
exist_branch = False
for b in branch_outputs:
if branch in b:
exist_branch = True
break
if not exist_branch:
run_command(["git", "config", "remote.origin.fetch", "+refs/heads/"+branch+":refs/remotes/origin/"+branch], path=path)
if args.git_full_clone:
run_command(["git", "fetch", "origin", branch], path=path)
else:
run_command(["git", "fetch", "origin", branch, "--depth", "1"], path=path)
current_branch_outputs = run_command(["git", "symbolic-ref", "HEAD"], path=path, get_result=True)
if current_branch_outputs is None:
raise Exception("git symbolic-ref failed!")
current_branch = current_branch_outputs[0]
if branch not in current_branch:
run_command(["git", "checkout", branch], path=path)
output_lines = run_command(["git", "pull", "origin", branch], path=path, get_result=True)
if output_lines is None:
raise Exception("git pull failed!")
for output in output_lines:
print(output.rstrip())
if "Already up to date." in output:
updated = False if branch in current_branch else True
elif args.git_full_clone:
run_command(["git", "clone", "-b", branch, url, path])
else:
run_command(["git", "clone", "-b", branch, "--depth", "1", url, path])
return updated
def get_distro():
distro = ""
if sys.platform != "linux":
pass
elif which("dnf"):
distro = "dnf"
elif which("emerge"):
distro = "emerge"
elif which("apt"):
distro = "apt"
elif which("pacman"):
distro = "pacman"
aur_helper = get_archlinux_aur_helper()
if (not args.ignore_core_deps and not args.ignore_sys_deps and len(args.install) == 0) or args.install_core_deps:
try:
run_command([aur_helper, '-Sy', '--noconfirm', '--needed'])
except:
print("Run command `{} -Sy --noconfirm --needed' failed.".format(aur_helper))
elif which("pkg"):
distro = "pkg"
elif which("guix"):
distro = "guix"
elif which("zypper"):
distro = "zypper"
elif which("brew"):
distro = "brew"
elif which("nix"):
distro = "nix"
elif sys.platform == "linux":
print("[EAF] Unsupported Linux distribution/package manager.")
print(" Please see dependencies.json for list of dependencies.")
if not (args.ignore_core_deps or args.ignore_sys_deps):
sys.exit(1)
return distro
def install_core_deps(distro, deps_dict):
print("[EAF] Installing core dependencies")
core_deps = []
if not args.ignore_sys_deps and sys.platform == "linux":
core_deps.extend(deps_dict[distro])
if len(core_deps) > 0:
install_sys_deps(distro, core_deps)
if (not args.ignore_py_deps or sys.platform != "linux") and sys.platform in deps_dict["pip"]:
# For pip dependencies, the distribution name takes precedence over the os name.
#
# For example, in Arch Linux, we need install PyQt from Arch repository to instead install from PIP repository
# to make EAF browser support HTML5 video:
#
# rm -rf ~/.local/lib/python3.10/site-packages/PyQt6*
# sudo rm -rf /usr/lib/python3.10/site-packages/PyQt6*
# sudo pacman -S python-pyqt6-webengine python-pyqt6 python-pyqt6-sip
distro = get_distro()
if distro in deps_dict["pip"]:
install_py_deps(deps_dict["pip"][distro])
else:
install_py_deps(deps_dict["pip"][sys.platform])
print("[EAF] Finished installing core dependencies")
def yes_no(question, default_yes=False, default_no=False):
key = input(question)
if default_yes:
return key.lower() == 'y' or key == ""
elif default_no:
return key.lower() == 'y' or not (key == "" or key.lower() == 'n')
else:
return key.lower() == 'y'
def get_installed_apps(app_dir):
apps_installed = [
f
for f in os.listdir(app_dir)
if os.path.isdir(os.path.join(app_dir, f))
# directories not managed, or not part of, EAF
if f not in ("__pycache__",)
]
for app in apps_installed:
git_dir = os.path.join(app_dir, app, ".git")
if app not in get_available_apps_dict().keys():
apps_installed.remove(app)
if not os.path.isdir(git_dir):
important_messages.append("[EAF] *WARN* 'app/{}' is not a git repo installed by install-eaf.py!".format(app))
apps_installed.remove(app)
return apps_installed
def get_installed_apps_dict(apps_installed):
return {app_name: available_apps_dict[app_name] for app_name in apps_installed}
def get_new_apps_dict(apps_installed):
not_installed_apps_dict = {}
new_apps_dict = {}
num = 1
for app_name, app_spec_dict in available_apps_dict.items():
if app_name not in apps_installed:
not_installed_apps_dict[app_name] = app_spec_dict
for app_name, app_spec_dict in not_installed_apps_dict.items():
indicator = "({}/{})".format(num, len(not_installed_apps_dict))
prompt = "[EAF] " + indicator + " " + app_spec_dict['name'] + ". " + app_spec_dict['desc'] + " - Install?"
install_p = yes_no(prompt + " (Y/n): ", default_yes=True) if app_spec_dict['default_install'] == 'true' else yes_no(prompt + " (y/N): ", default_no=True)
if install_p:
new_apps_dict[app_name] = app_spec_dict
num = num + 1
return new_apps_dict
def get_specific_install_apps_dict(apps_need_install):
need_install_apps_dict = {}
for app_name, app_spec_dict in available_apps_dict.items():
if app_name in apps_need_install:
need_install_apps_dict[app_name] = app_spec_dict
return need_install_apps_dict
def print_sample_config(app_dir):
for app in get_installed_apps(app_dir):
print("(require 'eaf-{})".format(app))
def get_install_apps(apps_installed):
if args.install_all_apps:
return [get_available_apps_dict()]
if len(args.install) > 0:
return [get_specific_install_apps_dict(args.install)]
pending_apps_dict_list = [get_installed_apps_dict(apps_installed)]
if args.install_new_apps or len(apps_installed) == 0:
pending_apps_dict_list.append(get_new_apps_dict(apps_installed))
elif not args.install_new_apps:
important_messages.append("[EAF] Use the flag '--install-new-apps' to install new applications.")
return pending_apps_dict_list
def install_app_deps(distro, deps_dict):
print("[EAF] Installing application dependencies")
app_dir = os.path.join(script_path, "app")
if not os.path.exists(app_dir):
os.makedirs(app_dir)
apps_installed = get_installed_apps(app_dir)
pending_apps_dict_list = get_install_apps(apps_installed)
sys_deps = []
py_deps = []
npm_global_deps = []
npm_install_apps = []
vue_install_apps = []
npm_rebuild_apps = []
for pending_apps_dict in pending_apps_dict_list:
for app_name, app_spec_dict in pending_apps_dict.items():
updated = True
try:
updated = add_or_update_app(app_name, app_spec_dict)
except Exception:
raise Exception("[EAF] There are unsaved changes in EAF " + app_name + " application. Please re-run command with --app-drop-local-edit or --app-save-local-edit")
app_path = os.path.join(app_dir, app_name)
app_dep_path = os.path.join(app_path, 'dependencies.json')
if (updated or args.force) and os.path.exists(app_dep_path):
with open(app_dep_path) as f:
deps_dict = json.load(f)
if not args.ignore_sys_deps and sys.platform == "linux" and distro in deps_dict:
sys_deps.extend(deps_dict[distro])
if not args.ignore_py_deps and 'pip' in deps_dict and sys.platform in deps_dict['pip']:
py_deps.extend(deps_dict['pip'][sys.platform])
if "npm_global" in deps_dict:
npm_global_deps.extend(deps_dict["npm_global"])
if not args.ignore_node_deps:
if 'npm_install' in deps_dict and deps_dict['npm_install']:
npm_install_apps.append(app_path)
if 'vue_install' in deps_dict and deps_dict['vue_install']:
vue_install_apps.append(app_path)
if 'npm_rebuild' in deps_dict and deps_dict['npm_rebuild']:
npm_rebuild_apps.append(app_path)
print("\n[EAF] Installing dependencies for the selected applications")
if not args.ignore_sys_deps and sys.platform == "linux" and len(sys_deps) > 0:
print("[EAF] Installing system dependencies")
install_sys_deps(distro, sys_deps)
if not args.ignore_py_deps and len(py_deps) > 0:
print("[EAF] Installing python dependencies")
install_py_deps(py_deps)
if len(npm_global_deps) > 0:
install_npm_gloal_deps(npm_global_deps)
if not args.ignore_node_deps:
if args.force:
if len(npm_install_apps) > 0:
remove_node_modules_path(npm_install_apps)
if len(vue_install_apps) > 0:
remove_node_modules_path(vue_install_apps)
if len(npm_install_apps) > 0:
install_npm_install(npm_install_apps)
if len(npm_rebuild_apps) > 0:
install_npm_rebuild(npm_rebuild_apps)
if len(vue_install_apps) > 0:
install_vue_install(vue_install_apps)
print("\n[EAF] Please always ensure the following config are added to your init.el:")
print_sample_config(app_dir)
global install_failed_sys
global install_failed_pys
global install_failed_npm_globals
global install_failed_apps
if len(install_failed_sys) > 0:
install_failed_sys = list(set(install_failed_sys))
print_warning_message("\n[EAF] Installation FAILED for the following system dependencies:")
for dep in install_failed_sys:
print_warning_message(dep)
if len(install_failed_pys) > 0:
install_failed_pys = list(set(install_failed_pys))
print_warning_message("\n[EAF] Installation FAILED for the following Python dependencies:")
for dep in install_failed_pys:
print_warning_message(dep)
if len(install_failed_npm_globals) > 0:
install_failed_npm_globals = list(set(install_failed_npm_globals))
print_warning_message("\n[EAF] Installation FAILED for the following NPM dependencies:")
for dep in install_failed_npm_globals:
print_warning_message(dep)
if len(install_failed_apps) > 0:
install_failed_apps = list(set(install_failed_apps))
print_warning_message("\n[EAF] Installation FAILED for following applications:")
for app in install_failed_apps:
print_warning_message(app)
if len(install_failed_sys) + len(install_failed_pys) + len(install_failed_apps) == 0:
print("[EAF] Installation SUCCESS!")
else:
print("[EAF] Please rerun ./install-eaf.py with `--force`, or install them manually!")
def print_warning_message(message):
print(bcolors.WARNING + message + bcolors.ENDC)
def main():
try:
distro = get_distro()
with open(os.path.join(script_path, 'dependencies.json')) as f:
deps_dict = json.load(f)
if (not args.ignore_core_deps and len(args.install) == 0) or args.install_core_deps:
print("[EAF] ------------------------------------------")
install_core_deps(distro, deps_dict)
print("[EAF] ------------------------------------------")
if not args.install_core_deps:
print("[EAF] ------------------------------------------")
install_app_deps(distro, deps_dict)
print("[EAF] ------------------------------------------")
current_desktop = os.getenv("XDG_CURRENT_DESKTOP") or os.getenv("XDG_SESSION_DESKTOP")
if current_desktop in ["Hyprland", "sway"]:
print("[EAF] Compiling reinput")
subprocess.Popen("gcc reinput/main.c -o reinput/reinput `pkg-config --cflags --libs libinput libevdev libudev`",
shell=True)
print("[EAF] install-eaf.py finished.\n")
for msg in important_messages:
print_warning_message(msg)
except KeyboardInterrupt:
print("[EAF] install-eaf.py aborted!")
sys.exit()
if __name__ == '__main__':
main()