Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/Support/BundleExclusions.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ class BundleExclusions
'*.neon.dist',
];

/** Directories the Android runtime expects to exist (added as empty dirs in zip). */
public const ANDROID_REQUIRED_DIRS = [
'bootstrap/cache',
'storage/framework/cache',
'storage/framework/sessions',
'storage/framework/views',
];

/** Specific vendor paths to exclude. */
public const VENDOR_PATHS = [
'vendor/nativephp/mobile/resources',
Expand Down
45 changes: 33 additions & 12 deletions src/Support/BundleFileManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,31 +61,52 @@ public static function copy(string $source, string $destination, array $configPa
File::ensureDirectoryExists($destination);
File::cleanDirectory($destination);

$excludes = self::excludes($configPaths, $source);

if (PHP_OS_FAMILY === 'Windows') {
self::copyWithRobocopy($source, $destination, $configPaths);
self::copyWithRobocopy($source, $destination, $excludes);
} else {
self::copyWithRsync($source, $destination, $configPaths);
self::copyWithRsync($source, $destination, $excludes);
}
}

private static function copyWithRsync(string $source, string $destination, array $configPaths): void
/**
* Copy a source directory to a destination without applying any exclusions.
* Useful for copying templates and binary artifacts that should be transferred as-is.
*/
public static function copyRaw(string $source, string $destination): void
{
$excludes = self::excludes($configPaths, $source);
$excludeFlags = implode(' ', array_map(fn ($d) => "--exclude='".str_replace("'", "'\\''", $d)."'", $excludes));
$source = rtrim($source, '/');
$destination = rtrim($destination, '/');

$result = Process::run("rsync -a --copy-links {$excludeFlags} \"{$source}/\" \"{$destination}/\"");
File::ensureDirectoryExists($destination);

if (! $result->successful()) {
throw new \Exception('Failed to copy app bundle: '.$result->errorOutput());
if (PHP_OS_FAMILY === 'Windows') {
self::copyWithRobocopy($source, $destination);
} else {
self::copyWithRsync($source, $destination);
}
}

private static function copyWithRobocopy(string $source, string $destination, array $configPaths): void
private static function copyWithRsync(string $source, string $destination, array $excludes = []): void
{
$excludes = self::excludes($configPaths, $source);
$excludeFlags = '';

if (! empty($excludes)) {
$excludeFlags = implode(' ', array_map(fn ($d) => "--exclude='".str_replace("'", "'\\''", $d)."'", $excludes)).' ';
}

$result = Process::run("rsync -a --copy-links {$excludeFlags}\"{$source}/\" \"{$destination}/\"");

if (! $result->successful()) {
throw new \Exception('Failed to copy directory: '.$result->errorOutput());
}
}

// Robocopy uses /XD for directories with absolute paths
private static function copyWithRobocopy(string $source, string $destination, array $excludes = []): void
{
$excludeArgs = '';

foreach ($excludes as $pattern) {
$dir = ltrim($pattern, '/\\');
$dir = str_replace('/', '\\', $dir);
Expand All @@ -96,7 +117,7 @@ private static function copyWithRobocopy(string $source, string $destination, ar

// Robocopy exit codes < 8 are success
if ($result->exitCode() >= 8) {
throw new \Exception('Failed to copy app bundle (robocopy exit code '.$result->exitCode().')');
throw new \Exception('Failed to copy directory (robocopy exit code '.$result->exitCode().')');
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/Traits/InstallsAndroid.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\File;
use Native\Mobile\Support\BundleFileManager;
use ZipArchive;

use function Laravel\Prompts\error;
Expand Down Expand Up @@ -53,7 +54,7 @@ private function createAndroidStudioProject(): void

$source = base_path('vendor/nativephp/mobile/resources/androidstudio');

$this->components->task('Creating Android project', fn () => $this->platformOptimizedCopy($source, $androidPath));
$this->components->task('Creating Android project', fn () => BundleFileManager::copyRaw($source, $androidPath));
}

private function installPHPAndroid(): void
Expand Down Expand Up @@ -197,7 +198,7 @@ private function installPHPAndroid(): void
$destination = base_path('nativephp/android/app/src/main');
File::ensureDirectoryExists($destination);

$this->components->task('Installing Android libraries', fn () => $this->platformOptimizedCopy($extractPath, $destination));
$this->components->task('Installing Android libraries', fn () => BundleFileManager::copyRaw($extractPath, $destination));

try {
$this->removeDirectory($extractPath);
Expand Down
9 changes: 5 additions & 4 deletions src/Traits/InstallsIos.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Process;
use Native\Mobile\Support\BundleFileManager;
use ZipArchive;

use function Laravel\Prompts\error;
Expand Down Expand Up @@ -49,7 +50,7 @@ private function createXcodeProject(): void
mkdir($this->iosPath, 0755, true);
}

$this->components->task('Creating Xcode project', fn () => File::copyDirectory(
$this->components->task('Creating Xcode project', fn () => BundleFileManager::copyRaw(
base_path('vendor/nativephp/mobile/resources/xcode'),
$this->iosPath
));
Expand Down Expand Up @@ -164,16 +165,16 @@ private function installPHPIos(): void
File::ensureDirectoryExists($this->iosPath);

$this->components->task('Installing iOS libraries', function () use ($extractPath) {
File::copyDirectory($extractPath.'/Libraries', $this->iosPath.'/Libraries');
File::copyDirectory($extractPath.'/Include', $this->iosPath.'/Include');
BundleFileManager::copyRaw($extractPath.'/Libraries', $this->iosPath.'/Libraries');
BundleFileManager::copyRaw($extractPath.'/Include', $this->iosPath.'/Include');
});

// Re-copy our custom Bridge files (PHP.c, PHP.h) which contain the persistent
// runtime implementation — the binary ZIP ships older/template versions
$bridgeSrc = __DIR__.'/../../resources/xcode/Include/Bridge';
$bridgeDst = $this->iosPath.'/Include/Bridge';
if (is_dir($bridgeSrc)) {
File::copyDirectory($bridgeSrc, $bridgeDst);
BundleFileManager::copyRaw($bridgeSrc, $bridgeDst);
}

try {
Expand Down
38 changes: 0 additions & 38 deletions src/Traits/PlatformFileOperations.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,6 @@

trait PlatformFileOperations
{
/**
* Platform-optimized file copy operation
*/
protected function platformOptimizedCopy(string $source, string $destination, array $excludedDirs = []): void
{
if (PHP_OS_FAMILY === 'Windows') {
// Use robocopy on Windows
if (! empty($excludedDirs)) {
$excludeArgs = '';
foreach ($excludedDirs as $dir) {
$excludeArgs .= " /XD \"{$source}\\{$dir}\"";
}
$cmd = "robocopy \"{$source}\" \"{$destination}\" /MIR /NFL /NDL /NJH /NJS /NP /R:0 /W:0{$excludeArgs}";
} else {
$cmd = "xcopy \"{$source}\\*\" \"{$destination}\\\" /E /I /Y /Q";
}

exec($cmd, $output, $result);

// Robocopy returns 0-7 as success codes
if ($result >= 8 && strpos($cmd, 'robocopy') !== false) {
$this->components->warn("robocopy failed with exit code $result");
}
} else {
// Use rsync on Unix-like systems
if (! empty($excludedDirs)) {
// Add specific exclusions for nested vendor directories that cause rsync cycles
$excludedDirs[] = 'vendor/*/vendor';
$excludedDirs[] = 'vendor/nativephp/mobile/vendor';
$excludeFlags = implode(' ', array_map(fn ($d) => "--exclude='{$d}'", $excludedDirs));
$cmd = "rsync -aL {$excludeFlags} \"{$source}/\" \"{$destination}/\"";
} else {
$cmd = "cp -a \"{$source}/.\" \"{$destination}/\"";
}
exec($cmd);
}
}

/**
* Platform-optimized directory removal
*/
Expand Down
75 changes: 17 additions & 58 deletions src/Traits/PreparesBuild.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
use Native\Mobile\Support\BundleExclusions;
use Native\Mobile\Support\BundleFileManager;
use Symfony\Component\Process\Process as SymfonyProcess;

trait PreparesBuild
Expand Down Expand Up @@ -225,19 +227,12 @@ protected function prepareLaravelBundle(bool $excludeDevDependencies = true): vo
unlink($destinationZip);
}

$excludedDirs = match (PHP_OS_FAMILY) {
'Windows' => array_merge(config('nativephp.cleanup_exclude_files'), ['.git', 'node_modules', 'nativephp', 'vendor/nativephp/mobile/resources']),
'Linux' => array_merge(config('nativephp.cleanup_exclude_files'), ['.git', 'node_modules', 'nativephp/ios', 'nativephp/android']),
'Darwin' => array_merge(config('nativephp.cleanup_exclude_files'), ['.git', 'node_modules', 'nativephp/ios', 'nativephp/android']),
default => config('nativephp.cleanup_exclude_files'),
};

$this->logToFile(' Excluded directories: '.implode(', ', $excludedDirs));
$configExcludes = config('nativephp.cleanup_exclude_files', []);

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

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

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

Expand Down Expand Up @@ -269,6 +264,9 @@ protected function prepareLaravelBundle(bool $excludeDevDependencies = true): vo
return $result->successful();
});

$this->logToFile(' Removing unnecessary files...');
$this->components->task('Removing unnecessary files', fn () => BundleFileManager::removeUnnecessaryFiles($tempDir, $configExcludes));

$version = config('nativephp.version', now()->format('Ymd-His'));
$versionCode = config('nativephp.version_code', 1);
$bundleVersionId = $version === 'DEBUG' ? 'DEBUG' : "{$version}b{$versionCode}";
Expand All @@ -289,7 +287,7 @@ protected function prepareLaravelBundle(bool $excludeDevDependencies = true): vo
}

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

if (! file_exists($destinationZip) || filesize($destinationZip) <= 1000) {
$this->logToFile('ERROR: Failed to create valid zip file');
Expand Down Expand Up @@ -328,7 +326,7 @@ protected function prepareLaravelBundle(bool $excludeDevDependencies = true): vo
/**
* Create ZIP bundle with cross-platform support
*/
protected function createZipBundle(string $source, string $destination, array $excludedDirs = []): void
protected function createZipBundle(string $source, string $destination): void
{
if (PHP_OS_FAMILY === 'Windows') {
$sevenZip = config('nativephp.android.7zip-location');
Expand All @@ -355,16 +353,9 @@ protected function createZipBundle(string $source, string $destination, array $e
exit(1);
}

$this->addDirectoryToZip($zip, $source, '', $excludedDirs);
$this->addDirectoryToZip($zip, $source);

$requiredDirs = [
'bootstrap/cache',
'storage/framework/cache',
'storage/framework/sessions',
'storage/framework/views',
];

foreach ($requiredDirs as $dir) {
foreach (BundleExclusions::ANDROID_REQUIRED_DIRS as $dir) {
if (! $zip->statName($dir)) {
$zip->addEmptyDir($dir);
}
Expand All @@ -380,52 +371,22 @@ protected function createZipBundle(string $source, string $destination, array $e
/**
* Add directory contents to ZIP archive
*/
protected function addDirectoryToZip(\ZipArchive $zip, string $source, string $prefix = '', array $excludedDirs = []): void
protected function addDirectoryToZip(\ZipArchive $zip, string $source, string $prefix = ''): void
{
$source = rtrim(str_replace('\\', '/', $source), '/').'/';

$files = iterator_to_array(new \RecursiveIteratorIterator(
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::LEAVES_ONLY
));
);

foreach ($files as $file) {
foreach ($iterator as $file) {
$filePath = str_replace('\\', '/', $file->getRealPath());
$relativePath = ltrim(str_replace('\\', '/', substr($filePath, strlen($source))), '/');

// Check against configured exclusions first
$shouldExclude = false;
foreach ($excludedDirs as $excludedDir) {
// Handle wildcard patterns (e.g., "public/fonts/*")
if (str_contains($excludedDir, '*')) {
$pattern = str_replace('*', '.*', preg_quote($excludedDir, '/'));
if (preg_match('/^'.$pattern.'/', $relativePath)) {
$shouldExclude = true;
break;
}
} else {
// Exact directory matching
if (Str::startsWith($relativePath, rtrim($excludedDir, '/').'/') || $relativePath === rtrim($excludedDir, '/')) {
$shouldExclude = true;
break;
}
}
}

// Always exclude these directories
if ($shouldExclude ||
Str::startsWith($relativePath, 'vendor/nativephp/mobile/resources') ||
Str::startsWith($relativePath, 'vendor/nativephp/mobile/vendor') ||
Str::startsWith($relativePath, 'vendor/endroid') ||
// Safety net for files that may be created between copy/cleanup and zip
if (Str::startsWith($relativePath, 'bootstrap/cache/') ||
Str::startsWith($relativePath, '.idea') ||
Str::startsWith($relativePath, 'output') ||
Str::startsWith($relativePath, 'storage/framework/views/') ||
Str::startsWith($relativePath, 'storage/framework/cache/') ||
Str::startsWith($relativePath, 'storage/framework/sessions/') ||
Str::startsWith($relativePath, 'storage/app/native-build') ||
Str::startsWith($relativePath, 'bootstrap/cache/') ||
Str::startsWith($relativePath, 'nativephp') ||
Str::startsWith($relativePath, 'public/storage') ||
Str::endsWith($relativePath, '.jks') ||
Str::endsWith($relativePath, '.zip')) {
continue;
Expand Down Expand Up @@ -905,6 +866,4 @@ abstract protected function updateIcuConfiguration(): void;
abstract protected function updateFirebaseConfiguration(): void;

abstract protected function removeDirectory(string $path): void;

abstract protected function platformOptimizedCopy(string $source, string $destination, array $excludedDirs): void;
}
Loading
Loading