Compare commits

...

10 Commits

Author SHA1 Message Date
Pavel Shevaev e68ad37770 Fixing main
Publish PHP Package / docker (push) Successful in 6s Details
2025-03-24 09:53:07 +03:00
Pavel Shevaev 8dc71ab534 Fixing disabled built-in tasks; Improving missing task error reporting
Publish PHP Package / docker (push) Successful in 6s Details
2025-03-24 09:50:17 +03:00
Pavel Shevaev d40c2fe201 Improving handling of file system events
Publish PHP Package / docker (push) Successful in 6s Details
2025-03-19 18:37:46 +03:00
Pavel Shevaev 0189c9d7af Adding more convenience stuff
Publish PHP Package / docker (push) Successful in 6s Details
2025-03-06 13:56:27 +03:00
Pavel Shevaev 3052d68f9b Restoring stuff
Publish PHP Package / docker (push) Successful in 10s Details
2025-03-06 13:19:36 +03:00
Pavel Shevaev 5cae1bf622 Starting to implement support for task artefacts and its dependencies 2025-03-06 13:17:43 +03:00
Pavel Shevaev 4cf34b0dba Fixing bug related to possible re-define of tasks
Publish PHP Package / docker (push) Successful in 9s Details
2025-03-03 15:39:36 +03:00
Pavel Shevaev 37c915e72e Improving startup times for before and after task dependencies
Publish PHP Package / docker (push) Successful in 6s Details
2025-02-24 19:04:27 +03:00
Pavel Shevaev 71205818be Making PHPStan happy
Publish PHP Package / docker (push) Successful in 7s Details
2024-08-07 13:24:14 +03:00
Pavel Shevaev c11528f477 Добавить CHANGELOG.md 2024-07-23 14:33:07 +03:00
6 changed files with 1188 additions and 517 deletions

2
CHANGELOG.md Normal file
View File

@ -0,0 +1,2 @@
## v2.3.0
- Adding support for enviroment variables prefixed with **TASKMAN_SET_** being set as props. For example: **TASKMAN_SET_FOO=1** results in property **FOO=1**

396
artefact.inc.php Normal file
View File

@ -0,0 +1,396 @@
<?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)
{
$all_src_specs = $this->getSourcesSpec();
//let's process a convenience special case
if(count($all_src_specs) > 0 && !is_array($all_src_specs[0]))
$all_src_specs = [$all_src_specs];
foreach($all_src_specs 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, $src_spec[1]);
$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 __toString() : string
{
return var_export($this->dir2files, true);
}
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);
}

284
internal.inc.php Normal file
View File

