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 $requests * @param array $options * @return array{installed:list, warnings:list, aborted:list, actions:array} */ public function add(array $requests, array $options): array { return $this->installRequests($requests, $options, false); } /** * @param list $requests * @param array $options * @return array{installed:list, warnings:list, aborted:list, actions:array} */ public function update(array $requests, array $options): array { return $this->installRequests($requests, $options, true); } /** * @param list $requests * @param array $options * @return array{installed:list, warnings:list, aborted:list, actions:array} */ 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), $manifest->coreIndependent($componentName) ); if (!in_array($compat['status'], [Compatibility::OK, Compatibility::INDEPENDENT], true)) { $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, 'description' => $manifest->descriptionOf($componentName), '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, $this->root, ); } } } return ['installed' => $installed, 'warnings' => $warnings, 'aborted' => $aborted, 'actions' => $actions]; } /** * @param list $ids * @param array $options * @return array{removed:list, aborted:list} */ 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 $options * @return array{files:array, actions:array, aborted:list, version:?string} */ public function installCore(CoreProvider $provider, array $options): array { return $this->writeCore($provider, $options, false); } /** * @param array $options * @return array{files:array, actions:array, aborted:list, version:?string} */ public function updateCore(CoreProvider $provider, array $options): array { return $this->writeCore($provider, $options, true); } /** * @param array $options * @return array{files:array, actions:array, aborted:list, 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; if ($options['dryRun'] ?? false) { $actions[$relative] = 'planned'; $installed[$relative] = 'sha256:' . hash('sha256', $provider->read($relative)); continue; } $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; } if ($options['dryRun'] ?? false) { $actions[$relative] = 'remove-planned'; 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 */ 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 */ 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) : []; } }