Add merged single APK support for XAPK and fix plugin version to 1.2.0

New merge-splits.sh script merges XAPK split APK contents (native libs,
resources, manifest cleanup) into the decoded base directory, enabling
single APK output installable via standard adb install. rebuild-apk.sh
gains --single-apk flag with auto-detection via .merged marker. SKILL.md
updated with Phase 5a/5b/5c workflow for XAPK rebuild choice.

Fixed plugin.json version from 1.0.0 to 1.2.0 to match marketplace.json.
Added versioning guidance to CLAUDE.md to prevent future mismatches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Simone Avogadro 2026-03-09 15:16:51 +01:00
parent e70920cf87
commit 3d68cf392f
5 changed files with 555 additions and 15 deletions

View File

@ -21,7 +21,7 @@ A Claude Code Skill (plugin) for Android reverse engineering, API extraction, an
- `plugins/android-reverse-engineering/skills/android-reverse-engineering/` — Core RE skill (5-phase workflow, references, scripts)
- `plugins/android-reverse-engineering/skills/tracker-analysis/` — Tracker/analytics SDK detection skill (4-phase workflow, references, find-trackers.sh)
- `plugins/android-reverse-engineering/skills/ad-analysis/` — Advertising SDK detection skill (3-phase workflow, references, find-ads.sh)
- `plugins/android-reverse-engineering/skills/sdk-neutralizer/` — SDK neutralization skill (6-phase workflow, references, decode-apk.sh, neutralize.sh, rebuild-apk.sh)
- `plugins/android-reverse-engineering/skills/sdk-neutralizer/` — SDK neutralization skill (6-phase workflow, references, decode-apk.sh, neutralize.sh, merge-splits.sh, rebuild-apk.sh)
## Key Scripts
@ -70,8 +70,11 @@ bash decode-apk.sh <file.apk|file.xapk> [-o <decoded-dir>]
# Neutralize SDK entry points in decoded APK (dry-run first)
bash neutralize.sh <decoded-dir> [--ads|--trackers|--all] [--dry-run] [--no-backup] [--no-manifest] [--targets-file <file>] [--replay] [--no-save-manifest]
# Rebuild and sign neutralized APK (auto-reassembles XAPK if .xapk-origin/ exists)
bash rebuild-apk.sh <decoded-dir> [--auto-keystore|--debug-key|--keystore <file>] [-o <output>] [--no-sign] [--no-res] [--zipalign]
# Merge XAPK splits into decoded base for single APK output (optional, for XAPK input)
bash merge-splits.sh <decoded-dir> [--abi <abi>] [--all-abis] [--skip-resources]
# Rebuild and sign neutralized APK (auto-reassembles XAPK if .xapk-origin/ exists, or single APK if merged)
bash rebuild-apk.sh <decoded-dir> [--auto-keystore|--debug-key|--keystore <file>] [-o <output>] [--no-sign] [--no-res] [--zipalign] [--single-apk]
```
## Architecture
@ -97,6 +100,14 @@ bash rebuild-apk.sh <decoded-dir> [--auto-keystore|--debug-key|--keystore <file>
This is a documentation-and-scripts plugin with no compiled code, no test suite, and no linter configuration. Changes are validated by reading the markdown/bash and testing scripts manually against APK files.
## Versioning
The plugin version is declared in **two** files that **must be kept in sync**:
- `.claude-plugin/marketplace.json``plugins[0].version` (marketplace catalog)
- `plugins/android-reverse-engineering/.claude-plugin/plugin.json``version` (plugin manifest)
Claude Code reads the version from `plugin.json` with priority — if that file has a stale version, `/plugin` will show the old number even if `marketplace.json` is updated. **Always bump both files together.**
## Conventions
- Line endings are LF (enforced via `.gitattributes` for WSL/Windows compatibility)

View File

@ -1,6 +1,6 @@
{
"name": "android-reverse-engineering",
"version": "1.0.0",
"version": "1.2.0",
"description": "Decompile Android APK/JAR/AAR with jadx, trace call flows, extract APIs, analyze tracker/ad SDKs for privacy auditing, and neutralize SDK entry points for enterprise deployment.",
"author": {
"name": "Simone Avogadro"

View File

@ -94,7 +94,7 @@ For XAPK input, the script also outputs `XAPK_ORIGIN:<path>` and creates:
- `.xapk-origin/manifest.json` — original XAPK manifest
- `.xapk-origin/splits/` — all split APKs (config.arm64_v8a.apk, config.en.apk, etc.)
If the input is an XAPK, inform the user that it's a split APK bundle and that all splits will be automatically re-signed during rebuild.
If the input is an XAPK, inform the user that it's a split APK bundle. Let them know that in Phase 5 (Rebuild) they will be asked whether to produce a **merged single APK** (easier to install) or keep the **XAPK bundle** (preserves all splits).
### Phase 3: Identify Targets
@ -224,6 +224,42 @@ After a successful (non-dry-run) neutralization, a `neutralize-manifest.json` is
Rebuild the decoded directory back into a signed APK (or XAPK if the original was an XAPK).
#### Phase 5a — XAPK Output Format Choice (XAPK input only)
**If the input was an XAPK**, you **MUST ask the user** how to rebuild:
> How would you like to rebuild the neutralized app?
>
> 1. **Merged single APK** (recommended for sideloading) — merges split contents into one APK, installable with standard `adb install`. May be missing some locale/density resources.
> 2. **XAPK bundle** (preserves original structure) — requires `adb install-multiple` or SAI app to install. All splits preserved exactly.
**If the user chooses option 1 (merged single APK):**
Run `merge-splits.sh` to merge split contents into the decoded base APK directory:
```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/merge-splits.sh <decoded-dir>
```
Options:
- `--abi <abi>` — merge only a specific ABI (e.g., `arm64-v8a`)
- `--all-abis` — merge all ABIs (larger but universal APK)
- `--skip-resources` — skip resource split merge (locale/density)
- Default (no flags): picks the most common ABI (`arm64-v8a` > `armeabi-v7a` > `x86_64` > `x86`)
Parse output for `MERGE_ABI:`, `MERGE_RESOURCES:`, `SKIPPED_RESOURCES:`, `FEATURE_SPLIT_WARNING:`, `MANIFEST_CLEANED:`, and `MERGE_COMPLETE:` lines.
**Important merge limitations to communicate to the user:**
- Resource splits (locale, density) are merged best-effort — compiled `resources.arsc` cannot be fused without `aapt2`. The merged APK uses default resources from the base APK.
- Feature module splits (containing DEX code) **cannot** be merged — the script warns about these.
- Native library merge changes `android:extractNativeLibs` to `true`, which increases installed size.
After merge, the rebuild script auto-detects the `.merged` marker and produces a single `.apk`.
**If the user chooses option 2 (XAPK bundle):** skip `merge-splits.sh` and proceed directly to rebuild — the script will auto-reassemble the XAPK.
#### Phase 5b — Signing Preference
**Before calling rebuild**, you **MUST ask the user** their signing preference:
> How would you like to sign the rebuilt APK?
@ -237,22 +273,31 @@ Map the user's choice to the corresponding flag:
- Option 2 → `--keystore <file> --key-alias <alias> --store-pass <pass> --key-pass <pass>`
- Option 3 → `--no-sign`
#### Phase 5c — Run Rebuild
**Action**: Run the rebuild script with the chosen signing option.
```bash
# Example with auto-keystore (recommended default)
# Single merged APK (after merge-splits.sh, auto-detected via .merged marker)
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh <decoded-dir> --auto-keystore
# Or explicitly force single APK output
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh <decoded-dir> --auto-keystore --single-apk
# XAPK bundle (default when .xapk-origin/ exists and no .merged marker)
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh <decoded-dir> --auto-keystore
```
Options:
- `-o <output>` — custom output path
- `--single-apk` — force single APK output (auto-enabled when `.merged` marker exists)
- `--auto-keystore` — auto-detect best keystore (recommended)
- `--debug-key` — always generate new debug keystore
- `--keystore <file>` — use a custom keystore
- `--no-sign` — output unsigned APK
- `--zipalign` / `--no-zipalign` — control zipalign step
For XAPK input, the rebuild is automatic: the script detects `.xapk-origin/`, re-signs all split APKs with the same keystore, and produces a `.xapk` output. Parse the output for `KEYSTORE_USED:`, `KEYSTORE_SOURCE:`, `SPLIT_SIGNED:`, and `XAPK_ASSEMBLED:` lines.
For XAPK input without merge, the rebuild is automatic: the script detects `.xapk-origin/`, re-signs all split APKs with the same keystore, and produces a `.xapk` output. Parse the output for `KEYSTORE_USED:`, `KEYSTORE_SOURCE:`, `SPLIT_SIGNED:`, and `XAPK_ASSEMBLED:` lines.
### Phase 6: Verify & Report
@ -308,13 +353,29 @@ Modifying APKs may violate the app's EULA, SDK provider agreements, or intellect
property laws. This tool is intended for authorized enterprise use, security research,
and privacy compliance only.
## Split Merge Details (if applicable)
If the original input was an XAPK and the user chose merged single APK output, include this section:
| Merge Step | Result |
|---|---|
| ABI splits merged | arm64-v8a (3 native libraries) |
| Resource splits | 2 merged (best-effort), 1 skipped |
| Feature splits | 0 (or: 1 warning — could not merge) |
| Manifest cleanup | isSplitRequired, extractNativeLibs→true, com.android.vending.splits.required |
**Merge limitations:**
- Locale/density resources use defaults from the base APK (compiled `resources.arsc` from splits cannot be fused)
- `android:extractNativeLibs` was set to `true` — native libs are extracted on install (uses more disk space)
- Feature module splits (if any) were NOT merged and their functionality may be missing
## Output
- Sanitized APK/XAPK: `<path>`
- Output format: APK (single) / XAPK (split bundle)
- Output format: APK (single) / APK (merged from XAPK) / XAPK (split bundle)
- Signed with: auto-detected debug key / generated debug key / custom keystore
- Keystore used: `<path>` (source: `KEYSTORE_SOURCE:` value)
- Install via: `adb install <path>` (APK) or `adb install-multiple <base.apk> <split1.apk> ...` (XAPK)
- Install via: `adb install <path>` (APK / merged APK) or `adb install-multiple <base.apk> <split1.apk> ...` (XAPK)
- For XAPK: can also use SAI (Split APKs Installer) or unzip and `adb install-multiple *.apk`
```

