Passed
Pull Request — master (#283)
by Théo
02:18
created

Compile   F

Complexity

Total Complexity 80

Size/Duplication

Total Lines 818
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 80
eloc 435
dl 0
loc 818
rs 2
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A registerMainScript() 0 36 3
A createStub() 0 55 5
A execute() 0 45 5
B bumpOpenFileDescriptorLimit() 0 48 8
A registerReplacementValues() 0 25 3
A registerCompactors() 0 35 3
A removeExistingArtifacts() 0 42 4
A commit() 0 10 2
A createPhar() 0 45 3
A renderMessages() 0 22 5
A signPhar() 0 52 5
A registerFileMapping() 0 7 1
A configureMetadata() 0 14 3
A registerRequirementsChecker() 0 29 3
A correctPermissions() 0 12 2
A logMap() 0 26 5
A configure() 0 40 1
A registerStub() 0 44 3
B addFiles() 0 42 6
A configureCompressionAlgorithm() 0 44 5
A logEndBuilding() 0 34 2
A checkComposerFiles() 0 13 3

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\MultiReasonException;
18
use Assert\Assertion;
19
use DateTimeImmutable;
20
use DateTimeZone;
21
use KevinGH\Box\Box;
22
use KevinGH\Box\Compactor;
23
use KevinGH\Box\Composer\ComposerConfiguration;
24
use KevinGH\Box\Configuration;
25
use KevinGH\Box\Console\Logger\CompileLogger;
26
use KevinGH\Box\Console\OutputConfigurator;
27
use KevinGH\Box\MapFile;
28
use KevinGH\Box\PhpSettingsHandler;
29
use KevinGH\Box\RequirementChecker\RequirementsDumper;
30
use KevinGH\Box\StubGenerator;
31
use RuntimeException;
32
use stdClass;
33
use Symfony\Component\Console\Helper\QuestionHelper;
34
use Symfony\Component\Console\Input\InputInterface;
35
use Symfony\Component\Console\Input\InputOption;
36
use Symfony\Component\Console\Logger\ConsoleLogger;
37
use Symfony\Component\Console\Output\OutputInterface;
38
use Symfony\Component\Console\Question\Question;
39
use Symfony\Component\Console\Style\SymfonyStyle;
40
use Symfony\Component\VarDumper\Cloner\VarCloner;
41
use Symfony\Component\VarDumper\Dumper\CliDumper;
42
use const DATE_ATOM;
43
use const KevinGH\Box\BOX_ALLOW_XDEBUG;
44
use const PHP_EOL;
45
use const POSIX_RLIMIT_INFINITY;
46
use const POSIX_RLIMIT_NOFILE;
47
use function array_shift;
48
use function count;
49
use function decoct;
50
use function explode;
51
use function filesize;
52
use function function_exists;
53
use function get_class;
54
use function implode;
55
use function is_string;
56
use function KevinGH\Box\disable_parallel_processing;
57
use function KevinGH\Box\FileSystem\chmod;
58
use function KevinGH\Box\FileSystem\dump_file;
59
use function KevinGH\Box\FileSystem\make_path_relative;
60
use function KevinGH\Box\FileSystem\remove;
61
use function KevinGH\Box\FileSystem\rename;
62
use function KevinGH\Box\format_size;
63
use function KevinGH\Box\get_phar_compression_algorithms;
64
use function posix_setrlimit;
65
use function putenv;
66
use function sprintf;
67
use function strlen;
68
use function substr;
69
70
/**
71
 * @final
72
 * @private
73
 * TODO: make final when Build is removed
74
 */
75
class Compile extends Configurable
76
{
77
    use ChangeableWorkingDirectory;
78
79
    private const HELP = <<<'HELP'
80
The <info>%command.name%</info> command will compile code in a new PHAR based on a variety of settings.
81
<comment>
82
  This command relies on a configuration file for loading
83
  PHAR packaging settings. If a configuration file is not
84
  specified through the <info>--config|-c</info> option, one of
85
  the following files will be used (in order): <info>box.json</info>,
86
  <info>box.json.dist</info>
87
</comment>
88
The configuration file is actually a JSON object saved to a file. For more
89
information check the documentation online:
90
<comment>
91
  https://github.com/humbug/box
92
</comment>
93
HELP;
94
95
    private const DEBUG_OPTION = 'debug';
96
    private const NO_PARALLEL_PROCESSING_OPTION = 'no-parallel';
97
    private const NO_RESTART_OPTION = 'no-restart';
98
    private const DEV_OPTION = 'dev';
99
    private const NO_CONFIG_OPTION = 'no-config';
100
101
    private const DEBUG_DIR = '.box_dump';
102
103
    /**
104
     * {@inheritdoc}
105
     */
106
    protected function configure(): void
107
    {
108
        parent::configure();
109
110
        $this->setName('compile');
111
        $this->setDescription('Compile an application into a PHAR');
112
        $this->setHelp(self::HELP);
113
114
        $this->addOption(
115
            self::DEBUG_OPTION,
116
            null,
117
            InputOption::VALUE_NONE,
118
            'Dump the files added to the PHAR in a `'.self::DEBUG_DIR.'` directory'
119
        );
120
        $this->addOption(
121
            self::NO_PARALLEL_PROCESSING_OPTION,
122
            null,
123
            InputOption::VALUE_NONE,
124
            'Disable the parallel processing'
125
        );
126
        $this->addOption(
127
            self::NO_RESTART_OPTION,
128
            null,
129
            InputOption::VALUE_NONE,
130
            'Do not restart the PHP process. Box restarts the process by default to disable xdebug and set `phar.readonly=0`'
131
        );
132
        $this->addOption(
133
            self::DEV_OPTION,
134
            null,
135
            InputOption::VALUE_NONE,
136
            'Skips the compression step'
137
        );
138
        $this->addOption(
139
            self::NO_CONFIG_OPTION,
140
            null,
141
            InputOption::VALUE_NONE,
142
            'Ignore the config file even when one is specified with the --config option'
143
        );
144
145
        $this->configureWorkingDirOption();
146
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151
    protected function execute(InputInterface $input, OutputInterface $output): void
152
    {
153
        $io = new SymfonyStyle($input, $output);
154
155
        OutputConfigurator::configure($output);
156
157
        if ($input->getOption(self::NO_RESTART_OPTION)) {
158
            putenv(BOX_ALLOW_XDEBUG.'=1');
159
        }
160
161
        if ($debug = $input->getOption(self::DEBUG_OPTION)) {
162
            $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);
163
        }
164
165
        (new PhpSettingsHandler(new ConsoleLogger($output)))->check();
166
167
        if ($input->getOption(self::NO_PARALLEL_PROCESSING_OPTION)) {
168
            disable_parallel_processing();
169
            $io->writeln('<info>[debug] Disabled parallel processing</info>', OutputInterface::VERBOSITY_DEBUG);
170
        }
171
172
        $this->changeWorkingDirectory($input);
173
174
        $io->writeln($this->getApplication()->getHelp());
175
        $io->newLine();
176
177
        $config = $input->getOption(self::NO_CONFIG_OPTION)
178
            ? Configuration::create(null, new stdClass())
179
            : $this->getConfig($input, $output, true)
180
        ;
181
        $path = $config->getOutputPath();
182
183
        $logger = new CompileLogger($io);
184
185
        $startTime = microtime(true);
186
187
        $this->removeExistingArtifacts($config, $logger, $debug);
188
189
        $logger->logStartBuilding($path);
190
191
        $box = $this->createPhar($config, $input, $output, $logger, $io, $debug);
192
193
        $this->correctPermissions($path, $config, $logger);
194
195
        $this->logEndBuilding($config, $logger, $io, $box, $path, $startTime);
196
    }
197
198
    private function createPhar(
199
        Configuration $config,
200
        InputInterface $input,
201
        OutputInterface $output,
202
        CompileLogger $logger,
203
        SymfonyStyle $io,
204
        bool $debug
205
    ): Box {
206
        $box = Box::create(
207
            $config->getTmpOutputPath()
208
        );
209
        $box->startBuffering();
210
211
        $this->registerReplacementValues($config, $box, $logger);
212
        $this->registerCompactors($config, $box, $logger);
213
        $this->registerFileMapping($config, $box, $logger);
214
215
        // Registering the main script _before_ adding the rest if of the files is _very_ important. The temporary
216
        // file used for debugging purposes and the Composer dump autoloading will not work correctly otherwise.
217
        $main = $this->registerMainScript($config, $box, $logger);
218
219
        $check = $this->registerRequirementsChecker($config, $box, $logger);
220
221
        $this->addFiles($config, $box, $logger, $io);
222
223
        $this->registerStub($config, $box, $main, $check, $logger);
224
        $this->configureMetadata($config, $box, $logger);
225
226
        $this->commit($box, $config, $logger);
227
228
        $this->checkComposerFiles($box, $config, $logger);
229
230
        $this->configureCompressionAlgorithm($config, $box, $input->getOption(self::DEV_OPTION), $io, $logger);
231
232
        if ($debug) {
233
            $box->getPhar()->extractTo(self::DEBUG_DIR, null, true);
234
        }
235
236
        $this->signPhar($config, $box, $config->getTmpOutputPath(), $input, $output, $logger);
237
238
        if ($config->getTmpOutputPath() !== $config->getOutputPath()) {
239
            rename($config->getTmpOutputPath(), $config->getOutputPath());
240
        }
241
242
        return $box;
243
    }
244
245
    private function removeExistingArtifacts(Configuration $config, CompileLogger $logger, bool $debug): void
246
    {
247
        $path = $config->getOutputPath();
248
249
        if ($debug) {
250
            remove(self::DEBUG_DIR);
251
252
            $date = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format(DATE_ATOM);
253
            $file = null !== $config->getConfigurationFile() ? $config->getConfigurationFile() : 'No config file';
254
255
            remove(self::DEBUG_DIR);
256
257
            dump_file(
258
                self::DEBUG_DIR.'/.box_configuration',
259
                <<<EOF
260
//
261
// Processed content of the configuration file "$file" dumped for debugging purposes
262
// Time: $date
263
//
264
265
266
EOF
267
                .(new CliDumper())->dump(
268
                    (new VarCloner())->cloneVar($config),
269
                    true
270
                )
271
            );
272
        }
273
274
        if (false === file_exists($path)) {
275
            return;
276
        }
277
278
        $logger->log(
279
            CompileLogger::QUESTION_MARK_PREFIX,
280
            sprintf(
281
                'Removing the existing PHAR "%s"',
282
                $path
283
            )
284
        );
285
286
        remove($path);
287
    }
288
289
    private function registerReplacementValues(Configuration $config, Box $box, CompileLogger $logger): void
290
    {
291
        $values = $config->getReplacements();
292
293
        if ([] === $values) {
294
            return;
295
        }
296
297
        $logger->log(
298
            CompileLogger::QUESTION_MARK_PREFIX,
299
            'Setting replacement values'
300
        );
301
302
        foreach ($values as $key => $value) {
303
            $logger->log(
304
                CompileLogger::PLUS_PREFIX,
305
                sprintf(
306
                    '%s: %s',
307
                    $key,
308
                    $value
309
                )
310
            );
311
        }
312
313
        $box->registerPlaceholders($values);
314
    }
315
316
    private function registerCompactors(Configuration $config, Box $box, CompileLogger $logger): void
317
    {
318
        $compactors = $config->getCompactors();
319
320
        if ([] === $compactors) {
321
            $logger->log(
322
                CompileLogger::QUESTION_MARK_PREFIX,
323
                'No compactor to register'
324
            );
325
326
            return;
327
        }
328
329
        $logger->log(
330
            CompileLogger::QUESTION_MARK_PREFIX,
331
            'Registering compactors'
332
        );
333
334
        $logCompactors = function (Compactor $compactor) use ($logger): void {
335
            $compactorClassParts = explode('\\', get_class($compactor));
336
337
            if ('_HumbugBox' === substr($compactorClassParts[0], 0, strlen('_HumbugBox'))) {
338
                // Keep the non prefixed class name for the user
339
                array_shift($compactorClassParts);
340
            }
341
342
            $logger->log(
343
                CompileLogger::PLUS_PREFIX,
344
                implode('\\', $compactorClassParts)
345
            );
346
        };
347
348
        array_map($logCompactors, $compactors);
349
350
        $box->registerCompactors($compactors);
351
    }
352
353
    private function registerFileMapping(Configuration $config, Box $box, CompileLogger $logger): void
354
    {
355
        $fileMapper = $config->getFileMapper();
356
357
        $this->logMap($fileMapper, $logger);
358
359
        $box->registerFileMapping($fileMapper);
360
    }
361
362
    private function addFiles(Configuration $config, Box $box, CompileLogger $logger, SymfonyStyle $io): void
363
    {
364
        $logger->log(CompileLogger::QUESTION_MARK_PREFIX, 'Adding binary files');
365
366
        $count = count($config->getBinaryFiles());
367
368
        $box->addFiles($config->getBinaryFiles(), true);
369
370
        $logger->log(
371
            CompileLogger::CHEVRON_PREFIX,
372
            0 === $count
373
                ? 'No file found'
374
                : sprintf('%d file(s)', $count)
375
        );
376
377
        $logger->log(
378
            CompileLogger::QUESTION_MARK_PREFIX,
379
            sprintf(
380
                'Auto-discover files? %s',
381
                $config->hasAutodiscoveredFiles() ? 'Yes' : 'No'
382
            )
383
        );
384
        $logger->log(CompileLogger::QUESTION_MARK_PREFIX, 'Adding files');
385
386
        $count = count($config->getFiles());
387
388
        try {
389
            $box->addFiles($config->getFiles(), false);
390
        } catch (MultiReasonException $exception) {
391
            // This exception is handled a different way to give me meaningful feedback to the user
392
            foreach ($exception->getReasons() as $reason) {
393
                $io->error($reason);
394
            }
395
396
            throw $exception;
397
        }
398
399
        $logger->log(
400
            CompileLogger::CHEVRON_PREFIX,
401
            0 === $count
402
                ? 'No file found'
403
                : sprintf('%d file(s)', $count)
404
        );
405
    }
406
407
    private function registerMainScript(Configuration $config, Box $box, CompileLogger $logger): ?string
408
    {
409
        if (false === $config->hasMainScript()) {
410
            $logger->log(
411
                CompileLogger::QUESTION_MARK_PREFIX,
412
                'No main script path configured'
413
            );
414
415
            return null;
416
        }
417
418
        $main = $config->getMainScriptPath();
419
420
        $logger->log(
421
            CompileLogger::QUESTION_MARK_PREFIX,
422
            sprintf(
423
                'Adding main file: %s',
424
                $main
425
            )
426
        );
427
428
        $localMain = $box->addFile(
429
            $main,
430
            $config->getMainScriptContents()
431
        );
432
433
        $relativeMain = make_path_relative($main, $config->getBasePath());
434
435
        if ($localMain !== $relativeMain) {
436
            $logger->log(
437
                CompileLogger::CHEVRON_PREFIX,
438
                $localMain
439
            );
440
        }
441
442
        return $localMain;
443
    }
444
445
    private function registerRequirementsChecker(Configuration $config, Box $box, CompileLogger $logger): bool
446
    {
447
        if (false === $config->checkRequirements()) {
448
            $logger->log(
449
                CompileLogger::QUESTION_MARK_PREFIX,
450
                'Skip requirements checker'
451
            );
452
453
            return false;
454
        }
455
456
        $logger->log(
457
            CompileLogger::QUESTION_MARK_PREFIX,
458
            'Adding requirements checker'
459
        );
460
461
        $checkFiles = RequirementsDumper::dump(
462
            $config->getDecodedComposerJsonContents() ?? [],
463
            $config->getDecodedComposerLockContents() ?? [],
464
            $config->getCompressionAlgorithm()
465
        );
466
467
        foreach ($checkFiles as $fileWithContents) {
468
            [$file, $contents] = $fileWithContents;
469
470
            $box->addFile('.box/'.$file, $contents, true);
471
        }
472
473
        return true;
474
    }
475
476
    private function registerStub(Configuration $config, Box $box, ?string $main, bool $checkRequirements, CompileLogger $logger): void
477
    {
478
        if ($config->isStubGenerated()) {
479
            $logger->log(
480
                CompileLogger::QUESTION_MARK_PREFIX,
481
                'Generating new stub'
482
            );
483
484
            $stub = $this->createStub($config, $main, $checkRequirements, $logger);
485
486
            $box->getPhar()->setStub($stub);
487
488
            return;
489
        }
490
        if (null !== ($stub = $config->getStubPath())) {
491
            $logger->log(
492
                CompileLogger::QUESTION_MARK_PREFIX,
493
                sprintf(
494
                    'Using stub file: %s',
495
                    $stub
496
                )
497
            );
498
499
            $box->registerStub($stub);
500
501
            return;
502
        }
503
504
        // TODO: add warning that the check requirements could not be added
505
        $aliasWasAdded = $box->getPhar()->setAlias($config->getAlias());
506
507
        Assertion::true(
508
            $aliasWasAdded,
509
            sprintf(
510
                'The alias "%s" is invalid. See Phar::setAlias() documentation for more information.',
511
                $config->getAlias()
512
            )
513
        );
514
515
        $box->getPhar()->setDefaultStub($main);
516
517
        $logger->log(
518
            CompileLogger::QUESTION_MARK_PREFIX,
519
            'Using default stub'
520
        );
521
    }
522
523
    private function configureMetadata(Configuration $config, Box $box, CompileLogger $logger): void
524
    {
525
        if (null !== ($metadata = $config->getMetadata())) {
526
            $logger->log(
527
                CompileLogger::QUESTION_MARK_PREFIX,
528
                'Setting metadata'
529
            );
530
531
            $logger->log(
532
                CompileLogger::MINUS_PREFIX,
533
                is_string($metadata) ? $metadata : var_export($metadata, true)
534
            );
535
536
            $box->getPhar()->setMetadata($metadata);
537
        }
538
    }
539
540
    private function commit(Box $box, Configuration $config, CompileLogger $logger): void
541
    {
542
        $message = $config->dumpAutoload()
543
            ? 'Dumping the Composer autoloader'
544
            : 'Skipping dumping the Composer autoloader'
545
        ;
546
547
        $logger->log(CompileLogger::QUESTION_MARK_PREFIX, $message);
548
549
        $box->endBuffering($config->dumpAutoload());
550
    }
551
552
    private function checkComposerFiles(Box $box, Configuration $config, CompileLogger $logger): void
553
    {
554
        $message = $config->excludeComposerFiles()
555
            ? 'Removing the Composer dump artefacts'
556
            : 'Keep the Composer dump artefacts'
557
        ;
558
559
        $logger->log(CompileLogger::QUESTION_MARK_PREFIX, $message);
560
561
        if ($config->excludeComposerFiles()) {
562
            $box->removeComposerArtefacts(
563
                ComposerConfiguration::retrieveVendorDir(
564
                    $config->getDecodedComposerJsonContents() ?? []
565
                )
566
            );
567
        }
568
    }
569
570
    private function configureCompressionAlgorithm(Configuration $config, Box $box, bool $dev, SymfonyStyle $io, CompileLogger $logger): void
571
    {
572
        if (null === ($algorithm = $config->getCompressionAlgorithm())) {
573
            $logger->log(
574
                CompileLogger::QUESTION_MARK_PREFIX,
575
                'No compression'
576
            );
577
578
            return;
579
        }
580
581
        $logger->log(
582
            CompileLogger::QUESTION_MARK_PREFIX,
583
            sprintf(
584
                'Compressing with the algorithm "<comment>%s</comment>"',
585
                array_search($algorithm, get_phar_compression_algorithms(), true)
586
            )
587
        );
588
589
        if ($dev) {
590
            // Skips the compression step in dev mode
591
            return;
592
        }
593
594
        $restoreLimit = self::bumpOpenFileDescriptorLimit($box, $io);
595
596
        try {
597
            $extension = $box->compress($algorithm);
598
599
            if (null !== $extension) {
600
                $logger->log(
601
                    CompileLogger::CHEVRON_PREFIX,
602
                    sprintf(
603
                        '<info>Warning: the extension "%s" will now be required to execute the PHAR</info>',
604
                        $extension
605
                    )
606
                );
607
            }
608
        } catch (RuntimeException $exception) {
609
            $io->error($exception->getMessage());
610
611
            // Continue: the compression failure should not result in completely bailing out the compilation process
612
        } finally {
613
            $restoreLimit();
614
        }
615
    }
616
617
    /**
618
     * Bumps the maximum number of open file descriptor if necessary.
619
     *
620
     * @return callable callable to call to restore the original maximum number of open files descriptors
621
     */
622
    private static function bumpOpenFileDescriptorLimit(Box $box, SymfonyStyle $io): callable
623
    {
624
        $filesCount = count($box) + 128;  // Add a little extra for good measure
625
626
        if (false === function_exists('posix_getrlimit') || false === function_exists('posix_setrlimit')) {
627
            $io->writeln(
628
                '<info>[debug] Could not check the maximum number of open file descriptors: the functions "posix_getrlimit()" and '
629
                .'"posix_setrlimit" could not be found.</info>',
630
                OutputInterface::VERBOSITY_DEBUG
631
            );
632
633
            return function (): void {};
634
        }
635
636
        $softLimit = posix_getrlimit()['soft openfiles'];
637
        $hardLimit = posix_getrlimit()['hard openfiles'];
638
639
        if ($softLimit < $filesCount) {
640
            $io->writeln(
641
                sprintf(
642
                    '<info>[debug] Increased the maximum number of open file descriptors from ("%s", "%s") to ("%s", "%s")'
643
                    .'</info>',
644
                    $softLimit,
645
                    $hardLimit,
646
                    $filesCount,
647
                    'unlimited'
648
                ),
649
                OutputInterface::VERBOSITY_DEBUG
650
            );
651
652
            posix_setrlimit(
653
                POSIX_RLIMIT_NOFILE,
654
                $filesCount,
655
                'unlimited' === $hardLimit ? POSIX_RLIMIT_INFINITY : $hardLimit
656
            );
657
        }
658
659
        return function () use ($io, $softLimit, $hardLimit): void {
660
            if (function_exists('posix_setrlimit') && isset($softLimit, $hardLimit)) {
661
                posix_setrlimit(
662
                    POSIX_RLIMIT_NOFILE,
663
                    $softLimit,
664
                    'unlimited' === $hardLimit ? POSIX_RLIMIT_INFINITY : $hardLimit
665
                );
666
667
                $io->writeln(
668
                    '<info>[debug] Restored the maximum number of open file descriptors</info>',
669
                    OutputInterface::VERBOSITY_DEBUG
670
                );
671
            }
672
        };
673
    }
674
675
    private function signPhar(
676
        Configuration $config,
677
        Box $box,
678
        string $path,
679
        InputInterface $input,
680
        OutputInterface $output,
681
        CompileLogger $logger
682
    ): void {
683
        // Sign using private key when applicable
684
        remove($path.'.pubkey');
685
686
        $key = $config->getPrivateKeyPath();
687
688
        if (null === $key) {
689
            if (null !== ($algorithm = $config->getSigningAlgorithm())) {
0 ignored issues
show
introduced by
The condition null !== $algorithm = $c...->getSigningAlgorithm() is always true.
Loading history...
690
                $box->getPhar()->setSignatureAlgorithm($algorithm);
691
            }
692
693
            return;
694
        }
695
696
        $logger->log(
697
            CompileLogger::QUESTION_MARK_PREFIX,
698
            'Signing using a private key'
699
        );
700
701
        $passphrase = $config->getPrivateKeyPassphrase();
702
703
        if ($config->promptForPrivateKey()) {
704
            if (false === $input->isInteractive()) {
705
                throw new RuntimeException(
706
                    sprintf(
707
                        'Accessing to the private key "%s" requires a passphrase but none provided. Either '
708
                        .'provide one or run this command in interactive mode.',
709
                        $key
710
                    )
711
                );
712
            }
713
714
            /** @var $dialog QuestionHelper */
715
            $dialog = $this->getHelper('question');
716
717
            $question = new Question('Private key passphrase:');
718
            $question->setHidden(false);
719
            $question->setHiddenFallback(false);
720
721
            $passphrase = $dialog->ask($input, $output, $question);
722
723
            $output->writeln('');
724
        }
725
726
        $box->signUsingFile($key, $passphrase);
727
    }
728
729
    private function correctPermissions(string $path, Configuration $config, CompileLogger $logger): void
730
    {
731
        if (null !== ($chmod = $config->getFileMode())) {
732
            $logger->log(
733
                CompileLogger::QUESTION_MARK_PREFIX,
734
                sprintf(
735
                    'Setting file permissions to <comment>%s</comment>',
736
                    '0'.decoct($chmod)
737
                )
738
            );
739
740
            chmod($path, $chmod);
741
        }
742
    }
743
744
    private function createStub(Configuration $config, ?string $main, bool $checkRequirements, CompileLogger $logger): string
745
    {
746
        $stub = StubGenerator::create()
747
            ->alias($config->getAlias())
748
            ->index($main)
749
            ->intercept($config->isInterceptFileFuncs())
750
            ->checkRequirements($checkRequirements)
751
        ;
752
753
        if (null !== ($shebang = $config->getShebang())) {
754
            $logger->log(
755
                CompileLogger::MINUS_PREFIX,
756
                sprintf(
757
                    'Using shebang line: %s',
758
                    $shebang
759
                )
760
            );
761
762
            $stub->shebang($shebang);
763
        } else {
764
            $logger->log(
765
                CompileLogger::MINUS_PREFIX,
766
                'No shebang line'
767
            );
768
        }
769
770
        if (null !== ($bannerPath = $config->getStubBannerPath())) {
771
            $logger->log(
772
                CompileLogger::MINUS_PREFIX,
773
                sprintf(
774
                    'Using custom banner from file: %s',
775
                    $bannerPath
776
                )
777
            );
778
779
            $stub->banner($config->getStubBannerContents());
780
        } elseif (null !== ($banner = $config->getStubBannerContents())) {
781
            $logger->log(
782
                CompileLogger::MINUS_PREFIX,
783
                'Using banner:'
784
            );
785
786
            $bannerLines = explode("\n", $banner);
787
788
            foreach ($bannerLines as $bannerLine) {
789
                $logger->log(
790
                    CompileLogger::CHEVRON_PREFIX,
791
                    $bannerLine
792
                );
793
            }
794
795
            $stub->banner($banner);
796
        }
797
798
        return $stub->generate();
799
    }
800
801
    private function logMap(MapFile $fileMapper, CompileLogger $logger): void
802
    {
803
        $map = $fileMapper->getMap();
804
805
        if ([] === $map) {
806
            return;
807
        }
808
809
        $logger->log(
810
            CompileLogger::QUESTION_MARK_PREFIX,
811
            'Mapping paths'
812
        );
813
814
        foreach ($map as $item) {
815
            foreach ($item as $match => $replace) {
816
                if ('' === $match) {
817
                    $match = '(all)';
818
                    $replace .= '/';
819
                }
820
821
                $logger->log(
822
                    CompileLogger::MINUS_PREFIX,
823
                    sprintf(
824
                        '%s <info>></info> %s',
825
                        $match,
826
                        $replace
827
                    )
828
                );
829
            }
830
        }
831
    }
832
833
    private function logEndBuilding(
834
        Configuration $config,
835
        CompileLogger $logger,
836
        SymfonyStyle $io,
837
        Box $box,
838
        string $path,
839
        float $startTime
840
    ): void {
841
        $logger->log(
842
            CompileLogger::STAR_PREFIX,
843
            'Done.'
844
        );
845
        $io->newLine();
846
847
        $this->renderMessages($config, $io);
848
849
        $io->comment(
850
            sprintf(
851
                'PHAR: %s (%s)',
852
                $box->count() > 1 ? $box->count().' files' : $box->count().' file',
853
                format_size(
854
                    filesize($path)
855
                )
856
            )
857
            .PHP_EOL
858
            .'You can inspect the generated PHAR with the "<comment>info</comment>" command.'
859
        );
860
861
        $io->comment(
862
            sprintf(
863
                '<info>Memory usage: %.2fMB (peak: %.2fMB), time: %.2fs<info>',
864
                round(memory_get_usage() / 1024 / 1024, 2),
865
                round(memory_get_peak_usage() / 1024 / 1024, 2),
866
                round(microtime(true) - $startTime, 2)
867
            )
868
        );
869
    }
870
871
    private function renderMessages(Configuration $config, SymfonyStyle $io): void
872
    {
873
        $recommendations = $config->getRecommendations();
874
        $warnings = $config->getWarnings();
875
876
        if ([] === $recommendations) {
877
            $io->writeln('No recommendation found.');
878
        } else {
879
            $io->writeln('Recommendations:');
880
881
            foreach ($recommendations as $recommendation) {
882
                $io->writeln("    - <recommendation>$recommendation</recommendation>");
883
            }
884
        }
885
886
        if ([] === $warnings) {
887
            $io->writeln('No warning found.');
888
        } else {
889
            $io->writeln('Warnings:');
890
891
            foreach ($warnings as $warning) {
892
                $io->writeln("    - <warning>$warning</warning>");
893
            }
894
        }
895
    }
896
}
897