Starting to implement support for task artefacts and its dependencies

This commit is contained in:
Pavel Shevaev 2025-03-06 13:13:11 +03:00
parent 4cf34b0dba
commit 5cae1bf622
5 changed files with 1159 additions and 559 deletions

386
artefact.inc.php Normal file
View File

@ -0,0 +1,386 @@
<?php
namespace taskman\artefact;
use Exception;
class TaskmanArtefact
{
private string $path;
private array $sources_fn = array();
private array $sources_spec = array();
private iterable $sources = array();
private array $sources_changed = array();
private array $sources_changed_fn = array();
private array $sources_newer = array();
function __construct(string $path, array $sources_spec)
{
$this->path = $path;
$this->sources_spec = $sources_spec;
}
function getPath() : string
{
return $this->path;
}
function getSourcesSpec() : array
{
return $this->sources_spec;
}
function getSources(int $idx) : iterable
{
if(isset($this->sources[$idx]))
return $this->sources[$idx];
if(isset($this->sources_fn[$idx]))
{
$fn = $this->sources_fn[$idx];
$sources = $fn();
$this->sources[$idx] = $sources;
return $sources;
}
return array();
}
function setSourcesFn(int $idx, \Closure $fn)
{
$this->sources_fn[$idx] = $fn;
}
function setSourcesChangedFn(int $idx, \Closure $fn)
{
$this->sources_changed_fn[$idx] = $fn;
}
function getChangedSources(int $idx) : iterable
{
if(isset($this->sources_changed[$idx]))
return $this->sources_changed[$idx];
if(isset($this->sources_changed_fn[$idx]))
{
$fn = $this->sources_changed_fn[$idx];
$changed = $fn();
$this->sources_changed[$idx] = $changed;
return $changed;
}
return array();
}
function isSourcesNewer(int $idx) : bool
{
return isset($this->sources_newer[$idx]) && $this->sources_newer[$idx];
}
function getNewerSourcesIndices() : array
{
return array_keys($this->sources_newer);
}
function isStale() : bool
{
return count($this->sources_newer) > 0;
}
function initSources(?\taskman\TaskmanFileChanges $file_changes)
{
foreach($this->getSourcesSpec() as $src_idx => $src_spec)
{
//[[dir1, dir2, ..], [ext1, ext2, ..]]
if(is_array($src_spec) && count($src_spec) == 2 &&
is_array($src_spec[0]) && is_array($src_spec[1]))
{
$this->setSourcesFn($src_idx, function() use($src_spec) {
$dir2files = array();
foreach($src_spec[0] as $spec_dir)
$dir2files[$spec_dir] = scan_files([$spec_dir], $src_spec[1]);
return new TaskmanDirFiles($dir2files);
});
if($file_changes != null)
{
$this->setSourcesChangedFn($src_idx, function() use($src_spec, $file_changes) {
$changed = array();
foreach($src_spec[0] as $spec_dir)
{
$matches = $file_changes->matchDirectory($spec_dir);
$changed[$spec_dir] = $matches;
}
return new TaskmanDirFiles($changed);
});
}
}
else if(is_array($src_spec) || $src_spec instanceof \Iterator)
{
$this->setSourcesFn($src_idx, fn() => $src_spec);
if($file_changes != null)
{
$this->setSourcesChangedFn($src_idx,
fn() => $file_changes->matchFiles($this->getSources($src_idx))
);
}
}
else
throw new Exception("Unknown artefact '{$this->getPath()}' source type" . gettype($src_spec));
}
}
function checkNewerSources(?\taskman\TaskmanFileChanges $file_changes) : bool
{
foreach($this->getSourcesSpec() as $src_idx => $src_spec)
{
$sources = $file_changes != null ? $this->getChangedSources($src_idx) : $this->getSources($src_idx);
if(is_stale($this->getPath(), $sources))
{
$this->sources_newer[$src_idx] = true;
return true;
}
}
return false;
}
}
class TaskmanDirFiles implements \ArrayAccess, \Countable, \Iterator
{
/*var array<string, string[]>*/
private array $dir2files = array();
private $iter_pos = 0;
function __construct(array $dir2files = array())
{
foreach($dir2files as $dir => $files)
$this->dir2files[$dir] = $files;
}
function toMap() : array
{
return $this->dir2files;
}
function clear()
{
$this->dir2files = array();
}
function isEmpty() : bool
{
return empty($this->dir2files);
}
function count() : int
{
$total = 0;
foreach($this->dir2files as $base_dir => $files)
$total += count($files);
return $total;
}
function apply(callable $fn)
{
foreach($this->dir2files as $base_dir => $files)
$this->dir2files[$base_dir] = $fn($base_dir, $files);
}
function filter(callable $filter)
{
foreach($this->dir2files as $base_dir => $files)
$this->dir2files[$base_dir] = array_filter($files, $filter);
}
function forEachFile(callable $fn)
{
foreach($this->dir2files as $base_dir => $files)
{
foreach($files as $file)
$fn($base_dir, $file);
}
}
function add(string $base_dir, string $file)
{
if(!isset($this->dir2files[$base_dir]))
$this->dir2files[$base_dir] = array();
$this->dir2files[$base_dir][] = $file;
}
//returns [[base_dir, file1], [base_dir, file2], ...]
function getFlatArray() : array
{
$flat = [];
foreach($this->dir2files as $base_dir => $files)
{
foreach($files as $file)
$flat[] = [$base_dir, $file];
}
return $flat;
}
function getAllFiles() : array
{
$all_files = [];
foreach($this->dir2files as $base_dir => $files)
$all_files = array_merge($all_files, $files);
return $all_files;
}
//ArrayAccess interface
function offsetExists(mixed $offset) : bool
{
if(!is_int($offset))
throw new Exception("Invalid offset");
return $this->count() > $offset;
}
function offsetGet(mixed $offset) : mixed
{
if(!is_int($offset))
throw new Exception("Invalid offset");
foreach($this->dir2files as $base_dir => $files)
{
$n = count($files);
if($offset - $n < 0)
return $files[$offset];
$offset -= $n;
}
return null;
}
function offsetSet(mixed $offset, mixed $value) : void
{
if(!is_int($offset))
throw new Exception("Invalid offset");
foreach($this->dir2files as $base_dir => &$files)
{
$n = count($files);
if($offset - $n < 0)
{
$files[$offset] = $value;
return;
}
$offset -= $n;
}
}
function offsetUnset(mixed $offset) : void
{
if(!is_int($offset))
throw new Exception("Invalid offset");
foreach($this->dir2files as $base_dir => $files)
{
$n = count($files);
if($offset - $n < 0)
{
unset($files[$offset]);
return;
}
$offset -= $n;
}
}
//Iterator interface
function rewind() : void
{
$this->iter_pos = 0;
}
function current() : mixed
{
return $this->offsetGet($this->iter_pos);
}
function key() : mixed
{
return $this->iter_pos;
}
function next() : void
{
++$this->iter_pos;
}
function valid() : bool
{
return $this->offsetExists($this->iter_pos);
}
}
function is_stale(string $file, iterable $deps) : bool
{
if(!is_file($file))
return true;
$fmtime = filemtime($file);
foreach($deps as $dep)
{
if($dep && is_file($dep) && (filemtime($dep) > $fmtime))
return true;
}
return false;
}
function scan_files(array $dirs, array $only_extensions = [], int $mode = 1) : array
{
$files = array();
foreach($dirs as $dir)
{
if(!is_dir($dir))
continue;
$dir = normalize_path($dir);
$iter_mode = $mode == 1 ? \RecursiveIteratorIterator::LEAVES_ONLY : \RecursiveIteratorIterator::SELF_FIRST;
$iter = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir), $iter_mode);
foreach($iter as $filename => $cur)
{
if(($mode == 1 && !$cur->isDir()) ||
($mode == 2 && $cur->isDir()))
{
if(!$only_extensions)
$files[] = $filename;
else
{
$flen = strlen($filename);
foreach($only_extensions as $ext)
{
if(substr_compare($filename, $ext, $flen-strlen($ext)) === 0)
$files[] = $filename;
}
}
}
}
}
return $files;
}
function normalize_path(string $path) : string
{
$path = str_replace('\\', '/', $path);
$path = preg_replace('/\/+/', '/', $path);
$parts = explode('/', $path);
$absolutes = array();
foreach($parts as $part)
{
if('.' == $part)
continue;
if('..' == $part)
array_pop($absolutes);
else
$absolutes[] = $part;
}
return implode(DIRECTORY_SEPARATOR, $absolutes);
}

