Skip to main content

Overview

Modern Android apps often use Split APK format, distributing separate files for different architectures, screen densities, and languages. XAPK compilation packages all these files into a single installable archive.

What is XAPK?

XAPK (Extended APK) is a packaging format that bundles:
  • Base APK: Core application logic
  • Split APKs: Architecture-specific, density-specific, and language-specific modules
  • manifest.json: Metadata describing the package structure
  • OBB files (optional): Additional game data
The entire package is compressed into a .xapk file (actually a ZIP archive).
XAPK is an open format created by APKPure. It’s supported by multiple installers including SAI, APKPure App, and APKMirror Installer.

Why Split APKs Need XAPK

Split APKs cannot be installed directly because:
  1. Multiple files: Android Package Manager expects a single installable unit
  2. Dependencies: Split APKs have interdependencies that must be resolved
  3. Installation order: Base APK must install first, then splits
XAPK solves this by:
  • Bundling all APK files together
  • Providing metadata for installers
  • Ensuring correct installation order
You can detect Split APKs when pm path returns multiple package paths.

Compilation Process

APK Extractor compiles XAPK files through these steps:

1. Detect Split APK

const pathOut = await runAdb(`shell pm path ${pkg}`, serial);
const paths = pathOut.split('\n').map(l => l.replace('package:', '').trim()).filter(Boolean);

if (paths.length < 2) {
  // Not a Split APK, use regular APK extraction
  return;
}

2. Extract All APK Files in Parallel

const tmpDir = path.join(os.tmpdir(), `xapk_${pkg}_${Date.now()}`);
fs.mkdirSync(tmpDir, { recursive: true });

// Pull all APKs in parallel
const pullResults = await Promise.all(paths.map(async (p, idx) => {
  const fname = path.basename(p).replace(/\r/g, '');
  const localPath = path.join(tmpDir, fname || `split_${idx}.apk`);
  try {
    await runAdb(`pull "${p.trim()}" "${localPath}"`, serial, 120000);
    return { fname, ok: true };
  } catch (e) {
    return { fname, ok: false, error: e.message };
  }
}));
All APK files extract simultaneously using Promise.all for maximum speed.

3. Generate manifest.json

The manifest describes the XAPK package structure:
const dump = await tryRunAdb(`shell dumpsys package ${pkg}`, serial);
const extr = (re) => { const m = dump.match(re); return m ? m[1].trim() : '1'; };
const versionName = extr(/versionName=([^\s\n]+)/) || '1.0';
const versionCode = extr(/versionCode=(\d+)/) || '1';
const minSdk = extr(/minSdk=(\d+)/) || '21';

const pulledFiles = fs.readdirSync(tmpDir).filter(f => f.endsWith('.apk'));
const splitApks = pulledFiles.map(f => ({ 
  file: f, 
  id: f.replace('.apk', '') 
}));

const manifest = {
  xapk_version: 2,
  package_name: pkg,
  name: pkg,
  version_code: versionCode,
  version_name: versionName,
  min_sdk_version: minSdk,
  target_sdk_version: '34',
  split_apks: splitApks,
  expansions: []
};

fs.writeFileSync(
  path.join(tmpDir, 'manifest.json'), 
  JSON.stringify(manifest, null, 2), 
  'utf8'
);

4. Compress into ZIP Archive

const xapkPath = path.join(os.tmpdir(), `${pkg}.xapk`);
const zipPath = xapkPath + '.zip';

const zipCmd = `powershell -Command "Compress-Archive -Path '${tmpDir}\\*' -DestinationPath '${zipPath}' -Force"`;

exec(zipCmd, (err) => {
  if (err || !fs.existsSync(zipPath)) {
    // Error handling
    return;
  }
  
  // Rename .zip to .xapk
  fs.renameSync(zipPath, xapkPath);
  
  // Clean up temp directory
  fs.rmSync(tmpDir, { recursive: true });
  
  // Ready for download
});

manifest.json Structure

