Completed
Push — master ( 01982b...3c7394 )
by ANTHONIUS
11s
created

CompileCommand   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 364
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 34
dl 0
loc 364
rs 9.2
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A processFiles() 0 12 2
A compile() 0 8 2
A getVersionDate() 0 3 1
A addFile() 0 16 4
A getRelativeFilePath() 0 8 2
B setupVersion() 0 25 5
A execute() 0 18 1
A getBranchAliasVersion() 0 3 1
A addDotfilesBin() 0 5 1
A registerFiles() 0 5 3
B getStub() 0 37 2
A configure() 0 6 1
A generatePhar() 0 71 1
B stripWhitespace() 0 26 6
A generateVersionFile() 0 18 1
A getVersion() 0 3 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the dotfiles project.
7
 *
8
 *     (c) Anthonius Munthi <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Dotfiles\Core\Command;
15
16
use Seld\PharUtils\Timestamps;
17
use Symfony\Component\Console\Helper\ProgressBar;
18
use Symfony\Component\Console\Input\InputArgument;
19
use Symfony\Component\Console\Input\InputInterface;
20
use Symfony\Component\Console\Output\OutputInterface;
21
use Symfony\Component\Finder\Finder;
22
use Symfony\Component\Process\Process;
23
24
/**
25
 * Class CompileCommand.
26
 *
27
 * @codeCoverageIgnore
28
 */
