Passed
Pull Request — master (#235)
by Théo
02:49
created

Compile::configureCompressionAlgorithm()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 41
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 41
rs 8.439
c 0
b 0
f 0
cc 5
eloc 25
nc 9
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 function_exists;
51
use function get_class;
52
use function implode;
53
use function KevinGH\Box\disable_parallel_processing;
54
use function KevinGH\Box\FileSystem\chmod;
55
use function KevinGH\Box\FileSystem\dump_file;
56
use function KevinGH\Box\FileSystem\make_path_relative;
57
use function KevinGH\Box\FileSystem\remove;
58
use function KevinGH\Box\FileSystem\rename;
59
use function KevinGH\Box\formatted_filesize;
60
use function KevinGH\Box\get_phar_compression_algorithms;
61
use function posix_setrlimit;
62
use function putenv;
63
use function sprintf;
64
use function strlen;
65
use function substr;
66
67
/**
68
 * @final
69
 * @private
70
 * TODO: make final when Build is removed
71
 */
72
class Compile extends Configurable
73
{
74
    use ChangeableWorkingDirectory;
75
76
    private const HELP = <<<'HELP'
77
The <info>%command.name%</info> command will compile code in a new PHAR based on a variety of settings.
78
<comment>
79
  This command relies on a configuration file for loading
80
  PHAR packaging settings. If a configuration file is not
81
  specified through the <info>--config|-c</info> option, one of
82
  the following files will be used (in order): <info>box.json</info>,
83
  <info>box.json.dist</info>
84
</comment>
85
The configuration file is actually a JSON object saved to a file. For more
86
information check the documentation online:
87
<comment>
88
  https://github.com/humbug/box
89
</comment>
90
HELP;
91
92
    private const DEBUG_OPTION = 'debug';
93
    private const NO_PARALLEL_PROCESSING_OPTION = 'no-parallel';
94
    private const NO_RESTART_OPTION = 'no-restart';
95
    private const DEV_OPTION = 'dev';
96
    private const NO_CONFIG_OPTION = 'no-config';
97
98
    private const DEBUG_DIR = '.box_dump';
99
100
    /**
101
     * {@inheritdoc}
102
     */
103
    protected function configure(): void
104
    {
105
        parent::configure();
106
107
        $this->setName('compile');
108
        $this->setDescription('Compile an application into a PHAR');
109
        $this->setHelp(self::HELP);
110
111
        $this->addOption(
112
            self::DEBUG_OPTION,
113
            null,
114
            InputOption::VALUE_NONE,
115
            'Dump the files added to the PHAR in a `'.self::DEBUG_DIR.'` directory'
116
        );
117
        $this->addOption(
118
            self::NO_PARALLEL_PROCESSING_OPTION,
119
            null,
120
            InputOption::VALUE_NONE,
121
            'Disable the parallel processing'
122
        );
123
        $this->addOption(
124
            self::NO_RESTART_OPTION,
125
            null,
126
            InputOption::VALUE_NONE,
127
            'Do not restart the PHP process. Box restarts the process by default to disable xdebug and set `phar.readonly=0`'
128
        );
129
        $this->addOption(
130
            self::DEV_OPTION,
131
            null,
132
            InputOption::VALUE_NONE,
133
            'Skips the compression step'
134
        );
135
        $this->addOption(
136
            self::NO_CONFIG_OPTION,
137
            null,
138
            InputOption::VALUE_NONE,
139
            'Ignore the config file even when one is specified with the --config option'
140
        );
141
142
        $this->configureWorkingDirOption();
143
    }
144
145
    /**
146
     * {@inheritdoc}
147
     */
148
    protected function execute(InputInterface $input, OutputInterface $output): void
149
    {
150
        $io = new SymfonyStyle($input, $output);
151
152
        if ($input->getOption(self::NO_RESTART_OPTION)) {
153
            putenv(BOX_ALLOW_XDEBUG.'=1');
154
        }
155
156
        if ($debug = $input->getOption(self::DEBUG_OPTION)) {
157
            $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);
158
        }
159
160
        (new PhpSettingsHandler(new ConsoleLogger($output)))->check();
161
162
        if ($input->getOption(self::NO_PARALLEL_PROCESSING_OPTION)) {
163
            disable_parallel_processing();
164
            $io->writeln('<info>[debug] Disabled parallel processing</info>', OutputInterface::VERBOSITY_DEBUG);
165
        }
166
167
        $this->changeWorkingDirectory($input);
168
169
        $io->writeln($this->getApplication()->getHelp());
170
        $io->writeln('');
171
172
        $config = $input->getOption(self::NO_CONFIG_OPTION)
173
            ? Configuration::create(null, new stdClass())
174
            : $this->getConfig($input, $output, true)
175
        ;
176
        $path = $config->getOutputPath();
177
178
        $logger = new BuildLogger($io);
179
180
        $startTime = microtime(true);
181
182
        $this->removeExistingArtifacts($config, $logger, $debug);
183
184
        $logger->logStartBuilding($path);
185
186
        $box = $this->createPhar($config, $input, $output, $logger, $io, $debug);
187
188
        $this->correctPermissions($path, $config, $logger);
189
190
        $this->logEndBuilding($logger, $io, $box, $path, $startTime);
191
    }
192
193
    private function createPhar(
194
        Configuration $config,
195
        InputInterface $input,
196
        OutputInterface $output,
197
        BuildLogger $logger,
198
        SymfonyStyle $io,
199
        bool $debug
200
    ): Box {
201
        $box = Box::create(
202
            $config->getTmpOutputPath()
203
        );
204
        $box->startBuffering();
205
206
        $this->registerReplacementValues($config, $box, $logger);
207
        $this->registerCompactors($config, $box, $logger);
208
        $this->registerFileMapping($config, $box, $logger);
209
210
        // Registering the main script _before_ adding the rest if of the files is _very_ important. The temporary
211
        // file used for debugging purposes and the Composer dump autoloading will not work correctly otherwise.
212
        $main = $this->registerMainScript($config, $box, $logger);
213
214
        $check = $this->registerRequirementsChecker($config, $box, $logger);
215
216
        $this->addFiles($config, $box, $logger, $io);
217
218
        $this->registerStub($config, $box, $main, $check, $logger);
219
        $this->configureMetadata($config, $box, $logger);
220
221
        $this->commit($box, $config, $logger);
222
223
        $this->checkComposerFiles($box, $config, $logger);
224
225
        $this->configureCompressionAlgorithm($config, $box, $input->getOption(self::DEV_OPTION), $io, $logger);
226
227
        if ($debug) {
228
            $box->getPhar()->extractTo(self::DEBUG_DIR, null, true);
229
        }
230
231
        $this->signPhar($config, $box, $config->getTmpOutputPath(), $input, $output, $logger);
232
233
        if ($config->getTmpOutputPath() !== $config->getOutputPath()) {
234
            rename($config->getTmpOutputPath(), $config->getOutputPath());
235
        }
236
237
        return $box;
238
    }
239
240
    private function removeExistingArtifacts(Configuration $config, BuildLogger $logger, bool $debug): void
241
    {
242
        $path = $config->getOutputPath();
243
244
        if ($debug) {
245
            remove(self::DEBUG_DIR);
246
247
            $date = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format(DATE_ATOM);
248
            $file = null !== $config->getFile() ? $config->getFile() : 'No config file';
249
250
            remove(self::DEBUG_DIR);
251
252
            dump_file(
253
                self::DEBUG_DIR.'/.box_configuration',
254
                <<<EOF
255
//
256
// Processed content of the configuration file "$file" dumped for debugging purposes
257
// Time: $date
258
//
259
260
261
EOF
262
                .(new CliDumper())->dump(
263
                    (new VarCloner())->cloneVar($config),
264
                    true
265
                )
266
            );
267
        }
268
269
        if (false === file_exists($path)) {
270
            return;
271
        }
272
273
        $logger->log(
274
            BuildLogger::QUESTION_MARK_PREFIX,
275
            sprintf(
276
                'Removing the existing PHAR "%s"',
277
                $path
278
            )
279
        );
280
281
        remove($path);
282
    }
283
284
    private function registerReplacementValues(Configuration $config, Box $box, BuildLogger $logger): void
285
    {
286
        $values = $config->getProcessedReplacements();
287
288
        if ([] === $values) {
289
            return;
290
        }
291
292
        $logger->log(
293
            BuildLogger::QUESTION_MARK_PREFIX,
294
            'Setting replacement values'
295
        );
296
297
        foreach ($values as $key => $value) {
298
            $logger->log(
299
                BuildLogger::PLUS_PREFIX,
300
                sprintf(
301
                    '%s: %s',
302
                    $key,
303
                    $value
304
                )
305
            );
306
        }
307
308
        $box->registerPlaceholders($values);
309
    }
310
311
    private function registerCompactors(Configuration $config, Box $box, BuildLogger $logger): void
312
    {
313
        $compactors = $config->getCompactors();
314
315
        if ([] === $compactors) {
316
            $logger->log(
317
                BuildLogger::QUESTION_MARK_PREFIX,
318
                'No compactor to register'
319
            );
320
321
            return;
322
        }
323
324
        $logger->log(
325
            BuildLogger::QUESTION_MARK_PREFIX,
326
            'Registering compactors'
327
        );
328
329
        $logCompactors = function (Compactor $compactor) use ($logger): void {
330
            $compactorClassParts = explode('\\', get_class($compactor));
331
332
            if ('_HumbugBox' === substr($compactorClassParts[0], 0, strlen('_HumbugBox'))) {
333
                // Keep the non prefixed class name for the user
334
                array_shift($compactorClassParts);
335
            }
336
337
            $logger->log(
338
                BuildLogger::PLUS_PREFIX,
339
                implode('\\', $compactorClassParts)
340
            );
341
        };
342
343
        array_map($logCompactors, $compactors);
344
345
        $box->registerCompactors($compactors);
346
    }
347
348
    private function registerFileMapping(Configuration $config, Box $box, BuildLogger $logger): void
349
    {
350
        $fileMapper = $config->getFileMapper();
351
352
        $this->logMap($fileMapper, $logger);
353
354
        $box->registerFileMapping(
355
            $config->getBasePath(),
356
            $fileMapper
357
        );
358
    }
359
360
    private function addFiles(Configuration $config, Box $box, BuildLogger $logger, SymfonyStyle $io): void
361
    {
362
        $logger->log(BuildLogger::QUESTION_MARK_PREFIX, 'Adding binary files');
363
364
        $count = count($config->getBinaryFiles());
365
366
        $box->addFiles($config->getBinaryFiles(), true);
367
368
        $logger->log(
369
            BuildLogger::CHEVRON_PREFIX,
370
            0 === $count
371
                ? 'No file found'
372
                : sprintf('%d file(s)', $count)
373
        );
374
375
        $logger->log(BuildLogger::QUESTION_MARK_PREFIX, 'Adding files');
376
377
        $count = count($config->getFiles());
378
379
        try {
380
            $box->addFiles($config->getFiles(), false);
381
        } catch (MultiReasonException $exception) {
382
            // This exception is handled a different way to give me meaningful feedback to the user
383
            foreach ($exception->getReasons() as $reason) {
384
                $io->error($reason);
385
            }
386
387
            throw $exception;
388
        }
389
390
        $logger->log(
391
            BuildLogger::CHEVRON_PREFIX,
392
            0 === $count
393
                ? 'No file found'
394
                : sprintf('%d file(s)', $count)
395
        );
396
    }
397
398
    private function registerMainScript(Configuration $config, Box $box, BuildLogger $logger): ?string
399
    {
400
        if (false === $config->hasMainScript()) {
401
            $logger->log(
402
                BuildLogger::QUESTION_MARK_PREFIX,
403
                'No main script path configured'
404
            );
405
406
            return null;
407
        }
408
409
        $main = $config->getMainScriptPath();
410
411
        $logger->log(
412
            BuildLogger::QUESTION_MARK_PREFIX,
413
            sprintf(
414
                'Adding main file: %s',
415
                $main
416
            )
417
        );
418
419
        $localMain = $box->addFile(
420
            $main,
421
            $config->getMainScriptContents()
422
        );
423
424
        $relativeMain = make_path_relative($main, $config->getBasePath());
425
426
        if ($localMain !== $relativeMain) {
427
            $logger->log(
428
                BuildLogger::CHEVRON_PREFIX,
429
                $localMain
430
            );
431
        }
432
433
        return $localMain;
434
    }
435
436
    private function registerRequirementsChecker(Configuration $config, Box $box, BuildLogger $logger): bool
437
    {
438
        if (false === $config->checkRequirements()) {
439
            $logger->log(
440
                BuildLogger::QUESTION_MARK_PREFIX,
441
                'Skip requirements checker'
442
            );
443
444
            return false;
445
        }
446
447
        $logger->log(
448
            BuildLogger::QUESTION_MARK_PREFIX,
449
            'Adding requirements checker'
450
        );
451
452
        $checkFiles = RequirementsDumper::dump(
453
            $config->getComposerJsonDecodedContents() ?? [],
454
            $config->getComposerLockDecodedContents() ?? [],
455
            $config->getCompressionAlgorithm()
456
        );
457
458
        foreach ($checkFiles as $fileWithContents) {
459
            [$file, $contents] = $fileWithContents;
460
461
            $box->addFile('.box/'.$file, $contents, true);
462
        }
463
464
        return true;
465
    }
466
467
    private function registerStub(Configuration $config, Box $box, ?string $main, bool $checkRequirements, BuildLogger $logger): void
468
    {
469
        if ($config->isStubGenerated()) {
470
            $logger->log(
471
                BuildLogger::QUESTION_MARK_PREFIX,
472
                'Generating new stub'
473
            );
474
475
            $stub = $this->createStub($config, $main, $checkRequirements, $logger);
476
477
            $box->getPhar()->setStub($stub->generate());
478
479
            return;
480
        }
481
        if (null !== ($stub = $config->getStubPath())) {
482
            $logger->log(
483
                BuildLogger::QUESTION_MARK_PREFIX,
484
                sprintf(
485
                    'Using stub file: %s',
486
                    $stub
487
                )
488
            );
489
490
            $box->registerStub($stub);
491
492
            return;
493
        }
494
495
        // TODO: add warning that the check requirements could not be added
496
        $aliasWasAdded = $box->getPhar()->setAlias($config->getAlias());
497
498
        Assertion::true(
499
            $aliasWasAdded,
500
            sprintf(
501
                'The alias "%s" is invalid. See Phar::setAlias() documentation for more information.',
502
                $config->getAlias()
503
            )
504
        );
505
506
        $box->getPhar()->setDefaultStub($main);
507
508
        $logger->log(
509
            BuildLogger::QUESTION_MARK_PREFIX,
510
            'Using default stub'
511
        );
512
    }
513
514
    private function configureMetadata(Configuration $config, Box $box, BuildLogger $logger): void
515
    {
516
        if (null !== ($metadata = $config->getMetadata())) {
517
            $logger->log(
518
                BuildLogger::QUESTION_MARK_PREFIX,
519
                'Setting metadata'
520
            );
521
522
            $logger->log(
523
                BuildLogger::MINUS_PREFIX,
524
                is_string($metadata) ? $metadata : var_export($metadata, true)
525
            );
526
527
            $box->getPhar()->setMetadata($metadata);
528
        }
529
    }
530
531
    private function commit(Box $box, Configuration $config, BuildLogger $logger): void
532
    {
533
        $message = $config->dumpAutoload()
534
            ? 'Dumping the Composer autoloader'
535
            : 'Skipping dumping the Composer autoloader'
536
        ;
537
538
        $logger->log(BuildLogger::QUESTION_MARK_PREFIX, $message);
539
540
        $box->endBuffering($config->dumpAutoload());
541
    }
542
543
    private function checkComposerFiles(Box $box, Configuration $config, BuildLogger $logger): void
544
    {
545
        $message = $config->excludeComposerFiles()
546
            ? 'Removing the Composer dump artefacts'
547
            : 'Keep the Composer dump artefacts'
548
        ;
549
550
        $logger->log(BuildLogger::QUESTION_MARK_PREFIX, $message);
551
552
        if ($config->excludeComposerFiles()) {
553
            $box->removeComposerArtefacts(
554
                ComposerConfiguration::retrieveVendorDir(
555
                    $config->getComposerJsonDecodedContents() ?? []
556
                )
557
            );
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