Build verified Morphe-patched Android APKs and modules
Project description
Morphe Builder
Verified Morphe-patched Android builds from stock Google Play splits, configured in one TOML file.
Morphe Builder downloads stock APK splits from Google Play, verifies their integrity and signing certificate, merges them, patches them with the Morphe CLI, signs the result, and packages non-root APKs plus deterministic Magisk/KernelSU modules.
Every app, architecture, patch source, and tool version is declared in a single TOML file, so a fork can build its own release matrix in CI without touching Python code. Nothing untrusted is published: downloads stay in a temporary directory until manifest coverage, hashes, package identity, version, ABI, and signer all pass, and each published set carries deterministic provenance.
Google Play delivery uses pinned goopdl releases from PyPI, with verified direct, archive, APKMirror, and Uptodown fallbacks.
Requirements
| Requirement | Version | Notes |
|---|---|---|
Python |
3.11–3.13 | 3.11 minimum for standard-library tomllib |
Java |
21+ | Required by Morphe CLI, APKEditor, and uber-apk-signer |
uv |
latest | Only for development installs from source |
Windows and Linux are supported. See docs/security.md for platform limits.
Features
- Verified downloads: Google manifest coverage, safe paths, sizes, SHA-256 hashes, package identity, version, split requirements, ABI, and signing certificate are all checked before publication.
- Signer pinning: YouTube and YouTube Music require known Google signers; any app can pin its own
expected-signerSHA-256. - Provider fallback: fixed order
google-play → direct → archive → apkmirror → uptodown; integrity failures stay terminal instead of falling through to a later provider. - Config-driven matrix: apps, architectures, densities, patch sets, and module output live in TOML; no hardcoded app registry.
- Per-app toolchains: each app can override Morphe CLI version, patch source, and patch version, resolved and cached per
owner/repo@tag. - Pinned, hashed tooling: Morphe CLI, patches, APKEditor, and uber-apk-signer are resolved to exact tags with recorded asset hashes in
toolchain-provenance.json. - Non-root APKs and modules: deterministic Magisk/KernelSU modules that install matching stock, survive reboots, and receive updates from the Magisk app.
- Reproducible provenance:
provenance.json,build-summary.json,release-manifest.json, andCHANGELOG.mdrecord every source, version, and hash. - Atomic publication: existing output is never overwritten; a failed job removes its temporary set and publishes nothing.
- Verified CI releases: daily update checks resolve upstream versions, skip unchanged builds, verify every artifact, and attach GitHub artifact attestations.
- Secret-safe by design: no committed credentials, no cached tokens or APK payloads, redacted error output.
Installation
Morphe Builder depends on reviewed, exactly pinned goopdl releases from PyPI.
Global tool install:
uv tool install morphe-builder
Into an existing environment:
python -m pip install morphe-builder
Development install from source:
git clone https://github.com/Villoh/morphe-builder
cd morphe-builder
uv sync
uv run morphe-builder --help
Quick start
morphe-builder auth --arch arm64
morphe-builder tools
MORPHE_KEYSTORE_PASSWORD=secret morphe-builder build morphe.toml \
--keystore /path/outside/repository/builder.p12 \
--output build
auth stores Google Play credentials outside the repository, tools fetches and hashes the pinned Java toolchain, and build produces the full configured matrix from morphe.toml.
Google Play access
Set these two together to download directly from Google's servers through the goopdl fork. This is the recommended setup: it is the only reliable path from CI runners, and it avoids the shared anonymous dispenser entirely.
| Variable | Value |
|---|---|
GOOPDL_ACCOUNT_EMAIL |
Google account email used for Play authentication. |
GOOPDL_AAS_TOKEN |
Persistent AAS token for that account. Must start with aas_et/. |
A value starting with g.a is a temporary bearer token, not an AAS token, and is rejected. Both variables are required together; a partial configuration fails instead of silently falling back. Treat the token as a password: use a dedicated account, keep it only in environment variables or GitHub Environment secrets, and never place it in TOML, argv, or committed files. Changing the account password invalidates it.
How to generate an AAS token with goopdl
goopdl aastoken prints a Google account AAS token. Credentials and tokens are used in memory only; no credentials.txt or token.txt file is created.
OAuth mode (recommended)
goopdl aastoken --oauth
Get the required one-time oauth_token:
- Open Google EmbeddedSetup.
- Sign in and select I agree.
- The page may stay frozen or loading forever. This is expected; the cookie is normally already available.
- Open browser developer tools (F12).
- In Chrome or Edge, open Application › Cookies ›
https://accounts.google.com. - Copy the value of the
oauth_tokencookie. It starts withoauth2_4/. - Run
goopdl aastoken --oauth, enter the same email, then paste the token. Both inputs are handled interactively and the token is hidden.
The oauth_token is short-lived and single-use. The resulting AAS token is printed to the console.
Password or app-password mode
# Interactive: asks for email and hides password input
goopdl aastoken
# Arguments: convenient, but password remains in shell history
goopdl aastoken user@example.com password
Google commonly rejects both account passwords and 16-character app passwords with BadAuthentication. Keep 2-Step Verification enabled and use OAuth mode instead.
Without these variables, goopdl falls back to the public anonymous dispenser, which is frequently blocked from datacenter IP ranges. In that case the builder continues through the verified APKMirror and Uptodown fallbacks declared in your configuration.
Other environment variables
| Variable | Used by | Purpose |
|---|---|---|
MORPHE_KEYSTORE_PASSWORD |
build |
Signing keystore password. Required; never accepted in argv. |
MORPHE_DISPENSER_URL |
auth, download |
Custom anonymous dispenser, used only when no Google account is configured. |
MORPHE_MODULE_VERSION_CODE |
build |
Monotonic module versionCode. CI passes the workflow run number. |
GITHUB_TOKEN |
tools, build, list-patches |
Authenticates GitHub API calls. Never sent to asset downloads. |
HTTP_PROXY, HTTPS_PROXY |
build |
Proxy for provider downloads. |
XDG_CACHE_HOME, LOCALAPPDATA |
all cached commands | Override the default cache root. |
Configuration
Everything a fork needs to change lives in morphe.toml. No Python code has to be touched to build a different set of apps.
The file has one [toolchain] table with the default Morphe CLI and patch sources, then one ordered [[apps]] entry per app:
[toolchain]
morphe-source = "MorpheApp/morphe-desktop"
morphe-version = "latest"
patches-source = "MorpheApp/morphe-patches"
patches-version = "latest"
[[apps]]
package = "com.google.android.youtube"
name = "YouTube"
slug = "youtube"
patched-package = "app.morphe.android.youtube"
version = "auto"
build-mode = "apk"
arch = "arm64-v8a"
density = "xxhdpi"
include-patches = ["GmsCore support", "SponsorBlock"]
exclude-patches = []
[apps.patch-options."SponsorBlock"]
toast-on-connection-error = true
[apps.google-play]
profile = "D2"
country = "US"
[apps.fallbacks]
apkmirror = ["https://www.apkmirror.com/apk/google-inc/youtube/"]
Only package and name are required. Everything else has a documented default:
| Setting | Required | Default when omitted |
|---|---|---|
package |
yes | Validated Android package name. |
name |
yes | Display name used in release notes and modules. |
slug |
no | package with dots replaced by hyphens. Controls output, cache, and module names; must be unique. |
patched-package |
no | No package change is expected after patching. Set it when patches rename the package. |
version |
no | auto: highest version supported by every selected patch. Also accepts latest or an exact version name. |
version-code |
no | Resolved from the version name. Requires an explicit version; auto and latest are rejected with it. |
build-mode |
no | apk: non-root APK only. Use module or both for Magisk/KernelSU modules. |
arch |
no | arm64-v8a. Also arm-v7a, all, or both (two independent jobs). |
density |
no | The profile's only available density split is selected automatically. |
include-patches |
no | Empty: the patch bundle's default selection is used. |
exclude-patches |
no | Empty: nothing is removed from the selection. |
exclusive-patches |
no | Unset. When set, only these patches apply; cannot be combined with include/exclude. |
patch-options |
no | Patch defaults. Scalar values only, never shell-interpolated. Every option must belong to a selected patch. |
expected-signer |
no | Unpinned: a valid provider-served signer is accepted and logged as unpinned signer accepted. YouTube and YouTube Music always enforce built-in Google signer pins. |
google-play.profile |
no | goopdl picks a profile. Leave unset with arch = "both", since one profile cannot cover both ABIs. |
google-play.country |
no | goopdl account region. |
google-play.proxy |
no | Direct connection. |
google-play.dispenser |
no | MORPHE_DISPENSER_URL, or the built-in public dispenser. |
fallbacks.* |
no | Empty: Google Play is the only source, so a Play failure fails the job. |
patches-source, patches-version, cli-source, morphe-version |
no | The [toolchain] values. |
The [toolchain] table itself is optional; omitting it uses MorpheApp/morphe-desktop and MorpheApp/morphe-patches at latest.
Parsing is strict: unknown keys, duplicate packages, duplicate slugs, unsupported values, and conflicting patch selections are all rejected before any download starts. Run morphe-builder list-patches morphe.toml to see which version each app will resolve to before committing a change.
For CI-built releases, declare at least one fallbacks entry per app. Anonymous Google Play access is unreliable from hosted runners, and a verified APKMirror or Uptodown source keeps the build working without credentials.
Full field reference, version-resolution order, and fallback URL rules: docs/configuration.md.
Usage
| Command | Purpose |
|---|---|
auth |
Authenticate against Google Play and cache the session outside the repository. |
download |
Download and verify one package's splits into an output directory. |
tools |
Resolve, download, and hash the pinned Java toolchain. |
list-patches |
Show patch-compatible versions for the configured apps. |
build |
Run the full configured matrix: download, merge, patch, sign, verify, package. |
verify |
Re-verify an existing split set against a package, architecture, and version. |
clean |
Remove the disposable work cache only; trusted sets and tools are preserved. |
morphe-builder download com.example.app --arch arm64 --output downloads
morphe-builder list-patches morphe.toml
morphe-builder verify trusted/splits com.google.android.youtube \
--arch arm64 --version-name 21.04.223
All commands accept --json (progress on stderr, machine-readable result on stdout). --arch both runs independent ARM64 and ARMv7 jobs. Keystore passwords come from MORPHE_KEYSTORE_PASSWORD, never argv. Exit codes are stable per failure category.
Full reference, exit-code table, and cache semantics: docs/cli.md.
Documentation
| Topic | Document |
|---|---|
| CLI reference and exit codes | docs/cli.md |
| TOML configuration and version resolution | docs/configuration.md |
| Toolchain cache, pins, and checksums | docs/toolchain.md |
| Google Play delivery and integrity checks | docs/google-play-integrity.md |
| Provider fallback order | docs/providers.md |
| Split selection and merge policy | docs/merge.md |
| Signed APK build flow | docs/build.md |
| Magisk/KernelSU module format | docs/modules.md |
| Threat model and platform limits | docs/security.md |
| CI, release verification, and publication | docs/releases.md |
| Third-party licenses | docs/THIRD_PARTY.md |
| Repository rules for contributors and agents | docs/rules/README.md |
| Active roadmap | docs/roadmap/ROADMAP.md |
| Completed milestones and evidence | docs/roadmap/COMPLETED.md |
License
Morphe Builder is released under the MIT License. Downloaded Morphe tooling remains separately licensed under GPL-3.0 and is not bundled in the Python distribution. See docs/THIRD_PARTY.md.
Credits
MorpheApp/morphe-desktopandMorpheApp/morphe-patches: the patching CLI and patch bundles this builder orchestrates. Morphe does the patching; this project only prepares verified stock input and packages the output.j-hc/revanced-magisk-module: prior art for the Magisk/KernelSU module approach and CI build matrix. Reviewed as design context under GPL-3.0; no code is copied. Seedocs/modules.md.REAndroid/APKEditorandpatrickfav/uber-apk-signer: split merge and APK signing tools.- ReVanced: the patch ecosystem Morphe builds on; ReVanced-compatible patch bundles can be selected per app via
patches-source.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file morphe_builder-0.2.0.tar.gz.
File metadata
- Download URL: morphe_builder-0.2.0.tar.gz
- Upload date:
- Size: 85.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
99cff41995dd807e287092b38e9e823d5419072dc9ca46f450ee6c666a46aa83
|
|
| MD5 |
918d630313ce7dcf4df41080a3374b2d
|
|
| BLAKE2b-256 |
f5ab009f88e8aec0bbbe0ea3a9f1489ce185acb471c27f700698771d3f0cf204
|
Provenance
The following attestation bundles were made for morphe_builder-0.2.0.tar.gz:
Publisher:
publish-pypi.yml on Villoh/morphe-builder
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
morphe_builder-0.2.0.tar.gz -
Subject digest:
99cff41995dd807e287092b38e9e823d5419072dc9ca46f450ee6c666a46aa83 - Sigstore transparency entry: 2275465908
- Sigstore integration time:
-
Permalink:
Villoh/morphe-builder@f3e13395a93e043bc76de9abe518477e35718c7e -
Branch / Tag:
refs/tags/pypi-v0.2.0 - Owner: https://github.com/Villoh
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@f3e13395a93e043bc76de9abe518477e35718c7e -
Trigger Event:
push
-
Statement type:
File details
Details for the file morphe_builder-0.2.0-py3-none-any.whl.
File metadata
- Download URL: morphe_builder-0.2.0-py3-none-any.whl
- Upload date:
- Size: 62.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
beb3011fbdfe5f4a80cbc57fd83edc3622db782629f322f9f6512cc7f9b89dec
|
|
| MD5 |
42727b4beb703c68f611dd520e5c2815
|
|
| BLAKE2b-256 |
561105571c3a4e5c84b722b5631a81ddba3e2d629791d0e2953aca8cd0437efa
|
Provenance
The following attestation bundles were made for morphe_builder-0.2.0-py3-none-any.whl:
Publisher:
publish-pypi.yml on Villoh/morphe-builder
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
morphe_builder-0.2.0-py3-none-any.whl -
Subject digest:
beb3011fbdfe5f4a80cbc57fd83edc3622db782629f322f9f6512cc7f9b89dec - Sigstore transparency entry: 2275466370
- Sigstore integration time:
-
Permalink:
Villoh/morphe-builder@f3e13395a93e043bc76de9abe518477e35718c7e -
Branch / Tag:
refs/tags/pypi-v0.2.0 - Owner: https://github.com/Villoh
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@f3e13395a93e043bc76de9abe518477e35718c7e -
Trigger Event:
push
-
Statement type: