1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
|
# Copyright (c) 2020, Riverbank Computing Limited
# All rights reserved.
#
# This copy of PyQt-builder is licensed for use under the terms of the SIP
# License Agreement. See the file LICENSE for more details.
#
# This copy of PyQt-builder may also used under the terms of the GNU General
# Public License v2 or v3 as published by the Free Software Foundation which
# can be found in the files LICENSE-GPL2 and LICENSE-GPL3 included in this
# package.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import glob
import os
import sys
from sipbuild import Bindings, BuildableExecutable, UserException, Option
class PyQtBindings(Bindings):
""" A base class for all PyQt-based bindings. """
def apply_nonuser_defaults(self, tool):
""" Set default values for non-user options that haven't been set yet.
"""
project = self.project
if self.sip_file is None:
# The (not very good) naming convention used by MetaSIP.
self.sip_file = os.path.join(self.name, self.name + 'mod.sip')
super().apply_nonuser_defaults(tool)
self._update_builder_settings('CONFIG', self.qmake_CONFIG)
self._update_builder_settings('QT', self.qmake_QT)
# Add the sources of any support code.
qpy_dir = os.path.join(project.root_dir, 'qpy', self.name)
if os.path.isdir(qpy_dir):
headers = self._matching_files(os.path.join(qpy_dir, '*.h'))
c_sources = self._matching_files(os.path.join(qpy_dir, '*.c'))
cpp_sources = self._matching_files(os.path.join(qpy_dir, '*.cpp'))
sources = c_sources + cpp_sources
self.headers.extend(headers)
self.sources.extend(sources)
if headers or sources:
self.include_dirs.append(qpy_dir)
def apply_user_defaults(self, tool):
""" Set default values for user options that haven't been set yet. """
# Although tags is not a user option, the default depends on one.
if len(self.tags) == 0:
project = self.project
self.tags = ['{}_{}'.format(project.tag_prefix,
project.builder.qt_version_tag)]
super().apply_user_defaults(tool)
def get_options(self):
""" Return the list of configurable options. """
options = super().get_options()
# The list of modifications to make to the CONFIG value in a .pro file.
# An element may start with '-' to specify that the value should be
# removed.
options.append(Option('qmake_CONFIG', option_type=list))
# The list of modifications to make to the QT value in a .pro file. An
# element may start with '-' to specify that the value should be
# removed.
options.append(Option('qmake_QT', option_type=list))
# The list of header files to #include in any internal test program.
options.append(Option('test_headers', option_type=list))
# The statement to execute in any internal test program.
options.append(Option('test_statement'))
return options
def handle_test_output(self, test_output):
""" Handle the output of any external test program and return True if
the bindings are buildable.
"""
# This default implementation assumes that the output is a list of
# disabled features.
if test_output:
self.project.progress(
"Disabled {} bindings features: {}.".format(self.name,
', '.join(test_output)))
self.disabled_features.extend(test_output)
return True
def is_buildable(self):
""" Return True of the bindings are buildable. """
project = self.project
test = 'cfgtest_' + self.name
test_source = test + '.cpp'
test_source_path = os.path.join(project.tests_dir, test_source)
if os.path.isfile(test_source_path):
# There is an external test program that should be run.
run_test = True
elif self.test_statement:
# There is an internal test program that doesn't need to be run.
test_source_path = None
run_test = False
else:
# There is no test program so defer to the super-class.
return super().is_buildable()
self.project.progress(
"Checking to see if the {0} bindings can be built".format(
self.name))
# Create a buildable for the test prgram.
buildable = BuildableExecutable(project, test, self.name)
buildable.builder_settings.extend(self.builder_settings)
buildable.debug = self.debug
buildable.define_macros.extend(self.define_macros)
buildable.include_dirs.extend(self.include_dirs)
buildable.libraries.extend(self.libraries)
buildable.library_dirs.extend(self.library_dirs)
if test_source_path is None:
# Save the internal test to a file.
includes = ['#include <{}>'.format(h) for h in self.test_headers]
source_text = '''%s
int main(int, char **)
{
%s;
}
''' % ('\n'.join(includes), self.test_statement)
test_source_path = os.path.join(buildable.build_dir, test_source)
tf = project.open_for_writing(test_source_path)
tf.write(source_text)
tf.close()
buildable.sources.append(test_source_path)
# Build the test program.
test_exe = project.builder.build_executable(buildable, fatal=False)
if test_exe is None:
return False
# If the test doesn't need to be run then we are done.
if not run_test:
return True
# Run the test and capture the output as a list of lines.
test_exe = os.path.join(buildable.build_dir, test_exe)
# Create the output file, first making sure it doesn't exist. Note
# that we don't use a pipe because we may want a copy of the output for
# debugging purposes.
out_file = os.path.join(buildable.build_dir, test + '.out')
try:
os.remove(out_file)
except OSError:
pass
# Make sure the Qt DLLs get picked up.
original_path = None
if sys.platform == 'win32':
qt_bin_dir = os.path.dirname(project.builder.qmake)
path = os.environ['PATH']
path_parts = path.split(os.path.pathsep)
if qt_bin_dir not in path_parts:
original_path = path
path_parts.insert(0, qt_bin_dir)
os.environ['PATH'] = os.pathsep.join(path_parts)
self.project.run_command([test_exe, out_file], fatal=False)
if original_path is not None:
os.environ['PATH'] = original_path
if not os.path.isfile(out_file):
raise UserException(
"'{0}' didn't create any output".format(test_exe))
# Read the details.
with open(out_file) as f:
test_output = f.read().strip()
test_output = test_output.split('\n') if test_output else []
return self.handle_test_output(test_output)
@staticmethod
def _matching_files(pattern):
""" Return a reproducable list of files that match a pattern. """
return sorted(glob.glob(pattern))
def _update_builder_settings(self, name, modifications):
""" Update the builder settings with a list of modifications to a
value.
"""
add = []
remove = []
for mod in modifications:
if mod.startswith('-'):
remove.append(mod[1:])
else:
add.append(mod)
if add:
self.builder_settings.append(
'{} += {}'.format(name, ' '.join(add)))
if remove:
self.builder_settings.append(
'{} -= {}'.format(name, ' '.join(remove)))
|