2013-05-03 17:02:53 +02:00
|
|
|
<?php
|
2013-05-16 03:16:56 +02:00
|
|
|
/**
|
|
|
|
* PHPCI - Continuous Integration for PHP
|
|
|
|
*
|
2013-05-16 03:57:02 +02:00
|
|
|
* @copyright Copyright 2013, Block 8 Limited.
|
|
|
|
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
|
|
|
|
* @link http://www.phptesting.org/
|
2013-05-16 03:16:56 +02:00
|
|
|
*/
|
2013-05-03 17:02:53 +02:00
|
|
|
|
|
|
|
namespace PHPCI;
|
2013-05-16 03:57:02 +02:00
|
|
|
|
2013-05-03 17:02:53 +02:00
|
|
|
use PHPCI\Model\Build;
|
|
|
|
use b8\Store;
|
2013-05-14 19:07:46 +02:00
|
|
|
use Symfony\Component\Yaml\Parser as YamlParser;
|
2013-05-03 17:02:53 +02:00
|
|
|
|
2013-05-16 03:16:56 +02:00
|
|
|
/**
|
|
|
|
* PHPCI Build Runner
|
2013-05-16 03:57:02 +02:00
|
|
|
* @author Dan Cryer <dan@block8.co.uk>
|
2013-05-16 03:16:56 +02:00
|
|
|
*/
|
2013-05-03 17:02:53 +02:00
|
|
|
class Builder
|
|
|
|
{
|
2013-05-16 03:57:02 +02:00
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
public $buildPath;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string[]
|
|
|
|
*/
|
|
|
|
public $ignore = array();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $ciDir;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $directory;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
protected $success = true;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $log = '';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
protected $verbose = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var bool[]
|
|
|
|
*/
|
|
|
|
protected $plugins = array();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var \PHPCI\Model\Build
|
|
|
|
*/
|
|
|
|
protected $build;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var callable
|
|
|
|
*/
|
|
|
|
protected $logCallback;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
protected $config;
|
2013-05-22 20:17:33 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* An array of key => value pairs that will be used for
|
|
|
|
* interpolation and environment variables
|
|
|
|
* @var array
|
|
|
|
* @see setInterpolationVars()
|
|
|
|
* @see getInterpolationVars()
|
|
|
|
*/
|
|
|
|
protected $interpolation_vars = array();
|
2013-05-16 03:57:02 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Set up the builder.
|
|
|
|
* @param \PHPCI\Model\Build
|
|
|
|
* @param callable
|
|
|
|
*/
|
|
|
|
public function __construct(Build $build, $logCallback = null)
|
|
|
|
{
|
|
|
|
$this->build = $build;
|
|
|
|
$this->store = Store\Factory::getStore('Build');
|
|
|
|
|
|
|
|
if (!is_null($logCallback) && is_callable($logCallback)) {
|
|
|
|
$this->logCallback = $logCallback;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the config array, as read from phpci.yml
|
|
|
|
* @param array
|
|
|
|
*/
|
|
|
|
public function setConfigArray(array $config)
|
|
|
|
{
|
|
|
|
$this->config = $config;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Access a variable from the phpci.yml file.
|
|
|
|
* @param string
|
|
|
|
*/
|
|
|
|
public function getConfig($key)
|
|
|
|
{
|
|
|
|
return isset($this->config[$key]) ? $this->config[$key] : null;
|
|
|
|
}
|
|
|
|
|
2013-06-01 14:28:42 +02:00
|
|
|
/**
|
|
|
|
* Access a variable from the config.yml
|
|
|
|
* @param $key
|
|
|
|
* @return mixed
|
|
|
|
*/
|
|
|
|
public function getSystemConfig($key)
|
|
|
|
{
|
|
|
|
return \b8\Registry::getInstance()->get($key);
|
|
|
|
}
|
|
|
|
|
2013-05-18 00:19:38 +02:00
|
|
|
/**
|
|
|
|
* Access the build.
|
|
|
|
* @param Build
|
|
|
|
*/
|
|
|
|
public function getBuild()
|
|
|
|
{
|
|
|
|
return $this->build;
|
|
|
|
}
|
|
|
|
|
2013-06-01 14:56:09 +02:00
|
|
|
/**
|
|
|
|
* Indicates if the build has passed or failed.
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function getSuccessStatus()
|
|
|
|
{
|
|
|
|
return $this->success;
|
|
|
|
}
|
|
|
|
|
2013-05-16 03:57:02 +02:00
|
|
|
/**
|
|
|
|
* Run the active build.
|
|
|
|
*/
|
|
|
|
public function execute()
|
|
|
|
{
|
|
|
|
// Update the build in the database, ping any external services.
|
|
|
|
$this->build->setStatus(1);
|
|
|
|
$this->build->setStarted(new \DateTime());
|
|
|
|
$this->store->save($this->build);
|
|
|
|
$this->build->sendStatusPostback();
|
|
|
|
|
|
|
|
try {
|
|
|
|
if ($this->setupBuild()) {
|
|
|
|
// Run setup steps:
|
|
|
|
$this->executePlugins('setup');
|
|
|
|
|
|
|
|
// Run the any tests:
|
|
|
|
$this->executePlugins('test');
|
|
|
|
$this->log('');
|
|
|
|
|
|
|
|
// Run build complete steps:
|
|
|
|
$this->executePlugins('complete');
|
|
|
|
|
|
|
|
// Run success or failure plugins:
|
|
|
|
if ($this->success) {
|
|
|
|
$this->executePlugins('success');
|
|
|
|
$this->logSuccess('BUILD SUCCESSFUL!');
|
|
|
|
$this->build->setStatus(2);
|
|
|
|
} else {
|
|
|
|
$this->executePlugins('failure');
|
|
|
|
$this->logFailure('BUILD FAILED!');
|
|
|
|
$this->build->setStatus(3);
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->log('');
|
|
|
|
} else {
|
|
|
|
$this->build->setStatus(3);
|
|
|
|
}
|
|
|
|
} catch (\Exception $ex) {
|
|
|
|
$this->logFailure($ex->getMessage());
|
|
|
|
$this->build->setStatus(3);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clean up:
|
|
|
|
$this->removeBuild();
|
|
|
|
|
|
|
|
// Update the build in the database, ping any external services, etc.
|
|
|
|
$this->build->sendStatusPostback();
|
|
|
|
$this->build->setFinished(new \DateTime());
|
|
|
|
$this->build->setLog($this->log);
|
|
|
|
$this->build->setPlugins(json_encode($this->plugins));
|
|
|
|
$this->store->save($this->build);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Used by this class, and plugins, to execute shell commands.
|
|
|
|
*/
|
|
|
|
public function executeCommand()
|
|
|
|
{
|
|
|
|
$command = call_user_func_array('sprintf', func_get_args());
|
|
|
|
|
|
|
|
$this->log('Executing: ' . $command, ' ');
|
|
|
|
|
|
|
|
$output = '';
|
|
|
|
$status = 0;
|
|
|
|
exec($command, $output, $status);
|
|
|
|
|
|
|
|
if (!empty($output) && ($this->verbose || $status != 0)) {
|
|
|
|
$this->log($output, ' ');
|
|
|
|
}
|
|
|
|
|
|
|
|
return ($status == 0) ? true : false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add an entry to the build log.
|
|
|
|
* @param string|string[]
|
|
|
|
* @param string
|
|
|
|
*/
|
|
|
|
public function log($message, $prefix = '')
|
|
|
|
{
|
|
|
|
if (is_array($message)) {
|
|
|
|
foreach ($message as $item) {
|
|
|
|
if (is_callable($this->logCallback)) {
|
2013-05-16 17:46:30 +02:00
|
|
|
call_user_func_array($this->logCallback, array($prefix . $item));
|
2013-05-16 03:57:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$this->log .= $prefix . $item . PHP_EOL;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$message = $prefix . $message;
|
|
|
|
$this->log .= $message . PHP_EOL;
|
|
|
|
|
|
|
|
if (isset($this->logCallback) && is_callable($this->logCallback)) {
|
2013-05-16 17:46:30 +02:00
|
|
|
call_user_func_array($this->logCallback, array($message));
|
2013-05-16 03:57:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->build->setLog($this->log);
|
|
|
|
$this->build->setPlugins(json_encode($this->plugins));
|
|
|
|
$this->store->save($this->build);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a success-coloured message to the log.
|
|
|
|
* @param string
|
|
|
|
*/
|
|
|
|
public function logSuccess($message)
|
|
|
|
{
|
|
|
|
$this->log("\033[0;32m" . $message . "\033[0m");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a failure-coloured message to the log.
|
|
|
|
* @param string
|
|
|
|
*/
|
|
|
|
public function logFailure($message)
|
|
|
|
{
|
|
|
|
$this->log("\033[0;31m" . $message . "\033[0m");
|
|
|
|
}
|
2013-05-22 20:17:33 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get an array key => value pairs that are used for interpolation
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getInterpolationVars()
|
|
|
|
{
|
|
|
|
return $this->interpolation_vars;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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-05-22 21:17:54 +02:00
|
|
|
$trans_table = array();
|
|
|
|
foreach ($this->getInterpolationVars() as $key => $value) {
|
|
|
|
$trans_table['%'.$key.'%'] = $value;
|
|
|
|
}
|
|
|
|
return strtr($input, $trans_table);
|
2013-05-22 20:17:33 +02:00
|
|
|
}
|
2013-05-16 03:57:02 +02:00
|
|
|
|
2013-05-22 20:17:33 +02:00
|
|
|
/**
|
|
|
|
* Sets the variables that will be used for interpolation. This must be run
|
|
|
|
* from setupBuild() because prior to that, we don't know the buildPath
|
|
|
|
*/
|
|
|
|
protected function setInterpolationVars()
|
|
|
|
{
|
|
|
|
$this->interpolation_vars = array(
|
|
|
|
'PHPCI' => 1,
|
|
|
|
'PHPCI_COMMIT' => $this->build->getCommitId(),
|
|
|
|
'PHPCI_PROJECT' => $this->build->getProject()->getId(),
|
|
|
|
'PHPCI_BUILD' => $this->build->getId(),
|
|
|
|
'PHPCI_PROJECT_TITLE' => $this->build->getProject()->getTitle(),
|
|
|
|
'PHPCI_BUILD_PATH' => $this->buildPath,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2013-05-16 03:57:02 +02:00
|
|
|
/**
|
|
|
|
* Set up a working copy of the project for building.
|
|
|
|
*/
|
|
|
|
protected function setupBuild()
|
|
|
|
{
|
|
|
|
$commitId = $this->build->getCommitId();
|
|
|
|
$buildId = 'project' . $this->build->getProject()->getId() . '-build' . $this->build->getId();
|
|
|
|
$this->ciDir = realpath(dirname(__FILE__) . '/../') . '/';
|
|
|
|
$this->buildPath = $this->ciDir . 'build/' . $buildId . '/';
|
2013-05-19 07:47:41 +02:00
|
|
|
|
2013-05-22 20:17:33 +02:00
|
|
|
$this->setInterpolationVars();
|
|
|
|
|
2013-05-19 07:47:41 +02:00
|
|
|
// Setup environment vars that will be accessible during exec()
|
2013-05-22 20:17:33 +02:00
|
|
|
foreach ($this->getInterpolationVars() as $key => $value) {
|
|
|
|
putenv($key.'='.$value);
|
|
|
|
}
|
|
|
|
|
2013-05-16 03:57:02 +02:00
|
|
|
// Create a working copy of the project:
|
|
|
|
if (!$this->build->createWorkingCopy($this, $this->buildPath)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Does the project's phpci.yml request verbose mode?
|
|
|
|
if (!isset($this->config['build_settings']['verbose']) || !$this->config['build_settings']['verbose']) {
|
|
|
|
$this->verbose = false;
|
|
|
|
} else {
|
|
|
|
$this->verbose = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Execute a the appropriate set of plugins for a given build stage.
|
|
|
|
*/
|
|
|
|
protected function executePlugins($stage)
|
|
|
|
{
|
|
|
|
// Ignore any stages for which we don't have plugins set:
|
|
|
|
if (!array_key_exists($stage, $this->config) || !is_array($this->config[$stage])) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($this->config[$stage] as $plugin => $options) {
|
|
|
|
$this->log('');
|
|
|
|
$this->log('RUNNING PLUGIN: ' . $plugin);
|
|
|
|
|
|
|
|
// Is this plugin allowed to fail?
|
|
|
|
if ($stage == 'test' && !isset($options['allow_failures'])) {
|
|
|
|
$options['allow_failures'] = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$class = str_replace('_', ' ', $plugin);
|
|
|
|
$class = ucwords($class);
|
|
|
|
$class = 'PHPCI\\Plugin\\' . str_replace(' ', '', $class);
|
|
|
|
|
|
|
|
if (!class_exists($class)) {
|
|
|
|
$this->logFailure('Plugin does not exist: ' . $plugin);
|
|
|
|
|
|
|
|
if ($stage == 'test') {
|
|
|
|
$this->plugins[$plugin] = false;
|
|
|
|
|
|
|
|
if (!$options['allow_failures']) {
|
|
|
|
$this->success = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
$obj = new $class($this, $options);
|
|
|
|
|
|
|
|
if (!$obj->execute()) {
|
|
|
|
if ($stage == 'test') {
|
|
|
|
$this->plugins[$plugin] = false;
|
|
|
|
|
|
|
|
if (!$options['allow_failures']) {
|
|
|
|
$this->success = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->logFailure('PLUGIN STATUS: FAILED');
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
} catch (\Exception $ex) {
|
|
|
|
$this->logFailure('EXCEPTION: ' . $ex->getMessage());
|
|
|
|
|
|
|
|
if ($stage == 'test') {
|
|
|
|
$this->plugins[$plugin] = false;
|
|
|
|
|
|
|
|
if (!$options['allow_failures']) {
|
|
|
|
$this->success = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->logFailure('PLUGIN STATUS: FAILED');
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($stage == 'test') {
|
|
|
|
$this->plugins[$plugin] = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->logSuccess('PLUGIN STATUS: SUCCESS!');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clean up our working copy.
|
|
|
|
*/
|
|
|
|
protected function removeBuild()
|
|
|
|
{
|
|
|
|
$this->log('Removing build.');
|
|
|
|
shell_exec(sprintf('rm -Rf "%s"', $this->buildPath));
|
|
|
|
}
|
|
|
|
}
|