Completed
Pull Request — master (#1)
by ANTHONIUS
07:07
created

CompileCommand::processFiles()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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