Passed
Pull Request — master (#287)
by Théo
02:12
created

Compile::generateDockerFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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