scripts/install-server.py

branch
server
changeset 10781
0e3d6e22efaf
child 10801
5859861e7a1f
equal deleted inserted replaced
10780:07670febf8c5 10781:0e3d6e22efaf
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

eric ide

mercurial