Passed
Pull Request — master (#116)
by Théo
02:06
created

Configuration::retrieveCheckRequirements()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 3
nc 2
nop 3
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the box project.
7
 *
8
 * (c) Kevin Herrera <[email protected]>
9
 *     Théo Fidry <[email protected]>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14
15
namespace KevinGH\Box;
16
17
use Assert\Assertion;
18
use Closure;
19
use DateTimeImmutable;
20
use Herrera\Annotations\Tokenizer;
21
use Herrera\Box\Compactor\Php as LegacyPhp;
22
use Humbug\PhpScoper\Configuration as PhpScoperConfiguration;
23
use InvalidArgumentException;
24
use KevinGH\Box\Compactor\Php;
25
use KevinGH\Box\Compactor\PhpScoper as PhpScoperCompactor;
26
use KevinGH\Box\Composer\ComposerConfiguration;
27
use KevinGH\Box\Json\Json;
28
use KevinGH\Box\PhpScoper\SimpleScoper;
29
use Phar;
30
use RuntimeException;
31
use Seld\JsonLint\ParsingException;
32
use SplFileInfo;
33
use stdClass;
34
use Symfony\Component\Finder\Finder;
35
use Symfony\Component\Process\Process;
36
use function array_filter;
37
use function array_key_exists;
38
use function array_map;
39
use function array_merge;
40
use function array_unique;
41
use function file_exists;
42
use function Humbug\PhpScoper\create_scoper;
43
use function is_file;
44
use function is_readable;
45
use function iter\chain;
46
use function iter\fn\method;
47
use function iter\map;
48
use function iter\toArray;
49
use function iterator_to_array;
50
use function KevinGH\Box\FileSystem\canonicalize;
51
use function KevinGH\Box\FileSystem\file_contents;
52
use function KevinGH\Box\FileSystem\longest_common_base_path;
53
use function KevinGH\Box\FileSystem\make_path_absolute;
54
use function KevinGH\Box\FileSystem\make_path_relative;
55
use function preg_match;
56
use function substr;
57
use function uniqid;
58
59
/**
60
 * @private
61
 */
62
final class Configuration
63
{
64
    private const DEFAULT_ALIAS = 'test.phar';
65
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
66
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s';
67
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
68
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
69
    private const DEFAULT_BANNER = <<<'BANNER'
70
Generated by Humbug Box.
71
72
@link https://github.com/humbug/box
73
BANNER;
74
    private const FILES_SETTINGS = [
75
        'files',
76
        'files-bin',
77
        'directories',
78
        'directories-bin',
79
        'finder',
80
        'finder-bin',
81
    ];
82
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
83
84
    private $file;
85
    private $fileMode;
86
    private $alias;
87
    private $basePath;
88
    private $composerJson;
89
    private $composerLock;
90
    private $files;
91
    private $binaryFiles;
92
    private $compactors;
93
    private $compressionAlgorithm;
94
    private $mainScriptPath;
95
    private $mainScriptContents;
96
    private $map;
97
    private $fileMapper;
98
    private $metadata;
99
    private $tmpOutputPath;
100
    private $outputPath;
101
    private $privateKeyPassphrase;
102
    private $privateKeyPath;
103
    private $isPrivateKeyPrompt;
104
    private $processedReplacements;
105
    private $shebang;
106
    private $signingAlgorithm;
107
    private $stubBannerContents;
108
    private $stubBannerPath;
109
    private $stubPath;
110
    private $isInterceptFileFuncs;
111
    private $isStubGenerated;
112
    private $checkRequirements;
113
114
    /**
115
     * @param null|string     $file
116
     * @param null|string     $alias
117
     * @param string          $basePath              Utility to private the base path used and be able to retrieve a path relative to it (the base path)
118
     * @param null[]|string[] $composerJson
119
     * @param null[]|string[] $composerLock
120
     * @param SplFileInfo[]   $files                 List of files
121
     * @param SplFileInfo[]   $binaryFiles           List of binary files
122
     * @param Compactor[]     $compactors            List of file contents compactors
123
     * @param null|int        $compressionAlgorithm  Compression algorithm constant value. See the \Phar class constants
124
     * @param null|int        $fileMode              File mode in octal form
125
     * @param string          $mainScriptPath        The main script file path
126
     * @param string          $mainScriptContents    The processed content of the main script file
127
     * @param MapFile         $fileMapper            Utility to map the files from outside and inside the PHAR
128
     * @param mixed           $metadata              The PHAR Metadata
129
     * @param string          $tmpOutputPath
130
     * @param string          $outputPath
131
     * @param null|string     $privateKeyPassphrase
132
     * @param null|string     $privateKeyPath
133
     * @param bool            $isPrivateKeyPrompt    If the user should be prompted for the private key passphrase
134
     * @param array           $processedReplacements The processed list of replacement placeholders and their values
135
     * @param null|string     $shebang               The shebang line
136
     * @param int             $signingAlgorithm      The PHAR siging algorithm. See \Phar constants
137
     * @param null|string     $stubBannerContents    The stub banner comment
138
     * @param null|string     $stubBannerPath        The path to the stub banner comment file
139
     * @param null|string     $stubPath              The PHAR stub file path
140
     * @param bool            $isInterceptFileFuncs  Whether or not Phar::interceptFileFuncs() should be used
141
     * @param bool            $isStubGenerated       Whether or not if the PHAR stub should be generated
142
     * @param bool            $checkRequirements     Whether the PHAR will check the application requirements before running
143
     */
144
    private function __construct(
145
        ?string $file,
146
        string $alias,
147
        string $basePath,
148
        array $composerJson,
149
        array $composerLock,
150
        array $files,
151
        array $binaryFiles,
152
        array $compactors,
153
        ?int $compressionAlgorithm,
154
        ?int $fileMode,
155
        string $mainScriptPath,
156
        string $mainScriptContents,
157
        MapFile $fileMapper,
158
        $metadata,
159
        string $tmpOutputPath,
160
        string $outputPath,
161
        ?string $privateKeyPassphrase,
162
        ?string $privateKeyPath,
163
        bool $isPrivateKeyPrompt,
164
        array $processedReplacements,
165
        ?string $shebang,
166
        int $signingAlgorithm,
167
        ?string $stubBannerContents,
168
        ?string $stubBannerPath,
169
        ?string $stubPath,
170
        bool $isInterceptFileFuncs,
171
        bool $isStubGenerated,
172
        bool $checkRequirements
173
    ) {
174
        Assertion::nullOrInArray(
175
            $compressionAlgorithm,
176
            get_phar_compression_algorithms(),
177
            sprintf(
178
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
179
                implode('", "', array_keys(get_phar_compression_algorithms()))
180
            )
181
        );
182
183
        $this->file = $file;
184
        $this->alias = $alias;
185
        $this->basePath = $basePath;
186
        $this->composerJson = $composerJson;
187
        $this->composerLock = $composerLock;
188
        $this->files = $files;
189
        $this->binaryFiles = $binaryFiles;
190
        $this->compactors = $compactors;
191
        $this->compressionAlgorithm = $compressionAlgorithm;
192
        $this->fileMode = $fileMode;
193
        $this->mainScriptPath = $mainScriptPath;
194
        $this->mainScriptContents = $mainScriptContents;
195
        $this->fileMapper = $fileMapper;
196
        $this->metadata = $metadata;
197
        $this->tmpOutputPath = $tmpOutputPath;
198
        $this->outputPath = $outputPath;
199
        $this->privateKeyPassphrase = $privateKeyPassphrase;
200
        $this->privateKeyPath = $privateKeyPath;
201
        $this->isPrivateKeyPrompt = $isPrivateKeyPrompt;
202
        $this->processedReplacements = $processedReplacements;
203
        $this->shebang = $shebang;
204
        $this->signingAlgorithm = $signingAlgorithm;
205
        $this->stubBannerContents = $stubBannerContents;
206
        $this->stubBannerPath = $stubBannerPath;
207
        $this->stubPath = $stubPath;
208
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
209
        $this->isStubGenerated = $isStubGenerated;
210
        $this->checkRequirements = $checkRequirements;
211
    }
212
213
    public static function create(?string $file, stdClass $raw): self
214
    {
215
        $alias = self::retrieveAlias($raw);
216
217
        $basePath = self::retrieveBasePath($file, $raw);
218
219
        $composerFiles = self::retrieveComposerFiles($basePath);
220
221
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles[0][1]);
222
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
223
224
        [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath);
225
226
        $composerJson = $composerFiles[0];
227
        $composerLock = $composerFiles[1];
228
229
        $devPackages = ComposerConfiguration::retrieveDevPackages($basePath, $composerJson[1], $composerLock[1]);
230
231
        [$excludedPaths, $blacklistFilter] = self::retrieveBlacklistFilter($raw, $basePath, $tmpOutputPath, $outputPath);
232
233
        if (self::shouldRetrieveAllFiles($file, $raw)) {
234
            $filesAggregate = self::retrieveAllFiles($basePath, $mainScriptPath, $blacklistFilter, $excludedPaths, $devPackages);
235
            $binaryFilesAggregate = [];
236
        } else {
237
            $files = self::retrieveFiles($raw, 'files', $basePath, $composerFiles);
238
239
            $directories = self::retrieveDirectories($raw, 'directories', $basePath, $blacklistFilter, $excludedPaths);
240
            $filesFromFinders = self::retrieveFilesFromFinders($raw, 'finder', $basePath, $blacklistFilter, $devPackages);
241
242
            $filesAggregate = array_unique(iterator_to_array(chain($files, $directories, ...$filesFromFinders)));
243
244
            $binaryFiles = self::retrieveFiles($raw, 'files-bin', $basePath);
245
            $binaryDirectories = self::retrieveDirectories($raw, 'directories-bin', $basePath, $blacklistFilter, $excludedPaths);
246
            $binaryFilesFromFinders = self::retrieveFilesFromFinders($raw, 'finder-bin', $basePath, $blacklistFilter, $devPackages);
247
248
            $binaryFilesAggregate = array_unique(iterator_to_array(chain($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders)));
249
        }
250
251
        $compactors = self::retrieveCompactors($raw, $basePath);
252
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw);
253
254
        $fileMode = self::retrieveFileMode($raw);
255
256
        $map = self::retrieveMap($raw);
257
        $fileMapper = new MapFile($map);
258
259
        $metadata = self::retrieveMetadata($raw);
260
261
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw);
262
        $privateKeyPath = self::retrievePrivateKeyPath($raw);
