Multi Source patch

This commit is contained in:
Nikhil Badyal
2025-06-19 22:31:46 +05:30
committed by Nikhil Badyal
parent a2ba0b89d1
commit 4283925f3e
6 changed files with 177 additions and 74 deletions
+26 -19
View File
@@ -47,7 +47,7 @@ You can use any of the following methods to build.
</details>
5. If the building process is successful, youll get your APKs in the<br>
5. If the building process is successful, you'll get your APKs in the<br>
<img src="https://i.imgur.com/S5d7qAO.png" width="700" style="left">
6. Make sure to do below steps once in a while(daily or weekly) to keep the builder bug free.<br>
<img src="https://i.imgur.com/CbdH7vM.png" width="700" style="left">
@@ -134,20 +134,20 @@ You can use any of the following methods to build.
`*` - Can be overridden for individual app.
### App Level Config
| Env Name | Description | Default |
|:--------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------:|:-------------------------------|
| [~~**APP_NAME**_CLI_DL~~](#global-resources) | DL for CLI to be used for patching **APP_NAME**.(Disabled Temp) | GLOBAL_CLI_DL |
| [**APP_NAME**_PATCHES_DL](#global-resources) | DL for Patches to be used for patching **APP_NAME**. | GLOBAL_PATCHES_DL |
| [**APP_NAME**_SPACE_FORMATTED_PATCHES](#global-resources) | Whether patches are space formatted. **APP_NAME**. | GLOBAL_SPACE_FORMATTED_PATCHES |
| [**APP_NAME**_KEYSTORE_FILE_NAME](#global-keystore-file-name) | Key file to be used for signing **APP_NAME**. | GLOBAL_KEYSTORE_FILE_NAME |
| [**APP_NAME**_OLD_KEY](#global-keystore-file-name) | Whether key used was generated with cli > v4(new) <br/><br/>**APP_NAME**. <br/> <br/> | GLOBAL_OLK_KEY |
| [**APP_NAME**_ARCHS_TO_BUILD](#global-archs-to-build) | Arch to keep in the patched **APP_NAME**. | GLOBAL_ARCHS_TO_BUILD |
| [**APP_NAME**_EXCLUDE_PATCH**](#custom-exclude-patching) | Patches to exclude while patching **APP_NAME**. | [] |
| [**APP_NAME**_INCLUDE_PATCH**](#custom-include-patching) | Patches to include while patching **APP_NAME**. | [] |
| [**APP_NAME**_VERSION](#app-version) | Version to use for download for patching. | Recommended by patch resources |
| [**APP_NAME**_PACKAGE_NAME***](#any-patch-apps) | Package name of the app to be patched | None |
| [**APP_NAME**_DL_SOURCE***](#any-patch-apps) | Download source of any of the supported scrapper | None |
| [**APP_NAME**_DL***](#app-dl) | Direct download Link for clean apk | None |
| Env Name | Description | Default |
|:--------------------------------------------------------------|:----------------------------------------------------------------------------------------------------:|:-------------------------------|
| [~~**APP_NAME**_CLI_DL~~](#global-resources) | DL for CLI to be used for patching **APP_NAME**.(Disabled Temp) | GLOBAL_CLI_DL |
| [**APP_NAME**_PATCHES_DL](#global-resources) | DL for Patches to be used for patching **APP_NAME**. Supports multiple bundles via comma separation. | GLOBAL_PATCHES_DL |
| [**APP_NAME**_SPACE_FORMATTED_PATCHES](#global-resources) | Whether patches are space formatted. **APP_NAME**. | GLOBAL_SPACE_FORMATTED_PATCHES |
| [**APP_NAME**_KEYSTORE_FILE_NAME](#global-keystore-file-name) | Key file to be used for signing **APP_NAME**. | GLOBAL_KEYSTORE_FILE_NAME |
| [**APP_NAME**_OLD_KEY](#global-keystore-file-name) | Whether key used was generated with cli > v4(new) <br/><br/>**APP_NAME**. <br/> <br/> | GLOBAL_OLK_KEY |
| [**APP_NAME**_ARCHS_TO_BUILD](#global-archs-to-build) | Arch to keep in the patched **APP_NAME**. | GLOBAL_ARCHS_TO_BUILD |
| [**APP_NAME**_EXCLUDE_PATCH**](#custom-exclude-patching) | Patches to exclude while patching **APP_NAME**. | [] |
| [**APP_NAME**_INCLUDE_PATCH**](#custom-include-patching) | Patches to include while patching **APP_NAME**. | [] |
| [**APP_NAME**_VERSION](#app-version) | Version to use for download for patching. | Recommended by patch resources |
| [**APP_NAME**_PACKAGE_NAME***](#any-patch-apps) | Package name of the app to be patched | None |
| [**APP_NAME**_DL_SOURCE***](#any-patch-apps) | Download source of any of the supported scrapper | None |
| [**APP_NAME**_DL***](#app-dl) | Direct download Link for clean apk | None |
`**` - By default all patches for a given app are included.<br>
`**` - Can be used to included universal patch.<br>
@@ -308,10 +308,17 @@ You can use any of the following methods to build.
```
With the config tool will try to patch YouTube with resources from inotia00 while other global resource will used
for patching other apps.<br>
**Multi-Patching Support**: You can now use multiple patch bundles from different creators for the same app:
```dotenv
# Comma-separated URLs
YOUTUBE_PATCHES_DL=https://github.com/ReVanced/revanced-patches,https://github.com/indrastorm/Dropped-patches
```
The tool will download all specified patch bundles and apply them together using the ReVanced CLI's multiple `-p` argument support.<br>
If you have want to provide resource locally in the apks folder. You can specify that by mentioning filename
prefixed with `local://`.<br>
*Note* - The link provided must be DLs. Unless they are from GitHub.<br>
*Note* - If your patches resource are available on GitHub and you want to select latest resource without excluding
_Note_ - The link provided must be DLs. Unless they are from GitHub.<br>
_Note_ - If your patches resource are available on GitHub and you want to select latest resource without excluding
pre-release you can add `latest-prerelease` to the URL.
Example:
```dotenv
@@ -323,7 +330,7 @@ You can use any of the following methods to build.
```
For above example tool while selecting latest patches will exclude any pre-release/beta ie. will consider only
stable releases..<br>
*Note* - Some of the patch source like inotia00 still provides **-** separated patches while revanced shifted to
_Note_ - Some of the patch source like inotia00 still provides **-** separated patches while revanced shifted to
Space formatted patches. Use `SPACE_FORMATTED_PATCHES` to define the type of patches.
8. <a id="global-keystore-file-name"></a>If you don't want to use default keystore. You can provide your own by
@@ -371,7 +378,7 @@ You can use any of the following methods to build.
```dotenv
YOUTUBE_ARCHS_TO_BUILD=arm64-v8a,armeabi-v7a
```
*Note* -
_Note_ -
1. Possible values are: `armeabi-v7a`,`x86`,`x86_64`,`arm64-v8a`
2. Make sure the patching resource(CLI) support this feature.
11. <a id="extra-files"></a>If you want to include any extra file to the Github upload. Set comma arguments
+74 -24
View File
@@ -31,10 +31,17 @@ class APP(object):
self.app_version = config.env.str(f"{app_name}_VERSION".upper(), None)
self.experiment = False
self.cli_dl = config.env.str(f"{app_name}_CLI_DL".upper(), config.global_cli_dl)
self.patches_dl = config.env.str(f"{app_name}_PATCHES_DL".upper(), config.global_patches_dl)
# Support multiple patch bundles via comma-separated URLs
patches_dl_raw = config.env.str(f"{app_name}_PATCHES_DL".upper(), config.global_patches_dl)
self.patches_dl_list = [url.strip() for url in patches_dl_raw.split(",") if url.strip()]
# Keep backward compatibility
self.patches_dl = patches_dl_raw
self.exclude_request: list[str] = config.env.list(f"{app_name}_EXCLUDE_PATCH".upper(), [])
self.include_request: list[str] = config.env.list(f"{app_name}_INCLUDE_PATCH".upper(), [])
self.resource: dict[str, dict[str, str]] = {}
self.patch_bundles: list[dict[str, str]] = [] # Store multiple patch bundles
self.no_of_patches: int = 0
self.keystore_name = config.env.str(f"{app_name}_KEYSTORE_FILE_NAME".upper(), config.global_keystore_name)
self.archs_to_build = config.env.list(f"{app_name}_ARCHS_TO_BUILD".upper(), config.global_archs_to_build)
@@ -113,18 +120,18 @@ class APP(object):
----------
url : str
The `url` parameter is a string that represents the URL of the resource you want to download.
It can be a URL from GitHub or a local file URL.
It can be a URL from GitHub or a local file URL.
config : RevancedConfig
The `config` parameter is an instance of the `RevancedConfig` class. It is used to provide
configuration settings for the download process.
configuration settings for the download process.
assets_filter : str
The `assets_filter` parameter is a string that is used to filter the assets to be downloaded
from a GitHub repository. It is used when the `url` parameter starts with "https://github". The
`assets_filter` string is matched against the names of the assets in the repository, and only
file_name : str
from a GitHub repository. It is used when the `url` parameter starts with "https://github". The
`assets_filter` string is matched against the names of the assets in the repository, and only
file_name : str
The `file_name` parameter is a string that represents the name of the file that will be
downloaded. If no value is provided for `file_name`, the function will generate a filename based
on the URL of the file being downloaded.
downloaded. If no value is provided for `file_name`, the function will generate a filename based
on the URL of the file being downloaded.
Returns
-------
@@ -148,6 +155,58 @@ class APP(object):
Downloader(config).direct_download(url, file_name)
return tag, file_name
def _setup_download_tasks(self: Self) -> list[tuple[str, str, None, str]]:
"""Setup download tasks for CLI and patch bundles."""
download_tasks = [
("cli", self.cli_dl, None, ".*jar"),
]
# Download multiple patch bundles
for i, patches_url in enumerate(self.patches_dl_list):
bundle_name = f"patches_{i}" if len(self.patches_dl_list) > 1 else "patches"
download_tasks.append((bundle_name, patches_url, None, ".*rvp"))
return download_tasks
def _handle_cached_resource(self: Self, resource_name: str, tag: str, file_name: str) -> None:
"""Handle cached resource and update appropriate data structures."""
if resource_name.startswith("patches"):
self.patch_bundles.append(
{
"name": resource_name,
"file_name": file_name,
"version": tag,
},
)
# Keep backward compatibility for single bundle
if resource_name == "patches" or len(self.patches_dl_list) == 1:
self.resource["patches"] = {
"file_name": file_name,
"version": tag,
}
else:
self.resource[resource_name] = {
"file_name": file_name,
"version": tag,
}
def _handle_downloaded_resource(
self: Self,
resource_name: str,
tag: str,
file_name: str,
download_tasks: list[tuple[str, str, RevancedConfig, str]],
resource_cache: dict[str, tuple[str, str]],
) -> None:
"""Handle newly downloaded resource and update cache."""
self._handle_cached_resource(resource_name, tag, file_name)
# Update cache for the corresponding URL
for task_name, task_url, _, _ in download_tasks:
if task_name == resource_name:
resource_cache[task_url.strip()] = (tag, file_name)
break
def download_patch_resources(
self: Self,
config: RevancedConfig,
@@ -159,14 +218,15 @@ class APP(object):
----------
config : RevancedConfig
The `config` parameter is an instance of the `RevancedConfig` class. It is used to provide
configuration settings for the resource download tasks.
configuration settings for the resource download tasks.
resource_cache: dict[str, tuple[str, str]]
"""
logger.info("Downloading resources for patching.")
download_tasks = [
("cli", self.cli_dl, config, ".*jar"),
("patches", self.patches_dl, config, ".*rvp"),
base_tasks = self._setup_download_tasks()
# Update download tasks with config
download_tasks: list[tuple[str, str, RevancedConfig, str]] = [
(name, url, config, filter_pattern) for name, url, _, filter_pattern in base_tasks
]
with ThreadPoolExecutor(1) as executor:
@@ -177,10 +237,7 @@ class APP(object):
if url in resource_cache:
logger.info(f"Skipping {resource_name} download, using cached resource: {url}")
tag, file_name = resource_cache[url]
self.resource[resource_name] = {
"file_name": file_name,
"version": tag,
}
self._handle_cached_resource(resource_name, tag, file_name)
continue
futures[resource_name] = executor.submit(self.download, url, cfg, assets_filter)
@@ -190,14 +247,7 @@ class APP(object):
for resource_name, future in futures.items():
try:
tag, file_name = future.result()
self.resource[resource_name] = {
"file_name": file_name,
"version": tag,
}
resource_cache[download_tasks[["cli", "patches"].index(resource_name)][1].strip()] = (
tag,
file_name,
)
self._handle_downloaded_resource(resource_name, tag, file_name, download_tasks, resource_cache)
except BuilderError as e:
msg = f"Failed to download {resource_name} resource."
raise PatchingFailedError(msg) from e
+3 -1
View File
@@ -51,7 +51,9 @@ class Github(Downloader):
"""Extract repo owner and url from github url."""
parsed_url = urlparse(url)
path_segments = parsed_url.path.strip("/").split("/")
if len(path_segments) < 2:
msg = f"Invalid GitHub URL format: {url}"
raise DownloadError(msg)
github_repo_owner = path_segments[0]
github_repo_name = path_segments[1]
tag_position = 3
+1 -1
View File
@@ -130,4 +130,4 @@ class PatchesJsonLoadError(BuilderError):
def __str__(self: Self) -> str:
"""Exception message."""
base_message = super().__str__()
return f"Message - {base_message} Url - {self.file_name}"
return f"Message - {base_message} File - {self.file_name}"
+41 -21
View File
@@ -24,6 +24,9 @@ class Parser(object):
OUTPUT_ARG = "-o"
KEYSTORE_ARG = "--keystore"
OPTIONS_ARG = "-O"
ENABLE_ARG = "-e"
DISABLE_ARG = "-d"
EXCLUSIVE_ARG = "--exclusive"
def __init__(self: Self, patcher: Patches, config: RevancedConfig) -> None:
self._PATCHES: list[str] = []
@@ -71,7 +74,7 @@ class Parser(object):
for opt in options:
pair = self.format_option(opt)
self._PATCHES[:0] = [self.OPTIONS_ARG, pair]
self._PATCHES[:0] = ["-e", name]
self._PATCHES[:0] = [self.ENABLE_ARG, name]
def exclude(self: Self, name: str) -> None:
"""The `exclude` function adds a given patch to the list of excluded patches.
@@ -81,7 +84,7 @@ class Parser(object):
name : str
The `name` parameter is a string that represents the name of the patch to be excluded.
"""
self._PATCHES.extend(["-d", name])
self._PATCHES.extend([self.DISABLE_ARG, name])
self._EXCLUDED.append(name)
def get_excluded_patches(self: Self) -> list[str]:
@@ -119,22 +122,27 @@ class Parser(object):
name = name.lower().replace(" ", "-")
indices = [i for i in range(len(self._PATCHES)) if self._PATCHES[i] == name]
for patch_index in indices:
if self._PATCHES[patch_index - 1] == "-e":
self._PATCHES[patch_index - 1] = "-d"
if self._PATCHES[patch_index - 1] == self.ENABLE_ARG:
self._PATCHES[patch_index - 1] = self.DISABLE_ARG
else:
self._PATCHES[patch_index - 1] = "-e"
self._PATCHES[patch_index - 1] = self.ENABLE_ARG
except ValueError:
return False
else:
return True
def exclude_all_patches(self: Self) -> None:
"""The function `exclude_all_patches` exclude all the patches."""
for idx, item in enumerate(self._PATCHES):
if idx == 0:
continue
if item == "-e":
self._PATCHES[idx] = "-d"
def enable_exclusive_mode(self: Self) -> None:
"""Enable exclusive mode - only explicitly enabled patches will run, all others disabled by default."""
logger.info("Enabling exclusive mode for fast testing - only keeping one patch enabled.")
# Clear all patches and keep only the first one enabled
if self._PATCHES:
# Find the first enable argument and its patch name
for idx in range(0, len(self._PATCHES), 2):
if idx < len(self._PATCHES) and self._PATCHES[idx] == self.ENABLE_ARG and idx + 1 < len(self._PATCHES):
first_patch = self._PATCHES[idx + 1]
# Clear all patches and set only the first one
self._PATCHES = [self.ENABLE_ARG, first_patch]
break
def fetch_patch_options(self: Self, name: str, options_list: list[dict[str, Any]]) -> dict[str, Any]:
"""The function `fetch_patch_options` finds patch options for the patch.
@@ -219,15 +227,27 @@ class Parser(object):
app.resource["cli"]["file_name"],
apk_arg,
app.download_file_name,
self.PATCHES_ARG,
app.resource["patches"]["file_name"],
self.OUTPUT_ARG,
app.get_output_file_name(),
self.KEYSTORE_ARG,
app.keystore_name,
exp,
]
args[1::2] = map(self.config.temp_folder.joinpath, args[1::2])
# Add multiple patch bundles using -p argument
if hasattr(app, "patch_bundles") and app.patch_bundles:
# Use multiple -p arguments for multiple bundles
for bundle in app.patch_bundles:
args.extend([self.PATCHES_ARG, bundle["file_name"]])
else:
# Fallback to single bundle for backward compatibility
args.extend([self.PATCHES_ARG, app.resource["patches"]["file_name"]])
args.extend(
[
self.OUTPUT_ARG,
app.get_output_file_name(),
self.KEYSTORE_ARG,
app.keystore_name,
exp,
],
)
args[1::2] = [str(self.config.temp_folder.joinpath(arg)) for arg in args[1::2]]
if app.old_key:
# https://github.com/ReVanced/revanced-cli/issues/272#issuecomment-1740587534
old_key_flags = [
@@ -237,7 +257,7 @@ class Parser(object):
]
args.extend(old_key_flags)
if self.config.ci_test:
self.exclude_all_patches()
self.enable_exclusive_mode()
if self._PATCHES:
args.extend(self._PATCHES)
if app.app_name in self.config.rip_libs_apps:
+32 -8
View File
@@ -1,7 +1,7 @@
"""Revanced Patches."""
import contextlib
from typing import ClassVar, Self
from typing import Any, ClassVar, Self
from loguru import logger
@@ -125,11 +125,35 @@ class Patches(object):
The `app` parameter is of type `APP`. It represents an instance of the `APP` class.
"""
self.patches_dict[app.app_name] = []
patches = convert_command_output_to_json(
f"{config.temp_folder}/{app.resource["cli"]["file_name"]}",
f"{config.temp_folder}/{app.resource["patches"]["file_name"]}",
)
# Handle multiple patch bundles
if hasattr(app, "patch_bundles") and app.patch_bundles:
for bundle in app.patch_bundles:
patches = convert_command_output_to_json(
f"{config.temp_folder}/{app.resource["cli"]["file_name"]}",
f"{config.temp_folder}/{bundle["file_name"]}",
)
self._process_patches(patches, app)
elif "patches" in app.resource:
# Fallback to single bundle for backward compatibility
patches = convert_command_output_to_json(
f"{config.temp_folder}/{app.resource["cli"]["file_name"]}",
f"{config.temp_folder}/{app.resource["patches"]["file_name"]}",
)
self._process_patches(patches, app)
app.no_of_patches = len(self.patches_dict[app.app_name])
def _process_patches(self: Self, patches: list[dict[Any, Any]], app: APP) -> None:
"""Process patches from a single bundle and add them to the patches dict.
Parameters
----------
patches : list[dict[Any, Any]]
List of patches from a bundle
app : APP
The app instance
"""
for patch in patches:
if not patch["compatiblePackages"]:
p = {x: patch[x] for x in ["name", "description"]}
@@ -142,9 +166,9 @@ class Patches(object):
p = {x: patch[x] for x in ["name", "description"]}
p["app"] = compatible_package
p["version"] = version[-1] if version else "all"
self.patches_dict[app.app_name].append(p)
app.no_of_patches = len(self.patches_dict[app.app_name])
# Avoid duplicate patches from multiple bundles
if not any(existing["name"] == p["name"] for existing in self.patches_dict[app.app_name]):
self.patches_dict[app.app_name].append(p)
def __init__(self: Self, config: RevancedConfig, app: APP) -> None:
self.patches_dict: dict[str, list[dict[str, str]]] = {"universal_patch": []}