Passed
Pull Request — master (#46)
by Théo
02:11
created

Build::setReplacementValues()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 8.8571
c 0
b 0
f 0
cc 3
eloc 14
nc 3
nop 3
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 Assert\Assertion;
18
use KevinGH\Box\Box;
19
use KevinGH\Box\Compactor;
20
use KevinGH\Box\Configuration;
21
use KevinGH\Box\Console\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 = count($config->getBinaryFiles());
259
260
        $box->addFiles($config->getBinaryFiles(), true);
261
262
        $logger->log(
263
            BuildLogger::CHEVRON_PREFIX,
264
            0 === $count
265
                ? 'No file found'
266
                : sprintf('%d file(s)', $count)
267
        );
268
269
        $logger->log(BuildLogger::QUESTION_MARK_PREFIX, 'Adding files');
270
271
        $count = count($config->getFiles());
272
273
        $box->addFiles($config->getFiles(), false);
274
275
        $logger->log(
276
            BuildLogger::CHEVRON_PREFIX,
277
            0 === $count
278
                ? 'No file found'
279
                : sprintf('%d file(s)', $count)
280
        );
281
    }
282
283
    private function registerMainScript(Configuration $config, Box $box, BuildLogger $logger): ?string
284
    {
285
        $main = $config->getMainScriptPath();
286
287
        if (null === $main) {
288
            return null;
289
        }
290
291
        $logger->log(
292
            BuildLogger::QUESTION_MARK_PREFIX,
293
            sprintf(
294
                'Adding main file: %s',
295
                $config->getBasePath().DIRECTORY_SEPARATOR.$main
296
            )
297
        );
298
299
        $localMain = $box->addFile(
300
            $main,
301
            $config->getMainScriptContent()
302
        );
303
304
        if ($localMain !== $main) {
305
            $logger->log(
306
                BuildLogger::CHEVRON_PREFIX,
307
                $localMain
308
            );
309
        }
310
311
        return $localMain;
312
    }
313
314
    private function registerStub(Configuration $config, Box $box, ?string $main, BuildLogger $logger): void
315
    {
316
        if (true === $config->isStubGenerated()) {
317
            $logger->log(
318
                BuildLogger::QUESTION_MARK_PREFIX,
319
                'Generating new stub'
320
            );
321
322
            $stub = $this->createStub($config, $main, $logger);
323
324
            $box->getPhar()->setStub($stub->generate());
325
        } elseif (null !== ($stub = $config->getStubPath())) {
326
            $stub = $config->getBasePath().DIRECTORY_SEPARATOR.$stub;
327
328
            $logger->log(
329
                BuildLogger::QUESTION_MARK_PREFIX,
330
                sprintf(
331
                    'Using stub file: %s',
332
                    $stub
333
                )
334
            );
335
336
            $box->registerStub($stub);
337
        } else {
338
            $aliasWasAdded = $box->getPhar()->setAlias($config->getAlias());
339
340
            Assertion::true(
341
                $aliasWasAdded,
342
                sprintf(
343
                    'The alias "%s" is invalid. See Phar::setAlias() documentation for more information.',
344
                    $config->getAlias()
345
                )
346
            );
347
348
            if (null !== $main) {
349
                $box->getPhar()->setDefaultStub($main, $main);
350
            }
351
352
            $logger->log(
353
                BuildLogger::QUESTION_MARK_PREFIX,
354
                'Using default stub'
355
            );
356
        }
357
    }
358
359
    private function configureMetadata(Configuration $config, Box $box, BuildLogger $logger): void
360
    {
361
        if (null !== ($metadata = $config->getMetadata())) {
362
            $logger->log(
363
                BuildLogger::QUESTION_MARK_PREFIX,
364
                'Setting metadata'
365
            );
366
367
            $logger->log(
368
                BuildLogger::MINUS_PREFIX,
369
                is_string($metadata) ? $metadata : var_export($metadata, true)
370
            );
371
372
            $box->getPhar()->setMetadata($metadata);
373
        }
374
    }
375
376
    private function configureCompressionAlgorithm(Configuration $config, Box $box, BuildLogger $logger): void
377
    {
378
        if (null !== ($algorithm = $config->getCompressionAlgorithm())) {
379
            $logger->log(
380
                BuildLogger::QUESTION_MARK_PREFIX,
381
                sprintf(
382
                    'Compressing with the algorithm "<comment>%s</comment>"',
383
                    array_search($algorithm, get_phar_compression_algorithms(), true)
384
                )
385
            );
386
387
            $box->getPhar()->compressFiles($algorithm);
388
        } else {
389
            $logger->log(
390
                BuildLogger::QUESTION_MARK_PREFIX,
391
                '<error>No compression</error>'
392
            );
393
        }
394
    }