View File

@ -0,0 +1,448 @@
#!/usr/bin/env bash
# merge-splits.sh — Merge XAPK split APKs into the decoded base APK directory
#
# Produces a single-APK-ready directory by merging native libraries,
# resources (best-effort), and cleaning the manifest of split-related attributes.
#
# Exit codes:
# 0 — success (splits merged)
# 1 — error (invalid input, missing files)
# 2 — not an XAPK (no .xapk-origin/ found)
set -euo pipefail
usage() {
cat <<EOF
Usage: merge-splits.sh <decoded-dir> [OPTIONS]
Merge XAPK split APK contents into the decoded base APK directory,
producing a directory ready to rebuild as a single merged APK.
Arguments:
<decoded-dir> Path to the apktool-decoded APK directory (must contain .xapk-origin/)
Options:
--abi <abi> Merge only this ABI (e.g., arm64-v8a, armeabi-v7a, x86_64, x86)
--all-abis Merge all ABIs found in splits (larger but universal)
--skip-resources Skip merging resource splits (locale, density)
-h, --help Show this help message
Output (machine-readable):
MERGE_ABI:<abi> (<count> native libraries)
MERGE_RESOURCES:<split> (<count> files)
SKIPPED_RESOURCES:<split>:<reason>
FEATURE_SPLIT_WARNING:<split> (feature modules cannot be merged)
MANIFEST_CLEANED:<attribute>
MERGE_COMPLETE:<decoded-dir>
EOF
exit 0
}
# =====================================================================
# Argument parsing
# =====================================================================
DECODED_DIR=""
TARGET_ABI=""
ALL_ABIS=false
SKIP_RESOURCES=false
while [[ $# -gt 0 ]]; do
case "$1" in
--abi)
shift
if [[ $# -eq 0 ]]; then echo "Error: --abi requires an argument" >&2; exit 1; fi
TARGET_ABI="$1"; shift ;;
--all-abis) ALL_ABIS=true; shift ;;
--skip-resources) SKIP_RESOURCES=true; shift ;;
-h|--help) usage ;;
-*) echo "Error: Unknown option $1" >&2; usage ;;
*) DECODED_DIR="$1"; shift ;;
esac
done
if [[ -z "$DECODED_DIR" ]]; then
echo "Error: No decoded directory specified." >&2
usage
fi
if [[ ! -d "$DECODED_DIR" ]]; then
echo "Error: Directory not found: $DECODED_DIR" >&2
exit 1
fi
# =====================================================================
# Validate XAPK origin
# =====================================================================
info() { echo "[INFO] $*"; }
ok() { echo "[OK] $*"; }
fail() { echo "[FAIL] $*" >&2; }
XAPK_ORIGIN_DIR="$DECODED_DIR/.xapk-origin"
if [[ ! -f "$XAPK_ORIGIN_DIR/metadata.json" ]]; then
info "Not an XAPK — no .xapk-origin/metadata.json found. Nothing to merge."
exit 2
fi
if [[ ! -d "$XAPK_ORIGIN_DIR/splits" ]]; then
info "No splits directory found in .xapk-origin/. Nothing to merge."
exit 2
fi
# Count split APKs
split_count=0
for f in "$XAPK_ORIGIN_DIR/splits/"*.apk; do
[[ -f "$f" ]] && (( split_count++ )) || true
done
if (( split_count == 0 )); then
info "No split APKs found in .xapk-origin/splits/. Nothing to merge."
exit 2
fi
echo "=== Merging XAPK Splits ==="
echo "Decoded dir: $DECODED_DIR"
echo "Split APKs found: $split_count"
echo
# =====================================================================
# Step 0: Classify splits (ABI, resource, feature)
# =====================================================================
declare -a abi_splits=()
declare -a resource_splits=()
declare -a feature_splits=()
for split_apk in "$XAPK_ORIGIN_DIR/splits/"*.apk; do
[[ -f "$split_apk" ]] || continue
split_name=$(basename "$split_apk")
# Check if it's an ABI split by looking for lib/ inside
if unzip -l "$split_apk" 2>/dev/null | grep -q "lib/[^/]*/"; then
abi_splits+=("$split_apk")
# Check if it's a feature split (contains classes.dex or AndroidManifest.xml with split name)
elif unzip -l "$split_apk" 2>/dev/null | grep -qE "(classes[0-9]*\.dex|^.*AndroidManifest\.xml)" && \
! [[ "$split_name" == config.* ]]; then
feature_splits+=("$split_apk")
else
# Config split (locale, density, etc.)
resource_splits+=("$split_apk")
fi
done
info "ABI splits: ${#abi_splits[@]}"
info "Resource splits: ${#resource_splits[@]}"
info "Feature splits: ${#feature_splits[@]}"
echo
# Warn about feature splits — cannot be merged
if (( ${#feature_splits[@]} > 0 )); then
for fs in "${feature_splits[@]}"; do
fname=$(basename "$fs")
echo "FEATURE_SPLIT_WARNING:$fname (feature modules cannot be merged — contains DEX code and own manifest)"
done
echo
fi
# =====================================================================
# Step 1: Merge native libraries (ABI splits)
# =====================================================================
echo "=== Step 1: Merge Native Libraries ==="
if (( ${#abi_splits[@]} == 0 )); then
info "No ABI splits found — skipping native library merge."
else
# Detect available ABIs
declare -A abi_to_split=()
for split_apk in "${abi_splits[@]}"; do
# Extract ABI name from the lib/ structure inside the APK
while IFS= read -r abi_name; do
abi_to_split["$abi_name"]="$split_apk"
done < <(unzip -l "$split_apk" 2>/dev/null | grep -oP 'lib/\K[^/]+(?=/)' | sort -u)
done
info "Available ABIs: ${!abi_to_split[*]}"
# Determine which ABIs to merge
declare -a abis_to_merge=()
if [[ -n "$TARGET_ABI" ]]; then
# User specified a specific ABI
if [[ -z "${abi_to_split[$TARGET_ABI]:-}" ]]; then
fail "Requested ABI '$TARGET_ABI' not found in splits. Available: ${!abi_to_split[*]}"
exit 1
fi
abis_to_merge=("$TARGET_ABI")
elif [[ "$ALL_ABIS" == true ]]; then
# Merge all ABIs
abis_to_merge=("${!abi_to_split[@]}")
else
# Default: pick most common ABI by priority
for preferred in arm64-v8a armeabi-v7a x86_64 x86; do
if [[ -n "${abi_to_split[$preferred]:-}" ]]; then
abis_to_merge=("$preferred")
break
fi
done
# Fallback: pick the first available
if (( ${#abis_to_merge[@]} == 0 )); then
for abi in "${!abi_to_split[@]}"; do
abis_to_merge=("$abi")
break
done
fi
fi
info "Merging ABIs: ${abis_to_merge[*]}"
# Create lib/ directory in decoded dir if it doesn't exist
mkdir -p "$DECODED_DIR/lib"
TMPDIR_NATIVE=$(mktemp -d "${TMPDIR:-/tmp}/merge-native-XXXXXX")
cleanup_native() { rm -rf "$TMPDIR_NATIVE"; }
trap cleanup_native EXIT
for abi in "${abis_to_merge[@]}"; do
split_apk="${abi_to_split[$abi]}"
split_name=$(basename "$split_apk")
info "Extracting native libs from $split_name (ABI: $abi)..."
# Extract lib/ contents
rm -rf "$TMPDIR_NATIVE/"*
unzip -qo "$split_apk" "lib/*" -d "$TMPDIR_NATIVE" 2>/dev/null || true
if [[ -d "$TMPDIR_NATIVE/lib/$abi" ]]; then
# Copy to decoded dir
mkdir -p "$DECODED_DIR/lib/$abi"
lib_count=0
for so_file in "$TMPDIR_NATIVE/lib/$abi/"*; do
[[ -f "$so_file" ]] || continue
cp "$so_file" "$DECODED_DIR/lib/$abi/"
(( lib_count++ ))
done
ok "Merged $lib_count native libraries for $abi"
echo "MERGE_ABI:$abi ($lib_count native libraries)"
else
info "No native libs found for ABI $abi in $split_name"
fi
done
cleanup_native
trap - EXIT
fi
echo
# =====================================================================
# Step 2: Merge resources (locale/density splits) — best-effort
# =====================================================================
echo "=== Step 2: Merge Resources (best-effort) ==="
if [[ "$SKIP_RESOURCES" == true ]]; then
info "Resource merge skipped (--skip-resources)."
for rs in "${resource_splits[@]}"; do
fname=$(basename "$rs")
echo "SKIPPED_RESOURCES:$fname:user-requested"
done
elif (( ${#resource_splits[@]} == 0 )); then
info "No resource splits found — skipping."
else
TMPDIR_RES=$(mktemp -d "${TMPDIR:-/tmp}/merge-res-XXXXXX")
cleanup_res() { rm -rf "$TMPDIR_RES"; }
trap cleanup_res EXIT
for split_apk in "${resource_splits[@]}"; do
split_name=$(basename "$split_apk")
# Extract everything except META-INF/ and resources.arsc (cannot merge compiled resources)
rm -rf "$TMPDIR_RES/"*
unzip -qo "$split_apk" -d "$TMPDIR_RES" -x "META-INF/*" "resources.arsc" 2>/dev/null || true
# Count extracted files (excluding directories)
file_count=0
while IFS= read -r -d '' f; do
(( file_count++ ))
done < <(find "$TMPDIR_RES" -type f -print0 2>/dev/null)
if (( file_count == 0 )); then
echo "SKIPPED_RESOURCES:$split_name:no-extractable-files"
continue
fi
# Copy res/ contents if present
res_copied=0
if [[ -d "$TMPDIR_RES/res" ]]; then
# Copy resource directories (values-*, drawable-*, etc.)
for res_subdir in "$TMPDIR_RES/res/"*; do
[[ -d "$res_subdir" ]] || continue
subdir_name=$(basename "$res_subdir")
mkdir -p "$DECODED_DIR/res/$subdir_name"
for res_file in "$res_subdir/"*; do
[[ -f "$res_file" ]] || continue
# Don't overwrite existing resources from the base APK
target="$DECODED_DIR/res/$subdir_name/$(basename "$res_file")"
if [[ ! -f "$target" ]]; then
cp "$res_file" "$target"
(( res_copied++ ))
fi
done
done
fi
# Copy assets/ contents if present
assets_copied=0
if [[ -d "$TMPDIR_RES/assets" ]]; then
mkdir -p "$DECODED_DIR/assets"
while IFS= read -r -d '' asset_file; do
rel_path="${asset_file#$TMPDIR_RES/assets/}"
target="$DECODED_DIR/assets/$rel_path"
if [[ ! -f "$target" ]]; then
mkdir -p "$(dirname "$target")"
cp "$asset_file" "$target"
(( assets_copied++ ))
fi
done < <(find "$TMPDIR_RES/assets" -type f -print0 2>/dev/null)
fi
total_copied=$(( res_copied + assets_copied ))
if (( total_copied > 0 )); then
ok "Merged $total_copied files from $split_name"
echo "MERGE_RESOURCES:$split_name ($total_copied files)"
else
echo "SKIPPED_RESOURCES:$split_name:all-files-already-exist-in-base"
fi
done
cleanup_res
trap - EXIT
fi
echo
# =====================================================================
# Step 3: Patch AndroidManifest.xml
# =====================================================================
echo "=== Step 3: Patch AndroidManifest.xml ==="
MANIFEST="$DECODED_DIR/AndroidManifest.xml"
if [[ ! -f "$MANIFEST" ]]; then
fail "AndroidManifest.xml not found in $DECODED_DIR"
exit 1
fi
# 3a: Remove android:isSplitRequired="true" → set to "false"
if grep -q 'android:isSplitRequired="true"' "$MANIFEST"; then
sed -i 's/android:isSplitRequired="true"/android:isSplitRequired="false"/g' "$MANIFEST"
ok "Patched: isSplitRequired=true → false"
echo "MANIFEST_CLEANED:isSplitRequired"
fi
# 3b: Remove split-related <meta-data> elements
# Each pattern removes the entire <meta-data ... /> tag (single-line or multi-line)
declare -a meta_names=(
"com.android.vending.splits.required"
"com.android.vending.splits"
"com.android.vending.derived.apk.id"
"com.android.stamp.source"
"com.android.stamp.type"
)
for meta_name in "${meta_names[@]}"; do
if grep -q "android:name=\"$meta_name\"" "$MANIFEST"; then
# Remove single-line <meta-data ... />
sed -i "/<meta-data[^>]*android:name=\"$meta_name\"[^>]*\/>/d" "$MANIFEST"
# Remove multi-line <meta-data ... > ... </meta-data> (rare but handle it)
# Use a simple approach: remove lines containing the meta-data name within meta-data blocks
sed -i "/<meta-data[^>]*android:name=\"$meta_name\"/,/<\/meta-data>/d" "$MANIFEST"
ok "Removed: <meta-data android:name=\"$meta_name\">"
echo "MANIFEST_CLEANED:$meta_name"
fi
done
# 3c: Remove hasFragileUserData if present (split-related cleanup)
if grep -q 'android:hasFragileUserData' "$MANIFEST"; then
sed -i 's/ *android:hasFragileUserData="[^"]*"//g' "$MANIFEST"
info "Removed: android:hasFragileUserData (split-related)"
fi
# 3d: Fix extractNativeLibs — change false to true when we have merged native libs
if [[ -d "$DECODED_DIR/lib" ]] && [[ -n "$(ls -A "$DECODED_DIR/lib/" 2>/dev/null)" ]]; then
if grep -q 'android:extractNativeLibs="false"' "$MANIFEST"; then
sed -i 's/android:extractNativeLibs="false"/android:extractNativeLibs="true"/g' "$MANIFEST"
ok "Patched: extractNativeLibs=false → true (required for merged native libs)"
echo "MANIFEST_CLEANED:extractNativeLibs→true"
elif ! grep -q 'android:extractNativeLibs' "$MANIFEST"; then
# Add extractNativeLibs="true" to the <application> tag
sed -i 's/<application/<application android:extractNativeLibs="true"/' "$MANIFEST"
ok "Added: extractNativeLibs=true to <application>"
echo "MANIFEST_CLEANED:extractNativeLibs→true (added)"
fi
fi
# 3e: Remove android:splitTypes attribute if present
if grep -q 'android:splitTypes=' "$MANIFEST"; then
sed -i 's/ *android:splitTypes="[^"]*"//g' "$MANIFEST"
ok "Removed: android:splitTypes"
echo "MANIFEST_CLEANED:splitTypes"
fi
# 3f: Remove android:requiredSplitTypes attribute if present
if grep -q 'android:requiredSplitTypes=' "$MANIFEST"; then
sed -i 's/ *android:requiredSplitTypes="[^"]*"//g' "$MANIFEST"
ok "Removed: android:requiredSplitTypes"
echo "MANIFEST_CLEANED:requiredSplitTypes"
fi
echo
# =====================================================================
# Step 4: Create merge marker
# =====================================================================
echo "=== Step 4: Finalize ==="
# Create .merged marker so rebuild-apk.sh knows to produce a single APK
touch "$XAPK_ORIGIN_DIR/.merged"
ok "Created merge marker: $XAPK_ORIGIN_DIR/.merged"
# Write merge metadata
merged_ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%SZ")
{
echo "{"
echo " \"merged_timestamp\": \"$merged_ts\","
echo " \"abi_splits_merged\": ${#abi_splits[@]},"
echo " \"resource_splits_merged\": ${#resource_splits[@]},"
echo " \"feature_splits_skipped\": ${#feature_splits[@]},"
echo " \"skip_resources\": $SKIP_RESOURCES"
echo "}"
} > "$XAPK_ORIGIN_DIR/.merged-metadata.json"
echo
echo "=== Merge Complete ==="
# Summary
echo
echo "Merged splits into: $DECODED_DIR"
if (( ${#feature_splits[@]} > 0 )); then
echo
echo "WARNING: ${#feature_splits[@]} feature module split(s) could NOT be merged."
echo " Feature modules contain their own DEX code and manifest entries."
echo " The merged APK may be missing functionality from these modules."
for fs in "${feature_splits[@]}"; do
echo " - $(basename "$fs")"
done
fi
echo
echo "MERGE_COMPLETE:$DECODED_DIR"
echo
echo "Next: rebuild with --single-apk flag or let rebuild-apk.sh auto-detect the .merged marker."
echo " rebuild-apk.sh $DECODED_DIR --auto-keystore"
exit 0

View File

@ -20,13 +20,17 @@ Usage: rebuild-apk.sh <decoded-dir> [OPTIONS]
Rebuild an apktool-decoded APK directory back into a signed APK.
If the decoded dir contains .xapk-origin/ (from decode-apk.sh), automatically
reassembles a complete XAPK with all split APKs re-signed.
reassembles a complete XAPK with all split APKs re-signed — unless --single-apk
is used or .xapk-origin/.merged exists (from merge-splits.sh), in which case
a single merged APK is produced instead.
Arguments:
<decoded-dir> Path to the apktool-decoded APK directory
Options:
-o, --output <file> Output path (default: <decoded-dir>-neutralized.apk/.xapk)
--single-apk Force output as single .apk even if .xapk-origin/ exists
(auto-enabled when .xapk-origin/.merged marker is present)
--auto-keystore Auto-detect best keystore: ~/.android/debug.keystore,
then previous .neutralizer-debug.keystore, then generate new
--debug-key Sign with an auto-generated debug keystore (default)
@ -70,6 +74,7 @@ DO_ZIPALIGN=true
NO_ZIPALIGN=false
FORCE_NO_RES=false
BUILD_USED_NO_RES=false
SINGLE_APK=false
while [[ $# -gt 0 ]]; do
case "$1" in
@ -95,6 +100,7 @@ while [[ $# -gt 0 ]]; do
shift
if [[ $# -eq 0 ]]; then echo "Error: --store-pass requires an argument" >&2; exit 1; fi
STORE_PASS="$1"; shift ;;
--single-apk) SINGLE_APK=true; shift ;;
--no-sign) DO_SIGN=false; shift ;;
--no-res) FORCE_NO_RES=true; shift ;;
--zipalign) DO_ZIPALIGN=true; shift ;;
@ -122,10 +128,16 @@ if [[ -f "$XAPK_ORIGIN_DIR/metadata.json" ]]; then
IS_XAPK=true
fi
# Default output name — .xapk if original was XAPK, .apk otherwise
# Auto-detect merged marker from merge-splits.sh
if [[ -f "$XAPK_ORIGIN_DIR/.merged" ]]; then
SINGLE_APK=true
echo "[INFO] Detected .xapk-origin/.merged marker — will produce a single merged APK"
fi
# Default output name — .xapk if original was XAPK (and not merging), .apk otherwise
if [[ -z "$OUTPUT" ]]; then
local_dir="${DECODED_DIR%/}"
if [[ "$IS_XAPK" == true ]]; then
if [[ "$IS_XAPK" == true ]] && [[ "$SINGLE_APK" == false ]]; then
OUTPUT="${local_dir}-neutralized.xapk"
else
OUTPUT="${local_dir}-neutralized.apk"
@ -435,7 +447,7 @@ fi
# Step 5: XAPK assembly (if original was XAPK)
# =====================================================================
if [[ "$IS_XAPK" == true ]] && [[ "$DO_SIGN" == true ]]; then
if [[ "$IS_XAPK" == true ]] && [[ "$DO_SIGN" == true ]] && [[ "$SINGLE_APK" == false ]]; then
echo
echo "=== Assembling XAPK ==="
@ -514,8 +526,10 @@ fi
echo
echo "=== Rebuild Complete ==="
if [[ "$IS_XAPK" == true ]]; then
if [[ "$IS_XAPK" == true ]] && [[ "$SINGLE_APK" == false ]]; then
echo "Output XAPK: $OUTPUT"
elif [[ "$IS_XAPK" == true ]] && [[ "$SINGLE_APK" == true ]]; then
echo "Output APK (merged from XAPK): $OUTPUT"
else
echo "Output APK: $OUTPUT"
fi
@ -543,10 +557,16 @@ fi
echo
echo "WARNING: Play Integrity / SafetyNet will FAIL — expected for enterprise sideloading."
if [[ "$IS_XAPK" == true ]]; then
if [[ "$IS_XAPK" == true ]] && [[ "$SINGLE_APK" == false ]]; then
echo "Install via: adb install-multiple <base.apk> <split1.apk> <split2.apk> ..."
echo " or: use a split APK installer (e.g., SAI — Split APKs Installer)"
echo " or: unzip the XAPK and run: adb install-multiple *.apk"
elif [[ "$IS_XAPK" == true ]] && [[ "$SINGLE_APK" == true ]]; then
echo "Install via: adb install $OUTPUT"
echo
echo "NOTE: This is a merged single APK from an XAPK split bundle."
echo " Some locale/density-specific resources may use defaults."
echo " Test thoroughly on target devices."
else
echo "Install via: adb install $OUTPUT"
fi