php-censor/PHPCI/Builder.php

463 lines
12 KiB
PHP
Raw Normal View History

2013-05-03 17:02:53 +02:00
<?php
2013-05-16 03:16:56 +02:00
/**
2013-10-26 17:25:34 +02:00
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2013, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link http://www.phptesting.org/
*/
2013-05-03 17:02:53 +02:00
namespace PHPCI;
use PHPCI\Helper\MailerFactory;
2013-05-03 17:02:53 +02:00
use PHPCI\Model\Build;
use b8\Store;
2013-07-30 03:55:29 +02:00
use b8\Config;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
2013-05-03 17:02:53 +02:00
2013-05-16 03:16:56 +02:00
/**
2013-10-26 17:25:34 +02:00
* PHPCI Build Runner
* @author Dan Cryer <dan@block8.co.uk>
*/
class Builder implements LoggerAwareInterface, BuildLogger
2013-05-03 17:02:53 +02:00
{
/**
2013-10-26 17:25:34 +02:00
* @var string
*/
public $buildPath;
/**
2013-10-26 17:25:34 +02:00
* @var string[]
*/
public $ignore = array();
/**
2013-10-26 17:25:34 +02:00
* @var string
*/
protected $ciDir;
/**
2013-10-26 17:25:34 +02:00
* @var string
*/
protected $directory;
/**
2013-10-26 17:25:34 +02:00
* @var bool
*/
protected $success = true;
/**
2013-10-26 17:25:34 +02:00
* @var bool
*/
protected $verbose = true;
/**
2013-10-26 17:25:34 +02:00
* @var \PHPCI\Model\Build
*/
protected $build;
2013-10-26 17:25:34 +02:00
/**
* @var LoggerInterface
*/
protected $logger;
/**
2013-10-26 17:25:34 +02:00
* @var array
*/
protected $config;
/**
* @var string
*/
protected $lastOutput;
2013-05-22 20:17:33 +02:00
/**
2013-10-26 17:25:34 +02:00
* An array of key => value pairs that will be used for
2013-05-22 20:17:33 +02:00
* interpolation and environment variables
* @var array
* @see setInterpolationVars()
*/
protected $interpolation_vars = array();
/**
* @var \PHPCI\Store\BuildStore
*/
protected $store;
2013-10-10 02:01:06 +02:00
/**
* @var bool
*/
public $quiet = false;
/**
* @var \PHPCI\Plugin\Util\Executor
*/
protected $pluginExecutor;
/**
2013-10-26 17:25:34 +02:00
* Set up the builder.
* @param \PHPCI\Model\Build $build
* @param LoggerInterface $logger
*/
public function __construct(Build $build, $logger = null)
{
2013-10-26 17:25:34 +02:00
if ($logger) {
$this->setLogger($logger);
}
$this->build = $build;
$this->store = Store\Factory::getStore('Build');
$this->pluginExecutor = new Plugin\Util\Executor($this->buildPluginFactory($build), $this);
}
/**
2013-10-26 17:25:34 +02:00
* Set the config array, as read from phpci.yml
* @param array
*/
public function setConfigArray(array $config)
{
$this->config = $config;
}
/**
2013-10-26 17:25:34 +02:00
* Access a variable from the phpci.yml file.
* @param string
*/
public function getConfig($key)
{
2013-10-10 02:01:06 +02:00
$rtn = null;
if (isset($this->config[$key])) {
$rtn = $this->config[$key];
}
return $rtn;
}
/**
* Access a variable from the config.yml
* @param $key
* @return mixed
*/
public function getSystemConfig($key)
{
2013-07-30 03:55:29 +02:00
return Config::getInstance()->get($key);
}
/**
* @return string The title of the project being built.
*/
2013-10-10 02:12:30 +02:00
public function getBuildProjectTitle()
{
2013-10-10 02:01:06 +02:00
return $this->build->getProject()->getTitle();
}
/**
2013-10-26 17:25:34 +02:00
* Run the active build.
*/
public function execute()
{
// Update the build in the database, ping any external services.
2013-12-07 15:29:25 +01:00
$this->build->setStatus(Build::STATUS_RUNNING);
$this->build->setStarted(new \DateTime());
$this->store->save($this->build);
$this->build->sendStatusPostback();
$this->success = true;
try {
2013-10-10 02:01:06 +02:00
// Set up the build:
$this->setupBuild();
2013-10-10 02:01:06 +02:00
// Run the core plugin stages:
foreach (array('setup', 'test', 'complete') as $stage) {
$this->success &= $this->pluginExecutor->executePlugins($this->config, $stage);
2013-10-10 02:01:06 +02:00
}
2013-10-10 02:01:06 +02:00
// Failed build? Execute failure plugins and then mark the build as failed.
if (!$this->success) {
$this->pluginExecutor->executePlugins($this->config, 'failure');
2013-10-10 02:01:06 +02:00
throw new \Exception('BUILD FAILED!');
}
2013-10-10 02:01:06 +02:00
// If we got this far, the build was successful!
if ($this->success) {
2013-12-07 15:29:25 +01:00
$this->build->setStatus(Build::STATUS_SUCCESS);
$this->pluginExecutor->executePlugins($this->config, 'success');
2013-10-10 02:01:06 +02:00
$this->logSuccess('BUILD SUCCESSFUL!');
}
2013-10-10 02:01:06 +02:00
} catch (\Exception $ex) {
$this->logFailure($ex->getMessage(), $ex);
2013-12-07 15:29:25 +01:00
$this->build->setStatus(Build::STATUS_FAILED);
}
2013-10-26 17:25:34 +02:00
// Clean up:
2013-10-10 02:01:06 +02:00
$this->log('Removing build.');
shell_exec(sprintf('rm -Rf "%s"', $this->buildPath));
// Update the build in the database, ping any external services, etc.
$this->build->sendStatusPostback();
$this->build->setFinished(new \DateTime());
$this->store->save($this->build);
}
/**
2013-10-26 17:25:34 +02:00
* Used by this class, and plugins, to execute shell commands.
*/
public function executeCommand()
{
$command = call_user_func_array('sprintf', func_get_args());
2013-10-10 02:01:06 +02:00
if (!$this->quiet) {
$this->log('Executing: ' . $command);
2013-10-10 02:01:06 +02:00
}
$status = 0;
exec($command, $this->lastOutput, $status);
if (!empty($this->lastOutput) && ($this->verbose || $status != 0)) {
$this->log($this->lastOutput);
}
2013-10-10 02:01:06 +02:00
$rtn = false;
if ($status == 0) {
$rtn = true;
}
return $rtn;
}
/**
* Returns the output from the last command run.
*/
public function getLastOutput()
{
return implode(PHP_EOL, $this->lastOutput);
}
2013-10-26 17:25:34 +02:00
/**
* Add an entry to the build log.
* @param string|string[] $message
* @param string $level
* @param mixed[] $context
*/
public function log($message, $level = LogLevel::INFO, $context = array())
{
2013-10-26 17:25:34 +02:00
// Skip if no logger has been loaded.
if (!$this->logger) {
return;
}
2013-10-10 02:01:06 +02:00
if (!is_array($message)) {
$message = array($message);
}
// The build is added to the context so the logger can use
// details from it if required.
$context['build'] = $this->build;
2013-10-10 02:01:06 +02:00
foreach ($message as $item) {
2013-10-26 17:25:34 +02:00
$this->logger->log($level, $item, $context);
}
}
/**
2013-10-26 17:25:34 +02:00
* Add a success-coloured message to the log.
* @param string
*/
public function logSuccess($message)
{
$this->log("\033[0;32m" . $message . "\033[0m");
}
/**
2013-10-26 17:25:34 +02:00
* Add a failure-coloured message to the log.
* @param string $message
* @param \Exception $exception The exception that caused the error.
2013-10-26 17:25:34 +02:00
*/
public function logFailure($message, \Exception $exception = null)
{
$context = array();
// The psr3 log interface stipulates that exceptions should be passed
// as the exception key in the context array.
if ($exception) {
$context['exception'] = $exception;
}
$this->log(
"\033[0;31m" . $message . "\033[0m",
LogLevel::ERROR,
$context
);
}
2013-10-26 17:25:34 +02:00
2013-05-22 20:17:33 +02:00
/**
* Replace every occurance of the interpolation vars in the given string
* Example: "This is build %PHPCI_BUILD%" => "This is build 182"
* @param string $input
* @return string
*/
public function interpolate($input)
{
2013-10-10 02:01:06 +02:00
$keys = array_keys($this->interpolation_vars);
$values = array_values($this->interpolation_vars);
return str_replace($keys, $values, $input);
2013-05-22 20:17:33 +02:00
}
2013-05-22 20:17:33 +02:00
/**
2013-10-26 17:25:34 +02:00
* Sets the variables that will be used for interpolation. This must be run
2013-05-22 20:17:33 +02:00
* from setupBuild() because prior to that, we don't know the buildPath
*/
protected function setInterpolationVars()
{
2013-10-10 02:01:06 +02:00
$this->interpolation_vars = array();
$this->interpolation_vars['%PHPCI%'] = 1;
$this->interpolation_vars['%COMMIT%'] = $this->build->getCommitId();
$this->interpolation_vars['%PROJECT%'] = $this->build->getProjectId();
$this->interpolation_vars['%BUILD%'] = $this->build->getId();
2013-10-26 17:25:34 +02:00
$this->interpolation_vars['%PROJECT_TITLE%'] = $this->getBuildProjectTitle(
);
2013-10-10 02:01:06 +02:00
$this->interpolation_vars['%BUILD_PATH%'] = $this->buildPath;
2013-10-26 17:25:34 +02:00
$this->interpolation_vars['%BUILD_URI%'] = PHPCI_URL . "build/view/" . $this->build->getId(
);
2013-10-10 02:01:06 +02:00
$this->interpolation_vars['%PHPCI_COMMIT%'] = $this->interpolation_vars['%COMMIT%'];
$this->interpolation_vars['%PHPCI_PROJECT%'] = $this->interpolation_vars['%PROJECT%'];
$this->interpolation_vars['%PHPCI_BUILD%'] = $this->interpolation_vars['%BUILD%'];
$this->interpolation_vars['%PHPCI_PROJECT_TITLE%'] = $this->interpolation_vars['%PROJECT_TITLE%'];
$this->interpolation_vars['%PHPCI_BUILD_PATH%'] = $this->interpolation_vars['%BUILD_PATH%'];
$this->interpolation_vars['%PHPCI_BUILD_URI%'] = $this->interpolation_vars['%BUILD_URI%'];
putenv('PHPCI=1');
2013-10-26 17:25:34 +02:00
putenv('PHPCI_COMMIT=' . $this->interpolation_vars['%COMMIT%']);
putenv('PHPCI_PROJECT=' . $this->interpolation_vars['%PROJECT%']);
putenv('PHPCI_BUILD=' . $this->interpolation_vars['%BUILD%']);
putenv(
'PHPCI_PROJECT_TITLE=' . $this->interpolation_vars['%PROJECT_TITLE%']
);
putenv('PHPCI_BUILD_PATH=' . $this->interpolation_vars['%BUILD_PATH%']);
putenv('PHPCI_BUILD_URI=' . $this->interpolation_vars['%BUILD_URI%']);
2013-05-22 20:17:33 +02:00
}
2013-10-26 17:25:34 +02:00
/**
2013-10-26 17:25:34 +02:00
* Set up a working copy of the project for building.
*/
protected function setupBuild()
{
2013-10-26 17:25:34 +02:00
$buildId = 'project' . $this->build->getProject()->getId(
) . '-build' . $this->build->getId();
$this->ciDir = dirname(__FILE__) . '/../';
$this->buildPath = $this->ciDir . 'build/' . $buildId . '/';
2013-05-22 20:17:33 +02:00
$this->setInterpolationVars();
2013-10-26 17:25:34 +02:00
// Create a working copy of the project:
if (!$this->build->createWorkingCopy($this, $this->buildPath)) {
2013-10-10 02:01:06 +02:00
throw new \Exception('Could not create a working copy.');
}
// Does the project's phpci.yml request verbose mode?
if (!isset($this->config['build_settings']['verbose']) || !$this->config['build_settings']['verbose']) {
$this->verbose = false;
}
// Does the project have any paths it wants plugins to ignore?
if (isset($this->config['build_settings']['ignore'])) {
$this->ignore = $this->config['build_settings']['ignore'];
}
$this->logSuccess('Working copy created: ' . $this->buildPath);
return true;
}
2013-10-08 09:50:10 +02:00
/**
* Find a binary required by a plugin.
* @param $binary
* @return null|string
*/
public function findBinary($binary)
{
if (is_string($binary)) {
$binary = array($binary);
}
foreach ($binary as $bin) {
// Check project root directory:
if (is_file(PHPCI_DIR . $bin)) {
return PHPCI_DIR . $bin;
}
// Check Composer bin dir:
if (is_file(PHPCI_DIR . 'vendor/bin/' . $bin)) {
return PHPCI_DIR . 'vendor/bin/' . $bin;
}
// Use "which"
$which = trim(shell_exec('which ' . $bin));
if (!empty($which)) {
return $which;
}
}
return null;
}
2013-10-26 17:25:34 +02:00
/**
* Sets a logger instance on the object
*
* @param LoggerInterface $logger
* @return null
*/
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* returns the logger attached to this builder.
*
* @return LoggerInterface
*/
public function getLogger()
{
return $this->logger;
}
private function buildPluginFactory(Build $build)
{
$pluginFactory = new Plugin\Util\Factory();
$self = $this;
$pluginFactory->registerResource(
function () use($self) {
return $self;
},
null,
'PHPCI\Builder'
);
$pluginFactory->registerResource(
function () use($build) {
return $build;
},
null,
'PHPCI\Model\Build'
);
$pluginFactory->registerResource(
function () use ($self) {
$factory = new MailerFactory($self->getSystemConfig('phpci'));
return $factory->getSwiftMailerFromConfig();
},
null,
2013-11-17 22:29:49 +01:00
'Swift_Mailer'
);
return $pluginFactory;
}
}