1# -*- Mode: Python -*-
2# GObject-Introspection - a framework for introspecting GObject libraries
3# Copyright (C) 2008  Johan Dahlin
4#
5# This library is free software; you can redistribute it and/or
6# modify it under the terms of the GNU Lesser General Public
7# License as published by the Free Software Foundation; either
8# version 2 of the License, or (at your option) any later version.
9#
10# This library is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13# Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public
16# License along with this library; if not, write to the
17# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
18# Boston, MA 02111-1307, USA.
19#
20
21import re
22import os
23import subprocess
24import platform
25import shutil
26import sys
27import time
28import giscanner.pkgconfig
29
30
31_debugflags = None
32
33
34def have_debug_flag(flag):
35    """Check for whether a specific debugging feature is enabled.
36Well-known flags:
37 * start: Drop into debugger just after processing arguments
38 * exception: Drop into debugger on fatalexception
39 * warning: Drop into debugger on warning
40 * posttrans: Drop into debugger just before introspectable pass
41"""
42    global _debugflags
43    if _debugflags is None:
44        _debugflags = os.environ.get('GI_SCANNER_DEBUG', '').split(',')
45        if '' in _debugflags:
46            _debugflags.remove('')
47    return flag in _debugflags
48
49
50def break_on_debug_flag(flag):
51    if have_debug_flag(flag):
52        import pdb
53        pdb.set_trace()
54
55
56# Copied from h2defs.py
57_upperstr_pat1 = re.compile(r'([^A-Z])([A-Z])')
58_upperstr_pat2 = re.compile(r'([A-Z][A-Z])([A-Z][0-9a-z])')
59_upperstr_pat3 = re.compile(r'^([A-Z])([A-Z])')
60
61
62def to_underscores(name):
63    """Converts a typename to the equivalent underscores name.
64    This is used to form the type conversion macros and enum/flag
65    name variables.
66    In particular, and differently from to_underscores_noprefix(),
67    this function treats the first character differently if it is
68    uppercase and followed by another uppercase letter."""
69    name = _upperstr_pat1.sub(r'\1_\2', name)
70    name = _upperstr_pat2.sub(r'\1_\2', name)
71    name = _upperstr_pat3.sub(r'\1_\2', name, count=1)
72    return name
73
74
75def to_underscores_noprefix(name):
76    """Like to_underscores, but designed for "unprefixed" names.
77    to_underscores("DBusFoo") => dbus_foo, not d_bus_foo."""
78    name = _upperstr_pat1.sub(r'\1_\2', name)
79    name = _upperstr_pat2.sub(r'\1_\2', name)
80    return name
81
82
83_libtool_pat = re.compile("dlname='([A-z0-9\\.\\-\\+]+)'\n")
84
85
86def _extract_dlname_field(la_file):
87    with open(la_file, encoding='utf-8') as f:
88        data = f.read()
89    m = _libtool_pat.search(data)
90    if m:
91        return m.groups()[0]
92    else:
93        return None
94
95
96_libtool_libdir_pat = re.compile("libdir='([^']+)'")
97
98
99def _extract_libdir_field(la_file):
100    with open(la_file, encoding='utf-8') as f:
101        data = f.read()
102    m = _libtool_libdir_pat.search(data)
103    if m:
104        return m.groups()[0]
105    else:
106        return None
107
108
109# Returns the name that we would pass to dlopen() the library
110# corresponding to this .la file
111def extract_libtool_shlib(la_file):
112    dlname = _extract_dlname_field(la_file)
113    if dlname is None:
114        return None
115
116    # Darwin uses absolute paths where possible; since the libtool files never
117    # contain absolute paths, use the libdir field
118    if platform.system() == 'Darwin':
119        dlbasename = os.path.basename(dlname)
120        libdir = _extract_libdir_field(la_file)
121        if libdir is None:
122            return dlbasename
123        return libdir + '/' + dlbasename
124    # Older libtools had a path rather than the raw dlname
125    return os.path.basename(dlname)
126
127
128# Returns arguments for invoking libtool, if applicable, otherwise None
129def get_libtool_command(options):
130    libtool_infection = not options.nolibtool
131    if not libtool_infection:
132        return None
133
134    libtool_path = options.libtool_path
135    if libtool_path:
136        # Automake by default sets:
137        # LIBTOOL = $(SHELL) $(top_builddir)/libtool
138        # To be strictly correct we would have to parse shell.  For now
139        # we simply split().
140        return libtool_path.split(' ')
141
142    libtool_cmd = 'libtool'
143    if platform.system() == 'Darwin':
144        # libtool on OS X is a completely different program written by Apple
145        libtool_cmd = 'glibtool'
146    try:
147        subprocess.check_call([libtool_cmd, '--version'],
148                              stdout=open(os.devnull, 'w'))
149    except (subprocess.CalledProcessError, OSError):
150        # If libtool's not installed, assume we don't need it
151        return None
152
153    return [libtool_cmd]
154
155
156def files_are_identical(path1, path2):
157    with open(path1, 'rb') as f1, open(path2, 'rb') as f2:
158        buf1 = f1.read(8192)
159        buf2 = f2.read(8192)
160        while buf1 == buf2 and buf1 != b'':
161            buf1 = f1.read(8192)
162            buf2 = f2.read(8192)
163        return buf1 == buf2
164
165
166def cflag_real_include_path(cflag):
167    if not cflag.startswith("-I"):
168        return cflag
169
170    return "-I" + os.path.realpath(cflag[2:])
171
172
173def host_os():
174    return os.environ.get("GI_HOST_OS", os.name)
175
176
177def which(program):
178    def is_exe(fpath):
179        return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
180
181    def is_nt_exe(fpath):
182        return not fpath.lower().endswith('.exe') and \
183            os.path.isfile(fpath + '.exe') and \
184            os.access(fpath + '.exe', os.X_OK)
185
186    fpath, fname = os.path.split(program)
187    if fpath:
188        if is_exe(program):
189            return program
190        if os.name == 'nt' and is_nt_exe(program):
191            return program + '.exe'
192    else:
193        for path in os.environ["PATH"].split(os.pathsep):
194            path = path.strip('"')
195            exe_file = os.path.join(path, program)
196            if is_exe(exe_file):
197                return exe_file
198            if os.name == 'nt' and is_nt_exe(exe_file):
199                return exe_file + '.exe'
200
201    return None
202
203
204def get_user_cache_dir(dir=None):
205    '''
206    This is a Python reimplemention of `g_get_user_cache_dir()` because we don't want to
207    rely on the python-xdg package and we can't depend on GLib via introspection.
208    If any changes are made to that function they'll need to be copied here.
209    '''
210
211    xdg_cache_home = os.environ.get('XDG_CACHE_HOME')
212    if xdg_cache_home is not None:
213        if dir is not None:
214            xdg_cache_home = os.path.join(xdg_cache_home, dir)
215        try:
216            os.makedirs(xdg_cache_home, mode=0o755, exist_ok=True)
217        except EnvironmentError:
218            # Let's fall back to ~/.cache below
219            pass
220        else:
221            return xdg_cache_home
222
223    homedir = os.path.expanduser('~')
224    if homedir is not None:
225        cachedir = os.path.join(homedir, '.cache')
226        if dir is not None:
227            cachedir = os.path.join(cachedir, dir)
228        try:
229            os.makedirs(cachedir, mode=0o755, exist_ok=True)
230        except EnvironmentError:
231            return None
232        else:
233            return cachedir
234
235    return None
236
237
238def get_system_data_dirs():
239    '''
240    This is a Python reimplemention of `g_get_system_data_dirs()` because we don't want to
241    rely on the python-xdg package and we can't depend on GLib via introspection.
242    If any changes are made to that function they'll need to be copied here.
243    '''
244    xdg_data_dirs = [x for x in os.environ.get('XDG_DATA_DIRS', '').split(os.pathsep)]
245    if not any(xdg_data_dirs) and os.name != 'nt':
246        xdg_data_dirs.append('/usr/local/share')
247        xdg_data_dirs.append('/usr/share')
248
249    return xdg_data_dirs
250
251
252def rmtree(*args, **kwargs):
253    '''
254    A variant of shutil.rmtree() which waits and tries again in case one of
255    the files in the directory tree can't be deleted.
256
257    This can be the case if a file is still in use by some process,
258    for example a Virus scanner on Windows scanning .exe files when they are
259    created.
260    '''
261
262    tries = 3
263    for i in range(1, tries + 1):
264        try:
265            shutil.rmtree(*args, **kwargs)
266        except OSError:
267            if i == tries:
268                raise
269            time.sleep(i)
270            continue
271        else:
272            return
273
274
275# Mainly used for builds against Python 3.8.x and later on Windows where we need to be
276# more explicit on where dependent DLLs are located, via the use of
277# os.add_dll_directory().  So, we make use of the envvar GI_EXTRA_BASE_DLL_DIRS and the
278# newly-added bindir() method of our pkgconfig module to acquire the paths where dependent
279# DLLs could be found.
280class dll_dirs():
281    _cached_dll_dirs = None
282    _cached_added_dll_dirs = None
283
284    def __init__(self):
285        if os.name == 'nt' and hasattr(os, 'add_dll_directory'):
286            self._cached_dll_dirs = []
287            self._cached_added_dll_dirs = []
288
289    def add_dll_dirs(self, pkgs):
290        if os.name == 'nt' and hasattr(os, 'add_dll_directory'):
291            if 'GI_EXTRA_BASE_DLL_DIRS' in os.environ:
292                for path in os.environ.get('GI_EXTRA_BASE_DLL_DIRS').split(os.pathsep):
293                    if path not in self._cached_dll_dirs:
294                        self._cached_dll_dirs.append(path)
295                        self._cached_added_dll_dirs.append(os.add_dll_directory(path))
296
297            for path in giscanner.pkgconfig.bindir(pkgs):
298                if path not in self._cached_dll_dirs:
299                    self._cached_dll_dirs.append(path)
300                    self._cached_added_dll_dirs.append(os.add_dll_directory(path))
301
302    def cleanup_dll_dirs(self):
303        if self._cached_added_dll_dirs is not None:
304            for added_dll_dir in self._cached_added_dll_dirs:
305                added_dll_dir.close()
306        if self._cached_dll_dirs is not None:
307            self._cached_dll_dirs.clear()
308
309
310# monkey patch distutils.cygwinccompiler
311# somehow distutils returns runtime library only up to
312# VS2010 / MSVC 10.0 (msc_ver 1600)
313def get_msvcr_overwrite():
314    try:
315        return orig_get_msvcr()
316    except ValueError:
317        pass
318
319    msc_pos = sys.version.find('MSC v.')
320    if msc_pos != -1:
321        msc_ver = sys.version[msc_pos + 6:msc_pos + 10]
322
323        if msc_ver == '1700':
324            # VS2012
325            return ['msvcr110']
326        elif msc_ver == '1800':
327            # VS2013
328            return ['msvcr120']
329        elif msc_ver >= '1900':
330            # VS2015
331            return ['vcruntime140']
332
333
334import distutils.cygwinccompiler
335orig_get_msvcr = distutils.cygwinccompiler.get_msvcr  # type: ignore
336distutils.cygwinccompiler.get_msvcr = get_msvcr_overwrite  # type: ignore
337