# Copyright (c) 2008-2011 by Enthought, Inc. # Copyright (c) 2013-2017 Continuum Analytics, Inc. # All rights reserved. from __future__ import absolute_import, unicode_literals import ctypes import locale import logging import os import sys from os.path import exists, isdir, join from ..platforms.win_utils.knownfolders import dirs_src, folder_path from ..platforms.win_utils.winshortcut import create_shortcut from ..utils import DEFAULT_BASE_PREFIX from .utils import rm_empty_dir, rm_rf # This allows debugging installer issues using DebugView from Microsoft. OutputDebugString = ctypes.windll.kernel32.OutputDebugStringW OutputDebugString.argtypes = [ctypes.c_wchar_p] class DbgViewHandler(logging.Handler): def emit(self, record): OutputDebugString(self.format(record)) logger = logging.getLogger("menuinst_win32") logger.setLevel(logging.DEBUG) stream_handler = logging.StreamHandler() stream_handler.setLevel(logging.WARNING) dbgview = DbgViewHandler() dbgview.setLevel(logging.DEBUG) logger.addHandler(dbgview) logger.addHandler(stream_handler) def quoted(s): """ quotes a string if necessary. """ # strip any existing quotes s = s.strip(u'"') # don't add quotes for minus or leading space if s[0] in (u'-', u' '): return s if u' ' in s or u'/' in s: return u'"%s"' % s else: return s def ensure_pad(name, pad="_"): """ Examples: >>> ensure_pad('conda') '_conda_' """ if not name or name[0] == name[-1] == pad: return name else: return "%s%s%s" % (pad, name, pad) def to_unicode(var, codec=locale.getpreferredencoding()): if not codec: codec = "utf-8" if hasattr(var, "decode"): var = var.decode(codec) return var def to_bytes(var, codec=locale.getpreferredencoding()): if isinstance(var, bytes): return var if not codec: codec = "utf-8" if hasattr(var, "encode"): var = var.encode(codec) return var unicode_root_prefix = to_unicode(DEFAULT_BASE_PREFIX) if u'\\envs\\' in unicode_root_prefix: logger.warning('menuinst called from non-root env %s', unicode_root_prefix) def substitute_env_variables(text, dir): # When conda is using Menuinst, only the root conda installation ever # calls menuinst. Thus, these calls to sys refer to the root conda # installation, NOT the child environment py_major_ver = sys.version_info[0] py_bitness = 8 * tuple.__itemsize__ env_prefix = to_unicode(dir['prefix']) root_prefix = to_unicode(dir['root_prefix']) text = to_unicode(text) env_name = to_unicode(dir['env_name']) for a, b in ( (u'${PREFIX}', env_prefix), (u'${ROOT_PREFIX}', root_prefix), (u'${DISTRIBUTION_NAME}', os.path.split(root_prefix)[-1]), ( u'${PYTHON_SCRIPTS}', os.path.normpath(join(env_prefix, u'Scripts')).replace(u"\\", u"/"), ), (u'${MENU_DIR}', join(env_prefix, u'Menu')), (u'${PERSONALDIR}', dir['documents']), (u'${USERPROFILE}', dir['profile']), (u'${ENV_NAME}', env_name), (u'${PY_VER}', u'%d' % (py_major_ver)), (u'${PLATFORM}', u"(%s-bit)" % py_bitness), ): if b: text = text.replace(a, b) return text class Menu(object): def __init__( self, name, prefix=unicode_root_prefix, env_name=u"", mode=None, root_prefix=unicode_root_prefix, ): """ Prefix is the system prefix to be used -- this is needed since there is the possibility of a different Python's packages being managed. """ # bytestrings passed in need to become unicode self.prefix = to_unicode(prefix) self.root_prefix = to_unicode(root_prefix) used_mode = ( mode if mode else ('user' if exists(join(self.prefix, u'.nonadmin')) else 'system') ) logger.debug( "Menu: name: '%s', prefix: '%s', env_name: '%s', mode: '%s', used_mode: '%s', root_prefix: '%s'" # noqa % (name, self.prefix, env_name, mode, used_mode, root_prefix) ) try: self.set_dir(name, self.prefix, env_name, used_mode, root_prefix) except WindowsError: # We get here if we aren't elevated. This is different from # permissions: a user can have permission, but elevation is still # required. If the process isn't elevated, we get the # WindowsError if 'user' in dirs_src and used_mode == 'system': logger.warn( "Insufficient permissions to write menu folder. " "Falling back to user location" ) try: self.set_dir(name, self.prefix, env_name, 'user') except: # noqa pass else: logger.fatal("Unable to create AllUsers menu folder") def set_dir(self, name, prefix, env_name, mode, root_prefix): self.mode = mode self.dir = dict() # I have chickened out on allowing check_other_mode. Really there needs # to be 3 distinct cases that 'menuinst' cares about: # priv-user doing system install # priv-user doing user-only install # non-priv-user doing user-only install # (priv-user only exists in an AllUsers installation). check_other_mode = False for k, v in dirs_src[mode].items(): # We may want to cache self.dir to some files, one for AllUsers # (system) installs and one for each subsequent user install? self.dir[k] = folder_path(mode, check_other_mode, k) self.dir['prefix'] = prefix self.dir['root_prefix'] = root_prefix self.dir['env_name'] = env_name folder_name = substitute_env_variables(name, self.dir) self.path = join(self.dir["start"], folder_name) self.create() def create(self): if not isdir(self.path): os.mkdir(self.path) def remove(self): rm_empty_dir(self.path) def extend_script_args(args, shortcut): try: args.append(shortcut['scriptargument']) except KeyError: pass try: args.extend(shortcut['scriptarguments']) except KeyError: pass def quote_args(args): # cmd.exe /K or /C expects a single string argument and requires # doubled-up quotes when any sub-arguments have spaces: # https://stackoverflow.com/a/6378038/3257826 if ( len(args) > 2 and ("CMD.EXE" in args[0].upper() or "%COMSPEC%" in args[0].upper()) and (args[1].upper() == '/K' or args[1].upper() == '/C') and any(' ' in arg for arg in args[2:]) ): args = [ ensure_pad(args[0], '"'), # cmd.exe args[1], # /K or /C '"%s"' % (' '.join(ensure_pad(arg, '"') for arg in args[2:])), # double-quoted ] else: args = [quoted(arg) for arg in args] return args class ShortCut(object): def __init__(self, menu, shortcut): self.menu = menu self.shortcut = shortcut def remove(self): self.create(remove=True) def create(self, remove=False): # Substitute env variables early because we may need to escape spaces in the value. args = [] fix_win_slashes = [0] prefix = self.menu.prefix.replace('/', '\\') unicode_root_prefix = self.menu.root_prefix.replace('/', '\\') root_py = join(unicode_root_prefix, u"python.exe") root_pyw = join(unicode_root_prefix, u"pythonw.exe") env_py = join(prefix, u"python.exe") env_pyw = join(prefix, u"pythonw.exe") cwp_py = [root_py, join(unicode_root_prefix, u'cwp.py'), prefix, env_py] cwp_pyw = [root_pyw, join(unicode_root_prefix, u'cwp.py'), prefix, env_pyw] if "pywscript" in self.shortcut: args = cwp_pyw fix_win_slashes = [len(args)] args += self.shortcut["pywscript"].split() elif "pyscript" in self.shortcut: args = cwp_py fix_win_slashes = [len(args)] args += self.shortcut["pyscript"].split() elif "webbrowser" in self.shortcut: args = [root_pyw, '-m', 'webbrowser', '-t', self.shortcut['webbrowser']] elif "script" in self.shortcut: # It is unclear whether running through cwp.py is what we want here. In # the long term I would rather this was made an explicit choice. args = [root_py, join(unicode_root_prefix, u'cwp.py'), prefix] fix_win_slashes = [len(args)] args += self.shortcut["script"].split() extend_script_args(args, self.shortcut) elif "system" in self.shortcut: args = self.shortcut["system"].split() extend_script_args(args, self.shortcut) else: raise Exception("Nothing to do: %r" % self.shortcut) args = [substitute_env_variables(arg, self.menu.dir) for arg in args] for fws in fix_win_slashes: args[fws] = args[fws].replace('/', '\\') args = quote_args(args) cmd = args[0] args = args[1:] logger.debug('Shortcut cmd is %s, args are %s' % (cmd, args)) workdir = self.shortcut.get('workdir', '') icon = self.shortcut.get('icon', '') workdir = substitute_env_variables(workdir, self.menu.dir) icon = substitute_env_variables(icon, self.menu.dir) # Fix up the '/' to '\' workdir = workdir.replace('/', '\\') icon = icon.replace('/', '\\') # Create the working directory if it doesn't exist if workdir: if not isdir(workdir): os.makedirs(workdir) else: workdir = '%HOMEPATH%' # Menu link dst_dirs = [self.menu.path] # Desktop link if self.shortcut.get('desktop'): dst_dirs.append(self.menu.dir['desktop']) # Quicklaunch link if self.shortcut.get('quicklaunch') and 'quicklaunch' in self.menu.dir: dst_dirs.append(self.menu.dir['quicklaunch']) name_suffix = ( " ({})".format(self.menu.dir['env_name']) if self.menu.dir['env_name'] else "" ) for dst_dir in dst_dirs: name = substitute_env_variables(self.shortcut['name'], self.menu.dir) dst = join(dst_dir, name + name_suffix + '.lnk') if remove: rm_rf(dst) else: # The API for the call to 'create_shortcut' has 3 # required arguments (path, description and filename) # and 4 optional ones (args, working_dir, icon_path and # icon_index). create_shortcut( u'' + cmd, u'' + name + name_suffix, u'' + dst, u' '.join(arg for arg in args), u'' + workdir, u'' + icon, )