diff --git a/.gitignore b/.gitignore index d1ac00fd4afb..3574efd83127 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ /example /vendor composer.lock + +# phar related +/build +rector.phar diff --git a/.travis.yml b/.travis.yml index 23446dee3a59..b7184f6e8545 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: php matrix: include: - php: 7.1 - env: STATIC_ANALYSIS=true RUN_RECTOR=true + env: BUILD_PHAR=true PHPUNIT_FLAGS="--testsuite=main" STATIC_ANALYSIS=true - php: 7.2 env: PHPUNIT_FLAGS="--coverage-clover coverage.xml --testsuite=contrib-rectors" - php: 7.2 @@ -24,8 +24,13 @@ script: - phpenv config-rm xdebug.ini || return 0 - if [[ $STATIC_ANALYSIS != "" ]]; then composer check-cs; fi - if [[ $STATIC_ANALYSIS != "" ]]; then composer phpstan; fi - # Rector demo run - - if [[ $RUN_RECTOR != "" ]]; then bin/rector process src tests --level symfony40 --dry-run; fi + # build phar test - 1st group is "build-phar" test step by step; 2nd group is all-together + - | + if [[ $BUILD_PHAR != "" ]]; then + packages/PharBuilder/bin/compile + php rector.phar + vendor/bin/phpunit --testsuite=phar-build + fi after_script: # upload coverage to Coveralls.io diff --git a/README.md b/README.md index 029d0e463003..4d41768d3c11 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ vendor/bin/ecs check --config vendor/rector/rector/ecs-after-rector.neon --fix - [How to Create Rector with Fluent Builder](/docs/FluentBuilderRector.md) - [How to Create Own Rector](/docs/HowToCreateOwnRector.md) - [Service Name to Type Provider](/docs/ServiceNameToTypeProvider.md) +- [3 Steps to Build PHAR](/docs/BuildingRectorPhar.md) ## How to Contribute diff --git a/bin/subtree-split-master-and-last-tag.sh b/bin/subtree-split-master-and-last-tag.sh new file mode 100755 index 000000000000..c36c2d672164 --- /dev/null +++ b/bin/subtree-split-master-and-last-tag.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +git subsplit init git@github.com:rectorphp/rector.git + +LAST_TAG=$(git tag -l --sort=committerdate | tail -n1); + +git subsplit publish --heads="master" --tags=$LAST_TAG packages/NodeTypeResolver:git@github.com:rectorphp/node-type-resolver.git + +rm -rf .subsplit/ + +# inspired by laravel: https://github.com/laravel/framework/blob/5.4/build/illuminate-split-full.sh +# they use SensioLabs now though: https://github.com/laravel/framework/pull/17048#issuecomment-269915319 diff --git a/composer.json b/composer.json index 61aa85253d1b..a6e71edafc8f 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "phpdocumentor/type-resolver": "^0.4", "rector/better-reflection": "^3.0.6", "sebastian/diff": "^3.0", + "seld/phar-utils": "^1.0", "symfony/console": "^4.0", "symfony/dependency-injection": "^4.0", "symfony/finder": "^4.0", @@ -28,6 +29,9 @@ "phpstan/phpstan-shim": "^0.9", "phpunit/phpunit": "^7.0", "slam/php-cs-fixer-extensions": "^1.13", + "symfony/expression-language": "^4.0", + "symfony/form": "^4.0", + "symplify/coding-standard": "^3.2", "tracy/tracy": "^2.4" }, "autoload": { @@ -36,6 +40,7 @@ "Rector\\BetterReflection\\": "packages/BetterReflection/src", "Rector\\ConsoleDiffer\\": "packages/ConsoleDiffer/src", "Rector\\RectorBuilder\\": "packages/RectorBuilder/src", + "Rector\\PharBuilder\\": "packages/PharBuilder/src", "Rector\\ReflectionDocBlock\\": "packages/ReflectionDocBlock/src", "Rector\\NodeTypeResolver\\": "packages/NodeTypeResolver/src", "Rector\\NodeTraverserQueue\\": "packages/NodeTraverserQueue/src" diff --git a/docs/BuildingRectorPhar.md b/docs/BuildingRectorPhar.md new file mode 100644 index 000000000000..7fd20eae157d --- /dev/null +++ b/docs/BuildingRectorPhar.md @@ -0,0 +1,7 @@ +# How to Build `rector.phar` + +To build `rector.phar` just run: + +```bash +packages/PharBuilder/bin/compile +``` diff --git a/easy-coding-standard.neon b/easy-coding-standard.neon index dc4834cc43b3..c1e7b4386f9c 100644 --- a/easy-coding-standard.neon +++ b/easy-coding-standard.neon @@ -26,6 +26,9 @@ checkers: - 'Rector\BetterReflection\SourceLocator\Located\LocatedSource' - 'phpDocumentor\Reflection\Types\*' - 'Rector\Reporting\FileDiff' + - 'Phar' + - 'Symfony\Component\Finder\Finder' + - 'Seld\PharUtils\Timestamps' Symplify\CodingStandard\Fixer\Naming\PropertyNameMatchingTypeFixer: extra_skipped_classes: diff --git a/packages/PharBuilder/bin/compile b/packages/PharBuilder/bin/compile new file mode 100755 index 000000000000..a518eb695472 --- /dev/null +++ b/packages/PharBuilder/bin/compile @@ -0,0 +1,36 @@ +#!/usr/bin/env php +create(); + + // 3. Run Console Application + /** @var Application $application */ + $application = $container->get(Application::class); + /** @var InputInterface $input */ + $input = $container->get(InputInterface::class); + /** @var OutputInterface $output */ + $output = $container->get(OutputInterface::class); + $statusCode = $application->run($input, $output); + exit($statusCode); +} catch (Throwable $throwable) { + $symfonyStyle = SymfonyStyleFactory::create(); + $symfonyStyle->error(sprintf( + '%s in %s on line %d', + $throwable->getMessage(), + $throwable->getFile(), + $throwable->getLine() + )); + exit($throwable->getCode()); +} diff --git a/packages/PharBuilder/src/Command/CompileCommand.php b/packages/PharBuilder/src/Command/CompileCommand.php new file mode 100644 index 000000000000..cfdd7ed8b8eb --- /dev/null +++ b/packages/PharBuilder/src/Command/CompileCommand.php @@ -0,0 +1,34 @@ +compiler = $compiler; + + parent::__construct(); + } + + protected function configure(): void + { + $this->setName(CommandNaming::classToName(self::class)); + } + + protected function execute(InputInterface $input, OutputInterface $output): void + { + $this->compiler->compile(); + } +} diff --git a/packages/PharBuilder/src/Compiler/Compiler.php b/packages/PharBuilder/src/Compiler/Compiler.php new file mode 100644 index 000000000000..a052aabde379 --- /dev/null +++ b/packages/PharBuilder/src/Compiler/Compiler.php @@ -0,0 +1,165 @@ +pharName = $pharName; + $this->pharFilesFinder = $pharFilesFinder; + $this->binFileName = $binFileName; + $this->symfonyStyle = $symfonyStyle; + $this->pathNormalizer = $pathNormalizer; + $this->finderToPharAdder = $finderToPharAdder; + $this->buildDirectory = realpath($buildDirectory); + } + + public function compile(): void + { + $this->symfonyStyle->note(sprintf('Starting PHAR build in "%s" directory', $this->buildDirectory)); + + // flags: KEY_AS_PATHNAME - use relative paths from Finder keys + $phar = new Phar($this->pharName, FilesystemIterator::KEY_AS_PATHNAME, $this->pharName); + $phar->setSignatureAlgorithm(Phar::SHA1); + $phar->startBuffering(); + + // use only dev deps + rebuild dump autoload +// $this->symfonyStyle->note('Removing dev packages from composer'); +// $process = new Process('composer update --no-dev', $buildDirectory); +// $process->run(); + + // dump autoload +// $this->symfonyStyle->note('Dumping new composer autoload'); +// $process = new Process('composer dump-autoload --optimize', $buildDirectory); +// $process->run(); + + $finder = $this->pharFilesFinder->createForDirectory($this->buildDirectory); + + $fileCount = $this->getFileCountFromFinder($finder); + $this->symfonyStyle->note(sprintf('Adding %d files', $fileCount)); + $this->symfonyStyle->progressStart($fileCount); + + $this->finderToPharAdder->addFinderToPhar($finder, $phar); + + $this->symfonyStyle->newLine(2); + $this->symfonyStyle->note('Adding bin'); + $this->addRectorBin($phar, $this->buildDirectory); + + $this->symfonyStyle->note('Setting stub'); + $phar->setStub($this->getStub()); + $phar->stopBuffering(); + + $timestamps = new Timestamps($this->pharName); + $timestamps->save($this->pharName, Phar::SHA1); + + // return dev deps +// $this->symfonyStyle->note('Returning dev packages to composer'); +// $process = new Process('composer update', $buildDirectory); +// $process->run(); + + $this->symfonyStyle->success(sprintf('Phar file "%s" build successful!', $this->pharName)); + } + + private function addRectorBin(Phar $phar, string $buildDirectory): void + { + $binFilePath = $buildDirectory . DIRECTORY_SEPARATOR . $this->binFileName; + $this->ensureBinFileExists($binFilePath); + + $content = file_get_contents($binFilePath); + $content = $this->removeShebang($content); + + // replace absolute paths by phar:// paths + $content = $this->pathNormalizer->normalizeAbsoluteToPharInContent($content); + + $phar->addFromString($this->binFileName, $content); + } + + private function getStub(): string + { + $stubTemplate = <<<'EOF' +#!/usr/bin/env php +pharName, $this->pharName, $this->binFileName); + } + + private function removeShebang(string $content): string + { + return preg_replace('~^#!/usr/bin/env php\s*~', '', $content); + } + + private function getFileCountFromFinder(Finder $finder): int + { + return count(iterator_to_array($finder->getIterator())); + } + + private function ensureBinFileExists(string $binFilePath): void + { + if (file_exists($binFilePath)) { + return; + } + + throw new BinFileNotFoundException(sprintf( + 'Bin file not found in "%s". Have you set it up in config.yml file?', + $binFilePath + )); + } +} diff --git a/packages/PharBuilder/src/DependencyInjection/ContainerFactory.php b/packages/PharBuilder/src/DependencyInjection/ContainerFactory.php new file mode 100644 index 000000000000..217f040ea3c4 --- /dev/null +++ b/packages/PharBuilder/src/DependencyInjection/ContainerFactory.php @@ -0,0 +1,20 @@ +boot(); + + // this is require to keep CLI verbosity independent on AppKernel dev/prod mode + // see: https://github.com/symfony/symfony/blob/c12c07865a6fe3a8e0edd8540ac3ab9a3bc75543/src/Symfony/Component/HttpKernel/Kernel.php#L115 + putenv('SHELL_VERBOSITY=0'); + + return $appKernel->getContainer(); + } +} diff --git a/packages/PharBuilder/src/DependencyInjection/PharBuilderKernel.php b/packages/PharBuilder/src/DependencyInjection/PharBuilderKernel.php new file mode 100644 index 000000000000..2c3f290b95e1 --- /dev/null +++ b/packages/PharBuilder/src/DependencyInjection/PharBuilderKernel.php @@ -0,0 +1,39 @@ +load(__DIR__ . '/../config/config.yml'); + } + + public function getCacheDir(): string + { + return sys_get_temp_dir() . '/_rector_phar_builder_cache'; + } + + public function getLogDir(): string + { + return sys_get_temp_dir() . '/_rector_phar_bulider_log'; + } + + /** + * @return BundleInterface[] + */ + public function registerBundles(): array + { + return []; + } +} diff --git a/packages/PharBuilder/src/Exception/BinFileNotFoundException.php b/packages/PharBuilder/src/Exception/BinFileNotFoundException.php new file mode 100644 index 000000000000..5f307d6a35f1 --- /dev/null +++ b/packages/PharBuilder/src/Exception/BinFileNotFoundException.php @@ -0,0 +1,9 @@ +pharName = $pharName; + } + + public function normalizeAbsoluteToPharInContent(string $content): string + { + return preg_replace( + "#__DIR__\\s*\\.\\s*'\\/\\.\\.\\/#", + sprintf("'phar://%s/", $this->pharName), + $content + ); + } +} diff --git a/packages/PharBuilder/src/Filesystem/PharFilesFinder.php b/packages/PharBuilder/src/Filesystem/PharFilesFinder.php new file mode 100644 index 000000000000..40f8b7c2d80f --- /dev/null +++ b/packages/PharBuilder/src/Filesystem/PharFilesFinder.php @@ -0,0 +1,35 @@ +files() + ->ignoreVCS(true) + ->name('*.{yml,php}') + // "in()" and "path()" have to be split to make SplFileInfo "getRelativePathname()" get path from $directory + ->in($directory) + ->path('#(bin|src|packages)#') // |vendor + ->exclude( + ['tests', 'docs', 'Tests', 'Testing', 'phpunit', 'sebastianbergman', 'vendor', 'packages/PharBuilder'] + ) + ->sort($this->sortFilesByName()); + } + + private function sortFilesByName(): Closure + { + return function (SplFileInfo $firstFileInfo, SplFileInfo $secondFileInfo) { + return strcmp( + strtr($firstFileInfo->getRealPath(), '\\', '/'), + strtr($secondFileInfo->getRealPath(), '\\', '/') + ); + }; + } +} diff --git a/packages/PharBuilder/src/FinderToPharAdder.php b/packages/PharBuilder/src/FinderToPharAdder.php new file mode 100644 index 000000000000..bc348ab9e20d --- /dev/null +++ b/packages/PharBuilder/src/FinderToPharAdder.php @@ -0,0 +1,33 @@ +symfonyStyle = $symfonyStyle; + } + + public function addFinderToPhar(Finder $finder, Phar $phar): void + { + foreach ($finder as $fileInfo) { + if ($this->symfonyStyle->isVerbose()) { + $this->symfonyStyle->note(sprintf('Adding "%s" file', $fileInfo->getRelativePathname())); + } else { + $this->symfonyStyle->progressAdvance(); + } + + $phar->addFromString($fileInfo->getRelativePathname(), $fileInfo->getContents()); + } + } +} diff --git a/packages/PharBuilder/src/config/config.yml b/packages/PharBuilder/src/config/config.yml new file mode 100644 index 000000000000..ed0ef64881df --- /dev/null +++ b/packages/PharBuilder/src/config/config.yml @@ -0,0 +1,34 @@ +parameters: + # customizable (via %ENV()?) + pharName: 'rector.phar' + binFileName: 'bin/rector' + buildDirectory: '%kernel.root_dir%/../../../..' + +services: + _defaults: + autowire: true + bind: + $pharName: '%pharName%' + $binFileName: '%binFileName%' + $buildDirectory: '%buildDirectory%' + # for "bin/compile" fetching + public: true + + Rector\PharBuilder\: + resource: '../' + + # Symfony\Console + Symfony\Component\Console\Application: + calls: + - ['add', ['@Rector\PharBuilder\Command\CompileCommand']] + - ['setDefaultCommand', ['compile', true]] + + Symfony\Component\Console\Style\SymfonyStyle: ~ + + Symfony\Component\Console\Input\ArgvInput: ~ + Symfony\Component\Console\Input\InputInterface: + alias: Symfony\Component\Console\Input\ArgvInput + + Symfony\Component\Console\Output\ConsoleOutput: ~ + Symfony\Component\Console\Output\OutputInterface: + alias: Symfony\Component\Console\Output\ConsoleOutput diff --git a/tests/Phar/PharTest.php b/tests/Phar/PharTest.php new file mode 100644 index 000000000000..b2f11512ee2b --- /dev/null +++ b/tests/Phar/PharTest.php @@ -0,0 +1,24 @@ +assertFileExists($rectorPharLocation); + + $process = new Process($rectorPharLocation); + $exitCode = $process->run(); + + $this->assertSame('', $process->getErrorOutput()); + $this->assertSame(1, $exitCode); + } +}