263
        $isPrivateKeyPrompt = self::retrieveIsPrivateKeyPrompt($raw);
264
265
        $replacements = self::retrieveReplacements($raw);
266
        $processedReplacements = self::retrieveProcessedReplacements($replacements, $raw, $file);
267
268
        $shebang = self::retrieveShebang($raw);
269
270
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw);
271
272
        $stubBannerContents = self::retrieveStubBannerContents($raw);
273
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath);
274
275
        if (null !== $stubBannerPath) {
276
            $stubBannerContents = file_contents($stubBannerPath);
277
        }
278
279
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
280
281
        $stubPath = self::retrieveStubPath($raw, $basePath);
282
283
        $isInterceptFileFuncs = self::retrieveIsInterceptFileFuncs($raw);
284
        $isStubGenerated = self::retrieveIsStubGenerated($raw, $stubPath);
285
286
        $checkRequirements = self::retrieveCheckRequirements(
287
            $raw,
288
            null !== $composerLock[0],
289
            $isStubGenerated
290
        );
291
292
        return new self(
293
            $file,
294
            $alias,
295
            $basePath,
296
            $composerJson,
297
            $composerLock,
298
            $filesAggregate,
299
            $binaryFilesAggregate,
300
            $compactors,
301
            $compressionAlgorithm,
302
            $fileMode,
303
            $mainScriptPath,
304
            $mainScriptContents,
305
            $fileMapper,
306
            $metadata,
307
            $tmpOutputPath,
308
            $outputPath,
309
            $privateKeyPassphrase,
310
            $privateKeyPath,
311
            $isPrivateKeyPrompt,
312
            $processedReplacements,
313
            $shebang,
314
            $signingAlgorithm,
315
            $stubBannerContents,
316
            $stubBannerPath,
317
            $stubPath,
318
            $isInterceptFileFuncs,
319
            $isStubGenerated,
320
            $checkRequirements
321
        );