395
396
    private function signPhar(
397
        Configuration $config,
398
        Box $box,
399
        string $path,
400
        InputInterface $input,
401
        OutputInterface $output,
402
        BuildLogger $logger
403
    ): void {
404
        // sign using private key, if applicable
405
        //TODO: check that out
406
        if (file_exists($path.'.pubkey')) {
407
            unlink($path.'.pubkey');
408
        }
409
410
        $key = $config->getPrivateKeyPath();
411
412
        if (null === $key) {
413
            if (null !== ($algorithm = $config->getSigningAlgorithm())) {
414
                $box->getPhar()->setSignatureAlgorithm($algorithm);
415
            }
416
417
            return;
418
        }
419
420
        $logger->log(
421
            BuildLogger::QUESTION_MARK_PREFIX,
422
            'Signing using a private key'
423
        );
424
425
        $passphrase = $config->getPrivateKeyPassphrase();
426
427
        if ($config->isPrivateKeyPrompt()) {
428
            if (false === $input->isInteractive()) {
429
                throw new RuntimeException(
430
                    sprintf(
431
                        'Accessing to the private key "%s" requires a passphrase but none provided. Either '
432
                        .'provide one or run this command in interactive mode.',
433
                        $key
434
                    )
435
                );
436
            }
437
438
            /** @var $dialog QuestionHelper */
439
            $dialog = $this->getHelper('question');
440
441
            $question = new Question('Private key passphrase:');
442
            $question->setHidden(false);
443
            $question->setHiddenFallback(false);
444
445
            $passphrase = $dialog->ask($input, $output, $question);
446
447
            $output->writeln('');
448
        }
449
450
        $box->signUsingFile($key, $passphrase);
451
    }
452
453
    private function correctPermissions(string $path, Configuration $config, BuildLogger $logger): void
454
    {
455
        if (null !== ($chmod = $config->getFileMode())) {
456
            $logger->log(
457
                BuildLogger::QUESTION_MARK_PREFIX,
458
                "Setting file permissions to <comment>$chmod</comment>"
459
            );
460
461
            chmod($path, $chmod);
462
        }
463
    }
464
465
    private function createStub(Configuration $config, ?string $main, BuildLogger $logger): StubGenerator
466
    {
467
        $stub = StubGenerator::create()
468
            ->alias($config->getAlias())
469
            ->extract($config->isExtractable())
470
            ->index($main)
471
            ->intercept($config->isInterceptFileFuncs())
472
            ->mimetypes($config->getMimetypeMapping())
473
            ->mung($config->getMungVariables())
474
            ->notFound($config->getNotFoundScriptPath())
475
            ->web($config->isWebPhar())
476
        ;
477
478
        if (null !== ($shebang = $config->getShebang())) {
479
            $logger->log(
480
                BuildLogger::MINUS_PREFIX,
481
                sprintf(
482
                    'Using custom shebang line: %s',
483
                    $shebang
484
                ),
485
                OutputInterface::VERBOSITY_VERY_VERBOSE
486
            );
487
488
            $stub->shebang($shebang);
489
        }
490
491
        if (null !== ($banner = $config->getStubBanner())) {
492
            $logger->log(
493
                BuildLogger::MINUS_PREFIX,
494
                sprintf(
495
                    'Using custom banner: %s',
496
                    $banner
497
                ),
498
                OutputInterface::VERBOSITY_VERY_VERBOSE
499
            );
500
501
            $stub->banner($banner);
502
        } elseif (null !== ($banner = $config->getStubBannerFromFile())) {
503
            $logger->log(
504
                BuildLogger::MINUS_PREFIX,
505
                sprintf(
506
                    'Using custom banner from file: %s',
507
                    $config->getBasePath().DIRECTORY_SEPARATOR.$config->getStubBannerPath()
508
                ),
509
                OutputInterface::VERBOSITY_VERY_VERBOSE
510
            );
511
512
            $stub->banner($banner);
513
        }
514
515
        return $stub;
516
    }
517
518
    private function logMap(MapFile $fileMapper, BuildLogger $logger): void
519
    {
520
        $map = $fileMapper->getMap();
521
522
        if ([] === $map) {
523
            return;
524
        }
525
526
        $logger->log(
527
            BuildLogger::QUESTION_MARK_PREFIX,
528
            'Mapping paths'
529
        );
530
531
        foreach ($map as $item) {
532
            foreach ($item as $match => $replace) {
533
                if (empty($match)) {
534
                    $match = '(all)';
535
                }
536
537
                $logger->log(
538
                    BuildLogger::MINUS_PREFIX,
539
                    sprintf(
540
                        '%s <info>></info> %s',
541
                        $match,
542
                        $replace
543
                    )
544
                );
545
            }
546
        }
547
    }
548
}
549