397 lines
8.7 KiB
PHP
397 lines
8.7 KiB
PHP
<?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);
|
|
}
|