diff --git a/artefact.inc.php b/artefact.inc.php new file mode 100644 index 0000000..71d6edd --- /dev/null +++ b/artefact.inc.php @@ -0,0 +1,386 @@ +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*/ + 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); +} diff --git a/internal.inc.php b/internal.inc.php new file mode 100644 index 0000000..d3bcfa6 --- /dev/null +++ b/internal.inc.php @@ -0,0 +1,257 @@ +") +{ + echo "\nUsage:\n $script_name [OPTIONS] [,,..] [-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[,,..] [-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;$ihasProp('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_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 -//}}} +} diff --git a/tasks.inc.php b/tasks.inc.php new file mode 100644 index 0000000..6ef6fa8 --- /dev/null +++ b/tasks.inc.php @@ -0,0 +1,72 @@ +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"; +}); + diff --git a/util.inc.php b/util.inc.php new file mode 100644 index 0000000..dca1f89 --- /dev/null +++ b/util.inc.php @@ -0,0 +1,247 @@ + $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 = "") +{ + internal\_default_usage($script_name); +} +