44
55use Carbon \CarbonInterface ;
66use Illuminate \Console \Command ;
7- use Illuminate \Http \Client \ConnectionException ;
7+ use Illuminate \Http \Client \Response ;
88use Illuminate \Support \Facades \Http ;
99use Illuminate \Support \Number ;
10+ use Illuminate \Support \Str ;
1011use Native \Laravel \Commands \Traits \CleansEnvFile ;
12+ use Native \Laravel \Commands \Traits \HandleApiRequests ;
1113use Symfony \Component \Finder \Finder ;
1214use ZipArchive ;
1315
1416class BundleCommand extends Command
1517{
16- use CleansEnvFile;
18+ use CleansEnvFile, HandleApiRequests ;
1719
1820 protected $ signature = 'native:bundle {--fetch} {--without-cleanup} ' ;
1921
@@ -25,25 +27,28 @@ class BundleCommand extends Command
2527
2628 private string $ zipName ;
2729
28- public function handle ()
30+ public function handle (): int
2931 {
32+ // Check for ZEPHPYR_KEY
3033 if (! $ this ->checkForZephpyrKey ()) {
3134 return static ::FAILURE ;
3235 }
3336
37+ // Check for ZEPHPYR_TOKEN
3438 if (! $ this ->checkForZephpyrToken ()) {
3539 return static ::FAILURE ;
3640 }
3741
42+ // Check if the token is valid
3843 if (! $ this ->checkAuthenticated ()) {
3944 $ this ->error ('Invalid API token: check your ZEPHPYR_TOKEN on ' .$ this ->baseUrl ().'user/api-tokens ' );
4045
4146 return static ::FAILURE ;
4247 }
4348
49+ // Download the latest bundle if requested
4450 if ($ this ->option ('fetch ' )) {
4551 if (! $ this ->fetchLatestBundle ()) {
46- $ this ->warn ('Latest bundle not yet available. Try again soon. ' );
4752
4853 return static ::FAILURE ;
4954 }
@@ -53,6 +58,11 @@ public function handle()
5358 return static ::SUCCESS ;
5459 }
5560
61+ // Check composer.json for symlinked or private packages
62+ if (! $ this ->checkComposerJson ()) {
63+ return static ::FAILURE ;
64+ }
65+
5666 // Package the app up into a zip
5767 if (! $ this ->zipApplication ()) {
5868 $ this ->error ("Failed to create zip archive at {$ this ->zipPath }. " );
@@ -61,67 +71,19 @@ public function handle()
6171 }
6272
6373 // Send the zip file
64- try {
65- $ result = $ this ->sendToZephpyr ();
66- } catch (ConnectionException $ e ) {
67- // Timeout, etc.
68- $ this ->error ('Failed to send to Zephpyr: ' .$ e ->getMessage ());
69- $ this ->cleanUp ();
70-
71- return static ::FAILURE ;
72- }
73-
74- if ($ result ->status () === 413 ) {
75- $ fileSize = Number::fileSize (filesize ($ this ->zipPath ));
76- $ this ->error ('The zip file is too large to upload to Zephpyr ( ' .$ fileSize .'). Please contact support. ' );
77-
78- $ this ->cleanUp ();
79-
80- return static ::FAILURE ;
81- } elseif ($ result ->status () === 422 ) {
82- $ this ->error ('Zephpyr returned the following error: ' );
83- $ this ->error (' → ' .$ result ->json ('message ' ));
84- $ this ->cleanUp ();
85-
86- return static ::FAILURE ;
87- } elseif ($ result ->status () === 429 ) {
88- $ 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 ).'. ' );
89- $ this ->cleanUp ();
90-
91- return static ::FAILURE ;
92- } elseif ($ result ->failed ()) {
93- $ this ->error ("Failed to upload zip to Zephpyr. Error code: {$ result ->status ()}" );
94- ray ($ result ->body ());
95- $ this ->cleanUp ();
96-
97- return static ::FAILURE ;
98- }
74+ $ result = $ this ->sendToZephpyr ();
75+ $ this ->handleApiErrors ($ result );
9976
77+ // Success
10078 $ this ->info ('Successfully uploaded to Zephpyr. ' );
10179 $ this ->line ('Use native:bundle --fetch to retrieve the latest bundle. ' );
10280
81+ // Clean up temp files
10382 $ this ->cleanUp ();
10483
10584 return static ::SUCCESS ;
10685 }
10786
108- protected function cleanUp (): void
109- {
110- if ($ this ->option ('without-cleanup ' )) {
111- return ;
112- }
113-
114- $ this ->line ('Cleaning up… ' );
115-
116- $ previousBuilds = glob (base_path ('temp/app_*.zip ' ));
117- $ failedZips = glob (base_path ('temp/app_*.part ' ));
118-
119- $ deleteFiles = array_merge ($ previousBuilds , $ failedZips );
120- foreach ($ deleteFiles as $ file ) {
121- @unlink ($ file );
122- }
123- }
124-
12587 private function zipApplication (): bool
12688 {
12789 $ this ->zipName = 'app_ ' .str ()->random (8 ).'.zip ' ;
@@ -149,14 +111,42 @@ private function zipApplication(): bool
149111 return true ;
150112 }
151113
152- private function addFilesToZip ( ZipArchive $ zip ): void
114+ private function checkComposerJson ( ): bool
153115 {
154- // TODO: Check the composer.json to make sure there are no symlinked
155- // or private packages as these will be a pain later
116+ $ composerJson = json_decode (file_get_contents (base_path ('composer.json ' )), true );
117+
118+ // Fail if there is symlinked packages
119+ foreach ($ composerJson ['repositories ' ] ?? [] as $ repository ) {
120+ if ($ repository ['type ' ] === 'path ' ) {
121+ $ this ->error ('Symlinked packages are not supported. Please remove them from your composer.json. ' );
122+
123+ return false ;
124+ } elseif ($ repository ['type ' ] === 'composer ' ) {
125+ if (! $ this ->checkComposerPackageAuth ($ repository ['url ' ])) {
126+ $ this ->error ('Cannot authenticate with ' .$ repository ['url ' ].'. ' );
127+ $ this ->error ('Go to ' .$ this ->baseUrl ().' and add your credentials for ' .$ repository ['url ' ].'. ' );
128+
129+ return false ;
130+ }
131+ }
132+ }
133+
134+ return true ;
135+ }
156136
157- // TODO: Fail if there is symlinked packages
158- // TODO: For private packages: make an endpoint to check if user gave us their credentials
137+ private function checkComposerPackageAuth (string $ repositoryUrl ): bool
138+ {
139+ $ host = parse_url ($ repositoryUrl , PHP_URL_HOST );
140+ $ this ->line ('Checking ' .$ host .' authentication… ' );
159141
142+ return Http::acceptJson ()
143+ ->withToken (config ('nativephp-internal.zephpyr.token ' ))
144+ ->get ($ this ->baseUrl ().'api/v1/project/ ' .$ this ->key .'/composer/auth/ ' .$ host )
145+ ->successful ();
146+ }
147+
148+ private function addFilesToZip (ZipArchive $ zip ): void
149+ {
160150 $ this ->line ('Creating zip archive… ' );
161151
162152 $ app = (new Finder )->files ()
@@ -178,18 +168,22 @@ private function addFilesToZip(ZipArchive $zip): void
178168 // Add .env file
179169 $ zip ->addFile (base_path ('.env ' ), '.env ' );
180170
171+ // Custom binaries
172+ $ binaryPath = Str::replaceStart (base_path ('vendor ' ), '' , config ('nativephp.binary_path ' ));
173+
174+ // Add composer dependencies without unnecessary files
181175 $ vendor = (new Finder )->files ()
182- // ->followLinks() // This is causing issues with excluded files
183176 ->exclude (array_filter ([
184177 'nativephp/php-bin ' ,
185178 'nativephp/electron/resources/js ' ,
186179 'nativephp/*/vendor ' ,
187- config ( ' nativephp.binary_path ' ), // User defined binary paths
180+ $ binaryPath ,
188181 ]))
189182 ->in (base_path ('vendor ' ));
190183
191184 $ this ->finderToZip ($ vendor , $ zip , 'vendor ' );
192185
186+ // Add javascript dependencies
193187 if (file_exists (base_path ('node_modules ' ))) {
194188 $ nodeModules = (new Finder )->files ()
195189 ->in (base_path ('node_modules ' ));
@@ -209,32 +203,18 @@ private function finderToZip(Finder $finder, ZipArchive $zip, ?string $path = nu
209203 }
210204 }
211205
212- private function baseUrl (): string
213- {
214- return str (config ('nativephp-internal.zephpyr.host ' ))->finish ('/ ' );
215- }
216-
217206 private function sendToZephpyr ()
218207 {
219208 $ this ->line ('Uploading zip to Zephpyr… ' );
220209
221210 return Http::acceptJson ()
222211 ->timeout (300 ) // 5 minutes
223- ->withoutRedirecting () // Upload won't work if we follow the redirect
212+ ->withoutRedirecting () // Upload won't work if we follow redirects (it transform POST to GET)
224213 ->withToken (config ('nativephp-internal.zephpyr.token ' ))
225214 ->attach ('archive ' , fopen ($ this ->zipPath , 'r ' ), $ this ->zipName )
226215 ->post ($ this ->baseUrl ().'api/v1/project/ ' .$ this ->key .'/build/ ' );
227216 }
228217
229- private function checkAuthenticated ()
230- {
231- $ this ->line ('Checking authentication… ' );
232-
233- return Http::acceptJson ()
234- ->withToken (config ('nativephp-internal.zephpyr.token ' ))
235- ->get ($ this ->baseUrl ().'api/v1/user ' )->successful ();
236- }
237-
238218 private function fetchLatestBundle (): bool
239219 {
240220 $ this ->line ('Fetching latest bundle… ' );
@@ -244,52 +224,68 @@ private function fetchLatestBundle(): bool
244224 ->get ($ this ->baseUrl ().'api/v1/project/ ' .$ this ->key .'/build/download ' );
245225
246226 if ($ response ->failed ()) {
227+
228+ if ($ response ->status () === 404 ) {
229+ $ this ->error ('Project or bundle not found. ' );
230+ } elseif ($ response ->status () === 500 ) {
231+ $ this ->error ('Build failed. Please try again later. ' );
232+ } elseif ($ response ->status () === 503 ) {
233+ $ this ->warn ('Bundle not ready. Please try again in ' .now ()->addSeconds (intval ($ response ->header ('Retry-After ' )))->diffForHumans (syntax: CarbonInterface::DIFF_ABSOLUTE ).'. ' );
234+ } else {
235+ $ this ->handleApiErrors ($ response );
236+ }
237+
247238 return false ;
248239 }
249240
241+ // Save the bundle
250242 @mkdir (base_path ('build ' ), recursive: true );
251243 file_put_contents (base_path ('build/__nativephp_app_bundle ' ), $ response ->body ());
252244
253245 return true ;
254246 }
255247
256- private function checkForZephpyrKey ()
248+ protected function exitWithMessage ( string $ message ): void
257249 {
258- $ this ->key = config ('nativephp-internal.zephpyr.key ' );
259-
260- if (! $ this ->key ) {
261- $ this ->line ('' );
262- $ this ->warn ('No ZEPHPYR_KEY found. Cannot bundle! ' );
263- $ this ->line ('' );
264- $ this ->line ('Add this app \'s ZEPHPYR_KEY to its .env file: ' );
265- $ this ->line (base_path ('.env ' ));
266- $ this ->line ('' );
267- $ this ->info ('Not set up with Zephpyr yet? Secure your NativePHP app builds and more! ' );
268- $ this ->info ('Check out ' .$ this ->baseUrl ().'' );
269- $ this ->line ('' );
250+ $ this ->error ($ message );
251+ $ this ->cleanUp ();
270252
271- return false ;
272- }
253+ exit ( static :: FAILURE ) ;
254+ }
273255
274- return true ;
256+ private function handleApiErrors (Response $ result ): void
257+ {
258+ if ($ result ->status () === 413 ) {
259+ $ fileSize = Number::fileSize (filesize ($ this ->zipPath ));
260+ $ this ->exitWithMessage ('File is too large to upload ( ' .$ fileSize .'). Please contact support. ' );
261+ } elseif ($ result ->status () === 422 ) {
262+ $ this ->error ('Request refused: ' .$ result ->json ('message ' ));
263+ } elseif ($ result ->status () === 429 ) {
264+ $ this ->exitWithMessage ('Too many requests. Please try again in ' .now ()->addSeconds (intval ($ result ->header ('Retry-After ' )))->diffForHumans (syntax: CarbonInterface::DIFF_ABSOLUTE ).'. ' );
265+ } elseif ($ result ->failed ()) {
266+ $ this ->exitWithMessage ("Request failed. Error code: {$ result ->status ()}" );
267+ }
275268 }
276269
277- private function checkForZephpyrToken ()
270+ protected function cleanUp (): void
278271 {
279- if (! config ('nativephp-internal.zephpyr.token ' )) {
280- $ this ->line ('' );
281- $ this ->warn ('No ZEPHPYR_TOKEN found. Cannot bundle! ' );
282- $ this ->line ('' );
283- $ this ->line ('Add your api ZEPHPYR_TOKEN to its .env file: ' );
284- $ this ->line (base_path ('.env ' ));
285- $ this ->line ('' );
286- $ this ->info ('Not set up with Zephpyr yet? Secure your NativePHP app builds and more! ' );
287- $ this ->info ('Check out ' .$ this ->baseUrl ().'' );
288- $ this ->line ('' );
272+ if ($ this ->option ('without-cleanup ' )) {
273+ return ;
274+ }
289275
290- return false ;
276+ $ previousBuilds = glob (base_path ('temp/app_*.zip ' ));
277+ $ failedZips = glob (base_path ('temp/app_*.part ' ));
278+
279+ $ deleteFiles = array_merge ($ previousBuilds , $ failedZips );
280+
281+ if (empty ($ deleteFiles )) {
282+ return ;
291283 }
292284
293- return true ;
285+ $ this ->line ('Cleaning up… ' );
286+
287+ foreach ($ deleteFiles as $ file ) {
288+ @unlink ($ file );
289+ }
294290 }
295291}
0 commit comments