Add Aurora Store, fix microG signature spoofing, overhaul updater
- AuroraStore: bundle the privileged preload variant with privapp + default-permissions; pull from AuroraOSS GitLab in vendorsetup.sh - microG sig-spoofing: keep FAKE_PACKAGE_SIGNATURE signature|privileged (system-level, non-requestable); drop the dead dangerous-model default-permissions grant; make GsfProxy privileged - updater: rewrite for correct sources (GitHub microG / GitLab Aurora / F-Droid), fix apps/ module paths, handle .microg_release and Aurora's .version_tag, add requirements.txt (replaces Pipfile) - README: rewrite for the trimmed PawletOS fork
This commit is contained in:
@@ -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
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<manifest>
|
||||
<project path="vendor/partner_gms" name="lineageos4microg/android_vendor_partner_gms" remote="github" revision="master" />
|
||||
</manifest>
|
||||
```
|
||||
|
||||
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`).
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
4.8.3
|
||||
@@ -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",
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2026 oxmc / PawletOS
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
Default (pre-)grants for the bundled Aurora Store so its runtime permissions
|
||||
don't prompt the user on first run. This only covers runtime/dangerous
|
||||
permissions; appop permissions (REQUEST_INSTALL_PACKAGES, MANAGE_EXTERNAL_STORAGE)
|
||||
are not granted here — MANAGE_EXTERNAL_STORAGE comes from its "preinstalled"
|
||||
protection flag, and privileged/signature grants live in
|
||||
privapp-permissions-com.aurora.store.xml. fixed="false" lets the user still
|
||||
revoke them.
|
||||
-->
|
||||
<exceptions>
|
||||
<exception package="com.aurora.store">
|
||||
<!-- Notifications: downloads, available updates, errors -->
|
||||
<permission name="android.permission.POST_NOTIFICATIONS" fixed="false" />
|
||||
<!-- Legacy storage for OBB expansion files (API <= 32) -->
|
||||
<permission name="android.permission.READ_EXTERNAL_STORAGE" fixed="false" />
|
||||
<permission name="android.permission.WRITE_EXTERNAL_STORAGE" fixed="false" />
|
||||
</exception>
|
||||
</exceptions>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2026 oxmc / PawletOS
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
Privileged-permission allowlist for the bundled Aurora Store (preload
|
||||
variant). Only signature|privileged permissions belong here; Aurora's other
|
||||
permissions (INTERNET, FOREGROUND_SERVICE*, QUERY_ALL_PACKAGES,
|
||||
REQUEST_INSTALL_PACKAGES/REQUEST_DELETE_PACKAGES, UPDATE_PACKAGES_WITHOUT_USER_ACTION,
|
||||
MANAGE_EXTERNAL_STORAGE, POST_NOTIFICATIONS, ...) are normal/runtime/appop and
|
||||
are granted without an allowlist. Without this entry, an enforcing build kills
|
||||
system_server at boot because Aurora is installed as a privileged app.
|
||||
-->
|
||||
<permissions>
|
||||
<privapp-permissions package="com.aurora.store">
|
||||
<!-- Claim update ownership so Aurora's later updates to apps it
|
||||
installed apply silently (signature|privileged, Android 14+). -->
|
||||
<permission name="android.permission.ENFORCE_UPDATE_OWNERSHIP" />
|
||||
</privapp-permissions>
|
||||
</permissions>
|
||||
@@ -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",
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<exceptions>
|
||||
<exception package="com.android.vending">
|
||||
<!-- for permissive signature spoofing, where the permission is "dangerous" -->
|
||||
<permission name="android.permission.FAKE_PACKAGE_SIGNATURE" fixed="false"/>
|
||||
<!-- Account -->
|
||||
<permission name="android.permission.GET_ACCOUNTS" fixed="false" />
|
||||
<!-- Location -->
|
||||
<permission name="android.permission.ACCESS_COARSE_LOCATION" fixed="false" />
|
||||
|
||||
<!-- Notifications -->
|
||||
<permission name="android.permission.POST_NOTIFICATIONS" fixed="false" />
|
||||
|
||||
</exception>
|
||||
</exceptions>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<boolean name="nominatim_enabled" value="true" />
|
||||
<boolean name="vending_licensing" value="true" />
|
||||
<boolean name="vending_licensing_purchase_free_apps" value="true" />
|
||||
<boolean name="vending_billing" value="true" />
|
||||
<boolean name="vending_billing" value="false" />
|
||||
<boolean name="vending_asset_delivery" value="true" />
|
||||
<boolean name="vending_device_sync" value="true" />
|
||||
</map>
|
||||
|
||||
@@ -3,6 +3,7 @@ android_app_import {
|
||||
apk: "GsfProxy.apk",
|
||||
presigned: true,
|
||||
preprocessed: true,
|
||||
privileged: true,
|
||||
dex_preopt: {
|
||||
enabled: false,
|
||||
},
|
||||
|
||||
@@ -3,4 +3,5 @@ PRODUCT_PACKAGES += \
|
||||
GsfProxy \
|
||||
FakeStore \
|
||||
FDroid \
|
||||
FDroidPrivilegedExtension
|
||||
FDroidPrivilegedExtension \
|
||||
AuroraStore
|
||||
|
||||
@@ -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"
|
||||
Generated
-107
@@ -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": {}
|
||||
}
|
||||
@@ -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)
|
||||
])
|
||||
|
||||
@@ -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'])
|
||||
+123
-34
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -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"])
|
||||
@@ -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/<id>/uploads/<hash>/<file>) —
|
||||
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))
|
||||
+42
-12
@@ -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/<id>/uploads/<hash>/<file>) — 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 ../..
|
||||
|
||||
|
||||
Reference in New Issue
Block a user