Single-file Python tool: autoporter.py builds super.img from Xiaomi OTA(s) + 2 GDrive mod archives. No packages, tests, lint, or typecheck.
pip install -r requirements.txt(needsrequests,tqdm)- CI reference (
.github/workflows/port.yml, manualworkflow_dispatchonly,device+build_target(base|fenrir|both— matrix overbothruns base+fenrir in parallel,fail-fast: false) + the usual inputs): Python 3.10 onubuntu-latest+sudo apt-get install -y android-sdk-libsparse-utils erofs-utils p7zip-full wget curl default-jre-headless(JRE needed for the baksmali/smali step). Checkout useslfs: true— without it template payloads (*/images/*,Modules_Root/*) check out as 130-byte LFS pointers;validate_template_payloads()additionally fails the build fast on any pointer/truncated file instead of bricking the device. - Full run:
sudo python -u autoporter.py(root is load-bearing: only as root does extraction restore owners/xattrs onto the trees, which the contexts pipeline needs; CI-onlysudo pip install --break-system-packages) — downloads ~tens of GB, do NOT run casually to "verify"; there is no quick/single-test mode.-u(unbuffered) is load-bearing: without it, Python block-buffers stdout to the CI pipe and child-tool stderr (lpmake, unbuffered) appears earlier in logs than the step that ran before it. CLI:--mode port|mod(defaultport; workflowchoiceinput —portmixes stock+port OTAs,modmodifies the full stock firmware only, no donors, no port OTA download),--hyper-version hos3|hos4(defaulthos4),--package-type universal|fastboot-only(defaultuniversal; workflow exposes both aschoiceinputs),--debloat auto|hos3|hos4|none(defaultauto= match--hyper-version; workflowchoiceinput),--dsv yes|no(defaultyes; workflowchoiceinput — gates ONLY the signature-verification lists viasignature_patch_lists(); functional smali fixes always apply),--decrypt-data yes|no(defaultno; workflowchoicedefaultyes),--density(default480; workflownumberinput — written aspersist.miui.density_v2+ro.sf.lcd_densityintoproduct/etc/build.prop),--aod-fullscreen true|false(defaulttrue; workflowchoiceinput —support_aod_fullscreeninduchamp.xml+NoSuchElementExceptioncatch inmiui-services.jar),--dialer yes|no(defaultyes; workflowchoiceinput — MIUI dialer on global firmwares only),--device duchamp(defaultduchamp; workflowchoiceinput — target device, selects firmware templates; device-specific values live inDEVICES),--variant base|fenrir(defaultbase; set from workflowbuild_targetmatrix — fenrir uses the{device}_template_fenrirtemplate (engineering preloader + patched LK) plusFENRIR_PROPS),--ext4-rw(comma list/all/none, defaultproduct,system; workflowstringinput) +--ext4-free-mb(default150; workflownumberinput). Firmware region is auto-detected from the OTA URL/filename (detect_region_code(): 2 letters before trailingXMin the OS version — port URL in port mode, stock URL in mod mode;CN= China, else global incl.EUbranch).
setup_tools(): mkdirstools/ moddedapps_hos3/ moddedapps_hos4/ moddedapps_hos4_gl/ extracted_stock/ extracted_port/ unpacked_stock/ unpacked_port/ port_meta/,chmod +xontools/*, prependstools/toPATH.- Downloads the selected mod set via direct link (
select_modded_apps():--hyper-version hos3|hos4|hos4_gl+ auto region — global hos4 takeshos4_gl, hos3+global warns and falls back to hos3; release assets on GitHub, no auth/quotas — GDrive was dropped after Drive flagged the archives), unpacks withshutil.unpack_archivefalling back to7z x(requiresp7zip-full), flattens single top-level dir, wipes stale dest names before move (re-runs safe), prints top-level layout, normalizes modes (normalize_tree_perms(): dirs 0755, files 0644, symlinks untouched, best-effort chown root:root when root — zips carry arbitrary modes), deletes zip. process_firmware(): downloads OTA zip → extracts onlypayload.bin(+META-INF/com/android/metadata{,.pb}→port_meta/, before zip deletion) → deletes zip immediately → dumps partitions viapayload-dumper-go -q(output captured, only tail shown on failure) → deletes payload.bin immediately. Disk-saving deletions are load-bearing; keep them. Port mode: stock →extracted_stock/, port →extracted_port/(separate dirs: stock extras share names with port partitions and would overwrite each other). Mod mode: full stock set (MOD_PARTITIONS) →extracted_stock/only, its ownMETA-INF→port_meta/, no port OTA download.unpack_partitions(): port mode unpacks stock.img(incl. extras) →unpacked_stock/<name>/, port.img→unpacked_port/<name>/; mod mode unpacksMOD_PARTITIONS→unpacked_stock/<name>/only (fresh extract, stale content wiped). Format by magic: ext4 viadebugfs rdump(e2fsprogs, preinstalled), erofs via vendoredtools/fsck.erofs --extract --xattrs(dev build + killpriv-order fix: restores owners/modes/xattrs as root; falls back to system fsck without--xattrson tooling failure, data errors always raise), sparse viasimg2imgif present. uid/gid/xattrs are NOT preserved bydebugfs rdump(native ext4) — only erofs trees carry them. 4b.flatten_pangu_system(): moves<product>/pangu/system/*up into<product>/(stock AND port OTAs nest product content there — pangu exists in duchamp stock too), merging dirs recursively, dropping the empty nesting; no-op when absent. Port mode flattens bothunpacked_port/product/andunpacked_stock/product/(donors are read from the stock tree, so without it they would miss intopangu/; stock product is donors-only, never rebuilt), mod mode flattensunpacked_stock/product/. Runs before donors+debloat so all paths match the final flat layout. 4c.apply_donor_files(): port mode only — copies stock donor blobs into the port tree —DONOR_FILES(currentlyproduct/etc/device_features/duchamp.xmlas-is; extend per device),DONOR_DIRSmerged recursively (currentlyproduct/etc/displayconfig),DONOR_DIR_FILES(currently 4 APEXes fromsystem_ext/apex+ 4 overlays fromproduct/overlay:AospFrameworkResOverlay,DevicesAndroidOverlay,DevicesOverlay,MiuiFrameworkResOverlay). Staleoat/next to any copied-in.apkis dropped viadrop_oat_next_to()(donor blobs come from a different build, so old precompiled code is invalid). Mod mode skips donors entirely (full stock, nothing to carry over). Missing sources only warn; runs before debloat+rebuild. 4c2.apply_modded_apps(): overlays the extracted mod set onto the target tree — port mode intounpacked_port/(set partitions missing there, e.g.vendor/with v4a, fall back intounpacked_stock/viafallback_root), mod mode intounpacked_stock/— each top-level partition dir (product/,system/,system_ext/,vendor/, ...) merged recursively viacopy_tree_into()(symlinks preserved, modded files replace existing ones;system/keeps its nestedsystem/level on both sides). Staleoat/next to every overlaid.apkis dropped viadrop_oat_next_to()(modded bytes invalidate old precompiled code). Runs after donors+flatten, before debloat, so oat strips and deletions apply to the final content. Missing partition dirs on both sides only warn. 4c3.apply_mi_ext_tweaks()on the build tree (patch_root, both modes —mi_extis the port's in port mode, the stock's in mod mode): patchesmi_ext/etc/build.prop(ro.product.mod_device= port codename from the OTA filename replaced withduchamp, replace-only, never appended;MI_EXT_DROP_PROP_KEYSremoved by key — ai version keys kept since they exist,ab_ota_partitionsdropped;ro.vendor.radio.5g=3only when present,ro.mi.os.version.incrementalvalue prefixed withTengeOS |when present,ro.miui.support.system.app.uninstall.v2moved intoproduct/etc/build.propvia upsert), moves every othermi_ext/etc/build.propkey intoproduct/etc/build.propvia upsert (exceptMI_EXT_COPY_EXCLUDE, deleted from mi_ext after copying), moves uninstall blobs from the nestedmi_ext/product/intoproduct/(MI_EXT_PRODUCT_MOVES:platform-miui-uninstall.xmlalways,rustruntime_cfg_v3_v5.xmlonly when--hyper-version hos4,miui-uninstall-empty.jar— CN mi_ext only, global warns-missing; moves preserve xattrs/symlinks viamove_file_preserve(), emptied parents dropped best-effort), global firmwares additionally movemi_ext/product/content intoproduct/(MI_EXT_GLOBAL_DIR_MOVES: app/data-app/priv-app entries, etc files + dir merges,framework/opcust/overlaymerges via_move_fs_entry()with stale-oat/drops) and dropproduct/overlay/MiuiTelephonyResCustOverlay.apkafterwards, deletesmi_ext/system+mi_ext/system_ext+mi_ext/product/app, and deletesproduct/etc/permissions/cn.google.services.xmlon CN only (kept on global; not debloat-gated, like fstab). Missing sources only warn; runs before debloat. 4c3b.apply_about_phone_description()on the build tree (patch_root, both modes): copies the committedabout_phone_description/duchamp/<hos3|hos4>/overlay for the selected--hyper-versionin —product/etc/device_info.json→product/etc/andsystem/system/priv-app/HTMLViewer/HTMLViewer.apk→system/system/. Copy (not move — the overlay is repo content) with xattrs/symlinks viacopy_file_preserve()(existing dests replaced); staleoat/next to a copied-in.apkis dropped viadrop_oat_next_to(); missing sources only warn; runs before debloat. 4c3c.apply_init_rc_tweak()on the build tree (patch_root, both modes): appends theanimationfixblock once to the end ofsystem/system/etc/init/hw/init.rc(SAR-nested;on property:sys.boot_completed=1→start animationfix,service animationfixforcingdeviceLevelListviasettings put). Idempotent (guard lineservice animationfixskips re-runs); missing file only warns; runs before debloat+rebuild. 4c4.apply_build_prop_tweaks()on the build tree (patch_root, both modes): writes--densityintoproduct/etc/build.prop(persist.miui.density_v2+ro.sf.lcd_density), appendsPRODUCT_PROP_APPEND(blur flags, handy mode, uninstall flag,ro.miui.product.home) to product,COMPUTILITY_PROPSon hos4/hos4_gl either mode (5 computility levels, replaced in place — the keys already exist in stock),FENRIR_PROPSon the fenrir variant intosystem/system/build.prop(verified-boot props), andSYSTEM_PROP_APPEND(gmscore, opa, dex2oat speed block with comment) tosystem/system/build.prop(SAR-nested in both trees), and normalizesro.product.locale=en-US+ro.build.host=wectazzin both. Upsert semantics (replace in place, else append at end — exact block order on fresh trees, idempotent); missing files only warn. 4c4b.apply_fingerprint()(DEVICES[device]["fingerprint"]over everyro.*.build.fingerprintkey in everybuild.propunder both unpacked trees, in place, idempotent; missing files/keys only warn; on the fenrir variant the exactro.build.fingerprintkey is additionally upserted intosystem/system/build.prop). No fingerprint asserts exist in either template's updater-script (only aro.product.device == duchampdevice check), so no metadata sync needed. 4c5.apply_device_features_overlay()+patch_device_features()on the build tree (patch_root, both modes — in port mode the donor file arrives via donors, so this runs after them): the committedduchamp/duchamp.xmlmod reference is copied overproduct/etc/device_features/duchamp.xml(missing overlay only warns, donor kept), then line-based tweaks —support_aod_fullscreenset/inserted per--aod-fullscreen(+ always-onsupport_aod_aon,doze_display_state_supported,doze_proximity_check_before_pulse_intent,config_displayBlanksAfterDoze=false,support_aod_notification_animate),is_only_support_keycode_goto=false, secondaod_support_keycode_goto_dismissfalse→true (first occurrence is already true, matched by value so it stays untouched),support_screen_enhance_engine+support_AI_displayinserted afterdefault_eyecare_mode,90Hzitem added tofpsList,defaultFps=120. Line-based, everything else byte-identical; idempotent; missing file only warns. 4c5b.apply_dialer()on the build tree (patch_root, both modes, gated by--dialer yes): CN skips (MIUI dialer ships by default). EU copies the committeddialer_gl/set (InCallUI,MIUIContactsTGlobal,MiuiMmsGlobal→product/priv-app/, chmod-normalized 0755/0644); other global regions move the same apps frommi_ext/product/priv-app/. Staleoat/next to installed APKs is dropped; missing sources only warn. ThenGmsConfigOverlayComms.apkstrings are repointed viapatch_apk_res()(com.google.android.dialer/contacts→com.android.contacts,com.google.android.apps.messaging→com.android.mmsinstrings.xml). Runs before debloat+rebuild. 4c6.apply_vibrator_fix()on the unpacked stock tree (port mode only —odmexists only in stock; not dsv-gated, it's a functional port fix): hos3→hos4 vibrator fix —/vibratorfeature→/defaultin the vintf manifestodm/etc/vintf/manifest/vendor.xiaomi.hardware.vibratorfeature.service.xml, and the same replacement NUL-padded to equal length in the hw service binaryodm/bin/hw/vendor.xiaomi.hardware.vibratorfeature.service(size kept; exactly-1 occurrence patched, anything else warn-skipped). The services.jar half rides the smali step below. 4d.apply_debloat()+apply_stock_debloat(): port mode deletes the debloat entries for the selected version from the unpacked port tree (hos4 + global region takesDEBLOAT_HOS4_GLOBALinstead of the CN list;hos4_glnormalizes tohos4for selection); mod mode applies the same lists onto the unpacked stock tree instead (mi_extentries just warn-missing when absent) —STOCK_DEBLOATalways goes onto the unpacked stock tree (currentlyvendor/etc/voicecommand— vendor exists only in stock, never in port). Entries resolve case-insensitively per path component via_resolve_insensitive()(exact match wins; OEMs shuffle capitalization likeMIPay/MIpay; resolved renames are logged as[case]). Paths are relative tounpacked_port//unpacked_stock/(NOTextracted_*— those hold.imgfiles). Missing entries only warn (OTAs differ); runs before the rebuild bakes the trees in. Same step also runsapply_vendor_build_prop()(vendor/build.propin the stock tree, both modes: everyro.hwui.use_vulkan=*→true,debug.renderengine.backend=*→skiavkthreaded,ro.control_privapp_permissions=enforceline dropped; missing file only warns) andpatch_vendor_fstab()(DNA rw/decrypt port): strips AVB options fromvendor/etc/fstab.*lines, dropsoverlaylines (rw), drops/mi_extmount lines (mi_ext is not packed into super), and with--decrypt-data yesrenamesfileencryption→fileencryptable(AVB strips run first so the renamed option can't dodge them). 4e.patch_jar_smali(): decompilessystem/system/framework/framework.jarper-dex withtools/baksmali.jar(--apifrom each dex header viaDEX_VERSION_API), injectsdsv/framework/<dex>/*.smaliat the path from its own.class(currentlyXdConfig→classes6/android/os/; skips classes already present), appliesFRAMEWORK_METHOD_PATCHES(body →XdConfigcall) +FRAMEWORK_INSERT_PATCHES(insert after invoke), reassembles withtools/smali.jarand swaps the dexes back into the jar (other entries byte-identical, atomic replace). Missing files/methods only warn. Needsjava(CI:default-jre-headless). Source tree isunpacked_port/in port mode,unpacked_stock/in mod mode (patch_root). Every rule here is signature-verification, so the whole step runs only with--dsv yes. LIMIT: jars are v3.0.10 (latest) — dex 041 is attempted, 042+ fails fast (no valid--api; 36+ writes zeroed headers). Every dex is verified after rebuild (magic + class count vs intake, since both tools exit 0 even on failure). Stale precompiled code is dropped per target: wholeoat/next to every patched APK, but only<name>.odex/.vdex/.artinoat/arm64/next to patched jars (oat/holds other jars' valid outputs too). Device recompiles on first boot via odrefresh/dalvik-cache. Host-side dex2oat is deliberately NOT done: real dex2oat is an ARM ART binary (CI is x86_64) needing qemu + ART sysroot + the firmware's own bootclasspath — version-mismatched oat = bootloop, unverifiable without a device. 4f. Samepatch_jar_smali()onsystem_ext/framework/miui-services.jar(underpatch_root) withMIUI_SERVICES_METHOD_PATCHES(void bodies, noXdConfigcall — signature, dsv-gated) +MIUI_SERVICES_START_PATCHES(insert secure-flag bypass block at the start ofnotAllowCaptureDisplay, matched by name + descriptor + body markers, unique:bypass_secure_flaglabel since baksmali renumbers:cond_Nitself — functional, always applied) +MIUI_SERVICES_REPLACE_PATCHES(literal field swapIS_INTERNATIONAL_BUILD→IS_MIUIfor notifications on Chinese ROMs — functional, always applied; no dsv dir yet — injection skips gracefully) + Baidu→Gboard keyboard strings inInputMethodManagerServiceImpl(functional, always applied, both modes) +MIUI_SERVICES_DRM_REPLACE_PATCHES(4-lineDrmBroadcastsequence inActivityManagerServiceImpldeleted outright — functional, always applied) +MIUI_SERVICES_AOD_CATCH_PATCHES(.catch Ljava/util/NoSuchElementException;inserted after theSecurityExceptioncatch whose previous.catchisRemoteExceptionwith the same try range + handler — fullscreen-AOD fix, gated by--aod-fullscreenlike the xml flag, NOT dsv-gated; try/catch labels captured from the file so hos3/hos4 numbering doesn't matter,*.smaliscan since the owning class isn't pinned, idempotent; missing anchor only warns). All dsv rules apply to hos3 and hos4 alike (no version branching). 4f2. Samepatch_jar_smali()onsystem_ext/framework/miui-framework.jar(underpatch_root, both modes, always) with keyboard strings inInputMethodServiceInjectoronly (no dsv dir — injection skips gracefully; missing jar only warns). 4g. Samepatch_jar_smali()onsystem/system/framework/services.jar(underpatch_root) withSERVICES_METHOD_PATCHES:checkUpgradeKeySetLocked+ signature-match methods (keep original.registers, body →XdConfigcall),checkDowngradeoverloads (void stubs),<clinit>(full custom block forcingALLOW_NON_PRELOADS_SYSTEM_SHAREDUIDS=true). Rules support fixed or kept registers plus custom body lines.SERVICES_START_PATCHES(secure-flag bypass at method start: empty-list variant forWindowManagerService.notifyScreenshotListeners, false variant forWindowState.isSecureLocked) +SERVICES_NEW_METHODS(brand-newWindowState.isBypassSecureFlag()readingpersist.sys.secure_flag, inserted beforeisLegacyPolicyVisibility, skipped if already present). PlusSERVICES_VIBRATOR_REPLACE_PATCHESin port mode only (hos3→hos4 vibrator fix: 4-lineMiuiStubUtil.getImplsequence inVibratorManagerServiceStub$Holder→ directnew-instance+<init>, via the literal whole-file replace). All three jar steps run only with--dsv yes(default; workflowchoiceinputdsv: "yes"|"no");"no"skips the DSV lists only (functional fixes still apply to all jars). 4g2.patch_apk_smali()(extended keyboard, always, both modes): fulltools/apkeditor.jardecode → literal whole-file replaces on the decodedsmali/tree → rebuild, with checks (decode must yield smali, rebuilt entry-name set must equal the original, atomic replace). Work dir is a system-temp dir (NOT the repo — slow mounts stall thousand-file decodes and litter on kills), wiped infinally. Internal dex lib handles dex up to 042 (unlike the jar tools). Rules: Baidu→Gboard strings inproduct/app/MIUIFrequentPhrase/MIUIFrequentPhrase.apk(InputMethodBottomManager),method_body_patchesinsystem_ext/priv-app/Provision/Provision.apk(isPocoDevice()Z → false) andsystem_ext/app/PowerKeeper/PowerKeeper.apk(getDisplayCtrlCode()I→ 0,setScreenEffect(+Internal) → void; jar step 3a semantics, missing targets only warn) andsystem_ext/priv-app/Settings/Settings.apk(AvailableVirtualKeyboardFragment), plus aconst/4 v0, 0x1after thegetIntmove-resultinInputMethodFunctionSelectUtils(force IME-bottom support; blank-line-exact block, proven against real Settings smali). PlusSETTINGS_ARRAY_PATCHES: 6.array-datafloat blocks (3 per group) matched by their 16 values as ints — never by:array_NNNlabels, which renumber between builds — and rewritten with the repeating 4-value pattern (0.05/0.2/0.1/1.0for group 1,0.7/0.8/0.68/0.8for group 2);*.smaliscan since the owning class isn't pinned, head/tail lines kept byte-identical, file's own item indent reused. Plus notification-icon-count extension in the same decode cycle:patch_notification_arrays()(notification_icon_counts_entries_3item duplicated to 3 copies, values gain5,7in everyarray*.xmlcarrying the arrays) andpatch_notification_count_smali()(setupShowNotificationIconCount()V: registers +2 preserving the directive (.locals 5→.locals 7),const v5/v6after the v0/v1/v2 triple,filled-new-arrayextended to 5 regs;*.smaliscan, idempotent, missing method only warns). Settings.apk goes throughpatch_apk_apktool()(apktool full aapt recompile — apkeditor builds against the original table and rejects NEW resources): same rule constants/helpers as the apkeditor path, plus the committedkashi_settings/merge (10 smali into the classes12 dex located by itsvendor/.../misyscontent, 6 new res incl. 2 PNGs + 3 layout replacements —miui_version_cardfull rewrite,device_layout2-column container,my_device_settingsfixed card height;public.xmlNOT carried — new IDs are pinned viakashi_public.xml<public>entries so the copied smali consts match, proven by public.xml comparison), wiring + ALPHA literal block replaces (exact-once or warn-skip; ALPHA only at the 6 reference sites, the other 8IS_INTERNATIONAL_BUILDin MiuiSettings stay), res appends (colors/night-colors/ids), andLINKER_FIX_REPLACE(stock rawtrue→#00000001, semantically identical via TypedArray data passthrough — strict aapt2 rejects the raw form). apktool needs a de-privated android framework (ensure_apktool_framework(): rebuild stock 1.apk from its own decode drops private flags keeping every ID, installed once via marker). Entry check is qualifier-normalized (-vNdropped,nxhdpi→440dpi, META-INF loss allowed — the rebuild is re-signed right after) + PNG/basename allowlist, else fail-fast. Proven: pipeline output diffed against the hand-patched reference shows zero unintended semantic changes (16 intended files only). Each call drops the staleoat/next to the APK (dex was rebuilt). NOTE: the rebuild drops the old signatures and re-signs with the committedtestkey/(pure-stdlib v1 signer — unsigned output would fail cert collection before DSV could help); DSV neuters the key-mismatch checks like with modded-apps overlays. Missing APKs only warn. 4g3.patch_apk_res()(product/overlay/DevicesOverlay.apk, always, both modes — in port mode the file arrives from stock via donors): apkeditor decode → regex edits on decoded text files → rebuild (no dex/smali required, so resource-only overlays are covered; decode must yield*.xml, rebuilt entry-name set must equal the original, atomic replace). Rule:status_bar_padding_top14.0px→25.0pxin any decoded xml carrying it (glob is*.xml— the value lives in dimen-port variants, not plaindimen.xml; tag+value anchored, indent-agnostic). Drops the staleoat/next to the APK. Missing APK only warns. 4h.move_data_app_to_app(): movesproduct/data-app/*intoproduct/app/on the build tree (patch_root), merging dirs recursively viamerge_tree_into()(existing entries replaced).data-app/itself is kept as an empty dir (never removed). Runs last, right before the rebuild, so debloat leftovers, DSV and modded apps all land inapp/; no-op whendata-app/is absent. 4i.build_sidecars(): writes<part>.fs_config+<part>.file_contextsnext to each target.imgfor erofs→ext4 conversions, and<part>.file_contextsonly for erofs→erofs rebuilds (uid/gid/mode/caps come from the tree itself at mkfs time, no fs_config needed); native ext4 keeps the legacy build with no sidecars.fs_config=/ 0 0 0755+<part>/<rel> uid gid mode [capabilities=0x..]walked live from the final tree (dirs, files AND symlinks — symlinks consume inodes too, omitting them starved system builds, proven by CI);file_contexts=/+ ownetc/selinux/*_file_contexts+ plat/vendor fallbacks (specific-first, dedup), ROM lines filtered by an allowlist grammar (safe path/context chars incl.(a|b)alternations,[^/]/[0-9]classes and@in HAL names, optional known file-kind, plus a compile check — an uncompilable pattern like unbalanced[fails lookups with cryptic errors, proven by bisecting a real file down to one poison line; anything else is dropped and logged, while$tiers keep every path covered regardless), exact paths converted to(/.*)?pattern form (+escaped) since this lookup only matches patterns, plus explicitlost+foundentries (mke2fs creates it, e2fsdroid fails fast on a miss) and$-anchored entries for every dir/file/symlink in the tree, ordered truth (dumped from the tree itself — wins over everything), ROM patterns, inherit (nearest resolved parent, elsesystem_fileroot).$paths escape all regex metacharacters (libc++.so,x(1).apk,y[2].binnever match themselves otherwise — proven). Stock policy files don't cover every path (whole priv-app castes proven uncovered in CI) — without the inherit tier the build could never go green;$lines match exactly their path so tiers can't shadow each other wrongly. The per-partition log line reports the truth/inherit split. Runs on final trees right before the rebuild; missing sources only warn (→ legacy build).rebuild_partition_images(): rebuilds.imgin place from (patched) trees — port mode: port list (minusSUPER_EXCLUDE, i.e. no mi_ext) →extracted_port/,STOCK_PARTITIONSonly →extracted_stock/(donor extrasproduct/system_extinunpacked_stock/are never rebuilt); mod mode: fullMOD_PARTITIONSset (minusSUPER_EXCLUDE) →extracted_stock/from the single stock tree. Format matches original by magic: erofs via vendoredtools/mkfs.erofs(-z<COMP> -T<fix> --mount-point=/<part> --file-contexts, owners/modes/caps from the tree; unmatched files stay unlabeled, no fail-fast here unlike e2fsdroid) when its sidecar exists, else legacy systemmkfs.erofs -z<COMP>, ext4 via vendored AOSPtools/mke2fs(empty fs:-L -I 256 -N <lines+8> -M /<part> -m 0 -b 4096, journal KEPT — diverges from packer/UKA^has_journaldeliberately) +tools/e2fsdroid -e -T1230768000 -C -S -f -a(populate/label;-sdedup only for non-RW) when sidecars exist, else legacymkfs.ext4 -d(size = max(original, tree*1.1+32MB)). Partitions in the--ext4-rwset are forced to ext4 regardless of original format, sized tree + free_mb + 128MB margin with-m 0 -b 4096(no root reserve so the free MBs are really visible; explicit-bbecause mke2fs silently picks 1K blocks on small filesystems); anything outside the set keeps legacy behavior. Build goes to<name>.img.new+ atomic rename, so failures keep the old image. 5b.main()deletesunpacked_stock/+unpacked_port/right after the rebuild — nothing below uses them, and lpmake needs ~super_size bytes free next.repack_super_image(stock_dir, port_dir, ...)[+mod_partitionsin mod mode, both minusSUPER_EXCLUDE(mi_ext is donors-only, never packed — like the stock extras)]: logs every image size +dfof the output dir, then fail-fastRuntimeError(clear message) when free < super+512MB — without it lpmake dies with crypticsparse_file_write failed (error code -22).lpmake --metadata-slots 3 --virtual-ab(no--sparse— rawsuper.img), groupsqti_dynamic_partitions_a(same size for_b), 4096-byte alignment, stepped packing (DNA-style: super = ceil((images total + 1MB metadata) / 512MB step) up to whole steps, groups = super − 1MB each, so free space remains inside the groups for COW/OTA — e.g. 8.62GB of images → 9.0GB super with ~392MB free; the 1MB metadata minimum is proven locally: anything less fails with exit 70; explicit--device super:SIZE:4096— without it lpmake pads ~1MB per partition; each image pre-padded to 4096 with truncate so partition size == file size, since short reads fail under explicit alignment; free-inside-groups is logged UKA-style). lpmake output is captured (it probes every raw image as sparse → "Invalid sparse file format" noise is expected and harmless; only tail shown on failure, plusdfat failure). Every partition is added twice:<name>_awith the real image,<name>_bas empty size-0 placeholder (no--image— unpacks as 0-byte file). Partition attr isnone(notreadonly— matches stock VAB + UnpackerSuper/UKA packing;readonlyis for retrofit devices). Group sum may exceed super size; allowed under--virtual-ab(COW).assemble_package(): fresh copy of the device template for the variant ({device}_template_fenrirwith engineering preloader + patched LK, else{device}_template_nonfenrir; validated byvalidate_template_payloads()— everyimages/*file and archive must be at least 512 bytes and must not be a git-lfs pointer (real payloads start at 4KB), otherwise fail fast instead of bricking) →package/,super.imgsplit into exactly 54 sparse chunksimages/super.img.0..53(UKA/img2simg method, pure-Python port insplit_file(): each chunk = sparse header + DONT_CARE offset + RAW data, so fastboot/recovery place every part correctly; raw slices would all land at offset 0. Sizing is exactly UKA — chunk = ceil(size_in_MB/54) whole MB, last takes the remainder (53 full + smaller last); names unpadded to match the scripts).package_type="universal"keepsMETA-INF(updater-script + update-binary, OTA metadata copied overMETA-INF/com/android/— port metadata in port mode, stock metadata in mod mode);"fastboot-only"deletes the wholeMETA-INFdir and skips the metadata copy.create_recovery_zip(): packspackage/intoHyperOS-<port|mod>-<device>-<ver>[-fenrir][-fastboot-only].zipwith standard DEFLATE-6 (level 9 deliberately NOT used — CI re-archives the artifact on upload, archive-in-archive, so max compression only burns minutes). The universal ZIP is flashable in TWRP/OrangeFox (updater-script + ARM update-binary already in the device template); the fastboot-only ZIP is a plain archive — unzip and run the install scripts. CI uploads only the ZIP (super.imgstays local to the run for chunking); on failure it also uploadsextracted_*/<part>.fs_config+.file_contextssidecars as debug artifacts.
- Stock (duchamp, HyperOS 3):
odm vendor odm_dlkm system_dlkm vendor_dlkm(+ extrasproduct system_ext, dumped/unpacked for reference only, never packed into super) - Port (chagall, HyperOS 4):
mi_ext product system system_ext - Mod (
--mode mod): full stock setMOD_PARTITIONS=STOCK_PARTITIONS+ missingPORT_PARTITIONSnames (all 9 fromSTOCK_URL_DUCHAMP, same order as the port super layout) - Firmware URLs (
STOCK_URL_DUCHAMP/PORT_URL) and mod-set URLs (HOS3_MOD_URL/HOS4_MOD_URL/HOS4_GL_MOD_URL, GitHub release assets) are constants at top ofautoporter.py. - HyperOS
system.imgroot contains a nestedsystem/dir (unpacked_port/system/system/app/..., SAR-style).moddedapps_hos*/system/system/...mirrors it — the extra level is correct, do not "flatten" it. Other partitions (product,system_ext, ...) are flat at root. - EROFS rebuild uses
EROFS_COMPRESSOR = "lz4hc,9"(fast + safe;"lzma,9"also readable on the 6.1 kernel since MicroLZMA needs 5.16+, but builds much slower). Do NOT switch to deflate: it needs 6.6+ — unreadable images = bootloop.
- Pushes from WSL always go through Windows Credential Manager: global
credential.helper = '!"/mnt/c/Program Files/Git/ucrt64/bin/git-credential-manager.exe"'(the!shell form is load-bearing — a plain path breaks on the space inProgram Files; no backslash-escaping inside quotes) +credential.credentialStore = wincredman, and NO repo-localcredential.helperoverride (it would shadow the global one). Just run plaingit push origin main— never embed tokens in URLs, never useGH_TOKENfor pushing. tools/holds Linux x86-64 ELFs (lpmake,lpunpack,payload-dumper-go,chunk_split, AOSPmke2fs,e2fsdroid, dev-buildfsck.erofswith xattr restore + killpriv-order fix, dev-buildmkfs.erofswith selinux) + Java jars (baksmali.jar,smali.jar,apktool.jar,apkeditor.jar1.4.9 for APK smali patching) committed to repo — ELFs won't run on Windows/macOS; use WSL2 or CI.lpunpackis currently unused.lpmakeis a modern build (android-15 line, handles images >= 4GiB) and needs only system libc — the previous ancient static blob crashed withsparse_file_write failed (error code -22)on any 4GiB(-exact) dense data run (32-bit length wrap → zero-length chunk →mmap(0)→ EINVAL, exit 73; proven with a local 4GiB-exact repro). Do NOT downgrade it.7zmust exist for non-standard GDrive archives;shutil.unpack_archivealone is insufficient. Do NOT use7z xfor ext4 images — it rewrites absolute symlink targets;debugfs rdumppreserves them.- No
extract.erofsexists upstream (erofs-utils ships onlydump/fsck/mkfs.erofs) — extraction isfsck.erofs --extract, which exists in both 1.7.1 (CI noble) and 1.9. Do NOT switch todump.erofs --cat: it is missing in 1.7.1 and would break CI. tools/holds only what apt cannot provide (lpmake,lpunpack,payload-dumper-go,chunk_split, Java jars, pluse2fsdroid, AOSPmke2fsand the xattr-capable devfsck.erofsfor the contexts pipeline). Everything else (dump/mkfs.erofs,simg2img,debugfs,7z, JRE) comes from the apt list inport.yml— do not vendor them.dsv/(smali sources injected into jars, e.g.XdConfig) IS committed, like the{device}_template_*dirs.- CI frees disk first (removes dotnet/android-sdk/ghc/boost,
docker image prune) — full local runs need similar headroom. - Outputs (
*.zip *.img *.bin,moddedapps_hos*/,extracted_stock/,extracted_port/,unpacked_stock/,unpacked_port/,package/,port_meta/) are gitignored; onlysuper.imgis uploaded as artifact. both{device}_template_fenrir/and{device}_template_nonfenrir/ARE committed (negations!*_template_*/**/*.{img,bin,zip}keep payloads visible to git despite the rules above; blobs ride LFS per.gitattributes).