Skip to content

Commit 105385a

Browse files
committed
refactor(android): replace duplicated build logic with shared BundleFileManager
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.
1 parent d5f4bf2 commit 105385a

4 files changed

Lines changed: 247 additions & 209 deletions

File tree

src/Support/BundleExclusions.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ class BundleExclusions
6161
'*.neon.dist',
6262
];
6363

64+
/** Directories the Android runtime expects to exist (added as empty dirs in zip). */
65+
public const ANDROID_REQUIRED_DIRS = [
66+
'bootstrap/cache',
67+
'storage/framework/cache',
68+
'storage/framework/sessions',
69+
'storage/framework/views',
70+
];
71+
6472
/** Specific vendor paths to exclude. */
6573
public const VENDOR_PATHS = [
6674
'vendor/nativephp/mobile/resources',

src/Traits/PlatformFileOperations.php

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,41 +6,6 @@
66

77
trait PlatformFileOperations
88
{
9-
/**
10-
* Platform-optimized file copy operation
11-
*/
12-
protected function platformOptimizedCopy(string $source, string $destination, array $excludedDirs = []): void
13-
{
14-
if (PHP_OS_FAMILY === 'Windows') {
15-
// Use robocopy on Windows
16-
if (! empty($excludedDirs)) {
17-
$excludeArgs = '';
18-
foreach ($excludedDirs as $dir) {
19-
$excludeArgs .= " /XD \"{$source}\\{$dir}\"";
20-
}
21-
$cmd = "robocopy \"{$source}\" \"{$destination}\" /MIR /NFL /NDL /NJH /NJS /NP /R:0 /W:0{$excludeArgs}";
22-
} else {
23-
$cmd = "xcopy \"{$source}\\*\" \"{$destination}\\\" /E /I /Y /Q";
24-
}
25-
26-
exec($cmd, $output, $result);
27-
28-
// Robocopy returns 0-7 as success codes
29-
if ($result >= 8 && strpos($cmd, 'robocopy') !== false) {
30-
$this->components->warn("robocopy failed with exit code $result");
31-
}
32-
} else {
33-
// Use rsync on Unix-like systems
34-
if (! empty($excludedDirs)) {
35-
$excludeFlags = implode(' ', array_map(fn ($d) => "--exclude='{$d}'", $excludedDirs));
36-
$cmd = "rsync -aL {$excludeFlags} \"{$source}/\" \"{$destination}/\"";
37-
} else {
38-
$cmd = "cp -a \"{$source}/.\" \"{$destination}/\"";
39-
}
40-
exec($cmd);
41-
}
42-
}
43-
449
/**
4510
* Platform-optimized directory removal
4611
*/

src/Traits/PreparesBuild.php

Lines changed: 14 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
use Illuminate\Support\Facades\File;
66
use Illuminate\Support\Facades\Process;
77
use Illuminate\Support\Str;
8+
use Native\Mobile\Support\BundleExclusions;
9+
use Native\Mobile\Support\BundleFileManager;
810
use Symfony\Component\Process\Process as SymfonyProcess;
911

1012
trait PreparesBuild
@@ -225,21 +227,12 @@ protected function prepareLaravelBundle(bool $excludeDevDependencies = true): vo
225227
unlink($destinationZip);
226228
}
227229

228-
$excludedDirs = $this->getExcludedPaths();
229-
230-
// Respect export-ignore from vendor package .gitattributes files
231-
foreach ($this->loadVendorExportIgnorePatterns($source) as $prefix => $patterns) {
232-
foreach ($patterns as $pattern) {
233-
$excludedDirs[] = $prefix.ltrim($pattern, '/');
234-
}
235-
}
236-
237-
$this->logToFile(' Excluded directories: '.implode(', ', $excludedDirs));
230+
$configExcludes = config('nativephp.cleanup_exclude_files', []);
238231

239232
$srcDir = base_path('vendor/nativephp/mobile/bootstrap/android');
240233

241234
$this->logToFile(' Copying Laravel source...');
242-
$this->components->task('Copying Laravel source', fn () => $this->platformOptimizedCopy($source, $tempDir, $excludedDirs));
235+
$this->components->task('Copying Laravel source', fn () => BundleFileManager::copy($source, $tempDir, $configExcludes));
243236

244237
$composerArgs = $excludeDevDependencies ? '--no-dev --no-interaction' : '--no-interaction';
245238

@@ -271,6 +264,9 @@ protected function prepareLaravelBundle(bool $excludeDevDependencies = true): vo
271264
return $result->successful();
272265
});
273266