@ -0,0 +1,284 @@
<?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)
{
$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(array &$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;
}
function _extract_lines_from_file(string $file_path) : array
{
$lines = array();
$fh = fopen($file_path, 'r+');
if($fh === false)
return $lines;
try
{
if(flock($fh, LOCK_EX))
{
while(($line = fgets($fh)) !== false)
$lines[] = $line;
ftruncate($fh, 0);
flock($fh, LOCK_UN);
}
}
finally
{
fclose($fh);
}
return $lines;
}

View File

@ -1,5 +1,6 @@
<?php
namespace {
namespace taskman;
use Exception;
$GLOBALS['TASKMAN_TASKS'] = array();
$GLOBALS['TASKMAN_CLOSURES'] = array();
@ -9,47 +10,48 @@ $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;
$GLOBALS['TASKMAN_FILES_CHANGES'] = null;
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;
include_once(__DIR__ . '/internal.inc.php');
include_once(__DIR__ . '/util.inc.php');
include_once(__DIR__ . '/tasks.inc.php');
include_once(__DIR__ . '/artefact.inc.php');
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 \Closure $func;
private string $file;
private int $line;
private string $name;
//initialized lazily
private ?array $_aliases = null;
private $args = array();
function __construct(\Closure $func, $name, $props = 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();
@ -58,53 +60,43 @@ class TaskmanTask
$this->func = $func;
$this->name = $name;
$this->props = $props;
$this->file_changes = $file_changes;
}
function validate()
{
try
{
foreach($this->_getBeforeDeps() as $dep_task)
get_task($dep_task);
foreach($this->_getDeps() as $dep_task)
get_task($dep_task);
foreach($this->_getAfterDeps() as $dep_task)
get_task($dep_task);
}
catch(Exception $e)
{
throw new Exception("Task '{$this->name}' validation error: " . $e->getMessage());
}
}
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()
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))
@ -115,6 +107,50 @@ 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 getArtefact(int $idx) : artefact\TaskmanArtefact
{
return $this->getArtefacts()[$idx];
}
function getFileChanges() : ?TaskmanFileChanges
{
return $this->file_changes;
}
function isIncrementalBuild() : bool
{
return $this->file_changes != null;
}
function run($args = array())
{
global $TASKMAN_CURRENT_TASK;
@ -128,36 +164,42 @@ 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;
msg_sys("***** ".str_repeat('-', $level)."task '" . $this->getName() . "' start *****\n");
log(0, "***** ".str_repeat('-', $level)."task '" . $this->getName() . "' start *****\n");
if(!$TASKMAN_NO_DEPS)
{
run_many($this->_getBeforeDeps());
run_many($this->_getDeps());
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));
$task_result = call_user_func_array($this->func, array($this->args, $this));
array_pop($TASKMAN_STACK);
if(!$TASKMAN_NO_DEPS)
run_many($this->_getAfterDeps());
run_many($this->after_deps);
msg_sys("***** ".str_repeat('-', $level)."task '" . $this->getName() . "' done(" .
log(0, "***** ".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;
@ -175,420 +217,203 @@ class TaskmanTask
return $task_result;
}
private function _getBeforeDeps()
function addBeforeDep($task)
{
return $this->_collectRelatedTasks("before");
$this->before_deps[] = $task;
}
private function _getAfterDeps()
function addAfterDep($task)
{
return $this->_collectRelatedTasks("after");
$this->after_deps[] = $task;
}
private function _collectRelatedTasks($prop_name)
function getDeps() : array
{
$arr = array();
foreach(get_tasks() as $task_obj)
{
if($this->getName() == $task_obj->getName())
continue;
$value = $task_obj->getPropOr($prop_name, "");
if($value == $this->getName() || in_array($value, $this->getAliases()))
$arr[] = $task_obj;
}
return $arr;
if($this->deps === null)
$this->deps = $this->_parseDeps();
return $this->deps;
}
private function _getDeps()
private function _parseDeps() : array
{
$deps = $this->getPropOr('deps', "");
$deps = $this->getPropOr("deps", "");
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;
}
}
function _collect_tasks()
{
global $TASKMAN_TASKS;
global $TASKMAN_TASK_ALIASES;
$TASKMAN_TASKS = array();
$TASKMAN_TASK_ALIASES = array();
$cands = _get_task_candidates();
foreach($cands as $name => $args)
private function _checkIfArtefactsStale() : bool
{
if(isset($TASKMAN_TASKS[$name]))
throw new TaskmanException("Task '$name' is already defined");
$file_changes = $this->getFileChanges();
if(is_array($args))
$stale_found = false;
foreach($this->getArtefacts() as $artefact)
{
$props = array();
if(sizeof($args) > 2)
$artefact->initSources($file_changes);
if(!$stale_found && $artefact->checkNewerSources($file_changes))
$stale_found = true;
if($artefact->isStale())
{
$props = $args[1];
$func = $args[2];
log(0, "Task '{$this->name}' artefact '{$artefact->getPath()}' (sources at ".implode(',', $artefact->getNewerSourcesIndices()).") is stale\n");
}
else
$func = $args[1];
$task = new TaskmanTask($func, $name, $props);
}
return $stale_found;
}
function findAnyStaleArtefact() : ?artefact\TaskmanArtefact
{
foreach($this->getArtefacts() as $artefact)
{
if($artefact->isStale())
return $artefact;
}
return null;
}
}
class TaskmanFileChanges
{
const Changed = 1;
const Created = 2;
const Renamed = 3;
const Deleted = 4;
//file => status
private $changed = array();
static function parse(string $json_or_file)
{
if($json_or_file[0] == '[')
$json = $json_or_file;
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 TaskmanException("Alias '$alias' is already defined for task '$name'");
$TASKMAN_TASK_ALIASES[$alias] = $task;
$lines = internal\_extract_lines_from_file($json_or_file);
$json = '[' . implode(',', $lines) . ']';
}
}
foreach($TASKMAN_TASKS as $task)
$task->validate();
}
$decoded = json_decode($json, true);
if(!is_array($decoded))
throw new Exception('Bad json: ' . $json);
function _get_task_candidates()
{
global $TASKMAN_CLOSURES;
$changed = array();
$cands = array();
$base_dir = dirname($_SERVER['PHP_SELF']);
//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 get_task($task)
{
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)
foreach($decoded as $items)
{
$name = substr($key, strlen('TASKMAN_PROP_'));
$props[$name] = get($name);
}
}
return $props;
}
if(count($items) < 2)
throw new Exception('Bad entry');
list($status, $file) = $items;
function task($name)
{
global $TASKMAN_CLOSURES;
$args = func_get_args();
$TASKMAN_CLOSURES[$name] = $args;
}
function get_tasks()
{
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(null);
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;
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;
}
msg_sys("Setting prop '$def_name'=" . var_export($def_value, true) . "\n");
set($def_name, $def_value);
$process_defs = false;
$file = artefact\normalize_path($file);
if($status == 'Changed')
$changed[$file] = self::Changed;
else if($status == 'Created')
$changed[$file] = self::Created;
else if($status == 'Renamed')
$changed[$file] = self::Renamed;
else if($status == 'Deleted')
$changed[$file] = self::Deleted;
else
throw new Exception('Unknown status: ' . $status);
}
else
$filtered[] = $v;
return new TaskmanFileChanges($changed);
}
//NOTE: maps: file => status
function __construct(array $changed)
{
$this->changed = $changed;
}
function isEmpty() : bool
{
return count($this->changed) == 0;
}
function matchDirectory(string $dir, array $extensions = array()) : array
{
$dir = rtrim($dir, '/\\');
$dir .= DIRECTORY_SEPARATOR;
$filtered = [];
foreach($this->changed as $path => $_)
if(self::matchDirAndExtension($path, $dir, $extensions))
$filtered[] = $path;
return $filtered;
}
static function matchDirAndExtension(string $path, string $dir, array $extensions) : bool
{
if(strpos($path, $dir) !== 0)
return false;
foreach($extensions as $ext)
if(!str_ends_with($path, $ext))
return false;
return true;
}
function matchFiles(iterable $files) : array
{
$filtered = [];
foreach($files as $file)
{
if(isset($this->changed[$file]))
$filtered[] = $file;
}
return $filtered;
}
$argv = $filtered;
}
function main($argv = array(), $help_func = null, $proc_argv = true, $read_env_vars = true)
function main(
array $argv = array(),
callable $help_func = null,
bool $proc_argv = true,
bool $read_env_vars = true
)
{
$GLOBALS['TASKMAN_START_TIME'] = microtime(true);
@ -596,14 +421,14 @@ function main($argv = array(), $help_func = null, $proc_argv = true, $read_env_v
$GLOBALS['TASKMAN_HELP_FUNC'] = $help_func;
if($read_env_vars)
_read_env_vars();
internal\_read_env_vars();
if($proc_argv)
_process_argv($argv);
internal\_process_argv($argv);
$GLOBALS['TASKMAN_SCRIPT'] = array_shift($argv);
_collect_tasks();
internal\_collect_tasks();
$always_tasks = array();
$default_task = null;
@ -621,14 +446,14 @@ function main($argv = array(), $help_func = null, $proc_argv = true, $read_env_v
}
foreach($always_tasks as $always_task)
$always_task->run(array());
run($always_task);
if(sizeof($argv) > 0)
{
$task_str = array_shift($argv);
$tasks = _parse_taskstr($task_str);
$tasks = internal\_parse_taskstr($task_str);
if(count($tasks) == 1 && !_isset_task($tasks[0]))
if(count($tasks) == 1 && !internal\_isset_task($tasks[0]))
{
$pattern = $tasks[0];
if($pattern[0] == '~')
@ -643,104 +468,25 @@ function main($argv = array(), $help_func = null, $proc_argv = true, $read_env_v
}
else
$is_similar = false;
$hints = _get_hints($pattern);
$hints = internal\_get_hints($pattern);
if($is_similar && count($hints) == 1)
$tasks = $hints;
else
{
printf("ERROR! Task %s not found\n", $tasks[0]);
$similars = '';
if($hints)
{
printf("Similar tasks:\n");
foreach($hints as $hint)
printf(" %s\n", $hint);
}
exit(1);
$similars .= "\nSimilar tasks: " . implode(', ', $hints) . ".";
throw new Exception("Task '{$tasks[0]}' not found. $similars");
}
}
run_many($tasks, $argv);
}
else if($default_task)
$default_task->run($argv);
run($default_task, $argv);
msg_sys("***** All done (".round(microtime(true)-$GLOBALS['TASKMAN_START_TIME'],2)." sec.) *****\n");
log(0, "***** 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)
{
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";
});
} //namespace taskman
//}}}

73
tasks.inc.php Normal file
View File

@ -0,0 +1,73 @@
<?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";
});

170
util.inc.php Normal file
View File

@ -0,0 +1,170 @@
<?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;
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);
}
}
//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 usage($script_name = "<taskman-script>")
{
internal\_default_usage($script_name);
}