This commit is contained in:
eric
2026-04-04 00:03:05 -05:00
parent a57303c579
commit 08b532236d
9 changed files with 254 additions and 65 deletions

4
.gitignore vendored
View File

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

View File

@@ -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 auto-loads the base image tar when `redroid/redroid:13.0.0-latest` is not already present in the local container runtime.
## 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
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 @@

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
import argparse
import os
import subprocess
from stuff.houdini import Houdini
@@ -13,6 +14,31 @@ 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,
)
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):
dockerfile = "FROM redroid/redroid:{}-latest\n".format(args.android)
tags = [args.android]
@@ -103,6 +129,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 +142,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)

View File

@@ -174,6 +174,15 @@ on property:init.svc.zygote=stopped
self._validate_archive()
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:
self.dl_link = self.custom_url
self.release_version = "url:" + os.path.basename(self.custom_url)

View File

@@ -1,39 +1,29 @@
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 run(args):
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()