Compare commits

...
Author SHA1 Message Date
wbruno b20483dd9a Merge pull request 'Update SmartDock to new version' (#218) from rduttshukla/feat/smartdock-update-and-permission-fix into main
Reviewed-on: #218
2026-10-12 14:01:14 -04:00
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
Rishi 2d3bd9432f Update SmartDock to new version and add WRITE_SECURE_SETTINGS to fix bootloop 2025-03-22 17:09:09 +05:30
7 changed files with 297 additions and 8 deletions
+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
+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"
]
}
+3 -2
View File
@@ -4,10 +4,10 @@ from stuff.general import General
class Smartdock(General):
id = "smartdock"
dl_link = "https://f-droid.org/repo/cu.axel.smartdock_1100.apk"
dl_link = "https://f-droid.org/repo/cu.axel.smartdock_1130.apk"
partition = "system"
dl_file_name = "smartdock.apk"
act_md5 = "f4087d34218eac902a5cca98ee03d215"
act_md5 = "6bfedb959ef5855c3782e8001cb67f86"
apply_props = { "qemu.hw.mainkeys" : "1" }
skip_extract = True
permissions = """<?xml version="1.0" encoding="utf-8"?>
@@ -30,6 +30,7 @@ class Smartdock(General):
<permission name="android.permission.ACCESS_SUPERUSER"/>
<permission name="android.permission.PACKAGE_USAGE_STATS" />
<permission name="android.permission.QUERY_ALL_PACKAGES" />
<permission name="android.permission.WRITE_SECURE_SETTINGS" />
</privapp-permissions>
</permissions>
"""
+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())