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