1# This file is part of the Frescobaldi project, http://www.frescobaldi.org/
2#
3# Copyright (c) 2008 - 2014 by Wilbert Berendsen
4#
5# This program is free software; you can redistribute it and/or
6# modify it under the terms of the GNU General Public License
7# as published by the Free Software Foundation; either version 2
8# of the License, or (at your option) any later version.
9#
10# This program 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
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18# See http://www.gnu.org/licenses/ for more information.
19
20"""
21Functions that have to do with Mac OS X-specific behaviour.
22
23Importing this package does nothing; the macosx.setup module sets up
24the Mac OS X-specific behaviour, such as the global menu and the file open event
25handler, etc.
26
27"""
28
29
30import os
31import sys
32
33
34def inside_app_bundle():
35    """Return True if we are inside a .app bundle."""
36    # Testing for sys.frozen == 'macosx_app' (i.e. are we inside a .app bundle
37    # packaged with py2app?) should be sufficient, but let's also check whether
38    # we are inside a .app-bundle-like folder structure.
39    return (getattr(sys, 'frozen', '') == 'macosx_app' or
40        '.app/Contents/MacOS' in os.path.abspath(sys.argv[0]))
41
42def inside_lightweight_app_bundle():
43    """Return True if we are inside a lightweight .app bundle."""
44    # A lightweight .app bundle (created with macosx/mac-app.py without
45    # the standalone option) contains a symlink to the Python interpreter
46    # instead of a copy.
47    return (inside_app_bundle()
48            and os.path.islink(os.getcwd() + '/../MacOS/python'))
49
50def use_osx_menu_roles():
51    """Return True if Mac OS X-specific menu roles are to be used."""
52    global _use_roles
53    try:
54        return _use_roles
55    except NameError:
56        _use_roles = inside_app_bundle()
57    return _use_roles
58
59def midi_so_arch(lilypondinfo):
60    """Find the midi.so library of the selected LilyPond installation,
61    if it exists, and return its architecture; otherwise return None.
62
63    """
64    import subprocess
65    for d in [lilypondinfo.versionString(), 'current']:
66        midi_so = os.path.abspath(lilypondinfo.bindir() + '/../lib/lilypond/' + d + '/python/midi.so')
67        if os.access(midi_so, os.R_OK):
68            s = subprocess.run(['/usr/bin/file', midi_so], capture_output = True)
69            if b'x86_64' in s.stdout:
70                return 'x86_64'
71            else:
72                return 'i386'
73    return None
74
75def system_python(major, arch):
76    """Return a list containing the command line to run the system Python.
77
78    (One of) the system-provided Python interpreter(s) is selected to run
79    the tools included in LilyPond, e.g. convert-ly.
80
81    Unless LilyPond is provided by MacPorts, the system-provided Python
82    interpreter must be explicitly called:
83    - the LilyPond-provided interpreter lacks many modules (and is
84      difficult to run properly from outside the application bundle);
85    - searching for the interpreter in the path is unreliable, since it
86      might lack some modules or it could be an incompatible version;
87      moreover if Frescobaldi is launched as an application bundle,
88      the PATH variable is not set;
89    - the interpreter included in Frescobaldi's application bundle,
90      when present, lacks some modules; moreover, Frescobaldi now uses
91      Python 3, while LilyPond's tools prior to version 2.21.0 are written
92      in Python 2.
93
94    If Python 2 is requested, the earliest Python 2 version >= 2.4 is
95    called, possibly avoiding the following:
96    - Python >= 2.5 gives a "C API version mismatch" RuntimeWarning
97      on `import midi`;
98    - Python >= 2.6 gives a DeprecationWarning on `import popen2`.
99    A Python 2 interpreter is always available (as of macOS 10.15 Catalina).
100    If Python 3 is requested, the earliest Python 3 version >= 3.5 is
101    called.
102
103    In LilyPond <= 2.19.54 midi2ly depends on the binary library midi.so
104    (replaced in 2.19.55 by a Python module), which is 32 bit in the app
105    bundle distributed by LilyPond and might be in other cases as well.
106    Thus, the selected Python interpreter is called with the corresponding
107    architecture.
108    If 32 bit architecture is required but the system Pythons do not
109    support it (as is the case on macOS >= 10.15 Catalina), return None.
110
111    """
112    import platform
113    mac_ver = platform.mac_ver()
114    if (arch == 'i386') and (int(mac_ver[0].split('.')[1]) >= 15):
115        return None
116    if major == 2:
117        minors = list(range(4, 8))
118    elif major == 3:
119        minors = list(range(5, 9))
120    for minor in minors:
121        version = str(major) + '.' + str(minor)
122        python = '/System/Library/Frameworks/Python.framework/Versions/' + version
123        python += '/bin/python' + version
124        if os.path.exists(python):
125            return ['/usr/bin/arch', '-' + arch, python, '-E']
126
127def best_python(lilypondinfo, tool):
128    """Return a list containing the Python command required to run
129    LilyPond's tools (the list may be empty), or None if no suitable
130    Python is found.
131
132    If the selected LilyPond installation is provided by MacPorts,
133    the #! line of LilyPond's tools is already set correctly, so
134    no command needs to be prepended.
135    The use of the #! line can be forced in the settings of the
136    individual LilyPond installations.
137    Otherwise a suitable system Python is searched.
138
139    """
140    if lilypondinfo.frommacports() or lilypondinfo.useshebang:
141        return []
142    if (tool == 'midi2ly') and (lilypondinfo.version() <= (2, 19, 54)) and midi_so_arch(lilypondinfo):
143        arch = midi_so_arch(lilypondinfo)
144    else:
145        arch = 'x86_64'
146    if lilypondinfo.version() >= (2, 21, 0):
147        major = 3
148    else:
149        major = 2
150    return system_python(major, arch)
151
152