This commit is contained in:
eric
2026-04-04 00:21:41 -05:00
parent 415fdd3238
commit 0712fcdb84
6 changed files with 158 additions and 69 deletions

View File

@@ -42,7 +42,7 @@ Expected local files:
- `offline/downloads/magisk.version` - `offline/downloads/magisk.version`
- `offline/images/redroid-redroid-13.0.0-latest.tar` - `offline/images/redroid-redroid-13.0.0-latest.tar`
`redroid.py` now auto-loads the base image tar when `redroid/redroid:13.0.0-latest` is not already present in the local container runtime. `redroid.py` now tries to pull `redroid/redroid:13.0.0-latest` first. If that fails, it falls back to the local image cache, then to `offline/images/redroid-redroid-13.0.0-latest.tar`.
## Dependencies ## Dependencies

View File

@@ -27,5 +27,5 @@ python verify_offline_bundle.py
1. Make sure your container runtime can load local image archives. 1. Make sure your container runtime can load local image archives.
2. Populate `downloads/` and `images/` with the files listed in `manifest.json`. 2. Populate `downloads/` and `images/` with the files listed in `manifest.json`.
3. Run `python redroid.py -mtg -i -m`. 3. Run `python redroid.py -mtg -i -m`.
4. If the base image tag is missing, `redroid.py` will automatically load `images/redroid-redroid-13.0.0-latest.tar`. 4. The build first tries to pull the base image and download dependency archives from their online sources.
5. The build modules read the zip/apk files from `downloads/` first and do not need the network when those files are present and valid. 5. If an online pull/download fails, the build falls back to the validated files in `downloads/` and `images/`.

View File

@@ -22,19 +22,37 @@ def ensure_base_image(container_runtime, android_version):
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
check=False, check=False,
) )
if inspect.returncode == 0: local_present = inspect.returncode == 0
helper.print_color(
"Trying to pull base image {} first ...".format(base_image),
helper.bcolors.GREEN,
)
pull = subprocess.run([container_runtime, "pull", base_image], check=False)
if pull.returncode == 0:
return "pulled"
if local_present:
helper.print_color(
"Online pull failed, falling back to local image cache ...",
helper.bcolors.YELLOW,
)
return "present" return "present"
tar_name = "redroid-redroid-{}-latest.tar".format(android_version) tar_name = "redroid-redroid-{}-latest.tar".format(android_version)
tar_path = os.path.join(helper.get_image_dir(), tar_name) tar_path = os.path.join(helper.get_image_dir(), tar_name)
if not os.path.isfile(tar_path): if not os.path.isfile(tar_path):
raise FileNotFoundError( raise FileNotFoundError(
"Base image {} is missing and offline archive {} was not found".format( "Online pull for {} failed, local image cache is missing, and offline archive {} was not found".format(
base_image, base_image,
tar_path, tar_path,
) )
) )
helper.print_color(
"Online pull failed, loading local image archive {} ...".format(tar_path),
helper.bcolors.YELLOW,
)
subprocess.run([container_runtime, "load", "-i", tar_path], check=True) subprocess.run([container_runtime, "load", "-i", tar_path], check=True)
return tar_path return tar_path

View File

@@ -1,24 +1,49 @@
import os import os
import zipfile import zipfile
import hashlib
from tools.helper import bcolors, download_file, print_color from tools.helper import bcolors, download_file, file_md5, print_color
class General: class General:
def _local_md5(self):
if not os.path.isfile(self.dl_file_name):
return ""
return file_md5(self.dl_file_name)
def _has_valid_local_file(self):
return os.path.isfile(self.dl_file_name) and self._local_md5() == self.act_md5
def download(self): def download(self):
loc_md5 = "" temp_path = self.dl_file_name + ".tmp"
if os.path.isfile(self.dl_file_name): local_valid = self._has_valid_local_file()
with open(self.dl_file_name,"rb") as f:
bytes = f.read() try:
loc_md5 = hashlib.md5(bytes).hexdigest() if os.path.isfile(temp_path):
while not os.path.isfile(self.dl_file_name) or loc_md5 != self.act_md5: os.remove(temp_path)
if os.path.isfile(self.dl_file_name): print_color("Trying online download first ...", bcolors.GREEN)
os.remove(self.dl_file_name) remote_md5 = download_file(self.dl_link, temp_path)
print_color("md5 mismatches, redownloading now ....",bcolors.YELLOW) if remote_md5 != self.act_md5:
loc_md5 = download_file(self.dl_link, self.dl_file_name) raise ValueError(
"Downloaded file md5 mismatch: expected {}, got {}".format(
self.act_md5,
remote_md5,
)
)
os.replace(temp_path, self.dl_file_name)
except Exception as exc:
if os.path.isfile(temp_path):
os.remove(temp_path)
if local_valid:
print_color(
"Online download failed, falling back to local {} ({})".format(
self.dl_file_name,
exc,
),
bcolors.YELLOW,
)
return
raise
def extract(self): def extract(self):
print_color("Extracting archive...", bcolors.GREEN) print_color("Extracting archive...", bcolors.GREEN)
print(self.dl_file_name) print(self.dl_file_name)

View File

