From 18f78a446b3baf378b00375e3998ab06576ea8a9 Mon Sep 17 00:00:00 2001 From: Tawana Musewe <4852702+tbtmuse@users.noreply.github.com> Date: Sun, 15 Feb 2026 13:00:30 +0200 Subject: [PATCH 1/2] fix: iOS build now respects cleanup_exclude_files config BuildIosAppCommand::copyLaravelAppIntoIosApp() had a bespoke RecursiveIteratorIterator with hardcoded exclusions that ignored the user-configurable nativephp.cleanup_exclude_files setting. Replace with the shared PlatformFileOperations::platformOptimizedCopy() trait already used by InstallCommand, PackageCommand, and RunCommand. This also merges config('nativephp.cleanup_exclude_files') into the exclusion list, matching the pattern in PreparesBuild for Android. --- src/Commands/BuildIosAppCommand.php | 72 +++++++++-------------------- 1 file changed, 21 insertions(+), 51 deletions(-) diff --git a/src/Commands/BuildIosAppCommand.php b/src/Commands/BuildIosAppCommand.php index 1b3ba4c..c3773aa 100644 --- a/src/Commands/BuildIosAppCommand.php +++ b/src/Commands/BuildIosAppCommand.php @@ -5,7 +5,6 @@ use Illuminate\Console\Command; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Process; -use Illuminate\Support\Str; use Native\Mobile\Plugins\Compilers\IOSPluginCompiler; use Native\Mobile\Plugins\PluginHookRunner; use Native\Mobile\Plugins\PluginRegistry; @@ -15,13 +14,14 @@ use Native\Mobile\Traits\DisplaysMarketingBanners; use Native\Mobile\Traits\InstallsAppIcon; use Native\Mobile\Traits\InstallsSplashScreen; +use Native\Mobile\Traits\PlatformFileOperations; use Native\Mobile\Traits\ValidatesAppConfig; use function Laravel\Prompts\error; class BuildIosAppCommand extends Command { - use ChecksLatestBuildNumber, CleansEnvFile, DisplaysMarketingBanners, InstallsAppIcon, InstallsSplashScreen, ValidatesAppConfig; + use ChecksLatestBuildNumber, CleansEnvFile, DisplaysMarketingBanners, InstallsAppIcon, InstallsSplashScreen, PlatformFileOperations, ValidatesAppConfig; private bool $verbose; @@ -144,58 +144,29 @@ private function copyLaravelAppIntoIosApp() { $destination = $this->appPath; - // Make sure we clear out any old version shell_exec("rm -rf {$destination}/*"); - $source = rtrim(str_replace('\\', '/', base_path()), '/').'/'; - - $visitedRealPaths = []; - $files = []; - - $iterator = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator( - $source, - \RecursiveDirectoryIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS - ), - \RecursiveIteratorIterator::LEAVES_ONLY + $source = base_path(); + + $excludedDirs = array_merge( + config('nativephp.cleanup_exclude_files', []), + [ + 'vendor/nativephp/mobile/resources', + 'vendor/nativephp/mobile/vendor', + 'vendor/', + 'node_modules/', + 'nativephp/', + 'output/', + 'build/', + 'dist/', + 'artifacts/', + '.git/', + 'storage/logs/', + 'storage/framework/cache/', + ] ); - foreach ($iterator as $file) { - $realPath = $file->getRealPath(); - - // Skip if we've already visited this real path (prevents infinite loops from circular symlinks) - if ($realPath === false || isset($visitedRealPaths[$realPath])) { - continue; - } - - $visitedRealPaths[$realPath] = true; - $files[] = $file; - } - - foreach ($files as $file) { - // Where the *link* lives (keeps relative paths correct) - $logicalPath = str_replace('\\', '/', $file->getPathname()); - // Where the link **points** (or the same file if not a link) - $realPath = str_replace('\\', '/', $file->getRealPath()); - - $relativePath = ltrim(substr($logicalPath, strlen($source)), '/'); - - if (Str::startsWith($relativePath, 'vendor/nativephp/mobile/resources') || - Str::startsWith($relativePath, 'vendor/nativephp/mobile/vendor') || - Str::startsWith($relativePath, 'nativephp') || - Str::startsWith($relativePath, 'output/') || - Str::startsWith($relativePath, 'build/') || - Str::startsWith($relativePath, 'dist/') || - Str::startsWith($relativePath, 'artifacts/') || - Str::startsWith($relativePath, '.git/') || - Str::startsWith($relativePath, 'storage/logs/') || - Str::startsWith($relativePath, 'storage/framework/cache/')) { - continue; - } - - @File::makeDirectory(dirname($destination.$relativePath), recursive: true, force: true); - @File::copy($realPath, $destination.$relativePath); - } + $this->platformOptimizedCopy($source, $destination, $excludedDirs); } private function updateAppVersion(): void @@ -794,7 +765,6 @@ private function installGoogleServicesPlist(): void private function removeUnnecessaryFiles(): void { - $directoriesToRemove = [ '.git', '.github', From 677a22bbaf0e450ffbe07bff80a046daf3c0e1ee Mon Sep 17 00:00:00 2001 From: Tawana Musewe <4852702+tbtmuse@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:22:31 +0200 Subject: [PATCH 2/2] Add tests for iOS build exclusions - Add IosBuildCopyTest --- src/Commands/BuildIosAppCommand.php | 2 +- tests/Feature/IosBuildCopyTest.php | 155 ++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/IosBuildCopyTest.php diff --git a/src/Commands/BuildIosAppCommand.php b/src/Commands/BuildIosAppCommand.php index c3773aa..f22de31 100644 --- a/src/Commands/BuildIosAppCommand.php +++ b/src/Commands/BuildIosAppCommand.php @@ -149,7 +149,7 @@ private function copyLaravelAppIntoIosApp() $source = base_path(); $excludedDirs = array_merge( - config('nativephp.cleanup_exclude_files', []), + config('nativephp.cleanup_exclude_files') ?? [], [ 'vendor/nativephp/mobile/resources', 'vendor/nativephp/mobile/vendor', diff --git a/tests/Feature/IosBuildCopyTest.php b/tests/Feature/IosBuildCopyTest.php new file mode 100644 index 0000000..a20ca51 --- /dev/null +++ b/tests/Feature/IosBuildCopyTest.php @@ -0,0 +1,155 @@ +testProjectPath = sys_get_temp_dir().'/nativephp_ios_copy_test_'.uniqid(); + File::makeDirectory($this->testProjectPath, 0755, true); + + $this->appPath = $this->testProjectPath.'/nativephp/ios/laravel/'; + File::makeDirectory($this->appPath, 0755, true); + + app()->setBasePath($this->testProjectPath); + } + + protected function tearDown(): void + { + File::deleteDirectory($this->testProjectPath); + parent::tearDown(); + } + + public function test_copy_respects_cleanup_exclude_files_config(): void + { + config(['nativephp.cleanup_exclude_files' => ['custom-excluded/']]); + + $source = $this->testProjectPath.'/source/'; + File::makeDirectory($source.'custom-excluded', 0755, true); + File::put($source.'custom-excluded/file.txt', 'should not be copied'); + File::makeDirectory($source.'app/Models', 0755, true); + File::put($source.'app/Models/User.php', 'setBasePath($source); + + $command = $this->app->make(BuildIosAppCommand::class); + $reflection = new ReflectionClass($command); + + $appPathProp = $reflection->getProperty('appPath'); + $appPathProp->setValue($command, $this->appPath); + + $method = $reflection->getMethod('copyLaravelAppIntoIosApp'); + $method->invoke($command); + + $this->assertDirectoryDoesNotExist( + $this->appPath.'custom-excluded', + 'User-configured excluded directory should not be copied' + ); + + $this->assertDirectoryExists( + $this->appPath.'app/Models', + 'Non-excluded directory should be copied' + ); + + $this->assertDirectoryDoesNotExist( + $this->appPath.'vendor', + 'Default excluded directory should not be copied' + ); + } + + public function test_copy_handles_empty_cleanup_exclude_files_config(): void + { + config(['nativephp.cleanup_exclude_files' => []]); + + $source = $this->testProjectPath.'/source/'; + File::makeDirectory($source.'node_modules', 0755, true); + File::put($source.'node_modules/package.json', '{}'); + File::makeDirectory($source.'app', 0755, true); + File::put($source.'app/test.php', 'setBasePath($source); + + $command = $this->app->make(BuildIosAppCommand::class); + $reflection = new ReflectionClass($command); + + $appPathProp = $reflection->getProperty('appPath'); + $appPathProp->setValue($command, $this->appPath); + + $method = $reflection->getMethod('copyLaravelAppIntoIosApp'); + $method->invoke($command); + + $this->assertDirectoryDoesNotExist($this->appPath.'node_modules'); + $this->assertDirectoryExists($this->appPath.'app'); + } + + public function test_copy_handles_null_cleanup_exclude_files_config(): void + { + config(['nativephp.cleanup_exclude_files' => null]); + + $source = $this->testProjectPath.'/source/'; + File::makeDirectory($source.'.git', 0755, true); + File::put($source.'.git/config', '[core]'); + File::makeDirectory($source.'storage', 0755, true); + File::put($source.'storage/test.txt', 'test'); + + app()->setBasePath($source); + + $command = $this->app->make(BuildIosAppCommand::class); + $reflection = new ReflectionClass($command); + + $appPathProp = $reflection->getProperty('appPath'); + $appPathProp->setValue($command, $this->appPath); + + $method = $reflection->getMethod('copyLaravelAppIntoIosApp'); + $method->invoke($command); + + $this->assertDirectoryDoesNotExist($this->appPath.'.git'); + $this->assertDirectoryExists($this->appPath.'storage'); + } + + public function test_copy_merges_user_config_with_defaults(): void + { + config(['nativephp.cleanup_exclude_files' => [ + 'custom-dir/', + 'another-custom/', + ]]); + + $source = $this->testProjectPath.'/source/'; + File::makeDirectory($source.'custom-dir', 0755, true); + File::makeDirectory($source.'another-custom', 0755, true); + File::makeDirectory($source.'vendor', 0755, true); + File::makeDirectory($source.'node_modules', 0755, true); + File::makeDirectory($source.'app', 0755, true); + + app()->setBasePath($source); + + $command = $this->app->make(BuildIosAppCommand::class); + $reflection = new ReflectionClass($command); + + $appPathProp = $reflection->getProperty('appPath'); + $appPathProp->setValue($command, $this->appPath); + + $method = $reflection->getMethod('copyLaravelAppIntoIosApp'); + $method->invoke($command); + + $this->assertDirectoryDoesNotExist($this->appPath.'custom-dir'); + $this->assertDirectoryDoesNotExist($this->appPath.'another-custom'); + $this->assertDirectoryDoesNotExist($this->appPath.'vendor'); + $this->assertDirectoryDoesNotExist($this->appPath.'node_modules'); + $this->assertDirectoryExists($this->appPath.'app'); + } +}