Passed
Push — master ( 4e89cd...0be633 )
by Théo
04:00
created

Compile::logEndBuilding()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 23
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 23
rs 9.0856
c 0
b 0
f 0
cc 2
eloc 16
nc 1
nop 5
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\Configuration;
24
use KevinGH\Box\Console\Logger\BuildLogger;
25
use KevinGH\Box\MapFile;
26
use KevinGH\Box\PhpSettingsHandler;
27
use KevinGH\Box\RequirementChecker\RequirementsDumper;
28
use KevinGH\Box\StubGenerator;
29
use const PHP_EOL;
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 POSIX_RLIMIT_INFINITY;
43
use const POSIX_RLIMIT_NOFILE;
44
use function array_shift;
45
use function count;
46
use function decoct;
47
use function explode;
48
use function function_exists;
49
use function get_class;
50
use function implode;
51
use function ini_get;
52
use function KevinGH\Box\disable_parallel_processing;
53
use function KevinGH\Box\FileSystem\chmod;
54
use function KevinGH\Box\FileSystem\dump_file;
55
use function KevinGH\Box\FileSystem\make_path_relative;
56
use function KevinGH\Box\FileSystem\remove;
57
use function KevinGH\Box\FileSystem\rename;
58
use function KevinGH\Box\formatted_filesize;
59
use function KevinGH\Box\get_phar_compression_algorithms;
60
use function KevinGH\Box\memory_to_bytes;
61
use function posix_setrlimit;
62
use function putenv;
63
use function sprintf;
64
use function strlen;
65
use function substr;
66
use function trim;
67
68
/**
69
 * @final
70
 * @private
71
 * TODO: make final when Build is removed
72
 */