257
internal.inc.php Normal file
View File

@ -0,0 +1,257 @@
<?php
namespace taskman\internal;
use Exception;
function _default_logger($msg)
{
echo $msg;
}
function _default_usage($script_name = "<taskman-script>")
{
echo "\nUsage:\n $script_name [OPTIONS] <task-name1>[,<task-name2>,..] [-D PROP1=value [-D PROP2]]\n\n";
echo "Available options:\n";
echo " -c specify PHP script to be included (handy for setting props,config options,etc)\n";
echo " -V be super verbose\n";
echo " -q be quite, only system messages\n";
echo " -b batch mode: be super quite, don't even output any system messages\n";
echo " -- pass all options verbatim after this mark\n";
}
function _collect_tasks()
{
global $TASKMAN_TASKS;
global $TASKMAN_TASK_ALIASES;
global $TASKMAN_FILE_CHANGES;
$TASKMAN_TASKS = array();
$TASKMAN_TASK_ALIASES = array();
$cands = _get_task_candidates();
foreach($cands as $name => $args)
{
if(isset($TASKMAN_TASKS[$name]))
throw new \taskman\TaskmanException("Task '$name' is already defined");
if(is_array($args))
{
$props = array();
if(sizeof($args) > 2)
{
$props = $args[1];
$func = $args[2];
}
else
$func = $args[1];
$task = new \taskman\TaskmanTask($func, $name, $props, $TASKMAN_FILE_CHANGES);
}
else
throw new Exception("Task '$name' is invalid");
$TASKMAN_TASKS[$name] = $task;
foreach($task->getAliases() as $alias)
{
if(isset($TASKMAN_TASKS[$alias]) || isset($TASKMAN_TASK_ALIASES[$alias]))
throw new \taskman\TaskmanException("Alias '$alias' is already defined for task '$name'");
$TASKMAN_TASK_ALIASES[$alias] = $task;
}
}
_validate_tasks();
}
function _validate_tasks()
{
global $TASKMAN_TASKS;
foreach($TASKMAN_TASKS as $task)
{
try
{
$before = $task->getPropOr("before", "");
if($before)
\taskman\get_task($before)->addBeforeDep($task);
$after = $task->getPropOr("after", "");
if($after)
\taskman\get_task($after)->addAfterDep($task);
foreach($task->getDeps() as $dep_task)
\taskman\get_task($dep_task);
}
catch(Exception $e)
{
throw new Exception("Task '{$task->getName()}' validation error: " . $e->getMessage());
}
}
}
function _get_task_candidates()
{
global $TASKMAN_CLOSURES;
$cands = array();
//get tasks defined as closures
foreach($TASKMAN_CLOSURES as $name => $args)
{
if(isset($cands[$name]))
throw new Exception("Task '$name' is already defined");
$cands[$name] = $args;
}
ksort($cands);
return $cands;
}
function _resolve_callable_prop($name)
{
$prop = $GLOBALS['TASKMAN_PROP_' . $name];
if(!($prop instanceof \Closure))
return;
$value = $prop();
$GLOBALS['TASKMAN_PROP_' . $name] = $value;
}
function _isset_task($task)
{
global $TASKMAN_TASKS;
global $TASKMAN_TASK_ALIASES;
return isset($TASKMAN_TASKS[$task]) || isset($TASKMAN_TASK_ALIASES[$task]);
}
function _get_hints($task)
{
global $TASKMAN_TASKS;
global $TASKMAN_TASK_ALIASES;
$tasks = array_merge(array_keys($TASKMAN_TASKS), array_keys($TASKMAN_TASK_ALIASES));
$found = array_filter($tasks, function($v) use($task) { return strpos($v, $task) === 0; });
$found = array_merge($found, array_filter($tasks, function($v) use($task) { $pos = strpos($v, $task); return $pos !== false && $pos > 0; }));
return $found;
}
//e.g: run,build,zip
function _parse_taskstr($str)
{
$task_spec = array();
$items = explode(',', $str);
foreach($items as $item)
{
$args = null;
$task = $item;
if(strpos($item, ' ') !== false)
@list($task, $args) = explode(' ', $item, 2);
if($args)
$task_spec[] = array($task, explode(' ', $args));
else
$task_spec[] = $task;
}
return $task_spec;
}
function _read_env_vars()
{
$envs = getenv();
foreach($envs as $k => $v)
{
if(strpos($k, 'TASKMAN_SET_') === 0)
{
$prop_name = substr($k, 12);
\taskman\log(0, "Setting prop '$prop_name' (with env '$k')\n");
\taskman\set($prop_name, $v);
}
}
}
function _process_argv(&$argv)
{
global $TASKMAN_LOG_LEVEL;
global $TASKMAN_BATCH;
global $TASKMAN_NO_DEPS;
global $TASKMAN_FILE_CHANGES;
$filtered = array();
$process_defs = false;
for($i=0;$i<sizeof($argv);++$i)
{
$v = $argv[$i];
if($v == '--')
{
for($j=$i+1;$j<sizeof($argv);++$j)
$filtered[] = $argv[$j];
break;
}
else if($v == '-D')
{
$process_defs = true;
}
else if($v == '-V')
{
$TASKMAN_LOG_LEVEL = 2;
}
else if($v == '-q')
{
$TASKMAN_LOG_LEVEL = 0;
}
else if($v == '-b')
{
$TASKMAN_LOG_LEVEL = -1;
}
else if($v == '-O')
{
$TASKMAN_NO_DEPS = true;
}
else if($v == '-c')
{
if(!isset($argv[$i+1]))
throw new \taskman\TaskmanException("Configuration file(-c option) is missing");
require_once($argv[$i+1]);
++$i;
}
else if($v == '-F')
{
if(!isset($argv[$i+1]))
throw new \taskman\TaskmanException("Argument(-F option) is missing");
$TASKMAN_FILE_CHANGES = \taskman\TaskmanFileChanges::parse($argv[$i+1]);
++$i;
}
else if($process_defs)
{
$eq_pos = strpos($v, '=');
if($eq_pos !== false)
{
$def_name = substr($v, 0, $eq_pos);
$def_value = substr($v, $eq_pos+1);
//TODO: this code must be more robust
if(strtolower($def_value) === 'true')
$def_value = true;
else if(strtolower($def_value) === 'false')
$def_value = false;
}
else
{
$def_name = $v;
$def_value = 1;
}
\taskman\log(0, "Setting prop '$def_name'=" . var_export($def_value, true) . "\n");
\taskman\set($def_name, $def_value);
$process_defs = false;
}
else
$filtered[] = $v;
}
$argv = $filtered;
}