322
    }
323
324
    public function getFile(): ?string
325
    {
326
        return $this->file;
327
    }
328
329
    public function getAlias(): string
330
    {
331
        return $this->alias;
332
    }
333
334
    public function getBasePath(): string
335
    {
336
        return $this->basePath;
337
    }
338
339
    public function getComposerJson(): ?string
340
    {
341
        return $this->composerJson[0];
342
    }
343
344
    public function getComposerJsonDecodedContents(): ?array
345
    {
346
        return $this->composerJson[1];
347
    }
348
349
    public function getComposerLock(): ?string
350
    {
351
        return $this->composerLock[0];
352
    }
353
354
    public function getComposerLockDecodedContents(): ?array
355
    {
356
        return $this->composerLock[1];
357
    }
358
359
    /**
360
     * @return string[]
361
     */
362
    public function getFiles(): array
363
    {
364
        return $this->files;
365
    }
366
367
    /**
368
     * @return string[]
369
     */
370
    public function getBinaryFiles(): array
371
    {
372
        return $this->binaryFiles;
373
    }
374
375
    /**
376
     * @return Compactor[] the list of compactors
377
     */
378
    public function getCompactors(): array
379
    {
380
        return $this->compactors;
381
    }
382
383
    public function getCompressionAlgorithm(): ?int
384
    {
385
        return $this->compressionAlgorithm;
386
    }
387
388
    public function getFileMode(): ?int
389
    {
390
        return $this->fileMode;
391
    }
392
393
    public function getMainScriptPath(): string
394
    {
395
        return $this->mainScriptPath;
396
    }
397
398
    public function getMainScriptContents(): string
399
    {
400
        return $this->mainScriptContents;
401
    }
402
403
    public function checkRequirements(): bool
404
    {
405
        return $this->checkRequirements;
406
    }
407
408
    public function getTmpOutputPath(): string
409
    {
410
        return $this->tmpOutputPath;
411
    }
412
413
    public function getOutputPath(): string
414
    {
415
        return $this->outputPath;
416
    }
417
418
    /**
419
     * @return string[]
420
     */
421
    public function getMap(): array
422
    {
423
        return $this->fileMapper->getMap();
424
    }
425
426
    public function getFileMapper(): MapFile
427
    {
428
        return $this->fileMapper;
429
    }
430
431
    /**
432
     * @return mixed
433
     */
434
    public function getMetadata()
435
    {
436
        return $this->metadata;
437
    }
438
439
    public function getPrivateKeyPassphrase(): ?string
440
    {
441
        return $this->privateKeyPassphrase;
442
    }
443
444
    public function getPrivateKeyPath(): ?string
445
    {
446
        return $this->privateKeyPath;
447
    }
448
449
    public function isPrivateKeyPrompt(): bool
450
    {
451
        return $this->isPrivateKeyPrompt;
452
    }
453
454
    public function getProcessedReplacements(): array
455
    {
456
        return $this->processedReplacements;
457
    }
458
459
    public function getShebang(): ?string
460
    {
461
        return $this->shebang;
462
    }
463
464
    public function getSigningAlgorithm(): int
465
    {
466
        return $this->signingAlgorithm;
467
    }
468
469
    public function getStubBannerContents(): ?string
470
    {
471
        return $this->stubBannerContents;
472
    }
473
474
    public function getStubBannerPath(): ?string
475
    {
476
        return $this->stubBannerPath;
477
    }
478
479
    public function getStubPath(): ?string
480
    {
481
        return $this->stubPath;
482
    }
483
484
    public function isInterceptFileFuncs(): bool
485
    {
486
        return $this->isInterceptFileFuncs;
487
    }
488
489
    public function isStubGenerated(): bool
490
    {
491
        return $this->isStubGenerated;
492
    }
493
494
    private static function retrieveAlias(stdClass $raw): string
495
    {
496
        if (false === isset($raw->alias)) {
497
            return uniqid('box-auto-generated-alias-', false).'.phar';
498
        }
499
500
        $alias = trim($raw->alias);
501
502
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
503
504
        return $alias;
505
    }
506
507
    private static function retrieveBasePath(?string $file, stdClass $raw): string
508
    {
509
        if (null === $file) {
510
            return getcwd();
511
        }
512
513
        if (false === isset($raw->{'base-path'})) {
514
            return realpath(dirname($file));
515
        }
516
517
        $basePath = trim($raw->{'base-path'});
518
519
        Assertion::directory(
520
            $basePath,
521
            'The base path "%s" is not a directory or does not exist.'
522
        );
523
524
        return realpath($basePath);
525
    }
526
527
    private static function shouldRetrieveAllFiles(?string $file, stdClass $raw): bool
528
    {
529
        if (null === $file) {
530
            return true;
531
        }
532
533
        // TODO: config should be casted into an array: it is easier to do and we need an array in several places now
534
        $rawConfig = (array) $raw;
535
536
        foreach (self::FILES_SETTINGS as $key) {
537
            if (array_key_exists($key, $rawConfig)) {
538
                return false;
539
            }
540
        }
541
542
        return true;
543
    }
544
545
    private static function retrieveBlacklistFilter(stdClass $raw, string $basePath, string ...$excludedPaths): array
546
    {
547
        $blacklist = self::retrieveBlacklist($raw, $basePath, ...$excludedPaths);
548
549
        $blacklistFilter = function (SplFileInfo $file) use ($blacklist): ?bool {
550
            if ($file->isLink()) {
551
                return false;
552
            }
553
554
            if (false === $file->getRealPath()) {
555
                return false;
556
            }
557
558
            if (in_array($file->getRealPath(), $blacklist, true)) {
559
                return false;
560
            }
561
562
            return null;
563
        };
564
565
        return [$blacklist, $blacklistFilter];
566
    }
