diff --git a/README.md b/README.md index 6c8a036..f0d67cd 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,52 @@ -# microG Mobile Services +# vendor/partner_gms — PawletOS + +Bundles a small set of prebuilt FOSS APKs (microG + app stores) into the +PawletOS build. Forked from +[lineageos4microg/android_vendor_partner_gms](https://github.com/lineageos4microg/android_vendor_partner_gms) +and trimmed down for PawletOS — this repo now just documents and installs the +apps below; it is no longer kept in sync with upstream. + +All APKs are official, unmodified prebuilts signed by their original developers +(`presigned: true`), so signature spoofing works and the stores can self-update. + +## Included apps + +| App | Package | Purpose | +|-----|---------|---------| +| GmsCore | `com.google.android.gms` | microG — FOSS reimplementation of Google Play Services | +| FakeStore | `com.android.vending` | Empty stub that mocks the Play Store's presence for apps that require it | +| GsfProxy | `com.google.android.gsf` | GmsCore proxy for legacy GCM compatibility | +| F-Droid | `org.fdroid.fdroid` | FOSS app catalogue | +| F-Droid Privileged Extension | `org.fdroid.fdroid.privileged` | Silent install/removal for F-Droid | +| Aurora Store | `com.aurora.store` | Play Store client (preload variant) | + +`apps/FDroid/additional_repos.xml` pre-seeds the microG F-Droid repo. + +## How it's wired + +- **`vendorsetup.sh`** downloads each APK (from GitHub / F-Droid / GitLab) into + its `apps/**` folder at sync time. Versions are pinned by the `.version_code` + file next to each app (and `.microg_release` for the microG bundle). +- **`gms.mk`** adds the modules to `PRODUCT_PACKAGES`. +- Each app's **`Android.bp`** imports the APK and, where needed, installs its + `privapp-permissions-*.xml` (privileged allowlist) and + `default-permissions-*.xml` (runtime pre-grants). + +## Signature spoofing + +Restricted / system-level only. The `FAKE_PACKAGE_SIGNATURE` permission is +declared `signature|privileged` by the framework patch in +`patches/frameworks/base/`, so **no third-party app can request it**. It is +granted only via privapp-permissions to the privileged microG apps that need it +(`com.google.android.gms`, `com.android.vending`). + +## Maintainer notes + +When bumping to a new microG release, update the pinned versions: -***Note to maintainers:*** -When updating to a new vesion of microG the following files must be updated - `.microg_release` -- `FakeStore/.version_code` -- `GmsCore/.version_code` -) +- `apps/GMS/GmsCore/.version_code` +- `apps/FakeStore/.version_code` - -This is a collection of FOSS APKs, coupled with the respective Makefiles for an -easy integration in the Android build system. - -To include them in your build, add a repo manifest file to include this repository as `vendor/partner_gms` and set -`WITH_GMS` to `true` when building. - -Example manifest: - -```xml - - - - -``` - -Note 1. You do not need to set `CUSTOM_PACKAGES` for the packages to be included when building with [our Docker engine](https://github.com/lineageos4microg/docker-lineage-cicd). LineageOS did not make those changes in the 17.1 branch, so to make a 17.1 build, you will need to apply the changes to `AndroidManifest.xml` and `strings.xml` from [the Android Q LOS 17.1 branch-specific patch](https://github.com/lineageos4microg/docker-lineage-cicd/blob/35b7190bda4f2988845f09e028d266a23fd8b559/src/signature_spoofing_patches/android_frameworks_base-Q.patch). You do not need to apply the other changes in that patch. (See [this issue, and all it's comments](https://github.com/lineageos4microg/android_vendor_partner_gms/issues/42#issuecomment-2674105896) for more information about this) - -Note 2. LineageOS now support ***restricted*** signature spoofing, in 18.1 and later branches, so it is no longer neccessary to patch their sources, unless you want ***unrestricted*** signature spoofing -~~For the microG packages to work, signature spoofing must be enabled, by patching the LineageOS sources before building.~~ -- ~~If you are building with [our Docker engine](https://github.com/lineageos4microg/docker-lineage-cicd), this is done automatically according to the values of the `SIGNATURE_SPOOFING` variable - see [here](https://github.com/lineageos4microg/docker-lineage-cicd#signature-spoofing). By default, this variable is set to `restricted`, allowing only system apps (those built-in to the ROM) to 'spoof' the signature of another app.~~ -- ~~If you are building manually, you can download and include the patches from [here](https://github.com/lineageos4microg/docker-lineage-cicd/tree/master/src/signature_spoofing_patches). If you follow the [microG patching instructions](https://github.com/microg/GmsCore/wiki/Signature-Spoofing), you ROM will allow _**any**_ app, not only system apps, to spoof app signatures. To apply only 'restricted' signature spoofing, you should do something similar to what the Docker engine `build.sh` does in [this code](https://github.com/lineageos4microg/docker-lineage-cicd/blob/c77eabe036a1620499a8c087b732e039e0734656/src/build.sh#L229C1-L231C1).~~ - -Note 3. If you encounter problems related to APK / app signing when using these components you may need to add the following line in the Android.mk for the component in question: -``` -LOCAL_REPLACE_PREBUILT_APK_INSTALLED := $(LOCAL_PATH)/$(LOCAL_MODULE).apk -``` -Such problems can occur when - - the app / APK is resigned with your keys; (this should not happen if the line LOCAL_CERTIFICATE := PRESIGNED is included in the app makefile) - app / APK signatures are 'stripped` during the during the deodexing phase of the build. For some apps the deodexed app ends up unsigned, and so will not run. - -The symptoms of the problem as some apps from this repo (e.g. FakeStore and GmsCore) missing completely from your launcher and acting like they're not installed. - -(Some background to this issue can be found [here](https://github.com/lineageos4microg/android_vendor_partner_gms/issues/30) and [here](https://gitlab.com/iode/os/public/lineage/vendor_extra/-/issues/4)) - ---------------- - -The included APKs are: - * FDroid packages (binaries sourced from [here](https://f-droid.org/packages/org.fdroid.fdroid/) and [here](https://f-droid.org/packages/org.fdroid.fdroid.privileged/)) - * FDroid: a catalogue of FOSS (Free and Open Source Software) applications for the Android platform - * FDroid Privileged Extension: a FDroid extension to ease the installation/removal of apps - * additional_repos.xml: a simple package to include the [microG F-Droid repository](https://microg.org/fdroid/repo/) in the ROM (requires FDroid >= 1.5) - * microG packages (binaries sourced from [here](https://github.com/microg/GmsCore/releases)) - * GmsCore: the main component of microG, a FOSS reimplementation of the Google Play Services (requires FakeStore - and GsfProxy and for older versions - for full functionality) - * FakeStore: an empty package that mocks the existence of the Google Play Store - * GsfProxy: From [here](https://github.com/microg/GsfProxy/releases)a GmsCore proxy for legacy GCM compatibility - -These are official unmodified prebuilt binaries, signed by the -corresponding developers. +Other apps pin their own `.version_code` (`apps/GMS/GsfProxy`, +`apps/FDroid`, `apps/FDroid/FDroidPrivilegedExtension`, `apps/AuroraStore`). diff --git a/apps/AuroraStore/.version_tag b/apps/AuroraStore/.version_tag new file mode 100644 index 0000000..f99c658 --- /dev/null +++ b/apps/AuroraStore/.version_tag @@ -0,0 +1 @@ +4.8.3 diff --git a/apps/AuroraStore/Android.bp b/apps/AuroraStore/Android.bp new file mode 100644 index 0000000..54b2193 --- /dev/null +++ b/apps/AuroraStore/Android.bp @@ -0,0 +1,34 @@ +prebuilt_etc { + name: "privapp-permissions-com.aurora.store.xml", + src: "privapp-permissions-com.aurora.store.xml", + sub_dir: "permissions", + product_specific: true, +} + +prebuilt_etc { + name: "default-permissions-com.aurora.store.xml", + src: "default-permissions-com.aurora.store.xml", + sub_dir: "default-permissions", + product_specific: true, +} + +android_app_import { + name: "AuroraStore", + apk: "AuroraStore.apk", + presigned: true, + preprocessed: true, + skip_preprocessed_apk_checks: true, + privileged: true, + dex_preopt: { + enabled: false, + }, + product_specific: true, + optional_uses_libs: [ + "androidx.window.extensions", + "androidx.window.sidecar", + ], + required: [ + "privapp-permissions-com.aurora.store.xml", + "default-permissions-com.aurora.store.xml", + ], +} diff --git a/apps/AuroraStore/default-permissions-com.aurora.store.xml b/apps/AuroraStore/default-permissions-com.aurora.store.xml new file mode 100644 index 0000000..a34f7e2 --- /dev/null +++ b/apps/AuroraStore/default-permissions-com.aurora.store.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/apps/AuroraStore/privapp-permissions-com.aurora.store.xml b/apps/AuroraStore/privapp-permissions-com.aurora.store.xml new file mode 100644 index 0000000..f53e08b --- /dev/null +++ b/apps/AuroraStore/privapp-permissions-com.aurora.store.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/apps/FDroid/additional_repos.json b/apps/FDroid/additional_repos.json index f5aa7dc..7d88e99 100644 --- a/apps/FDroid/additional_repos.json +++ b/apps/FDroid/additional_repos.json @@ -1,4 +1,18 @@ [ + { + "name": "F-Droid Official repo", + "address": "https://f-droid.org/repo", + "description": "This is the official F-Droid repository. It contains free and open source applications for Android.", + "certificate": "43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB", + "enabled": true + }, + { + "name": "IzzyOnDroid F-Droid repo", + "address": "https://apt.izzysoft.de/fdroid/repo", + "description": "This is an F-Droid style repository for Android apps, provided by IzzyOnDroid. Applications in this repository are official binaries built by the original application developers, taken from their resp. repositories (mostly Github).", + "certificate": "3BF0D6ABFEAE2F401707B6D966BE743BF0EEE49C2561B9BA39073711F628937A", + "enabled": true + }, { "name": "microG F-Droid repo", "address": "https://microg.org/fdroid/repo", diff --git a/apps/FakeStore/default-permissions-com.android.vending.xml b/apps/FakeStore/default-permissions-com.android.vending.xml index 2e31d04..94b93c0 100644 --- a/apps/FakeStore/default-permissions-com.android.vending.xml +++ b/apps/FakeStore/default-permissions-com.android.vending.xml @@ -1,15 +1,11 @@ - - - - diff --git a/apps/GMS/GmsCore/microg.xml b/apps/GMS/GmsCore/microg.xml index 06d25a4..701c08a 100644 --- a/apps/GMS/GmsCore/microg.xml +++ b/apps/GMS/GmsCore/microg.xml @@ -21,7 +21,7 @@ - + diff --git a/apps/GMS/GsfProxy/Android.bp b/apps/GMS/GsfProxy/Android.bp index 55eb35a..62dbc8f 100644 --- a/apps/GMS/GsfProxy/Android.bp +++ b/apps/GMS/GsfProxy/Android.bp @@ -3,6 +3,7 @@ android_app_import { apk: "GsfProxy.apk", presigned: true, preprocessed: true, + privileged: true, dex_preopt: { enabled: false, }, diff --git a/gms.mk b/gms.mk index 97d1991..3fbc994 100644 --- a/gms.mk +++ b/gms.mk @@ -3,4 +3,5 @@ PRODUCT_PACKAGES += \ GsfProxy \ FakeStore \ FDroid \ - FDroidPrivilegedExtension + FDroidPrivilegedExtension \ + AuroraStore diff --git a/updater/Pipfile b/updater/Pipfile deleted file mode 100644 index 90dc377..0000000 --- a/updater/Pipfile +++ /dev/null @@ -1,12 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -requests-cache = "==0.9.1" - -[dev-packages] - -[requires] -python_version = "3.9" diff --git a/updater/Pipfile.lock b/updater/Pipfile.lock deleted file mode 100644 index 8791c11..0000000 --- a/updater/Pipfile.lock +++ /dev/null @@ -1,107 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "43267acd3e0a2938456d3924e25339a1da500cb295a1afee2e8a41843ba8af63" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.9" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "appdirs": { - "hashes": [ - "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", - "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" - ], - "version": "==1.4.4" - }, - "attrs": { - "hashes": [ - "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", - "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.4.0" - }, - "cattrs": { - "hashes": [ - "sha256:211800f725cdecedcbcf4c753bbd22d248312b37d130f06045434acb7d9b34e1", - "sha256:35dd9063244263e63bd0bd24ea61e3015b00272cead084b2c40d788b0f857c46" - ], - "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==1.10.0" - }, - "certifi": { - "hashes": [ - "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", - "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" - ], - "version": "==2021.10.8" - }, - "charset-normalizer": { - "hashes": [ - "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45", - "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c" - ], - "markers": "python_version >= '3'", - "version": "==2.0.11" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3'", - "version": "==3.3" - }, - "requests": { - "hashes": [ - "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", - "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==2.27.1" - }, - "requests-cache": { - "hashes": [ - "sha256:3e3384c48dca231ee4c49e7ba53162bc0d99e16721baf085bfba1552d065d151", - "sha256:7737f83f0f48481a904bb9a9402233db5090931e46f9644c502646a573848d35" - ], - "index": "pypi", - "version": "==0.9.1" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "url-normalize": { - "hashes": [ - "sha256:d23d3a070ac52a67b83a1c59a0e68f8608d1cd538783b401bc9de2c0fac999b2", - "sha256:ec3c301f04e5bb676d333a7fa162fa977ad2ca04b7e652bfc9fac4e405728eed" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.4.3" - }, - "urllib3": { - "hashes": [ - "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", - "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4.0'", - "version": "==1.26.8" - } - }, - "develop": {} -} diff --git a/updater/__init__.py b/updater/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/updater/certificates.py b/updater/certificates.py deleted file mode 100644 index 547e4e7..0000000 --- a/updater/certificates.py +++ /dev/null @@ -1,11 +0,0 @@ -import subprocess - - -def get_apk_certificate(file: str): - output = subprocess.check_output(['keytool', '-printcert', '-rfc', '-jarfile', file], text=True) - lines = output.split("\n") - return '\n'.join(lines[ - lines.index('-----BEGIN CERTIFICATE-----'): - (lines.index('-----END CERTIFICATE-----')+1) - ]) - diff --git a/updater/git.py b/updater/git.py deleted file mode 100644 index 1c74b32..0000000 --- a/updater/git.py +++ /dev/null @@ -1,17 +0,0 @@ -import subprocess - - -user_name = 'Updater Robot' -user_email = 'robot@nowhere.invalid' - - -def add_commit_push(directory: str, message: str): - diff = subprocess.run(['git', 'diff', '--cached', '--exit-code'], capture_output=True, text=True) - if diff.returncode != 0: - status = subprocess.run(['git', 'status'], capture_output=True, text=True) - raise Exception('Unknown staged changes found: {}'.format(status.stdout)) - - subprocess.run(['git', 'add', '--all', directory], check=True) - subprocess.run(['git', '-c', 'user.name={}'.format(user_name), '-c', 'user.email={}'.format(user_email), - 'commit', '--message', message]) - subprocess.run(['git', 'push']) diff --git a/updater/main.py b/updater/main.py index 996fd93..8015647 100644 --- a/updater/main.py +++ b/updater/main.py @@ -1,49 +1,138 @@ +import os +import re +import tempfile import urllib.request from os import path -import certificates -import git -from sources import ApkRelease, fdroid_recommended_release +from .utils import certificates, sources + +# Repo root (vendor/partner_gms), i.e. the parent of this updater/ folder. +BASE = path.abspath(path.join(path.dirname(__file__), "..")) + +FDROID_MAIN = "https://f-droid.org/repo" -def update_if_needed(module: str, release: ApkRelease): - module_dir = path.abspath(path.join(path.dirname(__file__), '..', module)) - with open(path.join(module_dir, '.version_code'), 'r+') as version_code_file: - version_code = int(version_code_file.read()) - if version_code < release.version_code: - print('updating {} to {}'.format(module, release.version_name)) - apk_filename = path.join(module_dir, '{}.apk'.format(module)) +def _read(p): + with open(p, "r") as f: + return f.read().strip() - old_sig = certificates.get_apk_certificate(apk_filename) - print('downloading {} ...'.format(release.download_url)) - urllib.request.urlretrieve(release.download_url, apk_filename) +def _write(p, value): + with open(p, "w") as f: + f.write(str(value) + "\n") - new_sig = certificates.get_apk_certificate(apk_filename) - if old_sig != new_sig: - raise Exception('Signature mismatch for {} old sig: {} new sig: {}'.format(module, old_sig, new_sig)) - version_code_file.seek(0) - version_code_file.write(str(release.version_code)) - version_code_file.truncate() - version_code_file.close() +def _verify_same_signer(module, download_url): + """Best-effort: if a local APK is present, refuse to bump the pin when the + new release is signed by a different certificate (which would break the + presigned / signature-spoofing setup). APKs are gitignored, so on a fresh + tree there is nothing to compare against and we skip with a warning.""" + apk_name = path.basename(module) + ".apk" + local_apk = path.join(BASE, module, apk_name) + if not path.exists(local_apk): + print(" ! no local {} to compare against; skipping signer check".format(apk_name)) + return - print('Need to update {} to {}'.format(module, release.version_name)) + old_sig = certificates.get_apk_certificate(local_apk) + fd, tmp = tempfile.mkstemp(suffix=".apk") + os.close(fd) + try: + urllib.request.urlretrieve(download_url, tmp) + new_sig = certificates.get_apk_certificate(tmp) + if old_sig != new_sig: + raise Exception( + "signer changed for {} — refusing to pin new version".format(module) + ) + finally: + try: + os.remove(tmp) + except OSError: + pass + + +def apply(module, pin_file, release): + """Update a single pin file. `module` is repo-relative, e.g. apps/GMS/GmsCore.""" + p = path.join(BASE, module, pin_file) + current = _read(p) + + if release.numeric: + if int(current) == int(release.pin): + print("{}: up to date ({})".format(module, current)) return + if int(current) > int(release.pin): + print( + "{}: local {} ahead of upstream {} — leaving".format( + module, current, release.pin + ) + ) + return + elif current == release.pin: + print("{}: up to date ({})".format(module, current)) + return - print('commit and push...') - git.add_commit_push(module_dir, 'Update {} to {}'.format(module, release.version_name)) + print("{}: {} -> {} ({})".format(module, current, release.pin, release.display)) + _verify_same_signer(module, release.download_url) + _write(p, release.pin) - elif version_code > release.version_code: - print('{} ahead of suggested version ({} > {})'.format(module, version_code, release.version_code)) - elif version_code == release.version_code: - print('{} up to date.'.format(module)) -fdroid_main_repo = 'https://www.f-droid.org/repo' -fdroid_microg_repo = 'https://microg.org/fdroid/repo' +def update_microg(): + """GmsCore + FakeStore ship in one microG GmsCore GitHub release. Pin each + APK's versionCode and the shared release tag that vendorsetup.sh needs to + build the download URLs (.microg_release).""" + rel = sources.github_latest("microg", "GMSCore") + tag = rel["tag_name"] -update_if_needed('FakeStore', fdroid_recommended_release(fdroid_microg_repo, 'com.android.vending')) -update_if_needed('FDroid', fdroid_recommended_release(fdroid_main_repo, 'org.fdroid.fdroid')) -update_if_needed('FDroidPrivilegedExtension', fdroid_recommended_release(fdroid_main_repo, 'org.fdroid.fdroid.privileged')) -update_if_needed('GmsCore', fdroid_recommended_release(fdroid_microg_repo, 'com.google.android.gms')) -#update_if_needed('GsfProxy', fdroid_recommended_release(fdroid_microg_repo, 'com.google.android.gsf')) + gms_name, gms_url = sources.github_asset(rel, r"^com\.google\.android\.gms-\d+\.apk$") + fs_name, fs_url = sources.github_asset(rel, r"^com\.android\.vending-\d+\.apk$") + gms_vc = re.search(r"-(\d+)\.apk$", gms_name).group(1) + fs_vc = re.search(r"-(\d+)\.apk$", fs_name).group(1) + + apply( + "apps/GMS/GmsCore", ".version_code", + sources.Release(gms_vc, gms_url, numeric=True, display=tag), + ) + apply( + "apps/FakeStore", ".version_code", + sources.Release(fs_vc, fs_url, numeric=True, display=tag), + ) + + mr = path.join(BASE, ".microg_release") + if _read(mr) != tag: + print(".microg_release: {} -> {}".format(_read(mr), tag)) + _write(mr, tag) + else: + print(".microg_release: up to date ({})".format(tag)) + + +def update_fdroid(): + apply( + "apps/FDroid", ".version_code", + sources.fdroid_recommended(FDROID_MAIN, "org.fdroid.fdroid"), + ) + apply( + "apps/FDroid/FDroidPrivilegedExtension", ".version_code", + sources.fdroid_recommended(FDROID_MAIN, "org.fdroid.fdroid.privileged"), + ) + + +def update_aurora(): + """Aurora Store OEM 'preload' variant from the AuroraOSS GitLab (tag-based).""" + tag, url = sources.gitlab_latest_upload( + "6922885", r"/uploads/[a-f0-9]+/AuroraStore-preload-[^)\"]+\.apk" + ) + apply( + "apps/AuroraStore", ".version_tag", + sources.Release(tag, url, numeric=False, display=tag), + ) + + +def main(): + update_microg() + update_fdroid() + update_aurora() + # GsfProxy is no longer updated upstream — its .version_code (tag) is pinned + # manually and pulled by vendorsetup.sh from GitHub. + + +if __name__ == "__main__": + main() diff --git a/updater/requirements.txt b/updater/requirements.txt new file mode 100644 index 0000000..256e467 --- /dev/null +++ b/updater/requirements.txt @@ -0,0 +1,3 @@ +# Updater dependencies. Install with: pip install -r requirements.txt +# requests-cache brings in requests as a transitive dependency. +requests-cache==0.9.1 diff --git a/updater/sources.py b/updater/sources.py deleted file mode 100644 index 51c0bbf..0000000 --- a/updater/sources.py +++ /dev/null @@ -1,38 +0,0 @@ -from xml.dom import minidom, pulldom - -import requests_cache - -requests_session = requests_cache.CachedSession('updater', backend='memory') - - -class ApkRelease: - version_name: str - version_code: int - download_url: str - - def __init__(self, version_name: str, version_code: int, download_url: str): - self.version_name = version_name - self.version_code = version_code - self.download_url = download_url - - -def _child_el_content(el: minidom.Element, tag_name: str): - return el.getElementsByTagName(tag_name).item(0).firstChild.data - - -def fdroid_recommended_release(repo: str, application_id: str): - with requests_session.get('{}/index.xml'.format(repo)) as r: - doc = pulldom.parseString(r.text) - for event, node in doc: - if event == pulldom.START_ELEMENT and node.tagName == 'application': - if node.getAttribute('id') == application_id: - doc.expandNode(node) - marketvercode = _child_el_content(node, 'marketvercode') - for p in node.getElementsByTagName('package'): - if _child_el_content(p, 'versioncode') == marketvercode: - return ApkRelease( - _child_el_content(p, 'version'), - int(marketvercode), - '{}/{}'.format(repo, _child_el_content(p, 'apkname')) - ) - raise Exception('Did not find {} in repo {}'.format(application_id, repo)) diff --git a/updater/utils/__init__.py b/updater/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/updater/utils/certificates.py b/updater/utils/certificates.py new file mode 100644 index 0000000..86ad66f --- /dev/null +++ b/updater/utils/certificates.py @@ -0,0 +1,15 @@ +import subprocess + + +def get_apk_certificate(file: str): + output = subprocess.check_output( + ["keytool", "-printcert", "-rfc", "-jarfile", file], text=True + ) + lines = output.split("\n") + return "\n".join( + lines[ + lines.index("-----BEGIN CERTIFICATE-----") : ( + lines.index("-----END CERTIFICATE-----") + 1 + ) + ] + ) diff --git a/updater/utils/git.py b/updater/utils/git.py new file mode 100644 index 0000000..974e63b --- /dev/null +++ b/updater/utils/git.py @@ -0,0 +1,28 @@ +import subprocess + +user_name = "Updater Robot" +user_email = "robot@nowhere.invalid" + + +def add_commit_push(directory: str, message: str): + diff = subprocess.run( + ["git", "diff", "--cached", "--exit-code"], capture_output=True, text=True + ) + if diff.returncode != 0: + status = subprocess.run(["git", "status"], capture_output=True, text=True) + raise Exception("Unknown staged changes found: {}".format(status.stdout)) + + subprocess.run(["git", "add", "--all", directory], check=True) + subprocess.run( + [ + "git", + "-c", + "user.name={}".format(user_name), + "-c", + "user.email={}".format(user_email), + "commit", + "--message", + message, + ] + ) + subprocess.run(["git", "push"]) diff --git a/updater/utils/sources.py b/updater/utils/sources.py new file mode 100644 index 0000000..d57dd52 --- /dev/null +++ b/updater/utils/sources.py @@ -0,0 +1,94 @@ +import re +from xml.dom import pulldom + +import requests_cache + +# Cached so repeated calls in one run (e.g. GmsCore + FakeStore share the same +# GitHub release) don't re-hit the network. +session = requests_cache.CachedSession("updater", backend="memory") + + +class Release: + """A resolved upstream release. + + pin: the string written to the module's pin file and used to decide + whether an update is needed. For F-Droid apps this is the + integer versionCode; for GitHub/GitLab apps it is the tag. + numeric: True when `pin` is an integer versionCode (ordered + numerically); False for tag-based releases (compared for + equality only). + download_url: direct link to the APK, used for the signer check. + display: human-readable version for logging. + """ + + def __init__(self, pin, download_url, numeric, display=None): + self.pin = str(pin) + self.download_url = download_url + self.numeric = numeric + self.display = display or str(pin) + + +def _text(el, tag_name): + return el.getElementsByTagName(tag_name).item(0).firstChild.data + + +def fdroid_recommended(repo, application_id): + """Latest recommended (marketvercode) build from an F-Droid index.xml repo.""" + with session.get("{}/index.xml".format(repo)) as r: + r.raise_for_status() + doc = pulldom.parseString(r.text) + for event, node in doc: + if ( + event == pulldom.START_ELEMENT + and node.tagName == "application" + and node.getAttribute("id") == application_id + ): + doc.expandNode(node) + market = _text(node, "marketvercode") + for p in node.getElementsByTagName("package"): + if _text(p, "versioncode") == market: + return Release( + pin=int(market), + download_url="{}/{}".format(repo, _text(p, "apkname")), + numeric=True, + display=_text(p, "version"), + ) + raise Exception("did not find {} in {}".format(application_id, repo)) + + +def github_latest(owner, repo): + """Raw 'latest release' JSON from GitHub (carries tag_name + assets).""" + url = "https://api.github.com/repos/{}/{}/releases/latest".format(owner, repo) + with session.get(url) as r: + r.raise_for_status() + return r.json() + + +def github_asset(release_json, name_regex): + """(name, download_url) of the first asset whose name matches name_regex.""" + rx = re.compile(name_regex) + for a in release_json.get("assets", []): + if rx.search(a["name"]): + return a["name"], a["browser_download_url"] + raise Exception("no GitHub asset matching /{}/".format(name_regex)) + + +def gitlab_latest_upload(project_id, upload_regex): + """Newest GitLab release carrying a description upload matching upload_regex. + + Returns (tag, download_url). Mirrors vendorsetup.sh: the APKs are markdown + upload attachments in the release description, downloadable anonymously only + via the project-id upload path (/-/project//uploads//) — + namespace paths 403/404. Releases come back newest-first. + """ + api = "https://gitlab.com/api/v4/projects/{}/releases".format(project_id) + with session.get(api) as r: + r.raise_for_status() + releases = r.json() + rx = re.compile(upload_regex) + for rel in releases: + m = rx.search(rel.get("description", "")) + if m: + url = "https://gitlab.com/-/project/{}{}".format(project_id, m.group(0)) + return rel["tag_name"], url + raise Exception("no GitLab release upload matching /{}/".format(upload_regex)) diff --git a/vendorsetup.sh b/vendorsetup.sh index bf2d28f..1bed695 100755 --- a/vendorsetup.sh +++ b/vendorsetup.sh @@ -11,12 +11,17 @@ download_apk() { local destination_apk=$2 if [ -f "$destination_apk" ]; then - echo "$destination_apk exists: not downloading" - ## To Do - # Deal with the situation where we have an OLDER version hanging around - # may have to be handled in the Docker image + echo "$destination_apk exists: checking for updates" + local source_size destination_size + source_size=$(curl -sI "$source_apk" | grep -i Content-Length | awk '{print $2}' | tr -d '\r') + destination_size=$(stat -c %s "$destination_apk") + if [ "$source_size" != "$destination_size" ]; then + echo "updating $destination_apk" + curl -L --output "$destination_apk" "$source_apk" + else + echo "$destination_apk is up to date: skipping download" + fi else - # echo "downloading $source_apk to $destination_apk" curl -L --output "$destination_apk" "$source_apk" fi } @@ -25,7 +30,7 @@ get-fdroid-components() { local fdroid_repo="https://f-droid.org/repo/" local versioncode id apk_to_download - # F-Droid client app + # F-Droid client app (nested under apps/FDroid/) versioncode=$(cat "apps/FDroid/.version_code") id="org.fdroid.fdroid" apk_to_download="$fdroid_repo"/"$id"_"$versioncode".apk @@ -49,23 +54,48 @@ get-microg-components() { apk_to_download="$microg_repo_base"/GMSCore/releases/download/"$microg_release"/"$id"-"$versioncode".apk download_apk "$apk_to_download" "apps/GMS/GmsCore/GmsCore.apk" - # FakeStore + # GsfProxy (nested under apps/GMS/) + versioncode=$(cat "apps/GMS/GsfProxy/.version_code") + apk_to_download="$microg_repo_base"/android_packages_apps_GsfProxy/releases/download/"$versioncode"/GsfProxy.apk + download_apk "$apk_to_download" "apps/GMS/GsfProxy/GsfProxy.apk" + + # FakeStore (nested under apps/FakeStore/) versioncode=$(cat "apps/FakeStore/.version_code") id="com.android.vending" apk_to_download="$microg_repo_base"/GMSCore/releases/download/"$microg_release"/"$id"-"$versioncode".apk download_apk "$apk_to_download" "apps/FakeStore/FakeStore.apk" +} - # GsfProxy (nested under apps/GMS/) - # file: https://github.com/microg/android_packages_apps_GsfProxy/releases/download/v0.1.0/GsfProxy.apk - versioncode=$(cat "apps/GMS/GsfProxy/.version_code") - apk_to_download="$microg_repo_base"/android_packages_apps_GsfProxy/releases/download/"$versioncode"/GsfProxy.apk - download_apk "$apk_to_download" "apps/GMS/GsfProxy/GsfProxy.apk" +get-aurora-components() { + local project_id="6922885" # gitlab.com/AuroraOSS/AuroraStore + local api="https://gitlab.com/api/v4/projects/$project_id/releases" + local tag apk_path apk_url + + # Aurora Store OEM "preload" variant from the official AuroraOSS GitLab. + # .version_tag holds the release TAG (e.g. 4.8.3). The release APKs are + # markdown upload attachments in the release description; the only public + # anonymous download is the project-id upload path + # (/-/project//uploads//) — namespace paths 403/404. So pull + # the hash from the release description and build that URL. Saved as + # AuroraStore.apk. + tag=$(cat "apps/AuroraStore/.version_tag") + + apk_path=$(curl -sL "$api/$tag" \ + | grep -oE '/uploads/[a-f0-9]+/AuroraStore-preload-[^)"]+\.apk' \ + | head -n1) + if [ -z "$apk_path" ]; then + echo "ERROR: could not resolve Aurora Store preload upload for tag $tag" >&2 + return 1 + fi + apk_url="https://gitlab.com/-/project/$project_id$apk_path" + download_apk "$apk_url" "apps/AuroraStore/AuroraStore.apk" } # This script is called from the root directory, so we need to cd cd vendor/partner_gms get-fdroid-components get-microg-components +get-aurora-components # and back to the root directory cd ../..