@@ -137,13 +137,41 @@ on property:init.svc.zygote=stopped
with open(self.release_meta_file, "w", encoding="utf-8") as meta_file: with open(self.release_meta_file, "w", encoding="utf-8") as meta_file:
meta_file.write(self.release_version) meta_file.write(self.release_version)
def _validate_archive(self): def _validate_archive(self, path=None):
if not zipfile.is_zipfile(self.dl_file_name): archive_path = path or self.dl_file_name
if not zipfile.is_zipfile(archive_path):
raise ValueError( raise ValueError(
"Downloaded Magisk/Kitsune archive is not a valid APK/ZIP. " "Downloaded Magisk/Kitsune archive is not a valid APK/ZIP. "
"Set REDROID_MAGISK_APK to a local APK or REDROID_MAGISK_URL to a working direct APK URL." "Set REDROID_MAGISK_APK to a local APK or REDROID_MAGISK_URL to a working direct APK URL."
) )
def _has_valid_local_archive(self):
if not os.path.isfile(self.dl_file_name):
return False
try:
self._validate_archive()
except ValueError:
return False
return True
def _download_remote_archive(self):
temp_path = self.dl_file_name + ".tmp"
try:
if os.path.isfile(temp_path):
os.remove(temp_path)
print_color(
"Trying online download for Kitsune Mask {} ...".format(self.release_version),
bcolors.GREEN,
)
download_file(self.dl_link, temp_path)
self._validate_archive(temp_path)
os.replace(temp_path, self.dl_file_name)
self._write_version()
except Exception:
if os.path.isfile(temp_path):
os.remove(temp_path)
raise
def resolve_release(self): def resolve_release(self):
print_color("Resolving Kitsune Mask stable release ...", bcolors.GREEN) print_color("Resolving Kitsune Mask stable release ...", bcolors.GREEN)
response = requests.get(self.stable_json, timeout=30) response = requests.get(self.stable_json, timeout=30)
@@ -174,41 +202,42 @@ on property:init.svc.zygote=stopped
self._validate_archive() self._validate_archive()
return return
if not self.custom_url and os.path.isfile(self.dl_file_name): local_valid = self._has_valid_local_archive()
self.release_version = self._cached_version() or "bundled" local_version = self._cached_version() or "bundled"
print_color(
"Using bundled Kitsune Mask {} ...".format(self.release_version),
bcolors.GREEN,
)
self._validate_archive()
return
if self.custom_url: try:
self.dl_link = self.custom_url if self.custom_url:
self.release_version = "url:" + os.path.basename(self.custom_url) self.dl_link = self.custom_url
else: self.release_version = "url:" + os.path.basename(self.custom_url)
self.resolve_release() else:
self.resolve_release()
except Exception as exc:
if local_valid:
self.release_version = local_version
print_color(
"Resolving Kitsune Mask online failed, falling back to local {} ({})".format(
self.release_version,
exc,
),
bcolors.YELLOW,
)
return
raise
if ( try:
os.path.isfile(self.dl_file_name) self._download_remote_archive()
and self._cached_version() == self.release_version except Exception as exc:
): if local_valid:
print_color( self.release_version = local_version
"Using cached Kitsune Mask {} ...".format(self.release_version), print_color(
bcolors.GREEN, "Downloading Kitsune Mask online failed, falling back to local {} ({})".format(
) self.release_version,
self._validate_archive() exc,
return ),
bcolors.YELLOW,
print_color( )
"Downloading Kitsune Mask {} now .....".format(self.release_version), return
bcolors.GREEN, raise
)
if os.path.isfile(self.dl_file_name):
os.remove(self.dl_file_name)
download_file(self.dl_link, self.dl_file_name)
self._validate_archive()
self._write_version()
def extract(self): def extract(self):
print_color("Extracting archive...", bcolors.GREEN) print_color("Extracting archive...", bcolors.GREEN)

View File

@@ -25,6 +25,15 @@ def get_download_dir():
def get_image_dir(): def get_image_dir():
return ensure_dir(IMAGE_ROOT) return ensure_dir(IMAGE_ROOT)
def file_md5(path, block_size=1024 * 1024):
hasher = hashlib.md5()
with open(path, "rb") as handle:
for chunk in iter(lambda: handle.read(block_size), b""):
hasher.update(chunk)
return hasher.hexdigest()
def run(args): def run(args):
result = subprocess.run(args=args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) result = subprocess.run(args=args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if result.stderr: if result.stderr:
@@ -36,23 +45,31 @@ def run(args):
) )
return result return result
def download_file(url, f_name):
md5 = "" def download_file(url, f_name, timeout=60):
response = requests.get(url, stream=True) response = requests.get(url, stream=True, timeout=timeout)
total_size_in_bytes = int(response.headers.get('content-length', 0)) response.raise_for_status()
block_size = 1024 # 1 Kibibyte
progress_bar = tqdm(total=total_size_in_bytes, unit='iB', unit_scale=True) total_size_in_bytes = int(response.headers.get("content-length", 0))
with open(f_name, 'wb') as file: block_size = 1024 * 1024
for data in response.iter_content(block_size): progress_bar = tqdm(total=total_size_in_bytes, unit="iB", unit_scale=True)
progress_bar.update(len(data)) hasher = hashlib.md5()
file.write(data)
progress_bar.close() try:
with open(f_name, "rb") as f: with open(f_name, "wb") as file:
bytes = f.read() for data in response.iter_content(block_size):
md5 = hashlib.md5(bytes).hexdigest() if not data:
continue
progress_bar.update(len(data))
file.write(data)
hasher.update(data)
finally:
progress_bar.close()
response.close()
if total_size_in_bytes != 0 and progress_bar.n != total_size_in_bytes: if total_size_in_bytes != 0 and progress_bar.n != total_size_in_bytes:
raise ValueError("Something went wrong while downloading") raise ValueError("Something went wrong while downloading")
return md5 return hasher.hexdigest()
def host(): def host():
machine = platform.machine().lower() machine = platform.machine().lower()