From 8c5b5f8fb60276bf3c6fc079bb38f15a63e3fad5 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Tue, 10 Dec 2024 08:34:42 +0000 Subject: [PATCH 01/18] wip --- config/nativephp-internal.php | 5 ++ src/Commands/BundleCommand.php | 130 +++++++++++++++++++++++++++++++++ src/NativeServiceProvider.php | 2 + 3 files changed, 137 insertions(+) create mode 100644 src/Commands/BundleCommand.php diff --git a/config/nativephp-internal.php b/config/nativephp-internal.php index 4210df9..1c2734f 100644 --- a/config/nativephp-internal.php +++ b/config/nativephp-internal.php @@ -29,4 +29,9 @@ * The URL to the NativePHP API. */ 'api_url' => env('NATIVEPHP_API_URL', 'http://localhost:4000/api/'), + + 'zephpyr' => [ + 'host' => env('ZEPHPYR_HOST', 'zephpyr.com'), + 'key' => env('ZEPHPYR_SECRET'), + ], ]; diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php new file mode 100644 index 0000000..c5fe0ce --- /dev/null +++ b/src/Commands/BundleCommand.php @@ -0,0 +1,130 @@ +key = config('nativephp-internal.zephpyr.key'); + + if (!$this->key) { + $this->line(''); + $this->warn('No ZEPHPYR_SECRET found. Cannot bundle!'); + $this->line(''); + $this->line('Add this app\'s ZEPHPYR_SECRET to its .env file:'); + $this->line(base_path('.env')); + $this->line(''); + $this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!'); + $this->info('Check out https://zephpyr.com'); + $this->line(''); + + return static::FAILURE; + } + + // Package the app up into a zip + if (! $this->zipApplication()) { + $this->error("Failed to create zip archive at {$this->zipPath}."); + return static::FAILURE; + } + + // Send the zip file + if (! $this->sendToZephpyr()) { + $this->error("Failed to upload zip [{$this->zipPath}] to Zephpyr."); + return static::FAILURE; + } + + return static::SUCCESS; + } + + private function zipApplication(): bool + { + $this->zipName = 'app_' . str()->random(8) . '.zip'; + $this->zipPath = storage_path($this->zipName); + + $zip = new ZipArchive; + + if ($zip->open($this->zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { + return false; + } + + $this->prepareNativeEnv(); + + $this->addFilesToZip($zip); + + $zip->close(); + + $this->restoreWebEnv(); + + return true; + } + + private function addFilesToZip(ZipArchive $zip): void + { + $app = (new Finder())->files() + ->followLinks() + ->ignoreVCSIgnored(true) + ->in(base_path()) + ->exclude([ + 'tests', + ...config('nativephp.cleanup_exclude_files', []), + ]); + + $this->finderToZip($app, $zip); + + $vendor = (new Finder())->files() + ->exclude([ + 'vendor/nativephp/php-bin', + ]) + ->in(base_path('vendor')); + + $this->finderToZip($vendor, $zip); + + $nodeModules = (new Finder())->files() + ->in(base_path('node_modules')); + + $this->finderToZip($nodeModules, $zip); + + $env = (new Finder())->files() + ->ignoreDotFiles(false) + ->name('.env') + ->in(base_path()); + + $this->finderToZip($env, $zip); + } + + private function finderToZip(Finder $finder, ZipArchive $zip): void + { + foreach ($finder as $file) { + dump([$file->getRealPath(), $file->getRelativePath()]); + $zip->addFile($file->getRealPath(), $file->getRelativePathname()); + } + } + + private function sendToZephpyr(): bool + { + return false; + $response = Http::attach('archive', fopen($this->zipPath, 'r'), $this->zipName) + ->post(config('nativephp-internal.zephpyr.host'), [ + 'key' => $this->key, + ]); + + return $response->successful(); + } +} diff --git a/src/NativeServiceProvider.php b/src/NativeServiceProvider.php index 6ca8d62..2aa7b25 100644 --- a/src/NativeServiceProvider.php +++ b/src/NativeServiceProvider.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; use Native\Laravel\ChildProcess as ChildProcessImplementation; +use Native\Laravel\Commands\BundleCommand; use Native\Laravel\Commands\FreshCommand; use Native\Laravel\Commands\LoadPHPConfigurationCommand; use Native\Laravel\Commands\LoadStartupConfigurationCommand; @@ -40,6 +41,7 @@ public function configurePackage(Package $package): void FreshCommand::class, SeedDatabaseCommand::class, MinifyApplicationCommand::class, + BundleCommand::class, ]) ->hasConfigFile() ->hasRoute('api') From 8b929a6aa00c91226ef60298baa6def91c51e359 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Tue, 10 Dec 2024 08:37:26 +0000 Subject: [PATCH 02/18] Remove the minify command --- src/Commands/MinifyApplicationCommand.php | 111 ---------------------- src/NativeServiceProvider.php | 2 - 2 files changed, 113 deletions(-) delete mode 100644 src/Commands/MinifyApplicationCommand.php diff --git a/src/Commands/MinifyApplicationCommand.php b/src/Commands/MinifyApplicationCommand.php deleted file mode 100644 index 37d8378..0000000 --- a/src/Commands/MinifyApplicationCommand.php +++ /dev/null @@ -1,111 +0,0 @@ -argument('app')); - - if (! is_dir($appPath)) { - $this->error('The app path is not a directory'); - - return; - } - - $this->info('Minifying application…'); - - $this->cleanUpEnvFile($appPath); - $this->removeIgnoredFilesAndFolders($appPath); - - $compactor = new Php; - - $phpFiles = Finder::create() - ->files() - ->name('*.php') - ->in($appPath); - - foreach ($phpFiles as $phpFile) { - $minifiedContent = $compactor->compact($phpFile->getRealPath(), $phpFile->getContents()); - file_put_contents($phpFile->getRealPath(), $minifiedContent); - } - } - - protected function cleanUpEnvFile(string $appPath): void - { - $envFile = $appPath.'/.env'; - - if (! file_exists($envFile)) { - return; - } - - $this->info('Cleaning up .env file…'); - - $cleanUpKeys = config('nativephp.cleanup_env_keys', []); - - $envContent = file_get_contents($envFile); - $envValues = collect(explode("\n", $envContent)) - ->filter(function (string $line) use ($cleanUpKeys) { - $key = Str::before($line, '='); - - return ! Str::is($cleanUpKeys, $key); - }) - ->join("\n"); - - file_put_contents($envFile, $envValues); - } - - protected function removeIgnoredFilesAndFolders(string $appPath): void - { - $this->info('Cleaning up ignored files and folders…'); - - $itemsToRemove = config('nativephp.cleanup_exclude_files', []); - - foreach ($itemsToRemove as $item) { - $fullPath = $appPath.'/'.$item; - - if (file_exists($fullPath)) { - if (is_dir($fullPath)) { - $this->deleteDirectoryRecursive($fullPath); - } else { - array_map('unlink', glob($fullPath)); - } - } else { - foreach (glob($item) as $pathFound) { - unlink($pathFound); - } - } - } - } - - private function deleteDirectoryRecursive(string $directory): bool - { - if (! file_exists($directory)) { - return true; - } - - if (! is_dir($directory)) { - return unlink($directory); - } - - foreach (scandir($directory) as $item) { - if ($item == '.' || $item == '..') { - continue; - } - - if (! $this->deleteDirectoryRecursive($directory.'/'.$item)) { - return false; - } - } - - return rmdir($directory); - } -} diff --git a/src/NativeServiceProvider.php b/src/NativeServiceProvider.php index 2aa7b25..5c30c6b 100644 --- a/src/NativeServiceProvider.php +++ b/src/NativeServiceProvider.php @@ -13,7 +13,6 @@ use Native\Laravel\Commands\LoadPHPConfigurationCommand; use Native\Laravel\Commands\LoadStartupConfigurationCommand; use Native\Laravel\Commands\MigrateCommand; -use Native\Laravel\Commands\MinifyApplicationCommand; use Native\Laravel\Commands\SeedDatabaseCommand; use Native\Laravel\Contracts\ChildProcess as ChildProcessContract; use Native\Laravel\Contracts\GlobalShortcut as GlobalShortcutContract; @@ -40,7 +39,6 @@ public function configurePackage(Package $package): void MigrateCommand::class, FreshCommand::class, SeedDatabaseCommand::class, - MinifyApplicationCommand::class, BundleCommand::class, ]) ->hasConfigFile() From 6cdf785eac79391d8bdb1acbeeca90aeb6026978 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Thu, 19 Dec 2024 15:48:17 +0100 Subject: [PATCH 03/18] wip --- config/nativephp-internal.php | 3 +- config/nativephp.php | 3 +- src/Commands/BundleCommand.php | 95 +++++++++++++++++++++++----------- 3 files changed, 69 insertions(+), 32 deletions(-) diff --git a/config/nativephp-internal.php b/config/nativephp-internal.php index 1c2734f..a05783a 100644 --- a/config/nativephp-internal.php +++ b/config/nativephp-internal.php @@ -32,6 +32,7 @@ 'zephpyr' => [ 'host' => env('ZEPHPYR_HOST', 'zephpyr.com'), - 'key' => env('ZEPHPYR_SECRET'), + 'token' => env('ZEPHPYR_TOKEN'), + 'key' => env('ZEPHPYR_KEY'), ], ]; diff --git a/config/nativephp.php b/config/nativephp.php index 5c6e7a8..9da5f50 100644 --- a/config/nativephp.php +++ b/config/nativephp.php @@ -6,7 +6,7 @@ * It is used to determine if the app needs to be updated. * Increment this value every time you release a new version of your app. */ - 'version' => env('NATIVEPHP_APP_VERSION', '1.0.0'), + 'version' => env('NATIVEPHP_APP_VERSION', 1), /** * The ID of your application. This should be a unique identifier @@ -47,6 +47,7 @@ 'AWS_*', 'GITHUB_*', 'DO_SPACES_*', + 'ZEPHPYR_*', '*_SECRET', 'NATIVEPHP_UPDATER_PATH', 'NATIVEPHP_APPLE_ID', diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index c5fe0ce..0802e4c 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -3,8 +3,8 @@ namespace Native\Laravel\Commands; use Illuminate\Console\Command; +use Illuminate\Support\Facades\Http; use Native\Electron\Traits\CleansEnvFile; -use Native\Laravel\NativeServiceProvider; use Symfony\Component\Finder\Finder; use ZipArchive; @@ -12,23 +12,25 @@ class BundleCommand extends Command { use CleansEnvFile; - protected $name = 'native:bundle'; + protected $signature = 'native:bundle {--fetch}'; protected $description = 'Bundle your application for distribution.'; private ?string $key; + private string $zipPath; + private string $zipName; public function handle() { $this->key = config('nativephp-internal.zephpyr.key'); - if (!$this->key) { + if (! $this->key) { $this->line(''); - $this->warn('No ZEPHPYR_SECRET found. Cannot bundle!'); + $this->warn('No ZEPHPYR_KEY found. Cannot bundle!'); $this->line(''); - $this->line('Add this app\'s ZEPHPYR_SECRET to its .env file:'); + $this->line('Add this app\'s ZEPHPYR_KEY to its .env file:'); $this->line(base_path('.env')); $this->line(''); $this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!'); @@ -38,24 +40,43 @@ public function handle() return static::FAILURE; } + if ($this->option('fetch')) { + if (! $this->fetchLatestBundle()) { + $this->warn("Latest bundle not yet available. Try again soon."); + return static::FAILURE; + } + + $this->info("Latest bundle downloaded."); + return static::SUCCESS; + } + // Package the app up into a zip if (! $this->zipApplication()) { $this->error("Failed to create zip archive at {$this->zipPath}."); + return static::FAILURE; } // Send the zip file - if (! $this->sendToZephpyr()) { + dd($result = $this->sendToZephpyr()); + + if ($result->failed()) { $this->error("Failed to upload zip [{$this->zipPath}] to Zephpyr."); + return static::FAILURE; } + @unlink($this->zipPath); + + $this->info('Successfully uploaded to Zephpyr.'); + $this->line('Use native:bundle --fetch to retrieve the latest bundle.'); + return static::SUCCESS; } private function zipApplication(): bool { - $this->zipName = 'app_' . str()->random(8) . '.zip'; + $this->zipName = 'app_'.str()->random(8).'.zip'; $this->zipPath = storage_path($this->zipName); $zip = new ZipArchive; @@ -77,54 +98,68 @@ private function zipApplication(): bool private function addFilesToZip(ZipArchive $zip): void { - $app = (new Finder())->files() + // TODO: Check the composer.json to make sure there are no symlinked or private packages as these will be a + // pain later + + $app = (new Finder)->files() ->followLinks() ->ignoreVCSIgnored(true) ->in(base_path()) ->exclude([ + 'vendor', + 'dist', + 'build', 'tests', ...config('nativephp.cleanup_exclude_files', []), ]); $this->finderToZip($app, $zip); - $vendor = (new Finder())->files() + $vendor = (new Finder)->files() ->exclude([ - 'vendor/nativephp/php-bin', + 'nativephp/php-bin', + 'nativephp/electron/resources/js', + 'nativephp/*/vendor', ]) ->in(base_path('vendor')); - $this->finderToZip($vendor, $zip); + $this->finderToZip($vendor, $zip, 'vendor'); - $nodeModules = (new Finder())->files() + $nodeModules = (new Finder)->files() ->in(base_path('node_modules')); - $this->finderToZip($nodeModules, $zip); - - $env = (new Finder())->files() - ->ignoreDotFiles(false) - ->name('.env') - ->in(base_path()); - - $this->finderToZip($env, $zip); + $this->finderToZip($nodeModules, $zip, 'node_modules'); } - private function finderToZip(Finder $finder, ZipArchive $zip): void + private function finderToZip(Finder $finder, ZipArchive $zip, ?string $path = null): void { foreach ($finder as $file) { - dump([$file->getRealPath(), $file->getRelativePath()]); - $zip->addFile($file->getRealPath(), $file->getRelativePathname()); + if ($file->getRealPath() === false) { + continue; + } + + $zip->addFile($file->getRealPath(), str($path)->finish(DIRECTORY_SEPARATOR) . $file->getRelativePathname()); } } - private function sendToZephpyr(): bool + private function sendToZephpyr() { - return false; - $response = Http::attach('archive', fopen($this->zipPath, 'r'), $this->zipName) - ->post(config('nativephp-internal.zephpyr.host'), [ - 'key' => $this->key, - ]); + return Http::withToken(config('nativephp-internal.zephpyr.token')) + ->attach('archive', fopen($this->zipPath, 'r'), $this->zipName) + ->post(str(config('nativephp-internal.zephpyr.host'))->finish('/') . 'api/build/' . $this->key); + } + + private function fetchLatestBundle(): bool + { + $response = Http::withToken(config('nativephp-internal.zephpyr.token')) + ->get(str(config('nativephp-internal.zephpyr.host'))->finish('/') . 'api/download/' . $this->key); - return $response->successful(); + if ($response->failed()) { + return false; + } + + file_put_contents(base_path('build/__nativephp_app_bundle'), $response->body()); + + return true; } } From 89465df604024597db65d468996896b5ea55c725 Mon Sep 17 00:00:00 2001 From: simonhamp Date: Sun, 29 Dec 2024 15:42:25 +0000 Subject: [PATCH 04/18] Fix styling --- src/Commands/BundleCommand.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index 0802e4c..e2e2e76 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -42,11 +42,13 @@ public function handle() if ($this->option('fetch')) { if (! $this->fetchLatestBundle()) { - $this->warn("Latest bundle not yet available. Try again soon."); + $this->warn('Latest bundle not yet available. Try again soon.'); + return static::FAILURE; } - $this->info("Latest bundle downloaded."); + $this->info('Latest bundle downloaded.'); + return static::SUCCESS; } @@ -138,7 +140,7 @@ private function finderToZip(Finder $finder, ZipArchive $zip, ?string $path = nu continue; } - $zip->addFile($file->getRealPath(), str($path)->finish(DIRECTORY_SEPARATOR) . $file->getRelativePathname()); + $zip->addFile($file->getRealPath(), str($path)->finish(DIRECTORY_SEPARATOR).$file->getRelativePathname()); } } @@ -146,13 +148,13 @@ private function sendToZephpyr() { return Http::withToken(config('nativephp-internal.zephpyr.token')) ->attach('archive', fopen($this->zipPath, 'r'), $this->zipName) - ->post(str(config('nativephp-internal.zephpyr.host'))->finish('/') . 'api/build/' . $this->key); + ->post(str(config('nativephp-internal.zephpyr.host'))->finish('/').'api/build/'.$this->key); } private function fetchLatestBundle(): bool { $response = Http::withToken(config('nativephp-internal.zephpyr.token')) - ->get(str(config('nativephp-internal.zephpyr.host'))->finish('/') . 'api/download/' . $this->key); + ->get(str(config('nativephp-internal.zephpyr.host'))->finish('/').'api/download/'.$this->key); if ($response->failed()) { return false; From f86a459baf97a3309fb76c8302c5d2be6dac4520 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Wed, 8 Jan 2025 20:14:05 +0100 Subject: [PATCH 05/18] feat: more error messages --- composer.json | 3 +- src/Commands/BundleCommand.php | 74 +++++++++++++++++++++++++++++----- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index 9f7b760..006cd0c 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,8 @@ "php": "^8.1", "illuminate/contracts": "^10.0|^11.0", "spatie/laravel-package-tools": "^1.16.4", - "symfony/finder": "^6.2|^7.0" + "symfony/finder": "^6.2|^7.0", + "ext-zip": "*" }, "require-dev": { "guzzlehttp/guzzle": "^7.0", diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index e2e2e76..d64b4c4 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -40,6 +40,28 @@ public function handle() return static::FAILURE; } + if (! config('nativephp-internal.zephpyr.token')) { + $this->line(''); + $this->warn('No ZEPHPYR_TOKEN found. Cannot bundle!'); + $this->line(''); + $this->line('Add your api ZEPHPYR_TOKEN to its .env file:'); + $this->line(base_path('.env')); + $this->line(''); + $this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!'); + $this->info('Check out https://zephpyr.com'); + $this->line(''); + + return static::FAILURE; + } + + $result = $this->checkAuthenticated(); + + if ($result->failed()) { + $this->error('Invalid API token: check your ZEPHPYR_TOKEN on https://zephpyr.com/user/api-tokens'); + + return; + } + if ($this->option('fetch')) { if (! $this->fetchLatestBundle()) { $this->warn('Latest bundle not yet available. Try again soon.'); @@ -58,11 +80,19 @@ public function handle() return static::FAILURE; } + // $this->zipName = 'app_CcINfsoQ.zip'; + // $this->zipPath = base_path('temp/'.$this->zipName); // Send the zip file - dd($result = $this->sendToZephpyr()); + $result = $this->sendToZephpyr(); - if ($result->failed()) { + //dd($result->status(), json_decode($result->body())); + + if ($result->code() === 413) { + $this->error('The zip file is too large to upload to Zephpyr. Please contact support.'); + + return static::FAILURE; + } elseif ($result->failed()) { $this->error("Failed to upload zip [{$this->zipPath}] to Zephpyr."); return static::FAILURE; @@ -79,7 +109,12 @@ public function handle() private function zipApplication(): bool { $this->zipName = 'app_'.str()->random(8).'.zip'; - $this->zipPath = storage_path($this->zipName); + $this->zipPath = base_path('temp/'.$this->zipName); + + // Create zip path + if (! @mkdir(dirname($this->zipPath), recursive: true) && ! is_dir(dirname($this->zipPath))) { + return false; + } $zip = new ZipArchive; @@ -100,8 +135,10 @@ private function zipApplication(): bool private function addFilesToZip(ZipArchive $zip): void { - // TODO: Check the composer.json to make sure there are no symlinked or private packages as these will be a - // pain later + // TODO: Check the composer.json to make sure there are no symlinked + // or private packages as these will be a pain later + + $this->line('Adding files to zip…'); $app = (new Finder)->files() ->followLinks() @@ -144,17 +181,36 @@ private function finderToZip(Finder $finder, ZipArchive $zip, ?string $path = nu } } + private function baseUrl(): string + { + return str(config('nativephp-internal.zephpyr.host'))->finish('/'); + } + private function sendToZephpyr() { - return Http::withToken(config('nativephp-internal.zephpyr.token')) + $this->line('Uploading to Zephpyr…'); + + return Http::acceptJson() + ->withoutRedirecting() // Upload won't work if we follow the redirect + ->withToken(config('nativephp-internal.zephpyr.token')) ->attach('archive', fopen($this->zipPath, 'r'), $this->zipName) - ->post(str(config('nativephp-internal.zephpyr.host'))->finish('/').'api/build/'.$this->key); + ->post($this->baseUrl().'api/build/'.$this->key); + } + + private function checkAuthenticated() + { + $this->line('Checking authentication…'); + + return Http::acceptJson() + ->withToken(config('nativephp-internal.zephpyr.token')) + ->get($this->baseUrl().'api/user'); } private function fetchLatestBundle(): bool { - $response = Http::withToken(config('nativephp-internal.zephpyr.token')) - ->get(str(config('nativephp-internal.zephpyr.host'))->finish('/').'api/download/'.$this->key); + $response = Http::acceptJson() + ->withToken(config('nativephp-internal.zephpyr.token')) + ->get($this->baseUrl().'api/download/'.$this->key); if ($response->failed()) { return false; From d0667454817da042cc573081cadfa2a11310a895 Mon Sep 17 00:00:00 2001 From: SRWieZ Date: Wed, 8 Jan 2025 19:14:34 +0000 Subject: [PATCH 06/18] Fix styling --- src/Commands/BundleCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index d64b4c4..361a267 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -86,7 +86,7 @@ public function handle() // Send the zip file $result = $this->sendToZephpyr(); - //dd($result->status(), json_decode($result->body())); + // dd($result->status(), json_decode($result->body())); if ($result->code() === 413) { $this->error('The zip file is too large to upload to Zephpyr. Please contact support.'); From c55203571257a0b087d33c9cdc9d9f4c434e9b88 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Fri, 10 Jan 2025 17:01:20 +0100 Subject: [PATCH 07/18] fix: transfert CleansEnvFile to this repo nativephp/electron requires this repo, not the other way around --- src/Commands/BundleCommand.php | 2 +- src/Commands/Traits/CleansEnvFile.php | 46 +++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/Commands/Traits/CleansEnvFile.php diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index 361a267..e27f28b 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -4,7 +4,7 @@ use Illuminate\Console\Command; use Illuminate\Support\Facades\Http; -use Native\Electron\Traits\CleansEnvFile; +use Native\Laravel\Commands\Traits\CleansEnvFile; use Symfony\Component\Finder\Finder; use ZipArchive; diff --git a/src/Commands/Traits/CleansEnvFile.php b/src/Commands/Traits/CleansEnvFile.php new file mode 100644 index 0000000..4c0a3ea --- /dev/null +++ b/src/Commands/Traits/CleansEnvFile.php @@ -0,0 +1,46 @@ +line('Preparing production .env file…'); + + $envFile = app()->environmentFilePath(); + + if (! file_exists($backup = $this->getBackupEnvFilePath())) { + copy($envFile, $backup); + } + + $this->cleanEnvFile($envFile); + } + + protected function cleanEnvFile(string $path): void + { + $cleanUpKeys = config('nativephp.cleanup_env_keys', []); + + $contents = collect(file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)) + ->filter(function (string $line) use ($cleanUpKeys) { + $key = str($line)->before('='); + + return ! $key->is($cleanUpKeys) + && ! $key->startsWith('#'); + }) + ->join("\n"); + + file_put_contents($path, $contents); + } + + protected function restoreWebEnv(): void + { + copy($this->getBackupEnvFilePath(), app()->environmentFilePath()); + unlink($this->getBackupEnvFilePath()); + } + + protected function getBackupEnvFilePath(): string + { + return base_path('.env.backup'); + } +} From 31671f7f593d680749f81fc8bd237f3200216366 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Fri, 10 Jan 2025 18:48:24 +0100 Subject: [PATCH 08/18] fix: bundles getting bigger + tests --- config/nativephp.php | 4 +- src/Commands/BundleCommand.php | 112 +++++++++++++------- tests/Command/IgnoreFilesAndFoldersTest.php | 87 ++++++++------- tests/Pest.php | 77 ++++++++++++++ 4 files changed, 195 insertions(+), 85 deletions(-) diff --git a/config/nativephp.php b/config/nativephp.php index 9da5f50..37c3a89 100644 --- a/config/nativephp.php +++ b/config/nativephp.php @@ -58,9 +58,11 @@ /** * A list of files and folders that should be removed from the * final app before it is bundled for production. - * You may use glob / wildcard patterns here. + * You may use glob wildcard patterns here. */ 'cleanup_exclude_files' => [ + 'build', + 'temp', 'content', 'storage/app/framework/{sessions,testing,cache}', 'storage/logs/laravel.log', diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index e27f28b..99a06ff 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -4,6 +4,7 @@ use Illuminate\Console\Command; use Illuminate\Support\Facades\Http; +use Illuminate\Support\Number; use Native\Laravel\Commands\Traits\CleansEnvFile; use Symfony\Component\Finder\Finder; use ZipArchive; @@ -12,7 +13,7 @@ class BundleCommand extends Command { use CleansEnvFile; - protected $signature = 'native:bundle {--fetch}'; + protected $signature = 'native:bundle {--fetch} {--without-cleanup}'; protected $description = 'Bundle your application for distribution.'; @@ -24,42 +25,18 @@ class BundleCommand extends Command public function handle() { - $this->key = config('nativephp-internal.zephpyr.key'); - - if (! $this->key) { - $this->line(''); - $this->warn('No ZEPHPYR_KEY found. Cannot bundle!'); - $this->line(''); - $this->line('Add this app\'s ZEPHPYR_KEY to its .env file:'); - $this->line(base_path('.env')); - $this->line(''); - $this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!'); - $this->info('Check out https://zephpyr.com'); - $this->line(''); - + if (! $this->checkForZephpyrKey()) { return static::FAILURE; } - if (! config('nativephp-internal.zephpyr.token')) { - $this->line(''); - $this->warn('No ZEPHPYR_TOKEN found. Cannot bundle!'); - $this->line(''); - $this->line('Add your api ZEPHPYR_TOKEN to its .env file:'); - $this->line(base_path('.env')); - $this->line(''); - $this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!'); - $this->info('Check out https://zephpyr.com'); - $this->line(''); - + if (! $this->checkForZephpyrToken()) { return static::FAILURE; } - $result = $this->checkAuthenticated(); - - if ($result->failed()) { + if (! $this->checkAuthenticated()) { $this->error('Invalid API token: check your ZEPHPYR_TOKEN on https://zephpyr.com/user/api-tokens'); - return; + return static::FAILURE; } if ($this->option('fetch')) { @@ -80,32 +57,47 @@ public function handle() return static::FAILURE; } - // $this->zipName = 'app_CcINfsoQ.zip'; - // $this->zipPath = base_path('temp/'.$this->zipName); // Send the zip file $result = $this->sendToZephpyr(); - // dd($result->status(), json_decode($result->body())); + if ($result->status() === 413) { + $fileSize = Number::fileSize(filesize($this->zipPath)); + $this->error('The zip file is too large to upload to Zephpyr ('.$fileSize.'). Please contact support.'); - if ($result->code() === 413) { - $this->error('The zip file is too large to upload to Zephpyr. Please contact support.'); + $this->cleanUp(); return static::FAILURE; } elseif ($result->failed()) { - $this->error("Failed to upload zip [{$this->zipPath}] to Zephpyr."); + $this->error("Failed to upload zip to Zephpyr. Error: {$result->status()}"); + $this->cleanUp(); return static::FAILURE; } - @unlink($this->zipPath); - $this->info('Successfully uploaded to Zephpyr.'); $this->line('Use native:bundle --fetch to retrieve the latest bundle.'); + $this->cleanUp(); + return static::SUCCESS; } + protected function cleanUp(): void + { + if ($this->option('without-cleanup')) { + return; + } + + $this->line('Cleaning up…'); + + $previousBuilds = glob(base_path('temp/app_*.zip')); + + foreach ($previousBuilds as $previousBuild) { + @unlink($previousBuild); + } + } + private function zipApplication(): bool { $this->zipName = 'app_'.str()->random(8).'.zip'; @@ -138,7 +130,7 @@ private function addFilesToZip(ZipArchive $zip): void // TODO: Check the composer.json to make sure there are no symlinked // or private packages as these will be a pain later - $this->line('Adding files to zip…'); + $this->line('Creating zip archive…'); $app = (new Finder)->files() ->followLinks() @@ -188,7 +180,7 @@ private function baseUrl(): string private function sendToZephpyr() { - $this->line('Uploading to Zephpyr…'); + $this->line('Uploading zip to Zephpyr…'); return Http::acceptJson() ->withoutRedirecting() // Upload won't work if we follow the redirect @@ -203,7 +195,7 @@ private function checkAuthenticated() return Http::acceptJson() ->withToken(config('nativephp-internal.zephpyr.token')) - ->get($this->baseUrl().'api/user'); + ->get($this->baseUrl().'api/user')->successful(); } private function fetchLatestBundle(): bool @@ -220,4 +212,44 @@ private function fetchLatestBundle(): bool return true; } + + private function checkForZephpyrKey() + { + $this->key = config('nativephp-internal.zephpyr.key'); + + if (! $this->key) { + $this->line(''); + $this->warn('No ZEPHPYR_KEY found. Cannot bundle!'); + $this->line(''); + $this->line('Add this app\'s ZEPHPYR_KEY to its .env file:'); + $this->line(base_path('.env')); + $this->line(''); + $this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!'); + $this->info('Check out https://zephpyr.com'); + $this->line(''); + + return false; + } + + return true; + } + + private function checkForZephpyrToken() + { + if (! config('nativephp-internal.zephpyr.token')) { + $this->line(''); + $this->warn('No ZEPHPYR_TOKEN found. Cannot bundle!'); + $this->line(''); + $this->line('Add your api ZEPHPYR_TOKEN to its .env file:'); + $this->line(base_path('.env')); + $this->line(''); + $this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!'); + $this->info('Check out https://zephpyr.com'); + $this->line(''); + + return false; + } + + return true; + } } diff --git a/tests/Command/IgnoreFilesAndFoldersTest.php b/tests/Command/IgnoreFilesAndFoldersTest.php index e805cc0..0ac7f3e 100644 --- a/tests/Command/IgnoreFilesAndFoldersTest.php +++ b/tests/Command/IgnoreFilesAndFoldersTest.php @@ -1,6 +1,24 @@ artisan('native:minify resources/app'); - $this->assertFalse(file_exists($laravelLog)); + $this->artisan('native:bundle --without-cleanup') + ->expectsOutput('Creating zip archive'); - // Clean up after ourselves - if (file_exists($laravelLog)) { - unlink($laravelLog); - } - if (file_exists('resources/app/storage/logs')) { - rmdir('resources/app/storage/logs'); - } - if (file_exists('resources/app/storage')) { - rmdir('resources/app/storage'); - } - removeAppFolder(); + // Assert + expect(basename($laravelLog)) + ->not->toBeInZip(findLatestZipPath()); }); -it('will remove the content folder by default before building', function () { +it('will remove the content folder by default before bundling', function () { $contentPath = 'resources/app/content'; // Create a dummy copy of the folder @@ -36,14 +46,12 @@ } // Run the test - $this->artisan('native:minify resources/app'); - $this->assertFalse(file_exists($contentPath)); + $this->artisan('native:bundle --without-cleanup') + ->expectsOutput('Creating zip archive'); - // Clean up after ourselves - if (file_exists($contentPath)) { - unlink($contentPath); - } - removeAppFolder(); + // Assert + expect($contentPath) + ->not->toBeInZip(findLatestZipPath()); }); it('will remove only files that match a globbed path', function () { @@ -63,26 +71,17 @@ file_put_contents($noDeletePath, 'DO NOT DELETE ME'); // Run the test - $this->artisan('native:minify resources/app'); - $this->assertFalse(file_exists($yes1DeletePath)); - $this->assertFalse(file_exists($yes2DeletePath)); - $this->assertTrue(file_exists($noDeletePath)); - - // Clean up after ourselves - foreach ([$yes1DeletePath, $yes2DeletePath, $noDeletePath] as $remove) { - if (file_exists($remove)) { - unlink($remove); - } - } - if (file_exists($wildcardPath)) { - rmdir($wildcardPath); - } - removeAppFolder(); -}); + $this->artisan('native:bundle --without-cleanup') + ->expectsOutput('Creating zip archive'); -function removeAppFolder() -{ - if (file_exists('resources/app')) { - rmdir('resources/app'); - } -} + // Assert + $latestZip = findLatestZipPath(); + + expect($yes1DeletePath) + ->not->toBeInZip($latestZip); + expect($yes2DeletePath) + ->not->toBeInZip($latestZip); + expect($noDeletePath) + ->toBeInZip($latestZip); + +}); diff --git a/tests/Pest.php b/tests/Pest.php index defff2e..856437c 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -2,4 +2,81 @@ use Native\Laravel\Tests\TestCase; +/* +|-------------------------------------------------------------------------- +| Test Case +|-------------------------------------------------------------------------- +| +| The closure you provide to your test functions is always bound to a specific PHPUnit test +| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may +| need to change it using the "uses()" function to bind a different classes or traits. +| +*/ + uses(TestCase::class)->in(__DIR__); + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +| +| When you're writing tests, you often need to check that values meet certain conditions. The +| "expect()" function gives you access to a set of "expectations" methods that you can use +| to assert different things. Of course, you may extend the Expectation API at any time. +| +*/ + +expect()->extend('toBeInZip', function (string $zipFile) { + $zip = new ZipArchive; + $zip->open($zipFile); + + $found = $zip->locateName($this->value) !== false; + + $zip->close(); + + return $this->toBeTrue($found); +}); + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +| +| While Pest is very powerful out-of-the-box, you may have some testing code specific to your +| project that you don't want to repeat in every file. Here you can also expose helpers as +| global functions to help you to reduce the number of lines of code in your test files. +| +*/ + +function rmdir_recursive($dir): void +{ + foreach (scandir($dir) as $file) { + if ($file === '.' || $file === '..') { + continue; + } + if (is_dir("$dir/$file")) { + rmdir_recursive("$dir/$file"); + } else { + unlink("$dir/$file"); + } + } + rmdir($dir); +} + +function findLatestZipPath(): ?string +{ + $latestZip = null; + $latestTime = 0; + + dump(glob(base_path('temp/app_*.zip'))); + foreach (glob(base_path('temp/app_*.zip')) as $zip) { + $time = filemtime($zip); + + if ($time > $latestTime) { + $latestTime = $time; + $latestZip = $zip; + } + } + + return $latestZip; +} From 1c389f2472031876eebd55a8eb71c9982a7c0188 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Mon, 13 Jan 2025 13:41:48 +0100 Subject: [PATCH 09/18] new api endpoints + better error handling --- src/Commands/BundleCommand.php | 46 +++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index 99a06ff..a14f717 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -2,6 +2,7 @@ namespace Native\Laravel\Commands; +use Carbon\CarbonInterface; use Illuminate\Console\Command; use Illuminate\Support\Facades\Http; use Illuminate\Support\Number; @@ -34,7 +35,7 @@ public function handle() } if (! $this->checkAuthenticated()) { - $this->error('Invalid API token: check your ZEPHPYR_TOKEN on https://zephpyr.com/user/api-tokens'); + $this->error('Invalid API token: check your ZEPHPYR_TOKEN on https://'.$this->hostname().'/user/api-tokens'); return static::FAILURE; } @@ -67,9 +68,21 @@ public function handle() $this->cleanUp(); + return static::FAILURE; + } elseif ($result->status() === 422) { + $this->error('Zephpyr returned the following error:'); + $this->error(' → '.$result->json('message')); + $this->cleanUp(); + + return static::FAILURE; + } elseif ($result->status() === 429) { + $this->error('Zephpyr has a rate limit on builds per hour. Please try again in '.now()->addSeconds(intval($result->header('Retry-After')))->diffForHumans(syntax: CarbonInterface::DIFF_ABSOLUTE).'.'); + $this->cleanUp(); + return static::FAILURE; } elseif ($result->failed()) { - $this->error("Failed to upload zip to Zephpyr. Error: {$result->status()}"); + $this->error("Failed to upload zip to Zephpyr. Error code: {$result->status()}"); + ray($result->body()); $this->cleanUp(); return static::FAILURE; @@ -92,9 +105,11 @@ protected function cleanUp(): void $this->line('Cleaning up…'); $previousBuilds = glob(base_path('temp/app_*.zip')); + $failedZips = glob(base_path('temp/app_*.part')); - foreach ($previousBuilds as $previousBuild) { - @unlink($previousBuild); + $deleteFiles = array_merge($previousBuilds, $failedZips); + foreach ($deleteFiles as $file) { + @unlink($file); } } @@ -156,10 +171,12 @@ private function addFilesToZip(ZipArchive $zip): void $this->finderToZip($vendor, $zip, 'vendor'); - $nodeModules = (new Finder)->files() - ->in(base_path('node_modules')); + if (file_exists(base_path('node_modules'))) { + $nodeModules = (new Finder)->files() + ->in(base_path('node_modules')); - $this->finderToZip($nodeModules, $zip, 'node_modules'); + $this->finderToZip($nodeModules, $zip, 'node_modules'); + } } private function finderToZip(Finder $finder, ZipArchive $zip, ?string $path = null): void @@ -178,6 +195,11 @@ private function baseUrl(): string return str(config('nativephp-internal.zephpyr.host'))->finish('/'); } + protected function hostname(): string + { + return parse_url(config('nativephp-internal.zephpyr.host'), PHP_URL_HOST); + } + private function sendToZephpyr() { $this->line('Uploading zip to Zephpyr…'); @@ -186,7 +208,7 @@ private function sendToZephpyr() ->withoutRedirecting() // Upload won't work if we follow the redirect ->withToken(config('nativephp-internal.zephpyr.token')) ->attach('archive', fopen($this->zipPath, 'r'), $this->zipName) - ->post($this->baseUrl().'api/build/'.$this->key); + ->post($this->baseUrl().'api/v1/project/'.$this->key.'/build/'); } private function checkAuthenticated() @@ -195,14 +217,14 @@ private function checkAuthenticated() return Http::acceptJson() ->withToken(config('nativephp-internal.zephpyr.token')) - ->get($this->baseUrl().'api/user')->successful(); + ->get($this->baseUrl().'api/v1/user')->successful(); } private function fetchLatestBundle(): bool { $response = Http::acceptJson() ->withToken(config('nativephp-internal.zephpyr.token')) - ->get($this->baseUrl().'api/download/'.$this->key); + ->get($this->baseUrl().'api/v1/project/'.$this->key.'/build/download'); if ($response->failed()) { return false; @@ -225,7 +247,7 @@ private function checkForZephpyrKey() $this->line(base_path('.env')); $this->line(''); $this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!'); - $this->info('Check out https://zephpyr.com'); + $this->info('Check out https://'.$this->hostname().''); $this->line(''); return false; @@ -244,7 +266,7 @@ private function checkForZephpyrToken() $this->line(base_path('.env')); $this->line(''); $this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!'); - $this->info('Check out https://zephpyr.com'); + $this->info('Check out https://'.$this->hostname().''); $this->line(''); return false; From 6957ce6c1cdc4c79c680ace33f4d63959d82bfb1 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Mon, 13 Jan 2025 19:32:15 +0100 Subject: [PATCH 10/18] better errors --- config/nativephp-internal.php | 2 +- config/nativephp.php | 5 ++++ src/Commands/BundleCommand.php | 43 +++++++++++++++++++++------------- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/config/nativephp-internal.php b/config/nativephp-internal.php index a05783a..368b5b8 100644 --- a/config/nativephp-internal.php +++ b/config/nativephp-internal.php @@ -31,7 +31,7 @@ 'api_url' => env('NATIVEPHP_API_URL', 'http://localhost:4000/api/'), 'zephpyr' => [ - 'host' => env('ZEPHPYR_HOST', 'zephpyr.com'), + 'host' => env('ZEPHPYR_HOST', 'https://zephpyr.com'), 'token' => env('ZEPHPYR_TOKEN'), 'key' => env('ZEPHPYR_KEY'), ], diff --git a/config/nativephp.php b/config/nativephp.php index 37c3a89..f04b960 100644 --- a/config/nativephp.php +++ b/config/nativephp.php @@ -125,4 +125,9 @@ 'timeout' => 60, ], ], + + /** + * Custom PHP binary path. + */ + 'binary_path' => env('NATIVEPHP_BINARY_PATH', null), ]; diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index a14f717..3af8efa 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -4,6 +4,7 @@ use Carbon\CarbonInterface; use Illuminate\Console\Command; +use Illuminate\Http\Client\ConnectionException; use Illuminate\Support\Facades\Http; use Illuminate\Support\Number; use Native\Laravel\Commands\Traits\CleansEnvFile; @@ -35,7 +36,7 @@ public function handle() } if (! $this->checkAuthenticated()) { - $this->error('Invalid API token: check your ZEPHPYR_TOKEN on https://'.$this->hostname().'/user/api-tokens'); + $this->error('Invalid API token: check your ZEPHPYR_TOKEN on '.$this->baseUrl().'user/api-tokens'); return static::FAILURE; } @@ -60,7 +61,15 @@ public function handle() } // Send the zip file - $result = $this->sendToZephpyr(); + try { + $result = $this->sendToZephpyr(); + } catch (ConnectionException $e) { + // Timeout, etc. + $this->error('Failed to send to Zephpyr: '.$e->getMessage()); + $this->cleanUp(); + + return static::FAILURE; + } if ($result->status() === 413) { $fileSize = Number::fileSize(filesize($this->zipPath)); @@ -145,6 +154,9 @@ private function addFilesToZip(ZipArchive $zip): void // TODO: Check the composer.json to make sure there are no symlinked // or private packages as these will be a pain later + // TODO: Fail if there is symlinked packages + // TODO: For private packages: make an endpoint to check if user gave us their credentials + $this->line('Creating zip archive…'); $app = (new Finder)->files() @@ -152,21 +164,24 @@ private function addFilesToZip(ZipArchive $zip): void ->ignoreVCSIgnored(true) ->in(base_path()) ->exclude([ - 'vendor', - 'dist', - 'build', - 'tests', - ...config('nativephp.cleanup_exclude_files', []), + 'vendor', // We add this later + 'node_modules', // We add this later + 'dist', // Compiled nativephp assets + 'build', // Compiled box assets + 'tests', // Tests + ...config('nativephp.cleanup_exclude_files', []), // User defined ]); $this->finderToZip($app, $zip); $vendor = (new Finder)->files() - ->exclude([ + // ->followLinks() + ->exclude(array_filter([ 'nativephp/php-bin', 'nativephp/electron/resources/js', 'nativephp/*/vendor', - ]) + config('nativephp.binary_path'), // User defined binary paths + ])) ->in(base_path('vendor')); $this->finderToZip($vendor, $zip, 'vendor'); @@ -195,16 +210,12 @@ private function baseUrl(): string return str(config('nativephp-internal.zephpyr.host'))->finish('/'); } - protected function hostname(): string - { - return parse_url(config('nativephp-internal.zephpyr.host'), PHP_URL_HOST); - } - private function sendToZephpyr() { $this->line('Uploading zip to Zephpyr…'); return Http::acceptJson() + ->timeout(300) // 5 minutes ->withoutRedirecting() // Upload won't work if we follow the redirect ->withToken(config('nativephp-internal.zephpyr.token')) ->attach('archive', fopen($this->zipPath, 'r'), $this->zipName) @@ -247,7 +258,7 @@ private function checkForZephpyrKey() $this->line(base_path('.env')); $this->line(''); $this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!'); - $this->info('Check out https://'.$this->hostname().''); + $this->info('Check out '.$this->baseUrl().''); $this->line(''); return false; @@ -266,7 +277,7 @@ private function checkForZephpyrToken() $this->line(base_path('.env')); $this->line(''); $this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!'); - $this->info('Check out https://'.$this->hostname().''); + $this->info('Check out '.$this->baseUrl().''); $this->line(''); return false; From cf678699760b2533c523f65316caca9e6763eb35 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Mon, 13 Jan 2025 22:54:19 +0100 Subject: [PATCH 11/18] fix: .env not bundled + create build/ directory --- src/Commands/BundleCommand.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index 3af8efa..d055c63 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -168,14 +168,18 @@ private function addFilesToZip(ZipArchive $zip): void 'node_modules', // We add this later 'dist', // Compiled nativephp assets 'build', // Compiled box assets + 'temp', // Temp files 'tests', // Tests ...config('nativephp.cleanup_exclude_files', []), // User defined ]); $this->finderToZip($app, $zip); + // Add .env file + $zip->addFile(base_path('.env'), '.env'); + $vendor = (new Finder)->files() - // ->followLinks() + // ->followLinks() // This is causing issues with excluded files ->exclude(array_filter([ 'nativephp/php-bin', 'nativephp/electron/resources/js', @@ -233,6 +237,8 @@ private function checkAuthenticated() private function fetchLatestBundle(): bool { + $this->line('Fetching latest bundle…'); + $response = Http::acceptJson() ->withToken(config('nativephp-internal.zephpyr.token')) ->get($this->baseUrl().'api/v1/project/'.$this->key.'/build/download'); @@ -241,6 +247,7 @@ private function fetchLatestBundle(): bool return false; } + @mkdir(base_path('build'), recursive: true); file_put_contents(base_path('build/__nativephp_app_bundle'), $response->body()); return true; From 26477cdcc14762a3817ff7995dfb0a67dab8d0ef Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Tue, 14 Jan 2025 10:38:50 +0100 Subject: [PATCH 12/18] cleanup + early private packages support --- src/Commands/BundleCommand.php | 210 +++++++++++----------- src/Commands/Traits/HandleApiRequests.php | 62 +++++++ 2 files changed, 165 insertions(+), 107 deletions(-) create mode 100644 src/Commands/Traits/HandleApiRequests.php diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index d055c63..ac49ed9 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -4,16 +4,18 @@ use Carbon\CarbonInterface; use Illuminate\Console\Command; -use Illuminate\Http\Client\ConnectionException; +use Illuminate\Http\Client\Response; use Illuminate\Support\Facades\Http; use Illuminate\Support\Number; +use Illuminate\Support\Str; use Native\Laravel\Commands\Traits\CleansEnvFile; +use Native\Laravel\Commands\Traits\HandleApiRequests; use Symfony\Component\Finder\Finder; use ZipArchive; class BundleCommand extends Command { - use CleansEnvFile; + use CleansEnvFile, HandleApiRequests; protected $signature = 'native:bundle {--fetch} {--without-cleanup}'; @@ -25,25 +27,28 @@ class BundleCommand extends Command private string $zipName; - public function handle() + public function handle(): int { + // Check for ZEPHPYR_KEY if (! $this->checkForZephpyrKey()) { return static::FAILURE; } + // Check for ZEPHPYR_TOKEN if (! $this->checkForZephpyrToken()) { return static::FAILURE; } + // Check if the token is valid if (! $this->checkAuthenticated()) { $this->error('Invalid API token: check your ZEPHPYR_TOKEN on '.$this->baseUrl().'user/api-tokens'); return static::FAILURE; } + // Download the latest bundle if requested if ($this->option('fetch')) { if (! $this->fetchLatestBundle()) { - $this->warn('Latest bundle not yet available. Try again soon.'); return static::FAILURE; } @@ -53,6 +58,11 @@ public function handle() return static::SUCCESS; } + // Check composer.json for symlinked or private packages + if (! $this->checkComposerJson()) { + return static::FAILURE; + } + // Package the app up into a zip if (! $this->zipApplication()) { $this->error("Failed to create zip archive at {$this->zipPath}."); @@ -61,67 +71,19 @@ public function handle() } // Send the zip file - try { - $result = $this->sendToZephpyr(); - } catch (ConnectionException $e) { - // Timeout, etc. - $this->error('Failed to send to Zephpyr: '.$e->getMessage()); - $this->cleanUp(); - - return static::FAILURE; - } - - if ($result->status() === 413) { - $fileSize = Number::fileSize(filesize($this->zipPath)); - $this->error('The zip file is too large to upload to Zephpyr ('.$fileSize.'). Please contact support.'); - - $this->cleanUp(); - - return static::FAILURE; - } elseif ($result->status() === 422) { - $this->error('Zephpyr returned the following error:'); - $this->error(' → '.$result->json('message')); - $this->cleanUp(); - - return static::FAILURE; - } elseif ($result->status() === 429) { - $this->error('Zephpyr has a rate limit on builds per hour. Please try again in '.now()->addSeconds(intval($result->header('Retry-After')))->diffForHumans(syntax: CarbonInterface::DIFF_ABSOLUTE).'.'); - $this->cleanUp(); - - return static::FAILURE; - } elseif ($result->failed()) { - $this->error("Failed to upload zip to Zephpyr. Error code: {$result->status()}"); - ray($result->body()); - $this->cleanUp(); - - return static::FAILURE; - } + $result = $this->sendToZephpyr(); + $this->handleApiErrors($result); + // Success $this->info('Successfully uploaded to Zephpyr.'); $this->line('Use native:bundle --fetch to retrieve the latest bundle.'); + // Clean up temp files $this->cleanUp(); return static::SUCCESS; } - protected function cleanUp(): void - { - if ($this->option('without-cleanup')) { - return; - } - - $this->line('Cleaning up…'); - - $previousBuilds = glob(base_path('temp/app_*.zip')); - $failedZips = glob(base_path('temp/app_*.part')); - - $deleteFiles = array_merge($previousBuilds, $failedZips); - foreach ($deleteFiles as $file) { - @unlink($file); - } - } - private function zipApplication(): bool { $this->zipName = 'app_'.str()->random(8).'.zip'; @@ -149,14 +111,42 @@ private function zipApplication(): bool return true; } - private function addFilesToZip(ZipArchive $zip): void + private function checkComposerJson(): bool { - // TODO: Check the composer.json to make sure there are no symlinked - // or private packages as these will be a pain later + $composerJson = json_decode(file_get_contents(base_path('composer.json')), true); + + // Fail if there is symlinked packages + foreach ($composerJson['repositories'] ?? [] as $repository) { + if ($repository['type'] === 'path') { + $this->error('Symlinked packages are not supported. Please remove them from your composer.json.'); + + return false; + } elseif ($repository['type'] === 'composer') { + if (! $this->checkComposerPackageAuth($repository['url'])) { + $this->error('Cannot authenticate with '.$repository['url'].'.'); + $this->error('Go to '.$this->baseUrl().' and add your credentials for '.$repository['url'].'.'); + + return false; + } + } + } + + return true; + } - // TODO: Fail if there is symlinked packages - // TODO: For private packages: make an endpoint to check if user gave us their credentials + private function checkComposerPackageAuth(string $repositoryUrl): bool + { + $host = parse_url($repositoryUrl, PHP_URL_HOST); + $this->line('Checking '.$host.' authentication…'); + return Http::acceptJson() + ->withToken(config('nativephp-internal.zephpyr.token')) + ->get($this->baseUrl().'api/v1/project/'.$this->key.'/composer/auth/'.$host) + ->successful(); + } + + private function addFilesToZip(ZipArchive $zip): void + { $this->line('Creating zip archive…'); $app = (new Finder)->files() @@ -178,18 +168,22 @@ private function addFilesToZip(ZipArchive $zip): void // Add .env file $zip->addFile(base_path('.env'), '.env'); + // Custom binaries + $binaryPath = Str::replaceStart(base_path('vendor'), '', config('nativephp.binary_path')); + + // Add composer dependencies without unnecessary files $vendor = (new Finder)->files() - // ->followLinks() // This is causing issues with excluded files ->exclude(array_filter([ 'nativephp/php-bin', 'nativephp/electron/resources/js', 'nativephp/*/vendor', - config('nativephp.binary_path'), // User defined binary paths + $binaryPath, ])) ->in(base_path('vendor')); $this->finderToZip($vendor, $zip, 'vendor'); + // Add javascript dependencies if (file_exists(base_path('node_modules'))) { $nodeModules = (new Finder)->files() ->in(base_path('node_modules')); @@ -209,32 +203,18 @@ private function finderToZip(Finder $finder, ZipArchive $zip, ?string $path = nu } } - private function baseUrl(): string - { - return str(config('nativephp-internal.zephpyr.host'))->finish('/'); - } - private function sendToZephpyr() { $this->line('Uploading zip to Zephpyr…'); return Http::acceptJson() ->timeout(300) // 5 minutes - ->withoutRedirecting() // Upload won't work if we follow the redirect + ->withoutRedirecting() // Upload won't work if we follow redirects (it transform POST to GET) ->withToken(config('nativephp-internal.zephpyr.token')) ->attach('archive', fopen($this->zipPath, 'r'), $this->zipName) ->post($this->baseUrl().'api/v1/project/'.$this->key.'/build/'); } - private function checkAuthenticated() - { - $this->line('Checking authentication…'); - - return Http::acceptJson() - ->withToken(config('nativephp-internal.zephpyr.token')) - ->get($this->baseUrl().'api/v1/user')->successful(); - } - private function fetchLatestBundle(): bool { $this->line('Fetching latest bundle…'); @@ -244,52 +224,68 @@ private function fetchLatestBundle(): bool ->get($this->baseUrl().'api/v1/project/'.$this->key.'/build/download'); if ($response->failed()) { + + if ($response->status() === 404) { + $this->error('Project or bundle not found.'); + } elseif ($response->status() === 500) { + $this->error('Build failed. Please try again later.'); + } elseif ($response->status() === 503) { + $this->warn('Bundle not ready. Please try again in '.now()->addSeconds(intval($response->header('Retry-After')))->diffForHumans(syntax: CarbonInterface::DIFF_ABSOLUTE).'.'); + } else { + $this->handleApiErrors($response); + } + return false; } + // Save the bundle @mkdir(base_path('build'), recursive: true); file_put_contents(base_path('build/__nativephp_app_bundle'), $response->body()); return true; } - private function checkForZephpyrKey() + protected function exitWithMessage(string $message): void { - $this->key = config('nativephp-internal.zephpyr.key'); - - if (! $this->key) { - $this->line(''); - $this->warn('No ZEPHPYR_KEY found. Cannot bundle!'); - $this->line(''); - $this->line('Add this app\'s ZEPHPYR_KEY to its .env file:'); - $this->line(base_path('.env')); - $this->line(''); - $this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!'); - $this->info('Check out '.$this->baseUrl().''); - $this->line(''); + $this->error($message); + $this->cleanUp(); - return false; - } + exit(static::FAILURE); + } - return true; + private function handleApiErrors(Response $result): void + { + if ($result->status() === 413) { + $fileSize = Number::fileSize(filesize($this->zipPath)); + $this->exitWithMessage('File is too large to upload ('.$fileSize.'). Please contact support.'); + } elseif ($result->status() === 422) { + $this->error('Request refused:'.$result->json('message')); + } elseif ($result->status() === 429) { + $this->exitWithMessage('Too many requests. Please try again in '.now()->addSeconds(intval($result->header('Retry-After')))->diffForHumans(syntax: CarbonInterface::DIFF_ABSOLUTE).'.'); + } elseif ($result->failed()) { + $this->exitWithMessage("Request failed. Error code: {$result->status()}"); + } } - private function checkForZephpyrToken() + protected function cleanUp(): void { - if (! config('nativephp-internal.zephpyr.token')) { - $this->line(''); - $this->warn('No ZEPHPYR_TOKEN found. Cannot bundle!'); - $this->line(''); - $this->line('Add your api ZEPHPYR_TOKEN to its .env file:'); - $this->line(base_path('.env')); - $this->line(''); - $this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!'); - $this->info('Check out '.$this->baseUrl().''); - $this->line(''); + if ($this->option('without-cleanup')) { + return; + } - return false; + $previousBuilds = glob(base_path('temp/app_*.zip')); + $failedZips = glob(base_path('temp/app_*.part')); + + $deleteFiles = array_merge($previousBuilds, $failedZips); + + if (empty($deleteFiles)) { + return; } - return true; + $this->line('Cleaning up…'); + + foreach ($deleteFiles as $file) { + @unlink($file); + } } } diff --git a/src/Commands/Traits/HandleApiRequests.php b/src/Commands/Traits/HandleApiRequests.php new file mode 100644 index 0000000..ba6fe20 --- /dev/null +++ b/src/Commands/Traits/HandleApiRequests.php @@ -0,0 +1,62 @@ +finish('/'); + } + + private function checkAuthenticated() + { + $this->line('Checking authentication…'); + + return Http::acceptJson() + ->withToken(config('nativephp-internal.zephpyr.token')) + ->get($this->baseUrl().'api/v1/user')->successful(); + } + + private function checkForZephpyrKey() + { + $this->key = config('nativephp-internal.zephpyr.key'); + + if (! $this->key) { + $this->line(''); + $this->warn('No ZEPHPYR_KEY found. Cannot bundle!'); + $this->line(''); + $this->line('Add this app\'s ZEPHPYR_KEY to its .env file:'); + $this->line(base_path('.env')); + $this->line(''); + $this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!'); + $this->info('Check out '.$this->baseUrl().''); + $this->line(''); + + return false; + } + + return true; + } + + private function checkForZephpyrToken() + { + if (! config('nativephp-internal.zephpyr.token')) { + $this->line(''); + $this->warn('No ZEPHPYR_TOKEN found. Cannot bundle!'); + $this->line(''); + $this->line('Add your api ZEPHPYR_TOKEN to its .env file:'); + $this->line(base_path('.env')); + $this->line(''); + $this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!'); + $this->info('Check out '.$this->baseUrl().''); + $this->line(''); + + return false; + } + + return true; + } +} From 2d4a27b7bcef81c9987b86d1a16a544615fbaef3 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Tue, 14 Jan 2025 11:25:42 +0100 Subject: [PATCH 13/18] alternative way to support private packages --- src/Commands/BundleCommand.php | 40 +++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index ac49ed9..74b6520 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -121,29 +121,30 @@ private function checkComposerJson(): bool $this->error('Symlinked packages are not supported. Please remove them from your composer.json.'); return false; - } elseif ($repository['type'] === 'composer') { - if (! $this->checkComposerPackageAuth($repository['url'])) { - $this->error('Cannot authenticate with '.$repository['url'].'.'); - $this->error('Go to '.$this->baseUrl().' and add your credentials for '.$repository['url'].'.'); - - return false; - } } + // elseif ($repository['type'] === 'composer') { + // if (! $this->checkComposerPackageAuth($repository['url'])) { + // $this->error('Cannot authenticate with '.$repository['url'].'.'); + // $this->error('Go to '.$this->baseUrl().' and add your composer package credentials.'); + // + // return false; + // } + // } } return true; } - private function checkComposerPackageAuth(string $repositoryUrl): bool - { - $host = parse_url($repositoryUrl, PHP_URL_HOST); - $this->line('Checking '.$host.' authentication…'); - - return Http::acceptJson() - ->withToken(config('nativephp-internal.zephpyr.token')) - ->get($this->baseUrl().'api/v1/project/'.$this->key.'/composer/auth/'.$host) - ->successful(); - } + // private function checkComposerPackageAuth(string $repositoryUrl): bool + // { + // $host = parse_url($repositoryUrl, PHP_URL_HOST); + // $this->line('Checking '.$host.' authentication…'); + // + // return Http::acceptJson() + // ->withToken(config('nativephp-internal.zephpyr.token')) + // ->get($this->baseUrl().'api/v1/project/'.$this->key.'/composer/auth/'.$host) + // ->successful(); + // } private function addFilesToZip(ZipArchive $zip): void { @@ -165,9 +166,12 @@ private function addFilesToZip(ZipArchive $zip): void $this->finderToZip($app, $zip); - // Add .env file + // Add .env file manually because Finder ignores hidden files $zip->addFile(base_path('.env'), '.env'); + // Add auth.json file to support private packages + $zip->addFile(base_path('auth.json'), 'auth.json'); + // Custom binaries $binaryPath = Str::replaceStart(base_path('vendor'), '', config('nativephp.binary_path')); From c67ccb86e83ae37cdd9e8514e59064b994467aff Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Fri, 24 Jan 2025 13:40:09 +0100 Subject: [PATCH 14/18] fix: security --- src/Commands/BundleCommand.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index 74b6520..16f94d3 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -122,6 +122,7 @@ private function checkComposerJson(): bool return false; } + // Work with private packages but will not in the future // elseif ($repository['type'] === 'composer') { // if (! $this->checkComposerPackageAuth($repository['url'])) { // $this->error('Cannot authenticate with '.$repository['url'].'.'); @@ -170,7 +171,8 @@ private function addFilesToZip(ZipArchive $zip): void $zip->addFile(base_path('.env'), '.env'); // Add auth.json file to support private packages - $zip->addFile(base_path('auth.json'), 'auth.json'); + // WARNING: Only for testing purposes, don't uncomment this + // $zip->addFile(base_path('auth.json'), 'auth.json'); // Custom binaries $binaryPath = Str::replaceStart(base_path('vendor'), '', config('nativephp.binary_path')); From d4ca449712aec94638d7bc3271edfe0496e3225c Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Wed, 29 Jan 2025 10:51:39 +0100 Subject: [PATCH 15/18] fix: exclude nativephp from symlinked packages error --- src/Commands/BundleCommand.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index 16f94d3..2927809 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -116,8 +116,10 @@ private function checkComposerJson(): bool $composerJson = json_decode(file_get_contents(base_path('composer.json')), true); // Fail if there is symlinked packages - foreach ($composerJson['repositories'] ?? [] as $repository) { - if ($repository['type'] === 'path') { + foreach ($composerJson['repositories'] ?? [] as $key => $repository) { + + // Unless Eser is working on a PR ^^ + if ($repository['type'] === 'path' && ! in_array($key, ['nativephp-laravel', 'nativephp-electron'])) { $this->error('Symlinked packages are not supported. Please remove them from your composer.json.'); return false; From f00208dbc3dcbd44475a6322d28e9e0cee202915 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Wed, 29 Jan 2025 11:52:52 +0100 Subject: [PATCH 16/18] fix: symlinked packages error --- src/Commands/BundleCommand.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index 2927809..8ace10c 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -116,10 +116,10 @@ private function checkComposerJson(): bool $composerJson = json_decode(file_get_contents(base_path('composer.json')), true); // Fail if there is symlinked packages - foreach ($composerJson['repositories'] ?? [] as $key => $repository) { + foreach ($composerJson['repositories'] ?? [] as $repository) { - // Unless Eser is working on a PR ^^ - if ($repository['type'] === 'path' && ! in_array($key, ['nativephp-laravel', 'nativephp-electron'])) { + $symlinked = $repository['options']['symlink'] ?? true; + if ($repository['type'] === 'path' && $symlinked) { $this->error('Symlinked packages are not supported. Please remove them from your composer.json.'); return false; From 1c00a02d315935d4dedd8c5735e2809aee2688a0 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Wed, 29 Jan 2025 12:01:06 +0100 Subject: [PATCH 17/18] fix: database path in bundle --- src/NativeServiceProvider.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NativeServiceProvider.php b/src/NativeServiceProvider.php index 5c30c6b..0fbfc62 100644 --- a/src/NativeServiceProvider.php +++ b/src/NativeServiceProvider.php @@ -26,6 +26,7 @@ use Native\Laravel\Logging\LogWatcher; use Native\Laravel\PowerMonitor as PowerMonitorImplementation; use Native\Laravel\Windows\WindowManager as WindowManagerImplementation; +use Phar; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -150,7 +151,7 @@ public function rewriteDatabase() { $databasePath = config('nativephp-internal.database_path'); - if (config('app.debug')) { + if (config('app.debug') && !Phar::running()) { $databasePath = database_path('nativephp.sqlite'); if (! file_exists($databasePath)) { From 2027cd2359347a4fceef299ee845990db9727626 Mon Sep 17 00:00:00 2001 From: SRWieZ <1408020+SRWieZ@users.noreply.github.com> Date: Wed, 29 Jan 2025 11:01:35 +0000 Subject: [PATCH 18/18] Fix styling --- src/NativeServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NativeServiceProvider.php b/src/NativeServiceProvider.php index 0fbfc62..784f2a7 100644 --- a/src/NativeServiceProvider.php +++ b/src/NativeServiceProvider.php @@ -151,7 +151,7 @@ public function rewriteDatabase() { $databasePath = config('nativephp-internal.database_path'); - if (config('app.debug') && !Phar::running()) { + if (config('app.debug') && ! Phar::running()) { $databasePath = database_path('nativephp.sqlite'); if (! file_exists($databasePath)) {