pact-python-ffi 0.0.0__tar.gz → 0.4.22.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ src/pact_ffi/data
2
+ src/pact_ffi/__version__.py
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Pact Foundation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: pact-python-ffi
3
+ Version: 0.4.22.0
4
+ Summary: Python bindings for the Pact FFI library
5
+ Project-URL: Bug Tracker, https://github.com/pact-foundation/pact-python/issues
6
+ Project-URL: Changelog, https://github.com/pact-foundation/pact-python/blob/main/pact-python-ffi/CHANGELOG.md
7
+ Project-URL: Documentation, https://docs.pact.io
8
+ Project-URL: Homepage, https://pact.io
9
+ Project-URL: Repository, https://github.com/pact-foundation/pact-python
10
+ Author-email: Joshua Ellis <josh@jpellis.me>
11
+ Maintainer-email: Joshua Ellis <josh@jpellis.me>
12
+ License-Expression: MIT
13
+ License-File: LICENSE
14
+ Keywords: contract-testing,ffi,pact,pact-python
15
+ Classifier: Development Status :: 5 - Production/Stable
16
+ Classifier: Environment :: Console
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Classifier: Operating System :: MacOS :: MacOS X
20
+ Classifier: Operating System :: Microsoft :: Windows
21
+ Classifier: Operating System :: POSIX :: Linux
22
+ Classifier: Programming Language :: Python
23
+ Classifier: Programming Language :: Python :: 3 :: Only
24
+ Classifier: Programming Language :: Python :: 3.9
25
+ Classifier: Programming Language :: Python :: 3.10
26
+ Classifier: Programming Language :: Python :: 3.11
27
+ Classifier: Programming Language :: Python :: 3.12
28
+ Classifier: Programming Language :: Python :: 3.13
29
+ Classifier: Topic :: Software Development :: Testing
30
+ Requires-Python: >=3.9
31
+ Requires-Dist: cffi~=1.0
32
+ Provides-Extra: devel
33
+ Requires-Dist: mypy==1.17.0; extra == 'devel'
34
+ Requires-Dist: pytest-cov~=6.0; extra == 'devel'
35
+ Requires-Dist: pytest-mock~=3.0; extra == 'devel'
36
+ Requires-Dist: pytest~=8.0; extra == 'devel'
37
+ Requires-Dist: ruff==0.12.4; extra == 'devel'
38
+ Provides-Extra: devel-test
39
+ Requires-Dist: pytest-cov~=6.0; extra == 'devel-test'
40
+ Requires-Dist: pytest-mock~=3.0; extra == 'devel-test'
41
+ Requires-Dist: pytest~=8.0; extra == 'devel-test'
42
+ Provides-Extra: devel-types
43
+ Requires-Dist: mypy==1.17.0; extra == 'devel-types'
44
+ Description-Content-Type: text/markdown
45
+
46
+ # Pact Python FFI
47
+
48
+ > [!NOTE]
49
+ >
50
+ > This package provides direct access to the Pact Foreign Function Interface (FFI) with minimal abstraction. It is intended for advanced users who need low-level control over Pact operations in Python.
51
+
52
+ ---
53
+
54
+ This sub-package is part of the [Pact Python](https://github.com/pact-foundation/pact-python) project and exists to expose the [Pact FFI](https://github.com/pact-foundation/pact-reference) directly to Python. If you are looking for the main Pact Python library for contract testing, please see the [root package](https://github.com/pact-foundation/pact-python#pact-python).
55
+
56
+ ## Overview
57
+
58
+ - The module provides a thin Python wrapper around the Pact FFI (C API).
59
+ - Most classes correspond directly to structs from the FFI, and are designed to wrap the underlying C pointers.
60
+ - Many classes implement the `__del__` method to ensure memory allocated by the Rust library is freed when the Python object is destroyed, preventing memory leaks.
61
+ - Functions from the FFI are exposed directly: if a function `foo` exists in the FFI, it is accessible as `pact_ffi.foo(...)`.
62
+ - The API is not guaranteed to be stable and is intended for use by advanced users or for building higher-level libraries. For typical contract testing, use the main Pact Python client library.
63
+
64
+ ## Installation
65
+
66
+ You can install this package via pip:
67
+
68
+ ```console
69
+ pip install pact-python-ffi
70
+ ```
71
+
72
+ ## Usage
73
+
74
+ This package exposes the raw FFI bindings for Pact. It is suitable for advanced use cases, custom integrations, or for building higher-level libraries. For typical contract testing, prefer using the main Pact Python library.
75
+
76
+ ## Contributing
77
+
78
+ As this is a relatively thin wrapper around the Pact FFI, the code is unlikely to change frequently; however, contributions to improve the coverage of the FFI bindings or to improve existing functionality are welcome. See the [main contributing guide](https://github.com/pact-foundation/pact-python/blob/main/CONTRIBUTING.md) for details.
79
+
80
+ To release a new version of `pact-python-ffi`, simply push a tag in the format `pact-python-ffi/x.y.z.w`. This will automatically trigger a release process, pulling in version `x.y.z` of the underlying Pact FFI. Before creating and pushing such a tag, please ensure that the Python wrapper has been updated to reflect any changes or updates in the corresponding FFI version.
81
+
82
+ Higher-level abstractions or utilities should be implemented in separate libraries (such as [`pact-python`](https://github.com/pact-foundation/pact-python)).
83
+
84
+ ---
85
+
86
+ For questions or support, please visit the [Pact Foundation Slack](https://slack.pact.io) or [GitHub Discussions](https://github.com/pact-foundation/pact-python/discussions)
87
+
88
+ ---
@@ -0,0 +1,43 @@
1
+ # Pact Python FFI
2
+
3
+ > [!NOTE]
4
+ >
5
+ > This package provides direct access to the Pact Foreign Function Interface (FFI) with minimal abstraction. It is intended for advanced users who need low-level control over Pact operations in Python.
6
+
7
+ ---
8
+
9
+ This sub-package is part of the [Pact Python](https://github.com/pact-foundation/pact-python) project and exists to expose the [Pact FFI](https://github.com/pact-foundation/pact-reference) directly to Python. If you are looking for the main Pact Python library for contract testing, please see the [root package](https://github.com/pact-foundation/pact-python#pact-python).
10
+
11
+ ## Overview
12
+
13
+ - The module provides a thin Python wrapper around the Pact FFI (C API).
14
+ - Most classes correspond directly to structs from the FFI, and are designed to wrap the underlying C pointers.
15
+ - Many classes implement the `__del__` method to ensure memory allocated by the Rust library is freed when the Python object is destroyed, preventing memory leaks.
16
+ - Functions from the FFI are exposed directly: if a function `foo` exists in the FFI, it is accessible as `pact_ffi.foo(...)`.
17
+ - The API is not guaranteed to be stable and is intended for use by advanced users or for building higher-level libraries. For typical contract testing, use the main Pact Python client library.
18
+
19
+ ## Installation
20
+
21
+ You can install this package via pip:
22
+
23
+ ```console
24
+ pip install pact-python-ffi
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ This package exposes the raw FFI bindings for Pact. It is suitable for advanced use cases, custom integrations, or for building higher-level libraries. For typical contract testing, prefer using the main Pact Python library.
30
+
31
+ ## Contributing
32
+
33
+ As this is a relatively thin wrapper around the Pact FFI, the code is unlikely to change frequently; however, contributions to improve the coverage of the FFI bindings or to improve existing functionality are welcome. See the [main contributing guide](https://github.com/pact-foundation/pact-python/blob/main/CONTRIBUTING.md) for details.
34
+
35
+ To release a new version of `pact-python-ffi`, simply push a tag in the format `pact-python-ffi/x.y.z.w`. This will automatically trigger a release process, pulling in version `x.y.z` of the underlying Pact FFI. Before creating and pushing such a tag, please ensure that the Python wrapper has been updated to reflect any changes or updates in the corresponding FFI version.
36
+
37
+ Higher-level abstractions or utilities should be implemented in separate libraries (such as [`pact-python`](https://github.com/pact-foundation/pact-python)).
38
+
39
+ ---
40
+
41
+ For questions or support, please visit the [Pact Foundation Slack](https://slack.pact.io) or [GitHub Discussions](https://github.com/pact-foundation/pact-python/discussions)
42
+
43
+ ---
@@ -0,0 +1,113 @@
1
+ #:schema https://json.schemastore.org/any.json
2
+ # git-cliff configuration file
3
+ # https://git-cliff.org/docs/configuration
4
+
5
+ [changelog]
6
+ # template for the changelog header
7
+ header = """
8
+ # Pact Python FFI Changelog
9
+
10
+ All notable changes to this project will be documented in this file.
11
+
12
+ Note that this _only_ includes changes to the Python FFI interface. For changes to the Pact FFI itself, see the [Pact FFI changelog](https://github.com/pact-foundation/pact-reference/blob/master/rust/pact_ffi/CHANGELOG.md).
13
+
14
+ <!-- markdownlint-disable no-duplicate-heading -->
15
+ <!-- markdownlint-disable emph-style -->
16
+ <!-- markdownlint-disable strong-style -->
17
+
18
+ """
19
+
20
+ # template for the changelog body
21
+ # https://keats.github.io/tera/docs/#introduction
22
+ body = """
23
+ {% if version %}\
24
+ ## [{{ version | trim_start_matches(pat="v") }}] _{{ timestamp | date(format="%Y-%m-%d") }}_
25
+ {% else %}\
26
+ ## [unreleased]
27
+ {% endif %}\
28
+ {% for group, commits in commits | group_by(attribute="group") %}
29
+ ### {{ group | striptags | trim | upper_first }}
30
+ {% for commit in commits %}
31
+ - {% if commit.scope %}_({{ commit.scope }})_ {% endif %}\
32
+ {% if commit.breaking %}[**breaking**] {% endif %}\
33
+ {{ commit.message | upper_first }}\
34
+ {% if commit.breaking and commit.breaking_description %}
35
+ {{ " " }}\
36
+ > {{
37
+ commit.breaking_description
38
+ | split(pat="\n")
39
+ | join(sep=" ")
40
+ | replace(from=" ", to=" ")
41
+ | replace(from=" ", to=" ")
42
+ | replace(from=" ", to=" ")
43
+ | upper_first
44
+ }}\
45
+ {% endif %}\
46
+ {% endfor %}
47
+ {% endfor %}
48
+ {% if github.contributors %}\
49
+ ### Contributors
50
+ {% for contributor in github.contributors %}\
51
+ {% if contributor.username and contributor.username is ending_with("[bot]") %}{% continue %}{% endif %}
52
+ - @{{ contributor.username }}\
53
+ {% endfor %}
54
+ {% endif %}
55
+
56
+ """
57
+
58
+ # template for the changelog footer
59
+ footer = """\
60
+ <!-- generated by git-cliff on {{ now() | date(format="%Y-%m-%d") }}-->
61
+ """
62
+
63
+ # remove the leading and trailing s
64
+ trim = true
65
+ # postprocessors
66
+ postprocessors = []
67
+ # render body even when there are no releases to process
68
+ # render_always = true
69
+ # output file path
70
+ output = "CHANGELOG.md"
71
+
72
+ [git]
73
+ tag_pattern = "pact-python-ffi/.*"
74
+ # parse the commits based on https://www.conventionalcommits.org
75
+ conventional_commits = true
76
+ # filter out the commits that are not conventional
77
+ filter_unconventional = true
78
+ # process each line of a commit as an individual commit
79
+ split_commits = false
80
+ # regex for preprocessing the commit messages
81
+ commit_preprocessors = [
82
+ # Remove the PR number added by GitHub when merging PR in UI
83
+ { pattern = '\s*\(#([0-9]+)\)$', replace = "" },
84
+ # Check spelling of the commit with https://github.com/crate-ci/typos
85
+ { pattern = '.*', replace_command = 'typos --write-changes -' },
86
+ ]
87
+ # regex for parsing and grouping commits
88
+ commit_parsers = [
89
+ # Ignore deps commits from the changelog
90
+ { message = "^(chore|fix)\\(deps.*\\)", skip = true },
91
+ { message = "^chore: update changelog.*", skip = true },
92
+ # Group commits by type
93
+ { group = "<!-- 00 -->🚀 Features", message = "^feat" },
94
+ { group = "<!-- 01 -->🐛 Bug Fixes", message = "^fix" },
95
+ { group = "<!-- 50 -->🚜 Refactor", message = "^refactor" },
96
+ { group = "<!-- 51 -->⚡ Performance", message = "^perf" },
97
+ { group = "<!-- 52 -->🎨 Styling", message = "^style" },
98
+ { group = "<!-- 60 -->📚 Documentation", message = "^docs" },
99
+ { group = "<!-- 80 -->🧪 Testing", message = "^test" },
100
+ { group = "<!-- 98 -->◀️ Revert", message = "^revert" },
101
+ { group = "<!-- 99 -->⚙️ Miscellaneous Tasks", message = "^chore" },
102
+ { group = "<!-- 99 -->� Other", message = ".*" },
103
+ ]
104
+ # filter out the commits that are not matched by commit parsers
105
+ filter_commits = false
106
+ # sort the tags topologically
107
+ topo_order = false
108
+ # sort the commits inside sections by oldest/newest order
109
+ sort_commits = "oldest"
110
+
111
+ [remote.github]
112
+ owner = "pact-foundation"
113
+ repo = "pact-python"
@@ -0,0 +1,354 @@
1
+ """
2
+ Hatchling build hook for binary downloads.
3
+
4
+ Pact Python is built on top of the Ruby Pact binaries and the Rust Pact library.
5
+ This build script downloads the binaries and library for the current platform
6
+ and installs them in the `pact` directory under `/bin` and `/lib`.
7
+
8
+ The version of the binaries and library can be controlled with the
9
+ `PACT_BIN_VERSION` and `PACT_LIB_VERSION` environment variables. If these are
10
+ not set, a pinned version will be used instead.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import gzip
16
+ import os
17
+ import shutil
18
+ import sys
19
+ import tempfile
20
+ import urllib.request
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ import cffi
25
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
26
+ from packaging.tags import sys_tags
27
+
28
+ PKG_DIR = Path(__file__).parent.resolve() / "src" / "pact_ffi"
29
+ PACT_LIB_URL = "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{version}/{prefix}pact_ffi-{os}-{platform}{suffix}.{ext}"
30
+
31
+
32
+ class UnsupportedPlatformError(RuntimeError):
33
+ """Raised when the current platform is not supported."""
34
+
35
+ def __init__(self, platform: str) -> None:
36
+ """
37
+ Initialize the exception.
38
+
39
+ Args:
40
+ platform: The unsupported platform.
41
+ """
42
+ self.platform = platform
43
+ super().__init__(f"Unsupported platform {platform}")
44
+
45
+
46
+ class PactBuildHook(BuildHookInterface[Any]):
47
+ """Custom hook to download Pact binaries."""
48
+
49
+ PLUGIN_NAME = "pact-ffi"
50
+
51
+ def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401
52
+ """
53
+ Initialize the build hook.
54
+
55
+ For this hook, we additionally define the lib extension based on the
56
+ current platform.
57
+ """
58
+ super().__init__(*args, **kwargs)
59
+ self.tmpdir = Path(tempfile.TemporaryDirectory().name)
60
+ self.tmpdir.mkdir(parents=True, exist_ok=True)
61
+
62
+ def clean(self, versions: list[str]) -> None: # noqa: ARG002
63
+ """Clean up any files created by the build hook."""
64
+ for ffi in (PKG_DIR / "v3").glob("__init__.*"):
65
+ if ffi.suffix in (".so", ".dylib", ".dll", ".a", ".pyd"):
66
+ ffi.unlink()
67
+
68
+ def initialize(
69
+ self,
70
+ version: str, # noqa: ARG002
71
+ build_data: dict[str, Any],
72
+ ) -> None:
73
+ """Hook into Hatchling's build process."""
74
+ ffi_version = ".".join(self.metadata.version.split(".")[:3])
75
+ if not ffi_version:
76
+ self.app.display_error("Failed to determine Pact FFI version.")
77
+
78
+ try:
79
+ build_data["force_include"] = self._install(ffi_version)
80
+ except UnsupportedPlatformError as err:
81
+ msg = f"Pact FFI library is not available for {err.platform}"
82
+ self.app.display_error(msg)
83
+
84
+ self.app.display_debug(f"Wheel artifacts: {build_data['force_include']}")
85
+ build_data["tag"] = self._infer_tag()
86
+
87
+ def _sys_tag_platform(self) -> str:
88
+ """
89
+ Get the platform tag from the current system tags.
90
+
91
+ This is used to determine the target platform for the Pact library.
92
+ """
93
+ return next(t.platform for t in sys_tags())
94
+
95
+ def _install(self, version: str) -> dict[str, str]:
96
+ """
97
+ Install the Pact library binary.
98
+
99
+ This will download the Pact library binary for the current platform and
100
+ build the CFFI bindings for it.
101
+
102
+ Args:
103
+ version: The Pact version to install.
104
+ """
105
+ # Download the Pact library binary and header file
106
+ lib_url = self._lib_url(version)
107
+ header = self._download(lib_url.rsplit("/", 1)[0] + "/pact.h")
108
+ lib = self._extract_lib(self._download(lib_url))
109
+ if lib.suffix == ".dll":
110
+ dll_lib = self._extract_lib(
111
+ self._download(lib_url.replace(".dll.gz", ".dll.lib.gz"))
112
+ )
113
+ else:
114
+ dll_lib = None
115
+
116
+ # Compile the FFI extension
117
+ extension = self._compile(lib, header)
118
+
119
+ # Copy into the package directory, using the ABI3 marking for broad
120
+ # compatibility.
121
+ # NOTE: Windows does _not_ use the version infixation
122
+ extension_name, _, suffix = extension.name.split(".")
123
+ infix = ".abi3" if os.name != "nt" else ""
124
+ extension_dest = f"{extension_name}{infix}.{suffix}"
125
+ shutil.copy(extension, PKG_DIR / extension_dest)
126
+
127
+ if pact_lib_dir := os.getenv("PACT_LIB_DIR"):
128
+ # Copy the library to make it available by other processes (such as
129
+ # the wheel repair).
130
+ dir_path = Path(pact_lib_dir)
131
+ dir_path.mkdir(parents=True, exist_ok=True)
132
+ self.app.display_debug(f"Copying {lib.name} into {dir_path}")
133
+ shutil.copy(lib, dir_path / lib.name)
134
+ if dll_lib:
135
+ self.app.display_debug(f"Copying {dll_lib.name} into {dir_path}")
136
+ shutil.copy(dll_lib, dir_path / dll_lib.name)
137
+
138
+ return {str(extension): f"pact_ffi/{extension_dest}"}
139
+
140
+ def _lib_url(self, version: str) -> str: # noqa: C901, PLR0912
141
+ """
142
+ Generate the download URL for the Pact library.
143
+
144
+ Args:
145
+ version: The upstream Pact version.
146
+
147
+ Returns:
148
+ The URL to download the Pact library from.
149
+
150
+ Raises:
151
+ UnsupportedPlatformError:
152
+ If the current platform is not supported.
153
+ """
154
+ wheel_platform = self._sys_tag_platform()
155
+
156
+ aarch64 = ("_arm64", "_aarch64")
157
+ x86_64 = ("_x86_64", "_amd64")
158
+
159
+ # Simplified platform and architecture detection
160
+ if wheel_platform.startswith("macosx"):
161
+ os, ext = "macos", "dylib.gz"
162
+ prefix = "lib"
163
+ suffix = ""
164
+ if wheel_platform.endswith(aarch64):
165
+ platform = "aarch64"
166
+ elif wheel_platform.endswith(x86_64):
167
+ platform = "x86_64"
168
+ else:
169
+ raise UnsupportedPlatformError(wheel_platform)
170
+
171
+ elif wheel_platform.startswith("musllinux"):
172
+ os, ext = "linux", "a.gz" # MUSL uses static library
173
+ prefix = "lib"
174
+ suffix = "-musl"
175
+ if wheel_platform.endswith(aarch64):
176
+ platform = "aarch64"
177
+ elif wheel_platform.endswith(x86_64):
178
+ platform = "x86_64"
179
+ else:
180
+ raise UnsupportedPlatformError(wheel_platform)
181
+
182
+ elif wheel_platform.startswith("manylinux"):
183
+ os, ext = "linux", "so.gz"
184
+ prefix = "lib"
185
+ suffix = ""
186
+ if wheel_platform.endswith(aarch64):
187
+ platform = "aarch64"
188
+ elif wheel_platform.endswith(x86_64):
189
+ platform = "x86_64"
190
+ else:
191
+ raise UnsupportedPlatformError(wheel_platform)
192
+
193
+ elif wheel_platform.startswith("win"):
194
+ # TODO: Switch to using `dll.gz`
195
+ # https://github.com/python-cffi/cffi/issues/182
196
+ os, ext = "windows", "dll.gz"
197
+ prefix = ""
198
+ suffix = ""
199
+ if wheel_platform.endswith(aarch64):
200
+ platform = "aarch64"
201
+ elif wheel_platform.endswith(x86_64):
202
+ platform = "x86_64"
203
+ else:
204
+ raise UnsupportedPlatformError(wheel_platform)
205
+
206
+ else:
207
+ raise UnsupportedPlatformError(wheel_platform)
208
+
209
+ return PACT_LIB_URL.format(
210
+ version=version,
211
+ prefix=prefix,
212
+ os=os,
213
+ platform=platform,
214
+ suffix=suffix,
215
+ ext=ext,
216
+ )
217
+
218
+ def _extract_lib(self, artifact: Path) -> Path:
219
+ """
220
+ Extract the Pact library.
221
+
222
+ Args:
223
+ artifact: The URL to download the Pact binaries from.
224
+ """
225
+ target = PKG_DIR / (artifact.name.split("-")[0] + artifact.suffixes[-2])
226
+ with (
227
+ gzip.open(artifact, "rb") as f_in,
228
+ target.open("wb") as f_out,
229
+ ):
230
+ shutil.copyfileobj(f_in, f_out)
231
+ self.app.display_debug(f"Extracted Pact library to {target}")
232
+ return target
233
+
234
+ def _compile(self, lib: Path, header: Path) -> Path:
235
+ """
236
+ Build the CFFI bindings for the Pact library.
237
+
238
+ Args:
239
+ lib:
240
+ The path to the Pact library binary.
241
+
242
+ header:
243
+ The path to the Pact library header file.
244
+ """
245
+ if os.name == "nt":
246
+ extra_libs = [
247
+ "advapi32",
248
+ "bcrypt",
249
+ "crypt32",
250
+ "iphlpapi",
251
+ "ncrypt",
252
+ "netapi32",
253
+ "ntdll",
254
+ "ole32",
255
+ "oleaut32",
256
+ "pdh",
257
+ "powrprof",
258
+ "psapi",
259
+ "secur32",
260
+ "shell32",
261
+ "user32",
262
+ "userenv",
263
+ "ws2_32",
264
+ ]
265
+ else:
266
+ extra_libs = []
267
+
268
+ ffibuilder = cffi.FFI()
269
+ ffibuilder.cdef(
270
+ "\n".join(
271
+ line
272
+ for line in header.read_text().splitlines()
273
+ if not line.strip().startswith("#")
274
+ )
275
+ )
276
+
277
+ linker_args: list[str] = []
278
+ if os.name == "posix":
279
+ linker_args.append(f"-Wl,-rpath,{lib.parent}")
280
+ elif os.name == "nt":
281
+ # Windows has no equivalent to rpath, instead, the end-user must
282
+ # ensure that the PATH environment variable is updated to include
283
+ # the directory containing the Pact library.
284
+ self.app.display_warning(
285
+ "On Windows, ensure that the PATH environment variable includes "
286
+ f"{lib.parent} to load the Pact library at runtime."
287
+ )
288
+
289
+ ffibuilder.set_source(
290
+ "ffi",
291
+ header.read_text(),
292
+ libraries=["pact_ffi", *extra_libs],
293
+ library_dirs=[str(lib.parent)],
294
+ extra_link_args=linker_args,
295
+ )
296
+ extension = Path(ffibuilder.compile(verbose=True, tmpdir=str(self.tmpdir)))
297
+ self.app.display_debug(f"Compiled CFFI bindings to {extension}")
298
+ return extension
299
+
300
+ def _download(self, url: str) -> Path:
301
+ """
302
+ Download the target URL.
303
+
304
+ This will download the target URL to the `pact/data` directory. If the
305
+ download artifact is already present, its path will be returned.
306
+
307
+ If `extract` is True, the downloaded artifact will be extracted and the
308
+ path to the extract file will be returned instead.
309
+
310
+ Args:
311
+ url: The URL to download
312
+ extract: Whether to extract the downloaded artifact.
313
+
314
+ Return:
315
+ The path to the downloaded artifact.
316
+ """
317
+ filename = url.split("/")[-1]
318
+ artifact = PKG_DIR / "data" / filename
319
+ artifact.parent.mkdir(parents=True, exist_ok=True)
320
+
321
+ if not artifact.exists():
322
+ self.app.display_debug(f"Downloading {url} to {artifact}")
323
+ urllib.request.urlretrieve(url, artifact) # noqa: S310
324
+ else:
325
+ self.app.display_debug(f"Using cached artifact {artifact}")
326
+
327
+ return artifact
328
+
329
+ def _infer_tag(self) -> str:
330
+ """
331
+ Infer the tag for the current build.
332
+
333
+ The bindings are built to target ABI3, which is compatible with multiple
334
+ Python versions. As a result, we generate `py3-abi3-{platform}` tags for
335
+ the wheels.
336
+ """
337
+ python_version = f"{sys.version_info.major}{sys.version_info.minor}"
338
+
339
+ platform = self._sys_tag_platform()
340
+
341
+ # On macOS, the version needs to be set based on the deployment target
342
+ # (i.e., the version of the system libraries).
343
+ if sys.platform == "darwin" and (
344
+ deployment_target := os.getenv("MACOSX_DEPLOYMENT_TARGET")
345
+ ):
346
+ target = deployment_target.replace(".", "_")
347
+ if platform.endswith("_arm64"):
348
+ platform = f"macosx_{target}_arm64"
349
+ elif platform.endswith("_x86_64"):
350
+ platform = f"macosx_{target}_x86_64"
351
+ else:
352
+ raise UnsupportedPlatformError(platform)
353
+
354
+ return f"cp{python_version}-abi3-{platform}"