4
4
5
5
use Carbon \CarbonInterface ;
6
6
use Illuminate \Console \Command ;
7
- use Illuminate \Http \Client \ConnectionException ;
7
+ use Illuminate \Http \Client \Response ;
8
8
use Illuminate \Support \Facades \Http ;
9
9
use Illuminate \Support \Number ;
10
+ use Illuminate \Support \Str ;
10
11
use Native \Laravel \Commands \Traits \CleansEnvFile ;
12
+ use Native \Laravel \Commands \Traits \HandleApiRequests ;
11
13
use Symfony \Component \Finder \Finder ;
12
14
use ZipArchive ;
13
15
14
16
class BundleCommand extends Command
15
17
{
16
- use CleansEnvFile;
18
+ use CleansEnvFile, HandleApiRequests ;
17
19
18
20
protected $ signature = 'native:bundle {--fetch} {--without-cleanup} ' ;
19
21
@@ -25,25 +27,28 @@ class BundleCommand extends Command
25
27
26
28
private string $ zipName ;
27
29
28
- public function handle ()
30
+ public function handle (): int
29
31
{
32
+ // Check for ZEPHPYR_KEY
30
33
if (! $ this ->checkForZephpyrKey ()) {
31
34
return static ::FAILURE ;
32
35
}
33
36
37
+ // Check for ZEPHPYR_TOKEN
34
38
if (! $ this ->checkForZephpyrToken ()) {
35
39
return static ::FAILURE ;
36
40
}
37
41
42
+ // Check if the token is valid
38
43
if (! $ this ->checkAuthenticated ()) {
39
44
$ this ->error ('Invalid API token: check your ZEPHPYR_TOKEN on ' .$ this ->baseUrl ().'user/api-tokens ' );
40
45
41
46
return static ::FAILURE ;
42
47
}
43
48
49
+ // Download the latest bundle if requested
44
50
if ($ this ->option ('fetch ' )) {
45
51
if (! $ this ->fetchLatestBundle ()) {
46
- $ this ->warn ('Latest bundle not yet available. Try again soon. ' );
47
52
48
53
return static ::FAILURE ;
49
54
}
@@ -53,6 +58,11 @@ public function handle()
53
58
return static ::SUCCESS ;
54
59
}
55
60
61
+ // Check composer.json for symlinked or private packages
62
+ if (! $ this ->checkComposerJson ()) {
63
+ return static ::FAILURE ;
64
+ }
65
+
56
66
// Package the app up into a zip
57
67
if (! $ this ->zipApplication ()) {
58
68
$ this ->error ("Failed to create zip archive at {$ this ->zipPath }. " );
@@ -61,67 +71,19 @@ public function handle()
61
71
}
62
72
63
73
// 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 );
99
76
77
+ // Success
100
78
$ this ->info ('Successfully uploaded to Zephpyr. ' );
101
79
$ this ->line ('Use native:bundle --fetch to retrieve the latest bundle. ' );
102
80
81
+ // Clean up temp files
103
82
$ this ->cleanUp ();
104
83
105
84
return static ::SUCCESS ;
106
85
}
107
86
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
-
125
87
private function zipApplication (): bool
126
88
{
127
89
$ this ->zipName = 'app_ ' .str ()->random (8 ).'.zip ' ;
@@ -149,14 +111,42 @@ private function zipApplication(): bool
149
111
return true ;
150
112
}
151
113
152
- private function addFilesToZip ( ZipArchive $ zip ): void
114
+ private function checkComposerJson ( ): bool
153
115
{
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
+ }
156
136
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… ' );
159
141
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
+ {
160
150
$ this ->line ('Creating zip archive… ' );
161
151
162
152
$ app = (new Finder )->files ()
@@ -178,18 +168,22 @@ private function addFilesToZip(ZipArchive $zip): void
178
168
// Add .env file
179
169
$ zip ->addFile (base_path ('.env ' ), '.env ' );
180
170
171
+ // Custom binaries
172
+ $ binaryPath = Str::replaceStart (base_path ('vendor ' ), '' , config ('nativephp.binary_path ' ));
173
+
174
+ // Add composer dependencies without unnecessary files
181
175
$ vendor = (new Finder )->files ()
182
- // ->followLinks() // This is causing issues with excluded files
183
176
->exclude (array_filter ([
184
177
'nativephp/php-bin ' ,
185
178
'nativephp/electron/resources/js ' ,
186
179
'nativephp/*/vendor ' ,
187
- config ( ' nativephp.binary_path ' ), // User defined binary paths
180
+ $ binaryPath ,
188
181
]))
189
182
->in (base_path ('vendor ' ));
190
183
191
184
$ this ->finderToZip ($ vendor , $ zip , 'vendor ' );
192
185
186
+ // Add javascript dependencies
193
187
if (file_exists (base_path ('node_modules ' ))) {
194
188
$ nodeModules = (new Finder )->files ()
195
189
->in (base_path ('node_modules ' ));
@@ -209,32 +203,18 @@ private function finderToZip(Finder $finder, ZipArchive $zip, ?string $path = nu
209
203
}
210
204
}
211
205
212
- private function baseUrl (): string
213
- {
214
- return str (config ('nativephp-internal.zephpyr.host ' ))->finish ('/ ' );
215
- }
216
-
217
206
private function sendToZephpyr ()
218
207
{
219
208
$ this ->line ('Uploading zip to Zephpyr… ' );
220
209
221
210
return Http::acceptJson ()
222
211
->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)
224
213
->withToken (config ('nativephp-internal.zephpyr.token ' ))
225
214
->attach ('archive ' , fopen ($ this ->zipPath , 'r ' ), $ this ->zipName )
226
215
->post ($ this ->baseUrl ().'api/v1/project/ ' .$ this ->key .'/build/ ' );
227
216
}
228
217
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
-
238
218
private function fetchLatestBundle (): bool
239
219
{
240
220
$ this ->line ('Fetching latest bundle… ' );
@@ -244,52 +224,68 @@ private function fetchLatestBundle(): bool
244
224
->get ($ this ->baseUrl ().'api/v1/project/ ' .$ this ->key .'/build/download ' );
245
225
246
226
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
+
247
238
return false ;
248
239
}
249
240
241
+ // Save the bundle
250
242
@mkdir (base_path ('build ' ), recursive: true );
251
243
file_put_contents (base_path ('build/__nativephp_app_bundle ' ), $ response ->body ());
252
244
253
245
return true ;
254
246
}
255
247
256
- private function checkForZephpyrKey ()
248
+ protected function exitWithMessage ( string $ message ): void
257
249
{
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 ();
270
252
271
- return false ;
272
- }
253
+ exit ( static :: FAILURE ) ;
254
+ }
273
255
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
+ }
275
268
}
276
269
277
- private function checkForZephpyrToken ()
270
+ protected function cleanUp (): void
278
271
{
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
+ }
289
275
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 ;
291
283
}
292
284
293
- return true ;
285
+ $ this ->line ('Cleaning up… ' );
286
+
287
+ foreach ($ deleteFiles as $ file ) {
288
+ @unlink ($ file );
289
+ }
294
290
}
295
291
}
0 commit comments