1313from __future__ import annotations
1414
1515import logging
16+ import os
1617import shutil
18+ import sys
1719import tarfile
1820import tempfile
21+ import urllib .request
1922import zipfile
2023from pathlib import Path
2124from typing import Any
2225
23- import requests
26+ from hatchling . builders . config import BuilderConfig
2427from hatchling .builders .hooks .plugin .interface import BuildHookInterface
2528from packaging .tags import sys_tags
2629
2730logger = logging .getLogger (__name__ )
2831
32+ EXE = ".exe" if os .name == "nt" else ""
2933PKG_DIR = Path (__file__ ).parent .resolve () / "src" / "pact_cli"
30-
31- # Latest version available at:
32- # https://github.com/pact-foundation/pact-ruby-standalone/releases
3334PACT_BIN_URL = "https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v{version}/pact-{version}-{os}-{machine}.{ext}"
3435
3536
@@ -47,15 +48,12 @@ def __init__(self, platform: str) -> None:
4748 super ().__init__ (f"Unsupported platform { platform } " )
4849
4950
50- class PactBuildHook (BuildHookInterface [Any ]):
51+ class PactCliBuildHook (BuildHookInterface [BuilderConfig ]):
5152 """Custom hook to download Pact binaries."""
5253
53- PLUGIN_NAME = "custom"
54- """
55- This is a hard-coded name required by Hatch
56- """
54+ PLUGIN_NAME = "pact-cli"
5755
58- def __init__ (self , * args : Any , ** kwargs : Any ) -> None : # noqa: ANN401
56+ def __init__ (self , * args : object , ** kwargs : object ) -> None :
5957 """
6058 Initialize the build hook.
6159
@@ -64,7 +62,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401
6462 """
6563 super ().__init__ (* args , ** kwargs )
6664 self .tmpdir = Path (tempfile .TemporaryDirectory ().name )
67- self .tmpdir .mkdir (parents = True , exist_ok = True )
6865
6966 def clean (self , versions : list [str ]) -> None : # noqa: ARG002
7067 """Clean up any files created by the build hook."""
@@ -77,51 +74,54 @@ def initialize(
7774 build_data : dict [str , Any ],
7875 ) -> None :
7976 """Hook into Hatchling's build process."""
80- build_data ["infer_tag" ] = True
81- build_data ["pure_python" ] = False
82-
8377 cli_version = "." .join (self .metadata .version .split ("." )[:3 ])
8478 if not cli_version :
8579 self .app .display_error ("Failed to determine Pact CLI version." )
8680
8781 try :
88- self .pact_bin_install (cli_version )
82+ self ._pact_bin_install (cli_version )
8983 except UnsupportedPlatformError as err :
9084 msg = f"Pact CLI is not available for { err .platform } ."
9185 logger .exception (msg , RuntimeWarning , stacklevel = 2 )
9286
93- def pact_bin_install (self , version : str ) -> None :
87+ build_data ["tag" ] = self ._infer_tag ()
88+
89+ def _sys_tag_platform (self ) -> str :
90+ """
91+ Get the platform tag from the current system tags.
92+
93+ This is used to determine the target platform for the Pact binaries.
94+ """
95+ return next (t .platform for t in sys_tags ())
96+
97+ def _pact_bin_install (self , version : str ) -> None :
9498 """
9599 Install the Pact standalone binaries.
96100
97- The binaries are installed in `src/pact /bin`, and the relevant version for
98- the current operating system is determined automatically.
101+ The binaries are installed in `src/pact_cli /bin`, and the relevant
102+ version for the current operating system is determined automatically.
99103
100104 Args:
101- version: The Pact version to install.
105+ version:
106+ The Pact CLI version to install.
102107 """
103108 url = self ._pact_bin_url (version )
104- if url :
105- artifact = self ._download (url )
106- self ._pact_bin_extract (artifact )
109+ artifact = self ._download (url )
110+ self ._pact_bin_extract (artifact )
107111
108- def _pact_bin_url (self , version : str ) -> str | None :
112+ def _pact_bin_url (self , version : str ) -> str :
109113 """
110114 Generate the download URL for the Pact binaries.
111115
112- Generate the download URL for the Pact binaries based on the current
113- platform and specified version. This function mainly contains a lot of
114- matching logic to determine the correct URL to use, due to the
115- inconsistencies in naming conventions between ecosystems.
116-
117116 Args:
118- version: The upstream Pact version.
117+ version:
118+ The Pact CLI version to download.
119119
120120 Returns:
121- The URL to download the Pact binaries from, or None if the current
122- platform is not supported .
121+ The URL to download the Pact binaries from. If the platform is not
122+ supported, the resulting URL may be invalid .
123123 """
124- platform = next ( sys_tags ()). platform
124+ platform = self . _sys_tag_platform ()
125125
126126 if platform .startswith ("macosx" ):
127127 os = "osx"
@@ -161,22 +161,21 @@ def _pact_bin_extract(self, artifact: Path) -> None:
161161 Args:
162162 artifact: The path to the downloaded artifact.
163163 """
164- with tempfile .TemporaryDirectory () as tmpdir :
165- if str (artifact ).endswith (".zip" ):
166- with zipfile .ZipFile (artifact ) as f :
167- f .extractall (tmpdir ) # noqa: S202
168-
169- if str (artifact ).endswith (".tar.gz" ):
170- with tarfile .open (artifact ) as f :
171- f .extractall (tmpdir ) # noqa: S202
172-
173- for d in ["bin" , "lib" ]:
174- if (PKG_DIR / d ).is_dir ():
175- shutil .rmtree (PKG_DIR / d )
176- shutil .copytree (
177- Path (tmpdir ) / "pact" / d ,
178- PKG_DIR / d ,
179- )
164+ if str (artifact ).endswith (".zip" ):
165+ with zipfile .ZipFile (artifact ) as f :
166+ f .extractall (self .tmpdir ) # noqa: S202
167+
168+ if str (artifact ).endswith (".tar.gz" ):
169+ with tarfile .open (artifact ) as f :
170+ f .extractall (self .tmpdir ) # noqa: S202
171+
172+ for d in ["bin" , "lib" ]:
173+ if (PKG_DIR / d ).is_dir ():
174+ shutil .rmtree (PKG_DIR / d )
175+ shutil .copytree (
176+ Path (self .tmpdir ) / "pact" / d ,
177+ PKG_DIR / d ,
178+ )
180179
181180 def _download (self , url : str ) -> Path :
182181 """
@@ -196,13 +195,31 @@ def _download(self, url: str) -> Path:
196195 artifact .parent .mkdir (parents = True , exist_ok = True )
197196
198197 if not artifact .exists ():
199- response = requests .get (url , timeout = 30 )
200- try :
201- response .raise_for_status ()
202- except requests .HTTPError as e :
203- msg = f"Failed to download from { url } ."
204- raise RuntimeError (msg ) from e
205- with artifact .open ("wb" ) as f :
206- f .write (response .content )
198+ urllib .request .urlretrieve (url , artifact ) # noqa: S310
207199
208200 return artifact
201+
202+ def _infer_tag (self ) -> str :
203+ """
204+ Infer the tag for the current build.
205+
206+ Since we have a pure Python wrapper around a binary CLI, we are not
207+ tied to any specific Python version or ABI. As a result, we generate
208+ `py3-none-{platform}` tags for the wheels.
209+ """
210+ platform = self ._sys_tag_platform ()
211+
212+ # On macOS, the version needs to be set based on the deployment target
213+ # (i.e., the version of the system libraries).
214+ if sys .platform == "darwin" and (
215+ deployment_target := os .environ .get ("MACOSX_DEPLOYMENT_TARGET" )
216+ ):
217+ target = deployment_target .replace ("." , "_" )
218+ if platform .endswith ("_arm64" ):
219+ platform = f"macosx_{ target } _arm64"
220+ elif platform .endswith ("_x86_64" ):
221+ platform = f"macosx_{ target } _x86_64"
222+ else :
223+ raise UnsupportedPlatformError (platform )
224+
225+ return f"py3-none-{ platform } "
0 commit comments