re1sid-lib 0.1.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.
- re1sid_lib-0.1.0/PKG-INFO +12 -0
- re1sid_lib-0.1.0/README.md +0 -0
- re1sid_lib-0.1.0/pyproject.toml +21 -0
- re1sid_lib-0.1.0/src/re1sid_lib/__init__.py +4 -0
- re1sid_lib-0.1.0/src/re1sid_lib/common.py +10 -0
- re1sid_lib-0.1.0/src/re1sid_lib/downloader.py +82 -0
- re1sid_lib-0.1.0/src/re1sid_lib/patcher.py +346 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: re1sid-lib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: 09u2h4n
|
|
6
|
+
Author-email: 09u2h4n <09u2h4n.y1lm42@gmail.com>
|
|
7
|
+
Requires-Dist: httpx>=0.28.1
|
|
8
|
+
Requires-Dist: lxml>=6.1.1
|
|
9
|
+
Requires-Dist: pyaxmlparser>=0.3.31
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "re1sid-lib"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "09u2h4n", email = "09u2h4n.y1lm42@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"httpx>=0.28.1",
|
|
12
|
+
"lxml>=6.1.1",
|
|
13
|
+
"pyaxmlparser>=0.3.31",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
re1sid-lib = "re1sid_lib:main"
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["uv_build>=0.11.7,<0.12.0"]
|
|
21
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
# Directory where downloaded ReVanced resources (CLI jar + patches bundle) live.
|
|
4
|
+
RESOURCE_DIR = os.path.join(os.getcwd(), ".revanced_res")
|
|
5
|
+
|
|
6
|
+
PATCHES_PATH = os.path.join(RESOURCE_DIR, "patches.rvp")
|
|
7
|
+
CLI_PATH = os.path.join(RESOURCE_DIR, "revanced-cli.jar")
|
|
8
|
+
|
|
9
|
+
# Where patched APKs get written by default.
|
|
10
|
+
OUTPUT_DIR = os.path.join(os.getcwd(), "output")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from typing import Generator, Optional, Union
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
from lxml import html
|
|
6
|
+
|
|
7
|
+
from common import PATCHES_PATH, CLI_PATH
|
|
8
|
+
|
|
9
|
+
class Downloader:
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
self.PATCHES_PATH = PATCHES_PATH
|
|
12
|
+
self.CLI_PATH = CLI_PATH
|
|
13
|
+
self.api_url = "https://api.revanced.app/v5/"
|
|
14
|
+
|
|
15
|
+
def __download_common(self, url: str, save: bool = True, filename: Optional[str] = None,
|
|
16
|
+
chunk_size: int = 8192) -> Union[bool, Generator[bytes, None, None]]:
|
|
17
|
+
if filename is None:
|
|
18
|
+
raise ValueError("Filename must be provided when save is True.")
|
|
19
|
+
else:
|
|
20
|
+
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
|
21
|
+
if save:
|
|
22
|
+
with httpx.stream("GET", url, follow_redirects=True) as response:
|
|
23
|
+
response.raise_for_status()
|
|
24
|
+
with open(filename, "wb") as f:
|
|
25
|
+
for chunk in response.iter_bytes(chunk_size=chunk_size):
|
|
26
|
+
f.write(chunk)
|
|
27
|
+
return True
|
|
28
|
+
else:
|
|
29
|
+
# Return an inner generator to keep the outer function's execution immediate when save=True
|
|
30
|
+
def chunk_generator() -> Generator[bytes, None, None]:
|
|
31
|
+
with httpx.stream("GET", url, follow_redirects=True) as response:
|
|
32
|
+
response.raise_for_status()
|
|
33
|
+
for chunk in response.iter_bytes(chunk_size=chunk_size):
|
|
34
|
+
yield chunk
|
|
35
|
+
|
|
36
|
+
return chunk_generator()
|
|
37
|
+
|
|
38
|
+
def download_patches_rvp(self, save: bool = True, chunk_size: int = 8192) -> Union[bool, Generator[bytes, None, None]]:
|
|
39
|
+
filename = self.PATCHES_PATH
|
|
40
|
+
url = f"{self.api_url}patches.rvp"
|
|
41
|
+
return self.__download_common(url, save=save, filename=filename, chunk_size=chunk_size)
|
|
42
|
+
|
|
43
|
+
def download_cli(self, save: bool = True, chunk_size: int = 8192) -> Union[bool, Generator[bytes, None, None]]:
|
|
44
|
+
filename = self.CLI_PATH
|
|
45
|
+
api_url = "https://api.github.com/repos/ReVanced/revanced-cli/releases/latest"
|
|
46
|
+
|
|
47
|
+
response = httpx.get(api_url, follow_redirects=True)
|
|
48
|
+
|
|
49
|
+
if response.status_code == 403:
|
|
50
|
+
repo_url = "https://github.com/ReVanced/revanced-cli/releases/latest"
|
|
51
|
+
response = httpx.get(repo_url, follow_redirects=True)
|
|
52
|
+
download_url = f"https://github.com/ReVanced/revanced-cli/releases/expanded_assets/{str(response.url).split("/")[-1]}"
|
|
53
|
+
response = httpx.get(download_url, follow_redirects=True)
|
|
54
|
+
html_content = html.fromstring(response.text)
|
|
55
|
+
url = f"https://github.com{html_content.xpath('/html/body/div/ul/li[1]/div[1]/a/@href')[0]}"
|
|
56
|
+
response.raise_for_status()
|
|
57
|
+
else:
|
|
58
|
+
response = response.json()
|
|
59
|
+
assets = response.get("assets", [])
|
|
60
|
+
url = None
|
|
61
|
+
for asset in assets:
|
|
62
|
+
name = asset.get("name", "")
|
|
63
|
+
if name.endswith(".jar"):
|
|
64
|
+
url = asset.get("browser_download_url")
|
|
65
|
+
break
|
|
66
|
+
if not url and assets:
|
|
67
|
+
url = assets[0].get("browser_download_url")
|
|
68
|
+
|
|
69
|
+
if not url:
|
|
70
|
+
raise RuntimeError("Could not find a valid download URL in the ReVanced CLI release assets.")
|
|
71
|
+
|
|
72
|
+
return self.__download_common(url, save=save, filename=filename, chunk_size=chunk_size)
|
|
73
|
+
|
|
74
|
+
def download_all(self) -> None:
|
|
75
|
+
shutil.rmtree(".revanced_res", ignore_errors=True)
|
|
76
|
+
self.download_cli()
|
|
77
|
+
self.download_patches_rvp()
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
downloader = Downloader()
|
|
81
|
+
downloader.download_all()
|
|
82
|
+
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import os
|
|
3
|
+
from typing import List, Dict, Any, Union, Optional, Generator
|
|
4
|
+
from pyaxmlparser import APK
|
|
5
|
+
|
|
6
|
+
from common import PATCHES_PATH, CLI_PATH
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Patcher:
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
self.PATCHES_PATH = PATCHES_PATH
|
|
12
|
+
self.CLI_PATH = CLI_PATH
|
|
13
|
+
|
|
14
|
+
def __exec_cmd(
|
|
15
|
+
self, cmd: List[str], stream: bool = False
|
|
16
|
+
) -> Union[str, Generator[str, None, None]]:
|
|
17
|
+
"""
|
|
18
|
+
Executes a command.
|
|
19
|
+
|
|
20
|
+
:param cmd: The command to run as a list of strings.
|
|
21
|
+
:param stream: If True, returns a generator that yields output line-by-line.
|
|
22
|
+
If False, blocks and returns the entire stdout as a string.
|
|
23
|
+
"""
|
|
24
|
+
if stream:
|
|
25
|
+
|
|
26
|
+
def line_generator() -> Generator[str, None, None]:
|
|
27
|
+
process = subprocess.Popen(
|
|
28
|
+
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if process.stdout:
|
|
32
|
+
for line in iter(process.stdout.readline, ""):
|
|
33
|
+
yield line.strip()
|
|
34
|
+
|
|
35
|
+
return_code = process.wait()
|
|
36
|
+
if return_code != 0:
|
|
37
|
+
raise RuntimeError(f"Command failed with exit code {return_code}.")
|
|
38
|
+
|
|
39
|
+
return line_generator()
|
|
40
|
+
|
|
41
|
+
else:
|
|
42
|
+
# Standard blocking execution
|
|
43
|
+
process = subprocess.Popen(
|
|
44
|
+
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
|
45
|
+
)
|
|
46
|
+
stdout, stderr = process.communicate()
|
|
47
|
+
stdout_str = stdout.strip()
|
|
48
|
+
stderr_str = stderr.strip()
|
|
49
|
+
|
|
50
|
+
if process.returncode != 0:
|
|
51
|
+
err_msg = stderr_str if stderr_str else stdout_str
|
|
52
|
+
raise RuntimeError(
|
|
53
|
+
f"Command failed with exit code {process.returncode}. Error: {err_msg}"
|
|
54
|
+
)
|
|
55
|
+
return stdout_str
|
|
56
|
+
|
|
57
|
+
def __get_package_infos(self, package_name: Optional[str] = None) -> str:
|
|
58
|
+
if not os.path.exists(self.PATCHES_PATH) or not os.path.exists(self.CLI_PATH):
|
|
59
|
+
raise FileNotFoundError(
|
|
60
|
+
"Required files not found. Please ensure both patches and CLI are available."
|
|
61
|
+
)
|
|
62
|
+
cmd = [
|
|
63
|
+
"java",
|
|
64
|
+
"-jar",
|
|
65
|
+
self.CLI_PATH,
|
|
66
|
+
"list-patches",
|
|
67
|
+
"--packages",
|
|
68
|
+
"--versions",
|
|
69
|
+
"--options",
|
|
70
|
+
"-b",
|
|
71
|
+
"-p",
|
|
72
|
+
self.PATCHES_PATH,
|
|
73
|
+
]
|
|
74
|
+
if package_name:
|
|
75
|
+
cmd.append(f"--filter-package-name={package_name}")
|
|
76
|
+
return self.__exec_cmd(cmd)
|
|
77
|
+
|
|
78
|
+
def __parse_package_infos(
|
|
79
|
+
self, package_name: Optional[str] = None
|
|
80
|
+
) -> List[Dict[str, Any]]:
|
|
81
|
+
"""
|
|
82
|
+
Parses the CLI output of the ReVanced list-patches command into
|
|
83
|
+
a structured list of dictionaries.
|
|
84
|
+
"""
|
|
85
|
+
text = self.__get_package_infos(package_name)
|
|
86
|
+
patches = []
|
|
87
|
+
current_patch = None
|
|
88
|
+
current_option = None
|
|
89
|
+
current_package = None
|
|
90
|
+
|
|
91
|
+
# Track parser state for multi-line values/descriptions
|
|
92
|
+
state = None # Can be: None, "patch_desc", "opt_desc", "values", "versions"
|
|
93
|
+
|
|
94
|
+
for line in text.splitlines():
|
|
95
|
+
# Strip common logging prefixes if present
|
|
96
|
+
if line.startswith("INFO: "):
|
|
97
|
+
line = line[6:]
|
|
98
|
+
|
|
99
|
+
stripped = line.strip()
|
|
100
|
+
|
|
101
|
+
# Handle empty lines
|
|
102
|
+
if not stripped:
|
|
103
|
+
# Preserve empty lines inside multi-line descriptions
|
|
104
|
+
if state == "patch_desc" and current_patch:
|
|
105
|
+
current_patch["Description"] += "\n"
|
|
106
|
+
elif state == "opt_desc" and current_option:
|
|
107
|
+
current_option["Description"] += "\n"
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
# Determine indentation level (supporting both tabs and spaces)
|
|
111
|
+
tabs = len(line) - len(line.lstrip("\t"))
|
|
112
|
+
if tabs == 0:
|
|
113
|
+
spaces = len(line) - len(line.lstrip(" "))
|
|
114
|
+
tabs = spaces // 4 # Standard conversion of 4 spaces to 1 tab level
|
|
115
|
+
|
|
116
|
+
# --- LEVEL 0: Main Patch Parameters ---
|
|
117
|
+
if tabs == 0:
|
|
118
|
+
if stripped.startswith("Index:"):
|
|
119
|
+
# Save previous patch before starting a new one
|
|
120
|
+
if current_patch:
|
|
121
|
+
patches.append(current_patch)
|
|
122
|
+
|
|
123
|
+
index_val = int(stripped.split(":", 1)[1].strip())
|
|
124
|
+
current_patch = {
|
|
125
|
+
"Index": index_val,
|
|
126
|
+
"Name": "",
|
|
127
|
+
"Description": "",
|
|
128
|
+
"Enabled": False,
|
|
129
|
+
"Options": [],
|
|
130
|
+
"Compatible packages": [],
|
|
131
|
+
}
|
|
132
|
+
current_option = None
|
|
133
|
+
current_package = None
|
|
134
|
+
state = None
|
|
135
|
+
|
|
136
|
+
elif current_patch:
|
|
137
|
+
if stripped.startswith("Name:"):
|
|
138
|
+
current_patch["Name"] = stripped.split(":", 1)[1].strip()
|
|
139
|
+
state = None
|
|
140
|
+
elif stripped.startswith("Description:"):
|
|
141
|
+
current_patch["Description"] = stripped.split(":", 1)[1].strip()
|
|
142
|
+
state = "patch_desc"
|
|
143
|
+
elif stripped.startswith("Enabled:"):
|
|
144
|
+
current_patch["Enabled"] = (
|
|
145
|
+
stripped.split(":", 1)[1].strip().lower() == "true"
|
|
146
|
+
)
|
|
147
|
+
state = None
|
|
148
|
+
elif stripped in ("Options:", "Compatible packages:"):
|
|
149
|
+
state = None
|
|
150
|
+
|
|
151
|
+
# --- LEVEL 1: Options & Compatible Packages ---
|
|
152
|
+
elif tabs == 1:
|
|
153
|
+
if current_patch:
|
|
154
|
+
if stripped.startswith("Name:"):
|
|
155
|
+
current_option = {
|
|
156
|
+
"Name": stripped.split(":", 1)[1].strip(),
|
|
157
|
+
"Description": "",
|
|
158
|
+
"Required": False,
|
|
159
|
+
"Default": None,
|
|
160
|
+
"Possible values": [],
|
|
161
|
+
"Type": "",
|
|
162
|
+
}
|
|
163
|
+
current_patch["Options"].append(current_option)
|
|
164
|
+
current_package = None
|
|
165
|
+
state = None
|
|
166
|
+
|
|
167
|
+
elif stripped.startswith("Package name:"):
|
|
168
|
+
current_package = {
|
|
169
|
+
"Package name": stripped.split(":", 1)[1].strip(),
|
|
170
|
+
"Compatible versions": [],
|
|
171
|
+
}
|
|
172
|
+
current_patch["Compatible packages"].append(current_package)
|
|
173
|
+
current_option = None
|
|
174
|
+
state = None
|
|
175
|
+
|
|
176
|
+
elif current_option:
|
|
177
|
+
if stripped.startswith("Description:"):
|
|
178
|
+
current_option["Description"] = stripped.split(":", 1)[
|
|
179
|
+
1
|
|
180
|
+
].strip()
|
|
181
|
+
state = "opt_desc"
|
|
182
|
+
elif stripped.startswith("Required:"):
|
|
183
|
+
current_option["Required"] = (
|
|
184
|
+
stripped.split(":", 1)[1].strip().lower() == "true"
|
|
185
|
+
)
|
|
186
|
+
state = None
|
|
187
|
+
elif stripped.startswith("Default:"):
|
|
188
|
+
val = stripped.split(":", 1)[1].strip()
|
|
189
|
+
if val.lower() == "true":
|
|
190
|
+
current_option["Default"] = True
|
|
191
|
+
elif val.lower() == "false":
|
|
192
|
+
current_option["Default"] = False
|
|
193
|
+
else:
|
|
194
|
+
current_option["Default"] = val
|
|
195
|
+
state = None
|
|
196
|
+
elif stripped.startswith("Type:"):
|
|
197
|
+
current_option["Type"] = stripped.split(":", 1)[1].strip()
|
|
198
|
+
state = None
|
|
199
|
+
elif stripped.startswith("Possible values:"):
|
|
200
|
+
state = "values"
|
|
201
|
+
else:
|
|
202
|
+
# Append any un-keyed text here to support multi-line descriptions
|
|
203
|
+
if state == "opt_desc":
|
|
204
|
+
current_option["Description"] += "\n" + stripped
|
|
205
|
+
|
|
206
|
+
elif current_package:
|
|
207
|
+
if stripped.startswith("Compatible versions:"):
|
|
208
|
+
state = "versions"
|
|
209
|
+
|
|
210
|
+
# --- LEVEL 2: Nested Lists ---
|
|
211
|
+
elif tabs == 2:
|
|
212
|
+
if state == "values" and current_option:
|
|
213
|
+
current_option["Possible values"].append(stripped)
|
|
214
|
+
elif state == "versions" and current_package:
|
|
215
|
+
current_package["Compatible versions"].append(stripped)
|
|
216
|
+
elif state == "opt_desc" and current_option:
|
|
217
|
+
current_option["Description"] += "\n" + stripped
|
|
218
|
+
|
|
219
|
+
# Append the final patch
|
|
220
|
+
if current_patch:
|
|
221
|
+
patches.append(current_patch)
|
|
222
|
+
|
|
223
|
+
# Clean up excess whitespace from multi-line descriptions
|
|
224
|
+
for patch in patches:
|
|
225
|
+
patch["Description"] = patch["Description"].strip()
|
|
226
|
+
for opt in patch["Options"]:
|
|
227
|
+
opt["Description"] = opt["Description"].strip()
|
|
228
|
+
|
|
229
|
+
return patches
|
|
230
|
+
|
|
231
|
+
def list_patches(
|
|
232
|
+
self, package_name: Optional[str] = None, apk_path: Optional[str] = None
|
|
233
|
+
) -> List[Dict[str, Any]]:
|
|
234
|
+
"""
|
|
235
|
+
Public method to retrieve and parse the list of patches, optionally filtered by a package.
|
|
236
|
+
"""
|
|
237
|
+
if apk_path:
|
|
238
|
+
package_info = self.get_apk_info(apk_path)["package_name"]
|
|
239
|
+
return self.__parse_package_infos(package_info)
|
|
240
|
+
return self.__parse_package_infos(package_name)
|
|
241
|
+
|
|
242
|
+
def get_apk_info(self, apk_path: str) -> Dict[str, str]:
|
|
243
|
+
"""
|
|
244
|
+
Reads metadata from the APK file and returns its package name and version.
|
|
245
|
+
"""
|
|
246
|
+
if not os.path.exists(apk_path):
|
|
247
|
+
raise FileNotFoundError(f"APK file not found at path: {apk_path}")
|
|
248
|
+
apk = APK(apk_path)
|
|
249
|
+
return {"package_name": apk.package, "version_name": apk.version_name}
|
|
250
|
+
|
|
251
|
+
def _format_option_value(self, val: Any) -> str:
|
|
252
|
+
if val is True:
|
|
253
|
+
return "true"
|
|
254
|
+
elif val is False:
|
|
255
|
+
return "false"
|
|
256
|
+
elif isinstance(val, list):
|
|
257
|
+
return f"[{','.join(self._format_option_value(item) for item in val)}]"
|
|
258
|
+
elif val is None:
|
|
259
|
+
return ""
|
|
260
|
+
else:
|
|
261
|
+
return str(val)
|
|
262
|
+
|
|
263
|
+
def patch_apk(
|
|
264
|
+
self,
|
|
265
|
+
apk_path: str,
|
|
266
|
+
output_path: Optional[str] = None,
|
|
267
|
+
enabled_patches: Optional[List[Union[str, int]]] = None,
|
|
268
|
+
disabled_patches: Optional[List[Union[str, int]]] = None,
|
|
269
|
+
options: Optional[Dict[str, Any]] = None,
|
|
270
|
+
exclusive: bool = False,
|
|
271
|
+
force: bool = False,
|
|
272
|
+
bypass_verification: bool = True,
|
|
273
|
+
purge: bool = True,
|
|
274
|
+
stream_output: bool = False,
|
|
275
|
+
) -> Union[str, Generator[str, None, None]]:
|
|
276
|
+
"""
|
|
277
|
+
Patches an APK file using the ReVanced CLI.
|
|
278
|
+
|
|
279
|
+
:param apk_path: Path to the input APK file.
|
|
280
|
+
:param output_path: Path to save the patched APK.
|
|
281
|
+
:param enabled_patches: Names or indices of patches to enable.
|
|
282
|
+
:param disabled_patches: Names or indices of patches to disable.
|
|
283
|
+
:param options: Dict of option values keyed by option keys.
|
|
284
|
+
:param exclusive: If True, only specified enabled patches will be applied.
|
|
285
|
+
:param force: If True, compatibility checks will be bypassed.
|
|
286
|
+
:param bypass_verification: If True, bypass signature/provenance check on RVP files.
|
|
287
|
+
:param purge: If True, purge temporary files directory after patching.
|
|
288
|
+
:param stream_output: If True, returns a generator that yields output line-by-line.
|
|
289
|
+
:return: Standard output of the patch command.
|
|
290
|
+
"""
|
|
291
|
+
if not os.path.exists(self.PATCHES_PATH) or not os.path.exists(self.CLI_PATH):
|
|
292
|
+
raise FileNotFoundError(
|
|
293
|
+
"Required files not found. Please ensure both patches and CLI are available."
|
|
294
|
+
)
|
|
295
|
+
if not os.path.exists(apk_path):
|
|
296
|
+
raise FileNotFoundError(f"APK file not found at path: {apk_path}")
|
|
297
|
+
|
|
298
|
+
cmd = ["java", "-jar", self.CLI_PATH, "patch", "-p", self.PATCHES_PATH]
|
|
299
|
+
if bypass_verification:
|
|
300
|
+
cmd.append("-b")
|
|
301
|
+
|
|
302
|
+
if exclusive:
|
|
303
|
+
cmd.append("--exclusive")
|
|
304
|
+
if force:
|
|
305
|
+
cmd.append("-f")
|
|
306
|
+
if output_path:
|
|
307
|
+
cmd.extend(["-o", output_path])
|
|
308
|
+
|
|
309
|
+
if enabled_patches:
|
|
310
|
+
for patch in enabled_patches:
|
|
311
|
+
if isinstance(patch, int):
|
|
312
|
+
cmd.extend(["--ei", str(patch)])
|
|
313
|
+
else:
|
|
314
|
+
cmd.extend(["-e", str(patch)])
|
|
315
|
+
|
|
316
|
+
if disabled_patches:
|
|
317
|
+
for patch in disabled_patches:
|
|
318
|
+
if isinstance(patch, int):
|
|
319
|
+
cmd.extend(["--di", str(patch)])
|
|
320
|
+
else:
|
|
321
|
+
cmd.extend(["-d", str(patch)])
|
|
322
|
+
|
|
323
|
+
if options:
|
|
324
|
+
for key, val in options.items():
|
|
325
|
+
if val is None:
|
|
326
|
+
cmd.append(f"-O{key}")
|
|
327
|
+
else:
|
|
328
|
+
cmd.append(f"-O{key}={self._format_option_value(val)}")
|
|
329
|
+
|
|
330
|
+
if purge:
|
|
331
|
+
cmd.append("--purge")
|
|
332
|
+
|
|
333
|
+
cmd.append(apk_path)
|
|
334
|
+
print(" ".join(cmd))
|
|
335
|
+
return self.__exec_cmd(cmd, stream=stream_output)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
if __name__ == "__main__":
|
|
339
|
+
patcher = Patcher()
|
|
340
|
+
# Example usage: List patches for a specific package
|
|
341
|
+
try:
|
|
342
|
+
patches = patcher.list_patches(package_name="com.spotify.music")
|
|
343
|
+
for patch in patches:
|
|
344
|
+
print(patch)
|
|
345
|
+
except Exception as e:
|
|
346
|
+
print(f"Error: {e}")
|