false, 'coverage' => false ]) { $taskCodecept = $this->taskCodecept() ->args($args); if ($options['coverage']) { $taskCodecept->coverageXml('../../build/logs/clover.xml'); } if ($options['coverage-html']) { $taskCodecept->coverageHtml('../../build/logs/coverage'); } return $taskCodecept->run(); } /** * Code sniffer. * * Run the PHP Codesniffer on a file or directory. * * @param string $file * A file or directory to analyze. * @option $autofix Whether to run the automatic fixer or not. * @option $strict Show warnings as well as errors. * Default is to show only errors. */ public function sniff( $file = 'src/', $options = [ 'autofix' => false, 'strict' => false, ] ) { $strict = $options['strict'] ? '' : '-n'; $result = $this->taskExec("./vendor/bin/phpcs --standard=PSR2 --exclude=Squiz.Classes.ValidClassName {$strict} {$file}")->run(); if (!$result->wasSuccessful()) { if (!$options['autofix']) { $options['autofix'] = $this->confirm('Would you like to run phpcbf to fix the reported errors?'); } if ($options['autofix']) { $result = $this->taskExec("./vendor/bin/phpcbf --standard=PSR2 --exclude=Squiz.Classes.ValidClassName {$file}")->run(); } } return $result; } /** * Generate a new Robo task that wraps an existing utility class. * * @param $className The name of the existing utility class to wrap. * @param $wrapperClassName The name of the wrapper class to create. Optional. * @usage generate:task 'Symfony\Component\Filesystem\Filesystem' FilesystemStack */ public function generateTask($className, $wrapperClassName = "") { return $this->taskGenTask($className, $wrapperClassName)->run(); } /** * Release Robo. */ public function release($opts = ['beta' => false]) { $this->checkPharReadonly(); $version = \Robo\Robo::VERSION; $stable = !$opts['beta']; if ($stable) { $version = preg_replace('/-.*/', '', $version); } else { $version = $this->incrementVersion($version, 'beta'); } $this->writeVersion($version); $this->yell("Releasing Robo $version"); $this->docs(); $this->taskGitStack() ->add('-A') ->commit("Robo release $version") ->pull() ->push() ->run(); if ($stable) { $this->pharPublish(); } $this->publish(); $this->taskGitStack() ->tag($version) ->push('origin master --tags') ->run(); if ($stable) { $version = $this->incrementVersion($version) . '-dev'; $this->writeVersion($version); $this->taskGitStack() ->add('-A') ->commit("Prepare for $version") ->push() ->run(); } } /** * Update changelog. * * Add an entry to the Robo CHANGELOG.md file. * * @param string $addition The text to add to the change log. */ public function changed($addition) { $version = preg_replace('/-.*/', '', \Robo\Robo::VERSION); return $this->taskChangelog() ->version($version) ->change($addition) ->run(); } /** * Update the version of Robo. * * @param string $version The new verison for Robo. * Defaults to the next minor (bugfix) version after the current relelase. * @option stage The version stage: dev, alpha, beta or rc. Use empty for stable. */ public function versionBump($version = '', $options = ['stage' => '']) { // If the user did not specify a version, then update the current version. if (empty($version)) { $version = $this->incrementVersion(\Robo\Robo::VERSION, $options['stage']); } return $this->writeVersion($version); } /** * Write the specified version string back into the Robo.php file. * @param string $version */ protected function writeVersion($version) { // Write the result to a file. return $this->taskReplaceInFile(__DIR__.'/src/Robo.php') ->regex("#VERSION = '[^']*'#") ->to("VERSION = '".$version."'") ->run(); } /** * Advance to the next SemVer version. * * The behavior depends on the parameter $stage. * - If $stage is empty, then the patch or minor version of $version is incremented * - If $stage matches the current stage in the current version, then add one * to the stage (e.g. alpha3 -> alpha4) * - If $stage does not match the current stage in the current version, then * reset to '1' (e.g. alpha4 -> beta1) * * @param string $version A SemVer version * @param string $stage dev, alpha, beta, rc or an empty string for stable. * @return string */ protected function incrementVersion($version, $stage = '') { $stable = empty($stage); $versionStageNumber = '0'; preg_match('/-([a-zA-Z]*)([0-9]*)/', $version, $match); $match += ['', '', '']; $versionStage = $match[1]; $versionStageNumber = $match[2]; if ($versionStage != $stage) { $versionStageNumber = 0; } $version = preg_replace('/-.*/', '', $version); $versionParts = explode('.', $version); if ($stable) { $versionParts[count($versionParts)-1]++; } $version = implode('.', $versionParts); if (!$stable) { $version .= '-' . $stage; if ($stage != 'dev') { $versionStageNumber++; $version .= $versionStageNumber; } } return $version; } /** * Generate the Robo documentation files. */ public function docs() { $collection = $this->collectionBuilder(); $collection->progressMessage('Generate documentation from source code.'); $files = Finder::create()->files()->name('*.php')->in('src/Task'); $docs = []; foreach ($files as $file) { if ($file->getFileName() == 'loadTasks.php') { continue; } if ($file->getFileName() == 'loadShortcuts.php') { continue; } $ns = $file->getRelativePath(); if (!$ns) { continue; } $class = basename(substr($file, 0, -4)); class_exists($class = "Robo\\Task\\$ns\\$class"); $docs[$ns][] = $class; } ksort($docs); foreach ($docs as $ns => $tasks) { $taskGenerator = $collection->taskGenDoc("docs/tasks/$ns.md"); $taskGenerator->filterClasses(function (\ReflectionClass $r) { return !($r->isAbstract() || $r->isTrait()) && $r->implementsInterface('Robo\Contract\TaskInterface'); })->prepend("# $ns Tasks"); sort($tasks); foreach ($tasks as $class) { $taskGenerator->docClass($class); } $taskGenerator->filterMethods( function (\ReflectionMethod $m) { if ($m->isConstructor() || $m->isDestructor() || $m->isStatic()) { return false; } $undocumentedMethods = [ '', 'run', '__call', 'inflect', 'injectDependencies', 'getCommand', 'getPrinted', 'getConfig', 'setConfig', 'logger', 'setLogger', 'setProgressIndicator', 'progressIndicatorSteps', 'setBuilder', 'getBuilder', 'collectionBuilder', 'setVerbosityThreshold', 'verbosityThreshold', 'setOutputAdapter', 'outputAdapter', 'hasOutputAdapter', 'verbosityMeetsThreshold', 'writeMessage', 'detectInteractive', 'background', 'timeout', 'idleTimeout', 'env', 'envVars', 'setInput', 'interactive', 'silent', 'printed', 'printOutput', 'printMetadata', ]; return !in_array($m->name, $undocumentedMethods) && $m->isPublic(); // methods are not documented } )->processClassSignature( function ($c) { return "## " . preg_replace('~Task$~', '', $c->getShortName()) . "\n"; } )->processClassDocBlock( function (\ReflectionClass $c, $doc) { $doc = preg_replace('~@method .*?(.*?)\)~', '* `$1)` ', $doc); $doc = str_replace('\\'.$c->name, '', $doc); return $doc; } )->processMethodSignature( function (\ReflectionMethod $m, $text) { return str_replace('#### *public* ', '* `', $text) . '`'; } )->processMethodDocBlock( function (\ReflectionMethod $m, $text) { return $text ? ' ' . trim(strtok($text, "\n"), "\n") : ''; } ); } $collection->progressMessage('Documentation generation complete.'); return $collection->run(); } /** * Publish Robo. * * Builds a site in gh-pages branch. Uses mkdocs */ public function publish() { $current_branch = exec('git rev-parse --abbrev-ref HEAD'); return $this->collectionBuilder() ->taskGitStack() ->checkout('site') ->merge('master') ->completion($this->taskGitStack()->checkout($current_branch)) ->taskFilesystemStack() ->copy('CHANGELOG.md', 'docs/changelog.md') ->completion($this->taskFilesystemStack()->remove('docs/changelog.md')) ->taskExec('mkdocs gh-deploy') ->run(); } /** * Build the Robo phar executable. */ public function pharBuild() { $this->checkPharReadonly(); // Create a collection builder to hold the temporary // directory until the pack phar task runs. $collection = $this->collectionBuilder(); $workDir = $collection->tmpDir(); $roboBuildDir = "$workDir/robo"; // Before we run `composer install`, we will remove the dev // dependencies that we only use in the unit tests. Any dev dependency // that is in the 'suggested' section is used by a core task; // we will include all of those in the phar. $devProjectsToRemove = $this->devDependenciesToRemoveFromPhar(); // We need to create our work dir and run `composer install` // before we prepare the pack phar task, so create a separate // collection builder to do this step in. $prepTasks = $this->collectionBuilder(); $preparationResult = $prepTasks ->taskFilesystemStack() ->mkdir($workDir) ->taskRsync() ->fromPath( [ __DIR__ . '/composer.json', __DIR__ . '/scripts', __DIR__ . '/src', __DIR__ . '/data' ] ) ->toPath($roboBuildDir) ->recursive() ->progress() ->stats() ->taskComposerRemove() ->dir($roboBuildDir) ->dev() ->noUpdate() ->args($devProjectsToRemove) ->taskComposerInstall() ->dir($roboBuildDir) ->noScripts() ->printed(true) ->run(); // Exit if the preparation step failed if (!$preparationResult->wasSuccessful()) { return $preparationResult; } // Decide which files we're going to pack $files = Finder::create()->ignoreVCS(true) ->files() ->name('*.php') ->name('*.exe') // for 1symfony/console/Resources/bin/hiddeninput.exe ->name('GeneratedWrapper.tmpl') ->path('src') ->path('vendor') ->notPath('docs') ->notPath('/vendor\/.*\/[Tt]est/') ->in(is_dir($roboBuildDir) ? $roboBuildDir : __DIR__); // Build the phar return $collection ->taskPackPhar('robo.phar') ->addFiles($files) ->addFile('robo', 'robo') ->executable('robo') ->taskFilesystemStack() ->chmod('robo.phar', 0777) ->run(); } protected function checkPharReadonly() { if (ini_get('phar.readonly')) { throw new \Exception('Must set "phar.readonly = Off" in php.ini to build phars.'); } } /** * The phar:build command removes the project requirements from the * 'require-dev' section that are not in the 'suggest' section. * * @return array */ protected function devDependenciesToRemoveFromPhar() { $composerInfo = (array) json_decode(file_get_contents(__DIR__ . '/composer.json')); $devDependencies = array_keys((array)$composerInfo['require-dev']); $suggestedProjects = array_keys((array)$composerInfo['suggest']); return array_diff($devDependencies, $suggestedProjects); } /** * Install Robo phar. * * Installs the Robo phar executable in /usr/bin. Uses 'sudo'. */ public function pharInstall() { return $this->taskExec('sudo cp') ->arg('robo.phar') ->arg('/usr/bin/robo') ->run(); } /** * Publish Robo phar. * * Commits the phar executable to Robo's GitHub pages site. */ public function pharPublish() { $this->pharBuild(); $this->collectionBuilder() ->taskFilesystemStack() ->rename('robo.phar', 'robo-release.phar') ->taskGitStack() ->checkout('site') ->pull('origin site') ->taskFilesystemStack() ->remove('robotheme/robo.phar') ->rename('robo-release.phar', 'robotheme/robo.phar') ->taskGitStack() ->add('robotheme/robo.phar') ->commit('Update robo.phar to ' . \Robo\Robo::VERSION) ->push('origin site') ->checkout('master') ->run(); } }