Passed
Push — master ( 5154e0...001968 )
by Théo
02:07
created

Configuration::createPhpCompactor()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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