Skip to content

Latest commit

 

History

History
56 lines (51 loc) · 34.7 KB

AGENTS.md

File metadata and controls

56 lines (51 loc) · 34.7 KB

AGENTS.md

Single-file Python tool: autoporter.py builds super.img from Xiaomi OTA(s) + 2 GDrive mod archives. No packages, tests, lint, or typecheck.

Run

  • pip install -r requirements.txt (needs requests, tqdm)
  • CI reference (.github/workflows/port.yml, manual workflow_dispatch only, device + build_target (base|fenrir|both — matrix over both runs base+fenrir in parallel, fail-fast: false) + the usual inputs): Python 3.10 on ubuntu-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 uses lfs: 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-only sudo 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 (default port; workflow choice input — port mixes stock+port OTAs, mod modifies the full stock firmware only, no donors, no port OTA download), --hyper-version hos3|hos4 (default hos4), --package-type universal|fastboot-only (default universal; workflow exposes both as choice inputs), --debloat auto|hos3|hos4|none (default auto = match --hyper-version; workflow choice input), --dsv yes|no (default yes; workflow choice input — gates ONLY the signature-verification lists via signature_patch_lists(); functional smali fixes always apply), --decrypt-data yes|no (default no; workflow choice default yes), --density (default 480; workflow number input — written as persist.miui.density_v2 + ro.sf.lcd_density into product/etc/build.prop), --aod-fullscreen true|false (default true; workflow choice input — support_aod_fullscreen in duchamp.xml + NoSuchElementException catch in miui-services.jar), --dialer yes|no (default yes; workflow choice input — MIUI dialer on global firmwares only), --device duchamp (default duchamp; workflow choice input — target device, selects firmware templates; device-specific values live in DEVICES), --variant base|fenrir (default base; set from workflow build_target matrix — fenrir uses the {device}_template_fenrir template (engineering preloader + patched LK) plus FENRIR_PROPS), --ext4-rw (comma list/all/none, default product,system; workflow string input) + --ext4-free-mb (default 150; workflow number input). Firmware region is auto-detected from the OTA URL/filename (detect_region_code(): 2 letters before trailing XM in the OS version — port URL in port mode, stock URL in mod mode; CN = China, else global incl. EU branch).