View File

@ -1,5 +1,11 @@
<?php
namespace {
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();
@ -9,51 +15,43 @@ $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_default_usage';
$GLOBALS['TASKMAN_LOGGER'] = '_taskman_default_logger';
$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;
function _taskman_default_logger($msg)
{
echo $msg;
}
function _taskman_default_usage($script_name = "<taskman-script>")
{
echo "\nUsage:\n $script_name [OPTIONS] <task-name1>[,<task-name2>,..] [-D PROP1=value [-D PROP2]]\n\n";
echo "Available options:\n";
echo " -c specify PHP script to be included (handy for setting props,config options,etc)\n";
echo " -V be super verbose\n";
echo " -q be quite, only system messages\n";
echo " -b batch mode: be super quite, don't even output any system messages\n";
echo " -- pass all options verbatim after this mark\n";
}
} //namespace global
namespace taskman {
use Exception;
$GLOBALS['TASKMAN_FILES_CHANGES'] = null;
class TaskmanException extends Exception
{}
class TaskmanTask
{
private $func;
private $name;
private $file;
private $line;
private $props = array();
private $is_running = false;
private $has_run = array();
private $args = array();
private $deps = null;
private $aliases = null;
private $before_deps = array();
private $after_deps = array();
private \Closure $func;
function __construct(\Closure $func, $name, $props = array())
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();
@ -62,38 +60,40 @@ class TaskmanTask
$this->func = $func;
$this->name = $name;
$this->props = $props;
$this->file_changes = $file_changes;
}
function getName()
function getName() : string
{
return $this->name;
}
function getFile()
function getFile() : string
{
return $this->file;
}
function getLine()
function getLine() : int
{
return $this->line;
}
function getFunc()
function getFunc() : \Closure
{
return $this->func;
}
function getArgs()
function getArgs() : array
{
return $this->args;
}
function getAliases() : array
{
if($this->aliases === null)
$this->aliases = $this->_parseAliases();
return $this->aliases;
if($this->_aliases === null)
$this->_aliases = $this->_parseAliases();
return $this->_aliases;
}
function _parseAliases() : array
@ -107,6 +107,44 @@ class TaskmanTask
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;
@ -120,13 +158,19 @@ class TaskmanTask
$this->is_running)
return;
$this->is_running = true;
$this->args = $args;
$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;
@ -143,7 +187,7 @@ class TaskmanTask
$bench = microtime(true);
$task_result = call_user_func_array($this->func, array($this->args));
$task_result = call_user_func_array($this->func, array($this->args, $this));
array_pop($TASKMAN_STACK);
if(!$TASKMAN_NO_DEPS)
@ -190,559 +234,153 @@ class TaskmanTask
if(is_array($deps))
return $deps;
else if($deps && is_string($deps))
return _parse_taskstr($deps);
return internal\_parse_taskstr($deps);
return array();
}
static function extractName($func)
{
if(strpos($func, "task_") === 0)
return substr($func, strlen('task_'), strlen($func));
else if(strpos($func, 'taskman\task_') === 0)
return substr($func, strlen('taskman\task_'), strlen($func));
}
function getPropOr($name, $def)
function getPropOr(string $name, mixed $def) : mixed
{
return isset($this->props[$name]) ? $this->props[$name] : $def;
}
function getProp($name)
function getProp(string $name) : mixed
{
return $this->getPropOr($name, null);
}
function hasProp($name)
function hasProp(string $name) : bool
{
return isset($this->props[$name]);
}
function getProps()
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;
}
}
function _collect_tasks()
class TaskmanFileChanges
{
global $TASKMAN_TASKS;
global $TASKMAN_TASK_ALIASES;
private $changed = array();
private $removed = array();
$TASKMAN_TASKS = array();
$TASKMAN_TASK_ALIASES = array();
$cands = _get_task_candidates();
foreach($cands as $name => $args)
static function parse(string $json_or_file)
{
if(isset($TASKMAN_TASKS[$name]))
throw new TaskmanException("Task '$name' is already defined");
if(is_array($args))
{
$props = array();
if(sizeof($args) > 2)
{
$props = $args[1];
$func = $args[2];
}
else
$func = $args[1];
$task = new TaskmanTask($func, $name, $props);
}
if($json_or_file[0] == '[')
$json = $json_or_file;
else
throw new Exception("Task '$name' is invalid");
$json = file_get_contents($json_or_file);
$TASKMAN_TASKS[$name] = $task;
$decoded = json_decode($json, true);
if(!is_array($decoded))
throw new Exception('Bad json');
foreach($task->getAliases() as $alias)
$changed = array();
$removed = array();
$base_dir = dirname($_SERVER['PHP_SELF']);
foreach($decoded as $items)
{
if(isset($TASKMAN_TASKS[$alias]) || isset($TASKMAN_TASK_ALIASES[$alias]))
throw new TaskmanException("Alias '$alias' is already defined for task '$name'");
$TASKMAN_TASK_ALIASES[$alias] = $task;
}
}
if(count($items) != 2)
throw new Exception('Bad entry');
list($status, $file) = $items;
foreach($TASKMAN_TASKS as $task)
{
try
{
$before = $task->getPropOr("before", "");
if($before)
get_task($before)->addBeforeDep($task);
$after = $task->getPropOr("after", "");
if($after)
get_task($after)->addAfterDep($task);
foreach($task->getDeps() as $dep_task)
get_task($dep_task);
}
catch(Exception $e)
{
throw new Exception("Task '{$task->getName()}' validation error: " . $e->getMessage());
}
}
}
function _get_task_candidates()
{
global $TASKMAN_CLOSURES;
$cands = array();
//get tasks defined as closures
foreach($TASKMAN_CLOSURES as $name => $args)
$cands[$name] = $args;
ksort($cands);
return $cands;
}
function get_task(string $task) : TaskmanTask
{
global $TASKMAN_TASKS;
global $TASKMAN_TASK_ALIASES;
if(!is_scalar($task))
throw new TaskmanException("Bad task name");
if(isset($TASKMAN_TASKS[$task]))
return $TASKMAN_TASKS[$task];
if(isset($TASKMAN_TASK_ALIASES[$task]))
return $TASKMAN_TASK_ALIASES[$task];
throw new TaskmanException("Task with name/alias '{$task}' does not exist");
}
function _resolve_callable_prop($name)
{
$prop = $GLOBALS['TASKMAN_PROP_' . $name];
if(!($prop instanceof \Closure))
return;
$value = $prop();
$GLOBALS['TASKMAN_PROP_' . $name] = $value;
}
function get($name)
{
if(!isset($GLOBALS['TASKMAN_PROP_' . $name]))
throw new TaskmanException("Property '$name' is not set");
_resolve_callable_prop($name);
return $GLOBALS['TASKMAN_PROP_' . $name];
}
function getor($name, $def)
{
if(!isset($GLOBALS['TASKMAN_PROP_' . $name]))
return $def;
_resolve_callable_prop($name);
return $GLOBALS['TASKMAN_PROP_' . $name];
}
function set($name, $value)
{
$GLOBALS['TASKMAN_PROP_' . $name] = $value;
}
function setor($name, $value)
{
if(!isset($GLOBALS['TASKMAN_PROP_' . $name]))
$GLOBALS['TASKMAN_PROP_' . $name] = $value;
}
function is($name)
{
return isset($GLOBALS['TASKMAN_PROP_' . $name]);
}
function del($name)
{
unset($GLOBALS['TASKMAN_PROP_' . $name]);
}
function props()
{
$props = array();
foreach($GLOBALS as $key => $value)
{
if(($idx = strpos($key, 'TASKMAN_PROP_')) === 0)
{
$name = substr($key, strlen('TASKMAN_PROP_'));
$props[$name] = get($name);
}
}
return $props;
}
function task($name)
{
global $TASKMAN_CLOSURES;
//TODO: can it be a feature?
if(isset($TASKMAN_CLOSURES[$name]))
throw new TaskmanException("Task '$name' is already defined");
$args = func_get_args();
$TASKMAN_CLOSURES[$name] = $args;
}
function get_tasks() : array
{
global $TASKMAN_TASKS;
return $TASKMAN_TASKS;
}
function current_task()
{
global $TASKMAN_CURRENT_TASK;
return $TASKMAN_CURRENT_TASK;
}
function run($task, array $args = array())
{
if($task instanceof TaskmanTask)
$task_obj = $task;
else
$task_obj = get_task($task);
return $task_obj->run($args);
}
function run_many($tasks, $args = array())
{
foreach($tasks as $task_spec)
{
if(is_array($task_spec))
run($task_spec[0], $task_spec[1]);
else
run($task_spec, $args);
}
}
function msg_dbg($msg)
{
_log($msg, 2);
}
function msg($msg)
{
_log($msg, 1);
}
function msg_sys($msg)
{
_log($msg, 0);
}
function _log($msg, $level = 1)
{
global $TASKMAN_LOG_LEVEL;
if($TASKMAN_LOG_LEVEL < $level)
return;
$logger = $GLOBALS['TASKMAN_LOGGER'];
call_user_func_array($logger, array($msg));
}
function _($str)
{
if(strpos($str, '%') === false)
return $str;
$str = preg_replace_callback(
'~%\(([^\)]+)\)%~',
function($m) { return get($m[1]); },
$str
);
return $str;
}
function _isset_task($task)
{
global $TASKMAN_TASKS;
global $TASKMAN_TASK_ALIASES;
return isset($TASKMAN_TASKS[$task]) || isset($TASKMAN_TASK_ALIASES[$task]);
}
function _get_hints($task)
{
global $TASKMAN_TASKS;
global $TASKMAN_TASK_ALIASES;
$tasks = array_merge(array_keys($TASKMAN_TASKS), array_keys($TASKMAN_TASK_ALIASES));
$found = array_filter($tasks, function($v) use($task) { return strpos($v, $task) === 0; });
$found = array_merge($found, array_filter($tasks, function($v) use($task) { $pos = strpos($v, $task); return $pos !== false && $pos > 0; }));
return $found;
}
//e.g: run,build,zip
function _parse_taskstr($str)
{
$task_spec = array();
$items = explode(',', $str);
foreach($items as $item)
{
$args = null;
$task = $item;
if(strpos($item, ' ') !== false)
@list($task, $args) = explode(' ', $item, 2);
if($args)
$task_spec[] = array($task, explode(' ', $args));
else
$task_spec[] = $task;
}
return $task_spec;
}
function _read_env_vars()
{
$envs = getenv();
foreach($envs as $k => $v)
{
if(strpos($k, 'TASKMAN_SET_') === 0)
{
$prop_name = substr($k, 12);
msg_sys("Setting prop '$prop_name' (with env '$k')\n");
set($prop_name, $v);
}
}
}
function _process_argv(&$argv)
{
global $TASKMAN_LOG_LEVEL;
global $TASKMAN_BATCH;
global $TASKMAN_NO_DEPS;
$filtered = array();
$process_defs = false;
for($i=0;$i<sizeof($argv);++$i)
{
$v = $argv[$i];
if($v == '--')
{
for($j=$i+1;$j<sizeof($argv);++$j)
$filtered[] = $argv[$j];
break;
}
else if($v == '-D')
{
$process_defs = true;
}
else if($v == '-V')
{
$TASKMAN_LOG_LEVEL = 2;
}
else if($v == '-q')
{
$TASKMAN_LOG_LEVEL = 0;
}
else if($v == '-b')
{
$TASKMAN_LOG_LEVEL = -1;
}
else if($v == '-O')
{
$TASKMAN_NO_DEPS = true;
}
else if($v == '-c')
{
if(!isset($argv[$i+1]))
throw new TaskmanException("Configuration file(-c option) is missing");
require_once($argv[$i+1]);
++$i;
}
else if($process_defs)
{
$eq_pos = strpos($v, '=');
if($eq_pos !== false)
if(strlen($file) > 0)
{
$def_name = substr($v, 0, $eq_pos);
$def_value = substr($v, $eq_pos+1);
//TODO: this code must be more robust
if(strtolower($def_value) === 'true')
$def_value = true;
else if(strtolower($def_value) === 'false')
$def_value = false;
}
else
{
$def_name = $v;
$def_value = 1;
}
msg_sys("Setting prop '$def_name'=" . var_export($def_value, true) . "\n");
set($def_name, $def_value);
$process_defs = false;
}
else
$filtered[] = $v;
}
$argv = $filtered;
}
function main($argv = array(), $help_func = null, $proc_argv = true, $read_env_vars = true)
{
$GLOBALS['TASKMAN_START_TIME'] = microtime(true);
if($help_func)
$GLOBALS['TASKMAN_HELP_FUNC'] = $help_func;
if($read_env_vars)
_read_env_vars();
if($proc_argv)
_process_argv($argv);
$GLOBALS['TASKMAN_SCRIPT'] = array_shift($argv);
_collect_tasks();
$always_tasks = array();
$default_task = null;
foreach(get_tasks() as $task_obj)
{
if($task_obj->hasProp('always'))
array_unshift($always_tasks, $task_obj);
if($task_obj->hasProp('default'))
{
if($default_task)
throw new TaskmanException("Assigned default task '" . $default_task->getName() . "' conflicts with '" . $task_obj->getName() . "'");
else
$default_task = $task_obj;
}
}
foreach($always_tasks as $always_task)
$always_task->run(array());
if(sizeof($argv) > 0)
{
$task_str = array_shift($argv);
$tasks = _parse_taskstr($task_str);
if(count($tasks) == 1 && !_isset_task($tasks[0]))
{
$pattern = $tasks[0];
if($pattern[0] == '~')
{
$pattern = substr($pattern, 1, strlen($pattern) - 1);
$is_similar = true;
}
elseif(substr($pattern, -1, 1) == '~')
{
$pattern = substr($pattern, 0, strlen($pattern) - 1);
$is_similar = true;
}
else
$is_similar = false;
$hints = _get_hints($pattern);
if($is_similar && count($hints) == 1)
$tasks = $hints;
else
{
printf("ERROR! Task %s not found\n", $tasks[0]);
if($hints)
if(DIRECTORY_SEPARATOR == '/')
{
printf("Similar tasks:\n");
foreach($hints as $hint)
printf(" %s\n", $hint);
if($file[0] != '/')
$file = $base_dir . DIRECTORY_SEPARATOR . $file;
}
exit(1);
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);
}
run_many($tasks, $argv);
}
else if($default_task)
$default_task->run($argv);
return new TaskmanFileChanges($changed, $removed);
}
msg_sys("***** All done (".round(microtime(true)-$GLOBALS['TASKMAN_START_TIME'],2)." sec.) *****\n");
}
function usage($script_name = "<taskman-script>")
{
\_taskman_default_usage($script_name);
}
task('help', function($args = array())
{
$filter = '';
if(isset($args[0]))
$filter = $args[0];
$maxlen = -1;
$tasks = array();
$all = get_tasks();
foreach($all as $task)
//NOTE: these are actually maps: file => true
function __construct(array $changed, array $removed)
{
if($filter && (strpos($task->getName(), $filter) === false && strpos($task->getPropOr("alias", ""), $filter) === false))
continue;
if(strlen($task->getName()) > $maxlen)
$maxlen = strlen($task->getName());
$tasks[] = $task;
$this->changed = $changed;
$this->removed = $removed;
}
if(!$args)
function isEmpty() : bool
{
$help_func = $GLOBALS['TASKMAN_HELP_FUNC'];
$help_func();
echo "\n";
return count($this->changed) == 0 && count($this->removed) == 0;
}
echo "Available tasks:\n";
foreach($tasks as $task)
function matchDirectory(string $dir) : array
{
$props_string = '';
$pad = $maxlen - strlen($task->getName());
foreach($task->getProps() as $name => $value)
$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)
{
$props_string .= str_repeat(" ", $pad) .' @' . $name . ' ' . (is_string($value) ? $value : json_encode($value)) . "\n";
$pad = $maxlen + 1;
if(isset($this->changed[$file]) || isset($this->removed[$file]))
$filtered[] = $file;
}
$props_string = rtrim($props_string);
echo "---------------------------------\n";
$file = $task->getFile();
$line = $task->getLine();
echo " " . $task->getName() . $props_string . " ($file@$line)\n";
return $filtered;
}
echo "\n";
});
task('props', function($args = [])
{
$filter = '';
if(isset($args[0]))
$filter = $args[0];
$props = props();
echo "\n";
echo "Available props:\n";
foreach($props as $k => $v)
{
if($filter && stripos($k, $filter) === false)
continue;
echo "---------------------------------\n";
echo "$k : " . var_export($v, true) . "\n";
}
echo "\n";
});
} //namespace taskman
//}}}
}

