22import zipfile
33from pathlib import Path
44from platform import machine , system
5+ from shutil import rmtree
56from stat import S_IXGRP , S_IXOTH , S_IXUSR
67from subprocess import PIPE , Popen
78from typing import Callable
@@ -28,15 +29,14 @@ def download_file(url: str, directory_path: Path | str, force: bool = False) ->
2829 Path of the downloaded file.
2930 """
3031 directory_path = Path (directory_path ) # Ensure directory_path is a Path object
31- response = requests .get (url , stream = True )
32- response .raise_for_status () # Raise an error for bad responses
33-
3432 directory_path .mkdir (parents = True , exist_ok = True )
35-
3633 filename = url .split ("/" )[- 1 ] # Extract the filename from the URL
3734 file_path = directory_path / filename
3835
3936 if not file_path .exists () or force :
37+ response = requests .get (url , stream = True )
38+ response .raise_for_status () # Raise an error for bad responses
39+
4040 print (f"Downloading { url } to { file_path } ..." )
4141 with open (file_path , "wb" ) as file :
4242 for chunk in response .iter_content (chunk_size = 8192 ):
@@ -45,7 +45,7 @@ def download_file(url: str, directory_path: Path | str, force: bool = False) ->
4545 return file_path
4646
4747
48- def extract_archive (archive_path : Path | str , extract_path : Path | str ) -> Path : # noqa: PLR0912
48+ def extract_archive (archive_path : Path | str , extract_path : Path | str ) -> tuple [ Path , bool ] : # noqa: PLR0912
4949 """
5050 Extracts a compressed archive to the specified directory.
5151
@@ -94,10 +94,15 @@ def extract_archive(archive_path: Path | str, extract_path: Path | str) -> Path:
9494 if top_item .is_directory :
9595 extracted_parent_directory = extract_path / top_item .filename
9696
97+ extracted = False
9798 if not extracted_parent_directory .exists () or not any (extracted_parent_directory .iterdir ()):
9899 print (f"Extracting { archive_path } to { extract_path } ..." )
99- archive .extractall (path = extract_path )
100- return extracted_parent_directory
100+ if isinstance (archive , tarfile .TarFile ):
101+ archive .extractall (path = extract_path , filter = "data" )
102+ else :
103+ archive .extractall (path = extract_path )
104+ extracted = True
105+ return extracted_parent_directory , extracted
101106
102107
103108def make_executable (file_path : str | Path ) -> None :
@@ -107,6 +112,12 @@ def make_executable(file_path: str | Path) -> None:
107112 file_path .chmod (current_permissions | S_IXUSR | S_IXGRP | S_IXOTH )
108113
109114
115+ def remove_directory (directory_path : str | Path ) -> None :
116+ directory_path = Path (directory_path )
117+ if directory_path .exists ():
118+ rmtree (directory_path )
119+
120+
110121class BinaryVersion :
111122 def __init__ (self , version : str , parent : "BinaryManager" ):
112123 self .parent = parent
@@ -118,23 +129,27 @@ def __init__(self, version: str, parent: "BinaryManager"):
118129 package_name = self .parent .package_name ,
119130 )
120131 self .archive_name = self .url .split ("/" )[- 1 ]
121- self .archive_path = download_file (self .url , self .parent ._downloads_directory )
122- self .binary_directory_path = (
123- extract_archive (self .archive_path , self .parent ._package_directory / self .version )
124- / self .parent ._extracted_bin_path
125- )
126-
127- def call (self , command : str , * args : str ) -> str :
132+ self .archive_path = download_file (self .url , self .parent ._archive_directory )
133+ self .extraction_directory_path = self .parent ._package_directory / self .version
134+ extracted_path , extracted = extract_archive (self .archive_path , self .extraction_directory_path )
135+ self .binary_directory_path = extracted_path / self .parent ._extracted_bin_path
136+ if not self .binary_directory_path .exists ():
137+ raise FileNotFoundError (f"Binary directory path '{ self .binary_directory_path } ' does not exist." )
138+ if extracted :
139+ for call in self .parent ._post_extraction_calls :
140+ command , args = call
141+ print (self .call (command , args , self .binary_directory_path ))
142+
143+ def get_binary_path (self , command : str | None = None ) -> Path :
128144 """
129- Calls the binary with the specified command and arguments .
145+ Returns the path to the binary for the specified command.
130146
131147 Args:
132- command (str): The command to execute.
133- *args (str): Additional arguments for the command.
148+ command (str): The command to get the binary path for.
134149
135- Returns:
136- str: The output of the command.
137150 """
151+ if command is None :
152+ command = self .parent .package_name
138153 if self .parent ._system == "windows" :
139154 binary_path_exe = self .binary_directory_path / f"{ command } .exe"
140155 binary_path_bat = self .binary_directory_path / f"{ command } .bat"
@@ -144,33 +159,74 @@ def call(self, command: str, *args: str) -> str:
144159 command = f"{ command } .bat"
145160 binary_path = self .binary_directory_path / command
146161 if not binary_path .exists ():
147- raise FileNotFoundError (f"Binary { binary_path } does not exist." )
162+ raise FileNotFoundError (f"Binary '{ binary_path } ' does not exist." )
163+ return binary_path
164+
165+ def call (
166+ self , command : str | None = None , arguments : list [str ] | None = None , working_directory : Path | str = Path ("." )
167+ ) -> str :
168+ """
169+ Calls the binary with the specified command and arguments.
170+
171+ Args:
172+ command (str): The command to execute.
173+ *args (str): Additional arguments for the command.
174+
175+ Returns:
176+ str: The output of the command.
177+ """
178+ working_directory = Path (working_directory )
179+ if command is None :
180+ command = self .parent .package_name
181+ binary_path = self .get_binary_path (command )
148182
149183 make_executable (binary_path )
150184
151185 creation_flag = (
152186 0x08000000 if self .parent ._system == "windows" else 0
153187 ) # set creation flag to not open in new console on windows
154- process = Popen ([str (binary_path ), * args ], stdout = PIPE , stderr = PIPE , creationflags = creation_flag )
188+
189+ if arguments is None :
190+ arguments = []
191+ process = Popen (
192+ [str (binary_path )] + arguments , stdout = PIPE , stderr = PIPE , creationflags = creation_flag , cwd = working_directory
193+ )
155194 stdout , stderr = process .communicate ()
156195 if process .returncode != 0 :
157- raise RuntimeError (f"Command failed with error: { stderr .decode ('utf-8' )} " )
196+ raise RuntimeError (
197+ f"Command '{ binary_path } { ' ' .join (arguments )} ' failed with error:\n "
198+ f"{ stderr .decode ('utf-8' )} \n { stdout .decode ('utf-8' )} "
199+ )
158200 return stdout .decode ("utf-8" )
159201
160- # TODO: def clear_cache(self) -> None:
202+ def clear_cache (self ) -> None :
203+ """
204+ Clears the cache for the version.
205+ """
206+ remove_directory (self .extraction_directory_path )
207+ self .archive_path .unlink (missing_ok = True )
208+
209+
210+ def default_platform_string (system : str , architecture : str ) -> str :
211+ return f"{ system } -{ architecture } "
212+
213+
214+ def default_extracted_bin_path (system : str , architecture : str ) -> str :
215+ return "bin"
161216
162217
163218class BinaryManager :
164- def __init__ (
219+ def __init__ ( # noqa: PLR0913
165220 self ,
166221 package_name : str ,
167222 url_pattern : str ,
168223 get_archive_extension : Callable [[str ], str ], # returns archive extension based on system
169- get_platform_string : Callable [[ str , str ], str ] = lambda system ,
170- architecture : f" { system } - { architecture } " , # returns platform string used in url_pattern
171- get_extracted_bin_path : Callable [[ str , str ], str ] = lambda system ,
172- architecture : "bin" , # returns extracted bin path
224+ get_platform_string : Callable [
225+ [ str , str ], str
226+ ] = default_platform_string , # returns platform string used in url_pattern
227+ get_extracted_bin_path : Callable [[ str , str ], str ] = default_extracted_bin_path , # returns extracted bin path
173228 cache_directory : Path | str | None = None ,
229+ post_extraction_calls : list [tuple [str , list [str ]]] | None = None ,
174230 ):
175231 self .package_name = package_name
176232 self .url_pattern = url_pattern
@@ -179,6 +235,10 @@ def __init__(
179235 self ._platform_string = get_platform_string (self ._system , self ._architecture )
180236 self ._extension = get_archive_extension (self ._system )
181237 self ._extracted_bin_path = get_extracted_bin_path (self ._system , self ._architecture )
238+ if post_extraction_calls is None :
239+ self ._post_extraction_calls = []
240+ else :
241+ self ._post_extraction_calls = post_extraction_calls
182242
183243 self ._cache_directory : Path
184244 if cache_directory is None :
@@ -189,7 +249,7 @@ def __init__(
189249 else :
190250 self ._cache_directory = Path (cache_directory )
191251 self ._downloads_directory = self ._cache_directory / "downloads"
192- self ._archive_directory = self ._cache_directory / self .package_name
252+ self ._archive_directory = self ._downloads_directory / self .package_name
193253 self ._packages_directory = self ._cache_directory / "packages"
194254 self ._package_directory = self ._packages_directory / self .package_name
195255 self ._versions : dict [str , BinaryVersion ] = {}
@@ -208,4 +268,19 @@ def get_version(self, version: str) -> BinaryVersion:
208268 self ._versions [version ] = BinaryVersion (version , self )
209269 return self ._versions [version ]
210270
211- # TODO: def clear_cache(self) -> None:
271+ def add_post_extraction_call (self , command : str , args : list [str ]) -> None :
272+ """
273+ Adds a post-extraction call to the manager.
274+
275+ Args:
276+ command (str): The command to execute.
277+ args (list[str]): Additional arguments for the command.
278+ """
279+ self ._post_extraction_calls .append ((command , args ))
280+
281+ def clear_cache (self ) -> None :
282+ """
283+ Clears the cache for the package manager.
284+ """
285+ remove_directory (self ._archive_directory )
286+ remove_directory (self ._package_directory )
0 commit comments