387 lines
8.5 KiB
PHP
387 lines
8.5 KiB
PHP
<?php
|
|
namespace taskman;
|
|
use Exception;
|
|
|
|
include_once(__DIR__ . '/internal.inc.php');
|
|
include_once(__DIR__ . '/util.inc.php');
|
|
include_once(__DIR__ . '/tasks.inc.php');
|
|
include_once(__DIR__ . '/artefact.inc.php');
|
|
|
|
$GLOBALS['TASKMAN_TASKS'] = array();
|
|
$GLOBALS['TASKMAN_CLOSURES'] = array();
|
|
$GLOBALS['TASKMAN_STACK'] = array();
|
|
$GLOBALS['TASKMAN_TASK_ALIASES'] = array();
|
|
$GLOBALS['TASKMAN_LOG_LEVEL'] = 1; //0 - important, 1 - normal, 2 - debug
|
|
$GLOBALS['TASKMAN_NO_DEPS'] = false;
|
|
$GLOBALS['TASKMAN_SCRIPT'] = '';
|
|
$GLOBALS['TASKMAN_CURRENT_TASK'] = null;
|
|
$GLOBALS['TASKMAN_HELP_FUNC'] = '\taskman\internal\_default_usage';
|
|
$GLOBALS['TASKMAN_LOGGER'] = '\taskman\internal\_default_logger';
|
|
$GLOBALS['TASKMAN_ERROR_HANDLER'] = null;
|
|
$GLOBALS['TASKMAN_START_TIME'] = 0;
|
|
$GLOBALS['TASKMAN_FILES_CHANGES'] = null;
|
|
|
|
class TaskmanException extends Exception
|
|
{}
|
|
|
|
class TaskmanTask
|
|
{
|
|
private \Closure $func;
|
|
|
|
private string $file;
|
|
private int $line;
|
|
|
|
private string $name;
|
|
//initialized lazily
|
|
private ?array $_aliases = null;
|
|
|
|
private $args = array();
|
|
|
|
private $props = array();
|
|
private bool $is_running = false;
|
|
private array $has_run = array();
|
|
|
|
private ?array $deps = null;
|
|
private array $before_deps = array();
|
|
private array $after_deps = array();
|
|
|
|
//initialized lazily
|
|
private ?array $_artefacts = null;
|
|
|
|
private ?TaskmanFileChanges $file_changes;
|
|
|
|
function __construct(\Closure $func, string $name,
|
|
array $props = array(), ?TaskmanFileChanges $file_changes = null)
|
|
{
|
|
$refl = new \ReflectionFunction($func);
|
|
$this->file = $refl->getFileName();
|
|
$this->line = $refl->getStartLine();
|
|
|
|
$this->func = $func;
|
|
$this->name = $name;
|
|
$this->props = $props;
|
|
|
|
$this->file_changes = $file_changes;
|
|
}
|
|
|
|
function getName() : string
|
|
{
|
|
return $this->name;
|
|
}
|
|
|
|
function getFile() : string
|
|
{
|
|
return $this->file;
|
|
}
|
|
|
|
function getLine() : int
|
|
{
|
|
return $this->line;
|
|
}
|
|
|
|
function getFunc() : \Closure
|
|
{
|
|
return $this->func;
|
|
}
|
|
|
|
function getArgs() : array
|
|
{
|
|
return $this->args;
|
|
}
|
|
|
|
function getAliases() : array
|
|
{
|
|
if($this->_aliases === null)
|
|
$this->_aliases = $this->_parseAliases();
|
|
return $this->_aliases;
|
|
}
|
|
|
|
function _parseAliases() : array
|
|
{
|
|
$alias = $this->getPropOr("alias", "");
|
|
if(is_array($alias))
|
|
return $alias;
|
|
else if($alias && is_string($alias))
|
|
return explode(",", $alias);
|
|
else
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* @return artefact\TaskmanArtefact[]
|
|
*/
|
|
function getArtefacts() : array
|
|
{
|
|
if($this->_artefacts === null)
|
|
$this->_artefacts = $this->_parseArtefacts();
|
|
return $this->_artefacts;
|
|
}
|
|
|
|
/**
|
|
* @return artefact\TaskmanArtefact[]
|
|
*/
|
|
function _parseArtefacts() : array
|
|
{
|
|
$artefacts = array();
|
|
$specs = $this->getPropOr("artefacts", array());
|
|
if(is_callable($specs))
|
|
$specs = $specs();
|
|
if(is_array($specs))
|
|
{
|
|
foreach($specs as $dst => $src_spec)
|
|
$artefacts[] = new artefact\TaskmanArtefact($dst, $src_spec);
|
|
}
|
|
|
|
return $artefacts;
|
|
}
|
|
|
|
function getFileChanges() : ?TaskmanFileChanges
|
|
{
|
|
return $this->file_changes;
|
|
}
|
|
|
|
function isIncrementalBuild() : bool
|
|
{
|
|
return $this->file_changes != null;
|
|
}
|
|
|
|
function run($args = array())
|
|
{
|
|
global $TASKMAN_CURRENT_TASK;
|
|
global $TASKMAN_STACK;
|
|
global $TASKMAN_NO_DEPS;
|
|
global $TASKMAN_START_TIME;
|
|
|
|
$args_str = serialize($args);
|
|
|
|
if((isset($this->has_run[$args_str]) && $this->has_run[$args_str]) ||
|
|
$this->is_running)
|
|
return;
|
|
|
|
$task_result = null;
|
|
|
|
try
|
|
{
|
|
if($this->getArtefacts() && !$this->_checkIfArtefactsStale())
|
|
{
|
|
$this->has_run[$args_str] = true;
|
|
return;
|
|
}
|
|
|
|
$this->is_running = true;
|
|
$this->args = $args;
|
|
|
|
$TASKMAN_STACK[] = $this;
|
|
|
|
$level = count($TASKMAN_STACK)-1;
|
|
|
|
msg_sys("***** ".str_repeat('-', $level)."task '" . $this->getName() . "' start *****\n");
|
|
|
|
if(!$TASKMAN_NO_DEPS)
|
|
{
|
|
run_many($this->before_deps);
|
|
run_many($this->getDeps());
|
|
}
|
|
|
|
$TASKMAN_CURRENT_TASK = $this;
|
|
|
|
$bench = microtime(true);
|
|
|
|
$task_result = call_user_func_array($this->func, array($this->args, $this));
|
|
array_pop($TASKMAN_STACK);
|
|
|
|
if(!$TASKMAN_NO_DEPS)
|
|
run_many($this->after_deps);
|
|
|
|
msg_sys("***** ".str_repeat('-', $level)."task '" . $this->getName() . "' done(" .
|
|
round(microtime(true)-$bench,2) . '/' .round(microtime(true)-$TASKMAN_START_TIME,2) . " sec.) *****\n");
|
|
|
|
$this->has_run[$args_str] = true;
|
|
$this->is_running = false;
|
|
}
|
|
catch(Exception $e)
|
|
{
|
|
$error_handler = $GLOBALS['TASKMAN_ERROR_HANDLER'];
|
|
if($error_handler)
|
|
$error_handler($e);
|
|
else
|
|
throw $e;
|
|
}
|
|
|
|
return $task_result;
|
|
}
|
|
|
|
function addBeforeDep($task)
|
|
{
|
|
$this->before_deps[] = $task;
|
|
}
|
|
|
|
function addAfterDep($task)
|
|
{
|
|
$this->after_deps[] = $task;
|
|
}
|
|
|
|
function getDeps() : array
|
|
{
|
|
if($this->deps === null)
|
|
$this->deps = $this->_parseDeps();
|
|
return $this->deps;
|
|
}
|
|
|
|
private function _parseDeps() : array
|
|
{
|
|
$deps = $this->getPropOr("deps", "");
|
|
if(is_array($deps))
|
|
return $deps;
|
|
else if($deps && is_string($deps))
|
|
return internal\_parse_taskstr($deps);
|
|
return array();
|
|
}
|
|
|
|
function getPropOr(string $name, mixed $def) : mixed
|
|
{
|
|
return isset($this->props[$name]) ? $this->props[$name] : $def;
|
|
}
|
|
|
|
function getProp(string $name) : mixed
|
|
{
|
|
return $this->getPropOr($name, null);
|
|
}
|
|
|
|
function hasProp(string $name) : bool
|
|
{
|
|
return isset($this->props[$name]);
|
|
}
|
|
|
|
function getProps() : array
|
|
{
|
|
return $this->props;
|
|
}
|
|
|
|
private function _checkIfArtefactsStale() : bool
|
|
{
|
|
$file_changes = $this->getFileChanges();
|
|
|
|
$stale_found = false;
|
|
|
|
foreach($this->getArtefacts() as $artefact)
|
|
{
|
|
$artefact->initSources($file_changes);
|
|
|
|
if(!$stale_found && $artefact->checkNewerSources($file_changes))
|
|
$stale_found = true;
|
|
|
|
if($artefact->isStale())
|
|
{
|
|
msg_sys("Task '{$this->name}' artefact '{$artefact->getPath()}' (sources at ".implode(',', $artefact->getNewerSourcesIndices()).") is stale\n");
|
|
}
|
|
}
|
|
|
|
return $stale_found;
|
|
}
|
|
|
|
function findAnyStaleArtefact() : ?artefact\TaskmanArtefact
|
|
{
|
|
foreach($this->getArtefacts() as $artefact)
|
|
{
|
|
if($artefact->isStale())
|
|
return $artefact;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
class TaskmanFileChanges
|
|
{
|
|
private $changed = array();
|
|
private $removed = array();
|
|
|
|
static function parse(string $json_or_file)
|
|
{
|
|
if($json_or_file[0] == '[')
|
|
$json = $json_or_file;
|
|
else
|
|
$json = file_get_contents($json_or_file);
|
|
|
|
$decoded = json_decode($json, true);
|
|
if(!is_array($decoded))
|
|
throw new Exception('Bad json');
|
|
|
|
$changed = array();
|
|
$removed = array();
|
|
|
|
$base_dir = dirname($_SERVER['PHP_SELF']);
|
|
|
|
foreach($decoded as $items)
|
|
{
|
|
if(count($items) != 2)
|
|
throw new Exception('Bad entry');
|
|
list($status, $file) = $items;
|
|
|
|
if(strlen($file) > 0)
|
|
{
|
|
if(DIRECTORY_SEPARATOR == '/')
|
|
{
|
|
if($file[0] != '/')
|
|
$file = $base_dir . DIRECTORY_SEPARATOR . $file;
|
|
}
|
|
else if(strlen($file) > 1 && $file[1] != ':')
|
|
$file = $base_dir . DIRECTORY_SEPARATOR . $file;
|
|
}
|
|
|
|
$file = artefact\normalize_path($file);
|
|
|
|
if($status == 'C')
|
|
$changed[$file] = true;
|
|
else if($status == 'R')
|
|
$removed[$file] = true;
|
|
else
|
|
throw new Exception('Unknown status: ' . $status);
|
|
}
|
|
|
|
return new TaskmanFileChanges($changed, $removed);
|
|
}
|
|
|
|
//NOTE: these are actually maps: file => true
|
|
function __construct(array $changed, array $removed)
|
|
{
|
|
$this->changed = $changed;
|
|
$this->removed = $removed;
|
|
}
|
|
|
|
function isEmpty() : bool
|
|
{
|
|
return count($this->changed) == 0 && count($this->removed) == 0;
|
|
}
|
|
|
|
function matchDirectory(string $dir) : array
|
|
{
|
|
$dir = rtrim($dir, '/\\');
|
|
$dir .= DIRECTORY_SEPARATOR;
|
|
|
|
$filtered = [];
|
|
|
|
foreach($this->changed as $path => $_)
|
|
if(strpos($path, $dir) === 0)
|
|
$filtered[] = $path;
|
|
|
|
foreach($this->removed as $path => $_)
|
|
if(strpos($path, $dir) === 0)
|
|
$filtered[] = $path;
|
|
|
|
return $filtered;
|
|
}
|
|
|
|
function matchFiles(iterable $files) : array
|
|
{
|
|
$filtered = [];
|
|
foreach($files as $file)
|
|
{
|
|
if(isset($this->changed[$file]) || isset($this->removed[$file]))
|
|
$filtered[] = $file;
|
|
}
|
|
return $filtered;
|
|
}
|
|
}
|
|
|