72
tasks.inc.php Normal file
View File

@ -0,0 +1,72 @@
<?php
namespace taskman;
use Exception;
task('help', function($args = array())
{
$filter = '';
if(isset($args[0]))
$filter = $args[0];
$maxlen = -1;
$tasks = array();
$all = get_tasks();
foreach($all as $task)
{
if($filter && (strpos($task->getName(), $filter) === false && strpos($task->getPropOr("alias", ""), $filter) === false))
continue;
if(strlen($task->getName()) > $maxlen)
$maxlen = strlen($task->getName());
$tasks[] = $task;
}
if(!$args)
{
$help_func = $GLOBALS['TASKMAN_HELP_FUNC'];
$help_func();
echo "\n";
}
echo "Available tasks:\n";
foreach($tasks as $task)
{
$props_string = '';
$pad = $maxlen - strlen($task->getName());
foreach($task->getProps() as $name => $value)
{
$props_string .= str_repeat(" ", $pad) .' @' . $name . ' ' . (is_string($value) ? $value : json_encode($value)) . "\n";
$pad = $maxlen + 1;
}
$props_string = rtrim($props_string);
echo "---------------------------------\n";
$file = $task->getFile();
$line = $task->getLine();
echo " " . $task->getName() . $props_string . " ($file@$line)\n";
}
echo "\n";
});
task('props', function($args = [])
{
$filter = '';
if(isset($args[0]))
$filter = $args[0];
$props = props();
echo "\n";
echo "Available props:\n";
foreach($props as $k => $v)
{
if($filter && stripos($k, $filter) === false)
continue;
echo "---------------------------------\n";
echo "$k : " . var_export($v, true) . "\n";
}
echo "\n";
});