567
568
    /**
569
     * @param stdClass $raw
570
     * @param string   $basePath
571
     * @param string[] $excludedPaths
572
     *
573
     * @return string[]
574
     */
575
    private static function retrieveBlacklist(stdClass $raw, string $basePath, string ...$excludedPaths): array
576
    {
577
        /** @var string[] $blacklist */
578
        $blacklist = array_merge($excludedPaths, $raw->blacklist ?? []);
579
580
        $normalizedBlacklist = [];
581
582
        foreach ($blacklist as $file) {
583
            $normalizedBlacklist[] = self::normalizePath($file, $basePath);
584
            $normalizedBlacklist[] = canonicalize(make_path_relative(trim($file), $basePath));
585
        }
586
587
        return array_unique($normalizedBlacklist);
588
    }
589
590
    /**
591
     * @return SplFileInfo[]
592
     */
593
    private static function retrieveFiles(stdClass $raw, string $key, string $basePath, array $composerFiles = []): array
594
    {
595
        $files = [];
596
597
        if (isset($composerFiles[0][0])) {
598
            $files[] = $composerFiles[0][0];
599
        }
600
601
        if (isset($composerFiles[1][1])) {
602
            $files[] = $composerFiles[1][0];
603
        }
604
605
        if (false === isset($raw->{$key})) {
606
            return [];
607
        }
608
609
        $files = array_merge((array) $raw->{$key}, $files);
610
611
        Assertion::allString($files);
612
613
        $normalizePath = function (string $file) use ($basePath, $key): SplFileInfo {
614
            $file = self::normalizePath($file, $basePath);
615
616
            if (is_link($file)) {
617
                // TODO: add this to baberlei/assert
618
                throw new InvalidArgumentException(
619
                    sprintf(
620
                        'Cannot add the link "%s": links are not supported.',
621
                        $file
622
                    )
623
                );
624
            }
625
626
            Assertion::file(
627
                $file,
628
                sprintf(
629
                    '"%s" must contain a list of existing files. Could not find "%%s".',
630
                    $key
631
                )
632
            );
633
634
            return new SplFileInfo($file);
635
        };
636
637
        return array_map($normalizePath, $files);
638
    }
639
640
    /**
641
     * @param stdClass $raw
642
     * @param string   $key             Config property name
643
     * @param string   $basePath
644
     * @param Closure  $blacklistFilter
645
     * @param string[] $excludedPaths
646
     *
647
     * @return iterable|SplFileInfo[]
648
     */
649
    private static function retrieveDirectories(
650
        stdClass $raw,
651
        string $key,
652
        string $basePath,
653
        Closure $blacklistFilter,
654
        array $excludedPaths
655
    ): iterable {
656
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath);
657
658
        if ([] !== $directories) {
659
            $finder = Finder::create()
660
                ->files()
661
                ->filter($blacklistFilter)
662
                ->ignoreVCS(true)
663
                ->in($directories)
664
            ;
665
666
            foreach ($excludedPaths as $excludedPath) {
667
                $finder->notPath($excludedPath);
668
            }
669
670
            return $finder;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $finder returns the type Symfony\Component\Finder\Finder which is incompatible with the documented return type iterable|SplFileInfo[].
Loading history...
671
        }
672
673
        return [];
674
    }
675
676
    /**
677
     * @param stdClass $raw
678
     * @param string   $key
679
     * @param string   $basePath
680
     * @param Closure  $blacklistFilter
681
     * @param string[] $devPackages
682
     *
683
     * @return iterable[]|SplFileInfo[][]
684
     */
685
    private static function retrieveFilesFromFinders(
686
        stdClass $raw,
687
        string $key,
688
        string $basePath,
689
        Closure $blacklistFilter,
690
        array $devPackages
691
    ): array {
692
        if (isset($raw->{$key})) {
693
            return self::processFinders($raw->{$key}, $basePath, $blacklistFilter, $devPackages);
694
        }
695
696
        return [];
697
    }
698
699
    /**
700
     * @param array    $findersConfig
701
     * @param string   $basePath
702
     * @param Closure  $blacklistFilter
703
     * @param string[] $devPackages
704
     *
705
     * @return Finder[]|SplFileInfo[][]
706
     */
707
    private static function processFinders(
708
        array $findersConfig,
709
        string $basePath,
710
        Closure $blacklistFilter,
711
        array $devPackages
712
    ): array {
713
        $processFinderConfig = function (stdClass $config) use ($basePath, $blacklistFilter, $devPackages) {
714
            return self::processFinder($config, $basePath, $blacklistFilter, $devPackages);
715
        };
716
717
        return array_map($processFinderConfig, $findersConfig);
718
    }
719
720
    /**
721
     * @param stdClass $config
722
     * @param string   $basePath
723
     * @param Closure  $blacklistFilter
724
     * @param string[] $devPackages
725
     *
726
     * @return Finder|SplFileInfo[]
727
     */