What the script does (order matters)

  1. setup_tools(): mkdirs tools/ moddedapps_hos3/ moddedapps_hos4/ moddedapps_hos4_gl/ extracted_stock/ extracted_port/ unpacked_stock/ unpacked_port/ port_meta/, chmod +x on tools/*, prepends tools/ to PATH.
  2. Downloads the selected mod set via direct link (select_modded_apps(): --hyper-version hos3|hos4|hos4_gl + auto region — global hos4 takes hos4_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 with shutil.unpack_archive falling back to 7z x (requires p7zip-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.
  3. process_firmware(): downloads OTA zip → extracts only payload.bin (+ META-INF/com/android/metadata{,.pb} → port_meta/, before zip deletion) → deletes zip immediately → dumps partitions via payload-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 own META-INF → port_meta/, no port OTA download.
  4. unpack_partitions(): port mode unpacks stock .img (incl. extras) → unpacked_stock/<name>/, port .img → unpacked_port/<name>/; mod mode unpacks MOD_PARTITIONS → unpacked_stock/<name>/ only (fresh extract, stale content wiped). Format by magic: ext4 via debugfs rdump (e2fsprogs, preinstalled), erofs via vendored tools/fsck.erofs --extract --xattrs (dev build + killpriv-order fix: restores owners/modes/xattrs as root; falls back to system fsck without --xattrs on tooling failure, data errors always raise), sparse via simg2img if present. uid/gid/xattrs are NOT preserved by debugfs 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 both unpacked_port/product/ and unpacked_stock/product/ (donors are read from the stock tree, so without it they would miss into pangu/; stock product is donors-only, never rebuilt), mod mode flattens unpacked_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 (currently product/etc/device_features/duchamp.xml as-is; extend per device), DONOR_DIRS merged recursively (currently product/etc/displayconfig), DONOR_DIR_FILES (currently 4 APEXes from system_ext/apex + 4 overlays from product/overlay: AospFrameworkResOverlay, DevicesAndroidOverlay, DevicesOverlay, MiuiFrameworkResOverlay). Stale oat/ next to any copied-in .apk is dropped via drop_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 into unpacked_port/ (set partitions missing there, e.g. vendor/ with v4a, fall back into unpacked_stock/ via fallback_root), mod mode into unpacked_stock/ — each top-level partition dir (product/, system/, system_ext/, vendor/, ...) merged recursively via copy_tree_into() (symlinks preserved, modded files replace existing ones; system/ keeps its nested system/ level on both sides). Stale oat/ next to every overlaid .apk is dropped via drop_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_ext is the port's in port mode, the stock's in mod mode): patches mi_ext/etc/build.prop (ro.product.mod_device = port codename from the OTA filename replaced with duchamp, replace-only, never appended; MI_EXT_DROP_PROP_KEYS removed by key — ai version keys kept since they exist, ab_ota_partitions dropped; ro.vendor.radio.5g=3 only when present, ro.mi.os.version.incremental value prefixed with TengeOS | when present, ro.miui.support.system.app.uninstall.v2 moved into product/etc/build.prop via upsert), moves every other mi_ext/etc/build.prop key into product/etc/build.prop via upsert (except MI_EXT_COPY_EXCLUDE, deleted from mi_ext after copying), moves uninstall blobs from the nested mi_ext/product/ into product/ (MI_EXT_PRODUCT_MOVES: platform-miui-uninstall.xml always, rustruntime_cfg_v3_v5.xml only when --hyper-version hos4, miui-uninstall-empty.jar — CN mi_ext only, global warns-missing; moves preserve xattrs/symlinks via move_file_preserve(), emptied parents dropped best-effort), global firmwares additionally move mi_ext/product/ content into product/ (MI_EXT_GLOBAL_DIR_MOVES: app/data-app/priv-app entries, etc files + dir merges, framework/opcust/overlay merges via _move_fs_entry() with stale-oat/ drops) and drop product/overlay/MiuiTelephonyResCustOverlay.apk afterwards, deletes mi_ext/system + mi_ext/system_ext + mi_ext/product/app, and deletes product/etc/permissions/cn.google.services.xml on 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 committed about_phone_description/duchamp/<hos3|hos4>/ overlay for the selected --hyper-version in — product/etc/device_info.json → product/etc/ and system/system/priv-app/HTMLViewer/HTMLViewer.apk → system/system/. Copy (not move — the overlay is repo content) with xattrs/symlinks via copy_file_preserve() (existing dests replaced); stale oat/ next to a copied-in .apk is dropped via drop_oat_next_to(); missing sources only warn; runs before debloat. 4c3c. apply_init_rc_tweak() on the build tree (patch_root, both modes): appends the animationfix block once to the end of system/system/etc/init/hw/init.rc (SAR-nested; on property:sys.boot_completed=1 → start animationfix, service animationfix forcing deviceLevelList via settings put). Idempotent (guard line service animationfix skips re-runs); missing file only warns; runs before debloat+rebuild. 4c4. apply_build_prop_tweaks() on the build tree (patch_root, both modes): writes --density into product/etc/build.prop (persist.miui.density_v2 + ro.sf.lcd_density), appends PRODUCT_PROP_APPEND (blur flags, handy mode, uninstall flag, ro.miui.product.home) to product, COMPUTILITY_PROPS on hos4/hos4_gl either mode (5 computility levels, replaced in place — the keys already exist in stock), FENRIR_PROPS on the fenrir variant into system/system/build.prop (verified-boot props), and SYSTEM_PROP_APPEND (gmscore, opa, dex2oat speed block with comment) to system/system/build.prop (SAR-nested in both trees), and normalizes ro.product.locale=en-US + ro.build.host=wectazz in 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 every ro.*.build.fingerprint key in every build.prop under both unpacked trees, in place, idempotent; missing files/keys only warn; on the fenrir variant the exact ro.build.fingerprint key is additionally upserted into system/system/build.prop). No fingerprint asserts exist in either template's updater-script (only a ro.product.device == duchamp device 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 committed duchamp/duchamp.xml mod reference is copied over product/etc/device_features/duchamp.xml (missing overlay only warns, donor kept), then line-based tweaks — support_aod_fullscreen set/inserted per --aod-fullscreen (+ always-on support_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, second aod_support_keycode_goto_dismiss false→true (first occurrence is already true, matched by value so it stays untouched), support_screen_enhance_engine + support_AI_display inserted after default_eyecare_mode, 90Hz item added to fpsList, 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 committed dialer_gl/ set (InCallUI, MIUIContactsTGlobal, MiuiMmsGlobal → product/priv-app/, chmod-normalized 0755/0644); other global regions move the same apps from mi_ext/product/priv-app/. Stale oat/ next to installed APKs is dropped; missing sources only warn. Then GmsConfigOverlayComms.apk strings are repointed via patch_apk_res() (com.google.android.dialer/contacts → com.android.contacts, com.google.android.apps.messaging → com.android.mms in strings.xml). Runs before debloat+rebuild. 4c6. apply_vibrator_fix() on the unpacked stock tree (port mode only — odm exists only in stock; not dsv-gated, it's a functional port fix): hos3→hos4 vibrator fix — /vibratorfeature → /default in the vintf manifest odm/etc/vintf/manifest/vendor.xiaomi.hardware.vibratorfeature.service.xml, and the same replacement NUL-padded to equal length in the hw service binary odm/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 takes DEBLOAT_HOS4_GLOBAL instead of the CN list; hos4_gl normalizes to hos4 for selection); mod mode applies the same lists onto the unpacked stock tree instead (mi_ext entries just warn-missing when absent) — STOCK_DEBLOAT always goes onto the unpacked stock tree (currently vendor/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 like MIPay/MIpay; resolved renames are logged as [case]). Paths are relative to unpacked_port/ / unpacked_stock/ (NOT extracted_* — those hold .img files). Missing entries only warn (OTAs differ); runs before the rebuild bakes the trees in. Same step also runs apply_vendor_build_prop() (vendor/build.prop in the stock tree, both modes: every ro.hwui.use_vulkan=* → true, debug.renderengine.backend=* → skiavkthreaded, ro.control_privapp_permissions=enforce line dropped; missing file only warns) and patch_vendor_fstab() (DNA rw/decrypt port): strips AVB options from vendor/etc/fstab.* lines, drops overlay lines (rw), drops /mi_ext mount lines (mi_ext is not packed into super), and with --decrypt-data yes renames fileencryption → fileencryptable (AVB strips run first so the renamed option can't dodge them). 4e. patch_jar_smali(): decompiles system/system/framework/framework.jar per-dex with tools/baksmali.jar (--api from each dex header via DEX_VERSION_API), injects dsv/framework/<dex>/*.smali at the path from its own .class (currently XdConfig → classes6/android/os/; skips classes already present), applies FRAMEWORK_METHOD_PATCHES (body → XdConfig call) + FRAMEWORK_INSERT_PATCHES (insert after invoke), reassembles with tools/smali.jar and swaps the dexes back into the jar (other entries byte-identical, atomic replace). Missing files/methods only warn. Needs java (CI: default-jre-headless). Source tree is unpacked_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: whole oat/ next to every patched APK, but only <name>.odex/.vdex/.art in oat/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. Same patch_jar_smali() on system_ext/framework/miui-services.jar (under patch_root) with MIUI_SERVICES_METHOD_PATCHES (void bodies, no XdConfig call — signature, dsv-gated) + MIUI_SERVICES_START_PATCHES (insert secure-flag bypass block at the start of notAllowCaptureDisplay, matched by name + descriptor + body markers, unique :bypass_secure_flag label since baksmali renumbers :cond_N itself — functional, always applied) + MIUI_SERVICES_REPLACE_PATCHES (literal field swap IS_INTERNATIONAL_BUILD → IS_MIUI for notifications on Chinese ROMs — functional, always applied; no dsv dir yet — injection skips gracefully) + Baidu→Gboard keyboard strings in InputMethodManagerServiceImpl (functional, always applied, both modes) + MIUI_SERVICES_DRM_REPLACE_PATCHES (4-line DrmBroadcast sequence in ActivityManagerServiceImpl deleted outright — functional, always applied) + MIUI_SERVICES_AOD_CATCH_PATCHES (.catch Ljava/util/NoSuchElementException; inserted after the SecurityException catch whose previous .catch is RemoteException with the same try range + handler — fullscreen-AOD fix, gated by --aod-fullscreen like the xml flag, NOT dsv-gated; try/catch labels captured from the file so hos3/hos4 numbering doesn't matter, *.smali scan 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. Same patch_jar_smali() on system_ext/framework/miui-framework.jar (under patch_root, both modes, always) with keyboard strings in InputMethodServiceInjector only (no dsv dir — injection skips gracefully; missing jar only warns). 4g. Same patch_jar_smali() on system/system/framework/services.jar (under patch_root) with SERVICES_METHOD_PATCHES: checkUpgradeKeySetLocked + signature-match methods (keep original .registers, body → XdConfig call), checkDowngrade overloads (void stubs), <clinit> (full custom block forcing ALLOW_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 for WindowManagerService.notifyScreenshotListeners, false variant for WindowState.isSecureLocked) + SERVICES_NEW_METHODS (brand-new WindowState.isBypassSecureFlag() reading persist.sys.secure_flag, inserted before isLegacyPolicyVisibility, skipped if already present). Plus SERVICES_VIBRATOR_REPLACE_PATCHES in port mode only (hos3→hos4 vibrator fix: 4-line MiuiStubUtil.getImpl sequence in VibratorManagerServiceStub$Holder → direct new-instance+<init>, via the literal whole-file replace). All three jar steps run only with --dsv yes (default; workflow choice input dsv: "yes"|"no"); "no" skips the DSV lists only (functional fixes still apply to all jars). 4g2. patch_apk_smali() (extended keyboard, always, both modes): full tools/apkeditor.jar decode → literal whole-file replaces on the decoded smali/ 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 in finally. Internal dex lib handles dex up to 042 (unlike the jar tools). Rules: Baidu→Gboard strings in product/app/MIUIFrequentPhrase/MIUIFrequentPhrase.apk (InputMethodBottomManager), method_body_patches in system_ext/priv-app/Provision/Provision.apk (isPocoDevice()Z → false) and system_ext/app/PowerKeeper/PowerKeeper.apk (getDisplayCtrlCode()I → 0, setScreenEffect(+Internal) → void; jar step 3a semantics, missing targets only warn) and system_ext/priv-app/Settings/Settings.apk (AvailableVirtualKeyboardFragment), plus a const/4 v0, 0x1 after the getInt move-result in InputMethodFunctionSelectUtils (force IME-bottom support; blank-line-exact block, proven against real Settings smali). Plus SETTINGS_ARRAY_PATCHES: 6 .array-data float blocks (3 per group) matched by their 16 values as ints — never by :array_NNN labels, which renumber between builds — and rewritten with the repeating 4-value pattern (0.05/0.2/0.1/1.0 for group 1, 0.7/0.8/0.68/0.8 for group 2); *.smali scan 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 _3 item duplicated to 3 copies, values gain 5,7 in every array*.xml carrying the arrays) and patch_notification_count_smali() (setupShowNotificationIconCount()V: registers +2 preserving the directive (.locals 5→.locals 7), const v5/v6 after the v0/v1/v2 triple, filled-new-array extended to 5 regs; *.smali scan, idempotent, missing method only warns). Settings.apk goes through patch_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 committed kashi_settings/ merge (10 smali into the classes12 dex located by its vendor/.../misys content, 6 new res incl. 2 PNGs + 3 layout replacements — miui_version_card full rewrite, device_layout 2-column container, my_device_settings fixed card height; public.xml NOT carried — new IDs are pinned via kashi_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 8 IS_INTERNATIONAL_BUILD in MiuiSettings stay), res appends (colors/night-colors/ids), and LINKER_FIX_REPLACE (stock raw true → #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 (-vN dropped, 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 stale oat/ next to the APK (dex was rebuilt). NOTE: the rebuild drops the old signatures and re-signs with the committed testkey/ (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_top 14.0px → 25.0px in any decoded xml carrying it (glob is *.xml — the value lives in dimen-port variants, not plain dimen.xml; tag+value anchored, indent-agnostic). Drops the stale oat/ next to the APK. Missing APK only warns. 4h. move_data_app_to_app(): moves product/data-app/* into product/app/ on the build tree (patch_root), merging dirs recursively via merge_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 in app/; no-op when data-app/ is absent. 4i. build_sidecars(): writes <part>.fs_config + <part>.file_contexts next to each target .img for erofs→ext4 conversions, and <part>.file_contexts only 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 = / + own etc/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 explicit lost+found entries (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, else system_file root). $ paths escape all regex metacharacters (libc++.so, x(1).apk, y[2].bin never 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).
  5. rebuild_partition_images(): rebuilds .img in place from (patched) trees — port mode: port list (minus SUPER_EXCLUDE, i.e. no mi_ext) → extracted_port/, STOCK_PARTITIONS only → extracted_stock/ (donor extras product/system_ext in unpacked_stock/ are never rebuilt); mod mode: full MOD_PARTITIONS set (minus SUPER_EXCLUDE) → extracted_stock/ from the single stock tree. Format matches original by magic: erofs via vendored tools/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 system mkfs.erofs -z<COMP>, ext4 via vendored AOSP tools/mke2fs (empty fs: -L -I 256 -N <lines+8> -M /<part> -m 0 -b 4096, journal KEPT — diverges from packer/UKA ^has_journal deliberately) + tools/e2fsdroid -e -T1230768000 -C -S -f -a (populate/label; -s dedup only for non-RW) when sidecars exist, else legacy mkfs.ext4 -d (size = max(original, tree*1.1+32MB)). Partitions in the --ext4-rw set 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 -b because 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() deletes unpacked_stock/ + unpacked_port/ right after the rebuild — nothing below uses them, and lpmake needs ~super_size bytes free next.
  6. repack_super_image(stock_dir, port_dir, ...) [+ mod_partitions in mod mode, both minus SUPER_EXCLUDE (mi_ext is donors-only, never packed — like the stock extras)]: logs every image size + df of the output dir, then fail-fast RuntimeError (clear message) when free < super+512MB — without it lpmake dies with cryptic sparse_file_write failed (error code -22). lpmake --metadata-slots 3 --virtual-ab (no --sparse — raw super.img), groups qti_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, plus df at failure). Every partition is added twice: <name>_a with the real image, <name>_b as empty size-0 placeholder (no --image — unpacks as 0-byte file). Partition attr is none (not readonly — matches stock VAB + UnpackerSuper/UKA packing; readonly is for retrofit devices). Group sum may exceed super size; allowed under --virtual-ab (COW).
  7. assemble_package(): fresh copy of the device template for the variant ({device}_template_fenrir with engineering preloader + patched LK, else {device}_template_nonfenrir; validated by validate_template_payloads() — every images/* 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.img split into exactly 54 sparse chunks images/super.img.0..53 (UKA/img2simg method, pure-Python port in split_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" keeps META-INF (updater-script + update-binary, OTA metadata copied over META-INF/com/android/ — port metadata in port mode, stock metadata in mod mode); "fastboot-only" deletes the whole META-INF dir and skips the metadata copy.
  8. create_recovery_zip(): packs package/ into HyperOS-<port|mod>-<device>-<ver>[-fenrir][-fastboot-only].zip with 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.img stays local to the run for chunking); on failure it also uploads extracted_*/<part>.fs_config + .file_contexts sidecars as debug artifacts.

