Compare commits

..
Author SHA1 Message Date
wbruno 8020dbd8fc Merge pull request 'Fix forgotten commas in microg.py' (#270) from MarSoft/patch-1 into main
Reviewed-on: #270
2026-10-12 14:00:52 -04:00
wbruno b7e1c37e14 Merge pull request 'Fix /data/adb/magisk permissions so Zygisk can initialise' (#284) from MisterWizzard/fix/magisk-data-adb-permissions into main
Reviewed-on: #284
2026-10-12 14:00:19 -04:00
wbruno 2f6547f873 Merge pull request 'feat: add device spoof to fix Play Store device compatibility' (#275) from EsymDnB/main into main
Reviewed-on: #275
2026-10-12 13:59:31 -04:00
Suggon 48dbfaf34a Update houdini library with extended expiry date (July 1, 2028)
Intel's libhoudini library includes a built-in "expiry date" that
prevents execution after a certain date. The previous versions expired
on January 1, 2026 (#256) and September 1, 2026 respectively.

This update refreshes the library to a newer build per
https://github.com/supremegamers/vendor_intel_proprietary_houdini/issues/6
and has an extended expiry date of July 1, 2028.
2026-09-11 14:56:36 +02:00
MisterWizzardandClaude Opus 5 b09e018894 Fix /data/adb/magisk permissions so Zygisk can initialise
General.install() runs copy() -> extra1() -> set_perm(). set_perm() only walks
self.files, which are paths under copy_dir (the overlay), so the copy that
Magisk.setup() makes into <data_dir>/adb/magisk keeps the 0644 the binaries
inherit from the apk extraction.

A non-executable /data/adb/magisk/magisk64 makes magisk_env() abort at
post-fs-data. Root still works, because that runs from the overlay copy, but
Zygisk never initialises: the Magisk app reports "Zygisk: No" no matter what the
zygisk setting says, and no error is surfaced to explain why. This breaks every
Zygisk module (LSPosed, Shamiko, PlayIntegrityFix, ...).

Fix setup() to apply the same 0755 to its own destination that
Magisk.set_path_perm() already applies to magisk paths.

Adds a regression test that passes with the fix and fails without it. It needs no
Waydroid install, no root and no extra dependencies:

    python3 tests/test_magisk_setup_perms.py

Fixes #283

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 09:49:05 +10:00
mcheesemanandClaude Sonnet 4.6 3bc2b310a3 feat: add device spoof to fix Play Store device compatibility
Adds a device_spoof module that writes Android device identity
properties into waydroid.cfg, enabling games like Destiny Rising
that reject Waydroid's generic device profile. Includes Pixel 6
and Samsung Galaxy S21 profiles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 20:03:41 +08:00
Семён Марьясин 963e3511fe Fix forgotten commas in microg.py
Choosing Minimal variants resulted in this without my fix:

  File "/nix/store/2a3cl1fvl0418qmkrvndx4si29w9yvv5-waydroid-script-0-unstable-2026-01-05/lib/python3.13/site-packages/stuff/microg.py", line 109, in __init__
    self.act_md5 = self.dl_links[variant][1]
                   ~~~~~~~~~~~~~~~~~~~~~~^^^
IndexError: list index out of range
2026-04-14 19:04:01 +03:00
8 changed files with 307 additions and 59 deletions
-10
View File
@@ -1,13 +1,3 @@
# What's the difference from this fork ?
I added some small code lines to enable this script for beeing used with "MULTIPLE ANDROID USERS IN WAYDROID"
--> It won't work with only a single user created (maybe fixing that in the future..)
--> You need to create at least "two users" in Waydroid to be able to get the IDs for Android verifications
# Waydroid Extras Script
Script to add GApps and other stuff to Waydroid!
+19 -2
View File
@@ -18,6 +18,7 @@ from stuff.nodataperm import Nodataperm
from stuff.smartdock import Smartdock
from stuff.widevine import Widevine
from stuff.fdroidpriv import FDroidPriv
from stuff.device_spoof import DeviceSpoof, PROFILES
import tools.helper as helper
from tools import container
from tools import images
@@ -89,6 +90,8 @@ def install_app(args):
install_list.append(Mitm(args.ca_cert_file))
if "fdroidpriv" in app:
install_list.append(FDroidPriv(args.android_version))
if "device_spoof" in app:
DeviceSpoof(getattr(args, "spoof_profile", "pixel6")).install()
if not container.use_overlayfs():
copy_dir = "/tmp/waydroid"
@@ -140,6 +143,8 @@ def remove_app(args):
remove_list.append(Mitm())
if "fdroidpriv" in app:
remove_list.append(FDroidPriv(args.android_version))
if "device_spoof" in app:
DeviceSpoof().uninstall()
if "nodataperm" in app:
remove_list.append(Nodataperm(args.android_version))
if "hidestatusbar" in app:
@@ -229,7 +234,7 @@ def interact():
if not action:
exit()
install_choices = ["gapps", "microg", "libndk", "libhoudini", "magisk", "smartdock", "fdroidpriv", "widevine",]
install_choices = ["gapps", "microg", "libndk", "libhoudini", "magisk", "smartdock", "fdroidpriv", "widevine", "device_spoof"]
hack_choices = []
if android_version=="11":
hack_choices.extend(["nodataperm", "hidestatusbar"])
@@ -251,6 +256,13 @@ def interact():
default="Standard",
).execute()
args.microg_variant = microg_variant
if "device_spoof" in apps:
spoof_profile = inquirer.select(
message="Select device profile to spoof",
choices=list(PROFILES.keys()),
default="pixel6",
).execute()
args.spoof_profile = spoof_profile
args.app = apps
install_app(args)
elif action == "Remove":
@@ -296,7 +308,7 @@ def main():
certified.set_defaults(func=get_certified)
install_choices = ["gapps", "microg", "libndk", "libhoudini",
"magisk", "mitm", "smartdock", "widevine"]
"magisk", "mitm", "smartdock", "widevine", "device_spoof"]
hack_choices = ["nodataperm", "hidestatusbar"]
micrg_variants = ["Standard", "NoGoolag", "UNLP", "Minimal", "MinimalIAP"]
remove_choices = install_choices
@@ -327,6 +339,11 @@ widevine: Add support for widevine DRM L3
dest='ca_cert_file',
help='[for mitm only] The CA certificate file (*.pem) to install',
default=None)
install_parser.add_argument('-p', '--spoof-profile',
dest='spoof_profile',
help='[for device_spoof only] Device profile to spoof',
choices=list(PROFILES.keys()),
default="pixel6")
install_parser.set_defaults(func=install_app)
# remove and its aliases
+13 -43
View File
@@ -1,51 +1,21 @@
from tools import container
from tools.helper import shell
from tools.logger import Logger
import re
class AndroidId:
def get_id(self):
if not container.is_running():
def get_id(self):
if container.is_running():
try:
queryout = shell(
arg="sqlite3 /data/data/com.google.android.gsf/databases/gservices.db \"select * from main where name = 'android_id';\"",
env="ANDROID_RUNTIME_ROOT=/apex/com.android.runtime ANDROID_DATA=/data ANDROID_TZDATA_ROOT=/apex/com.android.tzdata ANDROID_I18N_ROOT=/apex/com.android.i18n"
)
except:
return
else:
Logger.error("Please make sure Waydroid is running and Gapps has been installed!")
return
try:
# Get list of user IDs and their names using 'pm list users'
users_raw = shell(
arg="pm list users",
env="ANDROID_RUNTIME_ROOT=/apex/com.android.runtime ANDROID_DATA=/data ANDROID_TZDATA_ROOT=/apex/com.android.tzdata ANDROID_I18N_ROOT=/apex/com.android.i18n"
)
user_map = {}
for line in users_raw.strip().splitlines():
match = re.search(r'UserInfo\{(\d+):([^:}]+)', line)
if match:
user_id, username = match.groups()
user_map[user_id] = username
except Exception as e:
Logger.error(f"Failed to parse user list: {e}")
return
print("------------------------------------------------------------------------")
print(f"{'Username':<20} | {'User ID':^10} | {'GSF Android ID'}")
print("------------------------------------------------------------------------")
for user_id, username in user_map.items():
try:
db_path = f"/data/user/{user_id}/com.google.android.gsf/databases/gservices.db"
query = f"sqlite3 {db_path} \"select value from main where name = 'android_id';\""
gsf_id = shell(
arg=query,
env="ANDROID_RUNTIME_ROOT=/apex/com.android.runtime ANDROID_DATA=/data ANDROID_TZDATA_ROOT=/apex/com.android.tzdata ANDROID_I18N_ROOT=/apex/com.android.i18n"
).strip()
if gsf_id:
print(f"{username:<20} | {user_id:^10} | {gsf_id}")
else:
print(f"{username:<20} | {user_id:^10} | (not found)")
except:
print(f"{username:<20} | {user_id:^10} | (query error)")
print("------------------------------------------------------------------------")
print(queryout.replace("android_id|", "").strip())
print(" ^----- Open https://google.com/android/uncertified/?pli=1")
print(" Login with your Google ID, then submit the form with the ID(s) shown above.")
print(" Login with your google id then submit the form with id shown above")
+71
View File
@@ -0,0 +1,71 @@
import configparser
from tools.logger import Logger
PROFILES = {
"pixel6": {
"ro.product.brand": "google",
"ro.product.manufacturer": "Google",
"ro.product.model": "Pixel 6",
"ro.product.name": "oriole",
"ro.product.device": "oriole",
"ro.product.board": "oriole",
"ro.build.product": "oriole",
"ro.build.fingerprint": "google/oriole/oriole:13/TQ3A.230901.001/10750268:user/release-keys",
"ro.build.description": "oriole-user 13 TQ3A.230901.001 10750268 release-keys",
"ro.build.version.release": "13",
"ro.build.version.sdk": "33",
"ro.build.type": "user",
"ro.build.tags": "release-keys",
},
"samsung_s21": {
"ro.product.brand": "samsung",
"ro.product.manufacturer": "samsung",
"ro.product.model": "SM-G991B",
"ro.product.name": "o1sxeea",
"ro.product.device": "o1s",
"ro.product.board": "lahaina",
"ro.build.product": "o1s",
"ro.build.fingerprint": "samsung/o1sxeea/o1s:13/TP1A.220624.014/G991BXXU5EWCA:user/release-keys",
"ro.build.description": "o1sxeea-user 13 TP1A.220624.014 G991BXXU5EWCA release-keys",
"ro.build.version.release": "13",
"ro.build.version.sdk": "33",
"ro.build.type": "user",
"ro.build.tags": "release-keys",
},
}
PROP_KEYS = list(next(iter(PROFILES.values())).keys())
class DeviceSpoof:
id = "device_spoof"
def __init__(self, profile_name="pixel6") -> None:
if profile_name not in PROFILES:
raise ValueError(f"Unknown profile '{profile_name}'. Choose from: {list(PROFILES.keys())}")
self.profile_name = profile_name
self.apply_props = PROFILES[profile_name]
def _read_cfg(self):
cfg = configparser.ConfigParser()
cfg.read("/var/lib/waydroid/waydroid.cfg")
if not cfg.has_section("properties"):
cfg.add_section("properties")
return cfg
def install(self):
cfg = self._read_cfg()
for key, value in self.apply_props.items():
cfg.set("properties", key, value)
with open("/var/lib/waydroid/waydroid.cfg", "w") as f:
cfg.write(f)
Logger.info(f"Device spoofed as '{self.profile_name}'. Restart Waydroid to apply.")
def uninstall(self):
cfg = self._read_cfg()
for key in PROP_KEYS:
cfg.remove_option("properties", key)
with open("/var/lib/waydroid/waydroid.cfg", "w") as f:
cfg.write(f)
Logger.info("Device spoof properties removed. Restart Waydroid to apply.")
+1 -1
View File
@@ -10,7 +10,7 @@ class Houdini(General):
partition = "system"
dl_links = {
"11": ["https://github.com/supremegamers/vendor_intel_proprietary_houdini/archive/81f2a51ef539a35aead396ab7fce2adf89f46e88.zip", "fbff756612b4144797fbc99eadcb6653"],
"13": ["https://github.com/supremegamers/vendor_intel_proprietary_houdini/archive/9e77896350caccd228b36b2e1b4a994aa4bd48da.zip", "3807fe029559db3037efe245d9e74270"]
"13": ["https://github.com/supremegamers/vendor_intel_proprietary_houdini/archive/2f8f088671182e17e67321e098e8411a3972a628.zip", "37fe0899f1e4da7f9a724cca1c1ab1ea"]
}
act_md5 = ...
dl_link = ...
+22 -1
View File
@@ -64,7 +64,28 @@ on property:init.svc.zygote=stopped
Logger.info("Additional setup")
magisk_absolute_dir = os.path.join(self.copy_dir, self.magisk_dir)
data_dir = get_data_dir()
shutil.copytree(magisk_absolute_dir, os.path.join(data_dir, "adb", "magisk"), dirs_exist_ok=True)
data_magisk_dir = os.path.join(data_dir, "adb", "magisk")
shutil.copytree(magisk_absolute_dir, data_magisk_dir, dirs_exist_ok=True)
# General.install() calls set_perm() only over self.files, which are paths
# under copy_dir (the overlay). This /data/adb copy is made here, before
# that runs, so it keeps the 0644 the binaries inherited from the apk
# extraction. A non-executable /data/adb/magisk/magisk64 makes magisk_env()
# abort at post-fs-data: root still works from the overlay copy, but Zygisk
# silently never initialises (Magisk reports "Zygisk: No" no matter what
# the zygisk setting says). Apply the same perms set_path_perm() gives
# magisk paths, to this copy too.
self.set_data_magisk_perm(data_magisk_dir)
def set_data_magisk_perm(self, data_magisk_dir):
if not os.path.isdir(data_magisk_dir):
return
os.chmod(data_magisk_dir, 0o755)
for root, dirnames, filenames in os.walk(data_magisk_dir):
for dirname in dirnames:
os.chmod(os.path.join(root, dirname), 0o755)
for filename in filenames:
os.chmod(os.path.join(root, filename), 0o755)
def copy(self):
magisk_absolute_dir = os.path.join(self.copy_dir, self.magisk_dir)
+2 -2
View File
@@ -36,11 +36,11 @@ class MicroG(General):
"6136b383153c2a6797d14fb4d7ca3f97"
],
"Minimal": [
"https://github.com/ayasa520/MinMicroG/releases/download/latest/MinMicroG-Minimal-2.11.1-20230429100521.zip"
"https://github.com/ayasa520/MinMicroG/releases/download/latest/MinMicroG-Minimal-2.11.1-20230429100521.zip",
"afb87eb64e7749cfd72c4760d85849da"
],
"MinimalIAP": [
"https://github.com/ayasa520/MinMicroG/releases/download/latest/MinMicroG-MinimalIAP-2.11.1-20230429100556.zip"
"https://github.com/ayasa520/MinMicroG/releases/download/latest/MinMicroG-MinimalIAP-2.11.1-20230429100556.zip",
"cc071f4f776cbc16c4c1f707aff1f7fa"
]
}
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""Regression test for Magisk.setup() permissions on the /data/adb/magisk copy.
install() runs copy() -> extra1() -> set_perm(). set_perm() only walks self.files,
which are paths under copy_dir (the overlay), so the copy that setup() makes into
<data>/adb/magisk keeps the 0644 the binaries inherit from the apk extraction.
A non-executable /data/adb/magisk/magisk64 makes magisk_env() abort at post-fs-data.
Root still works (it runs from the overlay copy), but Zygisk never initialises and
the Magisk app reports "Zygisk: No" with no error explaining why.
Runs as an ordinary user, needs no Waydroid install and no extra dependencies:
python3 tests/test_magisk_setup_perms.py
"""
import os
import shutil
import stat
import sys
import tempfile
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import stuff.magisk as magisk_module
from stuff.magisk import Magisk
MAGISK_BINARIES = [
"magisk64",
"magiskinit",
"magiskboot",
"magiskpolicy",
"busybox",
"util_functions.sh",
]
def make_overlay(base):
"""Mimic the tree copy() leaves behind: binaries written 0644 by shutil.copyfile."""
magisk_dir = os.path.join(base, "system", "etc", "init", "magisk")
os.makedirs(os.path.join(magisk_dir, "chromeos"), exist_ok=True)
for name in MAGISK_BINARIES:
path = os.path.join(magisk_dir, name)
with open(path, "wb") as f:
f.write(b"\x7fELF placeholder")
os.chmod(path, 0o644)
nested = os.path.join(magisk_dir, "chromeos", "futility")
with open(nested, "wb") as f:
f.write(b"placeholder")
os.chmod(nested, 0o644)
return magisk_dir
def file_modes(root):
modes = {}
for dirpath, _dirnames, filenames in os.walk(root):
for name in filenames:
path = os.path.join(dirpath, name)
modes[os.path.relpath(path, root)] = stat.S_IMODE(os.stat(path).st_mode)
return modes
def test_plain_copytree_loses_exec_bit():
"""Document the original behaviour: a bare copytree leaves the binaries 0644."""
base = tempfile.mkdtemp()
try:
src = make_overlay(base)
dst = os.path.join(base, "data", "adb", "magisk")
os.makedirs(os.path.dirname(dst), exist_ok=True)
shutil.copytree(src, dst, dirs_exist_ok=True)
modes = file_modes(dst)
assert modes["magisk64"] == 0o644, (
"expected the pre-fix behaviour to reproduce, got %o" % modes["magisk64"]
)
finally:
shutil.rmtree(base, ignore_errors=True)
def test_setup_makes_data_copy_executable():
"""setup() must leave every file and dir in <data>/adb/magisk executable."""
base = tempfile.mkdtemp()
try:
make_overlay(base)
data_dir = os.path.join(base, "data")
os.makedirs(data_dir, exist_ok=True)
original_get_data_dir = magisk_module.get_data_dir
original_info = magisk_module.Logger.info
magisk_module.get_data_dir = lambda: data_dir
magisk_module.Logger.info = lambda *a, **k: None
class StubMagisk(Magisk):
@property
def copy_dir(self):
return base
try:
StubMagisk().setup()
finally:
magisk_module.get_data_dir = original_get_data_dir
magisk_module.Logger.info = original_info
dst = os.path.join(data_dir, "adb", "magisk")
modes = file_modes(dst)
assert modes, "setup() copied nothing"
not_executable = {n: oct(m) for n, m in modes.items() if m != 0o755}
assert not not_executable, "files left non-0755: %s" % not_executable
assert stat.S_IMODE(os.stat(dst).st_mode) == 0o755
nested = os.path.join(dst, "chromeos")
assert stat.S_IMODE(os.stat(nested).st_mode) == 0o755, "nested dir not fixed"
finally:
shutil.rmtree(base, ignore_errors=True)
def test_setup_is_idempotent():
base = tempfile.mkdtemp()
try:
make_overlay(base)
data_dir = os.path.join(base, "data")
os.makedirs(data_dir, exist_ok=True)
original_get_data_dir = magisk_module.get_data_dir
original_info = magisk_module.Logger.info
magisk_module.get_data_dir = lambda: data_dir
magisk_module.Logger.info = lambda *a, **k: None
class StubMagisk(Magisk):
@property
def copy_dir(self):
return base
try:
StubMagisk().setup()
StubMagisk().setup()
finally:
magisk_module.get_data_dir = original_get_data_dir
magisk_module.Logger.info = original_info
dst = os.path.join(data_dir, "adb", "magisk")
assert all(m == 0o755 for m in file_modes(dst).values())
finally:
shutil.rmtree(base, ignore_errors=True)
def test_perm_helper_tolerates_missing_dir():
Magisk().set_data_magisk_perm(os.path.join(tempfile.gettempdir(), "definitely-not-here"))
def main():
tests = [
test_plain_copytree_loses_exec_bit,
test_setup_makes_data_copy_executable,
test_setup_is_idempotent,
test_perm_helper_tolerates_missing_dir,
]
failed = 0
for test in tests:
try:
test()
except AssertionError as exc:
failed += 1
print("FAIL %s: %s" % (test.__name__, exc))
except Exception as exc: # noqa: BLE001 - surface anything unexpected
failed += 1
print("ERROR %s: %r" % (test.__name__, exc))
else:
print("ok %s" % test.__name__)
if failed:
print("\n%d test(s) failed" % failed)
return 1
print("\nall %d tests passed" % len(tests))
return 0
if __name__ == "__main__":
sys.exit(main())