File: bindings.py

package info (click to toggle)
pyqt-builder 1.18.1%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 444 kB
  • sloc: python: 2,123; makefile: 18
file content (230 lines) | stat: -rw-r--r-- 7,610 bytes parent folder | download | duplicates (2)
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
# SPDX-License-Identifier: BSD-2-Clause

# Copyright (c) 2024 Phil Thompson <phil@riverbankcomputing.com>


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)))