1#!/usr/bin/env python 2# This file is part of MyPaint. 3# Copyright (C) 2007-2013 by Martin Renold <martinxyz@gmx.ch> 4# Copyright (C) 2013-2018 by the MyPaint Development Team. 5# 6# This program is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 2 of the License, or 9# (at your option) any later version. 10 11"""Platform-dependent setup, and program launch. 12 13This script does all the platform dependent stuff. 14Its main task is to figure out where MyPaint's python modules are, 15and set up paths for i18n message catalogs. 16 17It then passes control to gui.main.main() for command line launching. 18 19""" 20 21## Imports (standard Python only at this point) 22 23import sys 24import os 25import re 26import logging 27 28logger = logging.getLogger('mypaint') 29if sys.version_info >= (3,): 30 xrange = range 31 unicode = str 32 33 34## Logging classes 35 36class ColorFormatter (logging.Formatter): 37 """Minimal ANSI formatter, for use with non-Windows console logging.""" 38 39 # ANSI control sequences for various things 40 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) 41 FG = 30 42 BG = 40 43 LEVELCOL = { 44 "DEBUG": "\033[%02dm" % (FG+BLUE,), 45 "INFO": "\033[%02dm" % (FG+GREEN,), 46 "WARNING": "\033[%02dm" % (FG+YELLOW,), 47 "ERROR": "\033[%02dm" % (FG+RED,), 48 "CRITICAL": "\033[%02d;%02dm" % (FG+RED, BG+BLACK), 49 } 50 BOLD = "\033[01m" 51 BOLDOFF = "\033[22m" 52 ITALIC = "\033[03m" 53 ITALICOFF = "\033[23m" 54 UNDERLINE = "\033[04m" 55 UNDERLINEOFF = "\033[24m" 56 RESET = "\033[0m" 57 58 def _replace_bold(self, m): 59 return self.BOLD + m.group(0) + self.BOLDOFF 60 61 def _replace_underline(self, m): 62 return self.UNDERLINE + m.group(0) + self.UNDERLINEOFF 63 64 def format(self, record): 65 record = logging.makeLogRecord(record.__dict__) 66 msg = record.msg 67 token_formatting = [ 68 (re.compile(r'%r'), self._replace_bold), 69 (re.compile(r'%s'), self._replace_bold), 70 (re.compile(r'%\+?[0-9.]*d'), self._replace_bold), 71 (re.compile(r'%\+?[0-9.]*f'), self._replace_bold), 72 ] 73 for token_re, repl in token_formatting: 74 msg = token_re.sub(repl, msg) 75 record.msg = msg 76 record.reset = self.RESET 77 record.bold = self.BOLD 78 record.boldOff = self.BOLDOFF 79 record.italic = self.ITALIC 80 record.italicOff = self.ITALICOFF 81 record.underline = self.UNDERLINE 82 record.underlineOff = self.UNDERLINEOFF 83 record.levelCol = "" 84 if record.levelname in self.LEVELCOL: 85 record.levelCol = self.LEVELCOL[record.levelname] 86 return super(ColorFormatter, self).format(record) 87 88 89## Helper functions 90 91 92def win32_unicode_argv(): 93 # fix for https://gna.org/bugs/?17739 94 # code mostly comes from http://code.activestate.com/recipes/572200/ 95 """Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode 96 strings. 97 98 Versions 2.x of Python don't support Unicode in sys.argv on 99 Windows, with the underlying Windows API instead replacing multi-byte 100 characters with '?'. 101 """ 102 try: 103 from ctypes import POINTER, byref, cdll, c_int, windll 104 from ctypes.wintypes import LPCWSTR, LPWSTR 105 106 get_cmd = cdll.kernel32.GetCommandLineW 107 get_cmd.argtypes = [] 108 get_cmd.restype = LPCWSTR 109 get_argv = windll.shell32.CommandLineToArgvW 110 get_argv.argtypes = [LPCWSTR, POINTER(c_int)] 111 112 get_argv.restype = POINTER(LPWSTR) 113 cmd = get_cmd() 114 argc = c_int(0) 115 argv = get_argv(cmd, byref(argc)) 116 if argc.value > 0: 117 # Remove Python executable if present 118 if argc.value - len(sys.argv) == 1: 119 start = 1 120 else: 121 start = 0 122 return [argv[i] for i in xrange(start, argc.value)] 123 except Exception: 124 logger.exception( 125 "Specialized Win32 argument handling failed. Please " 126 "help us determine if this code is still needed, " 127 "and submit patches if it's not." 128 ) 129 logger.warning("Falling back to POSIX-style argument handling") 130 131 132def get_paths(): 133 join = os.path.join 134 135 # Convert sys.argv to a list of unicode objects 136 # (actually converting sys.argv confuses gtk, thus we add a new variable) 137 # Post-Py3: almost certainly not needed, but check *all* platforms 138 # before removing this stuff. 139 140 sys.argv_unicode = None 141 if sys.platform == 'win32': 142 sys.argv_unicode = win32_unicode_argv() 143 144 if sys.argv_unicode is None: 145 argv_unicode = [] 146 for s in sys.argv: 147 if hasattr(s, "decode"): 148 s = s.decode(sys.getfilesystemencoding()) 149 argv_unicode.append(s) 150 sys.argv_unicode = argv_unicode 151 152 # Script and its location, in canonical absolute form 153 scriptfile = os.path.realpath(sys.argv_unicode[0]) 154 scriptfile = os.path.abspath(os.path.normpath(scriptfile)) 155 scriptdir = os.path.dirname(scriptfile) 156 assert isinstance(scriptfile, unicode) 157 assert isinstance(scriptdir, unicode) 158 159 # Determine the installation's directory layout. 160 # Assume a conventional POSIX-style directory structure first, 161 # where the launch script resides in $prefix/bin/. 162 dir_install = scriptdir 163 prefix = os.path.dirname(dir_install) 164 assert isinstance(prefix, unicode) 165 libpath = join(prefix, 'share', 'mypaint') 166 localepath = join(prefix, 'share', 'locale') 167 iconspath = join(prefix, 'share', 'icons') 168 if os.path.exists(libpath) and os.path.exists(iconspath): 169 # This is a normal POSIX-like installation. 170 # The Windows standalone distribution works like this too. 171 libpath_compiled = join(prefix, 'lib', 'mypaint') # or lib64? 172 sys.path.insert(0, libpath) 173 sys.path.insert(0, libpath_compiled) 174 sys.path.insert(0, join(prefix, 'share')) # for libmypaint 175 logger.info("Installation layout: conventional POSIX-like structure " 176 "with prefix %r", 177 prefix) 178 elif all(map(os.path.exists, ['desktop', 'gui', 'lib'])): 179 # Testing from within the source tree. 180 prefix = None 181 libpath = u'.' 182 iconspath = u'desktop/icons' 183 localepath = os.path.join('build', 'locale') 184 logger.info("Installation layout: not installed, " 185 "testing from within the source tree") 186 elif sys.platform == 'win32': 187 prefix = None 188 # This is py2exe point of view, all executables in root of 189 # installdir. 190 # XXX: are py2exe builds still relevant? The 1.2.0-beta Windows 191 # installers are kitchen sink affairs. 192 libpath = os.path.realpath(scriptdir) 193 sys.path.insert(0, libpath) 194 sys.path.insert(0, join(prefix, 'share')) # for libmypaint 195 localepath = join(libpath, 'share', 'locale') 196 iconspath = join(libpath, 'share', 'icons') 197 logger.info("Installation layout: Windows fallback, assuming py2exe") 198 else: 199 logger.critical("Installation layout: unknown!") 200 raise RuntimeError("Unknown install type; could not determine paths") 201 202 assert isinstance(libpath, unicode) 203 204 datapath = libpath 205 206 # There is no need to return the datadir of mypaint-data. 207 # It will be set at build time. I still check brushes presence. 208 import lib.config 209 # Allow brushdir path to be set relative to the installation prefix 210 # Use string-formatting *syntax*, but not actual formatting. This is 211 # to not have to deal with the remote possibility of a legitimate 212 # brushdir path with brace-enclosed components (legal UNIX-paths). 213 brushdir_path = lib.config.mypaint_brushdir 214 pref_key = "{installation-prefix}/" 215 if brushdir_path.startswith(pref_key): 216 logger.info("Using brushdir path relative to installation-prefix") 217 brushdir_path = join(prefix, brushdir_path[len(pref_key):]) 218 if not os.path.isdir(brushdir_path): 219 logger.critical('Default brush collection not found!') 220 logger.critical('It should have been here: %r', brushdir_path) 221 sys.exit(1) 222 223 # When using a prefix-relative path, replace it with the absolute path 224 lib.config.mypaint_brushdir = brushdir_path 225 226 # Old style config file and user data locations. 227 # Return None if using XDG will be correct. 228 if sys.platform == 'win32': 229 old_confpath = None 230 else: 231 from lib import fileutils 232 homepath = fileutils.expanduser_unicode(u'~') 233 old_confpath = join(homepath, '.mypaint/') 234 235 if old_confpath: 236 if not os.path.isdir(old_confpath): 237 old_confpath = None 238 else: 239 logger.info("There is an old-style configuration area in %r", 240 old_confpath) 241 logger.info("Its contents can be migrated to $XDG_CONFIG_HOME " 242 "and $XDG_DATA_HOME if you wish.") 243 logger.info("See the XDG Base Directory Specification for info.") 244 245 assert isinstance(old_confpath, unicode) or old_confpath is None 246 assert isinstance(datapath, unicode) 247 assert isinstance(iconspath, unicode) 248 249 return datapath, iconspath, old_confpath, localepath 250 251## Program launch 252 253 254if __name__ == '__main__': 255 # Console logging 256 log_format = "%(levelname)s: %(name)s: %(message)s" 257 console_handler = logging.StreamHandler(stream=sys.stderr) 258 no_ansi_platforms = ["win32"] 259 can_use_ansi_formatting = ( 260 (sys.platform not in no_ansi_platforms) 261 and sys.stderr.isatty() 262 ) 263 if can_use_ansi_formatting: 264 log_format = ( 265 "%(levelCol)s%(levelname)s: " 266 "%(bold)s%(name)s%(reset)s%(levelCol)s: " 267 "%(message)s%(reset)s" 268 ) 269 console_formatter = ColorFormatter(log_format) 270 else: 271 console_formatter = logging.Formatter(log_format) 272 console_handler.setFormatter(console_formatter) 273 debug = os.environ.get("MYPAINT_DEBUG", False) 274 logging_level = logging.DEBUG if debug else logging.INFO 275 root_logger = logging.getLogger(None) 276 root_logger.addHandler(console_handler) 277 root_logger.setLevel(logging_level) 278 if logging_level == logging.DEBUG: 279 logger.info("Debugging output enabled via MYPAINT_DEBUG") 280 281 # Path determination 282 datapath, iconspath, old_confpath, localepath \ 283 = get_paths() 284 logger.debug('datapath: %r', datapath) 285 logger.debug('iconspath: %r', iconspath) 286 logger.debug('old_confpath: %r', old_confpath) 287 logger.debug('localepath: %r', localepath) 288 289 # Allow an override version string to be burned in during build. Comes 290 # from an active repository's git information and build timestamp, or 291 # the release_info file from a tarball release. 292 try: 293 version = MYPAINT_VERSION_CEREMONIAL 294 except NameError: 295 version = None 296 297 # Start the app. 298 from gui import main 299 main.main( 300 datapath, 301 iconspath, 302 localepath, 303 old_confpath, 304 version=version, 305 debug=debug, 306 ) 307