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

Compile::execute()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 37
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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

389
                    /** @scrutinizer ignore-type */ array_search($algorithm, get_phar_compression_algorithms(), true)
Loading history...
390
                )
391
            );
392
393
            $box->getPhar()->compressFiles($algorithm);
394
        } else {
395
            $logger->log(
396
                BuildLogger::QUESTION_MARK_PREFIX,
397
                '<error>No compression</error>'
398
            );
399
        }
400
    }
401
402
    private function signPhar(
403
        Configuration $config,
404
        Box $box,
405
        string $path,
406
        InputInterface $input,
407
        OutputInterface $output,
408
        BuildLogger $logger
409
    ): void {
410
        // sign using private key, if applicable
411
        //TODO: check that out
412
        remove($path.'.pubkey');
413
414
        $key = $config->getPrivateKeyPath();
415
416
        if (null === $key) {
417
            if (null !== ($algorithm = $config->getSigningAlgorithm())) {
0 ignored issues
show
introduced by
The condition null !== $algorithm = $c...->getSigningAlgorithm() can never be false.
Loading history...
418
                $box->getPhar()->setSignatureAlgorithm($algorithm);
419
            }
420
421
            return;
422
        }
423
424
        $logger->log(
425
            BuildLogger::QUESTION_MARK_PREFIX,
426
            'Signing using a private key'
427
        );
428
429
        $passphrase = $config->getPrivateKeyPassphrase();
430
431
        if ($config->isPrivateKeyPrompt()) {
432
            if (false === $input->isInteractive()) {
433
                throw new RuntimeException(
434
                    sprintf(
435
                        'Accessing to the private key "%s" requires a passphrase but none provided. Either '
436
                        .'provide one or run this command in interactive mode.',
437
                        $key
438
                    )
439
                );
440
            }
441
442
            /** @var $dialog QuestionHelper */
443
            $dialog = $this->getHelper('question');
444
445
            $question = new Question('Private key passphrase:');
446
            $question->setHidden(false);
447
            $question->setHiddenFallback(false);
448
449
            $passphrase = $dialog->ask($input, $output, $question);
450
451
            $output->writeln('');
452
        }
453
454
        $box->signUsingFile($key, $passphrase);
455
    }
456
457
    private function correctPermissions(string $path, Configuration $config, BuildLogger $logger): void
458
    {
459
        if (null !== ($chmod = $config->getFileMode())) {
460
            $logger->log(
461
                BuildLogger::QUESTION_MARK_PREFIX,
462
                "Setting file permissions to <comment>$chmod</comment>"
463
            );
464
465
            chmod($path, $chmod);
466
        }
467
    }
468
469
    private function createStub(Configuration $config, ?string $main, BuildLogger $logger): StubGenerator
470
    {
471
        $stub = StubGenerator::create()
472
            ->alias($config->getAlias())
473
            ->index($main)
474
            ->intercept($config->isInterceptFileFuncs())
475
        ;
476
477
        if (null !== ($shebang = $config->getShebang())) {
478
            $logger->log(
479
                BuildLogger::MINUS_PREFIX,
480
                sprintf(
481
                    'Using shebang line: %s',
482
                    $shebang
483
                )
484
            );
485
486
            $stub->shebang($shebang);
487
        } else {
488
            $logger->log(
489
                BuildLogger::MINUS_PREFIX,
490
                'No shebang line'
491
            );
492
        }
493
494
        if (null !== ($bannerPath = $config->getStubBannerPath())) {
495
            $logger->log(
496
                BuildLogger::MINUS_PREFIX,
497
                sprintf(
498
                    'Using custom banner from file: %s',
499
                    $bannerPath
500
                )
501
            );
502
503
            $stub->banner($config->getStubBannerContents());
504
        } elseif (null !== ($banner = $config->getStubBannerContents())) {
505
            $logger->log(
506
                BuildLogger::MINUS_PREFIX,
507
                'Using banner:'
508
            );
509
510
            $bannerLines = explode("\n", $banner);
511
512
            foreach ($bannerLines as $bannerLine) {
513
                $logger->log(
514
                    BuildLogger::CHEVRON_PREFIX,
515
                    $bannerLine
516
                );
517
            }
518
519
            $stub->banner($banner);
520
        }
521
522
        return $stub;
523
    }
524
525
    private function logMap(MapFile $fileMapper, BuildLogger $logger): void
526
    {
527
        $map = $fileMapper->getMap();
528
529
        if ([] === $map) {
530
            return;
531
        }
532
533
        $logger->log(
534
            BuildLogger::QUESTION_MARK_PREFIX,
535
            'Mapping paths'
536
        );
537
538
        foreach ($map as $item) {
539
            foreach ($item as $match => $replace) {
540
                if ('' === $match) {
541
                    $match = '(all)';
542
                    $replace .= '/';
543
                }
544
545
                $logger->log(
546
                    BuildLogger::MINUS_PREFIX,
547
                    sprintf(
548
                        '%s <info>></info> %s',
549
                        $match,
550
                        $replace
551
                    )
552
                );
553
            }
554
        }
555
    }
556
}
557