Compare commits
2 Commits
main
...
codex/offl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0712fcdb84 | ||
|
|
415fdd3238 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -4,3 +4,7 @@ Dockerfile
|
||||
houdini/
|
||||
magisk/
|
||||
mindthegapps/
|
||||
offline/downloads/*
|
||||
!offline/downloads/.gitkeep
|
||||
offline/images/*
|
||||
!offline/images/.gitkeep
|
||||
|
||||
98
README.md
98
README.md
@@ -1,48 +1,74 @@
|
||||
# redroid-android13
|
||||
|
||||
这个仓库只保留了 `5570` 那套优化过的 Android 13 redroid 最小集,不再带 `gapps/litegapps/ndk/widevine` 这些无关入口。
|
||||
This repo is the minimal Android 13 redroid bundle that matches the optimized `5570` instance.
|
||||
|
||||
目标很明确:
|
||||
It keeps only the parts that were actually used:
|
||||
|
||||
- 基于 `redroid/redroid:13.0.0`
|
||||
- 集成 `MindTheGapps`
|
||||
- 集成 `libhoudini`
|
||||
- 集成 `Kitsune Mask / Magisk Delta`
|
||||
- 启动后伪装成 `Pixel 7 Pro`
|
||||
- 修复 `Magisk Installed N/A` 和设置页不可用的问题
|
||||
- `redroid/redroid:13.0.0`
|
||||
- `MindTheGapps`
|
||||
- `libhoudini`
|
||||
- `Kitsune Mask / Magisk Delta`
|
||||
- `Pixel 7 Pro` device profile
|
||||
- the `post-fs-data` fix that keeps `Magisk Installed` and the settings page healthy
|
||||
|
||||
## 保留的文件
|
||||
## Included files
|
||||
|
||||
- `redroid.py`
|
||||
- `run_redroid.py`
|
||||
- `bootstrap_redroid.py`
|
||||
- `verify_offline_bundle.py`
|
||||
- `offline/README.md`
|
||||
- `offline/manifest.json`
|
||||
- `stuff/mindthegapps.py`
|
||||
- `stuff/houdini.py`
|
||||
- `stuff/houdini_hack.py`
|
||||
- `stuff/magisk.py`
|
||||
- `assets/redroid_extreme_post_fs_data.sh`
|
||||
- local `offline/downloads/`
|
||||
- local `offline/images/`
|
||||
|
||||
## 依赖
|
||||
## Offline archive
|
||||
|
||||
The repo tracks the offline bundle metadata under [offline](C:/Users/admin/Desktop/redroid-android13/offline). The large zip/apk/tar payloads are intentionally kept out of Git and should be stored locally under `offline/downloads/` and `offline/images/`.
|
||||
|
||||
Use [offline/manifest.json](C:/Users/admin/Desktop/redroid-android13/offline/manifest.json) for checksums and source metadata, and [offline/README.md](C:/Users/admin/Desktop/redroid-android13/offline/README.md) for the expected local layout.
|
||||
|
||||
Expected local files:
|
||||
|
||||
- `offline/downloads/mindthegapps.zip`
|
||||
- `offline/downloads/libhoudini.zip`
|
||||
- `offline/downloads/libhoudini_hack.zip`
|
||||
- `offline/downloads/magisk.apk`
|
||||
- `offline/downloads/magisk.version`
|
||||
- `offline/images/redroid-redroid-13.0.0-latest.tar`
|
||||
|
||||
`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
|
||||
|
||||
```bash
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
容器侧默认按 `docker` 写,`podman` 也能用。
|
||||
## Verify the offline bundle
|
||||
|
||||
## 构建 5570 对应镜像
|
||||
```bash
|
||||
python verify_offline_bundle.py
|
||||
```
|
||||
|
||||
## Build the 5570 image
|
||||
|
||||
```bash
|
||||
python redroid.py -mtg -i -m
|
||||
```
|
||||
|
||||
这会生成并构建:
|
||||
Expected image tag:
|
||||
|
||||
```bash
|
||||
redroid/redroid:13.0.0_mindthegapps_houdini_magisk
|
||||
```
|
||||
|
||||
## 启动容器
|
||||
## Launch the container
|
||||
|
||||
```bash
|
||||
python run_redroid.py \
|
||||
@@ -54,7 +80,7 @@ python run_redroid.py \
|
||||
--replace
|
||||
```
|
||||
|
||||
## 启动后做 5570 硬化
|
||||
## Apply the 5570 hardening
|
||||
|
||||
```bash
|
||||
python bootstrap_redroid.py \
|
||||
@@ -64,39 +90,39 @@ python bootstrap_redroid.py \
|
||||
--install-extreme-script
|
||||
```
|
||||
|
||||
这一步会做 5 件事:
|
||||
This post-boot step:
|
||||
|
||||
- 跳过 SetupWizard
|
||||
- 标记系统已完成初始化
|
||||
- 开启高精度定位模式
|
||||
- 允许 `com.android.shell` 使用 mock location
|
||||
- 应用 `Pixel 7 Pro / user / release-keys / locked / green` 画像
|
||||
- disables SetupWizard
|
||||
- marks the device provisioned
|
||||
- enables high accuracy location mode
|
||||
- allows `com.android.shell` to use mock location
|
||||
- applies the `Pixel 7 Pro / user / release-keys / locked / green` identity
|
||||
|
||||
如果带上 `--install-extreme-script`,还会把 [`assets/redroid_extreme_post_fs_data.sh`](C:\Users\admin\Desktop\redroid-android13\assets\redroid_extreme_post_fs_data.sh) 安装到 `/data/adb/post-fs-data.d/20-redroid-extreme.sh`,让下面两件事在重启后继续成立:
|
||||
With `--install-extreme-script`, the validated script [assets/redroid_extreme_post_fs_data.sh](C:/Users/admin/Desktop/redroid-android13/assets/redroid_extreme_post_fs_data.sh) is copied to `/data/adb/post-fs-data.d/20-redroid-extreme.sh` so that:
|
||||
|
||||
- `vendor/bin/su` 指向 Magisk applet,应用命名空间可见
|
||||
- `Pixel 7 Pro` 属性在每次开机时重新注入
|
||||
- `vendor/bin/su` stays visible to app namespaces after reboot
|
||||
- `Pixel 7 Pro` properties are re-applied on every boot
|
||||
|
||||
## 已验证结果
|
||||
## Verified 5570 state
|
||||
|
||||
这套仓库对应的 `5570` 实例已经验证过:
|
||||
The matching `5570` instance was already validated with:
|
||||
|
||||
- `ro.product.model=Pixel 7 Pro`
|
||||
- `ro.build.type=user`
|
||||
- `ro.build.tags=release-keys`
|
||||
- `ro.boot.vbmeta.device_state=locked`
|
||||
- `ro.boot.verifiedbootstate=green`
|
||||
- 应用 UID 下 `su -V` 返回 `27001`
|
||||
- 应用 UID 下 `su -mm -c id` 能拿到 `uid=0(root)`
|
||||
- `Kitsune` 首页的 `Installed` 正常显示
|
||||
- `Kitsune` 设置页正常打开,`SuList / MagiskHide` 可配置
|
||||
- app UID `su -V = 27001`
|
||||
- app UID `su -mm -c id = uid=0(root)`
|
||||
- `Kitsune` home page shows a normal `Installed` state
|
||||
- `Kitsune` settings page opens normally and `SuList / MagiskHide` is configurable
|
||||
|
||||
## 当前范围
|
||||
## Current scope
|
||||
|
||||
这个仓库只负责把 `5570` 的系统环境复现出来,不包含:
|
||||
This repo only reproduces the system environment for the optimized `5570` build.
|
||||
|
||||
- 虚拟摄像头
|
||||
It does not yet include:
|
||||
|
||||
- virtual camera
|
||||
- camera HAL
|
||||
- TikTok Live 专用补丁
|
||||
|
||||
后续如果要继续推进直播链路,直接在这个仓库往上叠加即可。
|
||||
- TikTok Live specific patches
|
||||
|
||||
31
offline/README.md
Normal file
31
offline/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Offline Bundle
|
||||
|
||||
This directory describes the local offline bundle required by the current Android 13 `5570` build path.
|
||||
|
||||
The large bundle payloads are intentionally not tracked in Git. Keep them in the local `downloads/` and `images/` directories and use `manifest.json` plus `verify_offline_bundle.py` to validate what is on disk.
|
||||
|
||||
## Layout
|
||||
|
||||
- `downloads/`
|
||||
- `mindthegapps.zip`
|
||||
- `libhoudini.zip`
|
||||
- `libhoudini_hack.zip`
|
||||
- `magisk.apk`
|
||||
- `magisk.version`
|
||||
- `images/`
|
||||
- `redroid-redroid-13.0.0-latest.tar`
|
||||
- `manifest.json`
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
python verify_offline_bundle.py
|
||||
```
|
||||
|
||||
## Offline build flow
|
||||
|
||||
1. Make sure your container runtime can load local image archives.
|
||||
2. Populate `downloads/` and `images/` with the files listed in `manifest.json`.
|
||||
3. Run `python redroid.py -mtg -i -m`.
|
||||
4. The build first tries to pull the base image and download dependency archives from their online sources.
|
||||
5. If an online pull/download fails, the build falls back to the validated files in `downloads/` and `images/`.
|
||||
1
offline/downloads/.gitkeep
Normal file
1
offline/downloads/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
offline/images/.gitkeep
Normal file
1
offline/images/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
53
offline/manifest.json
Normal file
53
offline/manifest.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"generated_at": "2026-04-03",
|
||||
"artifacts": [
|
||||
{
|
||||
"name": "mindthegapps.zip",
|
||||
"relative_path": "offline/downloads/mindthegapps.zip",
|
||||
"size": 197927264,
|
||||
"md5": "eee87a540b6e778f3a114fff29e133aa",
|
||||
"sha256": "9fcb1531cb72df521bd49af5443f7a1b269ebb9c06d8f7dfa37b975da9ec0af1",
|
||||
"source": "https://github.com/s1204IT/MindTheGappsBuilder/releases/download/20240226/MindTheGapps-13.0.0-x86_64-20240226.zip"
|
||||
},
|
||||
{
|
||||
"name": "libhoudini.zip",
|
||||
"relative_path": "offline/downloads/libhoudini.zip",
|
||||
"size": 75100163,
|
||||
"md5": "cb7ffac26d47ec7c89df43818e126b47",
|
||||
"sha256": "7c78abbcced1f7c5c1cc794a0afb4010a2e5bb0bb18eadd6de3af2fe13e9dc25",
|
||||
"source": "https://github.com/rote66/vendor_intel_proprietary_houdini/archive/debc3dc91cf12b5c5b8a1c546a5b0b7bf7f838a8.zip"
|
||||
},
|
||||
{
|
||||
"name": "libhoudini_hack.zip",
|
||||
"relative_path": "offline/downloads/libhoudini_hack.zip",
|
||||
"size": 88394,
|
||||
"md5": "8f71a58f3e54eca879a2f7de64dbed58",
|
||||
"sha256": "5c904d6db5e15adc37ffadde5f21437ccabf86c1e94a3a0e4f77371a90776983",
|
||||
"source": "https://github.com/rote66/redroid_libhoudini_hack/archive/a2194c5e294cbbfdfe87e51eb9eddb4c3621d8c3.zip"
|
||||
},
|
||||
{
|
||||
"name": "magisk.apk",
|
||||
"relative_path": "offline/downloads/magisk.apk",
|
||||
"size": 21099562,
|
||||
"md5": "bb730e78db0bb9dde2fccfa7bfb1b35c",
|
||||
"sha256": "af43697f67a229cf824494686e8ee07cb5668b57c9671a555e2a869c5bb64649",
|
||||
"source": "Pulled from the validated 5570 instance: /vendor/etc/magisk/magisk.apk"
|
||||
},
|
||||
{
|
||||
"name": "magisk.version",
|
||||
"relative_path": "offline/downloads/magisk.version",
|
||||
"size": 10,
|
||||
"md5": "57de9facce3180844a0460e9680c3836",
|
||||
"sha256": "e82ddd5337dd7a7da9f2215c7272f5f26a4fd3b50d9676dea23d6d69a62c46ee",
|
||||
"source": "Version name from the validated 5570 instance: b149cd26"
|
||||
},
|
||||
{
|
||||
"name": "redroid-redroid-13.0.0-latest.tar",
|
||||
"relative_path": "offline/images/redroid-redroid-13.0.0-latest.tar",
|
||||
"size": 2028835328,
|
||||
"md5": "a2c44b2d90fb3445be9c2d6df867f960",
|
||||
"sha256": "4474a84a78c62a793ca24c20b7159dc678196bd88a1ef4d9511b83e7090d1179",
|
||||
"source": "docker save redroid/redroid:13.0.0-latest"
|
||||
}
|
||||
]
|
||||
}
|
||||
46
redroid.py
46
redroid.py
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from stuff.houdini import Houdini
|
||||
@@ -13,6 +14,49 @@ import tools.helper as helper
|
||||
SUPPORTED_ANDROID_VERSIONS = ["13.0.0"]
|
||||
|
||||
|
||||
def ensure_base_image(container_runtime, android_version):
|
||||
base_image = "redroid/redroid:{}-latest".format(android_version)
|
||||
inspect = subprocess.run(
|
||||
[container_runtime, "image", "inspect", base_image],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=False,
|
||||
)
|
||||
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"
|
||||
|
||||
tar_name = "redroid-redroid-{}-latest.tar".format(android_version)
|
||||
tar_path = os.path.join(helper.get_image_dir(), tar_name)
|
||||
if not os.path.isfile(tar_path):
|
||||
raise FileNotFoundError(
|
||||
"Online pull for {} failed, local image cache is missing, and offline archive {} was not found".format(
|
||||
base_image,
|
||||
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)
|
||||
return tar_path
|
||||
|
||||
|
||||
def build_layers(args):
|
||||
dockerfile = "FROM redroid/redroid:{}-latest\n".format(args.android)
|
||||
tags = [args.android]
|
||||
@@ -103,6 +147,7 @@ def main():
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
base_image_source = ensure_base_image(args.container, args.android)
|
||||
dockerfile, tags = build_layers(args)
|
||||
|
||||
print("\nDockerfile\n{}".format(dockerfile))
|
||||
@@ -115,6 +160,7 @@ def main():
|
||||
"Successfully built {}".format(image_name),
|
||||
helper.bcolors.GREEN,
|
||||
)
|
||||
print("Base image source: {}".format(base_image_source))
|
||||
|
||||
if args.houdini:
|
||||
print_recommended_flow(image_name)
|
||||
|
||||
@@ -1,24 +1,49 @@
|
||||
|
||||
|
||||
import os
|
||||
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:
|
||||
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):
|
||||
loc_md5 = ""
|
||||
if os.path.isfile(self.dl_file_name):
|
||||
with open(self.dl_file_name,"rb") as f:
|
||||
bytes = f.read()
|
||||
loc_md5 = hashlib.md5(bytes).hexdigest()
|
||||
while not os.path.isfile(self.dl_file_name) or loc_md5 != self.act_md5:
|
||||
if os.path.isfile(self.dl_file_name):
|
||||
os.remove(self.dl_file_name)
|
||||
print_color("md5 mismatches, redownloading now ....",bcolors.YELLOW)
|
||||
loc_md5 = download_file(self.dl_link, self.dl_file_name)
|
||||
|
||||
temp_path = self.dl_file_name + ".tmp"
|
||||
local_valid = self._has_valid_local_file()
|
||||
|
||||
try:
|
||||
if os.path.isfile(temp_path):
|
||||
os.remove(temp_path)
|
||||
print_color("Trying online download first ...", bcolors.GREEN)
|
||||
remote_md5 = download_file(self.dl_link, temp_path)
|
||||
if remote_md5 != self.act_md5:
|
||||
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):
|
||||
print_color("Extracting archive...", bcolors.GREEN)
|
||||
print(self.dl_file_name)
|
||||
|
||||
@@ -137,13 +137,41 @@ on property:init.svc.zygote=stopped
|
||||
with open(self.release_meta_file, "w", encoding="utf-8") as meta_file:
|
||||
meta_file.write(self.release_version)
|
||||
|
||||
def _validate_archive(self):
|
||||
if not zipfile.is_zipfile(self.dl_file_name):
|
||||
def _validate_archive(self, path=None):
|
||||
archive_path = path or self.dl_file_name
|
||||
if not zipfile.is_zipfile(archive_path):
|
||||
raise ValueError(
|
||||
"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."
|
||||
)
|
||||
|
||||
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):
|
||||
print_color("Resolving Kitsune Mask stable release ...", bcolors.GREEN)
|
||||
response = requests.get(self.stable_json, timeout=30)
|
||||
@@ -174,32 +202,42 @@ on property:init.svc.zygote=stopped
|
||||
self._validate_archive()
|
||||
return
|
||||
|
||||
if self.custom_url:
|
||||
self.dl_link = self.custom_url
|
||||
self.release_version = "url:" + os.path.basename(self.custom_url)
|
||||
else:
|
||||
self.resolve_release()
|
||||
local_valid = self._has_valid_local_archive()
|
||||
local_version = self._cached_version() or "bundled"
|
||||
|
||||
if (
|
||||
os.path.isfile(self.dl_file_name)
|
||||
and self._cached_version() == self.release_version
|
||||
):
|
||||
print_color(
|
||||
"Using cached Kitsune Mask {} ...".format(self.release_version),
|
||||
bcolors.GREEN,
|
||||
)
|
||||
self._validate_archive()
|
||||
return
|
||||
try:
|
||||
if self.custom_url:
|
||||
self.dl_link = self.custom_url
|
||||
self.release_version = "url:" + os.path.basename(self.custom_url)
|
||||
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
|
||||
|
||||
print_color(
|
||||
"Downloading Kitsune Mask {} now .....".format(self.release_version),
|
||||
bcolors.GREEN,
|
||||
)
|
||||
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()
|
||||
try:
|
||||
self._download_remote_archive()
|
||||
except Exception as exc:
|
||||
if local_valid:
|
||||
self.release_version = local_version
|
||||
print_color(
|
||||
"Downloading Kitsune Mask online failed, falling back to local {} ({})".format(
|
||||
self.release_version,
|
||||
exc,
|
||||
),
|
||||
bcolors.YELLOW,
|
||||
)
|
||||
return
|
||||
raise
|
||||
|
||||
def extract(self):
|
||||
print_color("Extracting archive...", bcolors.GREEN)
|
||||
|
||||
@@ -1,39 +1,38 @@
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
import requests
|
||||
from tqdm import tqdm
|
||||
import hashlib
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
OFFLINE_ROOT = REPO_ROOT / "offline"
|
||||
DOWNLOAD_ROOT = OFFLINE_ROOT / "downloads"
|
||||
IMAGE_ROOT = OFFLINE_ROOT / "images"
|
||||
|
||||
|
||||
def ensure_dir(path):
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return str(path)
|
||||
|
||||
def get_download_dir():
|
||||
download_loc = ""
|
||||
current_user = (
|
||||
os.environ.get("SUDO_USER")
|
||||
or os.environ.get("USER")
|
||||
or os.environ.get("USERNAME")
|
||||
or "user"
|
||||
)
|
||||
if os.environ.get("XDG_CACHE_HOME", None) is None:
|
||||
if os.name == "nt":
|
||||
download_loc = os.path.join(
|
||||
os.environ.get("LOCALAPPDATA", os.path.expanduser("~")),
|
||||
"redroid",
|
||||
"downloads",
|
||||
)
|
||||
else:
|
||||
download_loc = os.path.join(
|
||||
'/',
|
||||
"home",
|
||||
current_user,
|
||||
".cache",
|
||||
"redroid",
|
||||
"downloads",
|
||||
)
|
||||
else:
|
||||
download_loc = os.path.join(os.environ["XDG_CACHE_HOME"], "redroid", "downloads")
|
||||
if not os.path.exists(download_loc):
|
||||
os.makedirs(download_loc)
|
||||
return download_loc
|
||||
return ensure_dir(DOWNLOAD_ROOT)
|
||||
|
||||
|
||||
def get_image_dir():
|
||||
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):
|
||||
result = subprocess.run(args=args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
@@ -46,23 +45,31 @@ def run(args):
|
||||
)
|
||||
return result
|
||||
|
||||
def download_file(url, f_name):
|
||||
md5 = ""
|
||||
response = requests.get(url, stream=True)
|
||||
total_size_in_bytes = int(response.headers.get('content-length', 0))
|
||||
block_size = 1024 # 1 Kibibyte
|
||||
progress_bar = tqdm(total=total_size_in_bytes, unit='iB', unit_scale=True)
|
||||
with open(f_name, 'wb') as file:
|
||||
for data in response.iter_content(block_size):
|
||||
progress_bar.update(len(data))
|
||||
file.write(data)
|
||||
progress_bar.close()
|
||||
with open(f_name, "rb") as f:
|
||||
bytes = f.read()
|
||||
md5 = hashlib.md5(bytes).hexdigest()
|
||||
|
||||
def download_file(url, f_name, timeout=60):
|
||||
response = requests.get(url, stream=True, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
|
||||
total_size_in_bytes = int(response.headers.get("content-length", 0))
|
||||
block_size = 1024 * 1024
|
||||
progress_bar = tqdm(total=total_size_in_bytes, unit="iB", unit_scale=True)
|
||||
hasher = hashlib.md5()
|
||||
|
||||
try:
|
||||
with open(f_name, "wb") as file:
|
||||
for data in response.iter_content(block_size):
|
||||
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:
|
||||
raise ValueError("Something went wrong while downloading")
|
||||
return md5
|
||||
return hasher.hexdigest()
|
||||
|
||||
def host():
|
||||
machine = platform.machine().lower()
|
||||
|
||||
47
verify_offline_bundle.py
Normal file
47
verify_offline_bundle.py
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
MANIFEST = ROOT / "offline" / "manifest.json"
|
||||
|
||||
|
||||
def digest(path, algorithm):
|
||||
hasher = hashlib.new(algorithm)
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
hasher.update(chunk)
|
||||
return hasher.hexdigest()
|
||||
|
||||
|
||||
def main():
|
||||
manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
|
||||
failures = []
|
||||
for entry in manifest["artifacts"]:
|
||||
path = ROOT / entry["relative_path"]
|
||||
if not path.is_file():
|
||||
failures.append("{} missing".format(entry["relative_path"]))
|
||||
continue
|
||||
if path.stat().st_size != entry["size"]:
|
||||
failures.append("{} size mismatch".format(entry["relative_path"]))
|
||||
continue
|
||||
sha256 = digest(path, "sha256")
|
||||
if sha256 != entry["sha256"]:
|
||||
failures.append("{} sha256 mismatch".format(entry["relative_path"]))
|
||||
continue
|
||||
print("OK {}".format(entry["relative_path"]))
|
||||
|
||||
if failures:
|
||||
for item in failures:
|
||||
print("FAIL {}".format(item))
|
||||
sys.exit(1)
|
||||
|
||||
print("Offline bundle is complete.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user