Compile   F
last analyzed

Complexity

Total Complexity 81

Size/Duplication

Total Lines 909
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 81
eloc 480
dl 0
loc 909
rs 2
c 2
b 1
f 0

28 Methods

Rating   Name   Duplication   Size   Complexity  
A createStub() 0 56 5
A signPharWithPrivateKey() 0 35 3
A registerMainScript() 0 36 3
B execute() 0 61 6
A registerReplacementValues() 0 25 3
A registerCompactors() 0 35 3
A removeExistingArtifacts() 0 26 3
A checkComposerVersion() 0 38 4
A addFilesWithErrorHandling() 0 14 2
A signPharWithoutPrivateKey() 0 17 2
A commit() 0 23 3
A createPhar() 0 60 3
A signPhar() 0 27 2
A registerFileMapping() 0 7 1
A getComposerBin() 0 5 2
A generateDockerFile() 0 7 1
A checkComposerArtifacts() 0 12 3
A getConfiguration() 0 58 1
A __construct() 0 2 1
A registerRequirementsChecker() 0 29 3
A configureMetadata() 0 18 4
A logMap() 0 26 5
A correctPermissions() 0 12 2
A registerStub() 0 40 3
A getDockerCommand() 0 3 1
A addFiles() 0 40 5
A logEndBuilding() 0 34 2
A configureCompressionAlgorithm() 0 52 5

How to fix   Complexity   

Complex Class

Complex classes like Compile often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Compile, and based on these observations, apply Extract Interface, too.

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\Console\Command;
16
17
use Amp\Parallel\Worker\TaskFailureThrowable;
18
use DateTimeImmutable;
19
use DateTimeInterface;
20
use Fidry\Console\Command\Command;
21
use Fidry\Console\Command\CommandAware;
22
use Fidry\Console\Command\CommandAwareness;
23
use Fidry\Console\Command\Configuration as CommandConfiguration;
24
use Fidry\Console\ExitCode;
25
use Fidry\Console\IO;
0 ignored issues
show
Bug introduced by
The type Fidry\Console\IO was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
26
use Fidry\FileSystem\FileSystem;
27
use Fidry\FileSystem\FS;
28
use Humbug\PhpScoper\Symbol\SymbolsRegistry;
29
use KevinGH\Box\Box;
30
use KevinGH\Box\Compactor\Compactor;
31
use KevinGH\Box\Composer\ComposerConfiguration;
32
use KevinGH\Box\Composer\ComposerOrchestrator;
33
use KevinGH\Box\Composer\ComposerProcessFactory;
34
use KevinGH\Box\Composer\Throwable\IncompatibleComposerVersion;
35
use KevinGH\Box\Configuration\Configuration;
36
use KevinGH\Box\Console\Logger\CompilerLogger;
0 ignored issues
show
Bug introduced by
The type KevinGH\Box\Console\Logger\CompilerLogger was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
37
use KevinGH\Box\Console\Logger\CompilerPsrLogger;
38
use KevinGH\Box\Console\MessageRenderer;
39
use KevinGH\Box\Console\OpenFileDescriptorLimiter;
40
use KevinGH\Box\Console\Php\PhpSettingsChecker;
41
use KevinGH\Box\Constants;
42
use KevinGH\Box\MapFile;
43
use KevinGH\Box\Phar\CompressionAlgorithm;
44
use KevinGH\Box\Phar\SigningAlgorithm;
45
use KevinGH\Box\RequirementChecker\RequirementsDumper;
46
use KevinGH\Box\StubGenerator;
47
use RuntimeException;
48
use stdClass;
49
use Symfony\Component\Console\Exception\RuntimeException as ConsoleRuntimeException;
50
use Symfony\Component\Console\Input\InputOption;
51
use Symfony\Component\Console\Input\StringInput;
52
use Symfony\Component\Console\Output\OutputInterface;
53
use Symfony\Component\Console\Question\Question;
54
use Symfony\Component\Filesystem\Path;
55
use function array_map;
56
use function array_shift;
57
use function count;
58
use function decoct;
59
use function explode;
60
use function file_exists;
61
use function filesize;
62
use function implode;
63
use function is_callable;
64
use function is_string;
65
use function KevinGH\Box\format_size;
66
use function KevinGH\Box\format_time;
67
use function memory_get_peak_usage;
68
use function memory_get_usage;
69
use function microtime;
70
use function putenv;
71
use function Safe\getcwd;
72
use function sprintf;
73
use function var_export;
74
use const PHP_EOL;
75
76
/**
77
 * @private
78
 */
