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