diff --git a/config/nativephp-internal.php b/config/nativephp-internal.php index 4210df9..bb7dbf2 100644 --- a/config/nativephp-internal.php +++ b/config/nativephp-internal.php @@ -29,4 +29,27 @@ * The URL to the NativePHP API. */ 'api_url' => env('NATIVEPHP_API_URL', 'http://localhost:4000/api/'), + + /** + * Configuration for the Zephpyr API. + */ + 'zephpyr' => [ + 'host' => env('ZEPHPYR_HOST', 'https://zephpyr.com'), + 'token' => env('ZEPHPYR_TOKEN'), + 'key' => env('ZEPHPYR_KEY'), + ], + + /** + * The credentials to use Apples Notarization service. + */ + 'notarization' => [ + 'apple_id' => env('NATIVEPHP_APPLE_ID'), + 'apple_id_pass' => env('NATIVEPHP_APPLE_ID_PASS'), + 'apple_team_id' => env('NATIVEPHP_APPLE_TEAM_ID'), + ], + + /** + * The binary path of PHP for NativePHP to use at build. + */ + 'php_binary_path' => env('NATIVEPHP_PHP_BINARY_PATH'), ]; diff --git a/config/nativephp.php b/config/nativephp.php index 6e4edcf..1ecd30a 100644 --- a/config/nativephp.php +++ b/config/nativephp.php @@ -48,6 +48,7 @@ 'GITHUB_*', 'DO_SPACES_*', '*_SECRET', + 'ZEPHPYR_*', 'NATIVEPHP_UPDATER_PATH', 'NATIVEPHP_APPLE_ID', 'NATIVEPHP_APPLE_ID_PASS', @@ -60,6 +61,8 @@ * You may use glob / wildcard patterns here. */ 'cleanup_exclude_files' => [ + 'build', + 'temp', 'content', 'node_modules', '*/tests', @@ -136,4 +139,9 @@ 'postbuild' => [ // 'rm -rf public/build', ], + + /** + * Custom PHP binary path. + */ + 'binary_path' => env('NATIVEPHP_PHP_BINARY_PATH', null), ]; diff --git a/src/App.php b/src/App.php index 81f1a26..039579e 100644 --- a/src/App.php +++ b/src/App.php @@ -3,11 +3,17 @@ namespace Native\Laravel; use Native\Laravel\Client\Client; +use Phar; class App { public function __construct(protected Client $client) {} + public function quit(): void + { + $this->client->post('app/quit'); + } + public function focus(): void { $this->client->post('app/focus'); @@ -57,4 +63,23 @@ public function clearRecentDocuments(): void { $this->client->delete('app/recent-documents'); } + + public function isRunningBundled(): bool + { + return Phar::running() !== ''; + + } + + public function openAtLogin(?bool $open = null): bool + { + if ($open === null) { + return (bool) $this->client->get('app/open-at-login')->json('open'); + } + + $this->client->post('app/open-at-login', [ + 'open' => $open, + ]); + + return $open; + } } diff --git a/src/ChildProcess.php b/src/ChildProcess.php index f524370..19a9420 100644 --- a/src/ChildProcess.php +++ b/src/ChildProcess.php @@ -80,7 +80,7 @@ public function start( * @param string|string[] $cmd * @return $this */ - public function php(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false): self + public function php(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false, ?array $iniSettings = null): self { $cmd = is_array($cmd) ? array_values($cmd) : [$cmd]; @@ -90,6 +90,7 @@ public function php(string|array $cmd, string $alias, ?array $env = null, ?bool 'cwd' => base_path(), 'env' => $env, 'persistent' => $persistent, + 'iniSettings' => $iniSettings, ])->json(); return $this->fromRuntimeProcess($process); @@ -99,13 +100,13 @@ public function php(string|array $cmd, string $alias, ?array $env = null, ?bool * @param string|string[] $cmd * @return $this */ - public function artisan(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false): self + public function artisan(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false, ?array $iniSettings = null): self { $cmd = is_array($cmd) ? array_values($cmd) : [$cmd]; $cmd = ['artisan', ...$cmd]; - return $this->php($cmd, $alias, env: $env, persistent: $persistent); + return $this->php($cmd, $alias, env: $env, persistent: $persistent, iniSettings: $iniSettings); } public function stop(?string $alias = null): void diff --git a/src/Commands/DebugCommand.php b/src/Commands/DebugCommand.php new file mode 100644 index 0000000..3900d3c --- /dev/null +++ b/src/Commands/DebugCommand.php @@ -0,0 +1,165 @@ +debugInfo = collect(); + intro('Generating Debug Information...'); + + $this->processEnvironment() + ->processNativePHP(); + + switch ($this->argument('output')) { + case 'File': + $this->outputToFile(); + break; + case 'Clipboard': + $this->outputToClipboard(); + break; + case 'Console': + $this->outputToConsole(); + break; + default: + error('Invalid output option specified.'); + } + + outro('Debug Information Generated.'); + } + + private function processEnvironment(): static + { + $locationCommand = 'which'; + + if (PHP_OS_FAMILY === 'Windows') { + $locationCommand = 'where'; + } + + info('Generating Environment Data...'); + $environment = [ + 'PHP' => [ + 'Version' => phpversion(), + 'Path' => PHP_BINARY, + ], + 'Laravel' => [ + 'Version' => app()->version(), + 'ConfigCached' => file_exists($this->laravel->getCachedConfigPath()), + 'DebugEnabled' => $this->laravel->hasDebugModeEnabled(), + ], + 'Node' => [ + 'Version' => trim(Process::run('node -v')->output()), + 'Path' => trim(Process::run("$locationCommand node")->output()), + ], + 'NPM' => [ + 'Version' => trim(Process::run('npm -v')->output()), + 'Path' => trim(Process::run("$locationCommand npm")->output()), + ], + 'OperatingSystem' => PHP_OS, + ]; + + $this->debugInfo->put('Environment', $environment); + + return $this; + } + + private function processNativePHP(): static + { + info('Processing NativePHP Data...'); + // Get composer versions + $versions = collect([ + 'nativephp/electron' => null, + 'nativephp/laravel' => null, + 'nativephp/php-bin' => null, + ])->mapWithKeys(function ($version, $key) { + try { + $version = InstalledVersions::getVersion($key); + } catch (\OutOfBoundsException) { + $version = 'Not Installed'; + } + + return [$key => $version]; + }); + + $isNotarizationConfigured = config('nativephp-internal.notarization.apple_id') + && config('nativephp-internal.notarization.apple_id_pass') + && config('nativephp-internal.notarization.apple_team_id'); + + $this->debugInfo->put( + 'NativePHP', + [ + 'Versions' => $versions, + 'Configuration' => [ + 'Provider' => config('nativephp.provider'), + 'BuildHooks' => [ + 'Pre' => config('nativephp.prebuild'), + 'Post' => config('nativephp.postbuild'), + ], + 'NotarizationEnabled' => $isNotarizationConfigured, + 'CustomPHPBinary' => config('nativephp-internal.php_binary_path') ?? false, + ], + ] + ); + + return $this; + } + + protected function promptForMissingArgumentsUsing(): array + { + return [ + 'output' => fn () => select( + 'Where would you like to output the debug information?', + ['File', 'Clipboard', 'Console'], + 'File' + ), + ]; + } + + private function outputToFile(): void + { + File::put(base_path('nativephp_debug.json'), json_encode($this->debugInfo->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + note('Debug information saved to '.base_path('nativephp_debug.json')); + } + + private function outputToConsole(): void + { + $this->output->writeln( + print_r($this->debugInfo->toArray(), true) + ); + } + + private function outputToClipboard(): void + { + $json = json_encode($this->debugInfo->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + + // Copy json to clipboard + if (PHP_OS_FAMILY === 'Windows') { + Process::run('echo '.escapeshellarg($json).' | clip'); + } elseif (PHP_OS_FAMILY === 'Linux') { + Process::run('echo '.escapeshellarg($json).' | xclip -selection clipboard'); + } else { + Process::run('echo '.escapeshellarg($json).' | pbcopy'); + } + } +} diff --git a/src/Contracts/ChildProcess.php b/src/Contracts/ChildProcess.php index 9859e3e..0a4f877 100644 --- a/src/Contracts/ChildProcess.php +++ b/src/Contracts/ChildProcess.php @@ -16,9 +16,9 @@ public function start( bool $persistent = false ): self; - public function php(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false): self; + public function php(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false, ?array $iniSettings = null): self; - public function artisan(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false): self; + public function artisan(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false, ?array $iniSettings = null): self; public function stop(?string $alias = null): void; diff --git a/src/Events/ChildProcess/StartupError.php b/src/Events/ChildProcess/StartupError.php new file mode 100644 index 0000000..8adb2ed --- /dev/null +++ b/src/Events/ChildProcess/StartupError.php @@ -0,0 +1,22 @@ +phps[] = [ 'cmd' => $cmd, 'alias' => $alias, 'env' => $env, 'persistent' => $persistent, + 'iniSettings' => $iniSettings, ]; return $this; @@ -93,13 +95,15 @@ public function artisan( array|string $cmd, string $alias, ?array $env = null, - ?bool $persistent = false + ?bool $persistent = false, + ?array $iniSettings = null ): self { $this->artisans[] = [ 'cmd' => $cmd, 'alias' => $alias, 'env' => $env, 'persistent' => $persistent, + 'iniSettings' => $iniSettings, ]; return $this; diff --git a/src/NativeServiceProvider.php b/src/NativeServiceProvider.php index b5b3225..7e000a7 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\DebugCommand; use Native\Laravel\Commands\FreshCommand; use Native\Laravel\Commands\LoadPHPConfigurationCommand; use Native\Laravel\Commands\LoadStartupConfigurationCommand; @@ -35,8 +36,9 @@ public function configurePackage(Package $package): void $package ->name('nativephp') ->hasCommands([ - MigrateCommand::class, + DebugCommand::class, FreshCommand::class, + MigrateCommand::class, SeedDatabaseCommand::class, ]) ->hasConfigFile() @@ -148,6 +150,7 @@ public function rewriteDatabase() { $databasePath = config('nativephp-internal.database_path'); + // Automatically create the database in development mode if (config('app.debug')) { $databasePath = database_path('nativephp.sqlite'); diff --git a/src/QueueWorker.php b/src/QueueWorker.php index 1eb0c00..875a5c2 100644 --- a/src/QueueWorker.php +++ b/src/QueueWorker.php @@ -24,24 +24,24 @@ public function up(string|QueueConfig $config): void throw new \InvalidArgumentException("Invalid queue configuration alias [$config]"); } - $this->childProcess->php( + $this->childProcess->artisan( [ - '-d', - "memory_limit={$config->memoryLimit}M", - 'artisan', 'queue:work', "--name={$config->alias}", '--queue='.implode(',', $config->queuesToConsume), "--memory={$config->memoryLimit}", "--timeout={$config->timeout}", ], - $config->alias, + 'queue_'.$config->alias, persistent: true, + iniSettings: [ + 'memory_limit' => "{$config->memoryLimit}M", + ] ); } public function down(string $alias): void { - $this->childProcess->stop($alias); + $this->childProcess->stop('queue_'.$alias); } } diff --git a/tests/Fakes/FakeChildProcessTest.php b/tests/Fakes/FakeChildProcessTest.php index 57b0c35..aae9d26 100644 --- a/tests/Fakes/FakeChildProcessTest.php +++ b/tests/Fakes/FakeChildProcessTest.php @@ -82,18 +82,18 @@ $fake->php('cmdA', 'aliasA', ['envA'], true); $fake->php('cmdB', 'aliasB', ['envB'], false); - $fake->assertPhp(fn ($cmd, $alias, $env, $persistent) => $alias === 'aliasA' && + $fake->assertPhp(fn ($cmd, $alias, $env, $persistent, $iniSettings) => $alias === 'aliasA' && $cmd === 'cmdA' && $env === ['envA'] && $persistent === true); - $fake->assertPhp(fn ($cmd, $alias, $env, $persistent) => $alias === 'aliasB' && + $fake->assertPhp(fn ($cmd, $alias, $env, $persistent, $iniSettings) => $alias === 'aliasB' && $cmd === 'cmdB' && $env === ['envB'] && $persistent === false); try { - $fake->assertPhp(fn ($cmd, $alias, $env, $persistent) => $alias === 'aliasC'); + $fake->assertPhp(fn ($cmd, $alias, $env, $persistent, $iniSettings) => $alias === 'aliasC'); } catch (AssertionFailedError) { return; } @@ -107,18 +107,18 @@ $fake->artisan('cmdA', 'aliasA', ['envA'], true); $fake->artisan('cmdB', 'aliasB', ['envB'], false); - $fake->assertArtisan(fn ($cmd, $alias, $env, $persistent) => $alias === 'aliasA' && + $fake->assertArtisan(fn ($cmd, $alias, $env, $persistent, $iniSettings) => $alias === 'aliasA' && $cmd === 'cmdA' && $env === ['envA'] && $persistent === true); - $fake->assertArtisan(fn ($cmd, $alias, $env, $persistent) => $alias === 'aliasB' && + $fake->assertArtisan(fn ($cmd, $alias, $env, $persistent, $iniSettings) => $alias === 'aliasB' && $cmd === 'cmdB' && $env === ['envB'] && $persistent === false); try { - $fake->assertArtisan(fn ($cmd, $alias, $env, $persistent) => $alias === 'aliasC'); + $fake->assertArtisan(fn ($cmd, $alias, $env, $persistent, $iniSettings) => $alias === 'aliasC'); } catch (AssertionFailedError) { return; } diff --git a/tests/QueueWorker/QueueWorkerTest.php b/tests/QueueWorker/QueueWorkerTest.php index a3fbd57..2a6c137 100644 --- a/tests/QueueWorker/QueueWorkerTest.php +++ b/tests/QueueWorker/QueueWorkerTest.php @@ -6,23 +6,27 @@ it('hits the child process with relevant queue config to spin up a new queue worker', function () { ChildProcess::fake(); - $config = new QueueConfig('some_worker', ['default'], 128, 61); + + $workerName = 'some_worker'; + + $config = new QueueConfig($workerName, ['default'], 128, 61); QueueWorker::up($config); - ChildProcess::assertPhp(function (array $cmd, string $alias, $env, $persistent) { + ChildProcess::assertArtisan(function (array $cmd, string $alias, $env, $persistent, $iniSettings) use ($workerName) { expect($cmd)->toBe([ - '-d', - 'memory_limit=128M', - 'artisan', 'queue:work', - "--name={$alias}", + "--name={$workerName}", '--queue=default', '--memory=128', '--timeout=61', ]); - expect($alias)->toBe('some_worker'); + expect($iniSettings)->toBe([ + 'memory_limit' => '128M', + ]); + + expect($alias)->toBe('queue_some_worker'); expect($env)->toBeNull(); expect($persistent)->toBeTrue(); @@ -35,5 +39,5 @@ QueueWorker::down('some_worker'); - ChildProcess::assertStop('some_worker'); + ChildProcess::assertStop('queue_some_worker'); });