1''' 2PyMOL Plugins Engine, Installation Routines 3 4(c) 2011-2012 Thomas Holder, PyMOL OS Fellow 5License: BSD-2-Clause 6 7''' 8 9from __future__ import print_function 10 11import os 12 13# supported file types for installation. Do not support pyc and pyo binaries, 14# we want text files that can be parsed for metadata. 15zip_extensions = ['zip', 'tar.gz'] 16supported_extensions = ['py'] + zip_extensions 17 18class InstallationCancelled(Exception): 19 pass 20 21class BadInstallationFile(Exception): 22 pass 23 24def get_default_user_plugin_path(): 25 ''' 26 User plugin directory defaults to ~/.pymol/startup on Linux and to 27 %APPDATA%\pymol\startup on windows. 28 ''' 29 if 'APPDATA' in os.environ: 30 return os.path.join(os.environ['APPDATA'], 'pymol', 'startup') 31 return os.path.expanduser('~/.pymol/startup') 32 33def is_writable(dirname): 34 ''' 35 Return True if directory is writable. 36 ''' 37 path = os.path.join(dirname, '__check_writable') 38 try: 39 f = open(path, 'wb') 40 f.close() 41 os.remove(path) 42 return True 43 except (IOError, OSError): 44 return False 45 46def cmp_version(v1, v2): 47 ''' 48 Compares two version strings. An empty version string is always considered 49 smaller than a non-empty version string. 50 51 Uses distutils.version.StrictVersion to evaluate non-empty version strings. 52 ''' 53 if v1 == v2: 54 return 0 55 if v1 == '': 56 return -1 57 if v2 == '': 58 return 1 59 try: 60 from distutils.version import StrictVersion as Version 61 return cmp(Version(v1), Version(v2)) 62 except: 63 print(' Warning: Version parsing failed for', v1, 'and/or', v2) 64 return 0 65 66def get_name_and_ext(ofile): 67 ''' 68 Given a filename, return module name and file extension. 69 70 Examples: 71 foo-1.0.py -> ('foo', 'py') 72 /foo/bar.tar.gz -> ('bar', 'tar.gz') 73 ''' 74 import re 75 76 basename = os.path.basename(ofile) 77 pattern = r'(\w+).*\.(%s)$' % '|'.join(supported_extensions) 78 m = re.match(pattern, basename, re.IGNORECASE) 79 80 if m is None: 81 raise BadInstallationFile('Not a valid plugin filename (%s).' % (basename)) 82 83 return m.group(1), m.group(2).lower() 84 85def check_valid_name(name): 86 ''' 87 Check if "name" is a valid python module name. 88 ''' 89 if '.' in name: 90 raise BadInstallationFile('name must not contain dots (%s).' % repr(name)) 91 92def extract_zipfile(ofile, ext): 93 ''' 94 Extract zip file to temporary directory 95 ''' 96 if ext == 'zip': 97 import zipfile 98 zf = zipfile.ZipFile(ofile) 99 else: 100 import tarfile 101 zf = tarfile.open(ofile) 102 zf.namelist = zf.getnames 103 # make sure pathnames are not absolute 104 cwd = os.getcwd() 105 namelist = zf.namelist() 106 for f in namelist: 107 f = os.path.normpath(f) 108 if not os.path.abspath(f).startswith(cwd): 109 raise BadInstallationFile('ZIP file contains absolute path names') 110 # analyse structure 111 namedict = dict() 112 for f in namelist: 113 x = namedict 114 for part in f.split('/'): # even on windows this is a forward slash (not os.sep) 115 if part != '': 116 x = x.setdefault(part, {}) 117 if len(namedict) == 0: 118 raise BadInstallationFile('Archive empty.') 119 120 # case 1: zip/<name>/__init__.py 121 names = [(name,) 122 for name in namedict 123 if '__init__.py' in namedict[name]] 124 if len(names) == 0: 125 # case 2: zip/<name>-<version>/<name>/__init__.py 126 names = [(pname, name) 127 for (pname, pdict) in namedict.items() 128 for name in pdict 129 if '__init__.py' in pdict[name]] 130 131 if len(names) == 0: 132 raise BadInstallationFile('Missing __init__.py') 133 if len(names) > 1: 134 # filter out "tests" directory 135 names = [n for n in names if n[-1] != 'tests'] 136 if len(names) > 1: 137 raise BadInstallationFile('Archive must contain a single package.') 138 check_valid_name(names[0][-1]) 139 140 # extract 141 import tempfile 142 tempdir = tempfile.mkdtemp() 143 zf.extractall(tempdir) 144 145 return tempdir, names[0] 146 147def get_plugdir(parent=None): 148 ''' 149 Get plugin directory, ask user if startup path has more than one entry 150 ''' 151 from . import get_startup_path 152 plugdirs = get_startup_path() 153 154 if len(plugdirs) == 1: 155 return plugdirs[0] 156 157 import sys 158 if 'pmg_qt.mimic_tk' in sys.modules: 159 from pymol.Qt import QtWidgets 160 value, result = QtWidgets.QInputDialog.getItem(None, 161 'Select plugin directory', 162 'In which directory should the plugin be installed?', plugdirs) 163 return value if result else '' 164 165 dialog_selection = [] 166 def plugdir_callback(result): 167 if result == 'OK': 168 dialog_selection[:] = dialog.getcurselection() 169 dialog.destroy() 170 171 import Pmw 172 dialog = Pmw.SelectionDialog(parent, title='Select plugin directory', 173 buttons = ('OK', 'Cancel'), defaultbutton='OK', 174 scrolledlist_labelpos='n', 175 label_text='In which directory should the plugin be installed?', 176 scrolledlist_items=plugdirs, 177 command=plugdir_callback) 178 dialog.component('scrolledlist').selection_set(0) 179 180 # wait for dialog to be closed 181 dialog.wait_window() 182 183 if not dialog_selection: 184 return '' 185 186 return dialog_selection[0] 187 188def installPluginFromFile(ofile, parent=None, plugdir=None): 189 ''' 190 Install plugin from file. 191 192 Takes python (.py) files and archives which contain a python module. 193 ''' 194 import shutil 195 from . import startup, PluginInfo 196 from . import get_startup_path, set_startup_path, pref_get 197 from .legacysupport import tkMessageBox, get_tk_focused 198 199 if parent is None: 200 parent = get_tk_focused() 201 202 showinfo = tkMessageBox.showinfo 203 askyesno = tkMessageBox.askyesno 204 205 plugdirs = get_startup_path() 206 207 if not plugdir: 208 plugdir = get_plugdir() 209 if not plugdir: 210 return 211 212 if not is_writable(plugdir): 213 user_plugdir = get_default_user_plugin_path() 214 if not askyesno('Warning', 215 'Unable to write to the plugin directory.\n' 216 'Should a user plugin directory be created at\n' + user_plugdir + '?', 217 parent=parent): 218 showinfo('Error', 'Installation aborted', parent=parent) 219 return 220 221 if not os.path.exists(user_plugdir): 222 try: 223 os.makedirs(user_plugdir) 224 except OSError: 225 showinfo('Error', 'Could not create user plugin directory', parent=parent) 226 return 227 228 plugdir = user_plugdir 229 230 if plugdir not in plugdirs: 231 set_startup_path([plugdir] + get_startup_path(True)) 232 233 def remove_if_exists(pathname, ask): 234 ''' 235 Remove existing plugin files before reinstallation. Will not remove 236 files if installing into different startup directory. 237 ''' 238 if not os.path.exists(pathname): 239 return 240 241 is_dir = os.path.isdir(pathname) 242 243 if ask: 244 if is_dir: 245 msg = 'Directory "%s" already exists, overwrite?' % pathname 246 else: 247 msg = 'File "%s" already exists, overwrite?' % pathname 248 if not tkMessageBox.askyesno('Confirm', msg, parent=parent): 249 raise InstallationCancelled('will not overwrite "%s"' % pathname) 250 251 if is_dir: 252 shutil.rmtree(pathname) 253 else: 254 os.remove(pathname) 255 256 def check_reinstall(name, pathname): 257 from . import plugins 258 259 if name not in plugins: 260 remove_if_exists(pathname, True) 261 return 262 263 v_installed = plugins[name].get_version() 264 v_new = PluginInfo(name, ofile).get_version() 265 c = cmp_version(v_new, v_installed) 266 if c > 0: 267 msg = 'An older version (%s) of this plugin is already installed. Install version %s now?' % (v_installed, v_new) 268 elif c == 0: 269 msg = 'Plugin already installed. Reinstall?' 270 else: 271 msg = 'A newer version (%s) of this plugin is already installed. Install anyway?' % (v_installed) 272 273 if not tkMessageBox.askokcancel('Confirm', msg, parent=parent): 274 raise InstallationCancelled 275 276 remove_if_exists(pathname, False) 277 278 name = "unknown" # fallback for error message 279 280 temppathnames = [] 281 try: 282 name, ext = get_name_and_ext(ofile) 283 284 if ext in zip_extensions: 285 # import archive 286 287 tempdir, dirnames = extract_zipfile(ofile, ext) 288 temppathnames.append((tempdir, 1)) 289 290 # install 291 name = dirnames[-1] 292 odir = os.path.join(tempdir, *dirnames) 293 ofile = os.path.join(odir, '__init__.py') 294 mod_dir = os.path.join(plugdir, name) 295 check_reinstall(name, mod_dir) 296 check_valid_name(name) 297 shutil.copytree(odir, mod_dir) 298 299 mod_file = os.path.join(mod_dir, '__init__.py') 300 301 elif name == '__init__': 302 # import directory 303 odir = os.path.dirname(ofile) 304 name = os.path.basename(odir) 305 mod_dir = os.path.join(plugdir, name) 306 check_reinstall(name, mod_dir) 307 check_valid_name(name) 308 shutil.copytree(odir, mod_dir) 309 310 mod_file = os.path.join(mod_dir, '__init__.py') 311 312 elif ext == 'py': 313 # import python file 314 mod_file = os.path.join(plugdir, name + '.py') 315 check_reinstall(name, mod_file) 316 check_valid_name(name) 317 shutil.copy(ofile, mod_file) 318 319 else: 320 raise UserWarning('this should never happen') 321 322 except InstallationCancelled: 323 showinfo('Info', 'Installation cancelled', parent=parent) 324 return 325 326 except Exception as e: 327 if pref_get('verbose', False): 328 import traceback 329 traceback.print_exc() 330 msg = 'Unable to install plugin "{}".\n{}'.format(name, e) 331 showinfo('Error', msg, parent=parent) 332 return 333 334 finally: 335 for (pathname, is_dir) in temppathnames: 336 if is_dir: 337 shutil.rmtree(pathname) 338 else: 339 os.remove(pathname) 340 341 prefix = startup.__name__ 342 info = PluginInfo(name, mod_file, prefix + '.' + name) 343 344 if info.load(force=1): 345 showinfo('Success', 'Plugin "%s" has been installed.' % name, parent=parent) 346 else: 347 showinfo('Error', 'Plugin "%s" has been installed but initialization failed.' % name, parent=parent) 348 349 if info.get_citation_required(): 350 if askyesno('Citation Required', 'This plugin requires citation. Show information now?' 351 '\n\n(You can always get this information from the Plugin Manager, click the "Info" button there)', 352 parent=parent): 353 from .managergui import plugin_info_dialog 354 plugin_info_dialog(parent, info) 355 356# vi:expandtab:smarttab:sw=4 357