Completed
Pull Request — master (#42)
by Théo
02:10
created

Build::execute()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 45
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 45
rs 8.8571
c 0
b 0
f 0
cc 3
eloc 26
nc 4
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\Command;
16
17
use Assert\Assertion;
18
use KevinGH\Box\Box;
19
use KevinGH\Box\Compactor;
20
use KevinGH\Box\Configuration;
21
use KevinGH\Box\Logger\BuildLogger;
22
use KevinGH\Box\MapFile;
23
use KevinGH\Box\StubGenerator;
24
use RuntimeException;
25
use Symfony\Component\Console\Helper\QuestionHelper;
26
use Symfony\Component\Console\Input\InputInterface;
27
use Symfony\Component\Console\Output\OutputInterface;
28
use Symfony\Component\Console\Question\Question;
29
use Symfony\Component\Console\Style\SymfonyStyle;
30
use Symfony\Component\Filesystem\Filesystem;
31
use function KevinGH\Box\formatted_filesize;
32
use function KevinGH\Box\get_phar_compression_algorithms;
33
34
final class Build extends Configurable
35
{
36
    use ChangeableWorkingDirectory;
37
38
    private const HELP = <<<'HELP'
39
The <info>%command.name%</info> command will build a new PHAR based on a variety of settings.
40
<comment>
41
  This command relies on a configuration file for loading
42
  PHAR packaging settings. If a configuration file is not
43
  specified through the <info>--config|-c</info> option, one of
44
  the following files will be used (in order): <info>box.json</info>,
45
  <info>box.json.dist</info>
46
</comment>
47
The configuration file is actually a JSON object saved to a file. For more
48
information check the documentation online:
49
<comment>
50
  https://github.com/humbug/box
51
</comment>
52
HELP;
53
54
    /**
55
     * {@inheritdoc}
56
     */
57
    protected function configure(): void
58
    {
59
        parent::configure();
60
61
        $this->setName('build');
62
        $this->setDescription('Builds a new PHAR');
63
        $this->setHelp(self::HELP);
64
65
        $this->configureWorkingDirOption();
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71
    protected function execute(InputInterface $input, OutputInterface $output): void
72
    {
73
        $this->changeWorkingDirectory($input);
74
75
        $io = new SymfonyStyle($input, $output);
76
77
        $io->writeln($this->getApplication()->getHelp());
78
        $io->writeln('');
79
80
        $config = $this->getConfig($input);
81
        $path = $config->getOutputPath();
82
83
        $logger = new BuildLogger($io);
84
85
        $startTime = microtime(true);
86
87
        $this->loadBootstrapFile($config, $logger);
88
        $this->removeExistingPhar($config, $logger);
89
90
        $logger->logStartBuilding($path);
91
92
        $this->createPhar($path, $config, $input, $output, $logger);
93
94
        $this->correctPermissions($path, $config, $logger);
95
96
        $logger->log(
97
            BuildLogger::STAR_PREFIX,
98
            'Done.'
99
        );
100
101
        if ($io->getVerbosity() >= OutputInterface::VERBOSITY_NORMAL) {
102
            $io->comment(
103
                sprintf(
104
                    "<info>Size: %s\nMemory usage: %.2fMB (peak: %.2fMB), time: %.2fs<info>",
105
                    formatted_filesize($path),
106
                    round(memory_get_usage() / 1024 / 1024, 2),
107
                    round(memory_get_peak_usage() / 1024 / 1024, 2),
108
                    round(microtime(true) - $startTime, 2)
109
                )
110
            );
111
        }
112
113
        if (false === file_exists($path)) {
114
            //TODO: check that one
115
            $io->warning('The archive was not generated because it did not have any contents');
116
        }
117
    }
118
119
    private function createPhar(
120
        string $path,
121
        Configuration $config,
122
        InputInterface $input,
123
        OutputInterface $output,
124
        BuildLogger $logger
125
    ): void {
126
        $box = Box::create($path);
127
128
        $box->getPhar()->startBuffering();
129
130
        $this->setReplacementValues($config, $box, $logger);
131
        $this->registerCompactors($config, $box, $logger);
132
        $this->registerFileMapping($config, $box, $logger);
133
134
        $this->addFiles($config, $box, $logger);
135
136
        $main = $this->registerMainScript($config, $box, $logger);
137
138
        $this->registerStub($config, $box, $main, $logger);
139
        $this->configureMetadata($config, $box, $logger);
140
        $this->configureCompressionAlgorithm($config, $box, $logger);
141
142
        $box->getPhar()->stopBuffering();
143
144
        $this->signPhar($config, $box, $path, $input, $output, $logger);
145
    }
146
147
    private function loadBootstrapFile(Configuration $config, BuildLogger $logger): void
148
    {
149
        $file = $config->getBootstrapFile();
150
151
        if (null === $file) {
152
            return;
153
        }
154
155
        $logger->log(
156
            BuildLogger::QUESTION_MARK_PREFIX,
157
            sprintf(
158
                'Loading the bootstrap file "%s"',
159
                $file
160
            )
161
        );
162
163
        $config->loadBootstrap();
164
    }
165
166
    private function removeExistingPhar(Configuration $config, BuildLogger $logger): void
167
    {
168
        $path = $config->getOutputPath();
169
170
        if (false === file_exists($path)) {
171
            return;
172
        }
173
174
        $logger->log(
175
            BuildLogger::QUESTION_MARK_PREFIX,
176
            sprintf(
177
                'Removing the existing PHAR "%s"',
178
                $path
179
            )
180
        );
181
182
        (new Filesystem())->remove($path);
183
    }
184
185
    private function setReplacementValues(Configuration $config, Box $box, BuildLogger $logger): void
186
    {
187
        $values = $config->getProcessedReplacements();
188
189
        if ([] === $values) {
190
            return;
191
        }
192
193
        $logger->log(
194
            BuildLogger::QUESTION_MARK_PREFIX,
195
            'Setting replacement values'
196
        );
197
198
        foreach ($values as $key => $value) {
199
            $logger->log(
200
                BuildLogger::PLUS_PREFIX,
201
                sprintf(
202
                    '%s: %s',
203
                    $key,
204
                    $value
205
                )
206
            );
207
        }
208
209
        $box->registerPlaceholders($values);
210
    }
211
212
    private function registerCompactors(Configuration $config, Box $box, BuildLogger $logger): void
213
    {
214
        $compactors = $config->getCompactors();
215
216
        if ([] === $compactors) {
217
            $logger->log(
218
                BuildLogger::QUESTION_MARK_PREFIX,
219
                'No compactor to register'
220
            );
221
222
            return;
223
        }
224
225
        $logger->log(
226
            BuildLogger::QUESTION_MARK_PREFIX,
227
            'Registering compactors'
228
        );
229
230
        $logCompactors = function (Compactor $compactor) use ($logger): void {
231
            $logger->log(
232
                BuildLogger::PLUS_PREFIX,
233
                get_class($compactor)
234
            );
235
        };
236
237
        array_map($logCompactors, $compactors);
238
239
        $box->registerCompactors($compactors);
240
    }
241
242
    private function registerFileMapping(Configuration $config, Box $box, BuildLogger $logger): void
243
    {
244
        $fileMapper = $config->getFileMapper();
245
246
        $this->logMap($fileMapper, $logger);
247
248
        $box->registerFileMapping(
249
            $config->getBasePathRetriever(),
250
            $fileMapper
251
        );
252
    }
253
254
    private function addFiles(Configuration $config, Box $box, BuildLogger $logger): void
255
    {
256
        $logger->log(BuildLogger::QUESTION_MARK_PREFIX, 'Adding binary files');
257
258
        $count = 0;
259
260
        foreach ($config->getBinaryFiles() as $file) {
261
            ++$count;
262
            $box->addFile((string) $file, null, true);
263
        }
264
265
        $logger->log(
266
            BuildLogger::CHEVRON_PREFIX,
267
            0 === $count
268
                ? 'No file found'
269
                : sprintf('%d file(s)', $count)
270
        );
271
272
        $logger->log(BuildLogger::QUESTION_MARK_PREFIX, 'Adding files');
273
274
        $count = 0;
275
276
        foreach ($config->getFiles() as $file) {
277
            ++$count;
278
            $box->addFile((string) $file);
279
        }
280
281
        $logger->log(
282
            BuildLogger::CHEVRON_PREFIX,
283
            0 === $count
284
                ? 'No file found'
285
                : sprintf('%d file(s)', $count)
286
        );
287
    }
288
289
    private function registerMainScript(Configuration $config, Box $box, BuildLogger $logger): ?string
290
    {
291
        $main = $config->getMainScriptPath();
292
293
        if (null === $main) {
294
            return null;
295
        }
296
297
        $logger->log(
298
            BuildLogger::QUESTION_MARK_PREFIX,
299
            sprintf(
300
                'Adding main file: %s',
301
                $config->getBasePath().DIRECTORY_SEPARATOR.$main
302
            )
303
        );
304
305
        $localMain = $box->addFile(
306
            $main,
307
            $config->getMainScriptContent()
308
        );
309
310
        if ($localMain !== $main) {
311
            $logger->log(
312
                BuildLogger::CHEVRON_PREFIX,
313
                $localMain
314
            );
315
        }
316
317
        return $localMain;
318
    }
319
320
    private function registerStub(Configuration $config, Box $box, ?string $main, BuildLogger $logger): void
321
    {
322
        if (true === $config->isStubGenerated()) {
323
            $logger->log(
324
                BuildLogger::QUESTION_MARK_PREFIX,
325
                'Generating new stub'
326
            );
327
328
            $stub = $this->createStub($config, $main, $logger);
329
330
            $box->getPhar()->setStub($stub->generate());
331
        } elseif (null !== ($stub = $config->getStubPath())) {
332
            $stub = $config->getBasePath().DIRECTORY_SEPARATOR.$stub;
333
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
            $aliasWasAdded = $box->getPhar()->setAlias($config->getAlias());
345
346
            Assertion::true(
347
                $aliasWasAdded,
348
                sprintf(
349
                    'The alias "%s" is invalid. See Phar::setAlias() documentation for more information.',
350
                    $config->getAlias()
351
                )
352
            );
353
354
            if (null !== $main) {
355
                $box->getPhar()->setDefaultStub($main, $main);
356
            }
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)
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
        if (file_exists($path.'.pubkey')) {
413
            unlink($path.'.pubkey');
414
        }
415
416
        $key = $config->getPrivateKeyPath();
417
418
        if (null === $key) {
419
            if (null !== ($algorithm = $config->getSigningAlgorithm())) {
420
                $box->getPhar()->setSignatureAlgorithm($algorithm);
421
            }
422
423
            return;
424
        }
425
426
        $logger->log(
427
            BuildLogger::QUESTION_MARK_PREFIX,
428
            'Signing using a private key'
429
        );
430
431
        $passphrase = $config->getPrivateKeyPassphrase();
432
433
        if ($config->isPrivateKeyPrompt()) {
434
            if (false === $input->isInteractive()) {
435
                throw new RuntimeException(
436
                    sprintf(
437
                        'Accessing to the private key "%s" requires a passphrase but none provided. Either '
438
                        .'provide one or run this command in interactive mode.',
439
                        $key
440
                    )
441
                );
442
            }
443
444
            /** @var $dialog QuestionHelper */
445
            $dialog = $this->getHelper('question');
446
447
            $question = new Question('Private key passphrase:');
448
            $question->setHidden(false);
449
            $question->setHiddenFallback(false);
450
451
            $passphrase = $dialog->ask($input, $output, $question);
452
453
            $output->writeln('');
454
        }
455
456
        $box->signUsingFile($key, $passphrase);
457
    }
458
459
    private function correctPermissions(string $path, Configuration $config, BuildLogger $logger): void
460
    {
461
        if (null !== ($chmod = $config->getFileMode())) {
462
            $logger->log(
463
                BuildLogger::QUESTION_MARK_PREFIX,
464
                "Setting file permissions to <comment>$chmod</comment>"
465
            );
466
467
            chmod($path, $chmod);
468
        }
469
    }
470
471
    private function createStub(Configuration $config, ?string $main, BuildLogger $logger): StubGenerator
472
    {
473
        $stub = StubGenerator::create()
474
            ->alias($config->getAlias())
475
            ->extract($config->isExtractable())
476
            ->index($main)
477
            ->intercept($config->isInterceptFileFuncs())
478
            ->mimetypes($config->getMimetypeMapping())
479
            ->mung($config->getMungVariables())
480
            ->notFound($config->getNotFoundScriptPath())
481
            ->web($config->isWebPhar())
482
        ;
483
484
        if (null !== ($shebang = $config->getShebang())) {
485
            $logger->log(
486
                BuildLogger::MINUS_PREFIX,
487
                sprintf(
488
                    'Using custom shebang line: %s',
489
                    $shebang
490
                ),
491
                OutputInterface::VERBOSITY_VERY_VERBOSE
492
            );
493
494
            $stub->shebang($shebang);
495
        }
496
497
        if (null !== ($banner = $config->getStubBanner())) {
498
            $logger->log(
499
                BuildLogger::MINUS_PREFIX,
500
                sprintf(
501
                    'Using custom banner: %s',
502
                    $banner
503
                ),
504
                OutputInterface::VERBOSITY_VERY_VERBOSE
505
            );
506
507
            $stub->banner($banner);
508
        } elseif (null !== ($banner = $config->getStubBannerFromFile())) {
509
            $logger->log(
510
                BuildLogger::MINUS_PREFIX,
511
                sprintf(
512
                    'Using custom banner from file: %s',
513
                    $config->getBasePath().DIRECTORY_SEPARATOR.$config->getStubBannerPath()
514
                ),
515
                OutputInterface::VERBOSITY_VERY_VERBOSE
516
            );
517
518
            $stub->banner($banner);
519
        }
520
521
        return $stub;
522
    }
523
524
    private function logMap(MapFile $fileMapper, BuildLogger $logger): void
525
    {
526
        $map = $fileMapper->getMap();
527
528
        if ([] === $map) {
529
            return;
530
        }
531
532
        $logger->log(
533
            BuildLogger::QUESTION_MARK_PREFIX,
534
            'Mapping paths'
535
        );
536
537
        foreach ($map as $item) {
538
            foreach ($item as $match => $replace) {
539
                if (empty($match)) {
540
                    $match = '(all)';
541
                }
542
543
                $logger->log(
544
                    BuildLogger::MINUS_PREFIX,
545
                    sprintf(
546
                        '%s <info>></info> %s',
547
                        $match,
548
                        $replace
549
                    )
550
                );
551
            }
552
        }
553
    }
554
}
555