Index: core/install/cache/class_structure.php =================================================================== --- core/install/cache/class_structure.php +++ core/install/cache/class_structure.php @@ -87,11 +87,13 @@ 'InPortal\\Core\\kernel\\Console\\Command\\IConsoleCommand' => '/core/kernel/Console/Command/IConsoleCommand.php', 'InPortal\\Core\\kernel\\Console\\Command\\ResetCacheCommand' => '/core/kernel/Console/Command/ResetCacheCommand.php', 'InPortal\\Core\\kernel\\Console\\Command\\RunEventCommand' => '/core/kernel/Console/Command/RunEventCommand.php', + 'InPortal\\Core\\kernel\\Console\\Command\\RunPhingCommand' => '/core/kernel/Console/Command/RunPhingCommand.php', 'InPortal\\Core\\kernel\\Console\\Command\\RunScheduledTaskCommand' => '/core/kernel/Console/Command/RunScheduledTaskCommand.php', 'InPortal\\Core\\kernel\\Console\\ConsoleApplication' => '/core/kernel/Console/ConsoleApplication.php', 'InPortal\\Core\\kernel\\Console\\ConsoleCommandProvider' => '/core/kernel/Console/ConsoleCommandProvider.php', 'InPortal\\Core\\kernel\\Console\\ConsoleIO' => '/core/kernel/Console/ConsoleIO.php', 'InPortal\\Core\\kernel\\Console\\IConsoleCommandProvider' => '/core/kernel/Console/IConsoleCommandProvider.php', + 'InPortal\\Core\\kernel\\Console\\PhingCache' => '/core/kernel/Console/PhingCache.php', 'InPortal\\Core\\kernel\\utility\\ClassDiscovery\\ClassDetector' => '/core/kernel/utility/ClassDiscovery/ClassDetector.php', 'InPortal\\Core\\kernel\\utility\\ClassDiscovery\\ClassMapBuilder' => '/core/kernel/utility/ClassDiscovery/ClassMapBuilder.php', 'InPortal\\Core\\kernel\\utility\\ClassDiscovery\\CodeFolderFilterIterator' => '/core/kernel/utility/ClassDiscovery/CodeFolderFilterIterator.php', @@ -882,6 +884,13 @@ 0 => 'InPortal\\Core\\kernel\\Console\\Command\\AbstractCommand', ), ), + 'InPortal\\Core\\kernel\\Console\\Command\\RunPhingCommand' => array( + 'type' => 1, + 'modifiers' => 0, + 'extends' => array( + 0 => 'InPortal\\Core\\kernel\\Console\\Command\\AbstractCommand', + ), + ), 'InPortal\\Core\\kernel\\Console\\Command\\RunScheduledTaskCommand' => array( 'type' => 1, 'modifiers' => 0, @@ -911,6 +920,13 @@ 'InPortal\\Core\\kernel\\Console\\IConsoleCommandProvider' => array( 'type' => 2, ), + 'InPortal\\Core\\kernel\\Console\\PhingCache' => array( + 'type' => 1, + 'modifiers' => 0, + 'extends' => array( + 0 => 'kBase', + ), + ), 'InPortal\\Core\\kernel\\utility\\ClassDiscovery\\ClassDetector' => array( 'type' => 1, 'modifiers' => 0, Index: core/kernel/Console/Command/CompletionCommand.php =================================================================== --- core/kernel/Console/Command/CompletionCommand.php +++ core/kernel/Console/Command/CompletionCommand.php @@ -17,6 +17,7 @@ use InPortal\Core\kernel\Console\ConsoleApplication; use Stecman\Component\Symfony\Console\BashCompletion\Completion; +use Stecman\Component\Symfony\Console\BashCompletion\Completion\ShellPathCompletion; use Stecman\Component\Symfony\Console\BashCompletion\CompletionCommand as BashCompletionCommand; use Stecman\Component\Symfony\Console\BashCompletion\CompletionHandler; @@ -66,6 +67,14 @@ */ protected function configureCompletion(CompletionHandler $handler) { + $handler->addHandler( + new ShellPathCompletion( + 'phing:run', + 'path', + Completion::TYPE_ARGUMENT + ) + ); + // This can be removed once https://github.com/stecman/symfony-console-completion v0.5.2 will be released. $handler->addHandler( new Completion( Index: core/kernel/Console/Command/RunPhingCommand.php =================================================================== --- /dev/null +++ core/kernel/Console/Command/RunPhingCommand.php @@ -0,0 +1,347 @@ +setName('phing:run') + ->setDescription('Executes/lists Phing targets') + ->addArgument( + 'target', + InputArgument::OPTIONAL, + 'Build target (e.g. "build")' + ) + ->addArgument( + 'path', + InputArgument::OPTIONAL, + 'Path to limit target activity with' + ); + } + + /** + * Initializes dependencies. + * + * @return void + */ + protected function initDependencies() + { + parent::initDependencies(); + + $this->phingCache = $this->Application->makeClass('InPortal\\Core\\kernel\\Console\\PhingCache'); + $this->phing = $this->createPhing(); + } + + /** + * Executes the current command. + * + * @param InputInterface $input An InputInterface instance. + * @param OutputInterface $output An OutputInterface instance. + * + * @return null|integer + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $target = $this->io->getArgument('target'); + + if ( $target !== null ) { + return $this->executePhingTarget($target); + } + + return $this->listPhingTargets(); + } + + /** + * Executes specific Phing target. + * + * @param string $target Target. + * + * @return integer + */ + protected function executePhingTarget($target) + { + if ( !array_key_exists($target, $this->getTargets()) ) { + throw new \RuntimeException('The "' . $target . '" target is unknown.'); + } + + $path = $this->io->getArgument('path'); + $phing = $this->getPhing()->add($target); + + if ( $path ) { + $absolute_path = realpath($path); + $relative_path = $this->getRelativePath($absolute_path); + + if ( $relative_path === $absolute_path ) { + throw new \RuntimeException('The "path" argument must be relative to root In-Portal directory.'); + } + + $phing->add('-Dscan.dir=${base.dir}' . $relative_path); + } + + $io = $this->io; + $process = $phing->build(); + $process->run(function ($type, $data) use ($io) { + $io->write($data); + }); + + return $process->getExitCode(); + } + + /** + * Lists Phing targets. + * + * @return integer + */ + protected function listPhingTargets() + { + $build_file = $this->getRelativePath($this->getBuildFile()); + $this->io->writeln(array('Phing targets (build file: ' . $build_file . '):', '')); + + $table = new Table($this->io->getOutput()); + $table->setHeaders(array('Name', 'Description')); + + foreach ( $this->getTargets() as $name => $description ) { + $table->addRow(array($name, $description)); + } + + $table->render(); + + return 0; + } + + /** + * Return possible values for the named argument. + * + * @param string $argumentName Argument name. + * @param CompletionContext $context Completion context. + * + * @return array + */ + public function completeArgumentValues($argumentName, CompletionContext $context) + { + $suggestions = parent::completeArgumentValues($argumentName, $context); + + if ( $argumentName === 'target' ) { + return array_keys($this->getTargets()); + } + + return $suggestions; + } + + /** + * Create process builder for executing Phing commands. + * + * @return \ProcessBuilder + * @throws \RuntimeException When "phing" executable not found in PATH. + */ + protected function createPhing() + { + if ( !$this->isExecutableInPath('phing') ) { + throw new \RuntimeException('Unable to locate "phing" executable in PATH.'); + } + + /** @var \ProcessBuilder $phing */ + $phing = $this->Application->makeClass('ProcessBuilder'); + $phing->setExecutable('phing'); + + // The "$this->io" isn't set during auto-complete. + if ( isset($this->io) && $this->io->isDecorated() ) { + $phing->add('-logger')->add('phing.listener.AnsiColorLogger'); + } + + $build_file = $this->getBuildFile(); + + if ( $build_file ) { + $phing->add('-f')->add($build_file); + } + + return $phing; + } + + /** + * Forks Phing process builder. + * + * @return \ProcessBuilder + */ + protected function getPhing() + { + return clone $this->phing; + } + + /** + * Returns possible task names. + * + * @return array + */ + protected function getTargets() + { + $targets = $this->phingCache->getTargets(); + + if ( !$targets ) { + $targets = $this->parseTargets(); + $this->phingCache->setTargets($targets); + } + + return $targets; + } + + /** + * Returns possible task names. + * + * @return array + */ + protected function parseTargets() + { + $process = $this->getPhing()->add('-l')->build(); + $process->mustRun(); + + $lines = explode(PHP_EOL, $process->getOutput()); + + $headings = array( + 'Default target:', + 'Main targets:', + 'Subtargets:', + ); + + $targets = array(); + $current_heading = ''; + + foreach ( $lines as $line ) { + if ( $line === '' ) { + $current_heading = ''; + continue; + } + elseif ( $line === '-------------------------------------------------------------------------------' ) { + continue; + } + + if ( in_array($line, $headings) ) { + $current_heading = $line; + $targets[$current_heading] = array(); + + continue; + } + + if ( $current_heading ) { + list ($target, $description) = explode(' ', $line, 2); + + // Only add targets from topmost project. + if ( strpos($target, '.') === false ) { + $targets[$current_heading][trim($target)] = trim($description); + } + } + } + + return $targets['Main targets:']; + } + + /** + * Checks if executable is in PATH. + * + * @param string $executable Executable. + * + * @return boolean + */ + protected function isExecutableInPath($executable) + { + /** @var \ProcessBuilder $process_builder */ + $process_builder = $this->Application->makeClass('ProcessBuilder'); + + $process = $process_builder->setExecutable('which')->add($executable)->build(); + $process->run(); + + return $process->isSuccessful(); + } + + /** + * Allows user to choose build file. + * + * @return string + * @throws \LogicException When no suitable build files found. + */ + protected function getBuildFile() + { + $build_file = $this->phingCache->getBuildFile(); + + // The "$this->io" isn't set during auto-complete. + if ( !$build_file && isset($this->io) ) { + $build_file_folder = FULL_PATH . '/tools/build'; + $build_files = glob($build_file_folder . '/*.xml'); + + if ( !$build_files ) { + throw new \LogicException('No build files found in "' . $build_file_folder . '" directory.'); + } + + foreach ( $build_files as $index => $build_file ) { + $build_files[$index] = $this->getRelativePath($build_file); + } + + $build_file = $this->io->choose('Please choose build file:', $build_files, null, 'Incorrect build file'); + + $this->phingCache->setBuildFile($build_file); + } + + if ( $build_file ) { + return FULL_PATH . $build_file; + } + + return null; + } + + /** + * Returns relative path to In-Portal root folder. + * + * @param string $absolute_path Absolute path. + * + * @return string + */ + protected function getRelativePath($absolute_path) + { + return preg_replace('/^' . preg_quote(FULL_PATH, '/') . '/', '', $absolute_path, 1); + } + +} Index: core/kernel/Console/PhingCache.php =================================================================== --- /dev/null +++ core/kernel/Console/PhingCache.php @@ -0,0 +1,133 @@ +cacheFile = WRITEABLE . '/phing_settings.json'; + $this->cacheData = $this->read(); + } + + /** + * Returns path to build file. + * + * @return string + */ + public function getBuildFile() + { + $build_file = isset($this->cacheData['build_file']) ? $this->cacheData['build_file'] : ''; + + if ( $build_file && file_exists(FULL_PATH . $build_file) ) { + return $build_file; + } + + return null; + } + + /** + * Sets build file. + * + * @param string $build_file Build file. + * + * @return void + */ + public function setBuildFile($build_file) + { + $this->cacheData['build_file'] = $build_file; + + // Build file change > force target re-parsing. + $this->setTargets(array()); + $this->store(); + } + + /** + * Returns supported targets. + * + * @return array + */ + public function getTargets() + { + return isset($this->cacheData['targets']) ? $this->cacheData['targets'] : array(); + } + + /** + * Sets targets. + * + * @param array $targets Targets. + * + * @return void + */ + public function setTargets(array $targets) + { + $this->cacheData['targets'] = $targets; + $this->store(); + } + + /** + * Returns build file from cache. + * + * @return string + * @throws \LogicException When cache file format can't be recognized. + */ + protected function read() + { + if ( !file_exists($this->cacheFile) ) { + return array(); + } + + $content = json_decode(file_get_contents($this->cacheFile), true); + + if ( $content === null ) { + throw new \LogicException('The "' . $this->cacheFile . '" file format can\'t be recognized.'); + } + + return $content; + } + + /** + * Stores new cache content. + * + * @return void + */ + protected function store() + { + $json_encode_options = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0; + file_put_contents($this->cacheFile, json_encode($this->cacheData, $json_encode_options)); + } + +}