29
class CompileCommand extends Command
30
{
31
    /**
32
     * @var string
33
     */
34
    private $baseDir;
35
36
    /**
37
     * @var string
38
     */
39
    private $branchAliasVersion = '';
40
41
    /**
42
     * @var array
43
     */
44
    private $files = array();
45
46
    /**
47
     * @var OutputInterface
48
     */
49
    private $output;
50
51
    /**
52
     * @var string
53
     */
54
    private $version;
55
56
    /**
57
     * @var \DateTime
58
     */
59
    private $versionDate;
60
61
    public function compile($pharFile = 'dotfiles.phar'): void
62
    {
63
        if (file_exists($pharFile)) {
64
            unlink($pharFile);
65
        }
66
67
        $this->setupVersion();
68
        $this->generatePhar($pharFile);
69
    }
70
71
    /**
72
     * @return string
73
     */
74
    public function getBranchAliasVersion(): string
75
    {
76
        return $this->branchAliasVersion;
77
    }
78
79
    /**
80
     * @return string
81
     */
82
    public function getVersion(): string
83
    {
84
        return $this->version;
85
    }
86
87
    /**
88
     * @return \DateTime
89
     */
90
    public function getVersionDate(): \DateTime
91
    {
92
        return $this->versionDate;
93
    }
94
95
    protected function configure(): void
96
    {
97
        $this
98
            ->setName('compile')
99
            ->setDescription('generate new dotfiles.phar')
100
            ->addArgument('target', InputArgument::OPTIONAL, 'Compile dotfiles.phar into this directory', getcwd().'/build')
101
        ;
102
    }
103
104
    protected function execute(InputInterface $input, OutputInterface $output): void
105
    {
106
        $cwd = getcwd();
107
        chdir(dirname(__DIR__.'/../../../../'));
108
109
        $this->baseDir = getcwd();
110
        $this->output = $output;
111
112
        // start compiling process
113
        $targetDir = realpath($input->getArgument('target'));
114
        $target = $targetDir.'/dotfiles.phar';
115
        $this->compile($target);
116
        $this->generateVersionFile($targetDir);
117
        chmod($target, 0755);
118
119
        chdir($cwd);
120
121
        $output->writeln("Completed! dotfiles.phar generated in <comment>$target</comment>");
122
    }
123
124
    private function addDotfilesBin($phar): void
125
    {
126
        $content = file_get_contents($this->baseDir.'/bin/dotfiles');
127
        $content = preg_replace('{^#!/usr/bin/env php\s*}', '', $content);
128
        $phar->addFromString('bin/dotfiles', $content);
129
    }
130
131
    /**
132
     * @param $phar
133
     * @param \SplFileInfo $file
134
     * @param bool         $strip
135
     */
136
    private function addFile($phar, \SplFileInfo $file, $strip = true): void
137
    {
138
        $path = $this->getRelativeFilePath($file);
139
        $content = file_get_contents($file->getRealPath());
140
        if ($strip) {
141
            $content = $this->stripWhitespace($content);
142
        } elseif ('LICENSE' === basename($file)) {
143
            $content = "\n".$content."\n";
144
        }
145
146
        if ('src/Core/Application.php' === $path) {
147
            $content = str_replace('@package_version@', $this->version, $content);
148
            $content = str_replace('@package_branch_alias_version@', $this->branchAliasVersion, $content);
149
            $content = str_replace('@release_date@', $this->versionDate->format('Y-m-d H:i:s'), $content);
150
        }
151
        $phar->addFromString($path, $content);
152
    }
153
154
    private function generatePhar($pharFile = 'dotfiles.phar'): void
155
    {
156
        $finderSort = function ($a, $b) {
157
            return strcmp(strtr($a->getRealPath(), '\\', '/'), strtr($b->getRealPath(), '\\', '/'));
158
        };
159
160
        $this->output->writeln("Start registering files in <comment>{$this->baseDir}</comment>");
161
        $finder = new Finder();
162
        $finder->files()
163
            ->ignoreVCS(true)
164
            ->ignoreDotFiles(false)
165
            ->exclude(array(
166
                'Tests',
167
            ))
168
            ->notName('Compiler.php')
169
            ->notName('SubsplitCommand.php')
170
            ->notName('CompilerCommand.php')
171
            ->in($this->baseDir.'/src/Core')
172
            ->sort($finderSort)
173
        ;
174
        $this->registerFiles($finder);
175
176
        $finder = new Finder();
177
        $finder->files()
178
            ->ignoreVCS(true)
179
            ->name('*.php')
180
            ->name('LICENSE')
181
            ->exclude('Tests')
182
            ->exclude('tests')
183
            ->exclude('docs')
184
            ->in($this->baseDir.'/vendor/symfony')
185
            ->in($this->baseDir.'/vendor/composer')
186
            ->in($this->baseDir.'/vendor/myclabs')
187
            ->in($this->baseDir.'/vendor/psr')
188
            ->in($this->baseDir.'/vendor/monolog')
189
            ->sort($finderSort)
190
        ;
191
        $this->registerFiles($finder);
192
193
        $finder->files()
194
            ->ignoreVCS(true)
195
            ->exclude('Tests')
196
            ->exclude('tests')
197
            ->exclude('docs')
198
            ->name('*.yaml')
199
            ->name('*.yml')
200
            ->name('*.php')
201
            ->in($this->baseDir.'/src/Plugins')
202
            ->sort($finderSort)
203
        ;
204
205
        $this->registerFiles($finder);
206
        $this->files[] = new \SplFileInfo($this->baseDir.'/vendor/autoload.php');
207
208
        $phar = new \Phar($pharFile, 0, 'dotfiles.phar');
209
        $phar->setSignatureAlgorithm(\Phar::SHA1);
210
        $phar->startBuffering();
211
212
        $count = count($this->files);
213
        $this->output->writeln("Start processing <comment>{$count} files</comment>");
214
        $this->processFiles($phar);
215
        $this->output->writeln('');
216
        $this->addDotfilesBin($phar);
217
        $phar->setStub($this->getStub());
218
        $phar->stopBuffering();
219
220
        unset($phar);
221
222
        $util = new Timestamps($pharFile);
223
        $util->updateTimestamps($this->versionDate);
224
        $util->save($pharFile, \Phar::SHA1);
225
    }
226
227
    private function generateVersionFile($targetDir): void
228
    {
229
        $version = $this->version;
230
        $branchAlias = $this->branchAliasVersion;
231
        $date = $this->versionDate->format('Y-m-d H:i:s');
232
        $sha256 = trim(shell_exec('sha256sum '.$targetDir.'/dotfiles.phar'));
233
        $sha256 = trim(str_replace($targetDir.'/dotfiles.phar', '', $sha256));
234
235
        $contents = <<<EOC
236
{
237
    "version": "${version}",
238
    "branch": "${branchAlias}",
239
    "date": "${date}",
240
    "sha256": "${sha256}"
241
}
242
243
EOC;
244
        file_put_contents($targetDir.'/dotfiles.phar.json', $contents, LOCK_EX);
245
    }
246
247
    /**
248
     * @param \SplFileInfo $file
249
     *
250
     * @return string
251
     */
252
    private function getRelativeFilePath($file)
253
    {
254
        $realPath = $file->getRealPath();
255
        $pathPrefix = $this->baseDir.'/';
256
        $pos = strpos($realPath, $pathPrefix);
257
        $relativePath = (false !== $pos) ? substr_replace($realPath, '', $pos, strlen($pathPrefix)) : $realPath;
258
259
        return strtr($relativePath, '\\', '/');
260
    }
261
262
    private function getStub()
263
    {
264
        $stub = <<<'EOF'
265
#!/usr/bin/env php
266
<?php
267
/*
268
 * This file is part of dotfiles project.
269
 *
270
 * (c) Anthonius Munthi <[email protected]>
271
 *
272
 * For the full copyright and license information, please view
273
 * the license that is located at the bottom of this file.
274
 */
275
276
// Avoid APC causing random fatal errors per https://github.com/composer/composer/issues/264
277
if (extension_loaded('apc') && ini_get('apc.enable_cli') && ini_get('apc.cache_by_default')) {
278
    if (version_compare(phpversion('apc'), '3.0.12', '>=')) {
279
        ini_set('apc.cache_by_default', 0);
280
    } else {
281
        fwrite(STDERR, 'Warning: APC <= 3.0.12 may cause fatal errors when running composer commands.'.PHP_EOL);
282
        fwrite(STDERR, 'Update APC, or set apc.enable_cli or apc.cache_by_default to 0 in your php.ini.'.PHP_EOL);
283
    }
284
}
285
286
define('DOTFILES_PHAR_MODE', true);
287
288
Phar::mapPhar('dotfiles.phar');
289
290
EOF;
291
292
        // add warning once the phar is older than 60 days
293
        if (preg_match('{^[a-f0-9]+$}', $this->version)) {
294
            $warningTime = $this->versionDate->format('U') + 60 * 86400;
295
            $stub .= "define('COMPOSER_DEV_WARNING_TIME', $warningTime);\n";
296
        }
297
298
        return $stub.<<<'EOF'
299
require 'phar://dotfiles.phar/bin/dotfiles';
300
301
__HALT_COMPILER();
302
EOF;
303
    }
304
305
    private function processFiles($phar): void
306
    {
307
        $files = $this->files;
308
        $progressBar = new ProgressBar($this->output, count($files));
309
310
        $progressBar->start();
311
        foreach ($files as $key => $file) {
312
            $this->addFile($phar, $file);
313
            $progressBar->advance();
314
        }
315
316
        $progressBar->finish();
317
    }
318
319
    /**
320
     * @param Finder $finder
321
     */
322
    private function registerFiles(Finder $finder): void
323
    {
324
        foreach ($finder as $file) {
325
            if (!in_array($file, $this->files)) {
326
                $this->files[] = $file;
327
            }
328
        }
329
    }
330
331
    private function setupVersion(): void
332
    {
333
        $process = new Process('git log --pretty="%H" -n1 HEAD', __DIR__);
334
        if (0 != $process->run()) {
335
            throw new \RuntimeException('Can\'t run git log. You must ensure to run compile from composer git repository clone and that git binary is available.');
336
        }
337
        $this->version = trim($process->getOutput());
338
339
        $process = new Process('git log -n1 --pretty=%ci HEAD', __DIR__);
340
        if (0 != $process->run()) {
341
            throw new \RuntimeException('Can\'t run git log. You must ensure to run compile from composer git repository clone and that git binary is available.');
342
        }
343
344
        $this->versionDate = new \DateTime(trim($process->getOutput()));
345
        $this->versionDate->setTimezone(new \DateTimeZone('UTC'));
346
        $process = new Process('git describe --tags --exact-match HEAD');
347
        if (0 == $process->run()) {
348
            $this->version = trim($process->getOutput());
349
        } else {
350
            // get branch-alias defined in composer.json for dev-master (if any)
351
            $localConfig = getcwd().'/composer.json';
352
            $contents = file_get_contents($localConfig);
353
            $json = json_decode($contents, true);
354
            if (isset($json['extra']['branch-alias']['dev-master'])) {
355
                $this->branchAliasVersion = $json['extra']['branch-alias']['dev-master'];
356
            }
357
        }
358
    }
359
360
    /**
361
     * Removes whitespace from a PHP source string while preserving line numbers.
362
     *
363
     * @param string $source A PHP string
364
     *
365
     * @return string The PHP string with the whitespace removed
366
     */
367
    private function stripWhitespace($source)
368
    {
369
        if (!function_exists('token_get_all')) {
370
            return $source;
371
        }
372
373
        $output = '';
374
        foreach (token_get_all($source) as $token) {
375
            if (is_string($token)) {
376
                $output .= $token;
377
            } elseif (in_array($token[0], array(T_COMMENT, T_DOC_COMMENT))) {
378
                $output .= str_repeat("\n", substr_count($token[1], "\n"));
379
            } elseif (T_WHITESPACE === $token[0]) {
380
                // reduce wide spaces
381
                $whitespace = preg_replace('{[ \t]+}', ' ', $token[1]);
382
                // normalize newlines to \n
383
                $whitespace = preg_replace('{(?:\r\n|\r|\n)}', "\n", $whitespace);
384
                // trim leading spaces
385
                $whitespace = preg_replace('{\n +}', "\n", $whitespace);
386
                $output .= $whitespace;
387
            } else {
388
                $output .= $token[1];
389
            }
390
        }
391
392
        return $output;
393
    }
394
}
395