This commit is contained in:
eric
2026-04-04 00:03:05 -05:00
parent a57303c579
commit 415fdd3238
10 changed files with 255 additions and 65 deletions

4
.gitignore vendored
View File

@@ -4,3 +4,7 @@ Dockerfile
houdini/ houdini/
magisk/ magisk/
mindthegapps/ mindthegapps/
offline/downloads/*
!offline/downloads/.gitkeep
offline/images/*
!offline/images/.gitkeep

View File

@@ -1,48 +1,74 @@
# redroid-android13 # 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` - `redroid/redroid:13.0.0`
- 集成 `MindTheGapps` - `MindTheGapps`
- 集成 `libhoudini` - `libhoudini`
- 集成 `Kitsune Mask / Magisk Delta` - `Kitsune Mask / Magisk Delta`
- 启动后伪装成 `Pixel 7 Pro` - `Pixel 7 Pro` device profile
- 修复 `Magisk Installed N/A` 和设置页不可用的问题 - the `post-fs-data` fix that keeps `Magisk Installed` and the settings page healthy
## 保留的文件 ## Included files
- `redroid.py` - `redroid.py`
- `run_redroid.py` - `run_redroid.py`
- `bootstrap_redroid.py` - `bootstrap_redroid.py`
- `verify_offline_bundle.py`
- `offline/README.md`
- `offline/manifest.json`
- `stuff/mindthegapps.py` - `stuff/mindthegapps.py`
- `stuff/houdini.py` - `stuff/houdini.py`
- `stuff/houdini_hack.py` - `stuff/houdini_hack.py`
- `stuff/magisk.py` - `stuff/magisk.py`
- `assets/redroid_extreme_post_fs_data.sh` - `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 auto-loads the base image tar when `redroid/redroid:13.0.0-latest` is not already present in the local container runtime.
## Dependencies
```bash ```bash
python -m pip install -r requirements.txt 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 ```bash
python redroid.py -mtg -i -m python redroid.py -mtg -i -m
``` ```
这会生成并构建: Expected image tag:
```bash ```bash
redroid/redroid:13.0.0_mindthegapps_houdini_magisk redroid/redroid:13.0.0_mindthegapps_houdini_magisk
``` ```
## 启动容器 ## Launch the container
```bash ```bash
python run_redroid.py \ python run_redroid.py \
@@ -54,7 +80,7 @@ python run_redroid.py \
--replace --replace
``` ```
## 启动后做 5570 硬化 ## Apply the 5570 hardening
```bash ```bash
python bootstrap_redroid.py \ python bootstrap_redroid.py \
@@ -64,39 +90,39 @@ python bootstrap_redroid.py \
--install-extreme-script --install-extreme-script
``` ```
这一步会做 5 件事: This post-boot step:
- 跳过 SetupWizard - disables SetupWizard
- 标记系统已完成初始化 - marks the device provisioned
- 开启高精度定位模式 - enables high accuracy location mode
- 允许 `com.android.shell` 使用 mock location - allows `com.android.shell` to use mock location
- 应用 `Pixel 7 Pro / user / release-keys / locked / green` 画像 - 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应用命名空间可见 - `vendor/bin/su` stays visible to app namespaces after reboot
- `Pixel 7 Pro` 属性在每次开机时重新注入 - `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.product.model=Pixel 7 Pro`
- `ro.build.type=user` - `ro.build.type=user`
- `ro.build.tags=release-keys` - `ro.build.tags=release-keys`
- `ro.boot.vbmeta.device_state=locked` - `ro.boot.vbmeta.device_state=locked`
- `ro.boot.verifiedbootstate=green` - `ro.boot.verifiedbootstate=green`
- 应用 UID `su -V` 返回 `27001` - app UID `su -V = 27001`
- 应用 UID `su -mm -c id` 能拿到 `uid=0(root)` - app UID `su -mm -c id = uid=0(root)`
- `Kitsune` 首页的 `Installed` 正常显示 - `Kitsune` home page shows a normal `Installed` state
- `Kitsune` 设置页正常打开,`SuList / MagiskHide` 可配置 - `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 - camera HAL
- TikTok Live 专用补丁 - TikTok Live specific patches
后续如果要继续推进直播链路,直接在这个仓库往上叠加即可。

31
offline/README.md Normal file
View 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. If the base image tag is missing, `redroid.py` will automatically load `images/redroid-redroid-13.0.0-latest.tar`.
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.

View File

@@ -0,0 +1 @@

1
offline/images/.gitkeep Normal file
View File

@@ -0,0 +1 @@

53
offline/manifest.json Normal file
View 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"
}
]
}

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse import argparse
import os
import subprocess import subprocess
from stuff.houdini import Houdini from stuff.houdini import Houdini
@@ -13,6 +14,31 @@ import tools.helper as helper
SUPPORTED_ANDROID_VERSIONS = ["13.0.0"] 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,
)
if inspect.returncode == 0:
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(
"Base image {} is missing and offline archive {} was not found".format(
base_image,
tar_path,
)
)
subprocess.run([container_runtime, "load", "-i", tar_path], check=True)
return tar_path
def build_layers(args): def build_layers(args):
dockerfile = "FROM redroid/redroid:{}-latest\n".format(args.android) dockerfile = "FROM redroid/redroid:{}-latest\n".format(args.android)
tags = [args.android] tags = [args.android]
@@ -103,6 +129,7 @@ def main():
) )
args = parser.parse_args() args = parser.parse_args()
base_image_source = ensure_base_image(args.container, args.android)
dockerfile, tags = build_layers(args) dockerfile, tags = build_layers(args)
print("\nDockerfile\n{}".format(dockerfile)) print("\nDockerfile\n{}".format(dockerfile))
@@ -115,6 +142,7 @@ def main():
"Successfully built {}".format(image_name), "Successfully built {}".format(image_name),
helper.bcolors.GREEN, helper.bcolors.GREEN,
) )
print("Base image source: {}".format(base_image_source))
if args.houdini: if args.houdini:
print_recommended_flow(image_name) print_recommended_flow(image_name)

View File

@@ -174,6 +174,15 @@ 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):
self.release_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: if self.custom_url:
self.dl_link = self.custom_url self.dl_link = self.custom_url
self.release_version = "url:" + os.path.basename(self.custom_url) self.release_version = "url:" + os.path.basename(self.custom_url)

View File

@@ -1,39 +1,29 @@
import hashlib
import os import os
from pathlib import Path
import platform import platform
import subprocess import subprocess
import requests import requests
from tqdm import tqdm 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(): def get_download_dir():
download_loc = "" return ensure_dir(DOWNLOAD_ROOT)
current_user = (
os.environ.get("SUDO_USER")
or os.environ.get("USER") def get_image_dir():
or os.environ.get("USERNAME") return ensure_dir(IMAGE_ROOT)
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
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)

47
verify_offline_bundle.py Normal file
View 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()