first commit.
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
<?php
|
||||
|
||||
namespace Toolbox;
|
||||
|
||||
/**
|
||||
* Motor de gestión compartido por el instalador global y el CLI del proyecto:
|
||||
* alta, baja, actualización de core/componentes, listado y diagnóstico.
|
||||
*/
|
||||
final class Installer
|
||||
{
|
||||
private readonly Catalog $catalog;
|
||||
private readonly Resolver $resolver;
|
||||
private readonly PackageSource $source;
|
||||
private readonly FileSync $sync;
|
||||
private readonly Copier $copier;
|
||||
private ProjectManifest $manifest;
|
||||
private Lock $lock;
|
||||
private readonly string $root;
|
||||
private readonly string $rootCore;
|
||||
|
||||
public function __construct(
|
||||
string $root,
|
||||
private readonly string $toolboxDir,
|
||||
private readonly Console $console,
|
||||
?string $rootCore = null,
|
||||
) {
|
||||
$this->root = rtrim($root, '/');
|
||||
$this->rootCore = $rootCore !== null ? rtrim($rootCore, '/') : $this->root . '/src';
|
||||
$this->catalog = Catalog::fromRoot($this->toolboxDir);
|
||||
$this->resolver = new Resolver($this->catalog);
|
||||
$this->source = new DirectorySource(rtrim($this->toolboxDir, '/') . '/packages');
|
||||
$this->sync = new FileSync();
|
||||
$this->copier = new Copier($this->sync);
|
||||
$this->manifest = ProjectManifest::load($this->root);
|
||||
$this->lock = Lock::load($this->root);
|
||||
}
|
||||
|
||||
public function catalog(): Catalog
|
||||
{
|
||||
return $this->catalog;
|
||||
}
|
||||
|
||||
public function lock(): Lock
|
||||
{
|
||||
return $this->lock;
|
||||
}
|
||||
|
||||
public function manifest(): ProjectManifest
|
||||
{
|
||||
return $this->manifest;
|
||||
}
|
||||
|
||||
public function coreVersion(): ?string
|
||||
{
|
||||
return $this->lock->core()['version'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $requests
|
||||
* @param array<string, bool|string> $options
|
||||
* @return array{installed:list<string>, warnings:list<string>, aborted:list<string>, actions:array<string,string>}
|
||||
*/
|
||||
public function add(array $requests, array $options): array
|
||||
{
|
||||
return $this->installRequests($requests, $options, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $requests
|
||||
* @param array<string, bool|string> $options
|
||||
* @return array{installed:list<string>, warnings:list<string>, aborted:list<string>, actions:array<string,string>}
|
||||
*/
|
||||
public function update(array $requests, array $options): array
|
||||
{
|
||||
return $this->installRequests($requests, $options, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $requests
|
||||
* @param array<string, bool|string> $options
|
||||
* @return array{installed:list<string>, warnings:list<string>, aborted:list<string>, actions:array<string,string>}
|
||||
*/
|
||||
private function installRequests(array $requests, array $options, bool $isUpdate): array
|
||||
{
|
||||
$clean = [];
|
||||
$constraints = [];
|
||||
foreach ($requests as $request) {
|
||||
[$id, $constraint] = array_pad(explode('@', $request, 2), 2, null);
|
||||
$clean[] = $id;
|
||||
if ($constraint !== null) {
|
||||
$constraints[$id] = $constraint;
|
||||
}
|
||||
}
|
||||
|
||||
$resolved = $this->resolver->resolve($clean);
|
||||
$warnings = [];
|
||||
$installed = [];
|
||||
$aborted = [];
|
||||
$actions = [];
|
||||
$post = [];
|
||||
|
||||
foreach ($resolved as $entry) {
|
||||
$package = $entry['package'];
|
||||
$componentName = $entry['component'];
|
||||
$manifest = $this->catalog->get($package);
|
||||
$component = $manifest->component($componentName);
|
||||
$id = $package . ':' . $componentName;
|
||||
|
||||
[$version, $commit] = $this->resolveVersion($package);
|
||||
|
||||
$compat = Compatibility::evaluateCore(
|
||||
$this->coreVersion(),
|
||||
$manifest->coreRequirement($componentName),
|
||||
$manifest->testedCore($componentName)
|
||||
);
|
||||
|
||||
if ($compat['status'] !== Compatibility::OK) {
|
||||
$warnings[] = $id . ': ' . $compat['message'];
|
||||
$assumeYes = (bool) ($options['yes'] ?? false);
|
||||
if (!$assumeYes && !$this->console->confirm(" {$id}: {$compat['message']}. ¿Continuar?", false)) {
|
||||
$aborted[] = $id;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$mode = ($options['link'] ?? false) ? 'link' : 'copy';
|
||||
$expected = $this->lock->component($id)['files'] ?? [];
|
||||
|
||||
$files = FileExpander::expand($this->source, $package, $component->files);
|
||||
|
||||
$result = $this->copier->install(
|
||||
$files,
|
||||
$this->source->baseDir($package),
|
||||
$this->root,
|
||||
$this->rootCore,
|
||||
$component->destinations,
|
||||
$mode,
|
||||
$options,
|
||||
$expected
|
||||
);
|
||||
|
||||
foreach ($result['actions'] as $rel => $action) {
|
||||
$actions[$rel] = $action;
|
||||
}
|
||||
|
||||
if ($result['aborted'] !== []) {
|
||||
foreach ($result['aborted'] as $rel) {
|
||||
$aborted[] = $rel;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!($options['dryRun'] ?? false)) {
|
||||
$this->lock->setComponent($id, [
|
||||
'package' => $package,
|
||||
'version' => $version,
|
||||
'toolbox_commit' => $commit,
|
||||
'mode' => $mode,
|
||||
'files' => $result['files'],
|
||||
]);
|
||||
}
|
||||
|
||||
$installed[] = $id;
|
||||
$post[$package] = $manifest;
|
||||
}
|
||||
|
||||
foreach ($constraints as $key => $constraint) {
|
||||
if (str_contains($key, ':')) {
|
||||
if (!$isUpdate) {
|
||||
$this->manifest->setConstraint($key, $constraint);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($installed as $id) {
|
||||
if (str_starts_with($id, $key . ':') && !$isUpdate) {
|
||||
$this->manifest->setConstraint($id, $constraint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ownership::fromLock($this->lock)->assertNoCollisions();
|
||||
|
||||
if (!($options['dryRun'] ?? false)) {
|
||||
$this->lock->save($this->root);
|
||||
$this->manifest->save($this->root);
|
||||
|
||||
foreach ($post as $manifest) {
|
||||
if ($manifest->postInstall !== []) {
|
||||
PostInstall::run(
|
||||
$manifest->postInstall,
|
||||
$this->console,
|
||||
(bool) ($options['yes'] ?? false),
|
||||
$options['postRunner'] ?? null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ['installed' => $installed, 'warnings' => $warnings, 'aborted' => $aborted, 'actions' => $actions];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $ids
|
||||
* @param array<string, bool|string> $options
|
||||
* @return array{removed:list<string>, aborted:list<string>}
|
||||
*/
|
||||
public function remove(array $ids, array $options): array
|
||||
{
|
||||
$removed = [];
|
||||
$aborted = [];
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$entry = $this->lock->component($id);
|
||||
if ($entry === null) {
|
||||
$aborted[] = $id;
|
||||
continue;
|
||||
}
|
||||
|
||||
$componentAbort = false;
|
||||
foreach ($entry['files'] as $relative => $hash) {
|
||||
$action = $this->sync->remove($this->root . '/' . $relative, $hash, $options);
|
||||
if ($action === FileSync::ABORTED) {
|
||||
$aborted[] = $relative;
|
||||
$componentAbort = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($componentAbort) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->lock->removeComponent($id);
|
||||
$this->manifest->removeConstraint($id);
|
||||
$removed[] = $id;
|
||||
}
|
||||
|
||||
if (!($options['dryRun'] ?? false)) {
|
||||
$this->lock->save($this->root);
|
||||
$this->manifest->save($this->root);
|
||||
}
|
||||
|
||||
return ['removed' => $removed, 'aborted' => $aborted];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, bool|string> $options
|
||||
* @return array{files:array<string,string>, actions:array<string,string>, aborted:list<string>, version:?string}
|
||||
*/
|
||||
public function installCore(CoreProvider $provider, array $options): array
|
||||
{
|
||||
return $this->writeCore($provider, $options, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, bool|string> $options
|
||||
* @return array{files:array<string,string>, actions:array<string,string>, aborted:list<string>, version:?string}
|
||||
*/
|
||||
public function updateCore(CoreProvider $provider, array $options): array
|
||||
{
|
||||
return $this->writeCore($provider, $options, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, bool|string> $options
|
||||
* @return array{files:array<string,string>, actions:array<string,string>, aborted:list<string>, version:?string}
|
||||
*/
|
||||
private function writeCore(CoreProvider $provider, array $options, bool $removeStale): array
|
||||
{
|
||||
$protected = $this->componentOwnedPaths();
|
||||
$expected = $this->lock->core()['files'] ?? [];
|
||||
$files = $provider->files();
|
||||
|
||||
$installed = [];
|
||||
$actions = [];
|
||||
$aborted = [];
|
||||
|
||||
foreach ($files as $relative) {
|
||||
if (isset($protected[$relative])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$absolute = $this->root . '/' . $relative;
|
||||
$action = $this->sync->write($absolute, $provider->read($relative), $expected[$relative] ?? null, $options);
|
||||
$actions[$relative] = $action;
|
||||
|
||||
if ($action === FileSync::ABORTED) {
|
||||
$aborted[] = $relative;
|
||||
continue;
|
||||
}
|
||||
|
||||
$installed[$relative] = Drift::hash($absolute);
|
||||
}
|
||||
|
||||
if ($removeStale) {
|
||||
foreach ($expected as $relative => $hash) {
|
||||
if (isset($installed[$relative]) || isset($protected[$relative]) || in_array($relative, $files, true)) {
|
||||
continue;
|
||||
}
|
||||
$action = $this->sync->remove($this->root . '/' . $relative, $hash, $options);
|
||||
if ($action === FileSync::ABORTED) {
|
||||
$aborted[] = $relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($aborted === [] && !($options['dryRun'] ?? false)) {
|
||||
$this->lock->setCore([
|
||||
'version' => $provider->version() ?? $this->coreVersion(),
|
||||
'commit' => $provider->commit() ?? ($this->lock->core()['commit'] ?? null),
|
||||
'files' => $installed,
|
||||
]);
|
||||
$this->lock->save($this->root);
|
||||
}
|
||||
|
||||
return ['files' => $installed, 'actions' => $actions, 'aborted' => $aborted, 'version' => $provider->version()];
|
||||
}
|
||||
|
||||
/** @return array<string, true> */
|
||||
private function componentOwnedPaths(): array
|
||||
{
|
||||
$paths = [];
|
||||
foreach ($this->lock->components() as $entry) {
|
||||
foreach (array_keys($entry['files'] ?? []) as $relative) {
|
||||
$paths[$relative] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:string, 1:?string}
|
||||
*/
|
||||
private function resolveVersion(string $package): array
|
||||
{
|
||||
if (is_dir($this->toolboxDir . '/.git')) {
|
||||
$resolver = new TagResolver(new ToolboxRepo($this->toolboxDir, ''), $this->yanked($package));
|
||||
$latest = $resolver->latest($package);
|
||||
if ($latest !== null) {
|
||||
return [$latest['version'], $latest['commit']];
|
||||
}
|
||||
}
|
||||
|
||||
return [$this->catalog->get($package)->version, null];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function yanked(string $package): array
|
||||
{
|
||||
$file = $this->toolboxDir . '/yanked.json';
|
||||
if (!is_file($file)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$data = json_decode((string) file_get_contents($file), true);
|
||||
if (!is_array($data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$list = $data[$package] ?? [];
|
||||
|
||||
return is_array($list) ? array_values($list) : [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user