247
util.inc.php Normal file
View File

@ -0,0 +1,247 @@
<?php
namespace taskman;
use Exception;
function get_task(string $task) : TaskmanTask
{
global $TASKMAN_TASKS;
global $TASKMAN_TASK_ALIASES;
if(!is_scalar($task))
throw new TaskmanException("Bad task name");
if(isset($TASKMAN_TASKS[$task]))
return $TASKMAN_TASKS[$task];
if(isset($TASKMAN_TASK_ALIASES[$task]))
return $TASKMAN_TASK_ALIASES[$task];
throw new TaskmanException("Task with name/alias '{$task}' does not exist");
}
function get($name)
{
if(!isset($GLOBALS['TASKMAN_PROP_' . $name]))
throw new TaskmanException("Property '$name' is not set");
internal\_resolve_callable_prop($name);
return $GLOBALS['TASKMAN_PROP_' . $name];
}
function getor($name, $def)
{
if(!isset($GLOBALS['TASKMAN_PROP_' . $name]))
return $def;
internal\_resolve_callable_prop($name);
return $GLOBALS['TASKMAN_PROP_' . $name];
}
function set($name, $value)
{
$GLOBALS['TASKMAN_PROP_' . $name] = $value;
}
function setor($name, $value)
{
if(!isset($GLOBALS['TASKMAN_PROP_' . $name]))
$GLOBALS['TASKMAN_PROP_' . $name] = $value;
}
function is($name)
{
return isset($GLOBALS['TASKMAN_PROP_' . $name]);
}
function del($name)
{
unset($GLOBALS['TASKMAN_PROP_' . $name]);
}
function props()
{
$props = array();
foreach($GLOBALS as $key => $value)
{
if(($idx = strpos($key, 'TASKMAN_PROP_')) === 0)
{
$name = substr($key, strlen('TASKMAN_PROP_'));
$props[$name] = get($name);
}
}
return $props;
}
function task($name)
{
global $TASKMAN_CLOSURES;
$args = func_get_args();
$TASKMAN_CLOSURES[$name] = $args;
}
function get_tasks() : array
{
global $TASKMAN_TASKS;
return $TASKMAN_TASKS;
}
function current_task()
{
global $TASKMAN_CURRENT_TASK;
return $TASKMAN_CURRENT_TASK;
}
function run($task, array $args = array())
{
if($task instanceof TaskmanTask)
$task_obj = $task;
else
$task_obj = get_task($task);
return $task_obj->run($args);
}
function run_many($tasks, $args = array())
{
foreach($tasks as $task_spec)
{
if(is_array($task_spec))
run($task_spec[0], $task_spec[1]);
else
run($task_spec, $args);
}
}
//the lower the level the more important the message: 0 - is the highest priority
function log(int $level, $msg)
{
global $TASKMAN_LOG_LEVEL;
if($TASKMAN_LOG_LEVEL < $level)
return;
$logger = $GLOBALS['TASKMAN_LOGGER'];
call_user_func_array($logger, array($msg));
}
//obsolete
function _log(string $msg, int $level = 1)
{
log($level, $msg);
}
//TODO: obsolete
function msg_dbg(string $msg)
{
log(2, $msg);
}
//TODO: obsolete
function msg(string $msg)
{
log(1, $msg);
}
//TODO: obsolete
function msg_sys(string $msg)
{
log(0, $msg);
}
function _(string $str) : string
{
if(strpos($str, '%') === false)
return $str;
$str = preg_replace_callback(
'~%\(([^\)]+)\)%~',
function($m) { return get($m[1]); },
$str
);
return $str;
}
function main($argv = array(), $help_func = null, $proc_argv = true, $read_env_vars = true)
{
$GLOBALS['TASKMAN_START_TIME'] = microtime(true);
if($help_func)
$GLOBALS['TASKMAN_HELP_FUNC'] = $help_func;
if($read_env_vars)
internal\_read_env_vars();
if($proc_argv)
internal\_process_argv($argv);
$GLOBALS['TASKMAN_SCRIPT'] = array_shift($argv);
internal\_collect_tasks();
$always_tasks = array();
$default_task = null;
foreach(get_tasks() as $task_obj)
{
if($task_obj->hasProp('always'))
array_unshift($always_tasks, $task_obj);
if($task_obj->hasProp('default'))
{
if($default_task)
throw new TaskmanException("Assigned default task '" . $default_task->getName() . "' conflicts with '" . $task_obj->getName() . "'");
else
$default_task = $task_obj;
}
}
foreach($always_tasks as $always_task)
run($always_task);
if(sizeof($argv) > 0)
{
$task_str = array_shift($argv);
$tasks = internal\_parse_taskstr($task_str);
if(count($tasks) == 1 && !internal\_isset_task($tasks[0]))
{
$pattern = $tasks[0];
if($pattern[0] == '~')
{
$pattern = substr($pattern, 1, strlen($pattern) - 1);
$is_similar = true;
}
elseif(substr($pattern, -1, 1) == '~')
{
$pattern = substr($pattern, 0, strlen($pattern) - 1);
$is_similar = true;
}
else
$is_similar = false;
$hints = internal\_get_hints($pattern);
if($is_similar && count($hints) == 1)
$tasks = $hints;
else
{
printf("ERROR! Task %s not found\n", $tasks[0]);
if($hints)
{
printf("Similar tasks:\n");
foreach($hints as $hint)
printf(" %s\n", $hint);
}
exit(1);
}
}
run_many($tasks, $argv);
}
else if($default_task)
run($default_task, $argv);
msg_sys("***** All done (".round(microtime(true)-$GLOBALS['TASKMAN_START_TIME'],2)." sec.) *****\n");
}
function usage($script_name = "<taskman-script>")
{
internal\_default_usage($script_name);
}