Hardcoded inputs

  • Stock (duchamp, HyperOS 3): odm vendor odm_dlkm system_dlkm vendor_dlkm (+ extras product 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 set MOD_PARTITIONS = STOCK_PARTITIONS + missing PORT_PARTITIONS names (all 9 from STOCK_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 of autoporter.py.
  • HyperOS system.img root contains a nested system/ 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.

Gotchas

  • 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 in Program Files; no backslash-escaping inside quotes) + credential.credentialStore = wincredman, and NO repo-local credential.helper override (it would shadow the global one). Just run plain git push origin main — never embed tokens in URLs, never use GH_TOKEN for pushing.
  • tools/ holds Linux x86-64 ELFs (lpmake, lpunpack, payload-dumper-go, chunk_split, AOSP mke2fs, e2fsdroid, dev-build fsck.erofs with xattr restore + killpriv-order fix, dev-build mkfs.erofs with selinux) + Java jars (baksmali.jar, smali.jar, apktool.jar, apkeditor.jar 1.4.9 for APK smali patching) committed to repo — ELFs won't run on Windows/macOS; use WSL2 or CI. lpunpack is currently unused. lpmake is a modern build (android-15 line, handles images >= 4GiB) and needs only system libc — the previous ancient static blob crashed with sparse_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.
  • 7z must exist for non-standard GDrive archives; shutil.unpack_archive alone is insufficient. Do NOT use 7z x for ext4 images — it rewrites absolute symlink targets; debugfs rdump preserves them.
  • No extract.erofs exists upstream (erofs-utils ships only dump/fsck/mkfs.erofs) — extraction is fsck.erofs --extract, which exists in both 1.7.1 (CI noble) and 1.9. Do NOT switch to dump.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, plus e2fsdroid, AOSP mke2fs and the xattr-capable dev fsck.erofs for the contexts pipeline). Everything else (dump/mkfs.erofs, simg2img, debugfs, 7z, JRE) comes from the apt list in port.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; only super.img is 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).