Here’s an example manifest for a Split APK:
{
  "xapk_version": 2,
  "package_name": "com.example.app",
  "name": "com.example.app",
  "version_code": "123456",
  "version_name": "2.5.0",
  "min_sdk_version": "21",
  "target_sdk_version": "34",
  "split_apks": [
    { "file": "base.apk", "id": "base" },
    { "file": "split_config.arm64_v8a.apk", "id": "split_config.arm64_v8a" },
    { "file": "split_config.xxhdpi.apk", "id": "split_config.xxhdpi" },
    { "file": "split_config.es.apk", "id": "split_config.es" }
  ],
  "expansions": []
}

Field Descriptions

FieldDescriptionExample
xapk_versionXAPK format version2
package_nameAndroid package identifiercom.example.app
nameDisplay name (usually same as package)com.example.app
version_codeInteger version code123456
version_nameHuman-readable version2.5.0
min_sdk_versionMinimum Android SDK21
target_sdk_versionTarget Android SDK34
split_apksArray of APK files in packageSee above
expansionsOBB files (usually empty)[]

Installing XAPK Files

XAPK files can be installed using:

SAI (Split APKs Installer)

Recommended installer - Open source, no ads, supports Android 5.0+

APKPure App

Official installer from APKPure (creator of XAPK format)
  • Download: apkpure.com
  • Features: Built-in app store, automatic updates

APKMirror Installer

Trusted installer from APKMirror team
adb install-multiple base.apk split_*.apk

Progress Tracking

The compilation process uses Server-Sent Events (SSE) for real-time progress updates:
app.get('/api/devices/:serial/apps/:package/compile-xapk', async (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  const send = (msg, progress = null) => {
    res.write(`data: ${JSON.stringify({ msg, progress })}\n\n`);
  };

  send('Obteniendo rutas del APK...', 5);
  // Extract paths
  
  send(`Encontrados ${paths.length} archivos APK...`, 10);
  // Pull APKs
  
  send('Obteniendo información del paquete...', 65);
  // Get version info
  
  send('Creando manifest.json...', 70);
  // Create manifest
  
  send('Empaquetando XAPK...', 80);
  // Compress archive
  
  send('XAPK listo. Descargando...', 95);
  // Trigger download
});

Progress Stages

ProgressStageDescription
5%Getting pathsQuery device for APK locations
10%ExtractingPull all APK files from device
10-60%File extractionProgress per file (parallel)
65%Package infoQuery version and SDK info
70%Manifest creationGenerate manifest.json
80%CompressionZIP all files
95%ReadyXAPK ready for download
The progress bar updates in real-time as each APK file finishes extracting.

Error Handling

Common Issues

If the app is not a Split APK:
if (paths.length < 2) {
  done(false, null, 'Esta app no es un Split APK. Usa "Extraer APK" en su lugar.');
  return;
}
Solution: Use the regular APK extraction feature instead.
If APK files cannot be pulled from device:
if (pulled === 0) {
  done(false, null, 'No se pudo extraer ningún APK del dispositivo.');
  return;
}
Solution: Check USB connection, restart ADB, or re-enable USB debugging.
If ZIP creation fails:
if (err || !fs.existsSync(zipPath)) {
  fs.rmSync(tmpDir, { recursive: true });
  done(false, null, 'Error al comprimir el XAPK: ' + err?.message);
  return;
}
Solution: Ensure sufficient disk space in temp directory.

Download API

After compilation, download the XAPK:
GET /api/devices/:serial/apps/:package/download-xapk
Response: Binary stream with headers:
Content-Disposition: attachment; filename="com.example.app.xapk"
Content-Type: application/octet-stream
Content-Length: 145678912
The file automatically deletes from the temp directory after download completes.

Performance Optimizations

  • Parallel extraction: All APK files pull simultaneously
  • Streaming download: Files stream directly without buffering
  • Auto cleanup: Temp files deleted immediately after use
  • Progress feedback: Real-time SSE updates prevent UI freezing
  • Timeout handling: 120s timeout per APK file for large apps

Build docs developers (and LLMs) love