728
    private static function processFinder(
729
        stdClass $config,
730
        string $basePath,
731
        Closure $blacklistFilter,
732
        array $devPackages
733
    ): Finder {
734
        $finder = Finder::create()
735
            ->files()
736
            ->filter($blacklistFilter)
737
            ->filter(
738
                function (SplFileInfo $fileInfo) use ($devPackages): bool {
739
                    foreach ($devPackages as $devPackage) {
740
                        if ($devPackage === longest_common_base_path([$devPackage, $fileInfo->getRealPath()])) {
741
                            // File belongs to the dev package
742
                            return false;
743
                        }
744
                    }
745
746
                    return true;
747
                }
748
            )
749
            ->ignoreVCS(true)
750
        ;
751
752
        $normalizedConfig = (function (array $config, Finder $finder): array {
753
            $normalizedConfig = [];
754
755
            foreach ($config as $method => $arguments) {
756
                $method = trim($method);
757
                $arguments = (array) $arguments;
758
759
                Assertion::methodExists(
760
                    $method,
761
                    $finder,
762
                    'The method "Finder::%s" does not exist.'
763
                );
764
765
                $normalizedConfig[$method] = $arguments;
766
            }
767
768
            krsort($normalizedConfig);
769
770
            return $normalizedConfig;
771
        })((array) $config, $finder);
772
773
        $createNormalizedDirectories = function (string $directory) use ($basePath): ?string {
774
            $directory = self::normalizePath($directory, $basePath);
775
776
            if (is_link($directory)) {
777
                // TODO: add this to baberlei/assert
778
                throw new InvalidArgumentException(
779
                    sprintf(
780
                        'Cannot append the link "%s" to the Finder: links are not supported.',
781
                        $directory
782
                    )
783
                );
784
            }
785
786
            Assertion::directory($directory);
787
788
            return $directory;
789
        };
790
791
        $normalizeFileOrDirectory = function (string &$fileOrDirectory) use ($basePath): void {
792
            $fileOrDirectory = self::normalizePath($fileOrDirectory, $basePath);
793
794
            if (is_link($fileOrDirectory)) {
795
                // TODO: add this to baberlei/assert
796
                throw new InvalidArgumentException(
797
                    sprintf(
798
                        'Cannot append the link "%s" to the Finder: links are not supported.',
799
                        $fileOrDirectory
800
                    )
801
                );
802
            }
803
804
            // TODO: add this to baberlei/assert
805
            if (false === file_exists($fileOrDirectory)) {
806
                throw new InvalidArgumentException(
807
                    sprintf(
808
                        'Path "%s" was expected to be a file or directory. It may be a symlink (which are unsupported).',
809
                        $fileOrDirectory
810
                    )
811
                );
812
            }
813
814
            // TODO: add fileExists (as file or directory) to Assert
815
            if (false === is_file($fileOrDirectory)) {
816
                Assertion::directory($fileOrDirectory);
817
            } else {
818
                Assertion::file($fileOrDirectory);
819
            }
820
        };
821
822
        foreach ($normalizedConfig as $method => $arguments) {
823
            if ('in' === $method) {
824
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
825
            }
826
827
            if ('exclude' === $method) {
828
                $arguments = array_unique(array_map('trim', $arguments));
829
            }
830
831
            if ('append' === $method) {
832
                array_walk($arguments, $normalizeFileOrDirectory);
833
834
                $arguments = [$arguments];
835
            }
836
837
            foreach ($arguments as $argument) {
838
                $finder->$method($argument);
839
            }
840
        }
841
842
        return $finder;
843
    }
844
845
    /**
846
     * @param string   $basePath
847
     * @param string   $mainScriptPath
848
     * @param Closure  $blacklistFilter
849
     * @param string[] $excludedPaths
850
     * @param string[] $devPackages
851
     *
852
     * @return SplFileInfo[]
853
     */
854
    private static function retrieveAllFiles(
855
        string $basePath,
856
        string $mainScriptPath,
857
        Closure $blacklistFilter,
858
        array $excludedPaths,
859
        array $devPackages
860
    ): array {
861
        $relativeDevPackages = array_map(
862
            function (string $packagePath) use ($basePath): string {
863
                return make_path_relative($packagePath, $basePath);
864
            },
865
            $devPackages
866
        );
867
868
        $finder = Finder::create()
869
            ->files()
870
            ->in($basePath)
871
            ->notPath(make_path_relative($mainScriptPath, $basePath))
872
            ->filter($blacklistFilter)
873
            ->exclude($relativeDevPackages)
874
            ->ignoreVCS(true)
875
        ;
876
877
        $excludedPaths = array_unique(
878
            array_filter(
879
                array_map(
880
                    function (string $path) use ($basePath): string {
881
                        return make_path_relative($path, $basePath);
882
                    },
883
                    $excludedPaths
884
                ),
885
                function (string $path): bool {
886
                    return '..' !== substr($path, 0, 2);
887
                }
888
            )
889
        );
890
891
        foreach ($excludedPaths as $excludedPath) {
892
            $finder->notPath($excludedPath);
893
        }
894
895
        return array_unique(
896
            toArray(
897
                map(
898
                    method('getRealPath'),
899
                    $finder
900
                )
901
            )
902
        );
903
    }
904
905
    /**
906
     * @param stdClass $raw
907
     * @param string   $key      Config property name
908
     * @param string   $basePath
909
     *
910
     * @return string[]
911
     */
912
    private static function retrieveDirectoryPaths(stdClass $raw, string $key, string $basePath): array
913
    {
914
        if (false === isset($raw->{$key})) {
915
            return [];
916
        }
917
918
        $directories = $raw->{$key};
919
920
        $normalizeDirectory = function (string $directory) use ($basePath, $key): string {
921
            $directory = self::normalizePath($directory, $basePath);
922
923
            if (is_link($directory)) {
924
                // TODO: add this to baberlei/assert
925
                throw new InvalidArgumentException(
926
                    sprintf(
927
                        'Cannot add the link "%s": links are not supported.',
928
                        $directory
929
                    )
930
                );
931
            }
932
933
            Assertion::directory(
934
                $directory,
935
                sprintf(
936
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
937
                    $key
938
                )
939
            );
940
941
            return $directory;
942
        };
943
944
        return array_map($normalizeDirectory, $directories);
945
    }
946
947
    private static function normalizePath(string $file, string $basePath): string
948
    {
949
        return make_path_absolute(trim($file), $basePath);
950
    }
951
952
    /**
953
     * @return Compactor[]
954
     */
955
    private static function retrieveCompactors(stdClass $raw, string $basePath): array
956
    {
957
        if (false === isset($raw->compactors)) {
958
            return [];
959
        }
960
961
        $compactorClasses = array_unique((array) $raw->compactors);
962
963
        return array_map(
964
            function (string $class) use ($raw, $basePath): Compactor {
965
                Assertion::classExists($class, 'The compactor class "%s" does not exist.');
966
                Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
967
968
                if (Php::class === $class || LegacyPhp::class === $class) {
969
                    return self::createPhpCompactor($raw);
970
                }
971
972
                if (PhpScoperCompactor::class === $class) {
973
                    $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath);
974
975
                    return new PhpScoperCompactor(
976
                        new SimpleScoper(
977
                            create_scoper(),
978
                            uniqid('_HumbugBox', false),
979
                            $phpScoperConfig->getWhitelist(),
980
                            $phpScoperConfig->getPatchers()
981
                        )
982
                    );
983
                }
984
985
                return new $class();
986
            },
987
            $compactorClasses
988
        );
