1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* pipelines - run bitbucket pipelines wherever they dock |
5
|
|
|
* |
6
|
|
|
* Copyright 2017, 2018 Tom Klingenberg <[email protected]> |
7
|
|
|
* |
8
|
|
|
* Licensed under GNU Affero General Public License v3.0 or later |
9
|
|
|
*/ |
10
|
|
|
|
11
|
|
|
namespace Ktomk\Pipelines\PharBuild; |
12
|
|
|
|
13
|
|
|
use DateTime; |
14
|
|
|
use Ktomk\Pipelines\File\BbplMatch; |
15
|
|
|
use Ktomk\Pipelines\Lib; |
16
|
|
|
use Phar; |
17
|
|
|
|
18
|
|
|
class Builder |
19
|
|
|
{ |
20
|
|
|
/** |
21
|
|
|
* @var string path of the phar file to build |
22
|
|
|
*/ |
23
|
|
|
private $fPhar; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* @var array to collect files to build the phar from $localName => $descriptor |
27
|
|
|
*/ |
28
|
|
|
private $files; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var string |
32
|
|
|
*/ |
33
|
|
|
private $stub; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @var array |
37
|
|
|
*/ |
38
|
|
|
private $errors; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @var int directory depth limit for ** glob pattern |
42
|
|
|
*/ |
43
|
|
|
private $limit; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* @var string pre-generated replacement pattern for self::$limit |
47
|
|
|
*/ |
48
|
|
|
private $double; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @var array |
52
|
|
|
*/ |
53
|
|
|
private $deps; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* @var array keep file path (as key) do be unlinked on __destruct() (housekeeping) |
57
|
|
|
*/ |
58
|
|
|
private $unlink; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* @param string $fphar phar file name |
62
|
|
|
* @return Builder |
63
|
|
|
*/ |
64
|
9 |
|
public static function create($fphar) |
65
|
|
|
{ |
66
|
9 |
|
$builder = new self(); |
67
|
9 |
|
$builder->_ctor($fphar); |
68
|
|
|
|
69
|
9 |
|
return $builder; |
70
|
|
|
} |
71
|
|
|
|
72
|
9 |
|
private function _ctor($fphar) |
73
|
|
|
{ |
74
|
9 |
|
$this->files = array(); |
75
|
9 |
|
$this->errors = array(); |
76
|
9 |
|
$this->limit(9); |
77
|
9 |
|
$this->fPhar = $fphar; |
78
|
9 |
|
} |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* @param string $file |
82
|
|
|
* @return $this |
83
|
|
|
*/ |
84
|
3 |
|
public function stubfile($file) |
85
|
|
|
{ |
86
|
3 |
|
unset($this->stub); |
87
|
|
|
|
88
|
3 |
|
if (!$buffer = file_get_contents($file)) { |
89
|
1 |
|
$this->err(sprintf('error reading stubfile: %s', $file)); |
90
|
|
|
} else { |
91
|
2 |
|
$this->stub = $buffer; |
92
|
|
|
} |
93
|
|
|
|
94
|
3 |
|
return $this; |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* set traversal limit for double-dot glob '**' |
99
|
|
|
* |
100
|
|
|
* @param int $limit 0 to 16, 0 makes '**' effectively act like '*' |
101
|
|
|
* @return $this |
102
|
|
|
*/ |
103
|
9 |
|
public function limit($limit) |
104
|
|
|
{ |
105
|
9 |
|
$limit = (int)min(16, max(0, $limit)); |
106
|
9 |
|
$this->limit = $limit; |
107
|
9 |
|
$this->double = $limit |
108
|
9 |
|
? str_repeat('{*/,', $limit) . str_repeat('}', $limit) . '*' |
109
|
2 |
|
: '*'; |
110
|
|
|
|
111
|
9 |
|
return $this; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* add files to build the phar archive of |
116
|
|
|
* |
117
|
|
|
* @param string|string[] $pattern one or more patterns to add as relative files |
118
|
|
|
* @param callable $callback [optional] to apply on each file found |
119
|
|
|
* @param string $directory [optional] where to add from |
120
|
|
|
* @param string $alias [optional] prefix local names |
121
|
|
|
* @return $this|Builder |
122
|
|
|
*/ |
123
|
4 |
|
public function add($pattern, $callback = null, $directory = null, $alias = null) |
124
|
|
|
{ |
125
|
4 |
|
if (null !== $directory) { |
126
|
2 |
|
$result = realpath($directory); |
127
|
2 |
|
if ($result === false || !is_dir($result)) { |
128
|
1 |
|
$this->err(sprintf('invalid directory: %s', $directory)); |
129
|
1 |
|
return $this; |
130
|
|
|
} |
131
|
2 |
|
$directory = $result . '/'; |
132
|
|
|
} |
133
|
|
|
|
134
|
4 |
|
if (null !== $alias) { |
135
|
2 |
|
$result = trim($alias, '/'); |
136
|
2 |
|
if ($result === '') { |
137
|
1 |
|
$this->err(sprintf( |
138
|
1 |
|
'%s: ineffective alias: %s', |
139
|
1 |
|
is_array($pattern) ? implode(';', $pattern) : $pattern, |
140
|
1 |
|
$alias |
141
|
|
|
)); |
142
|
1 |
|
$alias = null; |
143
|
|
|
} else { |
144
|
1 |
|
$alias = $result . '/'; |
145
|
|
|
} |
146
|
|
|
} |
147
|
|
|
|
148
|
4 |
|
foreach ((array)$pattern as $one) { |
149
|
4 |
|
$this->_add($one, $callback, "$directory", "$alias"); |
150
|
|
|
} |
151
|
|
|
|
152
|
4 |
|
return $this; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Take a snapshot of the file when added to the build, makes |
157
|
|
|
* it immune to later content changes. |
158
|
|
|
* |
159
|
|
|
* @return \Closure |
160
|
|
|
*/ |
161
|
|
|
public function snapShot() |
162
|
|
|
{ |
163
|
1 |
|
return function ($file) { |
164
|
1 |
|
$source = fopen($file, 'r'); |
165
|
1 |
|
if (false === $source) { |
166
|
1 |
|
$this->err(sprintf('failed to open for reading: %s', $file)); |
167
|
1 |
|
return null; |
168
|
|
|
} |
169
|
|
|
|
170
|
1 |
|
$target = tmpfile(); |
171
|
1 |
|
if (false === $target) { |
172
|
|
|
// @codeCoverageIgnoreStart |
173
|
|
|
fclose($source); |
174
|
|
|
$this->err(sprintf('failed to open temp file for writing')); |
175
|
|
|
return null; |
176
|
|
|
// @codeCoverageIgnoreEnd |
177
|
|
|
} |
178
|
|
|
|
179
|
1 |
|
stream_copy_to_stream($source, $target) || $this->err(sprintf('stream copy error: %s', $file)); |
|
|
|
|
180
|
1 |
|
fclose($source); |
181
|
|
|
|
182
|
1 |
|
$meta = stream_get_meta_data($target); |
183
|
1 |
|
$snapShotFile = $meta['uri']; |
184
|
1 |
|
$this->unlink[$snapShotFile] = $target; # (preserve file from deletion) |
185
|
|
|
|
186
|
1 |
|
return array('fil', $snapShotFile); |
187
|
1 |
|
}; |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* Drop first line from file when added to the build, e.g. |
192
|
|
|
* for removing a shebang line. |
193
|
|
|
* |
194
|
|
|
* @return \Closure |
195
|
|
|
*/ |
196
|
|
|
public function dropFirstLine() |
197
|
|
|
{ |
198
|
3 |
|
return function ($file) { |
199
|
3 |
|
$lines = file($file); |
200
|
3 |
|
if (false === $lines) { |
201
|
1 |
|
$this->err(sprintf('error reading file: %s', $file)); |
202
|
1 |
|
return null; |
203
|
|
|
} |
204
|
2 |
|
array_shift($lines); |
205
|
2 |
|
$buffer = implode("", $lines); |
206
|
|
|
|
207
|
2 |
|
return array('str', $buffer); |
208
|
3 |
|
}; |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
/** |
212
|
|
|
* String replace on file contents |
213
|
|
|
* |
214
|
|
|
* @param string $that |
215
|
|
|
* @param string $with |
216
|
|
|
* @return \Closure |
217
|
|
|
*/ |
218
|
|
|
public function replace($that, $with) |
219
|
|
|
{ |
220
|
1 |
|
return function ($file) use ($that, $with) { |
221
|
1 |
|
$buffer = file_get_contents($file); |
222
|
1 |
|
$buffer = strtr($buffer, array($that => $with)); |
223
|
|
|
|
224
|
1 |
|
return array('str', $buffer); |
225
|
1 |
|
}; |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
/** |
229
|
|
|
* add files to build the phar archive from |
230
|
|
|
* |
231
|
|
|
* @param string $pattern glob pattern of files to add |
232
|
|
|
* @param callable $callback [optional] callback to apply on each file found |
233
|
|
|
* @param string $directory [optional] |
234
|
|
|
* @param string $alias [optional] |
235
|
|
|
*/ |
236
|
4 |
|
private function _add($pattern, $callback = null, $directory = null, $alias = null) |
237
|
|
|
{ |
238
|
|
|
/** @var string $pwd [optional] previous working directory */ |
239
|
4 |
|
$pwd = null; |
240
|
|
|
|
241
|
4 |
|
if (strlen($directory)) { |
242
|
|
|
// TODO handle errors |
243
|
2 |
|
$pwd = getcwd(); |
244
|
2 |
|
chdir($directory); |
245
|
|
|
} |
246
|
|
|
|
247
|
4 |
|
$results = $this->_glob($pattern); |
248
|
4 |
|
foreach ($results as $result) { |
249
|
4 |
|
if (!is_file($result)) { |
250
|
1 |
|
continue; |
251
|
|
|
} |
252
|
|
|
|
253
|
4 |
|
$file = $directory . $result; |
254
|
4 |
|
$localName = $alias . $result; |
255
|
4 |
|
$descriptor = array('fil', $file); |
256
|
|
|
|
257
|
4 |
|
if (null !== $callback) { |
258
|
3 |
|
$descriptor = call_user_func($callback, $file); |
259
|
3 |
|
if (!is_array($descriptor) || count($descriptor) !== 2) { |
260
|
1 |
|
$this->err(sprintf( |
261
|
1 |
|
"%s: invalid callback return for pattern '%s': %s", |
262
|
1 |
|
$result, |
263
|
1 |
|
$pattern, |
264
|
1 |
|
rtrim(var_export($descriptor, true)) |
265
|
|
|
)); |
266
|
1 |
|
continue; |
267
|
|
|
} |
268
|
|
|
} |
269
|
|
|
|
270
|
4 |
|
$this->files[$localName] = $descriptor; |
271
|
|
|
} |
272
|
|
|
|
273
|
4 |
|
if (strlen($directory)) { |
274
|
|
|
// TODO handle errors |
275
|
2 |
|
chdir($pwd); |
276
|
|
|
} |
277
|
4 |
|
} |
278
|
|
|
|
279
|
|
|
/** |
280
|
|
|
* @see Builder::_glob() |
281
|
|
|
* |
282
|
|
|
* @param $glob |
283
|
|
|
* @param $flags |
284
|
|
|
* @return array|bool |
285
|
|
|
*/ |
286
|
4 |
|
private function _glob_brace($glob, $flags) |
287
|
|
|
{ |
288
|
4 |
|
$reservoir = array(); |
289
|
4 |
|
$globs = Lib::expandBrace($glob); |
290
|
4 |
|
foreach ($globs as $globEx) { |
291
|
4 |
|
$result = \glob($globEx, $flags); |
292
|
4 |
|
if ($result === false) { |
|
|
|
|
293
|
|
|
// @codeCoverageIgnoreStart |
294
|
|
|
$this->err(vsprintf( |
295
|
|
|
"glob failure '%s' <- '%s'", |
296
|
|
|
array($globEx, $glob) |
297
|
|
|
)); |
298
|
|
|
return false; |
299
|
|
|
// @codeCoverageIgnoreEnd |
300
|
|
|
} |
301
|
|
|
|
302
|
4 |
|
foreach ($result as $file) { |
303
|
4 |
|
$reservoir["k{$file}"] = $file; |
304
|
|
|
} |
305
|
|
|
} |
306
|
|
|
|
307
|
4 |
|
return array_values($reservoir); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* globbing with double dot (**) support |
312
|
|
|
* @param $pattern |
313
|
|
|
* @return array |
314
|
|
|
*/ |
315
|
4 |
|
private function _glob($pattern) |
316
|
|
|
{ |
317
|
|
|
/* enable double-dots (with recursion limit, @see Builder::limit */ |
318
|
4 |
|
$glob = strtr($pattern, array('\*' => '\*', '**' => $this->double)); |
319
|
|
|
|
320
|
4 |
|
$result = $this->_glob_brace($glob, GLOB_NOSORT); |
321
|
|
|
|
322
|
4 |
|
if ($result === false) { |
|
|
|
|
323
|
|
|
// @codeCoverageIgnoreStart |
324
|
|
|
$this->err(vsprintf( |
325
|
|
|
"glob failure '%s' -> '%s'", |
326
|
|
|
array($pattern, $glob) |
327
|
|
|
)); |
328
|
|
|
return array(); |
329
|
|
|
// @codeCoverageIgnoreEnd |
330
|
|
|
} |
331
|
4 |
|
if ($result === array()) $this->err(sprintf( |
332
|
1 |
|
"ineffective pattern: %s", |
333
|
1 |
|
$pattern === $glob |
334
|
1 |
|
? $pattern |
335
|
1 |
|
: sprintf("'%s' -> '%s'", $pattern, $glob) |
336
|
|
|
)); |
337
|
4 |
|
if (!is_array($result)) { |
|
|
|
|
338
|
|
|
// @codeCoverageIgnoreStart |
339
|
|
|
throw new \UnexpectedValueException( |
340
|
|
|
sprintf('glob: return value not an array: %s', var_export($result, true)) |
341
|
|
|
); |
342
|
|
|
// @codeCoverageIgnoreEnd |
343
|
|
|
} |
344
|
|
|
|
345
|
4 |
|
return $result; |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
/** |
349
|
|
|
* add dependency from vendor |
350
|
|
|
* @param $name |
351
|
|
|
* @return $this |
352
|
|
|
* @deprecated is over-aged for current build, not much need of dependency |
353
|
|
|
* management and it would be better that composer fixes auto- |
354
|
|
|
* load dumping. |
355
|
|
|
* @codeCoverageIgnore |
356
|
|
|
*/ |
357
|
|
|
public function dep($name) |
358
|
|
|
{ |
359
|
|
|
# TODO allow ; in patterns (better than array perhaps even) and allow !pattern) |
360
|
|
|
$trim = trim($name, '/'); |
361
|
|
|
$prefix = "vendor/$trim/"; |
362
|
|
|
$pattern = sprintf('%s**', $prefix); |
363
|
|
|
$this->deps[$trim] = 1; |
364
|
|
|
$this->add($pattern); |
365
|
|
|
|
366
|
|
|
# so far no deep dependencies to include, keeping for future |
367
|
|
|
# $composer = json_decode(file_get_contents("${prefix}composer.json"), true); |
368
|
|
|
# print_r($composer['require']); |
369
|
|
|
|
370
|
|
|
return $this; |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
/** |
374
|
|
|
* build phar file and optionally invoke it with parameters for |
375
|
|
|
* a quick smoke test |
376
|
|
|
* |
377
|
|
|
* @param string $params [options] |
378
|
|
|
* @return $this |
379
|
|
|
*/ |
380
|
6 |
|
public function build($params = null) |
381
|
|
|
{ |
382
|
6 |
|
$file = $this->fPhar; |
383
|
6 |
|
$files = $this->files; |
384
|
|
|
|
385
|
6 |
|
$temp = $this->_tempname('.phar'); |
386
|
6 |
|
if (false === $temp) { |
|
|
|
|
387
|
|
|
// @codeCoverageIgnoreStart |
388
|
|
|
$this->err('fatal: failed to create tmp phar archive file'); |
389
|
|
|
return $this; |
390
|
|
|
// @codeCoverageIgnoreEnd |
391
|
|
|
} |
392
|
|
|
|
393
|
6 |
|
if (file_exists($file) && !unlink($file)) { |
394
|
1 |
|
$this->err(sprintf("could not unlink existing file '%s'", $file)); |
395
|
1 |
|
return $this; |
396
|
|
|
} |
397
|
|
|
|
398
|
5 |
|
if (!Phar::canWrite()) { |
399
|
1 |
|
$this->err("phar: writing phar files is disabled by the php.ini setting 'phar.readonly'"); |
400
|
|
|
} |
401
|
|
|
|
402
|
5 |
|
if (empty($files)) { |
403
|
1 |
|
$this->err('no files, add some or do not remove all'); |
404
|
|
|
} |
405
|
|
|
|
406
|
5 |
|
if (!empty($this->errors)) { |
407
|
3 |
|
$this->err('fatal: build has errors, not building'); |
408
|
3 |
|
return $this; |
409
|
|
|
} |
410
|
|
|
|
411
|
2 |
|
$phar = new Phar($temp); |
412
|
2 |
|
$phar->startBuffering(); |
413
|
|
|
|
414
|
2 |
|
if (null !== $this->stub) { |
|
|
|
|
415
|
1 |
|
$phar->setStub($this->stub); |
416
|
|
|
} |
417
|
|
|
|
418
|
2 |
|
$count = $this->_bfiles($phar, $files); |
419
|
2 |
|
if (count($files) !== $count) { |
420
|
1 |
|
$this->err(sprintf('only %d of %d files could be added', $count, count($files))); |
421
|
|
|
} |
422
|
|
|
|
423
|
2 |
|
$phar->stopBuffering(); |
424
|
2 |
|
unset($phar); # save file |
425
|
|
|
|
426
|
2 |
|
if ($count === 0) { |
427
|
1 |
|
$this->err('fatal: no files in phar archive, must have at least one'); |
428
|
1 |
|
return $this; |
429
|
|
|
} |
430
|
|
|
|
431
|
1 |
|
copy($temp, $file); |
432
|
|
|
|
433
|
|
|
# chmod +x for lazy ones |
434
|
1 |
|
if (!chmod($file, 0775)) { |
435
|
|
|
// @codeCoverageIgnoreStart |
436
|
|
|
$this->err('error changing mode to 0775 on phar file'); |
437
|
|
|
// @codeCoverageIgnoreEnd |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
# smoke test TODO operate on secondary temp file, execution options |
441
|
1 |
|
if ($params !== null) { |
442
|
1 |
|
$this->exec(sprintf('./%s %s', $file, $params), $return); |
443
|
1 |
|
printf("%s\n", $return); |
444
|
|
|
} |
445
|
|
|
|
446
|
1 |
|
return $this; |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
/** |
450
|
|
|
* updates each file's unix timestamps in the phar archive, |
451
|
|
|
* useful for reproducible builds |
452
|
|
|
* |
453
|
|
|
* @param int|DateTime|string $timestamp Date string or DateTime or unix timestamp to use |
454
|
|
|
* @return $this |
455
|
|
|
*/ |
456
|
2 |
|
public function timestamps($timestamp = null) |
457
|
|
|
{ |
458
|
2 |
|
$file = $this->fPhar; |
459
|
2 |
|
if (!file_exists($file)) { |
460
|
1 |
|
$this->err(sprintf('no such file: %s', $file)); |
461
|
1 |
|
return $this; |
462
|
|
|
} |
463
|
1 |
|
require_once __DIR__ . '/Timestamps.php'; |
464
|
1 |
|
$ts = new Timestamps($file); |
465
|
1 |
|
$ts->updateTimestamps($timestamp); |
466
|
1 |
|
$ts->save($this->fPhar, Phar::SHA1); |
467
|
|
|
|
468
|
1 |
|
return $this; |
469
|
|
|
} |
470
|
|
|
|
471
|
|
|
/** |
472
|
|
|
* output information about built phar file |
473
|
|
|
*/ |
474
|
2 |
|
public function info() |
475
|
|
|
{ |
476
|
2 |
|
$filename = $this->fPhar; |
477
|
|
|
|
478
|
2 |
|
if (!is_file($filename)) { |
479
|
1 |
|
$this->err(sprintf('no such file: %s', $filename)); |
480
|
1 |
|
return $this; |
481
|
|
|
} |
482
|
|
|
|
483
|
1 |
|
printf("file.....: %s\n", $filename); |
484
|
1 |
|
printf("size.....: %s bytes\n", number_format(filesize($filename), 0, '.', ' ')); |
485
|
1 |
|
printf("SHA-1....: %s\n", sha1_file($filename)); |
486
|
1 |
|
printf("SHA-256..: %s\n", hash_file('sha256', $filename)); |
487
|
|
|
|
488
|
1 |
|
$pinfo = new \Phar($filename); |
489
|
1 |
|
printf("count....: %d file(s)\n", $pinfo->count()); |
490
|
1 |
|
$sig = $pinfo->getSignature(); |
491
|
1 |
|
printf("signature: %s %s\n", $sig['hash_type'], $sig['hash']); |
492
|
|
|
|
493
|
1 |
|
return $this; |
494
|
|
|
} |
495
|
|
|
|
496
|
|
|
/** |
497
|
|
|
* remove from collected files based on pattern |
498
|
|
|
* |
499
|
|
|
* @param $pattern |
500
|
|
|
* @return $this |
501
|
|
|
*/ |
502
|
2 |
|
public function remove($pattern) |
503
|
|
|
{ |
504
|
2 |
|
if (empty($this->files)) { |
505
|
1 |
|
$this->err(sprintf("can not remove from no files (pattern: '%s')", $pattern)); |
506
|
1 |
|
return $this; |
507
|
|
|
} |
508
|
|
|
|
509
|
2 |
|
require_once __DIR__ . '/../../src/File/BbplMatch.php'; |
510
|
|
|
|
511
|
2 |
|
$result = array(); |
512
|
2 |
|
foreach ($this->files as $key => $value) { |
513
|
2 |
|
if (!BbplMatch::match($pattern, $key)) { |
514
|
2 |
|
$result[$key] = $value; |
515
|
|
|
} |
516
|
|
|
} |
517
|
|
|
|
518
|
2 |
|
if (count($result) === count($this->files)) { |
519
|
1 |
|
$this->err(sprintf("ineffective removal pattern: '%s'", $pattern)); |
520
|
|
|
} else { |
521
|
2 |
|
$this->files = $result; |
522
|
|
|
} |
523
|
|
|
|
524
|
2 |
|
return $this; |
525
|
|
|
} |
526
|
|
|
|
527
|
|
|
/** |
528
|
|
|
* execute a system command |
529
|
|
|
* |
530
|
|
|
* @param string $command |
531
|
|
|
* @param string $return [by-ref] last line of the output (w/o newline/white space at end) |
532
|
|
|
* @return $this |
533
|
|
|
*/ |
534
|
2 |
|
public function exec($command, &$return = null) |
535
|
|
|
{ |
536
|
2 |
|
$return = exec($command, $output, $status); |
537
|
2 |
|
if ($status !== 0) { |
538
|
1 |
|
$this->err(sprintf('command failed: %s (exit status: %d)', $command, $status)); |
539
|
|
|
} |
540
|
|
|
|
541
|
2 |
|
$return = rtrim($return); |
542
|
|
|
|
543
|
2 |
|
return $this; |
544
|
|
|
} |
545
|
|
|
|
546
|
|
|
/** |
547
|
|
|
* build chunks from files (sorted by local name) |
548
|
|
|
* |
549
|
|
|
* @param array $files |
550
|
|
|
* @return array |
551
|
|
|
*/ |
552
|
2 |
|
private function _bchunks(array $files) |
553
|
|
|
{ |
554
|
2 |
|
ksort($files, SORT_STRING) || $this->err(); |
|
|
|
|
555
|
|
|
|
556
|
2 |
|
$lastType = null; |
557
|
2 |
|
$chunks = array(); |
558
|
2 |
|
$nodes = null; |
559
|
2 |
|
foreach ($files as $localName => $descriptor) { |
560
|
2 |
|
list($type, $context) = $descriptor; |
561
|
|
|
|
562
|
2 |
|
if ($type !== $lastType) { |
563
|
2 |
|
unset($nodes); |
564
|
2 |
|
$nodes = array(); |
565
|
2 |
|
$chunks[] = array('type' => $type, 'nodes' => &$nodes); |
566
|
2 |
|
$lastType = $type; |
567
|
|
|
} |
568
|
|
|
|
569
|
|
|
switch ($type) { |
570
|
2 |
|
case 'fil': # type is: key'ed file is (existing) file with relative path on system |
571
|
2 |
|
if (!is_file($context)) { |
572
|
1 |
|
$this->err(sprintf('%s: not a file: %s', $localName, $context)); |
573
|
|
|
} else { |
574
|
1 |
|
$nodes[$localName] = $context; |
575
|
|
|
} |
576
|
2 |
|
break; |
577
|
|
|
|
578
|
1 |
|
case 'str': # type is: key'ed file is string contents |
579
|
1 |
|
$nodes[$localName] = $context; |
580
|
1 |
|
break; |
581
|
|
|
|
582
|
|
|
default: |
583
|
2 |
|
throw new \UnexpectedValueException(sprintf("unknown type: %s", $type)); |
584
|
|
|
} |
585
|
|
|
} |
586
|
2 |
|
unset($nodes); |
587
|
|
|
|
588
|
2 |
|
return $chunks; |
589
|
|
|
} |
590
|
|
|
|
591
|
|
|
/** |
592
|
|
|
* create temporary file |
593
|
|
|
* |
594
|
|
|
* @param string $suffix [optional] |
595
|
|
|
* @return bool|string |
596
|
|
|
*/ |
597
|
6 |
|
private function _tempname($suffix = null) |
598
|
|
|
{ |
599
|
6 |
|
$temp = tempnam(sys_get_temp_dir(), 'pharbuild.'); |
600
|
6 |
|
if (false === $temp) { |
|
|
|
|
601
|
|
|
// @codeCoverageIgnoreStart |
602
|
|
|
$this->err('failed to acquire temp filename'); |
603
|
|
|
return false; |
604
|
|
|
// @codeCoverageIgnoreEnd |
605
|
|
|
} |
606
|
|
|
|
607
|
6 |
|
if (null !== $suffix) { |
608
|
6 |
|
unlink($temp); |
609
|
6 |
|
$temp .= $suffix; |
610
|
|
|
} |
611
|
|
|
|
612
|
6 |
|
$this->unlink[$temp] = 1; |
613
|
|
|
|
614
|
6 |
|
return $temp; |
615
|
|
|
} |
616
|
|
|
|
617
|
|
|
|
618
|
|
|
/** |
619
|
|
|
* @return array error messages |
620
|
|
|
*/ |
621
|
3 |
|
public function errors() |
622
|
|
|
{ |
623
|
3 |
|
return $this->errors; |
624
|
|
|
} |
625
|
|
|
|
626
|
|
|
/** |
627
|
|
|
* public to allow injection in tests |
628
|
|
|
* |
629
|
|
|
* @var null|resource to write errors to (if not set, standard error) |
630
|
|
|
*/ |
631
|
|
|
public $errHandle; |
632
|
|
|
|
633
|
|
|
/** |
634
|
|
|
* @param string $message [optional] |
635
|
|
|
*/ |
636
|
6 |
|
private function err($message = null) |
637
|
|
|
{ |
638
|
|
|
// fallback to global static: if STDIN is used for PHP |
639
|
|
|
// process, the default constants aren't ignored. |
640
|
6 |
|
if (null === $this->errHandle) { |
641
|
5 |
|
$this->errHandle = $this->errHandleFromEnvironment(); |
|
|
|
|
642
|
|
|
} |
643
|
|
|
|
644
|
6 |
|
$this->errors[] = $message; |
645
|
6 |
|
is_resource($this->errHandle) && fprintf($this->errHandle, "%s\n", $message); |
646
|
6 |
|
} |
647
|
|
|
|
648
|
5 |
|
private function errHandleFromEnvironment() |
649
|
|
|
{ |
650
|
5 |
|
if (defined('STDERR')) { |
651
|
|
|
// @codeCoverageIgnoreStart |
652
|
|
|
// phpunit can't tests this cleanly as it is always not defined in |
653
|
|
|
// phpt tests |
654
|
|
|
$handle = constant('STDERR'); |
655
|
|
|
if (false === is_resource($handle)) { |
656
|
|
|
$message = 'fatal i/o error: failed to acquire stream from STDERR'; |
657
|
|
|
$this->errors[] = $message; |
658
|
|
|
throw new \RuntimeException($message); |
659
|
|
|
} |
660
|
|
|
// @codeCoverageIgnoreEnd |
661
|
|
|
} else { |
662
|
5 |
|
$handle = fopen('php://stderr', 'w'); |
663
|
5 |
|
if (false === $handle) { |
664
|
|
|
// @codeCoverageIgnoreStart |
665
|
|
|
$message = 'fatal i/o error: failed to open php://stderr'; |
666
|
|
|
$this->errors[] = $message; |
667
|
|
|
throw new \RuntimeException($message); |
668
|
|
|
// @codeCoverageIgnoreEnd |
669
|
|
|
} |
670
|
|
|
} |
671
|
|
|
|
672
|
5 |
|
return $handle; |
673
|
|
|
} |
674
|
|
|
|
675
|
2 |
|
public function __destruct() |
676
|
|
|
{ |
677
|
2 |
|
foreach ((array)$this->unlink as $path => $test) { |
678
|
1 |
|
if (file_exists($path) && unlink($path)) { |
679
|
1 |
|
unset($this->unlink[$path]); |
680
|
|
|
} |
681
|
|
|
} |
682
|
2 |
|
} |
683
|
|
|
|
684
|
|
|
/** |
685
|
|
|
* @param Phar $phar |
686
|
|
|
* @param array $files |
687
|
|
|
* @return int number of files (successfully) added to the phar file |
688
|
|
|
*/ |
689
|
|
|
private function _bfiles(Phar $phar, array $files) |
690
|
|
|
{ |
691
|
|
|
$builders = array( |
692
|
2 |
|
'fil' => function (array $nodes) use ($phar) { |
693
|
2 |
|
$result = $phar->buildFromIterator( |
694
|
2 |
|
new \ArrayIterator($nodes) |
695
|
|
|
); |
696
|
2 |
|
return count($result); |
697
|
2 |
|
}, |
698
|
2 |
|
'str' => function (array $nodes) use ($phar) { |
699
|
1 |
|
$count = 0; |
700
|
1 |
|
foreach ($nodes as $localName => $contents) { |
701
|
1 |
|
$phar->addFromString($localName, $contents); |
702
|
1 |
|
$count++; |
703
|
|
|
} |
704
|
1 |
|
return $count; |
705
|
2 |
|
}, |
706
|
|
|
); |
707
|
|
|
|
708
|
2 |
|
$count = 0; |
709
|
2 |
|
foreach ($this->_bchunks($files) as $chunk) { |
710
|
2 |
|
$count += call_user_func($builders[$chunk['type']], $chunk['nodes']); |
711
|
|
|
} |
712
|
|
|
|
713
|
2 |
|
return $count; |
714
|
|
|
} |
715
|
|
|
} |
716
|
|
|
|
This check looks for function or method calls that always return null and whose return value is used.
The method
getObject()
can return nothing but null, so it makes no sense to use the return value.The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.