267+
$this->logToFile(' Removing unnecessary files...');
268+
$this->components->task('Removing unnecessary files', fn () => BundleFileManager::removeUnnecessaryFiles($tempDir, $configExcludes));
269+
274270
$version = config('nativephp.version', now()->format('Ymd-His'));
275271
$this->logToFile(" Writing version file: $version");
276272
file_put_contents($tempDir.DIRECTORY_SEPARATOR.'.version', $version.PHP_EOL);
@@ -289,7 +285,7 @@ protected function prepareLaravelBundle(bool $excludeDevDependencies = true): vo
289285
}
290286

291287
$this->logToFile(' Creating bundle archive...');
292-
$this->components->task('Creating bundle archive', fn () => $this->createZipBundle($tempDir, $destinationZip, $excludedDirs));
288+
$this->components->task('Creating bundle archive', fn () => $this->createZipBundle($tempDir, $destinationZip));
293289

294290
if (! file_exists($destinationZip) || filesize($destinationZip) <= 1000) {
295291
$this->logToFile('ERROR: Failed to create valid zip file');
@@ -306,133 +302,10 @@ protected function prepareLaravelBundle(bool $excludeDevDependencies = true): vo
306302
}
307303
}
308304

309-
/**
310-
* Paths to exclude from the app bundle.
311-
*
312-
* Patterns without a leading / match at any depth (e.g. inside vendor packages).
313-
* Patterns with a leading / are anchored to the project root.
314-
* Used by rsync during the initial copy step.
315-
*/
316-
protected function getExcludedPaths(): array
317-
{
318-
// Any depth (project root + inside vendor packages)
319-
$excludes = [
320-
'.git',
321-
'.github',
322-
'node_modules',
323-
'tests',
324-
'.DS_Store',
325-
'.gitignore',
326-
'.gitattributes',
327-
'.gitkeep',
328-
'.editorconfig',
329-
330-
// Non-runtime files inside vendor packages
331-
'*.md',
332-
'LICENSE*',
333-
'docs',
334-
'*.yml',
335-
'*.yaml',
336-
'*.neon',
337-
'*.neon.dist',
338-
339-
// Vendor-specific
340-
'vendor/nativephp/mobile/resources',
341-
'vendor/*/*/vendor',
342-
'vendor/livewire/livewire/src/Features/SupportFileUploads/browser_test_image_big.jpg',
343-
];
344-
345-
// Platform-specific project-level excludes
346-
if (PHP_OS_FAMILY === 'Windows') {
347-
$excludes[] = '/nativephp';
348-
} else {
349-
$excludes[] = '/nativephp/ios';
350-
$excludes[] = '/nativephp/android';
351-
}
352-
353-
// Project-level directories
354-
$excludes = array_merge($excludes, [
355-
'/output',
356-
'/build',
357-
'/dist',
358-
'/artifacts',
359-
'/storage/logs',
360-
'/storage/framework',
361-
'/public/storage',
362-
]);
363-
364-
// Project-level files
365-
$excludes = array_merge($excludes, [
366-
'/*.js',
367-
'/*.md',
368-
'/*.lock',
369-
'/*.xml',
370-
'/.env.example',
371-
'/artisan',
372-
]);
373-
374-
// User-configured exclusions
375-
$excludes = array_merge($excludes, config('nativephp.cleanup_exclude_files', []));
376-
377-
return $excludes;
378-
}
379-
380-
/**
381-
* Load export-ignore patterns from .gitattributes in vendor packages.
382-
*
383-
* @return array<string, string[]>
384-
*/
385-
protected function loadVendorExportIgnorePatterns(string $source): array
386-
{
387-
$patterns = [];
388-
$vendorPath = $source.'/vendor/';
389-
390-
if (! is_dir($vendorPath)) {
391-
return $patterns;
392-
}
393-
394-
foreach (new \DirectoryIterator($vendorPath) as $namespace) {
395-
if ($namespace->isDot() || ! $namespace->isDir()) {
396-
continue;
397-
}
398-
399-
foreach (new \DirectoryIterator($namespace->getPathname()) as $package) {
400-
if ($package->isDot() || ! $package->isDir()) {
401-
continue;
402-
}
403-
404-
$gitattributes = $package->getPathname().'/.gitattributes';
405-
if (! file_exists($gitattributes)) {
406-
continue;
407-
}
408-
409-
$ignores = [];
410-
foreach (file($gitattributes, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
411-
$line = trim($line);
412-
if ($line === '' || $line[0] === '#' || ! str_contains($line, 'export-ignore')) {
413-
continue;
414-
}
415-
416-
$path = trim(preg_split('/\s+/', $line, 2)[0] ?? '');
417-
if ($path !== '') {
418-
$ignores[] = ltrim($path, '/');
419-
}
420-
}
421-
422-
if (! empty($ignores)) {
423-
$prefix = 'vendor/'.$namespace->getFilename().'/'.$package->getFilename().'/';
424-
$patterns[$prefix] = $ignores;
425-
}
426-
}
427-
}
428-
429-
return $patterns;
430-
}
431-
432305
/**
433306
* Create ZIP bundle with cross-platform support
434307
*/
435-
protected function createZipBundle(string $source, string $destination, array $excludedDirs = []): void
308+
protected function createZipBundle(string $source, string $destination): void
436309
{
437310
if (PHP_OS_FAMILY === 'Windows') {
438311
$sevenZip = config('nativephp.android.7zip-location');
@@ -459,16 +332,9 @@ protected function createZipBundle(string $source, string $destination, array $e
459332
exit(1);
460333
}
461334

462-
$this->addDirectoryToZip($zip, $source, '', $excludedDirs);
463-
464-
$requiredDirs = [
465-
'bootstrap/cache',
466-
'storage/framework/cache',
467-
'storage/framework/sessions',
468-
'storage/framework/views',
469-
];
335+
$this->addDirectoryToZip($zip, $source);
470336

471-
foreach ($requiredDirs as $dir) {
337+
foreach (BundleExclusions::ANDROID_REQUIRED_DIRS as $dir) {
472338
if (! $zip->statName($dir)) {
473339
$zip->addEmptyDir($dir);
474340
}
@@ -484,7 +350,7 @@ protected function createZipBundle(string $source, string $destination, array $e
484350
/**
485351
* Add directory contents to ZIP archive
486352
*/
487-
protected function addDirectoryToZip(\ZipArchive $zip, string $source, string $prefix = '', array $excludedDirs = []): void
353+
protected function addDirectoryToZip(\ZipArchive $zip, string $source, string $prefix = ''): void
488354
{
489355
$source = rtrim(str_replace('\\', '/', $source), '/').'/';
490356

@@ -497,32 +363,8 @@ protected function addDirectoryToZip(\ZipArchive $zip, string $source, string $p
497363
$filePath = str_replace('\\', '/', $file->getRealPath());
498364
$relativePath = ltrim(str_replace('\\', '/', substr($filePath, strlen($source))), '/');
499365

500-
// Check against configured exclusions first
501-
$shouldExclude = false;
502-
foreach ($excludedDirs as $excludedDir) {
503-
// Handle wildcard patterns (e.g., "public/fonts/*")
504-
if (str_contains($excludedDir, '*')) {
505-
$pattern = str_replace('*', '.*', preg_quote($excludedDir, '/'));
506-
if (preg_match('/^'.$pattern.'/', $relativePath)) {
507-
$shouldExclude = true;
508-
break;
509-
}
510-
} else {
511-
// Exact directory matching
512-
if (Str::startsWith($relativePath, rtrim($excludedDir, '/').'/') || $relativePath === rtrim($excludedDir, '/')) {
513-
$shouldExclude = true;
514-
break;
515-
}
516-
}
517-
}
518-
519-
// Safety net for files that may be created between rsync and zip
520-
if ($shouldExclude ||
521-
Str::startsWith($relativePath, 'storage/framework/views/') ||
522-
Str::startsWith($relativePath, 'storage/framework/cache/') ||
523-
Str::startsWith($relativePath, 'storage/framework/sessions/') ||
524-
Str::startsWith($relativePath, 'storage/app/native-build') ||
525-
Str::startsWith($relativePath, 'bootstrap/cache/') ||
366+
// Safety net for files that may be created between copy/cleanup and zip
367+
if (Str::startsWith($relativePath, 'bootstrap/cache/') ||
526368
Str::startsWith($relativePath, '.idea') ||
527369
Str::endsWith($relativePath, '.jks') ||
528370
Str::endsWith($relativePath, '.zip')) {
@@ -990,6 +832,4 @@ abstract protected function updateIcuConfiguration(): void;
990832
abstract protected function updateFirebaseConfiguration(): void;
991833

992834
abstract protected function removeDirectory(string $path): void;
993-
994-
abstract protected function platformOptimizedCopy(string $source, string $destination, array $excludedDirs): void;
995835
}

0 commit comments

Comments
 (0)