file = $refl->getFileName(); $this->line = $refl->getStartLine(); $this->func = $func; $this->name = $name; $this->props = $props; $this->file_changes = $file_changes; } function getName() : string { return $this->name; } function getFile() : string { return $this->file; } function getLine() : int { return $this->line; } function getFunc() : \Closure { return $this->func; } function getArgs() : array { return $this->args; } function getAliases() : array { if($this->_aliases === null) $this->_aliases = $this->_parseAliases(); return $this->_aliases; } function _parseAliases() : array { $alias = $this->getPropOr("alias", ""); if(is_array($alias)) return $alias; else if($alias && is_string($alias)) return explode(",", $alias); else return array(); } /** * @return artefact\TaskmanArtefact[] */ function getArtefacts() : array { if($this->_artefacts === null) $this->_artefacts = $this->_parseArtefacts(); return $this->_artefacts; } /** * @return artefact\TaskmanArtefact[] */ function _parseArtefacts() : array { $artefacts = array(); $specs = $this->getPropOr("artefacts", array()); if(is_callable($specs)) $specs = $specs(); if(is_array($specs)) { foreach($specs as $dst => $src_spec) $artefacts[] = new artefact\TaskmanArtefact($dst, $src_spec); } return $artefacts; } function 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; global $TASKMAN_STACK; global $TASKMAN_NO_DEPS; global $TASKMAN_START_TIME; $args_str = serialize($args); if((isset($this->has_run[$args_str]) && $this->has_run[$args_str]) || $this->is_running) return; $task_result = null; try { if($this->getArtefacts() && !$this->_checkIfArtefactsStale()) { $this->has_run[$args_str] = true; return; } $this->is_running = true; $this->args = $args; $TASKMAN_STACK[] = $this; $level = count($TASKMAN_STACK)-1; log(0, "***** ".str_repeat('-', $level)."task '" . $this->getName() . "' start *****\n"); if(!$TASKMAN_NO_DEPS) { run_many($this->before_deps); run_many($this->getDeps()); } $TASKMAN_CURRENT_TASK = $this; $bench = microtime(true); $task_result = call_user_func_array($this->func, array($this->args, $this)); array_pop($TASKMAN_STACK); if(!$TASKMAN_NO_DEPS) run_many($this->after_deps); 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; $this->is_running = false; } catch(Exception $e) { $error_handler = $GLOBALS['TASKMAN_ERROR_HANDLER']; if($error_handler) $error_handler($e); else throw $e; } return $task_result; } function addBeforeDep($task) { $this->before_deps[] = $task; } function addAfterDep($task) { $this->after_deps[] = $task; } function getDeps() : array { if($this->deps === null) $this->deps = $this->_parseDeps(); return $this->deps; } private function _parseDeps() : array { $deps = $this->getPropOr("deps", ""); if(is_array($deps)) return $deps; else if($deps && is_string($deps)) return internal\_parse_taskstr($deps); return array(); } function getPropOr(string $name, mixed $def) : mixed { return isset($this->props[$name]) ? $this->props[$name] : $def; } function getProp(string $name) : mixed { return $this->getPropOr($name, null); } function hasProp(string $name) : bool { return isset($this->props[$name]); } function getProps() : array { return $this->props; } private function _checkIfArtefactsStale() : bool { $file_changes = $this->getFileChanges(); $stale_found = false; foreach($this->getArtefacts() as $artefact) { $artefact->initSources($file_changes); if(!$stale_found && $artefact->checkNewerSources($file_changes)) $stale_found = true; if($artefact->isStale()) { log(0, "Task '{$this->name}' artefact '{$artefact->getPath()}' (sources at ".implode(',', $artefact->getNewerSourcesIndices()).") is stale\n"); } } return $stale_found; } function findAnyStaleArtefact() : ?artefact\TaskmanArtefact { foreach($this->getArtefacts() as $artefact) { if($artefact->isStale()) return $artefact; } return null; } } class TaskmanFileChanges { 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 { $lines = internal\_extract_lines_from_file($json_or_file); $json = '[' . implode(',', $lines) . ']'; } $decoded = json_decode($json, true); if(!is_array($decoded)) throw new Exception('Bad json: ' . $json); $changed = array(); $base_dir = dirname($_SERVER['PHP_SELF']); foreach($decoded as $items) { if(count($items) < 2) throw new Exception('Bad entry'); list($status, $file) = $items; if(strlen($file) > 0) { if(DIRECTORY_SEPARATOR == '/') { if($file[0] != '/') $file = $base_dir . DIRECTORY_SEPARATOR . $file; } else if(strlen($file) > 1 && $file[1] != ':') $file = $base_dir . DIRECTORY_SEPARATOR . $file; } $file = artefact\normalize_path($file); if($status == '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); } 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; } } function main( array $argv = array(), callable $help_func = null, bool $proc_argv = true, bool $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 { $similars = ''; if($hints) $similars .= "\nSimilar tasks: " . implode(', ', $hints) . "."; throw new Exception("Task '{$tasks[0]}' not found. $similars"); } } run_many($tasks, $argv); } else if($default_task) run($default_task, $argv); log(0, "***** All done (".round(microtime(true)-$GLOBALS['TASKMAN_START_TIME'],2)." sec.) *****\n"); }