Completed
Pull Request — master (#108)
by Théo
03:03
created

Compile::removeExistingArtefacts()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 40
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 40
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 23
nc 6
nop 2
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\StubGenerator;
28
use RuntimeException;
29
use Symfony\Component\Console\Helper\QuestionHelper;
30
use Symfony\Component\Console\Input\InputInterface;
31
use Symfony\Component\Console\Input\InputOption;
32
use Symfony\Component\Console\Logger\ConsoleLogger;
33
use Symfony\Component\Console\Output\OutputInterface;
34
use Symfony\Component\Console\Question\Question;
35
use Symfony\Component\Console\Style\SymfonyStyle;
36
use Symfony\Component\VarDumper\Cloner\VarCloner;
37
use Symfony\Component\VarDumper\Dumper\CliDumper;
38
use const DATE_ATOM;
39
use function count;
40
use function KevinGH\Box\enable_debug;
41
use function KevinGH\Box\FileSystem\chmod;
42
use function KevinGH\Box\FileSystem\dump_file;
43
use function KevinGH\Box\FileSystem\make_path_relative;
44
use function KevinGH\Box\FileSystem\remove;
45
use function KevinGH\Box\FileSystem\rename;
46
use function KevinGH\Box\formatted_filesize;
47
use function KevinGH\Box\get_phar_compression_algorithms;
48
use function KevinGH\Box\is_debug_enabled;
49
50
/**
51
 * @final
52
 * TODO: make final when Build is removed
53
 */
54
class Compile extends Configurable
55
{
56
    use ChangeableWorkingDirectory;
57
58
    private const HELP = <<<'HELP'
59
The <info>%command.name%</info> command will compile code in a new PHAR based on a variety of settings.
60
<comment>
61
  This command relies on a configuration file for loading
62
  PHAR packaging settings. If a configuration file is not
63
  specified through the <info>--config|-c</info> option, one of
64
  the following files will be used (in order): <info>box.json</info>,
65
  <info>box.json.dist</info>
66
</comment>
67
The configuration file is actually a JSON object saved to a file. For more
68
information check the documentation online:
69
<comment>
70
  https://github.com/humbug/box
71
</comment>
72
HELP;
73
74
    private const DEBUG_OPTION = 'debug';
75
    private const DEV_OPTION = 'dev';
76
77
    /**
78
     * {@inheritdoc}
79
     */
80
    protected function configure(): void
81
    {
82
        parent::configure();
83
84
        $this->setName('compile');
85
        $this->setDescription('Compile an application into a PHAR');
86
        $this->setHelp(self::HELP);
87
88
        $this->addOption(
89
            self::DEBUG_OPTION,
90
            null,
91
            InputOption::VALUE_NONE,
92
            'Dump the files added to the PHAR in a `'.Box::DEBUG_DIR.'` directory'
93
        );
94
        $this->addOption(
95
            self::DEV_OPTION,
96
            null,
97
            InputOption::VALUE_NONE,
98
            'Skips the compression step'
99
        );
100
101
        $this->configureWorkingDirOption();
102
    }
103
104
    /**
105
     * {@inheritdoc}
106
     */
107
    protected function execute(InputInterface $input, OutputInterface $output): void
108
    {
109
        (new PhpSettingsHandler(new ConsoleLogger($output)))->check();
110
111
        if (true === $input->getOption(self::DEBUG_OPTION)) {
112
            enable_debug($output);
113
        }
114
115
        $this->changeWorkingDirectory($input);
116
117
        $io = new SymfonyStyle($input, $output);
118
119
        $io->writeln($this->getApplication()->getHelp());
120
        $io->writeln('');
121
122
        $config = $this->getConfig($input, $output, true);
123
        $path = $config->getOutputPath();
124
125
        $logger = new BuildLogger($io);
126
127
        $startTime = microtime(true);
128
129
        $this->removeExistingArtefacts($config, $logger);
130
131
        $logger->logStartBuilding($path);
132
133
        $this->createPhar($config, $input, $output, $logger, $io);
134
135
        $this->correctPermissions($path, $config, $logger);
136
137
        $logger->log(
138
            BuildLogger::STAR_PREFIX,
139
            'Done.'
140
        );
141
142
        $io->comment(
143
            sprintf(
144
                "<info>PHAR size: %s\nMemory usage: %.2fMB (peak: %.2fMB), time: %.2fs<info>",
145
                formatted_filesize($path),
146
                round(memory_get_usage() / 1024 / 1024, 2),
147
                round(memory_get_peak_usage() / 1024 / 1024, 2),
148
                round(microtime(true) - $startTime, 2)
149
            )
150
        );
151
    }
152
153
    private function createPhar(
154
        Configuration $config,
155
        InputInterface $input,
156
        OutputInterface $output,
157
        BuildLogger $logger,
158
        SymfonyStyle $io
159
    ): void {
160
        $box = Box::create(
161
            $config->getTmpOutputPath()
162
        );
163
        $box->getPhar()->startBuffering();
164
165
        $this->setReplacementValues($config, $box, $logger);
166
        $this->registerCompactors($config, $box, $logger);
167
        $this->registerFileMapping($config, $box, $logger);
168
169
        // Registering the main script _before_ adding the rest if of the files is _very_ important. The temporary
170
        // file used for debugging purposes and the Composer dump autoloading will not work correctly otherwise.
171
        $main = $this->registerMainScript($config, $box, $logger);
172
173
        $this->addFiles($config, $box, $logger, $io);
174
175
        $this->registerStub($config, $box, $main, $logger);
176
        $this->configureMetadata($config, $box, $logger);
177
178
        $this->configureCompressionAlgorithm($config, $box, $input->getOption(self::DEV_OPTION), $logger);
179
180
        $box->getPhar()->stopBuffering();
181
182
        $this->signPhar($config, $box, $config->getTmpOutputPath(), $input, $output, $logger);
183
184
        if ($config->getTmpOutputPath() !== $config->getOutputPath()) {
185
            rename($config->getTmpOutputPath(), $config->getOutputPath());
186
        }
187
    }
188
189
    private function removeExistingArtefacts(Configuration $config, BuildLogger $logger): void
190
    {
191
        $path = $config->getOutputPath();
192
193
        if (is_debug_enabled()) {
194
            $date = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format(DATE_ATOM);
195
            $file = null !== $config->getFile() ? $config->getFile() : 'No config file';
196
197
            remove(Box::DEBUG_DIR);
198
199
            dump_file(
200
                Box::DEBUG_DIR.'/.box_configuration',
201
                <<<EOF
202
//
203
// Processed content of the configuration file "$file" dumped for debugging purposes
204
// Time: $date
205
//
206
207
208
EOF
209
                .(new CliDumper())->dump(
210
                    (new VarCloner())->cloneVar($config),
211
                    true
212
                )
213
            );
214
        }
215
216
        if (false === file_exists($path)) {
217
            return;
218
        }
219
220
        $logger->log(
221
            BuildLogger::QUESTION_MARK_PREFIX,
222
            sprintf(
223
                'Removing the existing PHAR "%s"',
224
                $path
225
            )
226
        );
227
228
        remove($path);
229
    }
230
231
    private function setReplacementValues(Configuration $config, Box $box, BuildLogger $logger): void
232
    {
233
        $values = $config->getProcessedReplacements();
234
235
        if ([] === $values) {
236
            return;
237
        }
238
239
        $logger->log(
240
            BuildLogger::QUESTION_MARK_PREFIX,
241
            'Setting replacement values'
242
        );
243
244
        foreach ($values as $key => $value) {
245
            $logger->log(
246
                BuildLogger::PLUS_PREFIX,
247
                sprintf(
248
                    '%s: %s',
249
                    $key,
250
                    $value
251
                )
252
            );
253
        }
254
255
        $box->registerPlaceholders($values);
256
    }
257
258
    private function registerCompactors(Configuration $config, Box $box, BuildLogger $logger): void
259
    {
260
        $compactors = $config->getCompactors();
261
262
        if ([] === $compactors) {
263
            $logger->log(
264
                BuildLogger::QUESTION_MARK_PREFIX,
265
                'No compactor to register'
266
            );
267
268
            return;
269
        }
270
271
        $logger->log(
272
            BuildLogger::QUESTION_MARK_PREFIX,
273
            'Registering compactors'
274
        );
275
276
        $logCompactors = function (Compactor $compactor) use ($logger): void {
277
            $logger->log(
278
                BuildLogger::PLUS_PREFIX,
279
                get_class($compactor)
280
            );
281
        };
282
283
        array_map($logCompactors, $compactors);
284
285
        $box->registerCompactors($compactors);
286
    }
287
288
    private function registerFileMapping(Configuration $config, Box $box, BuildLogger $logger): void
289
    {
290
        $fileMapper = $config->getFileMapper();
291
292
        $this->logMap($fileMapper, $logger);
293
294
        $box->registerFileMapping(
295
            $config->getBasePath(),
296
            $fileMapper
297
        );
298
    }
299
300
    private function addFiles(Configuration $config, Box $box, BuildLogger $logger, SymfonyStyle $io): void
301
    {
302
        $logger->log(BuildLogger::QUESTION_MARK_PREFIX, 'Adding binary files');
303
304
        $count = count($config->getBinaryFiles());
305
306
        $box->addFiles($config->getBinaryFiles(), true);
307
308
        $logger->log(
309
            BuildLogger::CHEVRON_PREFIX,
310
            0 === $count
311
                ? 'No file found'
312
                : sprintf('%d file(s)', $count)
313
        );
314
315
        $logger->log(BuildLogger::QUESTION_MARK_PREFIX, 'Adding files');
316
317
        $count = count($config->getFiles());
318
319
        try {
320
            $box->addFiles($config->getFiles(), false);
321
        } catch (MultiReasonException $exception) {
322
            // This exception is handled a different way to give me meaningful feedback to the user
323
            foreach ($exception->getReasons() as $reason) {
324
                $io->error($reason);
325
            }
326
327
            throw $exception;
328
        }
329
330
        $logger->log(
331
            BuildLogger::CHEVRON_PREFIX,
332
            0 === $count
333
                ? 'No file found'
334
                : sprintf('%d file(s)', $count)
335
        );
336
    }
337
338
    private function registerMainScript(Configuration $config, Box $box, BuildLogger $logger): ?string
339
    {
340
        $main = $config->getMainScriptPath();
341
342
        $logger->log(
343
            BuildLogger::QUESTION_MARK_PREFIX,
344
            sprintf(
345
                'Adding main file: %s',
346
                $main
347
            )
348
        );
349
350
        $localMain = $box->addFile(
351
            $main,
352
            $config->getMainScriptContents()
353
        );
354
355
        $relativeMain = make_path_relative($main, $config->getBasePath());
356
357
        if ($localMain !== $relativeMain) {
358
            $logger->log(
359
                BuildLogger::CHEVRON_PREFIX,
360
                $localMain
361
            );
362
        }
363
364
        return $localMain;
365
    }
366
367
    private function registerStub(Configuration $config, Box $box, string $main, BuildLogger $logger): void
368
    {
369
        if (true === $config->isStubGenerated()) {
370
            $logger->log(
371
                BuildLogger::QUESTION_MARK_PREFIX,
372
                'Generating new stub'
373
            );
374
375
            $stub = $this->createStub($config, $main, $logger);
376
377
            $box->getPhar()->setStub($stub->generate());
378
        } elseif (null !== ($stub = $config->getStubPath())) {
379
            $logger->log(
380
                BuildLogger::QUESTION_MARK_PREFIX,
381
                sprintf(
382
                    'Using stub file: %s',
383
                    $stub
384
                )
385
            );
386
387
            $box->registerStub($stub);
388
        } else {
389
            $aliasWasAdded = $box->getPhar()->setAlias($config->getAlias());
390
391
            Assertion::true(
392
                $aliasWasAdded,
393
                sprintf(
394
                    'The alias "%s" is invalid. See Phar::setAlias() documentation for more information.',
395
                    $config->getAlias()
396
                )
397
            );
398
399
            $box->getPhar()->setDefaultStub($main);
400
401
            $logger->log(
402
                BuildLogger::QUESTION_MARK_PREFIX,
403
                'Using default stub'
404
            );
405
        }
406
    }
407
408
    private function configureMetadata(Configuration $config, Box $box, BuildLogger $logger): void
409
    {
410
        if (null !== ($metadata = $config->getMetadata())) {
411
            $logger->log(
412
                BuildLogger::QUESTION_MARK_PREFIX,
413
                'Setting metadata'
414
            );
415
416
            $logger->log(
417
                BuildLogger::MINUS_PREFIX,
418
                is_string($metadata) ? $metadata : var_export($metadata, true)
419
            );
420
421
            $box->getPhar()->setMetadata($metadata);
422
        }
423
    }
424
425
    private function configureCompressionAlgorithm(Configuration $config, Box $box, bool $dev, BuildLogger $logger): void
426
    {
427
        if (null !== ($algorithm = $config->getCompressionAlgorithm())) {
428
            $logger->log(
429
                BuildLogger::QUESTION_MARK_PREFIX,
430
                sprintf(
431
                    'Compressing with the algorithm "<comment>%s</comment>"',
432
                    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

432
                    /** @scrutinizer ignore-type */ array_search($algorithm, get_phar_compression_algorithms(), true)
Loading history...
433
                )
434
            );
435
436
            $box->getPhar()->compressFiles($algorithm);
437
        } else {
438
            $logger->log(
439
                BuildLogger::QUESTION_MARK_PREFIX,
440
                $dev
441
                    ? 'No compression'
442
                    : '<error>No compression</error>'
443
            );
444
        }
445
    }
446
447
    private function signPhar(
448
        Configuration $config,
449
        Box $box,
450
        string $path,
451
        InputInterface $input,
452
        OutputInterface $output,
453
        BuildLogger $logger
454
    ): void {
455
        // sign using private key, if applicable
456
        //TODO: check that out
457
        remove($path.'.pubkey');
458
459
        $key = $config->getPrivateKeyPath();
460
461
        if (null === $key) {
462
            if (null !== ($algorithm = $config->getSigningAlgorithm())) {
0 ignored issues
show
introduced by
The condition null !== $algorithm = $c...->getSigningAlgorithm() is always true.
Loading history...
463
                $box->getPhar()->setSignatureAlgorithm($algorithm);
464
            }
465
466
            return;
467
        }
468
469
        $logger->log(
470
            BuildLogger::QUESTION_MARK_PREFIX,
471
            'Signing using a private key'
472
        );
473
474
        $passphrase = $config->getPrivateKeyPassphrase();
475
476
        if ($config->isPrivateKeyPrompt()) {
477
            if (false === $input->isInteractive()) {
478
                throw new RuntimeException(
479
                    sprintf(
480
                        'Accessing to the private key "%s" requires a passphrase but none provided. Either '
481
                        .'provide one or run this command in interactive mode.',
482
                        $key
483
                    )
484
                );
485
            }
486
487
            /** @var $dialog QuestionHelper */
488
            $dialog = $this->getHelper('question');
489
490
            $question = new Question('Private key passphrase:');
491
            $question->setHidden(false);
492
            $question->setHiddenFallback(false);
493
494
            $passphrase = $dialog->ask($input, $output, $question);
495
496
            $output->writeln('');
497
        }
498
499
        $box->signUsingFile($key, $passphrase);
0 ignored issues
show
Bug introduced by
It seems like $passphrase can also be of type boolean; however, parameter $password of KevinGH\Box\Box::signUsingFile() does only seem to accept null|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

499
        $box->signUsingFile($key, /** @scrutinizer ignore-type */ $passphrase);
Loading history...
500
    }
501
502
    private function correctPermissions(string $path, Configuration $config, BuildLogger $logger): void
503
    {
504
        if (null !== ($chmod = $config->getFileMode())) {
505
            $logger->log(
506
                BuildLogger::QUESTION_MARK_PREFIX,
507
                "Setting file permissions to <comment>$chmod</comment>"
508
            );
509
510
            chmod($path, $chmod);
511
        }
512
    }
513
514
    private function createStub(Configuration $config, ?string $main, BuildLogger $logger): StubGenerator
515
    {
516
        $stub = StubGenerator::create()
517
            ->alias($config->getAlias())
518
            ->index($main)
519
            ->intercept($config->isInterceptFileFuncs())
520
        ;
521
522
        if (null !== ($shebang = $config->getShebang())) {
523
            $logger->log(
524
                BuildLogger::MINUS_PREFIX,
525
                sprintf(
526
                    'Using shebang line: %s',
527
                    $shebang
528
                )
529
            );
530
531
            $stub->shebang($shebang);
532
        } else {
533
            $logger->log(
534
                BuildLogger::MINUS_PREFIX,
535
                'No shebang line'
536
            );
537
        }
538
539
        if (null !== ($bannerPath = $config->getStubBannerPath())) {
540
            $logger->log(
541
                BuildLogger::MINUS_PREFIX,
542
                sprintf(
543
                    'Using custom banner from file: %s',
544
                    $bannerPath
545
                )
546
            );
547
548
            $stub->banner($config->getStubBannerContents());
549
        } elseif (null !== ($banner = $config->getStubBannerContents())) {
550
            $logger->log(
551
                BuildLogger::MINUS_PREFIX,
552
                'Using banner:'
553
            );
554
555
            $bannerLines = explode("\n", $banner);
556
557
            foreach ($bannerLines as $bannerLine) {
558
                $logger->log(
559
                    BuildLogger::CHEVRON_PREFIX,
560
                    $bannerLine
561
                );
562
            }
563
564
            $stub->banner($banner);
565
        }
566
567
        return $stub;
568
    }
569
570
    private function logMap(MapFile $fileMapper, BuildLogger $logger): void
571
    {
572
        $map = $fileMapper->getMap();
573
574
        if ([] === $map) {
575
            return;
576
        }
577
578
        $logger->log(
579
            BuildLogger::QUESTION_MARK_PREFIX,
580
            'Mapping paths'
581
        );
582
583
        foreach ($map as $item) {
584
            foreach ($item as $match => $replace) {
585
                if ('' === $match) {
586
                    $match = '(all)';
587
                    $replace .= '/';
588
                }
589
590
                $logger->log(
591
                    BuildLogger::MINUS_PREFIX,
592
                    sprintf(
593
                        '%s <info>></info> %s',
594
                        $match,
595
                        $replace
596
                    )
597
                );
598
            }
599
        }
600
    }
601
}
602