# -*- coding: utf-8 -*- # # Copyright © 2012 Pierre Raybaut # Licensed under the terms of the MIT License # (see winpython/__init__.py for details) """ WinPython Package Manager Created on Fri Aug 03 14:32:26 2012 """ from __future__ import print_function import os import os.path as osp import shutil import re import sys import subprocess # Local imports from winpython import utils from winpython.config import DATA_PATH from winpython.py3compat import configparser as cp # from former wppm separate script launcher from argparse import ArgumentParser from winpython import py3compat # import information reader # importlib_metadata before Python 3.8 try: from importlib import metadata as metadata # Python-3.8 metadata = metadata.metadata except: try: from importlib_metadata import metadata # = (3, 0) else 0 ) pip_list = [ line.split("@+@")[:2] for line in ("%s" % stdout)[ start_at: ].split("+!+") ] # there are only Packages installed with pip now # create pip package list wppm = [ Package( '%s-%s-py2.py3-none-any.whl' % (i[0].replace('-', '_').lower(), i[1]) , update=update) for i in pip_list ] except: pass return sorted( wppm, key=lambda tup: tup.name.lower() ) def find_package(self, name): """Find installed package""" for pack in self.get_installed_packages(): if normalize(pack.name) == normalize(name): return pack def uninstall_existing(self, package): """Uninstall existing package (or package name)""" if isinstance(package, str): pack = self.find_package(package) else: pack = self.find_package(package.name) if pack is not None: self.uninstall(pack) def patch_all_shebang( self, to_movable=True, max_exe_size=999999, targetdir="", ): """make all python launchers relatives""" import glob import os for ffname in glob.glob( r'%s\Scripts\*.exe' % self.target ): size = os.path.getsize(ffname) if size <= max_exe_size: utils.patch_shebang_line( ffname, to_movable=to_movable, targetdir=targetdir, ) for ffname in glob.glob( r'%s\Scripts\*.py' % self.target ): utils.patch_shebang_line_py( ffname, to_movable=to_movable, targetdir=targetdir, ) def install(self, package, install_options=None): """Install package in distribution""" assert package.is_compatible_with(self) # wheel addition if package.fname.endswith( ('.whl', '.tar.gz', '.zip') ): self.install_bdist_direct( package, install_options=install_options ) self.handle_specific_packages(package) # minimal post-install actions self.patch_standard_packages(package.name) def do_pip_action( self, actions=None, install_options=None ): """Do pip action in a distribution""" my_list = install_options if my_list is None: my_list = [] my_actions = actions if my_actions is None: my_actions = [] executing = osp.join( self.target, '..', 'scripts', 'env.bat' ) if osp.isfile(executing): complement = [ r'&&', 'cd', '/D', self.target, r'&&', osp.join(self.target, 'python.exe'), ] complement += ['-m', 'pip'] else: executing = osp.join(self.target, 'python.exe') complement = ['-m', 'pip'] try: fname = utils.do_script( this_script=None, python_exe=executing, architecture=self.architecture, verbose=self.verbose, install_options=complement + my_actions + my_list, ) except RuntimeError: if not self.verbose: print("Failed!") raise def patch_standard_packages( self, package_name='', to_movable=True ): """patch Winpython packages in need""" import filecmp # 'pywin32' minimal post-install (pywin32_postinstall.py do too much) if ( package_name.lower() == "pywin32" or package_name == '' ): origin = self.target + ( r"\Lib\site-packages\pywin32_system32" ) destin = self.target if osp.isdir(origin): for name in os.listdir(origin): here, there = ( osp.join(origin, name), osp.join(destin, name), ) if not os.path.exists( there ) or not filecmp.cmp(here, there): shutil.copyfile(here, there) # 'pip' to do movable launchers (around line 100) !!!! # rational: https://github.com/pypa/pip/issues/2328 if ( package_name.lower() == "pip" or package_name == '' ): # ensure pip will create movable launchers # sheb_mov1 = classic way up to WinPython 2016-01 # sheb_mov2 = tried way, but doesn't work for pip (at least) sheb_fix = " executable = get_executable()" sheb_mov1 = " executable = os.path.join(os.path.basename(get_executable()))" sheb_mov2 = " executable = os.path.join('..',os.path.basename(get_executable()))" if to_movable: utils.patch_sourcefile( self.target + r"\Lib\site-packages\pip\_vendor\distlib\scripts.py", sheb_fix, sheb_mov1, ) utils.patch_sourcefile( self.target + r"\Lib\site-packages\pip\_vendor\distlib\scripts.py", sheb_mov2, sheb_mov1, ) else: utils.patch_sourcefile( self.target + r"\Lib\site-packages\pip\_vendor\distlib\scripts.py", sheb_mov1, sheb_fix, ) utils.patch_sourcefile( self.target + r"\Lib\site-packages\pip\_vendor\distlib\scripts.py", sheb_mov2, sheb_fix, ) # ensure pip wheel will register relative PATH in 'RECORD' files # will be in standard pip 8.0.3 utils.patch_sourcefile( self.target + (r"\Lib\site-packages\pip\wheel.py"), " writer.writerow((f, h, l))", " writer.writerow((normpath(f, lib_dir), h, l))", ) # create movable launchers for previous package installations self.patch_all_shebang(to_movable=to_movable) if ( package_name.lower() == "spyder" or package_name == '' ): # spyder don't goes on internet without I ask utils.patch_sourcefile( self.target + ( r"\Lib\site-packages\spyderlib\config\main.py" ), "'check_updates_on_startup': True,", "'check_updates_on_startup': False,", ) utils.patch_sourcefile( self.target + ( r"\Lib\site-packages\spyder\config\main.py" ), "'check_updates_on_startup': True,", "'check_updates_on_startup': False,", ) # workaround bad installers if package_name.lower() == "numba": self.create_pybat(['numba', 'pycc']) else: self.create_pybat(package_name.lower()) def create_pybat( self, names='', contents=r"""@echo off ..\python "%~dpn0" %*""", ): """Create launcher batch script when missing""" scriptpy = osp.join( self.target, 'Scripts' ) # std Scripts of python if not list(names) == names: my_list = [ f for f in os.listdir(scriptpy) if '.' not in f and f.startswith(names) ] else: my_list = names for name in my_list: if osp.isdir(scriptpy) and osp.isfile( osp.join(scriptpy, name) ): if not osp.isfile( osp.join(scriptpy, name + '.exe') ) and not osp.isfile( osp.join(scriptpy, name + '.bat') ): fd = open( osp.join(scriptpy, name + '.bat'), 'w', ) fd.write(contents) fd.close() def handle_specific_packages(self, package): """Packages requiring additional configuration""" if package.name.lower() in ( 'pyqt4', 'pyqt5', 'pyside2', ): # Qt configuration file (where to find Qt) name = 'qt.conf' contents = """[Paths] Prefix = . Binaries = .""" self.create_file( package, name, osp.join( 'Lib', 'site-packages', package.name ), contents, ) self.create_file( package, name, '.', contents.replace( '.', './Lib/site-packages/%s' % package.name, ), ) # pyuic script if package.name.lower() == 'pyqt5': # see http://code.activestate.com/lists/python-list/666469/ tmp_string = r'''@echo off if "%WINPYDIR%"=="" call "%~dp0..\..\scripts\env.bat" "%WINPYDIR%\python.exe" -m PyQt5.uic.pyuic %1 %2 %3 %4 %5 %6 %7 %8 %9''' else: tmp_string = r'''@echo off if "%WINPYDIR%"=="" call "%~dp0..\..\scripts\env.bat" "%WINPYDIR%\python.exe" "%WINPYDIR%\Lib\site-packages\package.name\uic\pyuic.py" %1 %2 %3 %4 %5 %6 %7 %8 %9''' self.create_file( package, 'pyuic%s.bat' % package.name[-1], 'Scripts', tmp_string.replace( 'package.name', package.name ), ) # Adding missing __init__.py files (fixes Issue 8) uic_path = osp.join( 'Lib', 'site-packages', package.name, 'uic' ) for dirname in ('Loader', 'port_v2', 'port_v3'): self.create_file( package, '__init__.py', osp.join(uic_path, dirname), '', ) def _print(self, package, action): """Print package-related action text (e.g. 'Installing') indicating progress""" text = " ".join( [action, package.name, package.version] ) if self.verbose: utils.print_box(text) else: if self.indent: text = (' ' * 4) + text print(text + '...', end=" ") def _print_done(self): """Print OK at the end of a process""" if not self.verbose: print("OK") def uninstall(self, package): """Uninstall package from distribution""" self._print(package, "Uninstalling") if not package.name == 'pip': # trick to get true target (if not current) this_executable_path = self.target subprocess.call( [ this_executable_path + r'\python.exe', '-m', 'pip', 'uninstall', package.name, '-y', ], cwd=this_executable_path, ) # no more legacy, no package are installed by old non-pip means self._print_done() def install_bdist_direct( self, package, install_options=None ): """Install a package directly !""" self._print( package, "Installing %s" % package.fname.split(".")[-1], ) try: fname = utils.direct_pip_install( package.fname, python_exe=osp.join( self.target, 'python.exe' ), architecture=self.architecture, verbose=self.verbose, install_options=install_options, ) except RuntimeError: if not self.verbose: print("Failed!") raise package = Package(fname) self._print_done() def install_script(self, script, install_options=None): try: fname = utils.do_script( script, python_exe=osp.join( self.target, 'python.exe' ), architecture=self.architecture, verbose=self.verbose, install_options=install_options, ) except RuntimeError: if not self.verbose: print("Failed!") raise def main(test=False): if test: sbdir = osp.join( osp.dirname(__file__), os.pardir, os.pardir, os.pardir, 'sandbox', ) tmpdir = osp.join(sbdir, 'tobedeleted') # fname = osp.join(tmpdir, 'scipy-0.10.1.win-amd64-py2.7.exe') fname = osp.join( sbdir, 'VTK-5.10.0-Qt-4.7.4.win32-py2.7.exe' ) print(Package(fname)) sys.exit() target = osp.join( utils.BASE_DIR, 'build', 'winpython-2.7.3', 'python-2.7.3', ) fname = osp.join( utils.BASE_DIR, 'packages.src', 'docutils-0.9.1.tar.gz', ) dist = Distribution(target, verbose=True) pack = Package(fname) print(pack.description) # dist.install(pack) # dist.uninstall(pack) else: parser = ArgumentParser( description="WinPython Package Manager: install, " "uninstall or upgrade Python packages on a Windows " "Python distribution like WinPython." ) parser.add_argument( 'fname', metavar='package', type=str if py3compat.PY3 else unicode, help='path to a Python package', ) parser.add_argument( '-t', '--target', dest='target', default=sys.prefix, help='path to target Python distribution ' '(default: "%s")' % sys.prefix, ) parser.add_argument( '-i', '--install', dest='install', action='store_const', const=True, default=False, help='install package (this is the default action)', ) parser.add_argument( '-u', '--uninstall', dest='uninstall', action='store_const', const=True, default=False, help='uninstall package', ) args = parser.parse_args() if args.install and args.uninstall: raise RuntimeError( "Incompatible arguments: --install and --uninstall" ) if not args.install and not args.uninstall: args.install = True if not osp.isfile(args.fname) and args.install: raise IOError("File not found: %s" % args.fname) if utils.is_python_distribution(args.target): dist = Distribution(args.target) try: if args.uninstall: package = dist.find_package(args.fname) dist.uninstall(package) else: package = Package(args.fname) if ( args.install and package.is_compatible_with(dist) ): dist.install(package) else: raise RuntimeError( "Package is not compatible with Python " "%s %dbit" % ( dist.version, dist.architecture, ) ) except NotImplementedError: raise RuntimeError( "Package is not (yet) supported by WPPM" ) else: raise WindowsError( "Invalid Python distribution %s" % args.target ) if __name__ == '__main__': main()