├── .gitignore ├── .github ├── workflows │ ├── renovate.yaml │ └── release-single.yaml └── renovate.json5 ├── LICENSE ├── README.md └── rooted-ota.sh /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /ota.key 3 | /ota.crt 4 | /avb.key 5 | /*.zip 6 | /*.zip.patched 7 | /.tmp/ 8 | -------------------------------------------------------------------------------- /.github/workflows/renovate.yaml: -------------------------------------------------------------------------------- 1 | name: Renovate 2 | on: 3 | schedule: 4 | - cron: '0 20 * * *' 5 | 6 | workflow_dispatch: 7 | 8 | # Rebase open PRs on push 9 | push: 10 | branches: [ main ] 11 | 12 | pull_request: 13 | types: [synchronize, opened, reopened] 14 | paths: 15 | - .github/workflows/renovate.yaml 16 | jobs: 17 | renovate: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v6 22 | - name: Self-hosted Renovate 23 | uses: renovatebot/github-action@822441559e94f98b67b82d97ab89fe3003b0a247 # v44.2.0 24 | with: 25 | configurationFile: .github/renovate.json5 26 | # https://docs.renovatebot.com/modules/platform/github/#running-using-a-fine-grained-token 27 | token: ${{ secrets.RENOVATE_TOKEN }} 28 | env: 29 | RENOVATE_REPOSITORIES: ${{ github.repository }} 30 | LOG_LEVEL: ${{ inputs.logLevel || 'debug' }} 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 schnatterer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "https://docs.renovatebot.com/renovate-schema.json", 3 | extends: [ 4 | "config:best-practices", 5 | ":dependencyDashboard" 6 | ], 7 | prHourlyLimit: 0, 8 | labels: ["dependencies"], 9 | vulnerabilityAlerts: { 10 | labels: ["security", "dependencies"], 11 | }, 12 | packageRules: [ 13 | { 14 | matchPackageNames: ["*"], 15 | description: "Automatically merge minor and patch-level updates", 16 | matchUpdateTypes: ["minor", "patch" ], 17 | automerge: true, 18 | }, 19 | ], 20 | customManagers: [ 21 | { 22 | customType: "regex", 23 | fileMatch: ["rooted-ota.sh"], 24 | matchStrings: [ 25 | // Match lines like these: 26 | // # renovate: datasource=github-releases packageName=chenxiaolong/avbroot versioning=semver 27 | //AVB_ROOT_VERSION=3.9.0 28 | // See https://docs.renovatebot.com/modules/manager/regex/ 29 | "# renovate: datasource=(?[a-z-]+?)(?: depName=(?.+?))? packageName=(?.+?)(?: versioning=(?[a-z-]+?))?\\s.+?_VERSION=(?.+?)\\s", 30 | // # renovate: datasource=git-refs packageName=https://github.com/chenxiaolong/my-avbroot-setup currentValue=master 31 | //PATCH_PY_COMMIT=334e506d 32 | // https://docs.renovatebot.com/modules/datasource/git-refs/ 33 | "# renovate: datasource=(?[a-z-]+?)(?: depName=(?.+?))? packageName=(?.+?)(?: currentValue=(?[a-z-]+?))?\\s.+?_COMMIT=(?.+?)\\s", 34 | ] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/release-single.yaml: -------------------------------------------------------------------------------- 1 | name: Release single device 2 | 3 | on: 4 | # Note that push/PR will not release, only build to see if there are obvious failures 5 | push: 6 | branches: 7 | - '*' 8 | paths: 9 | - rooted-ota.sh 10 | pull_request: 11 | types: [synchronize, opened, reopened] 12 | paths: 13 | - rooted-ota.sh 14 | 15 | workflow_call: 16 | inputs: 17 | device-id: 18 | type: string 19 | magisk-preinit-device: 20 | type: string 21 | default: '' 22 | 23 | # Allows you to run this workflow manually from the Actions tab 24 | workflow_dispatch: 25 | inputs: 26 | device-id: 27 | description: Device ID 28 | required: true 29 | magisk-preinit-device: 30 | description: Magisk preinit device 31 | required: false 32 | skip-rootless: 33 | description: skip building rootless OTA 34 | type: boolean 35 | required: false 36 | skip-rooted: 37 | description: skip building rooted OTA 38 | type: boolean 39 | required: false 40 | upload-test-ota: 41 | description: Upload OTA to test folder 42 | required: false 43 | type: boolean 44 | force-build: 45 | description: Force artifacts to be built and uploaded to release if non-existing 46 | required: false 47 | type: boolean 48 | force-ota-server-upload: 49 | description: Force OTA server upload 50 | required: false 51 | type: boolean 52 | skip-release: 53 | description: Skip release (build only) 54 | required: false 55 | type: boolean 56 | ota-version: 57 | description: OTA version 58 | required: false 59 | magisk-version: 60 | description: Magisk version 61 | required: false 62 | jobs: 63 | build-device: 64 | runs-on: ubuntu-latest 65 | timeout-minutes: 20 # Make sure to not waste hours when some command does not return 66 | steps: 67 | - uses: actions/checkout@v6 68 | with: 69 | # Allow for switching to github-pages branch 70 | fetch-depth: 0 71 | - name: Set inputs 72 | # Empty means, use version defined in rooted-ota.sh 73 | # Note the difference between github.event.inputs (workflow_dispatch) and inputs (workflow_call) 74 | run: | 75 | echo "DEVICE_ID=$(echo '${{ github.event.inputs.device-id || inputs.device-id || 'shiba' }}' | xargs)" >> $GITHUB_ENV 76 | if [[ "${{ github.event.inputs.skip-rooted }}" != "true" ]]; then 77 | echo "MAGISK_PREINIT_DEVICE=$(echo '${{ github.event.inputs.magisk-preinit-device || inputs.magisk-preinit-device || 'sda10' }}' | xargs)" >> $GITHUB_ENV 78 | fi 79 | 80 | echo "MAGISK_VERSION=$(echo '${{ github.event.inputs.magisk-version || '' }}' | xargs)" >> $GITHUB_ENV 81 | echo "OTA_VERSION=$(echo '${{ github.event.inputs.ota-version || '' }}' | xargs)" >> $GITHUB_ENV 82 | echo "FORCE_OTA_SERVER_UPLOAD=$(echo '${{ github.event.inputs.force-ota-server-upload || '' }}' | xargs)" >> $GITHUB_ENV 83 | echo "FORCE_BUILD=$(echo '${{ github.event.inputs.force-build || '' }}' | xargs)" >> $GITHUB_ENV 84 | echo "UPLOAD_TEST_OTA=$(echo '${{ github.event.inputs.upload-test-ota || '' }}' | xargs)" >> $GITHUB_ENV 85 | echo "SKIP_ROOTLESS=$(echo '${{ github.event.inputs.skip-rootless || '' }}' | xargs)" >> $GITHUB_ENV 86 | echo "SKIP_RELEASE=$(echo '${{ github.event.inputs.skip-release || '' }}' | xargs)" >> $GITHUB_ENV 87 | 88 | if [[ "${{ github.event_name }}" == "push" ]] || [[ "${{ github.event_name }}" == "pull_request" ]]; then 89 | echo "Running on push: Simple build without release for device $DEVICE_ID" 90 | echo "FORCE_BUILD=true" >> $GITHUB_ENV 91 | echo "SKIP_RELEASE=true" >> $GITHUB_ENV 92 | echo "GITHUB_TOKEN=must-be-set-but-is-not-used" >> $GITHUB_ENV 93 | else 94 | echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV 95 | fi 96 | - run: sudo apt-get install -y jq curl git 97 | - name: release 98 | env: 99 | GITHUB_REPO: ${{ github.repository }} 100 | # Load dev keys from vars. This is a random key, only used for signing throwaway dev builds. Not a leak! 101 | # Loading the keys from secrets will make them inaccessible for untrusted PRs, leaving us with failed builds. 102 | KEY_AVB_BASE64: ${{ vars.KEY_AVB_BASE64 }} 103 | CERT_OTA_BASE64: ${{ vars.CERT_OTA_BASE64 }} 104 | KEY_OTA_BASE64: ${{ vars.KEY_OTA_BASE64 }} 105 | #PASSPHRASE_AVB: ${{ vars.PASSPHRASE_AVB }} 106 | #PASSPHRASE_OTA: ${{ vars.PASSPHRASE_OTA }} 107 | run: | 108 | if [[ $SKIP_RELEASE == 'true' ]]; then 109 | DEBUG=1 bash -c '. rooted-ota.sh && createRootedOta && createOtaServerData' 110 | else 111 | DEBUG=1 bash -c '. rooted-ota.sh && createAndReleaseRootedOta' 112 | fi 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | rooted-graphene 2 | === 3 | 4 | GrapheneOS over the air updates (OTAs) patched with Magisk allowing for AVB and locked bootloader *and* root access. 5 | Can be upgraded over the air using [Custota](https://github.com/chenxiaolong/Custota) and its own OTA server. 6 | Allows for switching between magisk and rootless via OTA upgrades. 7 | 8 | > ⚠️ OS and root work in general. However, zygisk does not (and [likely never will](https://github.com/topjohnwu/Magisk/pull/7606)) 9 | > work, leading to magisk being easily discovered by other apps and lots of banking apps not working. 10 | See [below](#using-other-rooting-mechanisms) for alternatives. 11 | 12 | ## Supported devices 13 | 14 | See [rooted-graphene/ota | .github/workflows/release-multiple.yaml](https://github.com/rooted-graphene/ota/blob/main/.github/workflows/release-multiple.yaml). 15 | 16 | I plan to support as many devices as the GitHub Action limit allows for as long as this project is useful to me. 17 | 18 | If you would like to see more devices, add them via PR to the file mentioned above. 19 | Alternatively, it's easy to [set up your own builds](#setting-up-your-own-ota-builds), which also makes you the owner of the signing keys. 20 | 21 | If this project is useful to you, please consider **[donating to GrapheneOS](https://grapheneos.org/donate)**. 22 | Please note that rooted-graphene is not an official GrapheneOS project. 23 | As they do most of the heavy lifting, I think they deserve every support they can get. 24 | 25 | ## Notable changelog 26 | 27 | These are only changes related to rooted-graphene, not GrapheneOS itself. 28 | See [grapheneos.org/releases](https://grapheneos.org/releases) for that. 29 | 30 | ### [#173](https://github.com/schnatterer/rooted-graphene/pull/173), Sept 27, 2025 31 | Rooted-graphene opts-in to use the `stable-security-preview`. 32 | 33 | Basically, this gets us security fixes a lot faster at the cost of patches not being open source at the moment of release. 34 | 35 | The fact that rooted-graphene is patched into the original OTA binaries and not built from source makes this possible. 36 | If you prefer staying with `stable` you can easily [set up your own builds](#setting-up-your-own-ota-builds) and set 37 | `OTA_CHANNEL` to `stable`. 38 | 39 | More info: 40 | > We're allowed to provide an early release with these patches and to list the CVEs but must wait until the embargo ends to publish sources or details on the patches. 41 | > The positive side is that we can now provide patches to people who truly need them without even the previous 1 month embargo delay. 42 | https://grapheneos.org/releases#2025092500 43 | 44 | > We do consider the security previews to be the normal and recommended choice. 45 | https://grapheneos.social/@GrapheneOS/115272851393143127 46 | 47 | ### [#141](https://github.com/schnatterer/rooted-graphene/pull/141), July 10, 2025 48 | 49 | Upgrade to Custota 5.12, which contained a major regression where settings did not get migrated properly and got reset. 50 | Unfortunately, you will have to set the OTA URL again, to get the next update. 51 | 52 | [Fixed with Custota 5.13](https://github.com/chenxiaolong/Custota/blob/v5.13/CHANGELOG.md), 6dc6c4f on July 18, 2025. 53 | 54 | > * Updating to this version will automatically restore the old settings without any manual intervention 55 | > * If noticed your settings get reset in 5.12 and already reconfigured the app, your new settings will not be touched. 56 | 57 | ### [#114](https://github.com/schnatterer/rooted-graphene/pull/114), May 22, 2025 58 | 59 | Upgrades to magisk 29. 60 | 61 | There seems to be a bug that can occur with magisk updates and avbroot. 62 | 63 | [chenxiaolong/avbroot#455 (comment)](https://github.com/chenxiaolong/avbroot/issues/455#issuecomment-2955973508) 64 | 65 | contains some approaches to troubleshooting. 66 | This worked for me (at the expense of resetting Magisk's settings); 67 | ```bash 68 | su -c 'rm -r /data/adb/magisk* && reboot' 69 | ``` 70 | 71 | See also [rooted-graphene#5](https://github.com/rooted-graphene/ota/issues/5). 72 | 73 | ### 2025032500 74 | 75 | The OTA builds moved into a separate GitHub organization to get full GitHub action minutes budget. 76 | With this, it is possible to add support for [devices discontinued lately](#2025030200) again 🥳. 77 | 78 | > ⚠️ You need to change the OTA server url in custota app to either 79 | > https://rooted-graphene.github.io/ota/magisk 80 | > or 81 | > https://rooted-graphene.github.io/ota/rootless 82 | 83 | Note that the old URL https://schnatterer.github.io/rooted-graphene/ will no longer receive new OTAs soon. 84 | 85 | Some more details: 86 | * A GitHub organization has 2000 free GitHub Action Minutes per month. 87 | * Each device build takes 10 Minutes. 88 | * There are about 4 stable releases per month. 89 | * The budget should last for the current devices and even provide room to support more 🥳 90 | 91 | ### 2025030200 92 | 93 | * Discontinued some devices (Pixel 8 Pro (husky), Pixel 8 (shiba), Pixel 6a (bluejay)), because the amount of GitHub actions minutes required for 94 | maintaining that many devices exceed my spending limit. 95 | Please fork this repo and build your own OTAs (see [Supported Devices](#supported-devices)). 96 | ![image](https://github.com/user-attachments/assets/11cf8fe9-b846-4979-8d7c-723408681354) 97 | * Switch from custota signature file version 1 to 2 (introduced with [custota 5](https://github.com/chenxiaolong/Custota/blob/v5.0/CHANGELOG.md) in october 2024) 98 | * If you're using custoa magisk module version < 5, please upgrade. 99 | Even better: Delete custota magisk module, because it is now packaged in the OTA. 100 | 101 | ### 2025021100 102 | * Start shipping custota app with OTA 103 | * This allows for OTA updates even when rootless and relieves you of the burden to keep the magisk module up to date. 104 | Starting with the next version, this will allow you to switch root and off by installing OTA updates! 105 | * In the `-magisk` flavor of rooted-graphene, the custota magisk module should be automatically disabled 106 | on start. You can safely remove it. Custota is now a system app. 107 | * In the `-rootless` flavor the custota should be new, so no problems. 108 | Except when you had it installed as magisk module before (using the `-magisk` flavor). 109 | Then you should `adb sideload` the `-magisk` first. Then custota should work as a system app. 110 | Then you should be able to switch to `-rootless` with custota working. 111 | Here are some troubleshooting tipps. 112 | * test, if an upgrading works by long pressing `Version` in custota and then selecting `Allow reinstall`. 113 | This way you can also switch from `-magisk` to `-rootless` (and back if everything works as planned). 114 | * you might have to change ownership or delete these files: 115 | * `/sdcard/Android/data/com.chiller3.custota/` 116 | * `/data/ota_packagecare_map.pb` 117 | * If you no longer have root, you can always delete modules using `adb`, see [#82](https://github.com/schnatterer/rooted-graphene/issues/82). 118 | 119 | ## Initial installation of OS 120 | 121 | ### Hints 122 | * Make sure the versions of the unpatched version initially installed and the one taken from this repo match. 123 | * You might want to start with the version before the latest to try if OTA is working before initializing your device. 124 | * Don't mix up **factory image** and OTA 125 | * The following steps are basically the ones described at [avbroot](https://github.com/chenxiaolong/avbroot#initial-setup) 126 | using the `avb_pkmd.bin` from [this repo](https://github.com/rooted-graphene/ota/). 127 | 128 | ### Installation 129 | 130 | ⚠️ Please be aware that there is always some risk involved when flashing your device. 131 | Especially since the first `Device is corrupt. It can't be trusted` messages started appearing in [2025032500](https://github.com/schnatterer/rooted-graphene/issues/89). 132 | In relation to this error, 133 | we heard [multiple](https://github.com/schnatterer/rooted-graphene/issues/96#issuecomment-3123443894) [reports](https://github.com/schnatterer/rooted-graphene/issues/96#issuecomment-3358048965) about hard bricks. 134 | The steps listed below should work around this issue, though. 135 | 136 | Still, if flashing fails, [**don't switch the slot**](https://github.com/schnatterer/rooted-graphene/issues/96#issuecomment-3128121844). 137 | Read through the comments on [this issue](https://github.com/schnatterer/rooted-graphene/issues/96) or reach out for help. 138 | In case your device should refuse to boot, [this project](https://github.com/JoshuaDoes/tensor-usbdl/) might be helpful. 139 | 140 | Be careful! 141 | I only provide this software. 142 | You are using it at your own risk. 143 | 144 | #### Install GrapheneOS 145 | 146 | ##### Web Installer 147 | 148 | Using the web installer is easier, but will always install the latest version. 149 | So it's not possible to verify if OTA upgrades work right away. 150 | 151 | Use the [web installer](https://grapheneos.org/install/web) to install GrapheneOS: 152 | * Write down the installed version, e.g. `Downloaded caiman-install-2024123000.zip release`. 153 | * Stop at `Locking the bootloader` and close the browser. 154 | We'll lock the bootloader later! 155 | 156 | ##### Manual install 157 | 158 | Alternative method to Web installer. 159 | 160 | Download [**factory image**](https://grapheneos.org/releases) and follow the [official instructions](https://grapheneos.org/install/cli) to install GrapheneOS. 161 | 162 | TLDR: 163 | 164 | * Enable OEM unlocking 165 | * Obtain latest `fastboot` 166 | * Unlock Bootloader: 167 | Enable usb debugging and execute `adb reboot bootloader`, or 168 | > The easiest approach is to reboot the device and begin holding the volume down button until it boots up into the bootloader interface. 169 | ```shell 170 | fastboot flashing unlock 171 | ``` 172 | * flash factory image 173 | 174 | ```shell 175 | bsdtar xvf DEVICE_NAME-factory-VERSION.zip # tar on windows and mac 176 | ./flash-all.sh # or .bat on windows 177 | ```` 178 | * Stop after that and reboot (leave bootloader unlocked) 179 | 180 | #### Patch GrapheneOS with OTAs from this image 181 | 182 | Once GrapheneOS is installed 183 | 184 | * Download the [OTA from releases](https://github.com/rooted-graphene/ota/releases) with **the same version** that you just installed. 185 | * Obtain latest `fastboot` 186 | * Install [avbroot](https://github.com/chenxiaolong/avbroot) 187 | * Extract the partition images from the patched OTA that are different from the original. 188 | ```bash 189 | avbroot ota extract \ 190 | --input /path/to/ota.zip.patched \ 191 | --directory extracted \ 192 | --fastboot 193 | ``` 194 | * Set this environment variable to match the extracted folder: 195 | 196 | For Linux/macOS: 197 | ```bash 198 | export ANDROID_PRODUCT_OUT=extracted 199 | ``` 200 | 201 | For Windows (powershell): 202 | ```powershell 203 | $env:ANDROID_PRODUCT_OUT = "extracted" 204 | ``` 205 | or (bat): 206 | ```bat 207 | set ANDROID_PRODUCT_OUT=extracted 208 | ``` 209 | 210 | * Flash the partitions using the command: 211 | ```bash 212 | fastboot flashall --skip-reboot 213 | ``` 214 | * Set up the custom AVB public key in the bootloader. 215 | (If you built your own OTA, use your `avb_pkmd.bin`.) 216 | ```bash 217 | fastboot reboot-bootloader 218 | fastboot erase avb_custom_key 219 | curl -s https://raw.githubusercontent.com/rooted-graphene/ota/refs/heads/main/avb_pkmd.bin > avb_pkmd.bin 220 | fastboot flash avb_custom_key avb_pkmd.bin 221 | ``` 222 | * Sideload the OTA 223 | (to avoid `Device is corrupt. It can't be trusted` error) 224 | 1. Run `fastboot reboot recovery` to get into recovery mode 225 | 2. You should see an android icon lying down with the text "No command". 226 | Hold the power button and press the volume up button a single time to get into the recovery GUI 227 | 3. Use volume buttons to navigate to "Apply update from ADB" and select it with the power button 228 | 4. Like the recovery prompt says, use 229 | `adb sideload ` 230 | to sideload the OTA 231 | 5. After sideloading, select reboot to bootloader 232 | * Lock the bootloader using the following command. 233 | This will trigger a data wipe again. 234 | ```bash 235 | fastboot flashing lock 236 | ``` 237 | * Confirm by pressing volume down and then power. Then reboot. 238 | * Remember: **Do not uncheck `OEM unlocking`!** (to avoid [hard-bricking](https://github.com/chenxiaolong/avbroot/blob/v3.12.0/README.md#warnings-and-caveats)) 239 | That is, in Graphene's startup wizard, leave this box unticked 👇️ 240 | Screenshot of GrapheneOS recommending to lock 241 | Note: The OTA contains [OEMUnlockOnBoot](https://github.com/chenxiaolong/OEMUnlockOnBoot), so OEM locking should be impossible. 242 | Still, better safe than sorry, keep it unlocked. 243 | 244 | #### Set up OTA updates 245 | 246 | * [Disable system updater app](https://github.com/chenxiaolong/avbroot#ota-updates). 247 | * Open Custota app and set the OTA server URL to point to this OTA server: https://rooted-graphene.github.io/ota/magisk 248 | 249 | Alternatively you could do updates manually via `adb sideload`: 250 | * reboot the device and begin holding the volume down button until it boots up into the bootloader interface 251 | * using volume buttons, toggle to recovery. Confirm by pressing power button 252 | * If the screen is stuck at a `No command` message, press the volume up button once while holding down the power button. 253 | * using volume buttons, toggle to `Apply update from ADB`. Confirm by pressing power button 254 | * `adb sideload xyz.zip` 255 | * See also [here](https://github.com/chenxiaolong/avbroot#updates). 256 | 257 | ## Switching between root and rootless 258 | 259 | To remove root, you can change to the "rootless" flavor. 260 | 261 | To do so, set the following URL in custota: https://rooted-graphene.github.io/ota/rootless/ 262 | And then upgrade. 263 | (if custota should tell you that you're on the latest version, you can force an upgrade by long pressing `Version` and 264 | then selecting `Allow reinstall`). 265 | 266 | If you want to gain root again, just switch back to this URL in custota: https://rooted-graphene.github.io/ota/magisk/ 267 | And then upgrade. 268 | 269 | ## Magisk preinit strings 270 | 271 | See [release-multiple.yaml](https://github.com/rooted-graphene/ota/blob/main/.github/workflows/release-multiple.yaml) for examples. 272 | 273 | How to extract: 274 | 275 | * Get boot.img either from factory image or from OTA via 276 | ```shell 277 | avbroot ota extract \ 278 | --input /path/to/ota.zip \ 279 | --directory . \ 280 | --boot-only 281 | ``` 282 | * Install magisk, patch boot.img, look for this string in the output: 283 | `Pre-init storage partition device ID: ` 284 | * Alternatively, extract from the patched boot.img: 285 | ```shell 286 | avbroot boot magisk-info \ 287 | --image magisk_patched-*.img 288 | ``` 289 | * See also: https://github.com/chenxiaolong/avbroot/blob/master/README.md#magisk-preinit-device 290 | 291 | ## Setting up your own OTA builds 292 | 293 | * Create your own keys `bash -c 'source rooted-ota.sh && generateKeys'` and store them in a dry and safe place. 294 | * Fork the [ota repo](https://github.com/rooted-graphene/ota) and add the following Repository secrets (`https://github.com/$YOU/$YOUR_REPO/settings/secrets/actions`) 295 | * CERT_OTA_BASE64 (`base64 -w0 < ota.crt`) 296 | * KEY_AVB_BASE64 (`base64 -w0 < avb.key`) 297 | * KEY_OTA_BASE64 (`base64 -w0 < ota.key`) 298 | * PASSPHRASE_AVB (The passphrase for `avb.key`) 299 | * PASSPHRASE_OTA (The passphrase for `ota.key`) 300 | * Uncomment or add your device(s) in `.github/workflows/release-multiple.yaml` 301 | See [Magisk preinit string](#magisk-preinit-strings). 302 | 303 | This sets up a cron job that builds the latest version of GrapheneOS, nightly. 304 | 305 | This way, you won't have too many maintenance efforts but own your own signing key! 306 | You can also add a 3rd-party-magisk package if you're willing to trust the authors 307 | (see [Using other rooting mechanisms](#using-other-rooting-mechanisms)). 308 | 309 | Alternatively, search the forks if someone maintains the device of your choice. 310 | Be aware that you would also have to trust them in addition to [me](https://github.com/schnatterer), [chenxiaolong](https://github.com/chenxiaolong) (the author of avbroot and Custota), 311 | the authors of magisk, the authors of GrapheneOS, and the authors of the android open source project. 312 | 313 | ## Script 314 | 315 | You can use the `rooted-ota.sh` script in this repo to create your own OTAs and run your own OTA server. 316 | 317 | ### Only create patched OTAs 318 | 319 | ```shell 320 | # Generate keys 321 | bash -c 'source rooted-ota.sh && generateKeys' 322 | 323 | # Enter passphrases interactively 324 | DEVICE_ID=oriole MAGISK_PREINIT_DEVICE='metadata' bash -c '. rooted-ota.sh && createRootedOta' 325 | 326 | # Enter passphrases via env (e.g. on CI) 327 | export PASSPHRASE_AVB=1 328 | export PASSPHRASE_OTA=1 329 | DEVICE_ID=oriole MAGISK_PREINIT_DEVICE='metadata' bash -c '. rooted-ota.sh && createRootedOta' 330 | ``` 331 | 332 | For IDs see [grapheneos.org/releases](https://grapheneos.org/releases). For Magisk preinit see,e.g. [here](#magisk-preinit-strings). 333 | 334 | ### Upload patched OTAs as GH release and provide OTA server via GH pages 335 | 336 | See GitHub actions for automating this: 337 | * [release single device](.github/workflows/release-single.yaml) 338 | * [release multiple devices](https://github.com/rooted-graphene/ota/blob/main/.github/workflows/release-multiple.yaml) regularly (using cron) 339 | 340 | ```shell 341 | GITHUB_TOKEN=gh... \ 342 | GITHUB_REPO=schnatterer/rooted-graphene \ 343 | DEVICE_ID=oriole \ 344 | MAGISK_PREINIT_DEVICE=metadata \ 345 | bash -c '. rooted-ota.sh && createAndReleaseRootedOta' 346 | ``` 347 | 348 | ### Using other rooting mechanisms 349 | 350 | As [magisk does not seem a perfect match for GrapheneOS](https://github.com/topjohnwu/Magisk/pull/7606), you might be looking for alternatives. 351 | 352 | I had a first go at [patching kernelsu](https://github.com/schnatterer/rooted-graphene/commit/201b6dc939ab3a202694fa892de6db2840e5c3d6) which booted but did not provide root. 353 | Patching kernelsu is much more complex that patching magisk. 354 | It might even be impossible to run GrapheneOS with it, without building GrapheneOS from scratch. 355 | Also, some parts of kernelsu seem to be closed source, which feels suspicious and inappropriate for a tool with so much influence on your device. 356 | 357 | Another alternative might be to use a version of magisk (like [the one maintained by pixincreate](https://github.com/pixincreate/Magisk)) that contains patches to make zygisk work. 358 | This still has some limitations, like [certain modules checking for magisk's signature won't work](https://github.com/schnatterer/rooted-graphene/commit/da0cd817c2665798df46df1aeb7caef9d98b79d0#r141746606). 359 | 360 | Another option [might be](https://github.com/schnatterer/rooted-graphene/pull/73#issuecomment-2666870886) Kitsune magisk. 361 | 362 | In general, using [magisk and especially zygisk with Graphene seems to have the risk of breaking things with every new release](https://github.com/chenxiaolong/avbroot/issues/213#issuecomment-1986637884). 363 | It's good to have the rootless version as a fallback! 364 | 365 | ## Development 366 | ```bash 367 | # DEBUG some parts of the script interactively 368 | DEBUG=1 bash --init-file rooted-ota.sh 369 | # Test loading secrets from env 370 | PASSPHRASE_AVB=1 PASSPHRASE_OTA=1 bash -c '. rooted-ota.sh && key2base64 && KEY_AVB=doesnotexist createAndReleaseRootedOta' 371 | 372 | # Avoid having to download OTA all over again: SKIP_CLEANUP=true or: 373 | mkdir -p .tmp && ln -s $PWD/shiba-ota_update-2023121200.zip .tmp/shiba-ota_update-2023121200.zip 374 | 375 | # Test only patching 376 | export PASSPHRASE_AVB=x PASSPHRASE_OTA=y 377 | SKIP_CLEANUP=true DEVICE_ID=oriole MAGISK_PREINIT_DEVICE='metadata' bash -c '. rooted-ota.sh && createRootedOta' 378 | 379 | # Test only releasing 380 | GITHUB_TOKEN=gh... \ 381 | DEBUG=true \ 382 | GITHUB_REPO=schnatterer/rooted-graphene \ 383 | OTA_VERSION=2025021100 \ 384 | RELEASE_ID='' \ 385 | bash -c '. rooted-ota.sh && releaseOta' 386 | # Test only GH pages deployment 387 | GITHUB_REPO=schnatterer/rooted-graphene \ 388 | DEVICE_ID=oriole \ 389 | MAGISK_PREINIT_DEVICE=metadata \ 390 | bash -c '. rooted-ota.sh && findLatestVersion && checkBuildNecessary && createOtaServerData && uploadOtaServerData' 391 | 392 | 393 | # e2e test 394 | GITHUB_TOKEN=gh... \ 395 | GITHUB_REPO=schnatterer/rooted-graphene \ 396 | DEVICE_ID=oriole \ 397 | MAGISK_PREINIT_DEVICE=metadata \ 398 | SKIP_CLEANUP=true \ 399 | DEBUG=1 \ 400 | bash -c '. rooted-ota.sh && createAndReleaseRootedOta' 401 | ``` 402 | 403 | ## References, Inspiration 404 | https://github.com/MuratovAS/grapheneos-magisk/blob/main/docker/Dockerfile 405 | 406 | https://xdaforums.com/t/guide-to-lock-bootloader-while-using-rooted-otaos-magisk-root.4510295/ 407 | -------------------------------------------------------------------------------- /rooted-ota.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Requires git, jq, and curl 4 | 5 | KEY_AVB=${KEY_AVB:-avb.key} 6 | KEY_OTA=${KEY_OTA:-ota.key} 7 | CERT_OTA=${CERT_OTA:-ota.crt} 8 | # Or else, set these env vars 9 | KEY_AVB_BASE64=${KEY_AVB_BASE64:-''} 10 | KEY_OTA_BASE64=${KEY_OTA_BASE64:-''} 11 | CERT_OTA_BASE64=${CERT_OTA_BASE64:-''} 12 | 13 | # Set these env vars, or else these params will be queries interactively 14 | # PASSPHRASE_AVB 15 | # PASSPHRASE_OTA 16 | 17 | # Enable debug output only after sensitive vars have been set, to reduce risk of leak 18 | DEBUG=${DEBUG:-''} 19 | if [[ -n "${DEBUG}" ]]; then set -x; fi 20 | 21 | # Mandatory params 22 | DEVICE_ID=${DEVICE_ID:-} # See here for device IDs https://grapheneos.org/releases 23 | GITHUB_TOKEN=${GITHUB_TOKEN:-''} 24 | GITHUB_REPO=${GITHUB_REPO:-''} 25 | 26 | # Optional 27 | # If you want an OTA patched with magisk, set the preinit for your device 28 | MAGISK_PREINIT_DEVICE=${MAGISK_PREINIT_DEVICE:-} 29 | # Skip creation of rootless OTA by setting to "true" 30 | SKIP_ROOTLESS=${SKIP_ROOTLESS:-'false'} 31 | # https://grapheneos.org/releases#stable-channel 32 | OTA_VERSION=${OTA_VERSION:-'latest'} 33 | 34 | # It's recommended to pin magisk version in combination with AVB_ROOT_VERSION. 35 | # Breaking changes in magisk might need to be adapted in new avbroot version 36 | # Find latest magisk version here: https://github.com/topjohnwu/Magisk/releases, or: 37 | # curl --fail -sL -I -o /dev/null -w '%{url_effective}' https://github.com/topjohnwu/Magisk/releases/latest | sed 's/.*\/tag\///;' 38 | # renovate: datasource=github-releases packageName=topjohnwu/Magisk versioning=semver-coerced 39 | DEFAULT_MAGISK_VERSION=v30.6 40 | MAGISK_VERSION=${MAGISK_VERSION:-${DEFAULT_MAGISK_VERSION}} 41 | 42 | SKIP_CLEANUP=${SKIP_CLEANUP:-''} 43 | 44 | # For committing to GH pages in different repo, clone it to a different folder and set this var 45 | PAGES_REPO_FOLDER=${PAGES_REPO_FOLDER:-''} 46 | 47 | # Set asset released by this script to latest version, even when OTA_VERSION already exists for this device 48 | FORCE_OTA_SERVER_UPLOAD=${FORCE_OTA_SERVER_UPLOAD:-'false'} 49 | # Forces the artifacts to be built (and uploaded to a release) 50 | # even it a release already contains the combination of device and flavor. 51 | # This will lead to multiple artifacts with different commits on the release (that are not linked in the OTA server and thus are likely never used). 52 | # However, except for test builds, we want the changes to be rolled out with new version. 53 | # So these artifacts are just a waste of storage resources. Example 54 | # shiba-2025020500-3e0add9-rootless.zip 55 | # shiba-2025020500-6718632-rootless.zip 56 | FORCE_BUILD=${FORCE_BUILD:-'false'} 57 | # Skip setting asset released by this script to latest version, even when OTA_VERSION is latest for this device 58 | # Takes precedence over FORCE_OTA_SERVER_UPLOAD 59 | SKIP_OTA_SERVER_UPLOAD=${SKIP_OTA_SERVER_UPLOAD:-'false'} 60 | # Skip patching modules (custota and oemunlockunboot) into OTA 61 | SKIP_MODULES=${SKIP_MODULES:-'false'} 62 | # Upload OTA to test folder on OTA server 63 | UPLOAD_TEST_OTA=${UPLOAD_TEST_OTA:-false} 64 | 65 | OTA_CHANNEL=${OTA_CHANNEL:-stable-security-preview} # Alternative: 'stable' or 'alpha' 66 | NO_COLOR=${NO_COLOR:-''} 67 | OTA_BASE_URL="https://releases.grapheneos.org" 68 | 69 | # renovate: datasource=github-releases packageName=chenxiaolong/avbroot versioning=semver 70 | AVB_ROOT_VERSION=3.23.3 71 | # renovate: datasource=github-releases packageName=chenxiaolong/Custota versioning=semver-coerced 72 | CUSTOTA_VERSION=5.19 73 | # renovate: datasource=git-refs packageName=https://github.com/chenxiaolong/my-avbroot-setup currentValue=master 74 | PATCH_PY_COMMIT=84139189c8cbe244a676582a3b3517f31fabc421 75 | # renovate: datasource=docker packageName=python 76 | PYTHON_VERSION=3.14.2-alpine 77 | # renovate: datasource=github-releases packageName=chenxiaolong/OEMUnlockOnBoot versioning=semver-coerced 78 | OEMUNLOCKONBOOT_VERSION=1.3 79 | # renovate: datasource=github-releases packageName=chenxiaolong/afsr versioning=semver 80 | AFSR_VERSION=1.0.4 81 | 82 | CHENXIAOLONG_PK='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDOe6/tBnO7xZhAWXRj3ApUYgn+XZ0wnQiXM8B7tPgv4' 83 | GIT_PUSH_RETRIES=10 84 | 85 | set -o nounset -o pipefail -o errexit 86 | 87 | declare -A POTENTIAL_ASSETS 88 | 89 | function generateKeys() { 90 | downloadAvBroot 91 | # https://github.com/chenxiaolong/avbroot/tree/077a80f4ce7233b0e93d4a1477d09334af0da246#generating-keys 92 | # Generate the AVB and OTA signing keys. 93 | .tmp/avbroot key generate-key -o $KEY_AVB 94 | .tmp/avbroot key generate-key -o $KEY_OTA 95 | 96 | # Convert the public key portion of the AVB signing key to the AVB public key metadata format. 97 | # This is the format that the bootloader requires when setting the custom root of trust. 98 | .tmp/avbroot key extract-avb -k $KEY_AVB -o avb_pkmd.bin 99 | 100 | # Generate a self-signed certificate for the OTA signing key. This is used by recovery to verify OTA updates when sideloading. 101 | .tmp/avbroot key generate-cert -k $KEY_OTA -o $CERT_OTA 102 | 103 | echo Upload these to your CI server, if necessary. 104 | echo The script takes these values as env or file 105 | key2base64 106 | } 107 | 108 | function key2base64() { 109 | KEY_AVB_BASE64=$(base64 -w0 "$KEY_AVB") && echo "KEY_AVB_BASE64=$KEY_AVB_BASE64" 110 | KEY_OTA_BASE64=$(base64 -w0 "$KEY_OTA") && echo "KEY_OTA_BASE64=$KEY_OTA_BASE64" 111 | CERT_OTA_BASE64=$(base64 -w0 "$CERT_OTA") && echo "CERT_OTA_BASE64=$CERT_OTA_BASE64" 112 | export KEY_AVB_BASE64 KEY_OTA_BASE64 CERT_OTA_BASE64 113 | } 114 | 115 | function createAndReleaseRootedOta() { 116 | createRootedOta 117 | releaseOta 118 | 119 | createOtaServerData 120 | uploadOtaServerData 121 | } 122 | 123 | function createRootedOta() { 124 | [[ "$SKIP_CLEANUP" != 'true' ]] && trap cleanup EXIT ERR 125 | 126 | findLatestVersion 127 | checkBuildNecessary 128 | downloadAndroidDependencies 129 | patchOTAs 130 | } 131 | 132 | function cleanup() { 133 | print "Cleaning up..." 134 | rm -rf .tmp 135 | unset KEY_AVB_BASE64 KEY_OTA_BASE64 CERT_OTA_BASE64 136 | print "Cleanup complete." 137 | } 138 | 139 | function checkBuildNecessary() { 140 | local currentCommit 141 | currentCommit=$(git rev-parse --short HEAD) 142 | POTENTIAL_ASSETS=() 143 | 144 | if [[ -n "$MAGISK_PREINIT_DEVICE" ]]; then 145 | # e.g. oriole-2023121200-magisk-v26.4-4647f74-dirty.zip 146 | POTENTIAL_ASSETS['magisk']="${DEVICE_ID}-${OTA_VERSION}-${currentCommit}-magisk-${MAGISK_VERSION}$(createAssetSuffix).zip" 147 | else 148 | printGreen "MAGISK_PREINIT_DEVICE not set for device, not creating magisk OTA" 149 | fi 150 | 151 | if [[ "$SKIP_ROOTLESS" != 'true' ]]; then 152 | POTENTIAL_ASSETS['rootless']="${DEVICE_ID}-${OTA_VERSION}-${currentCommit}-rootless$(createAssetSuffix).zip" 153 | else 154 | printGreen "SKIP_ROOTLESS set, not creating rootless OTA" 155 | fi 156 | 157 | RELEASE_ID='' 158 | local response 159 | 160 | if [[ -z "$GITHUB_REPO" ]]; then print "Env Var GITHUB_REPO not set, skipping check for existing release" && return; fi 161 | 162 | print "Potential release: ${OTA_VERSION}" 163 | 164 | local params=() 165 | local url="https://api.github.com/repos/${GITHUB_REPO}/releases" 166 | 167 | if [ -n "${GITHUB_TOKEN}" ]; then 168 | params+=("-H" "Authorization: token ${GITHUB_TOKEN}") 169 | fi 170 | 171 | params+=("-H" "Accept: application/vnd.github.v3+json") 172 | response=$( 173 | curl --fail -sL "${params[@]}" "${url}" | 174 | jq --arg release_tag "${OTA_VERSION}" '.[] | select(.tag_name == $release_tag) | {id, tag_name, name, published_at, assets}' 175 | ) 176 | 177 | if [[ -n ${response} ]]; then 178 | RELEASE_ID=$(echo "${response}" | jq -r '.id') 179 | print "Release ${OTA_VERSION} exists. ID=$RELEASE_ID" 180 | 181 | for flavor in "${!POTENTIAL_ASSETS[@]}"; do 182 | local selectedAsset POTENTIAL_ASSET_NAME="${POTENTIAL_ASSETS[$flavor]}" 183 | print "Checking if asset exists ${POTENTIAL_ASSET_NAME}" 184 | 185 | # Save some storage by not building and uploading every new commit as asset 186 | selectedAsset=$(echo "${response}" | jq -r --arg assetPrefix "${DEVICE_ID}-${OTA_VERSION}" \ 187 | '.assets[] | select(.name | startswith($assetPrefix)) | .name' \ 188 | | grep "${flavor}" || true) 189 | 190 | if [[ -n "${selectedAsset}" ]] && [[ "$FORCE_BUILD" != 'true' ]] && [[ "$UPLOAD_TEST_OTA" != 'true' ]]; then 191 | printGreen "Skipping build of asset name '$POTENTIAL_ASSET_NAME'. Because this flavor already is released with a different commit." \ 192 | "Set FORCE_BUILD or UPLOAD_TEST_OTA to force. Assets found on release: ${selectedAsset//$'\n'/ }" 193 | unset "POTENTIAL_ASSETS[$flavor]" 194 | else 195 | print "No asset found with name '$POTENTIAL_ASSET_NAME'." 196 | fi 197 | done 198 | 199 | if [ "${#POTENTIAL_ASSETS[@]}" -eq 0 ]; then 200 | printGreen "All potential assets already exist. Exiting" 201 | exit 0 202 | fi 203 | else 204 | print "Release ${OTA_VERSION} does not exist." 205 | fi 206 | } 207 | 208 | function checkMandatoryVariable() { 209 | for var_name in "$@"; do 210 | local var_value="${!var_name}" 211 | 212 | if [[ -z "$var_value" ]]; then 213 | printRed "Missing mandatory param $var_name" 214 | exit 1 215 | fi 216 | done 217 | } 218 | 219 | function createAssetSuffix() { 220 | local suffix='' 221 | if [[ "${SKIP_MODULES}" == 'true' ]]; then 222 | suffix+='-minimal' 223 | fi 224 | if [[ "${UPLOAD_TEST_OTA}" == 'true' ]]; then 225 | suffix+='-test' 226 | fi 227 | if [[ -n "$(git status --porcelain --untracked-files=no)" ]]; then 228 | suffix+='-dirty' 229 | fi 230 | echo "$suffix" 231 | } 232 | 233 | function downloadAndroidDependencies() { 234 | checkMandatoryVariable 'MAGISK_VERSION' 'OTA_TARGET' 235 | 236 | mkdir -p .tmp 237 | if ! ls ".tmp/magisk-$MAGISK_VERSION.apk" >/dev/null 2>&1 && [[ "${POTENTIAL_ASSETS['magisk']+isset}" ]]; then 238 | curl --fail -sLo ".tmp/magisk-$MAGISK_VERSION.apk" "https://github.com/topjohnwu/Magisk/releases/download/$MAGISK_VERSION/Magisk-$MAGISK_VERSION.apk" 239 | fi 240 | 241 | if ! ls ".tmp/$OTA_TARGET.zip" >/dev/null 2>&1; then 242 | curl --fail -sLo ".tmp/$OTA_TARGET.zip" "$OTA_URL" 243 | fi 244 | } 245 | 246 | function findLatestVersion() { 247 | checkMandatoryVariable DEVICE_ID 248 | 249 | if [[ "$MAGISK_VERSION" == 'latest' ]]; then 250 | MAGISK_VERSION=$(curl --fail -sL -I -o /dev/null -w '%{url_effective}' https://github.com/topjohnwu/Magisk/releases/latest | sed 's/.*\/tag\///;') 251 | fi 252 | print "Magisk version: $MAGISK_VERSION" 253 | 254 | # Search for a new version grapheneos. 255 | # e.g. https://releases.grapheneos.org/shiba-stable 256 | 257 | if [[ "$OTA_VERSION" == 'latest' ]]; then 258 | OTA_VERSION=$(curl --fail -sL "$OTA_BASE_URL/$DEVICE_ID-$OTA_CHANNEL" | head -n1 | awk '{print $1;}') 259 | fi 260 | GRAPHENE_TYPE=${GRAPHENE_TYPE:-'ota_update'} # Other option: factory 261 | OTA_TARGET="$DEVICE_ID-$GRAPHENE_TYPE-$OTA_VERSION" 262 | OTA_URL="$OTA_BASE_URL/$OTA_TARGET.zip" 263 | # e.g. shiba-ota_update-2023121200 264 | print "OTA target: $OTA_TARGET; OTA URL: $OTA_URL" 265 | } 266 | 267 | function downloadAvBroot() { 268 | downloadAndVerifyFromChenxiaolong 'avbroot' "$AVB_ROOT_VERSION" 269 | } 270 | 271 | function downloadAndVerifyFromChenxiaolong() { 272 | local repo="$1" 273 | local version="$2" 274 | local artifact="${3:-$1}" # optional: If not set, use repo name 275 | 276 | local url="https://github.com/chenxiaolong/${repo}/releases/download/v${version}/${artifact}-${version}-x86_64-unknown-linux-gnu.zip" 277 | local downloadedZipFile 278 | downloadedZipFile="$(mktemp)" 279 | 280 | mkdir -p .tmp 281 | 282 | if ! ls ".tmp/${artifact}" >/dev/null 2>&1; then 283 | curl --fail -sL "${url}" > "${downloadedZipFile}" 284 | curl --fail -sL "${url}.sig" > "${downloadedZipFile}.sig" 285 | 286 | # Validate against author's public key 287 | ssh-keygen -Y verify -I chenxiaolong -f <(echo "chenxiaolong $CHENXIAOLONG_PK") -n file \ 288 | -s "${downloadedZipFile}.sig" < "${downloadedZipFile}" 289 | 290 | echo N | unzip "${downloadedZipFile}" -d .tmp 291 | rm "${downloadedZipFile}"* 292 | chmod +x ".tmp/${artifact}" # e.g. .tmp/custota-tool 293 | fi 294 | } 295 | 296 | function patchOTAs() { 297 | 298 | downloadAvBroot 299 | downloadAndVerifyFromChenxiaolong 'afsr' "$AFSR_VERSION" 300 | if ! ls ".tmp/custota.zip" >/dev/null 2>&1; then 301 | curl --fail -sL "https://github.com/chenxiaolong/Custota/releases/download/v${CUSTOTA_VERSION}/Custota-${CUSTOTA_VERSION}-release.zip" > .tmp/custota.zip 302 | curl --fail -sL "https://github.com/chenxiaolong/Custota/releases/download/v${CUSTOTA_VERSION}/Custota-${CUSTOTA_VERSION}-release.zip.sig" > .tmp/custota.zip.sig 303 | fi 304 | if ! ls ".tmp/oemunlockonboot.zip" >/dev/null 2>&1; then 305 | curl --fail -sL "https://github.com/chenxiaolong/OEMUnlockOnBoot/releases/download/v${OEMUNLOCKONBOOT_VERSION}/OEMUnlockOnBoot-${OEMUNLOCKONBOOT_VERSION}-release.zip" > .tmp/oemunlockonboot.zip 306 | curl --fail -sL "https://github.com/chenxiaolong/OEMUnlockOnBoot/releases/download/v${OEMUNLOCKONBOOT_VERSION}/OEMUnlockOnBoot-${OEMUNLOCKONBOOT_VERSION}-release.zip.sig" > .tmp/oemunlockonboot.zip.sig 307 | fi 308 | if ! ls ".tmp/my-avbroot-setup" >/dev/null 2>&1; then 309 | git clone https://github.com/chenxiaolong/my-avbroot-setup .tmp/my-avbroot-setup 310 | (cd .tmp/my-avbroot-setup && git checkout ${PATCH_PY_COMMIT}) 311 | fi 312 | 313 | base642key 314 | 315 | for flavor in "${!POTENTIAL_ASSETS[@]}"; do 316 | local targetFile=".tmp/${POTENTIAL_ASSETS[$flavor]}" 317 | 318 | if ls "$targetFile" >/dev/null 2>&1; then 319 | printGreen "File $targetFile already exists locally, not patching." 320 | else 321 | local args=() 322 | 323 | args+=("--output" "$targetFile") 324 | args+=("--input" ".tmp/$OTA_TARGET.zip") 325 | args+=("--sign-key-avb" "$KEY_AVB") 326 | args+=("--sign-key-ota" "$KEY_OTA") 327 | args+=("--sign-cert-ota" "$CERT_OTA") 328 | if [[ "$flavor" == 'magisk' ]]; then 329 | args+=("--patch-arg=--magisk" "--patch-arg" ".tmp/magisk-$MAGISK_VERSION.apk") 330 | args+=("--patch-arg=--magisk-preinit-device" "--patch-arg" "$MAGISK_PREINIT_DEVICE") 331 | fi 332 | 333 | # If env vars not set, passphrases will be queried interactively 334 | if [ -v PASSPHRASE_AVB ]; then 335 | args+=("--pass-avb-env-var" "PASSPHRASE_AVB") 336 | fi 337 | 338 | if [ -v PASSPHRASE_OTA ]; then 339 | args+=("--pass-ota-env-var" "PASSPHRASE_OTA") 340 | fi 341 | 342 | if [[ "${SKIP_MODULES}" != 'true' ]]; then 343 | args+=("--module-custota" ".tmp/custota.zip") 344 | args+=("--module-oemunlockonboot" ".tmp/oemunlockonboot.zip") 345 | fi 346 | # We create csig and device JSON for OTA later if necessary 347 | args+=("--skip-custota-tool") 348 | 349 | # We need to add .tmp to PATH, but we can't use $PATH: because this would be the PATH of the host not the container 350 | # Python image is designed to run as root, so chown the files it creates back at the end 351 | # ... room for improvement 😐️ 352 | # shellcheck disable=SC2046 353 | docker run --rm -i $(tty &>/dev/null && echo '-t') -v "$PWD:/app" -w /app \ 354 | -e PATH='/bin:/usr/local/bin:/sbin:/usr/bin/:/app/.tmp' \ 355 | --env-file <(env) \ 356 | python:${PYTHON_VERSION} sh -c \ 357 | "apk add openssh && \ 358 | pip install -r .tmp/my-avbroot-setup/requirements.txt && \ 359 | python .tmp/my-avbroot-setup/patch.py ${args[*]} ; result=\$?; \ 360 | chown -R $(id -u):$(id -g) .tmp; exit \$result" 361 | 362 | printGreen "Finished patching file ${targetFile}" 363 | fi 364 | 365 | done 366 | } 367 | 368 | function base642key() { 369 | set +x # Don't expose secrets to log 370 | if [ -n "$KEY_AVB_BASE64" ]; then 371 | echo "$KEY_AVB_BASE64" | base64 -d >.tmp/$KEY_AVB 372 | KEY_AVB=.tmp/$KEY_AVB 373 | fi 374 | 375 | if [ -n "$KEY_OTA_BASE64" ]; then 376 | echo "$KEY_OTA_BASE64" | base64 -d >.tmp/$KEY_OTA 377 | KEY_OTA=.tmp/$KEY_OTA 378 | fi 379 | 380 | if [ -n "$CERT_OTA_BASE64" ]; then 381 | echo "$CERT_OTA_BASE64" | base64 -d >.tmp/$CERT_OTA 382 | CERT_OTA=.tmp/$CERT_OTA 383 | fi 384 | 385 | if [[ -n "${DEBUG}" ]]; then set -x; fi 386 | } 387 | 388 | function releaseOta() { 389 | 390 | createReleaseIfNecessary 391 | 392 | for flavor in "${!POTENTIAL_ASSETS[@]}"; do 393 | local assetName="${POTENTIAL_ASSETS[$flavor]}" 394 | uploadFile ".tmp/$assetName" "$assetName" "application/zip" 395 | done 396 | } 397 | 398 | function createReleaseIfNecessary() { 399 | checkMandatoryVariable 'GITHUB_REPO' 'GITHUB_TOKEN' 400 | 401 | local response changelog src_repo current_commit 402 | 403 | if [[ -z "$RELEASE_ID" ]]; then 404 | src_repo=$(extractGithubRepo "$(git config --get remote.origin.url)") 405 | 406 | # Security-preview releases end in suffix 01,but anchor links on release page always end in 00 407 | # e.g. 25092501 -> 25092500 408 | OTA_VERSION_ANCHOR="${OTA_VERSION/%01/00}" 409 | if [[ "${GITHUB_REPO}" == "${src_repo}" ]]; then 410 | changelog=$(curl -sL -X POST -H "Authorization: token $GITHUB_TOKEN" \ 411 | -d "{ 412 | \"tag_name\": \"$OTA_VERSION\", 413 | \"target_commitish\": \"main\" 414 | }" \ 415 | "https://api.github.com/repos/$GITHUB_REPO/releases/generate-notes" | jq -r '.body // empty') 416 | # Replace \n by \\n to keep them as chars 417 | changelog="Update to [GrapheneOS ${OTA_VERSION}](https://grapheneos.org/releases#${OTA_VERSION_ANCHOR}).\n\n$(echo "${changelog}" | sed ':a;N;$!ba;s/\n/\\n/g')" 418 | else 419 | # When pushing to different repo's GH pages, generating notes does not make too much sense. Refer to the used repo's "version" instead. 420 | current_commit=$(git rev-parse --short HEAD) 421 | changelog="Update to [GrapheneOS ${OTA_VERSION}](https://grapheneos.org/releases#${OTA_VERSION_ANCHOR}).\n\nRelease created using ${src_repo}@${current_commit}. See [Changelog](https://github.com/${src_repo}/blob/${current_commit}/README.md#notable-changelog)." 422 | fi 423 | 424 | response=$(curl -sL -X POST -H "Authorization: token $GITHUB_TOKEN" \ 425 | -d "{ 426 | \"tag_name\": \"$OTA_VERSION\", 427 | \"target_commitish\": \"main\", 428 | \"name\": \"$OTA_VERSION\", 429 | \"body\": \"${changelog}\" 430 | }" \ 431 | "https://api.github.com/repos/$GITHUB_REPO/releases") 432 | RELEASE_ID=$(echo "${response}" | jq -r '.id // empty') 433 | if [[ -n "${RELEASE_ID}" ]]; then 434 | printGreen "Release created successfully with ID: ${RELEASE_ID}" 435 | elif echo "${response}" | jq -e '.status == "422"' > /dev/null; then 436 | # In case release has been created in the meantime (e.g. matrix job for multiple devices concurrently) 437 | RELEASE_ID=$(curl -sL \ 438 | -H "Authorization: token $GITHUB_TOKEN" \ 439 | -H "Accept: application/vnd.github.v3+json" \ 440 | "https://api.github.com/repos/${GITHUB_REPO}/releases" | \ 441 | jq -r --arg release_tag "${OTA_VERSION}" '.[] | select(.tag_name == $release_tag) | .id // empty') 442 | if [[ -n "${RELEASE_ID}" ]]; then 443 | printGreen "Cannot create release but found existing release for ${OTA_VERSION}. ID=$RELEASE_ID" 444 | else 445 | printRed "Cannot create release for ${OTA_VERSION} because it seems to exist but still cannot find ID." 446 | exit 1 447 | fi 448 | else 449 | errors=$(echo "${response}" | jq -r '.errors') 450 | printRed "Failed to create release for ${OTA_VERSION}. Errors: ${errors}" 451 | exit 1 452 | fi 453 | fi 454 | } 455 | 456 | function uploadFile() { 457 | local sourceFileName="$1" 458 | local targetFileName="$2" 459 | local contentType="$3" 460 | 461 | # Note that --data-binary might lead to out of memory 462 | curl --fail -X POST -H "Authorization: token $GITHUB_TOKEN" \ 463 | -H "Content-Type: $contentType" \ 464 | --upload-file "$sourceFileName" \ 465 | "https://uploads.github.com/repos/$GITHUB_REPO/releases/$RELEASE_ID/assets?name=$targetFileName" 466 | } 467 | 468 | function createOtaServerData() { 469 | downloadCusotaTool 470 | 471 | for flavor in "${!POTENTIAL_ASSETS[@]}"; do 472 | local POTENTIAL_ASSET_NAME="${POTENTIAL_ASSETS[$flavor]}" 473 | local targetFile=".tmp/${POTENTIAL_ASSET_NAME}" 474 | 475 | local args=() 476 | 477 | args+=("--input" "${targetFile}") 478 | args+=("--output" "${targetFile}.csig") 479 | args+=("--key" "$KEY_OTA") 480 | args+=("--cert" "$CERT_OTA") 481 | 482 | # If env vars not set, passphrases will be queried interactively 483 | if [ -v PASSPHRASE_OTA ]; then 484 | args+=("--passphrase-env-var" "PASSPHRASE_OTA") 485 | fi 486 | 487 | .tmp/custota-tool gen-csig "${args[@]}" 488 | 489 | mkdir -p ".tmp/${flavor}" 490 | 491 | local args=() 492 | args+=("--file" ".tmp/${flavor}/${DEVICE_ID}.json") 493 | # e.g. https://github.com/schnatterer/rooted-graphene/releases/download/2023121200-v26.4-e54c67f/oriole-ota_update-2023121200.zip 494 | # Instead of constructing the location we could also parse it from the upload response 495 | args+=("--location" "https://github.com/$GITHUB_REPO/releases/download/$OTA_VERSION/$POTENTIAL_ASSET_NAME") 496 | 497 | .tmp/custota-tool gen-update-info "${args[@]}" 498 | done 499 | } 500 | 501 | function downloadCusotaTool() { 502 | downloadAndVerifyFromChenxiaolong 'Custota' "$CUSTOTA_VERSION" 'custota-tool' 503 | } 504 | 505 | function uploadOtaServerData() { 506 | 507 | # Update OTA server (github pages) 508 | local current_branch current_commit base_dir src_repo 509 | current_commit=$(git rev-parse --short HEAD) 510 | folderPrefix='' 511 | 512 | if [[ "${UPLOAD_TEST_OTA}" == 'true' ]]; then 513 | folderPrefix='test/' 514 | fi 515 | 516 | ( 517 | base_dir="$(pwd)" 518 | src_repo=$(extractGithubRepo "$(git config --get remote.origin.url)") 519 | if [[ -n "${PAGES_REPO_FOLDER}" ]]; then 520 | cd "${PAGES_REPO_FOLDER}" 521 | fi 522 | 523 | current_branch=$(git rev-parse --abbrev-ref HEAD) 524 | git checkout gh-pages 525 | 526 | for flavor in "${!POTENTIAL_ASSETS[@]}"; do 527 | local POTENTIAL_ASSET_NAME="${POTENTIAL_ASSETS[$flavor]}" 528 | local targetFile="${folderPrefix}${flavor}/${DEVICE_ID}.json" 529 | 530 | uploadFile "${base_dir}/.tmp/${POTENTIAL_ASSET_NAME}.csig" "$POTENTIAL_ASSET_NAME.csig" "application/octet-stream" 531 | 532 | mkdir -p "${folderPrefix}${flavor}" 533 | # update only, if current $DEVICE_ID.json does not contain $OTA_VERSION 534 | # We don't want to trigger users to upgrade on new commits from this repo or new magisk versions 535 | # They can manually upgrade by downloading the OTAs from the releases and "adb sideload" them 536 | if ! grep -q "$OTA_VERSION" "${targetFile}" || [[ "$FORCE_OTA_SERVER_UPLOAD" == 'true' ]] && [[ "$SKIP_OTA_SERVER_UPLOAD" != 'true' ]]; then 537 | cp "${base_dir}/.tmp/${flavor}/$DEVICE_ID.json" "${targetFile}" 538 | git add "${targetFile}" 539 | elif grep -q "${OTA_VERSION}" "${targetFile}"; then 540 | printGreen "Skipping update of OTA server, because ${OTA_VERSION} already in ${folderPrefix}${flavor}/${DEVICE_ID}.json and FORCE_OTA_SERVER_UPLOAD is false." 541 | else 542 | printGreen "Skipping update of OTA server, because SKIP_OTA_SERVER_UPLOAD is true." 543 | fi 544 | done 545 | 546 | if ! git diff-index --quiet HEAD; then 547 | # Commit and push only when there are changes 548 | git config user.name "GitHub Actions" && git config user.email "actions@github.com" 549 | git commit \ 550 | --message "Update device ${DEVICE_ID} basing on ${src_repo}@${current_commit}" \ 551 | 552 | gitPushWithRetries 553 | fi 554 | 555 | # Switch back to the original branch 556 | git checkout "$current_branch" 557 | ) 558 | } 559 | 560 | extractGithubRepo() { 561 | # Works for both HTTPS and SSH, e.g. 562 | # https://github.com/schnatterer/rooted-graphene 563 | # git@github.com:schnatterer/rooted-graphene.git 564 | 565 | local remote_url="$1" 566 | local repo 567 | 568 | # Remove the protocol and .git suffix 569 | remote_url=$(echo "$remote_url" | sed -e 's/.*:\/\/\|.*@//' -e 's/\.git$//') 570 | 571 | # Extract the owner/repo part 572 | repo=$(echo "$remote_url" | sed -e 's/.*[:\/]\([^\/]*\/[^\/]*\)$/\1/') 573 | 574 | echo "$repo" 575 | } 576 | 577 | function gitPushWithRetries() { 578 | local count=0 579 | 580 | while [ $count -lt $GIT_PUSH_RETRIES ]; do 581 | git pull --rebase 582 | if git push origin gh-pages; then 583 | break 584 | else 585 | count=$((count + 1)) 586 | printGreen "Retry $count/$GIT_PUSH_RETRIES failed. Retrying..." 587 | sleep 2 588 | fi 589 | done 590 | 591 | if [ $count -eq $GIT_PUSH_RETRIES ]; then 592 | printRed "Failed to push to gh-pages after $GIT_PUSH_RETRIES attempts." 593 | exit 1 594 | fi 595 | } 596 | 597 | function print() { 598 | echo -e "$(date '+%Y-%m-%d %H:%M:%S'): $*" 599 | } 600 | 601 | function printGreen() { 602 | if [[ -z "${NO_COLOR}" ]]; then 603 | echo -e "\e[32m$(date '+%Y-%m-%d %H:%M:%S'): $*\e[0m" 604 | else 605 | print "$@" 606 | fi 607 | } 608 | 609 | function printRed() { 610 | if [[ -z "${NO_COLOR}" ]]; then 611 | echo -e "\e[31m$(date '+%Y-%m-%d %H:%M:%S'): $*\e[0m" 612 | else 613 | print "$@" 614 | fi 615 | } 616 | --------------------------------------------------------------------------------