73
class Compile extends Configurable
74
{
75
    use ChangeableWorkingDirectory;
76
77
    private const HELP = <<<'HELP'
78
The <info>%command.name%</info> command will compile code in a new PHAR based on a variety of settings.
79
<comment>
80
  This command relies on a configuration file for loading
81
  PHAR packaging settings. If a configuration file is not
82
  specified through the <info>--config|-c</info> option, one of
83
  the following files will be used (in order): <info>box.json</info>,
84
  <info>box.json.dist</info>
85
</comment>
86
The configuration file is actually a JSON object saved to a file. For more
87
information check the documentation online:
88
<comment>
89
  https://github.com/humbug/box
90
</comment>
91
HELP;
92
93
    private const DEBUG_OPTION = 'debug';
94
    private const NO_PARALLEL_PROCESSING_OPTION = 'no-parallel';
95
    private const NO_RESTART_OPTION = 'no-restart';
96
    private const DEV_OPTION = 'dev';
97
    private const NO_CONFIG_OPTION = 'no-config';
98
99
    private const DEBUG_DIR = '.box_dump';
100
101
    /**
102
     * {@inheritdoc}
103
     */
104
    protected function configure(): void
105
    {
106
        parent::configure();
107
108
        $this->setName('compile');
109
        $this->setDescription('Compile an application into a PHAR');
110
        $this->setHelp(self::HELP);
111
112
        $this->addOption(
113
            self::DEBUG_OPTION,
114
            null,
115
            InputOption::VALUE_NONE,
116
            'Dump the files added to the PHAR in a `'.self::DEBUG_DIR.'` directory'
117
        );
118
        $this->addOption(
119
            self::NO_PARALLEL_PROCESSING_OPTION,
120
            null,
121
            InputOption::VALUE_NONE,
122
            'Disable the parallel processing'
123
        );
124
        $this->addOption(
125
            self::NO_RESTART_OPTION,
126
            null,
127
            InputOption::VALUE_NONE,
128
            'Do not restart the PHP process. Box restarts the process by default to disable xdebug and set `phar.readonly=0`'
129
        );
130
        $this->addOption(
131
            self::DEV_OPTION,
132
            null,
133
            InputOption::VALUE_NONE,
134
            'Skips the compression step'
135
        );
136
        $this->addOption(
137
            self::NO_CONFIG_OPTION,
138
            null,
139
            InputOption::VALUE_NONE,
140
            'Ignore the config file even when one is specified with the --config option'
141
        );
142
143
        $this->configureWorkingDirOption();
144
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149
    protected function execute(InputInterface $input, OutputInterface $output): void
150
    {
151
        $io = new SymfonyStyle($input, $output);
152
153
        if ($input->getOption(self::NO_RESTART_OPTION)) {
154
            putenv('BOX_ALLOW_XDEBUG=1');
155
        }
156
157
        if ($debug = $input->getOption(self::DEBUG_OPTION)) {
158
            $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);
159
        }
160
161
        self::checkPhpSettings($io, $output);
162
163
        if ($input->getOption(self::NO_PARALLEL_PROCESSING_OPTION)) {
164
            disable_parallel_processing();
165
            $io->writeln('<info>[debug] Disabled parallel processing</info>', OutputInterface::VERBOSITY_DEBUG);
166
        }
167
168
        $this->changeWorkingDirectory($input);
169
170
        $io->writeln($this->getApplication()->getHelp());
171
        $io->writeln('');
172
173
        $config = $input->getOption(self::NO_CONFIG_OPTION)
174
            ? Configuration::create(null, new stdClass())
175
            : $this->getConfig($input, $output, true)
176
        ;
177
        $path = $config->getOutputPath();
178
179
        $logger = new BuildLogger($io);
180
181
        $startTime = microtime(true);
182
183
        $this->removeExistingArtifacts($config, $logger, $debug);
184
185
        $logger->logStartBuilding($path);
186
187
        $box = $this->createPhar($config, $input, $output, $logger, $io, $debug);
188
189
        $this->correctPermissions($path, $config, $logger);
190
191
        $this->logEndBuilding($logger, $io, $box, $path, $startTime);
192
    }
193
194
    /**
195
     * Taken from Composer.
196
     *
197
     * @see https://github.com/composer/composer/blob/34c371f5f23e25eb9aa54ccc65136cf50930612e/bin/composer#L20-L50
198
     */
199
    private static function checkPhpSettings(SymfonyStyle $io, OutputInterface $output): void
200
    {
201
        (new PhpSettingsHandler(new ConsoleLogger($output)))->check();
202
203
        if (function_exists('ini_set')) {
204
            $memoryLimit = trim(ini_get('memory_limit'));
205
206
            // Increase memory_limit if it is lower than 500MB
207
            if ('-1' !== $memoryLimit && memory_to_bytes($memoryLimit) < 1024 * 1024 * 512 && false === getenv('BOX_MEMORY_LIMIT')) {
208
                @ini_set('memory_limit', '512M');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for ini_set(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

208
                /** @scrutinizer ignore-unhandled */ @ini_set('memory_limit', '512M');

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
209
210
                $io->writeln(
211
                    sprintf(
212
                        '<info>[debug] Bumped the memory limit from "%s" to "%s".</info>',
213
                        $memoryLimit,
214
                        '512M'
215
                    ),
216
                    OutputInterface::VERBOSITY_DEBUG
217
                );
218
            }
219
220
            // Set user defined memory limit
221
            if ($newMemoryLimit = getenv('BOX_MEMORY_LIMIT')) {
222
                @ini_set('memory_limit', $newMemoryLimit);
223
224
                $io->writeln(
225
                    sprintf(
226
                        '<info>[debug] Bumped the memory limit from "%s" to BOX_MEMORY_LIMIT="%s".</info>',
227
                        $memoryLimit,
228
                        $newMemoryLimit
229
                    ),
230
                    OutputInterface::VERBOSITY_DEBUG
231
                );
232
            }
233
234
            // Note that there is no need to restore since those changes will be applied only to the current process which will die once
235
            // the compilation is done.
236
        }
237
    }
238
239
    private function createPhar(
240
        Configuration $config,
241
        InputInterface $input,
242
        OutputInterface $output,
243
        BuildLogger $logger,
244
        SymfonyStyle $io,
245
        bool $debug
246
    ): Box {
247
        $box = Box::create(
248
            $config->getTmpOutputPath()
249
        );
250
        $box->startBuffering();
251
252
        $this->registerReplacementValues($config, $box, $logger);
253
        $this->registerCompactors($config, $box, $logger);
254
        $this->registerFileMapping($config, $box, $logger);
255
256
        // Registering the main script _before_ adding the rest if of the files is _very_ important. The temporary
257
        // file used for debugging purposes and the Composer dump autoloading will not work correctly otherwise.
258
        $main = $this->registerMainScript($config, $box, $logger);
259
260
        $check = $this->registerRequirementsChecker($config, $box, $logger);
261
262
        $this->addFiles($config, $box, $logger, $io);
263
264
        $this->registerStub($config, $box, $main, $check, $logger);
265
        $this->configureMetadata($config, $box, $logger);
266
267
        $box->endBuffering($config->dumpAutoload());
268
269
        $this->configureCompressionAlgorithm($config, $box, $input->getOption(self::DEV_OPTION), $io, $logger);
270
271
        if ($debug) {
272
            $box->getPhar()->extractTo(self::DEBUG_DIR, null, true);
273
        }
274
275
        $this->signPhar($config, $box, $config->getTmpOutputPath(), $input, $output, $logger);
276
277
        if ($config->getTmpOutputPath() !== $config->getOutputPath()) {
278
            rename($config->getTmpOutputPath(), $config->getOutputPath());
279
        }
280
281
        return $box;
282
    }
283
284
    private function removeExistingArtifacts(Configuration $config, BuildLogger $logger, bool $debug): void
285
    {
286
        $path = $config->getOutputPath();
287
288
        if ($debug) {
289
            remove(self::DEBUG_DIR);
290
291
            $date = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format(DATE_ATOM);
292
            $file = null !== $config->getFile() ? $config->getFile() : 'No config file';
293
294
            remove(self::DEBUG_DIR);
295
296
            dump_file(
297
                self::DEBUG_DIR.'/.box_configuration',
298
                <<<EOF
299
//
300
// Processed content of the configuration file "$file" dumped for debugging purposes
301
// Time: $date
302
//
303
304
305
EOF
306
                .(new CliDumper())->dump(
307
                    (new VarCloner())->cloneVar($config),
308
                    true
309
                )
310
            );
311
        }
312
313
        if (false === file_exists($path)) {
314
            return;
315
        }
316
317
        $logger->log(
318
            BuildLogger::QUESTION_MARK_PREFIX,
319
            sprintf(
320
                'Removing the existing PHAR "%s"',
321
                $path
322
            )
323
        );
324
325
        remove($path);
326
    }
327
328
    private function registerReplacementValues(Configuration $config, Box $box, BuildLogger $logger): void
329
    {
330
        $values = $config->getProcessedReplacements();
331
332
        if ([] === $values) {
333
            return;
334
        }
335
336
        $logger->log(
337
            BuildLogger::QUESTION_MARK_PREFIX,
338
            'Setting replacement values'
339
        );
340
341
        foreach ($values as $key => $value) {
342
            $logger->log(
343
                BuildLogger::PLUS_PREFIX,
344
                sprintf(
345
                    '%s: %s',
346
                    $key,
347
                    $value
348
                )
349
            );
350
        }
351
352
        $box->registerPlaceholders($values);
353
    }
354
355
    private function registerCompactors(Configuration $config, Box $box, BuildLogger $logger): void
356
    {
357
        $compactors = $config->getCompactors();
358
359
        if ([] === $compactors) {
360
            $logger->log(
361
                BuildLogger::QUESTION_MARK_PREFIX,
362
                'No compactor to register'
363
            );
364
365
            return;
366
        }
367
368
        $logger->log(
369
            BuildLogger::QUESTION_MARK_PREFIX,
370
            'Registering compactors'
371
        );
372
373
        $logCompactors = function (Compactor $compactor) use ($logger): void {
374
            $compactorClassParts = explode('\\', get_class($compactor));
375
376
            if ('_HumbugBox' === substr($compactorClassParts[0], 0, strlen('_HumbugBox'))) {
377
                // Keep the non prefixed class name for the user
378
                array_shift($compactorClassParts);
379
            }
380
381
            $logger->log(
382
                BuildLogger::PLUS_PREFIX,
383
                implode('\\', $compactorClassParts)
384
            );
385
        };
386
387
        array_map($logCompactors, $compactors);
388
389
        $box->registerCompactors($compactors);
390
    }
391
392
    private function registerFileMapping(Configuration $config, Box $box, BuildLogger $logger): void
393
    {
394
        $fileMapper = $config->getFileMapper();
395
396
        $this->logMap($fileMapper, $logger);
397
398
        $box->registerFileMapping(
399
            $config->getBasePath(),
400
            $fileMapper
401
        );
402
    }
403
404
    private function addFiles(Configuration $config, Box $box, BuildLogger $logger, SymfonyStyle $io): void
405
    {
406
        $logger->log(BuildLogger::QUESTION_MARK_PREFIX, 'Adding binary files');
407
408
        $count = count($config->getBinaryFiles());
409
410
        $box->addFiles($config->getBinaryFiles(), true);
411
412
        $logger->log(
413
            BuildLogger::CHEVRON_PREFIX,
414
            0 === $count
415
                ? 'No file found'
416
                : sprintf('%d file(s)', $count)
417
        );
418
419
        $logger->log(BuildLogger::QUESTION_MARK_PREFIX, 'Adding files');
420
421
        $count = count($config->getFiles());
422
423
        try {
424
            $box->addFiles($config->getFiles(), false);
425
        } catch (MultiReasonException $exception) {
426
            // This exception is handled a different way to give me meaningful feedback to the user
427
            foreach ($exception->getReasons() as $reason) {
428
                $io->error($reason);
429
            }
430
431
            throw $exception;
432
        }
433
434
        $logger->log(
435
            BuildLogger::CHEVRON_PREFIX,
436
            0 === $count
437
                ? 'No file found'
438
                : sprintf('%d file(s)', $count)
439
        );
440
    }
441
442
    private function registerMainScript(Configuration $config, Box $box, BuildLogger $logger): string
443
    {
444
        $main = $config->getMainScriptPath();
445
446
        $logger->log(
447
            BuildLogger::QUESTION_MARK_PREFIX,
448
            sprintf(
449
                'Adding main file: %s',
450
                $main
451
            )
452
        );
453
454
        $localMain = $box->addFile(
455
            $main,
456
            $config->getMainScriptContents()
457
        );
458
459
        $relativeMain = make_path_relative($main, $config->getBasePath());
460
461
        if ($localMain !== $relativeMain) {
462
            $logger->log(
463
                BuildLogger::CHEVRON_PREFIX,
464
                $localMain
465
            );
466
        }
467
468
        return $localMain;
469
    }
470
471
    private function registerRequirementsChecker(Configuration $config, Box $box, BuildLogger $logger): bool
472
    {
473
        if (false === $config->checkRequirements()) {
474
            return false;
475
        }
476
477
        $logger->log(
478
            BuildLogger::QUESTION_MARK_PREFIX,
479
            'Adding requirements checker'
480
        );
481
482
        $checkFiles = RequirementsDumper::dump(
483
            $config->getComposerJsonDecodedContents() ?? [],
484
            $config->getComposerLockDecodedContents() ?? [],
485
            $config->getCompressionAlgorithm()
486
        );
487
488
        foreach ($checkFiles as $fileWithContents) {
489
            [$file, $contents] = $fileWithContents;
490
491
            $box->addFile('.box/'.$file, $contents, true);
492
        }
493
494
        return true;
495
    }
496
497
    private function registerStub(Configuration $config, Box $box, string $main, bool $checkRequirements, BuildLogger $logger): void
498
    {
499
        if ($config->isStubGenerated()) {
500
            $logger->log(
501
                BuildLogger::QUESTION_MARK_PREFIX,
502
                'Generating new stub'
503
            );
504
505
            $stub = $this->createStub($config, $main, $checkRequirements, $logger);
506
507
            $box->getPhar()->setStub($stub->generate());
508
509
            return;
510
        }
511
        if (null !== ($stub = $config->getStubPath())) {
512
            $logger->log(
513
                BuildLogger::QUESTION_MARK_PREFIX,
514
                sprintf(
515
                    'Using stub file: %s',
516
                    $stub
517
                )
518
            );
519
520
            $box->registerStub($stub);
521
522
            return;
523
        }
524
525
        // TODO: add warning that the check requirements could not be added
526
        $aliasWasAdded = $box->getPhar()->setAlias($config->getAlias());
527
528
        Assertion::true(
529
            $aliasWasAdded,
530
            sprintf(
531
                'The alias "%s" is invalid. See Phar::setAlias() documentation for more information.',
532
                $config->getAlias()
533
            )
534
        );
535
536
        $box->getPhar()->setDefaultStub($main);
537
538
        $logger->log(
539
            BuildLogger::QUESTION_MARK_PREFIX,
540
            'Using default stub'
541
        );
542
    }
543
544
    private function configureMetadata(Configuration $config, Box $box, BuildLogger $logger): void
545
    {
546
        if (null !== ($metadata = $config->getMetadata())) {
547
            $logger->log(
548
                BuildLogger::QUESTION_MARK_PREFIX,
549
                'Setting metadata'
550
            );
551
552
            $logger->log(
553
                BuildLogger::MINUS_PREFIX,
554
                is_string($metadata) ? $metadata : var_export($metadata, true)
555
            );
556
557
            $box->getPhar()->setMetadata($metadata);
558
        }
559
    }
560
561
    private function configureCompressionAlgorithm(Configuration $config, Box $box, bool $dev, SymfonyStyle $io, BuildLogger $logger): void
562
    {
563
        if (null === ($algorithm = $config->getCompressionAlgorithm())) {
564
            $logger->log(
565
                BuildLogger::QUESTION_MARK_PREFIX,
566
                $dev
567
                    ? 'No compression'
568
                    : '<error>No compression</error>'
569
            );
570
571
            return;
572
        }
573
574
        $logger->log(
575
            BuildLogger::QUESTION_MARK_PREFIX,
576
            sprintf(
577
                'Compressing with the algorithm "<comment>%s</comment>"',
578
                array_search($algorithm, get_phar_compression_algorithms(), true)
1 ignored issue
show
Bug introduced by
It seems like array_search($algorithm,...ion_algorithms(), true) can also be of type false; however, parameter $args of sprintf() 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

578
                /** @scrutinizer ignore-type */ array_search($algorithm, get_phar_compression_algorithms(), true)
Loading history...
579
            )
580
        );
581
582
        $restoreLimit = $this->bumpOpenFileDescriptorLimit($box, $io);
583
584
        try {
585
            $extension = $box->compress($algorithm);
586
587
            if (null !== $extension) {
588
                $logger->log(
589
                    BuildLogger::CHEVRON_PREFIX,
590
                    sprintf(
591
                        '<info>Warning: the extension "%s" will now be required to execute the PHAR</info>',
592
                        $extension
593
                    )
594
                );
595
            }
596
        } catch (RuntimeException $exception) {
597
            $io->error($exception->getMessage());
598
599
            // Continue: the compression failure should not result in completely bailing out the compilation process
600
        } finally {
601
            $restoreLimit();
602
        }
603
    }
604
605
    /**
606
     * Bumps the maximum number of open file descriptor if necessary.
607
     *
608
     * @return callable callable to call to restore the original maximum number of open files descriptors
609
     */
610
    private function bumpOpenFileDescriptorLimit(Box $box, SymfonyStyle $io): callable
611
    {
612
        $filesCount = count($box) + 128;  // Add a little extra for good measure
613
614
        if (function_exists('posix_getrlimit') && function_exists('posix_setrlimit')) {
615
            $softLimit = posix_getrlimit()['soft openfiles'];
616
            $hardLimit = posix_getrlimit()['hard openfiles'];
617
618
            if ($softLimit < $filesCount) {
619
                $io->writeln(
620
                    sprintf(
621
                        '<info>[debug] Increased the maximum number of open file descriptors from ("%s", "%s") to ("%s", "%s")'
622
                        .'</info>',
623
                        $softLimit,
624
                        $hardLimit,
625
                        $filesCount,
626
                        'unlimited'
627
                    ),
628
                    OutputInterface::VERBOSITY_DEBUG
629
                );
630
631
                posix_setrlimit(
632
                    POSIX_RLIMIT_NOFILE,
633
                    $filesCount,
634
                    'unlimited' === $hardLimit ? POSIX_RLIMIT_INFINITY : $hardLimit
635
                );
636
            }
637
        } else {
638
            $io->writeln(
639
                '<info>[debug] Could not check the maximum number of open file descriptors: the functions "posix_getrlimit()" and '
640
                .'"posix_setrlimit" could not be found.</info>',
641
                OutputInterface::VERBOSITY_DEBUG
642
            );
643
        }
644
645
        return function () use ($io, $softLimit, $hardLimit): void {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $softLimit does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $hardLimit does not seem to be defined for all execution paths leading up to this point.
Loading history...
646
            if (function_exists('posix_setrlimit') && isset($softLimit, $hardLimit)) {
647
                posix_setrlimit(
648
                    POSIX_RLIMIT_NOFILE,
649
                    $softLimit,
650
                    'unlimited' === $hardLimit ? POSIX_RLIMIT_INFINITY : $hardLimit
651
                );
652
653
                $io->writeln(
654
                    '<info>[debug] Restored the maximum number of open file descriptors</info>',
655
                    OutputInterface::VERBOSITY_DEBUG
656
                );
657
            }
658
        };
659
    }
660
661
    private function signPhar(
662
        Configuration $config,
663
        Box $box,
664
        string $path,
665
        InputInterface $input,
666
        OutputInterface $output,
667
        BuildLogger $logger
668
    ): void {
669
        // sign using private key, if applicable
670
        //TODO: check that out
671
        remove($path.'.pubkey');
672
673
        $key = $config->getPrivateKeyPath();
674
675
        if (null === $key) {
676
            if (null !== ($algorithm = $config->getSigningAlgorithm())) {
0 ignored issues
show
introduced by
The condition null !== $algorithm = $c...->getSigningAlgorithm() is always true.
Loading history...
677
                $box->getPhar()->setSignatureAlgorithm($algorithm);
678
            }
679
680
            return;
681
        }
682
683
        $logger->log(
684
            BuildLogger::QUESTION_MARK_PREFIX,
685
            'Signing using a private key'
686
        );
687
688
        $passphrase = $config->getPrivateKeyPassphrase();
689
690
        if ($config->isPrivateKeyPrompt()) {
691
            if (false === $input->isInteractive()) {
692
                throw new RuntimeException(
693
                    sprintf(
694
                        'Accessing to the private key "%s" requires a passphrase but none provided. Either '
695
                        .'provide one or run this command in interactive mode.',
696
                        $key
697
                    )
698
                );
699
            }
700
701
            /** @var $dialog QuestionHelper */
702
            $dialog = $this->getHelper('question');
703
704
            $question = new Question('Private key passphrase:');
705
            $question->setHidden(false);
706
            $question->setHiddenFallback(false);
707
708
            $passphrase = $dialog->ask($input, $output, $question);
709
710
            $output->writeln('');
711
        }
712
713
        $box->signUsingFile($key, $passphrase);
714
    }
715
716
    private function correctPermissions(string $path, Configuration $config, BuildLogger $logger): void
717
    {
718
        if (null !== ($chmod = $config->getFileMode())) {
719
            $logger->log(
720
                BuildLogger::QUESTION_MARK_PREFIX,
721
                sprintf(
722
                    'Setting file permissions to <comment>%s</comment>',
723
                    '0'.decoct($chmod)
724
                )
725
            );
726
727
            chmod($path, $chmod);
728
        }
729
    }
730
731
    private function createStub(Configuration $config, ?string $main, bool $checkRequirements, BuildLogger $logger): StubGenerator
732
    {
733
        $stub = StubGenerator::create()
734
            ->alias($config->getAlias())
735
            ->index($main)
736
            ->intercept($config->isInterceptFileFuncs())
737
            ->checkRequirements($checkRequirements)
738
        ;
739
740
        if (null !== ($shebang = $config->getShebang())) {
741
            $logger->log(
742
                BuildLogger::MINUS_PREFIX,
743
                sprintf(
744
                    'Using shebang line: %s',
745
                    $shebang
746
                )
747
            );
748
749
            $stub->shebang($shebang);
750
        } else {
751
            $logger->log(
752
                BuildLogger::MINUS_PREFIX,
753
                'No shebang line'
754
            );
755
        }
756
757
        if (null !== ($bannerPath = $config->getStubBannerPath())) {
758
            $logger->log(
759
                BuildLogger::MINUS_PREFIX,
760
                sprintf(
761
                    'Using custom banner from file: %s',
762
                    $bannerPath
763
                )
764
            );
765
766
            $stub->banner($config->getStubBannerContents());
767
        } elseif (null !== ($banner = $config->getStubBannerContents())) {
768
            $logger->log(
769
                BuildLogger::MINUS_PREFIX,
770
                'Using banner:'
771
            );
772
773
            $bannerLines = explode("\n", $banner);
774
775
            foreach ($bannerLines as $bannerLine) {
776
                $logger->log(
777
                    BuildLogger::CHEVRON_PREFIX,
778
                    $bannerLine
779
                );
780
            }
781
782
            $stub->banner($banner);
783
        }
784
785
        return $stub;
786
    }
787
788
    private function logMap(MapFile $fileMapper, BuildLogger $logger): void
789
    {
790
        $map = $fileMapper->getMap();
791
792
        if ([] === $map) {
793
            return;
794
        }
795
796
        $logger->log(
797
            BuildLogger::QUESTION_MARK_PREFIX,
798
            'Mapping paths'
799
        );
800
801
        foreach ($map as $item) {
802
            foreach ($item as $match => $replace) {
803
                if ('' === $match) {
804
                    $match = '(all)';
805
                    $replace .= '/';
806
                }
807
808
                $logger->log(
809
                    BuildLogger::MINUS_PREFIX,
810
                    sprintf(
811
                        '%s <info>></info> %s',
812
                        $match,
813
                        $replace
814
                    )
815
                );
816
            }
817
        }
818
    }
819
820
    private function logEndBuilding(BuildLogger $logger, SymfonyStyle $io, Box $box, string $path, float $startTime): void
821
    {
822
        $logger->log(
823
            BuildLogger::STAR_PREFIX,
824
            'Done.'
825
        );
826
827
        $io->comment(
828
            sprintf(
829
                'PHAR: %s (%s)',
830
                $box->count() > 1 ? $box->count() . ' files' : $box->count() . ' file',
831
                formatted_filesize($path)
832
            )
833
            .PHP_EOL
834
            .'You can inspect the generated PHAR with the "<comment>info</comment>" command.'
835
        );
836
837
        $io->comment(
838
            sprintf(
839
                "<info>Memory usage: %.2fMB (peak: %.2fMB), time: %.2fs<info>",
840
                round(memory_get_usage() / 1024 / 1024, 2),
841
                round(memory_get_peak_usage() / 1024 / 1024, 2),
842
                round(microtime(true) - $startTime, 2)
843
            )
844
        );
845
    }
846
}
847