989
    }
990
991
    private static function retrieveCompressionAlgorithm(stdClass $raw): ?int
992
    {
993
        if (false === isset($raw->compression)) {
994
            return null;
995
        }
996
997
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
998
999
        Assertion::inArray(
1000
            $raw->compression,
1001
            $knownAlgorithmNames,
1002
            sprintf(
1003
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
1004
                implode('", "', $knownAlgorithmNames)
1005
            )
1006
        );
1007
1008
        $value = get_phar_compression_algorithms()[$raw->compression];
1009
1010
        // Phar::NONE is not valid for compressFiles()
1011
        if (Phar::NONE === $value) {
1012
            return null;
1013
        }
1014
1015
        return $value;
1016
    }
1017
1018
    private static function retrieveFileMode(stdClass $raw): ?int
1019
    {
1020
        if (isset($raw->chmod)) {
1021
            return intval($raw->chmod, 8);
1022
        }
1023
1024
        return null;
1025
    }
1026
1027
    private static function retrieveMainScriptPath(stdClass $raw, string $basePath, ?array $decodedJsonContents): string
1028
    {
1029
        if (isset($raw->main)) {
1030
            $main = $raw->main;
1031
        } else {
1032
            if (null === $decodedJsonContents
1033
                || false === array_key_exists('bin', $decodedJsonContents)
1034
                || false === $main = current($decodedJsonContents['bin'])
1035
            ) {
1036
                $main = self::DEFAULT_MAIN_SCRIPT;
1037
            }
1038
        }
1039
1040
        return self::normalizePath($main, $basePath);
1041
    }
1042
1043
    private static function retrieveMainScriptContents(string $mainScriptPath): string
1044
    {
1045
        $contents = file_contents($mainScriptPath);
1046
1047
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
1048
        // PHAR entry point file.
1049
        return preg_replace('/^#!.*\s*/', '', $contents);
1050
    }
1051
1052
    private static function retrieveComposerFiles(string $basePath): array
1053
    {
1054
        $retrieveFileAndContents = function (string $file): array {
1055
            $json = new Json();
1056
1057
            if (false === file_exists($file) || false === is_file($file) || false === is_readable($file)) {
1058
                return [null, null];
1059
            }
1060
1061
            try {
1062
                $contents = $json->decodeFile($file, true);
1063
            } catch (ParsingException $exception) {
1064
                throw new InvalidArgumentException(
1065
                    sprintf(
1066
                        'Expected the file "%s" to be a valid composer.json file but an error has been found: %s',
1067
                        $file,
1068
                        $exception->getMessage()
1069
                    ),
1070
                    0,
1071
                    $exception
1072
                );
1073
            }
1074
1075
            return [$file, $contents];
1076
        };
1077
1078
        [$composerJson, $composerJsonContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.json'));
1079
        [$composerLock, $composerLockContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.lock'));
1080
1081
        return [
1082
            [$composerJson, $composerJsonContents],
1083
            [$composerLock, $composerLockContents],
1084
        ];
1085
    }
1086
1087
    /**
1088
     * @return string[][]
1089
     */
1090
    private static function retrieveMap(stdClass $raw): array
1091
    {
1092
        if (false === isset($raw->map)) {
1093
            return [];
1094
        }
1095
1096
        $map = [];
1097
1098
        foreach ((array) $raw->map as $item) {
1099
            $processed = [];
1100
1101
            foreach ($item as $match => $replace) {
1102
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
1103
            }
1104
1105
            if (isset($processed['_empty_'])) {
1106
                $processed[''] = $processed['_empty_'];
1107
1108
                unset($processed['_empty_']);
1109
            }
1110
1111
            $map[] = $processed;
1112
        }
1113
1114
        return $map;
1115
    }
1116
1117
    /**
1118
     * @return mixed
1119
     */
1120
    private static function retrieveMetadata(stdClass $raw)
1121
    {
1122
        if (isset($raw->metadata)) {
1123
            if (is_object($raw->metadata)) {
1124
                return (array) $raw->metadata;
1125
            }
1126
1127
            return $raw->metadata;
1128
        }
1129
1130
        return null;
1131
    }
1132
1133
    /**
1134
     * @return string[] The first element is the temporary output path and the second the real one
1135
     */
1136
    private static function retrieveOutputPath(stdClass $raw, string $basePath, string $mainScriptPath): array
1137
    {
1138
        if (isset($raw->output)) {
1139
            $path = $raw->output;
1140
        } else {
1141
            if (1 === preg_match('/^(?<main>.*?)(?:\.[\p{L}\d]+)?$/', $mainScriptPath, $matches)) {
1142
                $path = $matches['main'].'.phar';
1143
            } else {
1144
                // Last resort, should not happen
1145
                $path = self::DEFAULT_ALIAS;
1146
            }
1147
        }
1148
1149
        $tmp = $real = self::normalizePath($path, $basePath);
1150
1151
        if ('.phar' !== substr($real, -5)) {
1152
            $tmp .= '.phar';
1153
        }
1154
1155
        return [$tmp, $real];
1156
    }
1157
1158
    private static function retrievePrivateKeyPassphrase(stdClass $raw): ?string
1159
    {
1160
        // TODO: add check to not allow this setting without the private key path
1161
        if (isset($raw->{'key-pass'})
1162
            && is_string($raw->{'key-pass'})
1163
        ) {
1164
            return $raw->{'key-pass'};
1165
        }
1166
1167
        return null;
1168
    }
1169
1170
    private static function retrievePrivateKeyPath(stdClass $raw): ?string
1171
    {
1172
        // TODO: If passed need to check its existence
1173
        // Also need
1174
1175
        if (isset($raw->key)) {
1176
            return $raw->key;
1177
        }
1178
1179
        return null;
1180
    }
1181
1182
    private static function retrieveReplacements(stdClass $raw): array
1183
    {
1184
        // TODO: add exmample in the doc
1185
        // Add checks against the values
1186
        if (isset($raw->replacements)) {
1187
            return (array) $raw->replacements;
1188
        }
1189
1190
        return [];
1191
    }
1192
1193
    private static function retrieveProcessedReplacements(
1194
        array $replacements,
1195
        stdClass $raw,
1196
        ?string $file
1197
    ): array {
1198
        if (null === $file) {
1199
            return [];
1200
        }
1201
1202
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw))) {
1203
            $replacements[$git] = self::retrieveGitHash($file);
1204
        }
