|
1 #!/usr/bin/env python3 |
|
2 # -*- coding: utf-8 -*- |
|
3 |
|
4 # Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> |
|
5 # |
|
6 # This is the install script for the eric-ide server. It may be used |
|
7 # to just install the server for remote editing and debugging. |
|
8 # |
|
9 |
|
10 """ |
|
11 Installation script for the eric-ide server. |
|
12 """ |
|
13 |
|
14 import argparse |
|
15 import compileall |
|
16 import contextlib |
|
17 import fnmatch |
|
18 import importlib |
|
19 import io |
|
20 import json |
|
21 import os |
|
22 import re |
|
23 import shutil |
|
24 import subprocess |
|
25 import sys |
|
26 import sysconfig |
|
27 |
|
28 # Define the globals. |
|
29 currDir = os.getcwd() |
|
30 scriptsDir = None |
|
31 modDir = None |
|
32 pyModDir = None |
|
33 distDir = None |
|
34 installPackage = "eric7" |
|
35 doCleanup = True |
|
36 doCompile = True |
|
37 doDepChecks = True |
|
38 sourceDir = "eric" |
|
39 eric7SourceDir = "" |
|
40 |
|
41 |
|
42 def exit(rcode=0): |
|
43 """ |
|
44 Exit the install script. |
|
45 |
|
46 @param rcode result code to report back |
|
47 @type int |
|
48 """ |
|
49 global currDir |
|
50 |
|
51 if sys.platform.startswith("win"): |
|
52 with contextlib.suppress(EOFError): |
|
53 input("Press enter to continue...") # secok |
|
54 |
|
55 os.chdir(currDir) |
|
56 |
|
57 sys.exit(rcode) |
|
58 |
|
59 |
|
60 def initGlobals(): |
|
61 """ |
|
62 Module function to set the values of globals that need more than a |
|
63 simple assignment. |
|
64 """ |
|
65 global modDir, pyModDir, scriptsDir |
|
66 |
|
67 # determine the platform scheme |
|
68 if sys.platform.startswith(("win", "cygwin")): |
|
69 scheme = "nt_user" |
|
70 elif sys.platform == "darwin": |
|
71 scheme = "osx_framework_user" |
|
72 else: |
|
73 scheme = "posix_user" |
|
74 |
|
75 # determine modules directory |
|
76 modDir = sysconfig.get_path("platlib") |
|
77 if not os.access(modDir, os.W_OK): |
|
78 # can't write to the standard path, use the 'user' path instead |
|
79 modDir = sysconfig.get_path("platlib", scheme) |
|
80 pyModDir = modDir |
|
81 |
|
82 # determine the scripts directory |
|
83 scriptsDir = sysconfig.get_path("scripts") |
|
84 if not os.access(scriptsDir, os.W_OK): |
|
85 # can't write to the standard path, use the 'user' path instead |
|
86 scriptsDir = sysconfig.get_path("scripts", scheme) |
|
87 |
|
88 |
|
89 def copyToFile(name, text): |
|
90 """ |
|
91 Copy a string to a file. |
|
92 |
|
93 @param name name of the file |
|
94 @type str |
|
95 @param text contents to copy to the file |
|
96 @type str |
|
97 """ |
|
98 with open(name, "w") as f: |
|
99 f.write(text) |
|
100 |
|
101 |
|
102 def copyTree(src, dst, filters, excludeDirs=None, excludePatterns=None): |
|
103 """ |
|
104 Copy files of a directory tree. |
|
105 |
|
106 @param src name of the source directory |
|
107 @type str |
|
108 @param dst name of the destination directory |
|
109 @type str |
|
110 @param filters list of filter pattern determining the files to be copied |
|
111 @type list of str |
|
112 @param excludeDirs list of (sub)directories to exclude from copying |
|
113 @type list of str |
|
114 @param excludePatterns list of filter pattern determining the files to |
|
115 be skipped |
|
116 @type str |
|
117 """ |
|
118 if excludeDirs is None: |
|
119 excludeDirs = [] |
|
120 if excludePatterns is None: |
|
121 excludePatterns = [] |
|
122 try: |
|
123 names = os.listdir(src) |
|
124 except OSError: |
|
125 # ignore missing directories |
|
126 return |
|
127 |
|
128 for name in names: |
|
129 skipIt = False |
|
130 for excludePattern in excludePatterns: |
|
131 if fnmatch.fnmatch(name, excludePattern): |
|
132 skipIt = True |
|
133 break |
|
134 if not skipIt: |
|
135 srcname = os.path.join(src, name) |
|
136 dstname = os.path.join(dst, name) |
|
137 for fileFilter in filters: |
|
138 if fnmatch.fnmatch(srcname, fileFilter): |
|
139 if not os.path.isdir(dst): |
|
140 os.makedirs(dst) |
|
141 shutil.copy2(srcname, dstname) |
|
142 os.chmod(dstname, 0o644) |
|
143 break |
|
144 else: |
|
145 if os.path.isdir(srcname) and srcname not in excludeDirs: |
|
146 copyTree(srcname, dstname, filters, excludePatterns=excludePatterns) |
|
147 |
|
148 |
|
149 def cleanupSource(dirName): |
|
150 """ |
|
151 Cleanup the sources directory to get rid of leftover files |
|
152 and directories. |
|
153 |
|
154 @param dirName name of the directory to prune |
|
155 @type str |
|
156 """ |
|
157 # step 1: delete the __pycache__ directory and all *.pyc files |
|
158 if os.path.exists(os.path.join(dirName, "__pycache__")): |
|
159 shutil.rmtree(os.path.join(dirName, "__pycache__")) |
|
160 for name in [f for f in os.listdir(dirName) if fnmatch.fnmatch(f, "*.pyc")]: |
|
161 os.remove(os.path.join(dirName, name)) |
|
162 |
|
163 # step 2: descent into subdirectories and delete them if empty |
|
164 for name in os.listdir(dirName): |
|
165 name = os.path.join(dirName, name) |
|
166 if os.path.isdir(name): |
|
167 cleanupSource(name) |
|
168 if len(os.listdir(name)) == 0: |
|
169 os.rmdir(name) |
|
170 |
|
171 |
|
172 def cleanUp(): |
|
173 """ |
|
174 Uninstall the old eric files. |
|
175 """ |
|
176 global installPackage, pyModDir, scriptsDir |
|
177 |
|
178 try: |
|
179 # Cleanup the package directories |
|
180 dirname = os.path.join(pyModDir, installPackage) |
|
181 if os.path.exists(dirname): |
|
182 shutil.rmtree(dirname, ignore_errors=True) |
|
183 except OSError as msg: |
|
184 sys.stderr.write("Error: {0}\nTry install with admin rights.\n".format(msg)) |
|
185 exit(7) |
|
186 |
|
187 # Remove the wrapper scripts |
|
188 rem_wnames = ["eric7_server"] |
|
189 try: |
|
190 for rem_wname in rem_wnames: |
|
191 for rwname in wrapperNames(scriptsDir, rem_wname): |
|
192 if os.path.exists(rwname): |
|
193 os.remove(rwname) |
|
194 except OSError as msg: |
|
195 sys.stderr.write("Error: {0}\nTry install with admin rights.\n".format(msg)) |
|
196 exit(7) |
|
197 |
|
198 |
|
199 def wrapperNames(dname, wfile): |
|
200 """ |
|
201 Create the platform specific names for the wrapper script. |
|
202 |
|
203 @param dname name of the directory to place the wrapper into |
|
204 @type str |
|
205 @param wfile basename (without extension) of the wrapper script |
|
206 @type str |
|
207 @return list containing the names of the wrapper scripts |
|
208 @rtype list of str |
|
209 """ |
|
210 wnames = ( |
|
211 [dname + "\\" + wfile + ".cmd", dname + "\\" + wfile + ".bat"] |
|
212 if sys.platform.startswith(("win", "cygwin")) |
|
213 else [dname + "/" + wfile] |
|
214 ) |
|
215 |
|
216 return wnames |
|
217 |
|
218 |
|
219 def createPyWrapper(pydir, wfile, saveDir): |
|
220 """ |
|
221 Create an executable wrapper for a Python script. |
|
222 |
|
223 @param pydir name of the directory where the Python script will |
|
224 eventually be installed |
|
225 @type str |
|
226 @param wfile basename of the wrapper |
|
227 @type str |
|
228 @param saveDir directory to save the file into |
|
229 @type str |
|
230 @return the platform specific name of the wrapper |
|
231 @rtype str |
|
232 """ |
|
233 # all kinds of Windows systems |
|
234 if sys.platform.startswith(("win", "cygwin")): |
|
235 wname = wfile + ".cmd" |
|
236 wrapper = """@"{0}" "{1}\\{2}.py" %1 %2 %3 %4 %5 %6 %7 %8 %9\n""".format( |
|
237 sys.executable, pydir, wfile |
|
238 ) |
|
239 |
|
240 # Mac OS X |
|
241 elif sys.platform == "darwin": |
|
242 major = sys.version_info.major |
|
243 pyexec = "{0}/bin/python{1}".format(sys.exec_prefix, major) |
|
244 wname = wfile |
|
245 wrapper = ( |
|
246 """#!/bin/sh\n""" |
|
247 """\n""" |
|
248 """exec "{0}" "{1}/{2}.py" "$@"\n""".format(pyexec, pydir, wfile) |
|
249 ) |
|
250 |
|
251 # *nix systems |
|
252 else: |
|
253 wname = wfile |
|
254 wrapper = ( |
|
255 """#!/bin/sh\n""" |
|
256 """\n""" |
|
257 """exec "{0}" "{1}/{2}.py" "$@"\n""".format(sys.executable, pydir, wfile) |
|
258 ) |
|
259 |
|
260 wname = os.path.join(saveDir, wname) |
|
261 copyToFile(wname, wrapper) |
|
262 os.chmod(wname, 0o755) # secok |
|
263 |
|
264 return wname |
|
265 |
|
266 |
|
267 def shutilCopy(src, dst, perm=0o644): |
|
268 """ |
|
269 Wrapper function around shutil.copy() to ensure the permissions. |
|
270 |
|
271 @param src source file name |
|
272 @type str |
|
273 @param dst destination file name or directory name |
|
274 @type str |
|
275 @param perm permissions to be set |
|
276 @type int |
|
277 """ |
|
278 shutil.copy(src, dst) |
|
279 if os.path.isdir(dst): |
|
280 dst = os.path.join(dst, os.path.basename(src)) |
|
281 os.chmod(dst, perm) |
|
282 |
|
283 |
|
284 def pipInstall(packageName, message, force=True): |
|
285 """ |
|
286 Install the given package via pip. |
|
287 |
|
288 @param packageName name of the package to be installed |
|
289 @type str |
|
290 @param message message to be shown to the user |
|
291 @type str |
|
292 @param force flag indicating to perform the installation |
|
293 without asking the user |
|
294 @type bool |
|
295 @return flag indicating a successful installation |
|
296 @rtype bool |
|
297 """ |
|
298 ok = False |
|
299 if force: |
|
300 answer = "y" |
|
301 else: |
|
302 print( |
|
303 "{0}\nShall '{1}' be installed using pip? (Y/n)".format( |
|
304 message, packageName |
|
305 ), |
|
306 end=" ", |
|
307 ) |
|
308 answer = input() # secok |
|
309 if answer in ("", "Y", "y"): |
|
310 exitCode = subprocess.run( # secok |
|
311 [ |
|
312 sys.executable, |
|
313 "-m", |
|
314 "pip", |
|
315 "install", |
|
316 "--prefer-binary", |
|
317 "--upgrade", |
|
318 packageName, |
|
319 ] |
|
320 ).returncode |
|
321 ok = exitCode == 0 |
|
322 |
|
323 return ok |
|
324 |
|
325 |
|
326 def isPipOutdated(): |
|
327 """ |
|
328 Check, if pip is outdated. |
|
329 |
|
330 @return flag indicating an outdated pip |
|
331 @rtype bool |
|
332 """ |
|
333 try: |
|
334 pipOut = ( |
|
335 subprocess.run( # secok |
|
336 [sys.executable, "-m", "pip", "list", "--outdated", "--format=json"], |
|
337 check=True, |
|
338 capture_output=True, |
|
339 text=True, |
|
340 ) |
|
341 .stdout.strip() |
|
342 .splitlines()[0] |
|
343 ) |
|
344 # only the first line contains the JSON data |
|
345 except (OSError, subprocess.CalledProcessError): |
|
346 pipOut = "[]" # default empty list |
|
347 try: |
|
348 jsonList = json.loads(pipOut) |
|
349 except Exception: |
|
350 jsonList = [] |
|
351 for package in jsonList: |
|
352 if isinstance(package, dict) and package["name"] == "pip": |
|
353 print( |
|
354 "'pip' is outdated (installed {0}, available {1})".format( |
|
355 package["version"], package["latest_version"] |
|
356 ) |
|
357 ) |
|
358 return True |
|
359 |
|
360 return False |
|
361 |
|
362 |
|
363 def updatePip(): |
|
364 """ |
|
365 Update the installed pip package. |
|
366 """ |
|
367 global yes2All |
|
368 |
|
369 if yes2All: |
|
370 answer = "y" |
|
371 else: |
|
372 print("Shall 'pip' be updated (recommended)? (Y/n)", end=" ") |
|
373 answer = input() # secok |
|
374 if answer in ("", "Y", "y"): |
|
375 subprocess.run( # secok |
|
376 [sys.executable, "-m", "pip", "install", "--upgrade", "pip"] |
|
377 ) |
|
378 |
|
379 |
|
380 def doDependancyChecks(): |
|
381 """ |
|
382 Perform some dependency checks. |
|
383 """ |
|
384 try: |
|
385 isSudo = os.getuid() == 0 and sys.platform != "darwin" |
|
386 # disregard sudo installs on macOS |
|
387 except AttributeError: |
|
388 isSudo = False |
|
389 |
|
390 print("Checking dependencies") |
|
391 |
|
392 # update pip first even if we don't need to install anything |
|
393 if not isSudo and isPipOutdated(): |
|
394 updatePip() |
|
395 print("\n") |
|
396 |
|
397 # perform dependency checks |
|
398 if sys.version_info < (3, 8, 0) or sys.version_info >= (3, 13, 0): |
|
399 print("Sorry, you must have Python 3.8.0 or higher, but less 3.13.0.") |
|
400 print("Yours is {0}.".format(".".join(str(v) for v in sys.version_info[:3]))) |
|
401 exit(5) |
|
402 |
|
403 requiredModulesList = { |
|
404 # key is pip project name |
|
405 # value is tuple of package name, pip install constraint |
|
406 "coverage": ("coverage", ">=6.5.0"), |
|
407 "EditorConfig": ("editorconfig", ""), |
|
408 } |
|
409 |
|
410 # check required modules |
|
411 print("Required Packages") |
|
412 print("-----------------") |
|
413 requiredMissing = False |
|
414 for requiredPackage in sorted(requiredModulesList): |
|
415 try: |
|
416 importlib.import_module(requiredModulesList[requiredPackage][0]) |
|
417 print("Found", requiredPackage) |
|
418 except ImportError as err: |
|
419 if isSudo: |
|
420 print("Required '{0}' could not be detected.".format(requiredPackage)) |
|
421 requiredMissing = True |
|
422 else: |
|
423 pipInstall( |
|
424 requiredPackage + requiredModulesList[requiredPackage][1], |
|
425 "Required '{0}' could not be detected.{1}".format( |
|
426 requiredPackage, "\nError: {0}".format(err) |
|
427 ), |
|
428 force=True, |
|
429 ) |
|
430 if requiredMissing: |
|
431 print("Some required packages are missing and could not be installed.") |
|
432 print("Install them manually.") |
|
433 |
|
434 print() |
|
435 print("All dependencies ok.") |
|
436 print() |
|
437 |
|
438 |
|
439 def installEricServer(): |
|
440 """ |
|
441 Actually perform the installation steps. |
|
442 |
|
443 @return result code |
|
444 @rtype int |
|
445 """ |
|
446 global distDir, sourceDir, modDir, scriptsDir |
|
447 |
|
448 # set install prefix, if not None |
|
449 targetDir = ( |
|
450 os.path.normpath(os.path.join(distDir, installPackage)) |
|
451 if distDir |
|
452 else os.path.join(modDir, installPackage) |
|
453 ) |
|
454 if not os.path.isdir(targetDir): |
|
455 os.makedirs(targetDir) |
|
456 |
|
457 # Create the platform specific wrapper. |
|
458 tmpScriptsDir = "install_scripts" |
|
459 if not os.path.isdir(tmpScriptsDir): |
|
460 os.mkdir(tmpScriptsDir) |
|
461 wrapper = createPyWrapper(targetDir, "eric7_server", tmpScriptsDir) |
|
462 |
|
463 try: |
|
464 # Install the files |
|
465 # copy the various parts of eric-ide server |
|
466 for package in ("DebugClients", "RemoteServer"): |
|
467 copyTree( |
|
468 os.path.join(eric7SourceDir, package), |
|
469 os.path.join(targetDir, package), |
|
470 ["*.py", "*.pyc", "*.pyo", "*.pyw"], |
|
471 ) |
|
472 # copy the needed parts of SystemUtilities |
|
473 os.makedirs(os.path.join(targetDir, "SystemUtilities")) |
|
474 for module in ("__init__.py", "FileSystemUtilities.py", "OSUtilities.py"): |
|
475 shutilCopy( |
|
476 os.path.join(eric7SourceDir, "SystemUtilities", module), |
|
477 os.path.join(targetDir, "SystemUtilities"), |
|
478 ) |
|
479 |
|
480 # copy the top level files |
|
481 for module in ("__init__.py", "__version__.py", "eric7_server.py"): |
|
482 shutilCopy(os.path.join(eric7SourceDir, module), targetDir) |
|
483 |
|
484 # copy the license and README files |
|
485 for infoFile in ("LICENSE.txt", "README-server.md"): |
|
486 shutilCopy(os.path.join(sourceDir, "docs", infoFile), targetDir) |
|
487 |
|
488 # copy the wrapper |
|
489 shutilCopy(wrapper, scriptsDir, perm=0o755) |
|
490 shutil.rmtree(tmpScriptsDir) |
|
491 |
|
492 except OSError as msg: |
|
493 sys.stderr.write("\nError: {0}\nTry install with admin rights.\n".format(msg)) |
|
494 return 7 |
|
495 |
|
496 return 0 |
|
497 |
|
498 |
|
499 def createArgumentParser(): |
|
500 """ |
|
501 Function to create an argument parser. |
|
502 |
|
503 @return created argument parser object |
|
504 @rtype argparse.ArgumentParser |
|
505 """ |
|
506 parser = argparse.ArgumentParser( |
|
507 description="Install eric-ide server from the source code tree." |
|
508 ) |
|
509 |
|
510 parser.add_argument( |
|
511 "-d", |
|
512 metavar="dir", |
|
513 default=modDir, |
|
514 help="directory where eric-ide server files will be installed" |
|
515 " (default: {0})".format(modDir), |
|
516 ) |
|
517 parser.add_argument( |
|
518 "-b", |
|
519 metavar="dir", |
|
520 default=scriptsDir, |
|
521 help="directory where the binaries will be installed (default: {0})".format( |
|
522 scriptsDir |
|
523 ), |
|
524 ) |
|
525 if not sys.platform.startswith(("win", "cygwin")): |
|
526 parser.add_argument( |
|
527 "-i", |
|
528 metavar="dir", |
|
529 default=distDir, |
|
530 help="temporary install prefix (default: {0})".format(distDir), |
|
531 ) |
|
532 parser.add_argument( |
|
533 "-c", |
|
534 action="store_false", |
|
535 help="don't cleanup old installation first", |
|
536 ) |
|
537 parser.add_argument( |
|
538 "-z", |
|
539 action="store_false", |
|
540 help="don't compile the installed Python files", |
|
541 ) |
|
542 parser.add_argument( |
|
543 "-x", |
|
544 action="store_false", |
|
545 help="don't perform dependency checks (use on your own risk)", |
|
546 ) |
|
547 |
|
548 return parser |
|
549 |
|
550 |
|
551 def main(argv): |
|
552 """ |
|
553 The main function of the script. |
|
554 |
|
555 @param argv the list of command line arguments |
|
556 @type list of str |
|
557 """ |
|
558 global modDir, doCleanup, doCompile, doDepChecks, distDir, scriptsDir |
|
559 global sourceDir, eric7SourceDir |
|
560 |
|
561 if sys.version_info < (3, 8, 0) or sys.version_info >= (4, 0, 0): |
|
562 print("Sorry, the eric debugger requires Python 3.8 or better for running.") |
|
563 exit(5) |
|
564 |
|
565 if os.path.dirname(argv[0]): |
|
566 os.chdir(os.path.dirname(argv[0])) |
|
567 |
|
568 initGlobals() |
|
569 |
|
570 parser = createArgumentParser() |
|
571 args = parser.parse_args() |
|
572 |
|
573 modDir = args.d |
|
574 scriptsDir = args.b |
|
575 doDepChecks = args.x |
|
576 doCleanup = args.c |
|
577 doCompile = args.z |
|
578 if not sys.platform.startswith(("win", "cygwin")) and args.i: |
|
579 distDir = os.path.normpath(args.i) |
|
580 |
|
581 # check dependencies |
|
582 if doDepChecks: |
|
583 doDependancyChecks() |
|
584 |
|
585 installFromSource = not os.path.isdir(sourceDir) |
|
586 if installFromSource: |
|
587 sourceDir = os.path.abspath("..") |
|
588 |
|
589 eric7SourceDir = ( |
|
590 os.path.join(sourceDir, "eric7") |
|
591 if os.path.exists(os.path.join(sourceDir, "eric7")) |
|
592 else os.path.join(sourceDir, "src", "eric7") |
|
593 ) |
|
594 |
|
595 # cleanup source if installing from source |
|
596 if installFromSource: |
|
597 print("Cleaning up source ...", end="", flush=True) |
|
598 cleanupSource(os.path.join(eric7SourceDir, "DebugClients")) |
|
599 print(" Done") |
|
600 |
|
601 # cleanup old installation |
|
602 try: |
|
603 if doCleanup: |
|
604 print("Cleaning up old installation ...", end="", flush=True) |
|
605 if distDir: |
|
606 shutil.rmtree(distDir, ignore_errors=True) |
|
607 else: |
|
608 cleanUp() |
|
609 print(" Done") |
|
610 except OSError as msg: |
|
611 sys.stderr.write("Error: {0}\nTry install as root.\n".format(msg)) |
|
612 exit(7) |
|
613 |
|
614 if doCompile: |
|
615 print("Compiling source files ...", end="", flush=True) |
|
616 skipRe = re.compile(r"DebugClients[\\/]Python[\\/]") |
|
617 sys.stdout = io.StringIO() |
|
618 if distDir: |
|
619 compileall.compile_dir( |
|
620 os.path.join(eric7SourceDir, "DebugClients"), |
|
621 ddir=os.path.join(distDir, modDir, installPackage), |
|
622 rx=skipRe, |
|
623 quiet=True, |
|
624 ) |
|
625 else: |
|
626 compileall.compile_dir( |
|
627 os.path.join(eric7SourceDir, "DebugClients"), |
|
628 ddir=os.path.join(modDir, installPackage), |
|
629 rx=skipRe, |
|
630 quiet=True, |
|
631 ) |
|
632 sys.stdout = sys.__stdout__ |
|
633 print(" Done") |
|
634 |
|
635 print("Installing eric-ide server ...", end="", flush=True) |
|
636 res = installEricServer() |
|
637 print(" Done") |
|
638 |
|
639 print("Installation complete.") |
|
640 print() |
|
641 |
|
642 exit(res) |
|
643 |
|
644 |
|
645 if __name__ == "__main__": |
|
646 try: |
|
647 main(sys.argv) |
|
648 except SystemExit: |
|
649 raise |
|
650 except Exception: |
|
651 print( |
|
652 """An internal error occured. Please report all the output""" |
|
653 """ of the program,\nincluding the following traceback, to""" |
|
654 """ eric-bugs@eric-ide.python-projects.org.\n""" |
|
655 ) |
|
656 raise |
|
657 |
|
658 # |
|
659 # eflag: noqa = M801 |