static-php-cli/src/StaticPHP/Artifact/ArtifactDownloader.php

676 lines
32 KiB
PHP

<?php
declare(strict_types=1);
namespace StaticPHP\Artifact;
use Psr\Log\LogLevel;
use StaticPHP\Artifact\Downloader\DownloadResult;
use StaticPHP\Artifact\Downloader\Type\BitBucketTag;
use StaticPHP\Artifact\Downloader\Type\DownloadTypeInterface;
use StaticPHP\Artifact\Downloader\Type\FileList;
use StaticPHP\Artifact\Downloader\Type\Git;
use StaticPHP\Artifact\Downloader\Type\GitHubRelease;
use StaticPHP\Artifact\Downloader\Type\GitHubTarball;
use StaticPHP\Artifact\Downloader\Type\HostedPackageBin;
use StaticPHP\Artifact\Downloader\Type\LocalDir;
use StaticPHP\Artifact\Downloader\Type\PhpRelease;
use StaticPHP\Artifact\Downloader\Type\PIE;
use StaticPHP\Artifact\Downloader\Type\Url;
use StaticPHP\Artifact\Downloader\Type\ValidatorInterface;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\DownloaderException;
use StaticPHP\Exception\ExecutionException;
use StaticPHP\Exception\SPCException;
use StaticPHP\Exception\ValidationException;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Runtime\Shell\Shell;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\InteractiveTerm;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Logger\ConsoleColor;
/**
* Artifact Downloader class
*/
class ArtifactDownloader
{
/** @var array<string, class-string<DownloadTypeInterface>> */
public const array DOWNLOADERS = [
'bitbuckettag' => BitBucketTag::class,
'filelist' => FileList::class,
'git' => Git::class,
'ghrel' => GitHubRelease::class,
'ghtar' => GitHubTarball::class,
'ghtagtar' => GitHubTarball::class,
'local' => LocalDir::class,
'pie' => PIE::class,
'url' => Url::class,
'php-release' => PhpRelease::class,
'hosted' => HostedPackageBin::class,
];
/** @var array<string, Artifact> Artifact objects */
protected array $artifacts = [];
/** @var int Parallel process number (1 and 0 as single-threaded mode) */
protected int $parallel = 1;
protected int $retry = 0;
/** @var array<string, string> Override custom download urls from options */
protected array $custom_urls = [];
/** @var array<string, array{0: string, 1: string}> Override custom git options from options ([branch, git url]) */
protected array $custom_gits = [];
/** @var array<string, string> Override custom local paths from options */
protected array $custom_locals = [];
/** @var int Fetch type preference */
protected int $default_fetch_pref = Artifact::FETCH_PREFER_SOURCE;
/** @var array<string, int> Specific fetch preference */
protected array $fetch_prefs = [];
/** @var array<string>|bool Whether to ignore cache for specific artifacts or all */
protected array|bool $ignore_cache = false;
/** @var bool Whether to enable alternative mirror downloads */
protected bool $alt = true;
private array $_before_files;
/**
* @param array{
* parallel?: int,
* retry?: int,
* custom-url?: array<string>,
* custom-git?: array<string>,
* custom-local?: array<string>,
* prefer-source?: null|bool|string,
* prefer-pre-built?: null|bool|string,
* prefer-binary?: null|bool|string,
* source-only?: null|bool|string,
* binary-only?: null|bool|string,
* ignore-cache?: null|bool|string,
* ignore-cache-sources?: null|bool|string,
* no-alt?: bool,
* no-shallow-clone?: bool
* } $options Downloader options
*/
public function __construct(protected array $options = [])
{
// Allow setting concurrency via options
$this->parallel = max(1, (int) ($options['parallel'] ?? 1));
// Allow setting retry via options
$this->retry = max(0, (int) ($options['retry'] ?? 0));
// Prefer source (default)
if (array_key_exists('prefer-source', $options)) {
if (is_string($options['prefer-source'])) {
$ls = parse_comma_list($options['prefer-source']);
foreach ($ls as $name) {
$this->fetch_prefs[$name] = Artifact::FETCH_PREFER_SOURCE;
}
} elseif ($options['prefer-source'] === null || $options['prefer-source'] === true) {
$this->default_fetch_pref = Artifact::FETCH_PREFER_SOURCE;
}
}
// Prefer binary (originally prefer-pre-built)
if (array_key_exists('prefer-binary', $options)) {
if (is_string($options['prefer-binary'])) {
$ls = parse_comma_list($options['prefer-binary']);
foreach ($ls as $name) {
$this->fetch_prefs[$name] = Artifact::FETCH_PREFER_BINARY;
}
} elseif ($options['prefer-binary'] === null || $options['prefer-binary'] === true) {
$this->default_fetch_pref = Artifact::FETCH_PREFER_BINARY;
}
}
if (array_key_exists('prefer-pre-built', $options)) {
if (is_string($options['prefer-pre-built'])) {
$ls = parse_comma_list($options['prefer-pre-built']);
foreach ($ls as $name) {
$this->fetch_prefs[$name] = Artifact::FETCH_PREFER_BINARY;
}
} elseif ($options['prefer-pre-built'] === null || $options['prefer-pre-built'] === true) {
$this->default_fetch_pref = Artifact::FETCH_PREFER_BINARY;
}
}
// Source only
if (array_key_exists('source-only', $options)) {
if (is_string($options['source-only'])) {
$ls = parse_comma_list($options['source-only']);
foreach ($ls as $name) {
$this->fetch_prefs[$name] = Artifact::FETCH_ONLY_SOURCE;
}
} elseif ($options['source-only'] === null || $options['source-only'] === true) {
$this->default_fetch_pref = Artifact::FETCH_ONLY_SOURCE;
}
}
// Binary only
if (array_key_exists('binary-only', $options)) {
if (is_string($options['binary-only'])) {
$ls = parse_comma_list($options['binary-only']);
foreach ($ls as $name) {
$this->fetch_prefs[$name] = Artifact::FETCH_ONLY_BINARY;
}
} elseif ($options['binary-only'] === null || $options['binary-only'] === true) {
$this->default_fetch_pref = Artifact::FETCH_ONLY_BINARY;
}
}
// Ignore cache
if (array_key_exists('ignore-cache', $options)) {
if (is_string($options['ignore-cache'])) {
$this->ignore_cache = parse_comma_list($options['ignore-cache']);
} elseif ($options['ignore-cache'] === null || $options['ignore-cache'] === true) {
$this->ignore_cache = true;
}
}
// backward compatibility for ignore-cache-sources
if (array_key_exists('ignore-cache-sources', $options)) {
if (is_string($options['ignore-cache-sources'])) {
$this->ignore_cache = parse_comma_list($options['ignore-cache-sources']);
} elseif ($options['ignore-cache-sources'] === null || $options['ignore-cache-sources'] === true) {
$this->ignore_cache = true;
}
}
// Allow setting custom urls via options
foreach (($options['custom-url'] ?? []) as $value) {
[$artifact_name, $url] = explode(':', $value, 2);
$this->custom_urls[$artifact_name] = $url;
$this->ignore_cache = match ($this->ignore_cache) {
true => true,
false => [$artifact_name],
default => array_merge($this->ignore_cache, [$artifact_name]),
};
}
// Allow setting custom git options via options
foreach (($options['custom-git'] ?? []) as $value) {
[$artifact_name, $branch, $git_url] = explode(':', $value, 3) + [null, null, null];
$this->custom_gits[$artifact_name] = [$branch ?? 'main', $git_url];
$this->ignore_cache = match ($this->ignore_cache) {
true => true,
false => [$artifact_name],
default => array_merge($this->ignore_cache, [$artifact_name]),
};
}
// Allow setting custom local paths via options
foreach (($options['custom-local'] ?? []) as $value) {
[$artifact_name, $local_path] = explode(':', $value, 2);
$this->custom_locals[$artifact_name] = $local_path;
$this->ignore_cache = match ($this->ignore_cache) {
true => true,
false => [$artifact_name],
default => array_merge($this->ignore_cache, [$artifact_name]),
};
}
// no alt
if (array_key_exists('no-alt', $options) && $options['no-alt'] === true) {
$this->alt = false;
}
// read downloads dir
$this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: [];
}
/**
* Add an artifact to the download list.
*
* @param Artifact|string $artifact Artifact instance or artifact name
*/
public function add(Artifact|string $artifact): static
{
if (is_string($artifact)) {
$artifact_instance = ArtifactLoader::getArtifactInstance($artifact);
} else {
$artifact_instance = $artifact;
}
if ($artifact_instance === null) {
$name = $artifact;
throw new WrongUsageException("Artifact '{$name}' not found, please check the name.");
}
// only add if not already added
if (!isset($this->artifacts[$artifact_instance->getName()])) {
$this->artifacts[$artifact_instance->getName()] = $artifact_instance;
}
return $this;
}
/**
* Add multiple artifacts to the download list.
*
* @param array<Artifact|string> $artifacts Multiple artifacts to add
*/
public function addArtifacts(array $artifacts): static
{
foreach ($artifacts as $artifact) {
$this->add($artifact);
}
return $this;
}
/**
* Set the concurrency limit for parallel downloads.
*
* @param int $parallel Number of concurrent downloads (default: 3)
*/
public function setParallel(int $parallel): static
{
$this->parallel = max(1, $parallel);
return $this;
}
/**
* Download all artifacts, with optional parallel processing.
*
* @param bool $interactive Enable interactive mode with Ctrl+C handling
*/
public function download(bool $interactive = true): void
{
if ($interactive) {
Shell::passthruCallback(function () {
InteractiveTerm::advance();
});
keyboard_interrupt_register(function () {
echo PHP_EOL;
InteractiveTerm::error('Download cancelled by user.');
// scan changed files
$after_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: [];
$new_files = array_diff($after_files, $this->_before_files);
// remove new files
foreach ($new_files as $file) {
if ($file === '.cache.json') {
continue;
}
logger()->debug("Removing corrupted artifact: {$file}");
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $file;
if (is_dir($path)) {
FileSystem::removeDir($path);
} elseif (is_file($path)) {
FileSystem::removeFileIfExists($path);
}
}
exit(2);
});
}
$this->applyCustomDownloads();
$count = count($this->artifacts);
$artifacts_str = implode(',', array_map(fn ($x) => '' . ConsoleColor::yellow($x->getName()), $this->artifacts));
// mute the first line if not interactive
if ($interactive) {
InteractiveTerm::notice("Downloading {$count} artifacts: {$artifacts_str} ...");
}
try {
// Create dir
if (!is_dir(DOWNLOAD_PATH)) {
FileSystem::createDir(DOWNLOAD_PATH);
}
logger()->info('Downloading' . implode(', ', array_map(fn ($x) => " '{$x->getName()}'", $this->artifacts)) . " with concurrency {$this->parallel} ...");
// Download artifacts parallely
if ($this->parallel > 1) {
$this->downloadWithConcurrency();
} else {
// normal sequential download
$current = 0;
$skipped = [];
foreach ($this->artifacts as $artifact) {
++$current;
if ($this->downloadWithType($artifact, $current, $count, interactive: $interactive) === SPC_DOWNLOAD_STATUS_SKIPPED) {
$skipped[] = $artifact->getName();
continue;
}
$this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: [];
}
if ($interactive) {
$skip_msg = !empty($skipped) ? ' (Skipped ' . count($skipped) . ' artifacts for being already downloaded)' : '';
InteractiveTerm::success("Downloaded all {$count} artifacts.{$skip_msg}", true);
echo PHP_EOL;
}
}
} catch (SPCException $e) {
array_map(fn ($x) => InteractiveTerm::error($x), explode("\n", $e->getMessage()));
throw new WrongUsageException();
} finally {
if ($interactive) {
Shell::passthruCallback(null);
keyboard_interrupt_unregister();
}
}
}
public function getRetry(): int
{
return $this->retry;
}
public function getArtifacts(): array
{
return $this->artifacts;
}
public function getOption(string $name, mixed $default = null): mixed
{
return $this->options[$name] ?? $default;
}
private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false, bool $interactive = true): int
{
$queue = $this->generateQueue($artifact);
// already downloaded
if ($queue === []) {
logger()->debug("Artifact '{$artifact->getName()}' is already downloaded, skipping.");
return SPC_DOWNLOAD_STATUS_SKIPPED;
}
$try = false;
foreach ($queue as $item) {
try {
$instance = null;
$call = self::DOWNLOADERS[$item['config']['type']] ?? null;
$type_display_name = match (true) {
$item['lock'] === 'source' && ($callback = $artifact->getCustomSourceCallback()) !== null => 'user defined source downloader',
$item['lock'] === 'binary' && ($callback = $artifact->getCustomBinaryCallback()) !== null => 'user defined binary downloader',
default => SPC_DOWNLOAD_TYPE_DISPLAY_NAME[$item['config']['type']] ?? $item['config']['type'],
};
$try_h = $try ? 'Try downloading' : 'Downloading';
logger()->info("{$try_h} artifact '{$artifact->getName()}' {$item['display']} ...");
if ($parallel === false && $interactive) {
InteractiveTerm::indicateProgress("[{$current}/{$total}] Downloading artifact " . ConsoleColor::green($artifact->getName()) . " {$item['display']} from {$type_display_name} ...");
}
// is valid download type
if ($item['lock'] === 'source' && ($callback = $artifact->getCustomSourceCallback()) !== null) {
$lock = ApplicationContext::invoke($callback, [
Artifact::class => $artifact,
ArtifactDownloader::class => $this,
]);
} elseif ($item['lock'] === 'binary' && ($callback = $artifact->getCustomBinaryCallback()) !== null) {
$lock = ApplicationContext::invoke($callback, [
Artifact::class => $artifact,
ArtifactDownloader::class => $this,
]);
} elseif (is_a($call, DownloadTypeInterface::class, true)) {
$instance = new $call();
$lock = $instance->download($artifact->getName(), $item['config'], $this);
} else {
if ($item['config']['type'] === 'custom') {
$msg = "Artifact [{$artifact->getName()}] has no valid custom " . SystemTarget::getCurrentPlatformString() . ' download callback defined.';
} else {
$msg = "Artifact has invalid download type '{$item['config']['type']}' for {$item['display']}.";
}
throw new ValidationException($msg);
}
if (!$lock instanceof DownloadResult) {
throw new ValidationException("Artifact {$artifact->getName()} has invalid custom return value. Must be instance of DownloadResult.");
}
// verifying hash if possible
$hash_validator = $instance ?? null;
$verified = $lock->verified;
if ($hash_validator instanceof ValidatorInterface) {
if (!$hash_validator->validate($artifact->getName(), $item['config'], $this, $lock)) {
throw new ValidationException("Hash validation failed for artifact '{$artifact->getName()}' {$item['display']}.");
}
$verified = true;
}
// process lock
ApplicationContext::get(ArtifactCache::class)->lock($artifact, $item['lock'], $lock, SystemTarget::getCurrentPlatformString());
if ($parallel === false && $interactive) {
$ver = $lock->hasVersion() ? (' (' . ConsoleColor::yellow($lock->version) . ')') : '';
InteractiveTerm::finish('Downloaded ' . ($verified ? 'and verified ' : '') . 'artifact ' . ConsoleColor::green($artifact->getName()) . $ver . " {$item['display']} .");
}
return SPC_DOWNLOAD_STATUS_SUCCESS;
} catch (DownloaderException|ExecutionException $e) {
if ($parallel === false && $interactive) {
InteractiveTerm::finish("Download artifact {$artifact->getName()} {$item['display']} failed !", false);
InteractiveTerm::error("Failed message: {$e->getMessage()}", true);
}
$try = true;
continue;
} catch (ValidationException $e) {
if ($parallel === false) {
InteractiveTerm::finish("Download artifact {$artifact->getName()} {$item['display']} failed !", false);
InteractiveTerm::error("Validation failed: {$e->getMessage()}");
}
break;
}
}
$vvv = ApplicationContext::isDebug() ? "\nIf the problem persists, consider using `-vvv` to enable verbose mode, and disable parallel downloading for more details." : '';
throw new DownloaderException("Download artifact '{$artifact->getName()}' failed. Please check your internet connection and try again.{$vvv}");
}
private function downloadWithConcurrency(): void
{
$skipped = [];
$fiber_pool = [];
$old_verbosity = null;
$old_debug = null;
try {
$count = count($this->artifacts);
// must mute
$output = ApplicationContext::get(OutputInterface::class);
if ($output->isVerbose()) {
$old_verbosity = $output->getVerbosity();
$old_debug = ApplicationContext::isDebug();
logger()->warning('Parallel download is not supported in verbose mode, I will mute the output temporarily.');
$output->setVerbosity(OutputInterface::VERBOSITY_NORMAL);
ApplicationContext::setDebug(false);
logger()->setLevel(LogLevel::ERROR);
}
$pool_count = $this->parallel;
$downloaded = 0;
$total = count($this->artifacts);
Shell::passthruCallback(function () {
InteractiveTerm::advance();
\Fiber::suspend();
});
InteractiveTerm::indicateProgress("[{$downloaded}/{$total}] Downloading artifacts with concurrency {$this->parallel} ...");
$failed_downloads = [];
while (true) {
// fill pool
while (count($fiber_pool) < $pool_count && ($artifact = array_shift($this->artifacts)) !== null) {
$current = $count - count($this->artifacts);
$fiber = new \Fiber(function () use ($artifact, $current, $count) {
return [$artifact, $this->downloadWithType($artifact, $current, $count, true)];
});
$fiber->start();
$fiber_pool[] = $fiber;
}
// check pool
foreach ($fiber_pool as $index => $fiber) {
if ($fiber->isTerminated()) {
try {
[$artifact, $int] = $fiber->getReturn();
if ($int === SPC_DOWNLOAD_STATUS_SKIPPED) {
$skipped[] = $artifact->getName();
}
} catch (\Throwable $e) {
$artifact_name = 'unknown';
if (isset($artifact)) {
$artifact_name = $artifact->getName();
}
$failed_downloads[] = ['artifact' => $artifact_name, 'error' => $e];
InteractiveTerm::setMessage("[{$downloaded}/{$total}] Download failed: {$artifact_name}");
InteractiveTerm::advance();
}
// remove from pool
unset($fiber_pool[$index]);
++$downloaded;
InteractiveTerm::setMessage("[{$downloaded}/{$total}] Downloading artifacts with concurrency {$this->parallel} ...");
InteractiveTerm::advance();
} else {
$fiber->resume();
}
}
// all done
if (count($this->artifacts) === 0 && count($fiber_pool) === 0) {
if (!empty($failed_downloads)) {
InteractiveTerm::finish('Download completed with ' . count($failed_downloads) . ' failure(s).', false);
foreach ($failed_downloads as $failure) {
InteractiveTerm::error("Failed to download '{$failure['artifact']}': {$failure['error']->getMessage()}");
}
throw new DownloaderException('Failed to download ' . count($failed_downloads) . ' artifact(s). Please check your internet connection and try again.');
}
$skip_msg = !empty($skipped) ? ' (Skipped ' . count($skipped) . ' artifacts for being already downloaded)' : '';
InteractiveTerm::finish("Downloaded all {$total} artifacts.{$skip_msg}");
break;
}
}
} catch (\Throwable $e) {
// throw to all fibers to make them stop
foreach ($fiber_pool as $fiber) {
if (!$fiber->isTerminated()) {
try {
$fiber->throw($e);
} catch (\Throwable) {
// ignore errors when stopping fibers
}
}
}
InteractiveTerm::finish('Parallel download failed !', false);
throw $e;
} finally {
if ($old_verbosity !== null) {
ApplicationContext::get(OutputInterface::class)->setVerbosity($old_verbosity);
logger()->setLevel(match ($old_verbosity) {
OutputInterface::VERBOSITY_VERBOSE => LogLevel::INFO,
OutputInterface::VERBOSITY_VERY_VERBOSE, OutputInterface::VERBOSITY_DEBUG => LogLevel::DEBUG,
default => LogLevel::WARNING,
});
}
if ($old_debug !== null) {
ApplicationContext::setDebug($old_debug);
}
Shell::passthruCallback(null);
}
}
/**
* Generate download queue based on type preference.
*/
private function generateQueue(Artifact $artifact): array
{
/** @var array<array{display: string, lock: string, config: array}> $queue */
$queue = [];
$binary_downloaded = $artifact->isBinaryDownloaded(compare_hash: true);
$source_downloaded = $artifact->isSourceDownloaded(compare_hash: true);
$item_source = ['display' => 'source', 'lock' => 'source', 'config' => $artifact->getDownloadConfig('source')];
$item_source_mirror = ['display' => 'source (mirror)', 'lock' => 'source', 'config' => $artifact->getDownloadConfig('source-mirror')];
// For binary config, handle both array configs and custom callbacks
$binary_config = $artifact->getDownloadConfig('binary');
$has_custom_binary = $artifact->getCustomBinaryCallback() !== null;
$item_binary_config = null;
if (is_array($binary_config)) {
$item_binary_config = $binary_config[SystemTarget::getCurrentPlatformString()] ?? null;
} elseif ($has_custom_binary) {
// For custom binaries, create a dummy config to allow queue generation
$item_binary_config = ['type' => 'custom'];
}
$item_binary = ['display' => 'binary', 'lock' => 'binary', 'config' => $item_binary_config];
$binary_mirror_config = $artifact->getDownloadConfig('binary-mirror');
$item_binary_mirror_config = null;
if (is_array($binary_mirror_config)) {
$item_binary_mirror_config = $binary_mirror_config[SystemTarget::getCurrentPlatformString()] ?? null;
}
$item_binary_mirror = ['display' => 'binary (mirror)', 'lock' => 'binary', 'config' => $item_binary_mirror_config];
$pref = $this->fetch_prefs[$artifact->getName()] ?? $this->default_fetch_pref;
if ($pref === Artifact::FETCH_PREFER_SOURCE) {
$queue[] = $item_source['config'] !== null ? $item_source : null;
$queue[] = $item_source_mirror['config'] !== null && $this->alt ? $item_source_mirror : null;
$queue[] = $item_binary['config'] !== null ? $item_binary : null;
$queue[] = $item_binary_mirror['config'] !== null && $this->alt ? $item_binary_mirror : null;
} elseif ($pref === Artifact::FETCH_PREFER_BINARY) {
$queue[] = $item_binary['config'] !== null ? $item_binary : null;
$queue[] = $item_binary_mirror['config'] !== null && $this->alt ? $item_binary_mirror : null;
$queue[] = $item_source['config'] !== null ? $item_source : null;
$queue[] = $item_source_mirror['config'] !== null && $this->alt ? $item_source_mirror : null;
} elseif ($pref === Artifact::FETCH_ONLY_SOURCE) {
$queue[] = $item_source['config'] !== null ? $item_source : null;
$queue[] = $item_source_mirror['config'] !== null && $this->alt ? $item_source_mirror : null;
} elseif ($pref === Artifact::FETCH_ONLY_BINARY) {
$queue[] = $item_binary['config'] !== null ? $item_binary : null;
$queue[] = $item_binary_mirror['config'] !== null && $this->alt ? $item_binary_mirror : null;
}
// filter nulls
$queue = array_values(array_filter($queue));
// always download
if ($this->ignore_cache === true || is_array($this->ignore_cache) && in_array($artifact->getName(), $this->ignore_cache)) {
// validate: ensure at least one download source is available
if (empty($queue)) {
throw new ValidationException("Artifact '{$artifact->getName()}' does not provide any download source for current platform (" . SystemTarget::getCurrentPlatformString() . ').');
}
return $queue;
}
// check if already downloaded
$has_usable_download = false;
if ($pref === Artifact::FETCH_PREFER_SOURCE) {
// prefer source: check source first, if not available check binary
$has_usable_download = $source_downloaded || $binary_downloaded;
} elseif ($pref === Artifact::FETCH_PREFER_BINARY) {
// prefer binary: check binary first, if not available check source
$has_usable_download = $binary_downloaded || $source_downloaded;
} elseif ($pref === Artifact::FETCH_ONLY_SOURCE) {
// source-only: only check if source is downloaded
$has_usable_download = $source_downloaded;
} elseif ($pref === Artifact::FETCH_ONLY_BINARY) {
// binary-only: only check if binary for current platform is downloaded
$has_usable_download = $binary_downloaded;
}
// if already downloaded, skip
if ($has_usable_download) {
return [];
}
// validate: ensure at least one download source is available
if (empty($queue)) {
if ($pref === Artifact::FETCH_ONLY_SOURCE) {
throw new ValidationException("Artifact '{$artifact->getName()}' does not provide source download, cannot use --source-only mode.");
}
if ($pref === Artifact::FETCH_ONLY_BINARY) {
throw new ValidationException("Artifact '{$artifact->getName()}' does not provide binary download for current platform (" . SystemTarget::getCurrentPlatformString() . '), cannot use --binary-only mode.');
}
// prefer modes should also throw error if no download source available
throw new ValidationException("Validation failed: Artifact '{$artifact->getName()}' does not provide any download source for current platform (" . SystemTarget::getCurrentPlatformString() . ').');
}
return $queue;
}
private function applyCustomDownloads(): void
{
foreach ($this->custom_urls as $artifact_name => $custom_url) {
if (isset($this->artifacts[$artifact_name])) {
$this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $custom_url) {
return (new Url())->download($artifact_name, ['url' => $custom_url], $downloader);
});
}
}
foreach ($this->custom_gits as $artifact_name => [$branch, $git_url]) {
if (isset($this->artifacts[$artifact_name])) {
$this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $branch, $git_url) {
return (new Git())->download($artifact_name, ['rev' => $branch, 'url' => $git_url], $downloader);
});
}
}
foreach ($this->custom_locals as $artifact_name => $local_path) {
if (isset($this->artifacts[$artifact_name])) {
$this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $local_path) {
return (new LocalDir())->download($artifact_name, ['dirname' => $local_path], $downloader);
});
}
}
}
}