1205
1206
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw))) {
1207
            $replacements[$git] = self::retrieveGitHash($file, true);
1208
        }
1209
1210
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw))) {
1211
            $replacements[$git] = self::retrieveGitTag($file);
1212
        }
1213
1214
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw))) {
1215
            $replacements[$git] = self::retrieveGitVersion($file);
1216
        }
1217
1218
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw))) {
1219
            $replacements[$date] = self::retrieveDatetimeNow(
1220
                self::retrieveDatetimeFormat($raw)
1221
            );
1222
        }
1223
1224
        $sigil = self::retrieveReplacementSigil($raw);
1225
1226
        foreach ($replacements as $key => $value) {
1227
            unset($replacements[$key]);
1228
            $replacements["$sigil$key$sigil"] = $value;
1229
        }
1230
1231
        return $replacements;
1232
    }
1233
1234
    private static function retrieveGitHashPlaceholder(stdClass $raw): ?string
1235
    {
1236
        if (isset($raw->{'git-commit'})) {
1237
            return $raw->{'git-commit'};
1238
        }
1239
1240
        return null;
1241
    }
1242
1243
    /**
1244
     * @param string $file
1245
     * @param bool   $short Use the short version
1246
     *
1247
     * @return string the commit hash
1248
     */
1249
    private static function retrieveGitHash(string $file, bool $short = false): string
1250
    {
1251
        return self::runGitCommand(
1252
            sprintf(
1253
                'git log --pretty="%s" -n1 HEAD',
1254
                $short ? '%h' : '%H'
1255
            ),
1256
            $file
1257
        );
1258
    }
1259
1260
    private static function retrieveGitShortHashPlaceholder(stdClass $raw): ?string
1261
    {
1262
        if (isset($raw->{'git-commit-short'})) {
1263
            return $raw->{'git-commit-short'};
1264
        }
1265
1266
        return null;
1267
    }
1268
1269
    private static function retrieveGitTagPlaceholder(stdClass $raw): ?string
1270
    {
1271
        if (isset($raw->{'git-tag'})) {
1272
            return $raw->{'git-tag'};
1273
        }
1274
1275
        return null;
1276
    }
1277
1278
    private static function retrieveGitTag(string $file): ?string
1279
    {
1280
        return self::runGitCommand('git describe --tags HEAD', $file);
1281
    }
1282
1283
    private static function retrieveGitVersionPlaceholder(stdClass $raw): ?string
1284
    {
1285
        if (isset($raw->{'git-version'})) {
1286
            return $raw->{'git-version'};
1287
        }
1288
1289
        return null;
1290
    }
1291
1292
    private static function retrieveGitVersion(string $file): ?string
1293
    {
1294
        // TODO: check if is still relevant as IMO we are better off using OcramiusVersionPackage
1295
        // to avoid messing around with that
1296
1297
        try {
1298
            return self::retrieveGitTag($file);
1299
        } catch (RuntimeException $exception) {
1300
            try {
1301
                return self::retrieveGitHash($file, true);
1302
            } catch (RuntimeException $exception) {
1303
                throw new RuntimeException(
1304
                    sprintf(
1305
                        'The tag or commit hash could not be retrieved from "%s": %s',
1306
                        dirname($file),
1307
                        $exception->getMessage()
1308
                    ),
1309
                    0,
1310
                    $exception
1311
                );
1312
            }
1313
        }
1314
    }
1315
1316
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw): ?string
1317
    {
1318
        // TODO: double check why this is done and how it is used it's not completely clear to me.
1319
        // Also make sure the documentation is up to date after.
1320
        // Instead of having two sistinct doc entries for `datetime` and `datetime-format`, it would
1321
        // be better to have only one element IMO like:
1322
        //
1323
        // "datetime": {
1324
        //   "value": "val",
1325
        //   "format": "Y-m-d"
1326
        // }
1327
        //
1328
        // Also add a check that one cannot be provided without the other. Or maybe it should? I guess
1329
        // if the datetime format is the default one it's ok; but in any case the format should not
1330
        // be added without the datetime value...
1331
1332
        if (isset($raw->{'datetime'})) {
1333
            return $raw->{'datetime'};
1334
        }
1335
1336
        return null;
1337
    }
1338
1339
    private static function retrieveDatetimeNow(string $format)
1340
    {
1341
        $now = new DateTimeImmutable('now');
1342
1343
        $datetime = $now->format($format);
1344
1345
        if (!$datetime) {
1346
            throw new InvalidArgumentException(
1347
                sprintf(
1348
                    '""%s" is not a valid PHP date format',
1349
                    $format
1350
                )
1351
            );
1352
        }
1353
1354
        return $datetime;
1355
    }
1356
1357
    private static function retrieveDatetimeFormat(stdClass $raw): string
1358
    {
1359
        if (isset($raw->{'datetime_format'})) {
1360
            return $raw->{'datetime_format'};
1361
        }
1362
1363
        return self::DEFAULT_DATETIME_FORMAT;
1364
    }
1365
1366
    private static function retrieveReplacementSigil(stdClass $raw)
1367
    {
1368
        if (isset($raw->{'replacement-sigil'})) {
1369
            return $raw->{'replacement-sigil'};
1370
        }
1371
1372
        return self::DEFAULT_REPLACEMENT_SIGIL;
1373
    }
1374
1375
    private static function retrieveShebang(stdClass $raw): ?string
