Passed
Pull Request — master (#236)
by Théo
02:54
created

Compile::logEndBuilding()   B

Complexity

Conditions 2
Paths 1

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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

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