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