Passed
Pull Request — master (#279)
by Théo
02:32
created

Compile   F

Complexity

Total Complexity 75

Size/Duplication

Total Lines 779
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 75
eloc 418
dl 0
loc 779
rs 2.4
c 0
b 0
f 0

21 Methods

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