pact-python-ffi 0.5.10.0__tar.gz → 0.5.12.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.
@@ -1,3 +1,4 @@
1
+ src/pact_ffi/__version__.py
1
2
  src/pact_ffi/data
2
3
  src/pact_ffi/*.pyd
3
4
  src/pact_ffi/*.so
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pact-python-ffi
3
- Version: 0.5.10.0
3
+ Version: 0.5.12.0
4
4
  Summary: Python bindings for the Pact FFI library
5
5
  Project-URL: Bug Tracker, https://github.com/pact-foundation/pact-python/issues
6
6
  Project-URL: Changelog, https://github.com/pact-foundation/pact-python/blob/main/pact-python-ffi/CHANGELOG.md
@@ -7,13 +7,15 @@ CFFI bindings for it.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- import gzip
10
+ import hashlib
11
11
  import os
12
12
  import shutil
13
13
  import subprocess
14
14
  import sys
15
+ import tarfile
15
16
  import tempfile
16
17
  import urllib.request
18
+ import zipfile
17
19
  from pathlib import Path
18
20
  from typing import TYPE_CHECKING, Any
19
21
 
@@ -25,7 +27,7 @@ if TYPE_CHECKING:
25
27
  from collections.abc import Mapping, MutableMapping, Sequence
26
28
 
27
29
  PKG_DIR = Path(__file__).parent.resolve() / "src" / "pact_ffi"
28
- PACT_LIB_URL = "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{version}/{prefix}pact_ffi-{os}-{platform}{suffix}.{ext}"
30
+ PACT_RELEASE_URL = "https://github.com/pact-foundation/pact-reference/releases/download/pact_ffi-v{version}"
29
31
 
30
32
 
31
33
  class UnsupportedPlatformError(RuntimeError):
@@ -169,15 +171,30 @@ class PactBuildHook(BuildHookInterface[Any]):
169
171
  `dst` is the corresponding path within the wheel.
170
172
  """
171
173
  # Download the Pact library binary and header file
172
- lib_url = self._lib_url(version)
173
- header = self._download(lib_url.rsplit("/", 1)[0] + "/pact.h")
174
- lib = self._extract_lib(self._download(lib_url))
175
- if lib.suffix == ".dll":
176
- dll_lib = self._extract_lib(
177
- self._download(lib_url.replace(".dll.gz", ".dll.lib.gz"))
178
- )
174
+ release_url = PACT_RELEASE_URL.format(version=version)
175
+ target = self._target()
176
+ header = self._download(f"{release_url}/pact.h")
177
+ if "windows" in target:
178
+ archive_url = f"{release_url}/pact_ffi-{target}.zip"
179
+ else:
180
+ archive_url = f"{release_url}/pact_ffi-{target}.tar.xz"
181
+ archive = self._download(archive_url)
182
+ self._verify(archive, self._download(f"{archive_url}.sha256"))
183
+
184
+ dll_lib: Path | None = None
185
+ if "windows" in target:
186
+ lib = self._extract_lib(archive, "pact_ffi.dll", "pact_ffi.dll")
187
+ # MSVC resolves `pact_ffi` to `pact_ffi.lib`. The archive's own
188
+ # `pact_ffi.lib` is the static library, so the import library
189
+ # takes its name.
190
+ dll_lib = self._extract_lib(archive, "pact_ffi.dll.lib", "pact_ffi.lib")
191
+ elif "darwin" in target:
192
+ lib = self._extract_lib(archive, "libpact_ffi.dylib", "libpact_ffi.dylib")
193
+ elif "musl" in target:
194
+ # MUSL uses static library
195
+ lib = self._extract_lib(archive, "libpact_ffi.a", "libpact_ffi.a")
179
196
  else:
180
- dll_lib = None
197
+ lib = self._extract_lib(archive, "libpact_ffi.so", "libpact_ffi.so")
181
198
 
182
199
  # Compile the FFI extension
183
200
  extension = self._compile(lib, header)
@@ -204,16 +221,12 @@ class PactBuildHook(BuildHookInterface[Any]):
204
221
 
205
222
  return {str(extension): f"pact_ffi/{extension_dest}"}
206
223
 
207
- def _lib_url(self, version: str) -> str: # noqa: C901, PLR0912
224
+ def _target(self) -> str:
208
225
  """
209
- Generate the download URL for the Pact library.
210
-
211
- Args:
212
- version:
213
- The upstream Pact version.
226
+ Determine the Rust target triple matching the wheel platform.
214
227
 
215
228
  Returns:
216
- The URL to download the Pact library from.
229
+ The Rust target triple of the upstream Pact library archive.
217
230
 
218
231
  Raises:
219
232
  UnsupportedPlatformError:
@@ -221,83 +234,96 @@ class PactBuildHook(BuildHookInterface[Any]):
221
234
  """
222
235
  wheel_platform = self._sys_tag_platform()
223
236
 
224
- aarch64 = ("_arm64", "_aarch64")
225
- x86_64 = ("_x86_64", "_amd64")
237
+ if wheel_platform.endswith(("_arm64", "_aarch64")):
238
+ arch = "aarch64"
239
+ elif wheel_platform.endswith(("_x86_64", "_amd64")):
240
+ arch = "x86_64"
241
+ else:
242
+ raise UnsupportedPlatformError(wheel_platform)
226
243
 
227
- # Simplified platform and architecture detection
228
244
  if wheel_platform.startswith("macosx"):
229
- os, ext = "macos", "dylib.gz"
230
- prefix = "lib"
231
- suffix = ""
232
- if wheel_platform.endswith(aarch64):
233
- platform = "aarch64"
234
- elif wheel_platform.endswith(x86_64):
235
- platform = "x86_64"
236
- else:
237
- raise UnsupportedPlatformError(wheel_platform)
238
-
239
- elif wheel_platform.startswith("musllinux"):
240
- os, ext = "linux", "a.gz" # MUSL uses static library
241
- prefix = "lib"
242
- suffix = "-musl"
243
- if wheel_platform.endswith(aarch64):
244
- platform = "aarch64"
245
- elif wheel_platform.endswith(x86_64):
246
- platform = "x86_64"
247
- else:
248
- raise UnsupportedPlatformError(wheel_platform)
249
-
250
- elif wheel_platform.startswith("manylinux"):
251
- os, ext = "linux", "so.gz"
252
- prefix = "lib"
253
- suffix = ""
254
- if wheel_platform.endswith(aarch64):
255
- platform = "aarch64"
256
- elif wheel_platform.endswith(x86_64):
257
- platform = "x86_64"
258
- else:
259
- raise UnsupportedPlatformError(wheel_platform)
260
-
261
- elif wheel_platform.startswith("win"):
262
- # TODO: Switch to using `dll.gz`
263
- # https://github.com/python-cffi/cffi/issues/182
264
- os, ext = "windows", "dll.gz"
265
- prefix = ""
266
- suffix = ""
267
- if wheel_platform.endswith(aarch64):
268
- platform = "aarch64"
269
- elif wheel_platform.endswith(x86_64):
270
- platform = "x86_64"
271
- else:
272
- raise UnsupportedPlatformError(wheel_platform)
245
+ return f"{arch}-apple-darwin"
246
+ if wheel_platform.startswith("musllinux"):
247
+ return f"{arch}-unknown-linux-musl"
248
+ if wheel_platform.startswith("manylinux"):
249
+ return f"{arch}-unknown-linux-gnu"
250
+ if wheel_platform.startswith("win"):
251
+ return f"{arch}-pc-windows-msvc"
252
+ raise UnsupportedPlatformError(wheel_platform)
253
+
254
+ def _verify(self, artefact: Path, checksum: Path) -> None:
255
+ """
256
+ Verify an artefact against its SHA-256 checksum file.
273
257
 
274
- else:
275
- raise UnsupportedPlatformError(wheel_platform)
258
+ A mismatched artefact is removed so that the next build downloads it
259
+ again.
276
260
 
277
- return PACT_LIB_URL.format(
278
- version=version,
279
- prefix=prefix,
280
- os=os,
281
- platform=platform,
282
- suffix=suffix,
283
- ext=ext,
284
- )
261
+ Args:
262
+ artefact:
263
+ The path to the downloaded artefact.
264
+
265
+ checksum:
266
+ The path to the checksum file, in the `sha256sum` format
267
+ `<hex> *<filename>`.
285
268
 
286
- def _extract_lib(self, artefact: Path) -> Path:
269
+ Raises:
270
+ ValueError:
271
+ If the checksum does not match.
287
272
  """
288
- Extract the Pact library.
273
+ expected = checksum.read_text().split()[0].lower()
274
+ digest = hashlib.sha256()
275
+ with artefact.open("rb") as f:
276
+ for chunk in iter(lambda: f.read(1 << 20), b""):
277
+ digest.update(chunk)
278
+ if digest.hexdigest() != expected:
279
+ artefact.unlink()
280
+ checksum.unlink()
281
+ msg = (
282
+ f"Checksum mismatch for {artefact.name}: "
283
+ f"expected {expected}, got {digest.hexdigest()}"
284
+ )
285
+ self.app.display_error(msg)
286
+ raise ValueError(msg)
287
+ self.app.display_debug(f"Verified checksum of {artefact.name}")
288
+
289
+ def _extract_lib(self, archive: Path, member: str, name: str) -> Path:
290
+ """
291
+ Extract a file from the Pact library archive.
292
+
293
+ The `.tar.xz` archives nest their files under a `pact_ffi-<target>`
294
+ directory, while the `.zip` archives keep them at the root.
289
295
 
290
296
  Args:
291
- artefact:
292
- The URL to download the Pact binaries from.
297
+ archive:
298
+ The path to the downloaded archive.
299
+
300
+ member:
301
+ The file name within the archive.
302
+
303
+ name:
304
+ The file name to extract to within the package directory.
305
+
306
+ Returns:
307
+ The path to the extracted file.
293
308
  """
294
- target = PKG_DIR / (artefact.name.split("-")[0] + artefact.suffixes[-2])
295
- with (
296
- gzip.open(artefact, "rb") as f_in,
297
- target.open("wb") as f_out,
298
- ):
299
- shutil.copyfileobj(f_in, f_out)
300
- self.app.display_debug(f"Extracted Pact library to {target}")
309
+ target = PKG_DIR / name
310
+ if archive.suffix == ".zip":
311
+ with (
312
+ zipfile.ZipFile(archive) as zf,
313
+ zf.open(member) as f_in,
314
+ target.open("wb") as f_out,
315
+ ):
316
+ shutil.copyfileobj(f_in, f_out)
317
+ else:
318
+ prefix = archive.name.removesuffix(".tar.xz")
319
+ with tarfile.open(archive, "r:xz") as tf:
320
+ f_tar = tf.extractfile(f"{prefix}/{member}")
321
+ if f_tar is None:
322
+ msg = f"{member} in {archive.name} is not a regular file"
323
+ raise ValueError(msg)
324
+ with f_tar, target.open("wb") as f_out:
325
+ shutil.copyfileobj(f_tar, f_out)
326
+ self.app.display_debug(f"Extracted {member} from {archive.name} to {target}")
301
327
  return target
302
328
 
303
329
  def _compile(self, lib: Path, header: Path) -> Path:
@@ -386,46 +412,36 @@ class PactBuildHook(BuildHookInterface[Any]):
386
412
  """
387
413
  Patch the upstream header so that CFFI can compile it.
388
414
 
389
- The upstream `pact.h` emits the callback parameters of
390
- `pactffi_register_plugin_log_callback` and
391
- `pactffi_register_log_callback` as opaque structs passed by value, which
392
- cannot be compiled. The Rust signatures are `Option<extern "C" fn(...)>`,
393
- whose ABI is a nullable function pointer. See
394
- https://github.com/pact-foundation/pact-reference/issues/544.
415
+ The upstream `pact.h` emits the callback parameter of
416
+ `pactffi_register_log_callback` as an opaque struct passed by value,
417
+ which cannot be compiled. The Rust signature is
418
+ `Option<extern "C" fn(...)>`, whose ABI is a nullable function pointer.
419
+ See https://github.com/pact-foundation/pact-reference/issues/544.
395
420
 
396
421
  Args:
397
422
  source:
398
423
  The contents of the upstream `pact.h`.
399
424
 
400
425
  Returns:
401
- The header contents with the callbacks declared as function
402
- pointers.
426
+ The header contents with the callback declared as a function
427
+ pointer.
403
428
  """
404
- for callback, register, first_arg in (
405
- (
406
- "PluginLogCallback",
407
- "pactffi_register_plugin_log_callback",
408
- "plugin_instance_id",
409
- ),
410
- ("LogCallback", "pactffi_register_log_callback", "log_id"),
411
- ):
412
- opaque = f"typedef struct Option_{callback} Option_{callback};"
413
- if opaque not in source:
414
- continue
415
- self.app.display_debug(f"Patching Option_{callback} in pact.h")
416
- source = source.replace(
417
- opaque,
418
- f"typedef void (*{callback})("
419
- f"const char *{first_arg}, "
420
- "const char *test_run_id, "
421
- "const char *level, "
422
- "const char *target, "
423
- "const char *message);",
424
- ).replace(
425
- f"void {register}(struct Option_{callback} callback);",
426
- f"void {register}({callback} callback);",
427
- )
428
- return source
429
+ opaque = "typedef struct Option_LogCallback Option_LogCallback;"
430
+ if opaque not in source:
431
+ return source
432
+ self.app.display_debug("Patching Option_LogCallback in pact.h")
433
+ return source.replace(
434
+ opaque,
435
+ "typedef void (*LogCallback)("
436
+ "const char *log_id, "
437
+ "const char *test_run_id, "
438
+ "const char *level, "
439
+ "const char *target, "
440
+ "const char *message);",
441
+ ).replace(
442
+ "void pactffi_register_log_callback(struct Option_LogCallback callback);",
443
+ "void pactffi_register_log_callback(LogCallback callback);",
444
+ )
429
445
 
430
446
  def _download(self, url: str) -> Path:
431
447
  """