79
final class Compile implements CommandAware
80
{
81
    use CommandAwareness;
82
83
    public const NAME = 'compile';
84
85
    private const HELP = <<<'HELP'
86
        The <info>%command.name%</info> command will compile code in a new PHAR based on a variety of settings.
87
        <comment>
88
          This command relies on a configuration file for loading
89
          PHAR packaging settings. If a configuration file is not
90
          specified through the <info>--config|-c</info> option, one of
91
          the following files will be used (in order): <info>box.json</info>,
92
          <info>box.json.dist</info>
93
        </comment>
94
        The configuration file is actually a JSON object saved to a file. For more
95
        information check the documentation online:
96
        <comment>
97
          https://github.com/humbug/box
98
        </comment>
99
        HELP;
100
101
    private const DEBUG_OPTION = 'debug';
102
    private const NO_PARALLEL_PROCESSING_OPTION = 'no-parallel';
103
    private const NO_RESTART_OPTION = 'no-restart';
104
    private const DEV_OPTION = 'dev';
105
    private const NO_CONFIG_OPTION = 'no-config';
106
    private const WITH_DOCKER_OPTION = 'with-docker';
107
    private const COMPOSER_BIN_OPTION = 'composer-bin';
108
    private const ALLOW_COMPOSER_COMPOSER_CHECK_FAILURE_OPTION = 'allow-composer-check-failure';
109
110
    private const DEBUG_DIR = '.box_dump';
111
112
    public function __construct(private readonly string $header)
113
    {
114
    }
115
116
    public function getConfiguration(): CommandConfiguration
117
    {
118
        return new CommandConfiguration(
119
            self::NAME,
120
            '🔨  Compiles an application into a PHAR',
121
            self::HELP,
122
            [],
123
            [
124
                new InputOption(
125
                    self::DEBUG_OPTION,
126
                    null,
127
                    InputOption::VALUE_NONE,
128
                    'Dump the files added to the PHAR in a `'.self::DEBUG_DIR.'` directory',
129
                ),
130
                new InputOption(
131
                    self::NO_PARALLEL_PROCESSING_OPTION,
132
                    null,
133
                    InputOption::VALUE_NONE,
134
                    'Disable the parallel processing',
135
                ),
136
                new InputOption(
137
                    self::NO_RESTART_OPTION,
138
                    null,
139
                    InputOption::VALUE_NONE,
140
                    'Do not restart the PHP process. Box restarts the process by default to disable xdebug and set `phar.readonly=0`',
141
                ),
142
                new InputOption(
143
                    self::DEV_OPTION,
144
                    null,
145
                    InputOption::VALUE_NONE,
146
                    'Skips the compression step',
147
                ),
148
                new InputOption(
149
                    self::NO_CONFIG_OPTION,
150
                    null,
151
                    InputOption::VALUE_NONE,
152
                    'Ignore the config file even when one is specified with the --config option',
153
                ),
154
                new InputOption(
155
                    self::WITH_DOCKER_OPTION,
156
                    null,
157
                    InputOption::VALUE_NONE,
158
                    'Generates a Dockerfile',
159
                ),
160
                new InputOption(
161
                    self::COMPOSER_BIN_OPTION,
162
                    null,
163
                    InputOption::VALUE_REQUIRED,
164
                    'Composer binary to use',
165
                ),
166
                new InputOption(
167
                    self::ALLOW_COMPOSER_COMPOSER_CHECK_FAILURE_OPTION,
168
                    null,
169
                    InputOption::VALUE_NONE,
170
                    'To continue even if an unsupported Composer version is detected',
171
                ),
172
                ConfigOption::getOptionInput(),
173
                ChangeWorkingDirOption::getOptionInput(),
174
            ],
175
        );
176
    }
177
178
    public function execute(IO $io): int
179
    {
180
        if ($io->getTypedOption(self::NO_RESTART_OPTION)->asBoolean()) {
181
            putenv(Constants::ALLOW_XDEBUG.'=1');
182
        }
183
184
        $debug = $io->getTypedOption(self::DEBUG_OPTION)->asBoolean();
185
186
        if ($debug) {
187
            $io->setVerbosity(OutputInterface::VERBOSITY_DEBUG);
188
        }
189
190
        PhpSettingsChecker::check($io);
191
192
        $disableParallelization = $io->getTypedOption(self::NO_PARALLEL_PROCESSING_OPTION)->asBoolean();
193
194
        if ($disableParallelization) {
195
            $io->writeln(
196
                '<info>[debug] Disabled parallel processing</info>',
197
                OutputInterface::VERBOSITY_DEBUG,
198
            );
199
        }
200
201
        ChangeWorkingDirOption::changeWorkingDirectory($io);
202
203
        $io->writeln($this->header);
204
205
        $config = $io->getTypedOption(self::NO_CONFIG_OPTION)->asBoolean()
206
            ? Configuration::create(null, new stdClass())
207
            : ConfigOption::getConfig($io, true);
208
        $config->setComposerBin(self::getComposerBin($io));
209
        $path = $config->getOutputPath();
210
211
        $logger = new CompilerLogger($io);
212
213
        $startTime = microtime(true);
214
215
        $logger->logStartBuilding($path);
216
217
        $this->removeExistingArtifacts($config, $logger, $debug);
218
219
        // Adding files might result in opening a lot of files. Either because not parallelized or when creating the
220
        // workers for parallelization.
221
        // As a result, we bump the file descriptor to an arbitrary number to ensure this process can run correctly
222
        $restoreLimit = OpenFileDescriptorLimiter::bumpLimit(2048, $io);
223
224
        try {
225
            $box = $this->createPhar($config, $logger, $io, !$disableParallelization, $debug);
226
        } finally {
227
            $restoreLimit();
228
        }
229
230
        self::correctPermissions($path, $config, $logger);
231
232
        self::logEndBuilding($config, $logger, $io, $box, $path, $startTime);
0 ignored issues
show
Bug introduced by
It seems like $startTime can also be of type string; however, parameter $startTime of KevinGH\Box\Console\Comm...mpile::logEndBuilding() does only seem to accept double, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

232
        self::logEndBuilding($config, $logger, $io, $box, $path, /** @scrutinizer ignore-type */ $startTime);
Loading history...
233
234
        if ($io->getTypedOption(self::WITH_DOCKER_OPTION)->asBoolean()) {
235
            return $this->generateDockerFile($io);
236
        }
237
238
        return ExitCode::SUCCESS;
239
    }
240
241
    private function createPhar(
242
        Configuration $config,
243
        CompilerLogger $logger,
244
        IO $io,
245
        bool $enableParallelization,
246
        bool $debug,
247
    ): Box {
248
        $tmpOutputPath = $config->getTmpOutputPath();
249
        $box = Box::create($tmpOutputPath, enableParallelization: $enableParallelization);
250
        $composerOrchestrator = new ComposerOrchestrator(
251
            ComposerProcessFactory::create(
252
                $config->getComposerBin(),
253
                $io,
254
            ),
255
            new CompilerPsrLogger($logger),
256
            new FileSystem(),
257
        );
258
259
        self::checkComposerVersion($composerOrchestrator, $config, $logger, $io);
260
261
        $box->startBuffering();
262
263
        self::registerReplacementValues($config, $box, $logger);
264
        self::registerCompactors($config, $box, $logger);
265
        self::registerFileMapping($config, $box, $logger);
266
267
        // Registering the main script _before_ adding the rest if of the files is _very_ important. The temporary
268
        // file used for debugging purposes and the Composer dump autoloading will not work correctly otherwise.
269
        $main = self::registerMainScript($config, $box, $logger);
270
271
        $check = self::registerRequirementsChecker($config, $box, $logger);
272
273
        self::addFiles($config, $box, $logger, $io);
274
275
        self::registerStub($config, $box, $main, $check, $logger);
276
        self::configureMetadata($config, $box, $logger);
277
278
        self::commit($box, $composerOrchestrator, $config, $logger);
279
280
        self::checkComposerArtifacts($box, $config, $logger);
281
282
        if ($debug) {
283
            $box->extractTo(self::DEBUG_DIR, true);
284
        }
285
286
        self::configureCompressionAlgorithm(
287
            $config,
288
            $box,
289
            $io->getTypedOption(self::DEV_OPTION)->asBoolean(),
290
            $io,
291
            $logger,
292
        );
293
294
        self::signPhar($config, $box, $tmpOutputPath, $io, $logger);
295
296
        if ($tmpOutputPath !== $config->getOutputPath()) {
297
            FS::rename($tmpOutputPath, $config->getOutputPath());
298
        }
299
300
        return $box;
301
    }
302
303
    private static function getComposerBin(IO $io): ?string
304
    {
305
        $composerBin = $io->getTypedOption(self::COMPOSER_BIN_OPTION)->asNullableNonEmptyString();
306
307
        return null === $composerBin ? null : Path::makeAbsolute($composerBin, getcwd());
308
    }
309
310
    private function removeExistingArtifacts(Configuration $config, CompilerLogger $logger, bool $debug): void
311
    {
312
        $path = $config->getOutputPath();
313
314
        if ($debug) {
315
            FS::remove(self::DEBUG_DIR);
316
317
            FS::dumpFile(
318
                self::DEBUG_DIR.'/.box_configuration',
319
                ConfigurationExporter::export($config),
320
            );
321
        }
322
323
        if (false === file_exists($path)) {
324
            return;
325
        }
326
327
        $logger->log(
328
            CompilerLogger::QUESTION_MARK_PREFIX,
329
            sprintf(
330
                'Removing the existing PHAR "%s"',
331
                $path,
332
            ),
333
        );
334
335
        FS::remove($path);
336
    }
337
338
    private static function checkComposerVersion(
339
        ComposerOrchestrator $composerOrchestrator,
340
        Configuration $config,
341
        CompilerLogger $logger,
342
        IO $io,
343
    ): void {
344
        if (!$config->dumpAutoload()) {
345
            $logger->log(
346
                CompilerLogger::QUESTION_MARK_PREFIX,
347
                'Skipping the Composer compatibility check: the autoloader is not dumped',
348
            );
349
350
            return;
351
        }
352
353
        $logger->log(
354
            CompilerLogger::QUESTION_MARK_PREFIX,
355
            'Checking Composer compatibility',
356
        );
357
358
        try {
359
            $composerOrchestrator->checkVersion();
360
361
            $logger->log(
362
                CompilerLogger::CHEVRON_PREFIX,
363
                'Supported version detected',
364
            );
365
        } catch (IncompatibleComposerVersion $incompatibleComposerVersion) {
366
            if ($io->getTypedOption(self::ALLOW_COMPOSER_COMPOSER_CHECK_FAILURE_OPTION)->asBoolean()) {
367
                $logger->log(
368
                    CompilerLogger::CHEVRON_PREFIX,
369
                    'Warning! Incompatible composer version detected: '.$incompatibleComposerVersion->getMessage(),
370
                );
371
372
                return; // Swallow the exception
373
            }
374
375
            throw $incompatibleComposerVersion;
376
        }
377
    }
378
379
    private static function registerReplacementValues(Configuration $config, Box $box, CompilerLogger $logger): void
380
    {
381
        $values = $config->getReplacements();
382
383
        if (0 === count($values)) {
384
            return;
385
        }
386
387
        $logger->log(
388
            CompilerLogger::QUESTION_MARK_PREFIX,
389
            'Setting replacement values',
390
        );
391
392
        foreach ($values as $key => $value) {
393
            $logger->log(
394
                CompilerLogger::PLUS_PREFIX,
395
                sprintf(
396
                    '%s: %s',
397
                    $key,
398
                    $value,
399
                ),
400
            );
401
        }
402
403
        $box->registerPlaceholders($values);
404
    }
405
406
    private static function registerCompactors(Configuration $config, Box $box, CompilerLogger $logger): void
407
    {
408
        $compactors = $config->getCompactors();
409
410
        if (0 === count($compactors)) {
411
            $logger->log(
412
                CompilerLogger::QUESTION_MARK_PREFIX,
413
                'No compactor to register',
414
            );
415
416
            return;
417
        }
418
419
        $logger->log(
420
            CompilerLogger::QUESTION_MARK_PREFIX,
421
            'Registering compactors',
422
        );
423
424
        $logCompactors = static function (Compactor $compactor) use ($logger): void {
425
            $compactorClassParts = explode('\\', $compactor::class);
426
427
            if (str_starts_with($compactorClassParts[0], '_HumbugBox')) {
428
                // Keep the non prefixed class name for the user
429
                array_shift($compactorClassParts);
430
            }
431
432
            $logger->log(
433
                CompilerLogger::PLUS_PREFIX,
434
                implode('\\', $compactorClassParts),
435
            );
436
        };
437
438
        array_map($logCompactors, $compactors->toArray());
439
440
        $box->registerCompactors($compactors);
441
    }
442
443
    private static function registerFileMapping(Configuration $config, Box $box, CompilerLogger $logger): void
444
    {
445
        $fileMapper = $config->getFileMapper();
446
447
        self::logMap($fileMapper, $logger);
448
449
        $box->registerFileMapping($fileMapper);
450
    }
451
452
    private static function addFiles(Configuration $config, Box $box, CompilerLogger $logger, IO $io): void
453
    {
454
        $logger->log(CompilerLogger::QUESTION_MARK_PREFIX, 'Adding binary files');
455
456
        $count = count($config->getBinaryFiles());
457
458
        $box->addFiles($config->getBinaryFiles(), true);
459
460
        $logger->log(
461
            CompilerLogger::CHEVRON_PREFIX,
462
            0 === $count
463
                ? 'No file found'
464
                : sprintf('%d file(s)', $count),
465
        );
466
467
        $logger->log(
468
            CompilerLogger::QUESTION_MARK_PREFIX,
469
            sprintf(
470
                'Auto-discover files? %s',
471
                $config->hasAutodiscoveredFiles() ? 'Yes' : 'No',
472
            ),
473
        );
474
        $logger->log(
475
            CompilerLogger::QUESTION_MARK_PREFIX,
476
            sprintf(
477
                'Exclude dev files? %s',
478
                $config->excludeDevFiles() ? 'Yes' : 'No',
479
            ),
480
        );
481
        $logger->log(CompilerLogger::QUESTION_MARK_PREFIX, 'Adding files');
482
483
        $count = count($config->getFiles());
484
485
        self::addFilesWithErrorHandling($config, $box, $io);
486
487
        $logger->log(
488
            CompilerLogger::CHEVRON_PREFIX,
489
            0 === $count
490
                ? 'No file found'
491
                : sprintf('%d file(s)', $count),
492
        );
493
    }
494
495
    private static function addFilesWithErrorHandling(Configuration $config, Box $box, IO $io): void
0 ignored issues
show
Unused Code introduced by
The parameter $io is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

495
    private static function addFilesWithErrorHandling(Configuration $config, Box $box, /** @scrutinizer ignore-unused */ IO $io): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
496
    {
497
        try {
498
            $box->addFiles($config->getFiles(), false);
499
500
            return;
501
        } catch (TaskFailureThrowable $ampFailure) {
502
            throw new ConsoleRuntimeException(
503
                sprintf(
504
                    'An Amp\Parallel error occurred. To diagnostic if it is an Amp error related, you may try again with "--no-parallel".'
505
                    .'Reason(s) of the failure: %s',
506
                    $ampFailure->getMessage(),
507
                ),
508
                previous: $ampFailure,
509
            );
510
        }
511
    }
512
513
    private static function registerMainScript(Configuration $config, Box $box, CompilerLogger $logger): ?string
514
    {
515
        if (false === $config->hasMainScript()) {
516
            $logger->log(
517
                CompilerLogger::QUESTION_MARK_PREFIX,
518
                'No main script path configured',
519
            );
520
521
            return null;
522
        }
523
524
        $main = $config->getMainScriptPath();
525
526
        $logger->log(
527
            CompilerLogger::QUESTION_MARK_PREFIX,
528
            sprintf(
529
                'Adding main file: %s',
530
                $main,
531
            ),
532
        );
533
534
        $localMain = $box->addFile(
535
            $main,
536
            $config->getMainScriptContents(),
537
        );
538
539
        $relativeMain = Path::makeRelative($main, $config->getBasePath());
540
541
        if ($localMain !== $relativeMain) {
542
            $logger->log(
543
                CompilerLogger::CHEVRON_PREFIX,
544
                $localMain,
545
            );
546
        }
547
548
        return $localMain;
549
    }
550
551
    private static function registerRequirementsChecker(Configuration $config, Box $box, CompilerLogger $logger): bool
552
    {
553
        if (false === $config->checkRequirements()) {
554
            $logger->log(
555
                CompilerLogger::QUESTION_MARK_PREFIX,
556
                'Skip requirements checker',
557
            );
558
559
            return false;
560
        }
561
562
        $logger->log(
563
            CompilerLogger::QUESTION_MARK_PREFIX,
564
            'Adding requirements checker',
565
        );
566
567
        $checkFiles = RequirementsDumper::dump(
568
            $config->getComposerJson(),
569
            $config->getComposerLock(),
570
            $config->getCompressionAlgorithm(),
571
        );
572
573
        foreach ($checkFiles as $fileWithContents) {
574
            [$file, $contents] = $fileWithContents;
575
576
            $box->addFile('.box/'.$file, $contents, true);
577
        }
578
579
        return true;
580
    }
581
582
    private static function registerStub(
583
        Configuration $config,
584
        Box $box,
585
        ?string $main,
586
        bool $checkRequirements,
587
        CompilerLogger $logger,
588
    ): void {
589
        if ($config->isStubGenerated()) {
590
            $logger->log(
591
                CompilerLogger::QUESTION_MARK_PREFIX,
592
                'Generating new stub',
593
            );
594
595
            $stub = self::createStub($config, $main, $checkRequirements, $logger);
596
597
            $box->setStub($stub);
598
599
            return;
600
        }
601
602
        if (null !== ($stub = $config->getStubPath())) {
603
            $logger->log(
604
                CompilerLogger::QUESTION_MARK_PREFIX,
605
                sprintf(
606
                    'Using stub file: %s',
607
                    $stub,
608
                ),
609
            );
610
611
            $box->registerStub($stub);
612
613
            return;
614
        }
615
616
        $box->setAlias($config->getAlias());
617
        $box->setDefaultStub($main);
0 ignored issues
show
Bug introduced by
It seems like $main can also be of type null; however, parameter $main of KevinGH\Box\Box::setDefaultStub() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

617
        $box->setDefaultStub(/** @scrutinizer ignore-type */ $main);
Loading history...
618
619
        $logger->log(
620
            CompilerLogger::QUESTION_MARK_PREFIX,
621
            'Using default stub',
622
        );
623
    }
624
625
    private static function configureMetadata(Configuration $config, Box $box, CompilerLogger $logger): void
626
    {
627
        if (null !== ($metadata = $config->getMetadata())) {
628
            $logger->log(
629
                CompilerLogger::QUESTION_MARK_PREFIX,
630
                'Setting metadata',
631
            );
632
633
            if (is_callable($metadata)) {
634
                $metadata = $metadata();
635
            }
636
637
            $logger->log(
638
                CompilerLogger::MINUS_PREFIX,
639
                is_string($metadata) ? $metadata : var_export($metadata, true),
640
            );
641
642
            $box->setMetadata($metadata);
643
        }
644
    }
645
646
    private static function commit(
647
        Box $box,
648
        ComposerOrchestrator $composerOrchestrator,
649
        Configuration $config,
650
        CompilerLogger $logger,
651
    ): void {
652
        $message = $config->dumpAutoload()
653
            ? 'Dumping the Composer autoloader'
654
            : 'Skipping dumping the Composer autoloader';
655
656
        $logger->log(CompilerLogger::QUESTION_MARK_PREFIX, $message);
657
658
        $excludeDevFiles = $config->excludeDevFiles();
659
660
        $box->endBuffering(
661
            $config->dumpAutoload()
662
                ? static fn (SymbolsRegistry $symbolsRegistry, string $prefix, array $excludeScoperFiles) => $composerOrchestrator->dumpAutoload(
663
                    $symbolsRegistry,
664
                    $prefix,
665
                    $excludeDevFiles,
666
                    $excludeScoperFiles,
667
                )
668
                : null,
669
        );
670
    }
671
672
    private static function checkComposerArtifacts(Box $box, Configuration $config, CompilerLogger $logger): void
673
    {
674
        $message = $config->excludeComposerArtifacts()
675
            ? 'Removing the Composer dump artefacts'
676
            : 'Keep the Composer dump artefacts';
677
678
        $logger->log(CompilerLogger::QUESTION_MARK_PREFIX, $message);
679
680
        if ($config->excludeComposerArtifacts()) {
681
            $box->removeComposerArtifacts(
682
                ComposerConfiguration::retrieveVendorDir(
683
                    $config->getComposerJson(),
684
                ),
685
            );
686
        }
687
    }
688
689
    private static function configureCompressionAlgorithm(
690
        Configuration $config,
691
        Box $box,
692
        bool $dev,
693
        IO $io,
694
        CompilerLogger $logger,
695
    ): void {
696
        $algorithm = $config->getCompressionAlgorithm();
697
698
        if (CompressionAlgorithm::NONE === $algorithm) {
699
            $logger->log(
700
                CompilerLogger::QUESTION_MARK_PREFIX,
701
                'No compression',
702
            );
703
704
            return;
705
        }
706
707
        if ($dev) {
708
            $logger->log(CompilerLogger::QUESTION_MARK_PREFIX, 'Dev mode detected: skipping the compression');
709
710
            return;
711
        }
712
713
        $logger->log(
714
            CompilerLogger::QUESTION_MARK_PREFIX,
715
            sprintf(
716
                'Compressing with the algorithm "<comment>%s</comment>"',
717
                $algorithm->name,
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on KevinGH\Box\Phar\CompressionAlgorithm.
Loading history...
718
            ),
719
        );
720
721
        $restoreLimit = OpenFileDescriptorLimiter::bumpLimit(count($box), $io);
722
723
        try {
724
            $extension = $box->compress($algorithm);
725
726
            if (null !== $extension) {
727
                $logger->log(
728
                    CompilerLogger::CHEVRON_PREFIX,
729
                    sprintf(
730
                        '<info>Warning: the extension "%s" will now be required to execute the PHAR</info>',
731
                        $extension,
732
                    ),
733
                );
734
            }
735
        } catch (RuntimeException $exception) {
736
            $io->error($exception->getMessage());
737
738
            // Continue: the compression failure should not result in completely bailing out the compilation process
739
        } finally {
740
            $restoreLimit();
741
        }
742
    }
743
744
    private static function signPhar(
745
        Configuration $config,
746
        Box $box,
747
        string $path,
748
        IO $io,
749
        CompilerLogger $logger,
750
    ): void {
751
        // Sign using private key when applicable
752
        FS::remove($path.'.pubkey');
753
754
        $key = $config->getPrivateKeyPath();
755
756
        if (null === $key) {
757
            self::signPharWithoutPrivateKey(
758
                $box,
759
                $config->getSigningAlgorithm(),
760
                $config->getTimestamp(),
761
                $logger,
762
            );
763
        } else {
764
            self::signPharWithPrivateKey(
765
                $box,
766
                $key,
767
                $config->getPrivateKeyPassphrase(),
768
                $config->promptForPrivateKey(),
769
                $io,
770
                $logger,
771
            );
772
        }
773
    }
774
775
    private static function signPharWithoutPrivateKey(
776
        Box $box,
777
        SigningAlgorithm $signingAlgorithm,
778
        ?DateTimeImmutable $timestamp,
779
        CompilerLogger $logger,
780
    ): void {
781
        if (null !== $timestamp) {
782
            $logger->log(
783
                CompilerLogger::QUESTION_MARK_PREFIX,
784
                sprintf(
785
                    'Correcting the timestamp to "%s".',
786
                    $timestamp->format(DateTimeInterface::ATOM),
787
                ),
788
            );
789
        }
790
791
        $box->sign($signingAlgorithm, $timestamp);
792
    }
793
794
    private static function signPharWithPrivateKey(
795
        Box $box,
796
        string $key,
797
        ?string $passphrase,
798
        bool $prompt,
799
        IO $io,
800
        CompilerLogger $logger,
801
    ): void {
802
        $logger->log(
803
            CompilerLogger::QUESTION_MARK_PREFIX,
804
            'Signing using a private key',
805
        );
806
        $io->newLine();
807
808
        if ($prompt) {
809
            if (false === $io->isInteractive()) {
810
                throw new RuntimeException(
811
                    sprintf(
812
                        'Accessing to the private key "%s" requires a passphrase but none provided. Either '
813
                        .'provide one or run this command in interactive mode.',
814
                        $key,
815
                    ),
816
                );
817
            }
818
819
            $question = new Question('Private key passphrase');
820
            $question->setHidden(false);
821
            $question->setHiddenFallback(false);
822
823
            $passphrase = $io->askQuestion($question);
824
825
            $io->writeln('');
826
        }
827
828
        $box->signUsingFile($key, $passphrase);
829
    }
830
831
    private static function correctPermissions(string $path, Configuration $config, CompilerLogger $logger): void
832
    {
833
        if (null !== ($chmod = $config->getFileMode())) {
834
            $logger->log(
835
                CompilerLogger::QUESTION_MARK_PREFIX,
836
                sprintf(
837
                    'Setting file permissions to <comment>%s</comment>',
838
                    '0'.decoct($chmod),
839
                ),
840
            );
841
842
            FS::chmod($path, $chmod);
843
        }
844
    }
845
846
    private static function createStub(
847
        Configuration $config,
848
        ?string $main,
849
        bool $checkRequirements,
850
        CompilerLogger $logger,
851
    ): string {
852
        $shebang = $config->getShebang();
853
        $bannerPath = $config->getStubBannerPath();
854
        $bannerContents = $config->getStubBannerContents();
855
856
        if (null !== $shebang) {
857
            $logger->log(
858
                CompilerLogger::MINUS_PREFIX,
859
                sprintf(
860
                    'Using shebang line: %s',
861
                    $shebang,
862
                ),
863
            );
864
        } else {
865
            $logger->log(
866
                CompilerLogger::MINUS_PREFIX,
867
                'No shebang line',
868
            );
869
        }
870
871
        if (null !== $bannerPath) {
872
            $logger->log(
873
                CompilerLogger::MINUS_PREFIX,
874
                sprintf(
875
                    'Using custom banner from file: %s',
876
                    $bannerPath,
877
                ),
878
            );
879
        } elseif (null !== $bannerContents) {
880
            $logger->log(
881
                CompilerLogger::MINUS_PREFIX,
882
                'Using banner:',
883
            );
884
885
            $bannerLines = explode("\n", $bannerContents);
886
887
            foreach ($bannerLines as $bannerLine) {
888
                $logger->log(
889
                    CompilerLogger::CHEVRON_PREFIX,
890
                    $bannerLine,
891
                );
892
            }
893
        }
894
895
        return StubGenerator::generateStub(
896
            $config->getAlias(),
897
            $bannerContents,
898
            $main,
899
            $config->isInterceptFileFuncs(),
900
            $shebang,
901
            $checkRequirements,
902
        );
903
    }
904
905
    private static function logMap(MapFile $fileMapper, CompilerLogger $logger): void
906
    {
907
        $map = $fileMapper->getMap();
908
909
        if (0 === count($map)) {
910
            return;
911
        }
912
913
        $logger->log(
914
            CompilerLogger::QUESTION_MARK_PREFIX,
915
            'Mapping paths',
916
        );
917
918
        foreach ($map as $item) {
919
            foreach ($item as $match => $replace) {
920
                if ('' === $match) {
921
                    $match = '(all)';
922
                    $replace .= '/';
923
                }
924
925
                $logger->log(
926
                    CompilerLogger::MINUS_PREFIX,
927
                    sprintf(
928
                        '%s <info>></info> %s',
929
                        $match,
930
                        $replace,
931
                    ),
932
                );
933
            }
934
        }
935
    }
936
937
    private static function logEndBuilding(
938
        Configuration $config,
939
        CompilerLogger $logger,
940
        IO $io,
941
        Box $box,
942
        string $path,
943
        float $startTime,
944
    ): void {
945
        $logger->log(
946
            CompilerLogger::STAR_PREFIX,
947
            'Done.',
948
        );
949
        $io->newLine();
950
951
        MessageRenderer::render($io, $config->getRecommendations(), $config->getWarnings());
952
953
        $io->comment(
954
            sprintf(
955
                'PHAR: %s (%s)',
956
                $box->count() > 1 ? $box->count().' files' : $box->count().' file',
957
                format_size(
958
                    filesize($path),
959
                ),
960
            )
961
            .PHP_EOL
962
            .'You can inspect the generated PHAR with the "<comment>info</comment>" command.',
963
        );
964
965
        $io->comment(
966
            sprintf(
967
                '<info>Memory usage: %s (peak: %s), time: %s</info>',
968
                format_size(memory_get_usage()),
969
                format_size(memory_get_peak_usage()),
970
                format_time(microtime(true) - $startTime),
971
            ),
972
        );
973
    }
974
975
    private function generateDockerFile(IO $io): int
976
    {
977
        $input = new StringInput('');
978
        $input->setInteractive(false);
979
980
        return $this->getDockerCommand()->execute(
981
            new IO($input, $io->getOutput()),
982
        );
983
    }
984
985
    private function getDockerCommand(): Command
986
    {
987
        return $this->getCommandRegistry()->findCommand(GenerateDockerFile::NAME);
988
    }
989
}
990