1376
    {
1377
        if (false === array_key_exists('shebang', (array) $raw)) {
1378
            return self::DEFAULT_SHEBANG;
1379
        }
1380
1381
        if (null === $raw->shebang) {
1382
            return null;
1383
        }
1384
1385
        $shebang = trim($raw->shebang);
1386
1387
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
1388
        Assertion::true(
1389
            '#!' === substr($shebang, 0, 2),
1390
            sprintf(
1391
                'The shebang line must start with "#!". Got "%s" instead',
1392
                $shebang
1393
            )
1394
        );
1395
1396
        return $shebang;
1397
    }
1398
1399
    private static function retrieveSigningAlgorithm(stdClass $raw): int
1400
    {
1401
        // TODO: trigger warning: if no signing algorithm is given provided we are not in dev mode
1402
        // TODO: trigger a warning if the signing algorithm used is weak
1403
        // TODO: no longer accept strings & document BC break
1404
        if (false === isset($raw->algorithm)) {
1405
            return Phar::SHA1;
1406
        }
1407
1408
        if (false === defined('Phar::'.$raw->algorithm)) {
1409
            throw new InvalidArgumentException(
1410
                sprintf(
1411
                    'The signing algorithm "%s" is not supported.',
1412
                    $raw->algorithm
1413
                )
1414
            );
1415
        }
1416
1417
        return constant('Phar::'.$raw->algorithm);
1418
    }
1419
1420
    private static function retrieveStubBannerContents(stdClass $raw): ?string
1421
    {
1422
        if (false === array_key_exists('banner', (array) $raw)) {
1423
            return self::DEFAULT_BANNER;
1424
        }
1425
1426
        if (null === $raw->banner) {
1427
            return null;
1428
        }
1429
1430
        $banner = $raw->banner;
1431
1432
        if (is_array($banner)) {
1433
            $banner = implode("\n", $banner);
1434
        }
1435
1436
        return $banner;
1437
    }
1438
1439
    private static function retrieveStubBannerPath(stdClass $raw, string $basePath): ?string
1440
    {
1441
        if (false === isset($raw->{'banner-file'})) {
1442
            return null;
1443
        }
1444
1445
        $bannerFile = make_path_absolute($raw->{'banner-file'}, $basePath);
1446
1447
        Assertion::file($bannerFile);
1448
1449
        return $bannerFile;
1450
    }
1451
1452
    private static function normalizeStubBannerContents(?string $contents): ?string
1453
    {
1454
        if (null === $contents) {
1455
            return null;
1456
        }
1457
1458
        $banner = explode("\n", $contents);
1459
        $banner = array_map('trim', $banner);
1460
1461
        return implode("\n", $banner);
1462
    }
1463
1464
    private static function retrieveStubPath(stdClass $raw, string $basePath): ?string
1465
    {
1466
        if (isset($raw->stub) && is_string($raw->stub)) {
1467
            $stubPath = make_path_absolute($raw->stub, $basePath);
1468
1469
            Assertion::file($stubPath);
1470
1471
            return $stubPath;
1472
        }
1473
1474
        return null;
1475
    }
1476
1477
    private static function retrieveIsInterceptFileFuncs(stdClass $raw): bool
1478
    {
1479
        if (isset($raw->intercept)) {
1480
            return $raw->intercept;
1481
        }
1482
1483
        return false;
1484
    }
1485
1486
    private static function retrieveIsPrivateKeyPrompt(stdClass $raw): bool
1487
    {
1488
        return isset($raw->{'key-pass'}) && (true === $raw->{'key-pass'});
1489
    }
1490
1491
    private static function retrieveIsStubGenerated(stdClass $raw, ?string $stubPath): bool
1492
    {
1493
        return null === $stubPath && (false === isset($raw->stub) || false !== $raw->stub);
1494
    }
1495
1496
    private static function retrieveCheckRequirements(stdClass $raw, bool $hasComposerLock, bool $generateStub): bool
1497
    {
1498
        // TODO: emit warning when stub is not generated and check requirements is explicitly set to true
1499
        // TODO: emit warning when no composer lock is found but check requirements is explicitely set to true
1500
        if (false === $hasComposerLock) {
1501
            return false;
1502
        }
1503
1504
        return $raw->{'check-requirements'} ?? true;
1505
    }
1506
1507
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath): PhpScoperConfiguration
1508
    {
1509
        if (!isset($raw->{'php-scoper'})) {
1510
            $configFilePath = make_path_absolute(self::PHP_SCOPER_CONFIG, $basePath);
1511
1512
            return file_exists($configFilePath)
1513
                ? PhpScoperConfiguration::load($configFilePath)
1514
                : PhpScoperConfiguration::load()
1515
             ;
1516
        }
1517
1518
        $configFile = $raw->phpScoper;
1519
1520
        Assertion::string($configFile);
1521
1522
        $configFilePath = make_path_absolute($configFile, $basePath);
1523
1524
        Assertion::file($configFilePath);
1525
        Assertion::readable($configFilePath);
1526
1527
        return PhpScoperConfiguration::load($configFilePath);
1528
    }
1529
1530
    /**
1531
     * Runs a Git command on the repository.
1532
     *
1533
     * @param string $command the command
1534
     *
1535
     * @return string the trimmed output from the command
1536
     */
1537
    private static function runGitCommand(string $command, string $file): string
1538
    {
1539
        $path = dirname($file);
1540
1541
        $process = new Process($command, $path);
1542
1543
        if (0 === $process->run()) {
1544
            return trim($process->getOutput());
1545
        }
1546
1547
        throw new RuntimeException(
1548
            sprintf(
1549
                'The tag or commit hash could not be retrieved from "%s": %s',
1550
                $path,
1551
                $process->getErrorOutput()
1552
            )
1553
        );
1554
    }
1555
1556
    private static function createPhpCompactor(stdClass $raw): Compactor
1557
    {
1558
        // TODO: false === not set; check & add test/doc
1559
        $tokenizer = new Tokenizer();
1560
1561
        if (false === empty($raw->annotations) && isset($raw->annotations->ignore)) {
1562
            $tokenizer->ignore(
1563
                (array) $raw->annotations->ignore
1564
            );
1565
        }
1566
1567
        return new Php($tokenizer);
1568
    }
1569
}
1570