1#!/usr/local/bin/python3.8
2
3# Copyright (C) 2002-2006 Stephen Kennedy <stevek@gnome.org>
4# Copyright (C) 2009-2014 Kai Willadsen <kai.willadsen@gmail.com>
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 (at
9# your option) any later version.
10#
11# This program is distributed in the hope that it will be useful, but
12# WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14# General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
19import locale
20import logging
21import os
22import signal
23import subprocess
24import sys
25from multiprocessing import freeze_support
26
27# On Windows, pythonw.exe (which doesn't display a console window) supplies
28# dummy stdout and stderr streams that silently throw away any output. However,
29# these streams seem to have issues with flush() so we just redirect stdout and
30# stderr to actual dummy files (the equivalent of /dev/null).
31# Regarding pythonw.exe stdout, see also http://bugs.python.org/issue706263
32# Also cx_Freeze built with Win32GUI base sets sys.stdout to None
33# leading to exceptions in print() and freeze_support() that uses flush()
34if sys.executable.endswith("pythonw.exe") or sys.stdout is None:
35    devnull = open(os.devnull, "w")
36    sys.stdout = sys.stderr = devnull
37
38# Main module hasn't multiprocessing workers, so not imported in subprocesses.
39# This allows skipping '__name__ == "main"' guard, but freezed case is special.
40freeze_support()
41
42
43def disable_stdout_buffering():
44
45    class Unbuffered:
46
47        def __init__(self, file):
48            self.file = file
49
50        def write(self, arg):
51            self.file.write(arg)
52            self.file.flush()
53
54        def __getattr__(self, attr):
55            return getattr(self.file, attr)
56
57    sys.stdout = Unbuffered(sys.stdout)
58
59
60def get_meld_dir():
61    global frozen
62    if frozen:
63        return os.path.dirname(sys.executable)
64
65    # Support running from an uninstalled version
66    self_path = os.path.realpath(__file__)
67    return os.path.abspath(os.path.join(os.path.dirname(self_path), ".."))
68
69
70frozen = getattr(sys, 'frozen', False)
71melddir = get_meld_dir()
72
73uninstalled = False
74if os.path.exists(os.path.join(melddir, "meld.doap")):
75    sys.path[0:0] = [melddir]
76    uninstalled = True
77devel = os.path.exists(os.path.join(melddir, ".git"))
78
79import meld.conf  # noqa: E402
80
81# Silence warnings on non-devel releases (minor version is divisible by 2)
82is_stable = not bool(int(meld.conf.__version__.split('.')[1]) % 2)
83if is_stable:
84    import warnings
85    warnings.simplefilter("ignore")
86
87if uninstalled:
88    meld.conf.uninstalled()
89elif frozen:
90    meld.conf.frozen()
91
92# TODO: Possibly move to elib.intl
93import gettext  # noqa: E402, I100
94locale_domain = meld.conf.__package__
95locale_dir = meld.conf.LOCALEDIR
96
97gettext.bindtextdomain(locale_domain, locale_dir)
98try:
99    locale.setlocale(locale.LC_ALL, '')
100except locale.Error as e:
101    print("Couldn't set the locale: %s; falling back to 'C' locale" % e)
102    locale.setlocale(locale.LC_ALL, 'C')
103gettext.textdomain(locale_domain)
104trans = gettext.translation(locale_domain, localedir=locale_dir, fallback=True)
105try:
106    _ = meld.conf._ = trans.ugettext
107    meld.conf.ngettext = trans.ungettext
108except AttributeError:
109    # py3k
110    _ = meld.conf._ = trans.gettext
111    meld.conf.ngettext = trans.ngettext
112
113try:
114    if os.name == 'nt':
115        from ctypes import cdll
116        if frozen:
117            libintl = cdll['libintl-8']
118        else:
119            libintl = cdll.intl
120        libintl.bindtextdomain(locale_domain, locale_dir)
121        libintl.bind_textdomain_codeset(locale_domain, 'UTF-8')
122        del libintl
123    else:
124        locale.bindtextdomain(locale_domain, locale_dir)
125        locale.bind_textdomain_codeset(locale_domain, 'UTF-8')
126except AttributeError as e:
127    # Python builds linked without libintl (i.e., OSX) don't have
128    # bindtextdomain(), which causes Gtk.Builder translations to fail.
129    print(
130        "Couldn't bind the translation domain. Some translations won't "
131        "work.\n{}".format(e))
132except locale.Error as e:
133    print(
134        "Couldn't bind the translation domain. Some translations won't "
135        "work.\n{}".format(e))
136except WindowsError as e:
137    # Accessing cdll.intl sometimes fails on Windows for unknown reasons.
138    # Let's just continue, as translations are non-essential.
139    print(
140        "Couldn't bind the translation domain. Some translations won't "
141        "work.\n{}".format(e))
142
143
144def show_error_and_exit(error_text):
145    """
146    Show error in a robust way: always print to stdout and try to
147    display gui message via gtk or tkinter (first available).
148    Empty toplevel window is used as message box parent since
149    parentless message box cause toolkit and windowing system problems.
150    This function is both python 2 and python 3 compatible since it is used
151    to display wrong python version.
152    """
153    print(error_text)
154    raise_as_last_resort_to_display = False
155    try:
156        import gi
157        gi.require_version("Gtk", "3.0")
158        from gi.repository import Gtk
159        toplevel = Gtk.Window(title="Meld")
160        toplevel.show()
161        Gtk.MessageDialog(
162            toplevel, 0, Gtk.MessageType.ERROR,
163            Gtk.ButtonsType.CLOSE, error_text).run()
164    except Exception:
165        # Although tkinter is imported here, it isn't meld's dependency:
166        # if found it is used to show GUI error about lacking true dependecies.
167        try:
168            if sys.version_info < (3, 0):
169                from Tkinter import Tk
170                from tkMessageBox import showerror
171            else:
172                from tkinter import Tk
173                from tkinter.messagebox import showerror
174            toplevel = Tk(className="Meld")
175            toplevel.wait_visibility()
176            showerror("Meld", error_text, parent=toplevel)
177        except Exception:
178            # Displaying with tkinter failed too, just exit if not frozen.
179            # Frozen app may lack console but be able to show exceptions.
180            raise_as_last_resort_to_display = frozen
181    if raise_as_last_resort_to_display:
182        raise Exception(error_text)
183    sys.exit(1)
184
185
186def check_requirements():
187
188    gtk_requirement = (3, 20)
189    glib_requirement = (2, 48)
190    gtksourceview_requirement = (3, 20, 0)
191
192    def missing_reqs(mod, ver, exc=None):
193        if isinstance(exc, ImportError):
194            show_error_and_exit(_("Cannot import: ") + mod + "\n" + str(exc))
195        else:
196            modver = mod + " " + ".".join(map(str, ver))
197            show_error_and_exit(_("Meld requires %s or higher.") % modver)
198
199    if sys.version_info[:2] < meld.conf.PYTHON_REQUIREMENT_TUPLE:
200        missing_reqs("Python", meld.conf.PYTHON_REQUIREMENT_TUPLE)
201
202    # gtk+ and related imports
203    try:
204        # FIXME: Extra clause for gi
205        import gi
206        gi.require_version("Gtk", "3.0")
207        from gi.repository import Gtk
208        version = (Gtk.get_major_version(), Gtk.get_minor_version())
209        assert version >= gtk_requirement
210    except (ImportError, AssertionError, ValueError) as e:
211        missing_reqs("GTK+", gtk_requirement, e)
212
213    try:
214        from gi.repository import GLib
215        assert (GLib.MAJOR_VERSION, GLib.MINOR_VERSION) >= glib_requirement
216    except (ImportError, AssertionError, ValueError) as e:
217        missing_reqs("GLib", glib_requirement, e)
218
219    try:
220        gi.require_version('GtkSource', '3.0')
221        from gi.repository import GtkSource
222        # TODO: There is no way to get at GtkSourceView's actual version
223        assert hasattr(GtkSource, 'SearchSettings')
224        assert hasattr(GtkSource, 'Tag')
225    except (ImportError, AssertionError, ValueError) as e:
226        missing_reqs("GtkSourceView 3", gtksourceview_requirement, e)
227
228
229def setup_resources():
230    from gi.repository import GLib
231    from gi.repository import Gtk
232    from gi.repository import Gdk
233    from gi.repository import GtkSource
234
235    icon_dir = os.path.join(meld.conf.DATADIR, "icons")
236    Gtk.IconTheme.get_default().append_search_path(icon_dir)
237
238    css_file = os.path.join(meld.conf.DATADIR, "meld.css")
239    provider = Gtk.CssProvider()
240    try:
241        provider.load_from_path(css_file)
242    except GLib.GError as err:
243        print(_("Couldn’t load Meld-specific CSS (%s)\n%s") % (css_file, err))
244    Gtk.StyleContext.add_provider_for_screen(
245        Gdk.Screen.get_default(), provider,
246        Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
247
248    style_path = os.path.join(meld.conf.DATADIR, "styles")
249    GtkSource.StyleSchemeManager.get_default().append_search_path(style_path)
250
251
252def setup_settings():
253    import meld.conf
254
255    schema_path = os.path.join(meld.conf.DATADIR, "org.gnome.meld.gschema.xml")
256    compiled_schema_path = os.path.join(meld.conf.DATADIR, "gschemas.compiled")
257
258    try:
259        schema_mtime = os.path.getmtime(schema_path)
260        compiled_mtime = os.path.getmtime(compiled_schema_path)
261        have_schema = schema_mtime < compiled_mtime
262    except OSError:
263        have_schema = False
264
265    if uninstalled and not have_schema:
266        subprocess.call(["glib-compile-schemas", meld.conf.DATADIR],
267                        cwd=melddir)
268
269    import meld.settings
270    meld.settings.create_settings()
271
272
273def setup_logging():
274    log = logging.getLogger()
275
276    # If we're running uninstalled and from Git, turn up the logging level
277    if uninstalled and devel:
278        log.setLevel(logging.INFO)
279    else:
280        log.setLevel(logging.CRITICAL)
281
282    if sys.platform == 'win32':
283        from gi.repository import GLib
284
285        log_path = os.path.join(GLib.get_user_data_dir(), "meld.log")
286        handler = logging.FileHandler(log_path)
287        log.setLevel(logging.INFO)
288    else:
289        handler = logging.StreamHandler()
290
291    formatter = logging.Formatter("%(asctime)s %(levelname)s "
292                                  "%(name)s: %(message)s")
293    handler.setFormatter(formatter)
294    log.addHandler(handler)
295
296
297def setup_glib_logging():
298    from gi.repository import GLib
299    levels = {
300        GLib.LogLevelFlags.LEVEL_DEBUG: logging.DEBUG,
301        GLib.LogLevelFlags.LEVEL_INFO: logging.INFO,
302        GLib.LogLevelFlags.LEVEL_MESSAGE: logging.INFO,
303        GLib.LogLevelFlags.LEVEL_WARNING: logging.WARNING,
304        GLib.LogLevelFlags.LEVEL_ERROR: logging.ERROR,
305        GLib.LogLevelFlags.LEVEL_CRITICAL: logging.CRITICAL,
306    }
307    level_flag = (
308        GLib.LogLevelFlags.LEVEL_WARNING |
309        GLib.LogLevelFlags.LEVEL_ERROR |
310        GLib.LogLevelFlags.LEVEL_CRITICAL
311    )
312
313    log_domain = "Gtk"
314    log = logging.getLogger(log_domain)
315
316    def silence(message):
317        if "Drawing a gadget with negative dimensions" in message:
318            return True
319        return False
320
321    # This logging handler is for "old" glib logging using a simple
322    # syslog-style API.
323    def log_adapter(domain, level, message, user_data):
324        if not silence(message):
325            log.log(levels.get(level, logging.WARNING), message)
326
327    try:
328        GLib.log_set_handler(log_domain, level_flag, log_adapter, None)
329    except AttributeError:
330        # Only present in glib 2.46+
331        pass
332
333    # This logging handler is for new glib logging using a structured
334    # API. Unfortunately, it was added in such a way that the old
335    # redirection API became a no-op, so we need to hack both of these
336    # handlers to get it to work.
337    def structured_log_adapter(level, fields, field_count, user_data):
338        # Don't even format the message if it will be discarded
339        py_logging_level = levels.get(level, logging.WARNING)
340        if log.isEnabledFor(py_logging_level):
341            # at least glib 2.52 log_writer_format_fields can raise on win32
342            try:
343                message = GLib.log_writer_format_fields(level, fields, True)
344                if not silence(message):
345                    log.log(py_logging_level, message)
346            except Exception:
347                GLib.log_writer_standard_streams(level, fields, user_data)
348        return GLib.LogWriterOutput.HANDLED
349
350    try:
351        GLib.log_set_writer_func(structured_log_adapter, None)
352    except AttributeError:
353        # Only present in glib 2.50+
354        pass
355
356
357def environment_hacks():
358    # MSYSTEM is set by git, and confuses our
359    # msys-packaged version's library search path -
360    # for frozen build the lib subdirectory is excluded.
361    # workaround it by adding as first path element.
362    # This may confuse vc utils run from meld
363    # but otherwise meld just crash on start, see #267
364
365    global frozen
366    if frozen and "MSYSTEM" in os.environ:
367        lib_dir = os.path.join(get_meld_dir(), "lib")
368        os.environ["PATH"] = lib_dir + os.pathsep + os.environ["PATH"]
369    # We manage cwd ourselves for git operations, and GIT_DIR in particular
370    # can mess with this when set.
371    for var in ('GIT_DIR', 'GIT_WORK_TREE'):
372        try:
373            del os.environ[var]
374        except KeyError:
375            pass
376
377
378if __name__ == '__main__':
379    environment_hacks()
380    setup_logging()
381    disable_stdout_buffering()
382    check_requirements()
383    setup_glib_logging()
384    setup_resources()
385    setup_settings()
386
387    import meld.meldapp
388    if sys.platform != 'win32':
389        from gi.repository import GLib
390        GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT,
391                             lambda *args: meld.meldapp.app.quit(), None)
392    status = meld.meldapp.app.run(sys.argv)
393    sys.exit(status)
394