name: Build WSA New on: workflow_dispatch: inputs: arch: description: "Build architecture" required: true default: "x64" type: choice options: - x64 - arm64 - x64 & arm64 release_type: description: "WSA release type" required: true default: "insider fast" type: choice options: - retail - release preview - insider slow - insider fast magisk_apk: description: "Magisk version" required: true default: "stable" type: choice options: - stable - beta - canary - debug gapps_variant: description: "GApps variant" required: true default: "pico" type: choice options: - none - full - pico remove_amazon: description: "Remove Amazon Appstore" required: true default: "remove" type: choice options: - keep - remove root_sol: description: "Install Magisk" required: true default: "magisk" type: choice options: - magisk - none permissions: contents: write jobs: matrix: runs-on: ubuntu-latest outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - name: Generate build matrix id: set-matrix uses: actions/github-script@v6 with: script: | let matrix = {}; let arch = "${{ github.event.inputs.arch }}" switch ( arch ) { case "x64": matrix.arch = ["x64"]; break; case "arm64": matrix.arch = ["arm64"]; break; default: matrix.arch = ["x64", "arm64"]; break; } core.setOutput("matrix",JSON.stringify(matrix)); build: runs-on: ubuntu-20.04 needs: matrix strategy: matrix: ${{fromJson(needs.matrix.outputs.matrix)}} steps: - name: Dependencies run: | sudo apt-get update && sudo apt-get install setools lzip wine winetricks patchelf e2fsprogs aria2 wget -qO- "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/archive/$GITHUB_REF.tar.gz" | sudo tar --wildcards -zxvf- -C ~ --strip-component=2 '*/wine/*' '*/linker/*' '*/xml/*' winetricks msxml6 - name: Generate download links shell: python run: | import requests from xml.dom import minidom import html import re import zipfile import os import json import re from pathlib import Path arch = "${{ matrix.arch }}" release_type_map = {"retail": "Retail", "release preview": "RP", "insider slow": "WIS", "insider fast": "WIF"} release_type = release_type_map["${{ github.event.inputs.release_type }}"] if "${{ github.event.inputs.release_type }}" != "" else "Retail" cat_id = '858014f3-3934-4abe-8078-4aa193e74ca8' print(f"Generating WSA download link: arch={arch} release_type={release_type}", flush=True) with open(Path.home() / "GetCookie.xml", "r") as f: cookie_content = f.read() out = requests.post( 'https://fe3.delivery.mp.microsoft.com/ClientWebService/client.asmx', data=cookie_content, headers={'Content-Type': 'application/soap+xml; charset=utf-8'}, verify=False ) doc = minidom.parseString(out.text) cookie = doc.getElementsByTagName('EncryptedData')[0].firstChild.nodeValue with open(Path.home() / "WUIDRequest.xml", "r") as f: cat_id_content = f.read().format(cookie, cat_id, release_type) out = requests.post( 'https://fe3.delivery.mp.microsoft.com/ClientWebService/client.asmx', data=cat_id_content, headers={'Content-Type': 'application/soap+xml; charset=utf-8'}, verify=False ) doc = minidom.parseString(html.unescape(out.text)) filenames = {} for node in doc.getElementsByTagName('Files'): filenames[node.parentNode.parentNode.getElementsByTagName('ID')[0].firstChild.nodeValue] = f"{node.firstChild.attributes['InstallerSpecificIdentifier'].value}_{node.firstChild.attributes['FileName'].value}" pass identities = [] for node in doc.getElementsByTagName('SecuredFragment'): filename = filenames[node.parentNode.parentNode.parentNode.getElementsByTagName('ID')[0].firstChild.nodeValue] update_identity = node.parentNode.parentNode.firstChild identities += [(update_identity.attributes['UpdateID'].value, update_identity.attributes['RevisionNumber'].value, filename)] with open(Path.home() / "FE3FileUrl.xml", "r") as f: file_content = f.read() tmpdownlist = open("download.list", 'a') for i, v, f in identities: if re.match(f"Microsoft\.UI\.Xaml\..*_{arch}_.*\.appx", f): out_file = "xaml.appx" elif re.match(f"MicrosoftCorporationII\.WindowsSubsystemForAndroid_.*\.msixbundle", f): out_file = "wsa.zip" else: continue out = requests.post( 'https://fe3.delivery.mp.microsoft.com/ClientWebService/client.asmx/secured', data=file_content.format(i, v, release_type), headers={'Content-Type': 'application/soap+xml; charset=utf-8'}, verify=False ) doc = minidom.parseString(out.text) for l in doc.getElementsByTagName("FileLocation"): url = l.getElementsByTagName("Url")[0].firstChild.nodeValue if len(url) != 99: print(f"download link: {url} to {out_file}", flush=True) tmpdownlist.writelines(url + '\n') tmpdownlist.writelines(f' dir=.\n') tmpdownlist.writelines(f' out={out_file}\n') tmpdownlist.writelines(f'https://aka.ms/Microsoft.VCLibs.{arch}.14.00.Desktop.appx\n') tmpdownlist.writelines(f' dir=.\n') tmpdownlist.writelines(f' out=vclibs.appx\n') tmpdownlist.close magisk_ver = """${{ github.event.inputs.magisk_ver }}""" print(f"Generating Magisk download link: release type={magisk_ver}", flush=True) if not magisk_ver: magisk_ver = "stable" if magisk_ver == "stable" or magisk_ver == "beta" or magisk_ver == "canary" or magisk_ver == "debug": magisk_ver = json.loads(requests.get(f"https://github.com/topjohnwu/magisk-files/raw/master/{magisk_ver}.json").content)['magisk']['link'] print(f"download link: {magisk_ver}", flush=True) out_file = "magisk.zip" with open("download.list", 'a') as f: f.writelines(f'{magisk_ver}\n') f.writelines(f' dir=.\n') f.writelines(f' out=magisk.zip\n') variant = "${{ github.event.inputs.gapps_variant }}" if variant != "none" and variant != "": print(f"Generating OpenGapps download link: arch={arch} variant={variant}", flush=True) abi_map = {"x64": "x86_64", "arm64": "arm64"} if arch == "x64" and variant == "pico": link = "http://peternjeim.ddns.net:8081/ipfs/QmPDiAyqUvZHo9QU7WfoEE9XMqC8ppGyUsSwKQY7chfwHX" elif arch == "x64" and variant == "full": link = "http://peternjeim.ddns.net:8081/ipfs/QmULfSMwWuukQR7r9KEvwD2XzsChHTvpswmNqJyEU64jwM" else: variant = "pico" print("only pico gapps variant is supported on arm64", flush=True) try: res = requests.get(f"https://api.opengapps.org/list") j = json.loads(res.content) link = {i["name"]: i for i in j["archs"][abi_map[arch]]["apis"]["11.0"]["variants"]}[variant]["zip"] except Exception: print("Failed to fetch from opengapps api, fallbacking to sourceforge rss...") res = requests.get(f'https://sourceforge.net/projects/opengapps/rss?path=/{abi_map[arch]}&limit=100') link = re.search(f'https://.*{abi_map[arch]}/.11.0.*{variant}.*\.zip/download', res.text).group().replace('.zip/download', '.zip').replace('sourceforge.net/projects/opengapps/files', 'downloads.sourceforge.net/project/opengapps') print(f"download link: {link}", flush=True) with open("download.list", 'a') as f: f.writelines(f'{link}\n') f.writelines(f' dir=.\n') f.writelines(f' out=gapps.zip\n') f.close - name: Download artifacts run: | if ! aria2c --no-conf --log-level=info --log="./aria2_download.log" -x16 -s16 -j5 -c -R -m0 -i"download.list"; then echo "We have encountered an error while downloading files." exit 1 fi - name: Extract WSA shell: python run: | import warnings import zipfile import os warnings.filterwarnings("ignore") arch = "${{ matrix.arch }}" zip_name = "" with zipfile.ZipFile("wsa.zip") as zip: for f in zip.filelist: if arch in f.filename.lower(): zip_name = f.filename if not os.path.isfile(zip_name): print(f"unzipping to {zip_name}", flush=True) zip.extract(f) ver_no = zip_name.split("_") long_ver = ver_no[1] ver = long_ver.split(".") main_ver = ver[0] with open(os.environ['GITHUB_ENV'], 'a') as g: g.write(f'WSA_VER={long_ver}\n') with open(os.environ['GITHUB_ENV'], 'a') as g: g.write(f'WSA_MAIN_VER={main_ver}\n') rel = ver_no[3].split(".") rell = str(rel[0]) with open(os.environ['GITHUB_ENV'], 'a') as g: g.write(f'WSA_REL={rell}\n') if 'language' in f.filename.lower() or 'scale' in f.filename.lower(): name = f.filename.split("-", 1)[1].split(".")[0] zip.extract(f) with zipfile.ZipFile(f.filename) as l: for g in l.filelist: if g.filename == 'resources.pri': g.filename = f'{name}.pri' l.extract(g, 'pri') print(f"extract resource pack {g.filename}") elif g.filename == 'AppxManifest.xml': g.filename = f'{name}.xml' l.extract(g, 'xml') with zipfile.ZipFile(zip_name) as zip: if not os.path.isdir(arch): print(f"unzipping from {zip_name}", flush=True) zip.extractall(arch) print("done", flush=True) - name: Extract Magisk shell: python run: | import zipfile import os magisk_zip = "magisk.zip" arch = "${{ matrix.arch }}" abi_map = {"x64": ["x86_64", "x86"], "arm64": ["arm64-v8a", "armeabi-v7a"]} def extract_as(zip, name, as_name, dir): info = zip.getinfo(name) info.filename = as_name zip.extract(info, dir) with zipfile.ZipFile(magisk_zip) as zip: extract_as(zip, f"lib/{ abi_map[arch][0] }/libmagisk64.so", "magisk64", "magisk") extract_as(zip, f"lib/{ abi_map[arch][1] }/libmagisk32.so", "magisk32", "magisk") standalone_policy = False try: zip.getinfo(f"lib/{ abi_map[arch][0] }/libmagiskpolicy.so") standalone_policy = True except: pass extract_as(zip, f"lib/{ abi_map[arch][0] }/libmagiskinit.so", "magiskinit", "magisk") if standalone_policy: extract_as(zip, f"lib/{ abi_map[arch][0] }/libmagiskpolicy.so", "magiskpolicy", "magisk") else: extract_as(zip, f"lib/{ abi_map[arch][0] }/libmagiskinit.so", "magiskpolicy", "magisk") extract_as(zip, f"lib/{ abi_map[arch][0] }/libmagiskboot.so", "magiskboot", "magisk") extract_as(zip, f"lib/{ abi_map[arch][0] }/libbusybox.so", "busybox", "magisk") if standalone_policy: extract_as(zip, f"lib/{ abi_map['x64'][0] }/libmagiskpolicy.so", "magiskpolicy", ".") else: extract_as(zip, f"lib/{ abi_map['x64'][0] }/libmagiskinit.so", "magiskpolicy", ".") extract_as(zip, f"assets/boot_patch.sh", "boot_patch.sh", "magisk") extract_as(zip, f"assets/util_functions.sh", "util_functions.sh", "magisk") - name: Extract GApps if: ${{ github.event.inputs.gapps_variant != 'none' && github.event.inputs.gapps_variant != '' }} run: | mkdir gapps unzip -p gapps.zip {Core,GApps}/'*.lz' | tar --lzip -C gapps -xf - -i --strip-components=2 --exclude='setupwizardtablet-x86_64' --exclude='packageinstallergoogle-all' --exclude='speech-common' --exclude='markup-lib-arm' --exclude='markup-lib-arm64' --exclude='markup-all' --exclude='setupwizarddefault-x86_64' --exclude='pixellauncher-all' --exclude='pixellauncher-common' - name: Expand images run: | e2fsck -yf ${{ matrix.arch }}/system_ext.img SYSTEM_EXT_SIZE=$(($(du --apparent-size -sB512 ${{ matrix.arch }}/system_ext.img | cut -f1) + 20000)) if [ -d gapps/system_ext ]; then SYSTEM_EXT_SIZE=$(( SYSTEM_EXT_SIZE + $(du --apparent-size -sB512 gapps/system_ext | cut -f1) )) fi resize2fs ${{ matrix.arch }}/system_ext.img "$SYSTEM_EXT_SIZE"s e2fsck -yf ${{ matrix.arch }}/product.img PRODUCT_SIZE=$(($(du --apparent-size -sB512 ${{ matrix.arch }}/product.img | cut -f1) + 20000)) if [ -d gapps/product ]; then PRODUCT_SIZE=$(( $PRODUCT_SIZE + $(du --apparent-size -sB512 gapps/product | cut -f1) )) fi resize2fs ${{ matrix.arch }}/product.img "$PRODUCT_SIZE"s e2fsck -yf ${{ matrix.arch }}/system.img SYSTEM_SIZE=$(($(du --apparent-size -sB512 ${{ matrix.arch }}/system.img | cut -f1) + 20000)) if [ -d gapps ]; then SYSTEM_SIZE=$(( $SYSTEM_SIZE + $(du --apparent-size -sB512 gapps | cut -f1) - $(du --apparent-size -sB512 gapps/product | cut -f1) )) if [ -d gapps/system_ext ]; then SYSTEM_SIZE=$(( $SYSTEM_SIZE - $(du --apparent-size -sB512 gapps/system_ext | cut -f1) )) fi fi if [ -d magisk ]; then SYSTEM_SIZE=$(( $SYSTEM_SIZE +$(du --apparent-size -sB512 magisk | cut -f1) )) fi if [ -f magisk.zip ]; then SYSTEM_SIZE=$(( $SYSTEM_SIZE +$(du --apparent-size -sB512 magisk.zip | cut -f1) )) fi if [ -d system ]; then SYSTEM_SIZE=$(( $SYSTEM_SIZE +$(du --apparent-size -sB512 system | cut -f1) )) fi resize2fs ${{ matrix.arch }}/system.img "$SYSTEM_SIZE"s e2fsck -yf ${{ matrix.arch }}/vendor.img VENDOR_SIZE=$(($(du --apparent-size -sB512 ${{ matrix.arch }}/vendor.img | cut -f1) + 20000)) resize2fs ${{ matrix.arch }}/vendor.img "$VENDOR_SIZE"s - name: Mount images run: | sudo mkdir system sudo mount -o loop ${{ matrix.arch }}/system.img system sudo mount -o loop ${{ matrix.arch }}/vendor.img system/vendor sudo mount -o loop ${{ matrix.arch }}/product.img system/product sudo mount -o loop ${{ matrix.arch }}/system_ext.img system/system_ext - name: Remove Amazon AppStore if: ${{ github.event.inputs.remove_amazon == 'remove' }} run: | find system/product/{etc/permissions,etc/sysconfig,framework,priv-app} | grep -e amazon -e venezia | sudo xargs rm -rf - name: Integrate Magisk if: ${{ github.event.inputs.root_sol == 'magisk' || github.event.inputs.root_sol == '' }} run: | sudo mkdir system/sbin sudo chcon --reference system/init.environ.rc system/sbin sudo chown root:root system/sbin sudo chmod 0700 system/sbin sudo cp magisk/* system/sbin/ sudo cp magisk.zip system/sbin/magisk.apk sudo tee -a system/sbin/loadpolicy.sh < EOF wine64 ~/makepri.exe new /pr pri /in MicrosoftCorporationII.WindowsSubsystemForAndroid /cf priconfig.xml /of ${{ matrix.arch }}/resources.pri /o sed -i -zE "s//\n$(cat xml/* | grep -Po ']*/>' | sed ':a;N;$!ba;s/\n/\\n/g' | sed 's/\$/\\$/g' | sed 's/\//\\\//g')\n<\/Resources>/g" ${{ matrix.arch }}/AppxManifest.xml - name: Add extra packages run: | wget -qO- "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/archive/$GITHUB_REF.tar.gz" | sudo tar --wildcards -zxvf- --strip-component=2 '*/${{ matrix.arch }}/system/*' find system/system/priv-app/ -maxdepth 1 -mindepth 1 -printf '%P\n' | xargs -I dir sudo find system/system/priv-app/dir -type d -exec chmod 0755 {} \; find system/system/priv-app/ -maxdepth 1 -mindepth 1 -printf '%P\n' | xargs -I dir sudo find system/system/priv-app/dir -type f -exec chmod 0644 {} \; find system/system/priv-app/ -maxdepth 1 -mindepth 1 -printf '%P\n' | xargs -I dir sudo find system/system/priv-app/dir -exec chown root:root {} \; find system/system/priv-app/ -maxdepth 1 -mindepth 1 -printf '%P\n' | xargs -I dir sudo find system/system/priv-app/dir -exec chcon --reference=system/system/priv-app {} \; find system/system/etc/permissions/ -maxdepth 1 -mindepth 1 -printf '%P\n' | xargs -I file sudo find system/system/etc/permissions/file -type f -exec chmod 0644 {} \; find system/system/etc/permissions/ -maxdepth 1 -mindepth 1 -printf '%P\n' | xargs -I file sudo find system/system/etc/permissions/file -exec chown root:root {} \; find system/system/etc/permissions/ -maxdepth 1 -mindepth 1 -printf '%P\n' | xargs -I file sudo find system/system/etc/permissions/file -type f -exec chcon --reference=system/system/etc/permissions/platform.xml {} \; - name: Integrate GApps if: ${{ github.event.inputs.gapps_variant != 'none' && github.event.inputs.gapps_variant != '' }} run: | find "gapps/" -mindepth 1 -type d -exec sudo chmod 0755 {} \; find "gapps/" -mindepth 1 -type d -exec sudo chown root:root {} \; file_list="$(find "gapps/" -mindepth 1 -type f | cut -d/ -f5-)" for file in $file_list; do sudo chown root:root "gapps/${file}" sudo chmod 0644 "gapps/${file}" done find gapps/ -maxdepth 1 -mindepth 1 -type d -not -path '*product' -exec sudo cp --preserve=all -r {} system \; || abort sudo cp --preserve=all -r gapps/product/* product || abort find gapps/product/overlay -maxdepth 1 -mindepth 1 -printf '%P\n' | xargs -I file sudo find product/overlay/file -type f -exec chcon --reference=product/overlay/FontNotoSerifSource/FontNotoSerifSourceOverlay.apk {} \; find gapps/app/ -maxdepth 1 -mindepth 1 -printf '%P\n' | xargs -I dir sudo find system/app/dir -type d -exec chcon --reference=system/app {} \; find gapps/framework/ -maxdepth 1 -mindepth 1 -printf '%P\n' | xargs -I dir sudo find system/framework/dir -type d -exec chcon --reference=system/framework {} \; find gapps/priv-app/ -maxdepth 1 -mindepth 1 -printf '%P\n' | xargs -I dir sudo find system/priv-app/dir -type d -exec chcon --reference=system/priv-app {} \; find gapps/app/ -maxdepth 1 -mindepth 1 -printf '%P\n' | xargs -I file sudo find system/app/file -type f -exec chcon --reference=system/app/KeyChain/KeyChain.apk {} \; find gapps/framework/ -maxdepth 1 -mindepth 1 -printf '%P\n' | xargs -I file sudo find system/framework/file -type f -exec chcon --reference=system/framework/ext.jar {} \; find gapps/priv-app/ -maxdepth 1 -mindepth 1 -printf '%P\n' | xargs -I file sudo find system/priv-app/file -type f -exec chcon --reference=system/priv-app/Shell/Shell.apk {} \; find gapps/etc/ -maxdepth 1 -mindepth 1 -printf '%P\n' | xargs -I dir sudo find system/etc/dir -type d -exec chcon --reference=system/etc/permissions {} \; find gapps/etc/ -maxdepth 1 -mindepth 1 -printf '%P\n' | xargs -I dir sudo find system/etc/dir -type f -exec chcon --reference=system/etc/permissions {} \; sudo patchelf --replace-needed libc.so "${HOME}/libc.so" ./magiskpolicy || true sudo patchelf --replace-needed libm.so "${HOME}/libm.so" ./magiskpolicy || true sudo patchelf --replace-needed libdl.so "${HOME}/libdl.so" ./magiskpolicy || true sudo patchelf --set-interpreter "${HOME}/linker64" ./magiskpolicy || true chmod +x ./magiskpolicy sudo ./magiskpolicy --load system/vendor/etc/selinux/precompiled_sepolicy --save system/vendor/etc/selinux/precompiled_sepolicy "allow gmscore_app gmscore_app vsock_socket { create connect write read }" "allow gmscore_app device_config_runtime_native_boot_prop file read" "allow gmscore_app system_server_tmpfs dir search" "allow gmscore_app system_server_tmpfs file open" - name: Fix GApps prop if: ${{ github.event.inputs.gapps_variant != 'none' && github.event.inputs.gapps_variant != '' }} shell: sudo python {0} run: | from __future__ import annotations from io import TextIOWrapper from os import system, path from typing import OrderedDict class Prop(OrderedDict): def __init__(self, file: TextIOWrapper) -> None: super().__init__() for i, line in enumerate(file.read().splitlines(False)): if '=' in line: k, v = line.split('=', 2) self[k] = v else: self[f".{i}"] = line def __str__(self) -> str: return '\n'.join([v if k.startswith('.') else f"{k}={v}" for k, v in self.items()]) def __iadd__(self, other: str) -> Prop: self[f".{len(self)}"] = other return self new_props = { ("product", "brand"): "google", ("product", "manufacturer"): "Google", ("build", "product"): "redfin", ("product", "name"): "redfin", ("product", "device"): "redfin", ("product", "model"): "Pixel 5", ("build", "flavor"): "redfin-user" } def description(sec: str, p: Prop) -> str: return f"{p[f'ro.{sec}.build.flavor']} {p[f'ro.{sec}.build.version.release_or_codename']} {p[f'ro.{sec}.build.id']} {p[f'ro.{sec}.build.version.incremental']} {p[f'ro.{sec}.build.tags']}" def fingerprint(sec: str, p: Prop) -> str: return f"""{p[f"ro.product.{sec}.brand"]}/{p[f"ro.product.{sec}.name"]}/{p[f"ro.product.{sec}.device"]}:{p[f"ro.{sec}.build.version.release"]}/{p[f"ro.{sec}.build.id"]}/{p[f"ro.{sec}.build.version.incremental"]}:{p[f"ro.{sec}.build.type"]}/{p[f"ro.{sec}.build.tags"]}""" def fix_prop(sec, prop): if not path.exists(prop): return print(f"fixing {prop}", flush=True) with open(prop, 'r') as f: p = Prop(f) p += "# extra prop added by MagiskOnWSA" for k, v in new_props.items(): p[f"ro.{k[0]}.{k[1]}"] = v if k[0] == "build": p[f"ro.{sec}.{k[0]}.{k[1]}"] = v elif k[0] == "product": p[f"ro.{k[0]}.{sec}.{k[1]}"] = v p["ro.build.description"] = description(sec, p) p[f"ro.build.fingerprint"] = fingerprint(sec, p) p[f"ro.{sec}.build.description"] = description(sec, p) p[f"ro.{sec}.build.fingerprint"] = fingerprint(sec, p) p[f"ro.bootimage.build.fingerprint"] = fingerprint(sec, p) with open(prop, 'w') as f: f.write(str(p)) for sec, prop in {"system": "system/system/build.prop", "product": "system/product/build.prop", "system_ext": "system/system_ext/build.prop", "vendor": "system/vendor/build.prop", "odm": "system/vendor/odm/etc/build.prop"}.items(): fix_prop(sec, prop) - name: Umount images run: | sudo find system -exec touch -amt 200901010000.00 {} \;> /dev/null 2>&1 sudo umount system/vendor sudo umount system/product sudo umount system/system_ext sudo umount system - name: Shrink images run: | e2fsck -yf ${{ matrix.arch }}/system.img resize2fs -M ${{ matrix.arch }}/system.img e2fsck -yf ${{ matrix.arch }}/vendor.img resize2fs -M ${{ matrix.arch }}/vendor.img e2fsck -yf ${{ matrix.arch }}/product.img resize2fs -M ${{ matrix.arch }}/product.img e2fsck -yf ${{ matrix.arch }}/system_ext.img resize2fs -M ${{ matrix.arch }}/system_ext.img - name: Remove signature and add scripts run: | rm -rf ${{ matrix.arch }}/\[Content_Types\].xml ${{ matrix.arch }}/AppxBlockMap.xml ${{ matrix.arch }}/AppxSignature.p7x ${{ matrix.arch }}/AppxMetadata cp vclibs.appx xaml.appx ${{ matrix.arch }} tee ${{ matrix.arch }}/Install.ps1 <