Skip to content

feat(android): port iOS rsync optimisations to Android build pipeline#31

Open
benjam-es wants to merge 4 commits intoNativePHP:mainfrom
benjam-es:feature/android-installation-optimisation
Open

feat(android): port iOS rsync optimisations to Android build pipeline#31
benjam-es wants to merge 4 commits intoNativePHP:mainfrom
benjam-es:feature/android-installation-optimisation

Conversation

@benjam-es
Copy link

@benjam-es benjam-es commented Feb 22, 2026

Problem

The Android build pipeline in PreparesBuild.php and PlatformFileOperations.php has the same issues that were fixed for iOS in #30:

  1. Minimal exclude rules — Only 4–6 hardcoded excludes (.git, node_modules, nativephp/*, vendor/nativephp/mobile/resources), so rsync copies thousands of non-runtime files (tests, docs, dotfiles, YAML configs, LICENSE files) into the temp directory.

  2. Wrong vendor globvendor/*/vendor only matches one level deep, missing the actual Composer layout vendor/namespace/package/vendor. The separate vendor/nativephp/mobile/vendor line was a workaround for this bug.

  3. No .gitattributes support — Vendor packages that declare export-ignore patterns are fully copied, including their test suites and CI configs.

  4. Memory spike in ZIP stepaddDirectoryToZip() calls iterator_to_array() to materialise every file into a PHP array before iterating, consuming ~20 MB unnecessarily.

  5. Redundant hardcoded excludesaddDirectoryToZip() maintains its own list of 14 hardcoded excludes that partially overlap with rsync's excludes, making the filtering logic harder to maintain.

Solution

Port the iOS optimisations from #30 to the Android build:

  • getExcludedPaths() — shared method with comprehensive exclude list (dotfiles, tests, docs, vendor non-runtime files, project-level build artifacts), with platform-aware nativephp/ handling
  • loadVendorExportIgnorePatterns() — reads .gitattributes export-ignore from vendor packages
  • prepareLaravelBundle() — replaces the match (PHP_OS_FAMILY) block with getExcludedPaths() + loadVendorExportIgnorePatterns()
  • platformOptimizedCopy() — fixes vendor/*/vendorvendor/*/*/vendor, removes hardcoded appends (now provided by caller)
  • addDirectoryToZip() — streams iterator directly instead of materialising array; removes excludes now guaranteed absent after rsync (keeps safety-net excludes for files created between rsync and zip)

Benchmarks

Profiled on kitchen-sink-mobile-vue (Android build flow):

Rsync copy

Stock Patched
Exclude rules 10 50
Files copied 12,253 10,486
Copy size 131 MB 108 MB
Copy time 3,170 ms 2,552 ms

ZIP creation

Stock Patched
Files loaded into array 12,253 0 (streaming)
Files iterated 12,253 10,486
Files added to ZIP 12,253 10,486
ZIP time 5,750 ms 4,078 ms
ZIP size 48 MB 44 MB

Total

Stock Patched
Time (copy + zip) 9,093 ms 6,783 ms
Peak memory 20 MB 2 MB

Key takeaways:

  • 10x less memory (2 MB vs 20 MB) — removing iterator_to_array is the big win
  • 25% faster overall (6.8s vs 9.1s)
  • 4 MB smaller ZIP (tests, docs, dotfiles excluded)
  • Zero files excluded during ZIP in both cases — rsync is doing all the filtering, confirming the addDirectoryToZip() hardcoded excludes cleanup was safe

Test plan

  • Profiled on kitchen-sink-mobile-vue (Android build flow)
  • Verified final ZIP bundle contents match expected output
  • Confirmed composer install step still runs correctly in temp directory
  • Verified exclude list parity with iOS getExcludedPaths()
  • Tested platform-specific nativephp/ exclude logic (Windows vs Unix)

Replace RecursiveIteratorIterator with rsync -a --copy-links in
copyLaravelAppIntoIosApp() to eliminate unnecessary file copying and
prevent OOM crashes with symlinked local packages.

The old approach collected all files into a PHP array before copying,
consuming 60+ MB on standard projects and exceeding the 128 MB memory
limit when Composer path repositories introduced nested vendor dirs.
It also copied ~19,000 files (notably node_modules) that
removeUnnecessaryFiles() immediately deleted.

- Add getExcludedPaths() as single source of truth for rsync excludes
- Add loadVendorExportIgnorePatterns() to respect .gitattributes
- Add glob support in removeUnnecessaryFiles for wildcard dir patterns
- Exclude non-runtime files (*.md, LICENSE*, docs, CI configs)
- Fix vendor nested dir pattern (vendor/*/*/vendor vs vendor/*/vendor)
- Wrap copy step in components->task() for progress feedback
@benjam-es
Copy link
Author

This ports the same optimisations introduced in the iOS build in #30 — shared getExcludedPaths(), .gitattributes export-ignore support, fixed vendor glob pattern, and streaming the ZIP iterator instead of materialising it into memory.

The Android pipeline is a bit more involved since it needs to work across macOS, Linux, and Windows (robocopy vs rsync, different nativephp/ exclude logic per platform), so this might need more testing on those platforms to make sure nothing slips through. The core exclude list is identical to iOS, but the platform-conditional branches are worth a closer look.

@benjam-es benjam-es force-pushed the feature/android-installation-optimisation branch from 5ce4ddc to ad955fd Compare February 22, 2026 21:54
Move hardcoded exclusion lists, copy logic, and cleanup logic out of
BuildIosAppCommand into BundleExclusions (data) and BundleFileManager
(behaviour). This makes the lists testable, maintainable, and reusable
across platforms.
- Add shared getExcludedPaths() with comprehensive exclude list matching iOS
- Add loadVendorExportIgnorePatterns() for .gitattributes export-ignore support
- Fix vendor glob pattern from vendor/*/vendor to vendor/*/*/vendor
- Replace platform-specific match block with unified getExcludedPaths() call
- Stream ZIP iterator directly instead of materialising via iterator_to_array()
- Remove redundant addDirectoryToZip() excludes now handled by rsync
…ileManager

Remove getExcludedPaths(), loadVendorExportIgnorePatterns(), and
platformOptimizedCopy() that duplicated the extracted BundleExclusions
and BundleFileManager classes. Android bundle prep now uses the same
copy/cleanup pipeline as iOS.
@benjam-es
Copy link
Author

Manual profiling: kitchen-sink-mobile-vue

Tested on real device build (native:run android) against the kitchen-sink-vue app (124 MB vendor, 14 plugins).

Build step timing

Step v3.0.4 (original) Optimised Change
Copying Laravel source 3s 2s -1s
Removing unnecessary files 4.56ms new step
Creating bundle archive 4s 3s -1s
Bundle size 44.79 MB 28.97 MB -15.82 MB (35%)

What the 15.8 MB reduction is made of

Breakdown of the 1,794 files (34 MB pre-compression) now excluded from the bundle:

Category Files Size
vendor/laravel/pint/builds/ 1 14.88 MB
vendor/nativephp/mobile/resources/ (Xcode/AS templates, .h, .swift, .kt) 372 12.92 MB
vendor/*/tests/ 806 3.88 MB
vendor/*/*.md (README, CHANGELOG, etc) 313 1.30 MB
Root files (*.js, *.md, *.xml, *.lock) 4 0.41 MB
vendor/*/docs/ 58 0.18 MB
vendor/*/LICENSE* 121 0.17 MB
nativephp/ (build output dirs) 4 0.15 MB
vendor/*/.github/ 20 0.04 MB
vendor/*/*.neon 5 0.03 MB
tests/ (project) 15 0.02 MB
Dot files, storage, yml, artisan, etc 75 0.02 MB
Total 1,794 34.08 MB

Profiling script results (3-run average)

Metric v3.0.4 Optimised Change
Total time (copy + zip) 7.02s 6.01s -1.01s (14%)
Peak PHP memory 42.0 MB 24.0 MB -18 MB (43%)
Files before zip 12,250 10,462 -1,788
Pre-zip size 99.2 MB 65.1 MB -34.1 MB
Final zip size 44.8 MB 29.0 MB -15.8 MB (35%)

@benjam-es
Copy link
Author

Safety of removed files

All excluded content is non-runtime:

Category Why safe to exclude
vendor/laravel/pint/builds/ (14.9 MB) Standalone Pint binary — CLI formatting tool, never used on device
vendor/nativephp/mobile/resources/ (12.9 MB) Xcode/Android Studio project templates — used by native:install to scaffold the native project, not at runtime
vendor/*/tests/ (3.9 MB) Package test suites — never executed on device
*.md, LICENSE*, docs/, .github/ Documentation and repo metadata
*.yml, *.yaml, *.neon CI configs, PHPStan configs — not loaded at runtime
*.lock, artisan Copied for composer install in the temp dir, then cleaned up after. Android's artisan.php bootstrap has a different filename and survives cleanup
Dot files (.gitignore, .editorconfig, .DS_Store) SCM/editor config

The cleanup step (BundleFileManager::removeUnnecessaryFiles) runs after composer install and autoloader optimisation, so files needed by Composer (like composer.lock and artisan) are available during install and only removed from the final bundle.

@benjam-es benjam-es force-pushed the feature/android-installation-optimisation branch from ad955fd to 105385a Compare February 23, 2026 20:34
@benjam-es
Copy link
Author

Depends on #30 — this PR is rebased on feature/ios-build-rsync-copy and reuses the BundleFileManager / BundleExclusions classes introduced there. Merge #30 first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants