ncc/src/ncc/Managers/PackageManager.php
Netkas 0820cd3c32
- Corrected code-smell and code style issues in \ncc\Utilities > IO
- Corrected code-smell and code style issues in `\ncc > ncc`
 - Corrected code-smell and code style issues in `\ncc\CLI > Main`
 - Removed unused exception `FileNotFoundException` in `\ncc\CLI > HelpMenu`
 - Corrected code-smell and code style issues in `\ncc\Managers > ProjectManager`
 - Corrected code-smell and code style issues in `\ncc\Objects\NccVersionInformation > Component`
 - Corrected code-smell and code style issues in `\ncc\Objects\Package > Component`
 - Corrected code-smell and code style issues in `\ncc\Managers > ConfigurationManager`
 - Corrected code-smell and code style issues in `\ncc\Managers > CredentialManager`
 - Refactored `\ncc\Utilities > PathFinder` to remove all Win32 references
 - Corrected code-smell and code style issues in `\ncc\Objects > ExecutionPointers`
 - Corrected code-smell and code style issues in `\ncc\Managers > ExecutionPointerManager`
 - Corrected code-smell and code style issues in `\ncc\Utilities > Functions`
 - Corrected code-smell and code style issues in `\ncc\Managers > PackageManager`
 - Removed `FileNotFoundException` and `DirectoryNotFoundException` from `\ncc\Exceptions`
 - Removed the use of `InvalidScopeException` across the project
 - Removed references of Win32 from the project as Windows is not going supported
 - Added new exception `PathNotFoundException` and implemented it in replacement for `DirectoryNotFoundException` and
   `FileNotFoundException` in `\ncc\Exceptions`
 - Corrected code-smell and code style issues in `src/installer/hash_check.php`
 - Renamed `Abstracts` namespace to `Enums`
 - Updated class type to "final class" in `\ncc\Enums\Options > BuildConfigurationValues`
 - Updated class type to "final class" in `\ncc\Enums\Options > InitializeProjectOptions`
 - Updated class type to "final class" in `\ncc\Enums\Options > InstallPackageOptions`
 - Updated class type to "final class" in `\ncc\Enums\SpecialConstants > AssemblyConstants`
 - Updated class type to "final class" in `\ncc\Enums\SpecialConstants > BuildConstants`
 - Updated class type to "final class" in `\ncc\Enums\SpecialConstants > DateTimeConstants`
 - Updated class type to "final class" in `\ncc\Enums\SpecialConstants > InstallConstants`
 - Updated class type to "final class" in `\ncc\Enums\SpecialConstants > RuntimeConstants`
 - Updated class type to "final class" in `\ncc\Enums > AuthenticationType`
 - Updated class type to "final class" in `\ncc\Enums > CompilerExtensionDefaultVersions`
 - Updated class type to "final class" in `\ncc\Enums > CompilerExtensions`
 - Updated class type to "final class" in `\ncc\Enums > CompilerExtensionSupportedVersions`
 - Updated class type to "final class" in `\ncc\Enums > ComponentDataType`
 - Updated class type to "final class" in `\ncc\Enums > ComponentFileExtensions`
 - Updated class type to "final class" in `\ncc\Enums > ComposerPackageTypes`
 - Updated class type to "final class" in `\ncc\Enums > ComposerStabilityTypes`
 - Updated class type to "final class" in `\ncc\Enums > EncoderType`
 - Updated class type to "final class" in `\ncc\Enums > ExceptionCodes`
 - Updated class type to "final class" in `\ncc\Enums > HttpRequestType`
 - Updated class type to "final class" in `\ncc\Enums > HttpStatusCodes`
 - Updated class type to "final class" in `\ncc\Enums > LogLevel`
 - Updated class type to "final class" in `\ncc\Enums > NccBuildFlags`
 - Updated class type to "final class" in `\ncc\Enums > PackageStandardVersions`
 - Updated class type to "final class" in `\ncc\Enums > PackageStructureVersions`
 - Updated class type to "final class" in `\ncc\Enums > ProjectType`
 - Updated class type to "final class" in `\ncc\Enums > RegexPattern`
 - Updated class type to "final class" in `\ncc\Enums > RemoteSourceType`
 - Updated class type to "final class" in `\ncc\Enums > Runners`
 - Updated class type to "final class" in `\ncc\Enums > Scopes`
 - Updated class type to "final class" in `\ncc\Enums > Versions`
 - Corrected code-smell and code style issues in `\ncc\Classes > NccExtension > ConstantCompiler`
 - Corrected code-smell and code style issues in `\ncc\Classes > GitlabExtension > GitlabService`
 - Corrected code-smell and code style issues in `\ncc\Classes > GithubExtension > GithubService`
2023-08-19 02:42:22 -04:00

1074 lines
No EOL
44 KiB
PHP

<?php
/*
* Copyright (c) Nosial 2022-2023, all rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction, including without
* limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions
* of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
/** @noinspection PhpMissingFieldTypeInspection */
namespace ncc\Managers;
use Exception;
use ncc\Enums\CompilerExtensions;
use ncc\Enums\ConstantReferences;
use ncc\Enums\DependencySourceType;
use ncc\Enums\LogLevel;
use ncc\Enums\Options\InstallPackageOptions;
use ncc\Enums\RemoteSourceType;
use ncc\Enums\Scopes;
use ncc\Enums\Versions;
use ncc\Classes\ComposerExtension\ComposerSourceBuiltin;
use ncc\Classes\GitClient;
use ncc\Classes\NccExtension\PackageCompiler;
use ncc\Classes\PhpExtension\PhpInstaller;
use ncc\CLI\Main;
use ncc\Exceptions\AccessDeniedException;
use ncc\Exceptions\InstallationException;
use ncc\Exceptions\InvalidPackageNameException;
use ncc\Exceptions\InvalidScopeException;
use ncc\Exceptions\IOException;
use ncc\Exceptions\MissingDependencyException;
use ncc\Exceptions\NotImplementedException;
use ncc\Exceptions\PackageAlreadyInstalledException;
use ncc\Exceptions\PackageFetchException;
use ncc\Exceptions\PackageLockException;
use ncc\Exceptions\PackageNotFoundException;
use ncc\Exceptions\PackageParsingException;
use ncc\Exceptions\PathNotFoundException;
use ncc\Exceptions\RunnerExecutionException;
use ncc\Exceptions\SymlinkException;
use ncc\Exceptions\UnsupportedCompilerExtensionException;
use ncc\Exceptions\VersionNotFoundException;
use ncc\Objects\DefinedRemoteSource;
use ncc\Objects\InstallationPaths;
use ncc\Objects\Package;
use ncc\Objects\PackageLock\PackageEntry;
use ncc\Objects\PackageLock\VersionEntry;
use ncc\Objects\ProjectConfiguration\Dependency;
use ncc\Objects\RemotePackageInput;
use ncc\Objects\Vault\Entry;
use ncc\ThirdParty\jelix\Version\VersionComparator;
use ncc\ThirdParty\Symfony\Filesystem\Filesystem;
use ncc\ThirdParty\theseer\DirectoryScanner\DirectoryScanner;
use ncc\Utilities\Console;
use ncc\Utilities\Functions;
use ncc\Utilities\IO;
use ncc\Utilities\PathFinder;
use ncc\Utilities\Resolver;
use ncc\Utilities\RuntimeCache;
use ncc\Utilities\Validate;
use ncc\ZiProto\ZiProto;
use SplFileInfo;
use Throwable;
class PackageManager
{
/**
* @var string
*/
private $packages_path;
/**
* @var PackageLockManager|null
*/
private $package_lock_manager;
/**
* @throws PackageLockException
*/
public function __construct()
{
$this->packages_path = PathFinder::getPackagesPath(Scopes::SYSTEM);
$this->package_lock_manager = new PackageLockManager();
$this->package_lock_manager->load();
}
/**
* Installs a local package onto the system
*
* @param string $package_path
* @param Entry|null $entry
* @param array $options
* @return string
* @throws AccessDeniedException
* @throws IOException
* @throws InstallationException
* @throws InvalidPackageNameException
* @throws InvalidScopeException
* @throws MissingDependencyException
* @throws NotImplementedException
* @throws PackageAlreadyInstalledException
* @throws PackageLockException
* @throws PackageNotFoundException
* @throws PackageParsingException
* @throws PathNotFoundException
* @throws RunnerExecutionException
* @throws SymlinkException
* @throws UnsupportedCompilerExtensionException
* @throws VersionNotFoundException
*/
public function install(string $package_path, ?Entry $entry=null, array $options=[]): string
{
if(Resolver::resolveScope() !== Scopes::SYSTEM)
{
throw new AccessDeniedException('Insufficient permission to install packages');
}
if(!file_exists($package_path) || !is_file($package_path) || !is_readable($package_path))
{
throw new PathNotFoundException($package_path);
}
$package = Package::load($package_path);
if(RuntimeCache::get(sprintf('installed.%s=%s', $package->Assembly->Package, $package->Assembly->Version)))
{
Console::outDebug(sprintf('skipping installation of %s=%s, already processed', $package->Assembly->Package, $package->Assembly->Version));
return $package->Assembly->Package;
}
$extension = $package->Header->CompilerExtension->Extension;
$installation_paths = new InstallationPaths($this->packages_path . DIRECTORY_SEPARATOR . $package->Assembly->Package . '=' . $package->Assembly->Version);
$installer = match ($extension)
{
CompilerExtensions::PHP => new PhpInstaller($package),
default => throw new UnsupportedCompilerExtensionException('The compiler extension \'' . $extension . '\' is not supported'),
};
if($this->getPackageVersion($package->Assembly->Package, $package->Assembly->Version) !== null)
{
if(in_array(InstallPackageOptions::REINSTALL, $options, true))
{
if($this->getPackageLockManager()?->getPackageLock()?->packageExists($package->Assembly->Package, $package->Assembly->Version))
{
$this->getPackageLockManager()?->getPackageLock()?->removePackageVersion(
$package->Assembly->Package, $package->Assembly->Version
);
}
}
else
{
throw new PackageAlreadyInstalledException('The package ' . $package->Assembly->Package . '=' . $package->Assembly->Version . ' is already installed');
}
}
$execution_pointer_manager = new ExecutionPointerManager();
PackageCompiler::compilePackageConstants($package, [
ConstantReferences::INSTALL => $installation_paths
]);
// Process all the required dependencies before installing the package
if($package->Dependencies !== null && count($package->Dependencies) > 0 && !in_array(InstallPackageOptions::SKIP_DEPENDENCIES, $options, true))
{
foreach($package->Dependencies as $dependency)
{
// Uninstall the dependency if the option Reinstall is passed on
if(in_array(InstallPackageOptions::REINSTALL, $options, true) && $this->getPackageLockManager()?->getPackageLock()?->packageExists($dependency->Name, $dependency->Version))
{
if($dependency->Version === null)
{
$this->uninstallPackage($dependency->Name);
}
else
{
$this->uninstallPackageVersion($dependency->Name, $dependency->Version);
}
}
$this->processDependency($dependency, $package, $package_path, $entry, $options);
}
}
Console::outVerbose(sprintf('Installing %s', $package_path));
if(Resolver::checkLogLevel(LogLevel::DEBUG, Main::getLogLevel()))
{
Console::outDebug(sprintf('installer.install_path: %s', $installation_paths->getInstallationPath()));
Console::outDebug(sprintf('installer.data_path: %s', $installation_paths->getDataPath()));
Console::outDebug(sprintf('installer.bin_path: %s', $installation_paths->getBinPath()));
Console::outDebug(sprintf('installer.src_path: %s', $installation_paths->getSourcePath()));
foreach($package->Assembly->toArray() as $prop => $value)
{
Console::outDebug(sprintf('assembly.%s: %s', $prop, ($value ?? 'n/a')));
}
foreach($package->Header->CompilerExtension->toArray() as $prop => $value)
{
Console::outDebug(sprintf('header.compiler.%s: %s', $prop, ($value ?? 'n/a')));
}
}
Console::out('Installing ' . $package->Assembly->Package);
// Four For Directory Creation, preInstall, postInstall & initData methods
$steps = (4 + count($package->Components) + count ($package->Resources) + count ($package->ExecutionUnits));
// Include the Execution units
if($package->Installer?->PreInstall !== null)
{
$steps += count($package->Installer->PreInstall);
}
if($package->Installer?->PostInstall!== null)
{
$steps += count($package->Installer->PostInstall);
}
$current_steps = 0;
$filesystem = new Filesystem();
try
{
$filesystem->mkdir($installation_paths->getInstallationPath(), 0755);
$filesystem->mkdir($installation_paths->getBinPath(), 0755);
$filesystem->mkdir($installation_paths->getDataPath(), 0755);
$filesystem->mkdir($installation_paths->getSourcePath(), 0755);
++$current_steps;
Console::inlineProgressBar($current_steps, $steps);
}
catch(Exception $e)
{
throw new InstallationException('Error while creating directory, ' . $e->getMessage(), $e);
}
try
{
Console::outDebug(sprintf('saving shadow package to %s', $installation_paths->getDataPath() . DIRECTORY_SEPARATOR . 'pkg'));
self::initData($package, $installation_paths);
$package->save($installation_paths->getDataPath() . DIRECTORY_SEPARATOR . 'pkg');
++$current_steps;
Console::inlineProgressBar($current_steps, $steps);
}
catch(Exception $e)
{
throw new InstallationException('Cannot initialize package install, ' . $e->getMessage(), $e);
}
// Execute the pre-installation stage before the installation stage
try
{
$installer->preInstall($installation_paths);
++$current_steps;
Console::inlineProgressBar($current_steps, $steps);
}
catch (Exception $e)
{
throw new InstallationException('Pre installation stage failed, ' . $e->getMessage(), $e);
}
if($package->Installer?->PreInstall !== null && count($package->Installer->PreInstall) > 0)
{
foreach($package->Installer->PreInstall as $unit_name)
{
try
{
$execution_pointer_manager->temporaryExecute($package, $unit_name);
}
catch(Exception $e)
{
Console::outWarning('Cannot execute unit ' . $unit_name . ', ' . $e->getMessage());
}
++$current_steps;
Console::inlineProgressBar($current_steps, $steps);
}
}
// Process & Install the components
foreach($package->Components as $component)
{
Console::outDebug(sprintf('processing component %s (%s)', $component->name, $component->data_types));
try
{
$data = $installer->processComponent($component);
if($data !== null)
{
$component_path = $installation_paths->getSourcePath() . DIRECTORY_SEPARATOR . $component->name;
$component_dir = dirname($component_path);
if(!$filesystem->exists($component_dir))
{
$filesystem->mkdir($component_dir);
}
IO::fwrite($component_path, $data);
}
}
catch(Exception $e)
{
throw new InstallationException('Cannot process one or more components, ' . $e->getMessage(), $e);
}
++$current_steps;
Console::inlineProgressBar($current_steps, $steps);
}
// Process & Install the resources
foreach($package->Resources as $resource)
{
Console::outDebug(sprintf('processing resource %s', $resource->Name));
try
{
$data = $installer->processResource($resource);
if($data !== null)
{
$resource_path = $installation_paths->getSourcePath() . DIRECTORY_SEPARATOR . $resource->Name;
$resource_dir = dirname($resource_path);
if(!$filesystem->exists($resource_dir))
{
$filesystem->mkdir($resource_dir);
}
IO::fwrite($resource_path, $data);
}
}
catch(Exception $e)
{
throw new InstallationException('Cannot process one or more resources, ' . $e->getMessage(), $e);
}
++$current_steps;
Console::inlineProgressBar($current_steps, $steps);
}
// Install execution units
if($package->ExecutionUnits !== null && count($package->ExecutionUnits) > 0)
{
Console::outDebug('package contains execution units, processing');
$execution_pointer_manager = new ExecutionPointerManager();
$unit_paths = [];
/** @var Package\ExecutionUnit $executionUnit */
foreach($package->ExecutionUnits as $executionUnit)
{
Console::outDebug(sprintf('processing execution unit %s', $executionUnit->execution_policy->Name));
$execution_pointer_manager->addUnit($package->Assembly->Package, $package->Assembly->Version, $executionUnit);
++$current_steps;
Console::inlineProgressBar($current_steps, $steps);
}
IO::fwrite($installation_paths->getDataPath() . DIRECTORY_SEPARATOR . 'exec', ZiProto::encode($unit_paths));
}
else
{
Console::outDebug('package does not contain execution units, skipping');
}
// After execution units are installed, create a symlink if needed
if(isset($package->Header->Options['create_symlink']) && $package->Header->Options['create_symlink'])
{
if($package->MainExecutionPolicy === null)
{
throw new InstallationException('Cannot create symlink, no main execution policy is defined');
}
Console::outDebug(sprintf('creating symlink to %s', $package->Assembly->Package));
$SymlinkManager = new SymlinkManager();
$SymlinkManager->add($package->Assembly->Package, $package->MainExecutionPolicy);
}
// Execute the post-installation stage after the installation is complete
try
{
Console::outDebug('executing post-installation stage');
$installer->postInstall($installation_paths);
++$current_steps;
Console::inlineProgressBar($current_steps, $steps);
}
catch (Exception $e)
{
throw new InstallationException('Post installation stage failed, ' . $e->getMessage(), $e);
}
if($package->Installer?->PostInstall !== null && count($package->Installer->PostInstall) > 0)
{
Console::outDebug('executing post-installation units');
foreach($package->Installer->PostInstall as $unit_name)
{
try
{
$execution_pointer_manager->temporaryExecute($package, $unit_name);
}
catch(Exception $e)
{
Console::outWarning('Cannot execute unit ' . $unit_name . ', ' . $e->getMessage());
}
finally
{
++$current_steps;
Console::inlineProgressBar($current_steps, $steps);
}
}
}
else
{
Console::outDebug('no post-installation units to execute');
}
if($package->Header->UpdateSource !== null && $package->Header->UpdateSource->Repository !== null)
{
$sources_manager = new RemoteSourcesManager();
if($sources_manager->getRemoteSource($package->Header->UpdateSource->Repository->Name) === null)
{
Console::outVerbose('Adding remote source ' . $package->Header->UpdateSource->Repository->Name);
$defined_remote_source = new DefinedRemoteSource();
$defined_remote_source->Name = $package->Header->UpdateSource->Repository->Name;
$defined_remote_source->Host = $package->Header->UpdateSource->Repository->Host;
$defined_remote_source->Type = $package->Header->UpdateSource->Repository->Type;
$defined_remote_source->SSL = $package->Header->UpdateSource->Repository->SSL;
$sources_manager->addRemoteSource($defined_remote_source);
}
}
$this->getPackageLockManager()?->getPackageLock()?->addPackage($package, $installation_paths->getInstallationPath());
$this->getPackageLockManager()?->save();
RuntimeCache::set(sprintf('installed.%s=%s', $package->Assembly->Package, $package->Assembly->Version), true);
return $package->Assembly->Package;
}
/**
* @param string $source
* @param Entry|null $entry
* @return string
* @throws InstallationException
* @throws NotImplementedException
* @throws PackageFetchException
*/
public function fetchFromSource(string $source, ?Entry $entry=null): string
{
$input = new RemotePackageInput($source);
if($input->source === null)
{
throw new PackageFetchException('No source specified');
}
if($input->package === null)
{
throw new PackageFetchException('No package specified');
}
if($input->version === null)
{
$input->version = Versions::LATEST;
}
Console::outVerbose('Fetching package ' . $input->package . ' from ' . $input->source . ' (' . $input->version . ')');
$remote_source_type = Resolver::detectRemoteSourceType($input->source);
if($remote_source_type === RemoteSourceType::BUILTIN)
{
Console::outDebug('using builtin source ' . $input->source);
if ($input->source === 'composer')
{
try
{
return ComposerSourceBuiltin::fetch($input);
}
catch (Exception $e)
{
throw new PackageFetchException('Cannot fetch package from composer source, ' . $e->getMessage(), $e);
}
}
throw new NotImplementedException('Builtin source type ' . $input->source . ' is not implemented');
}
if($remote_source_type === RemoteSourceType::DEFINED)
{
Console::outDebug('using defined source ' . $input->source);
/** @noinspection CallableParameterUseCaseInTypeContextInspection */
$source = (new RemoteSourcesManager())->getRemoteSource($input->source);
if($source === null)
{
throw new InstallationException('Remote source ' . $input->source . ' is not defined');
}
$repositoryQueryResults = Functions::getRepositoryQueryResults($input, $source, $entry);
$exceptions = [];
if($repositoryQueryResults->Files->ZipballUrl !== null)
{
try
{
Console::outDebug(sprintf('fetching package %s from %s', $input->package, $repositoryQueryResults->Files->ZipballUrl));
$archive = Functions::downloadGitServiceFile($repositoryQueryResults->Files->ZipballUrl, $entry);
return PackageCompiler::tryCompile(Functions::extractArchive($archive), $repositoryQueryResults->Version);
}
catch(Throwable $e)
{
Console::outDebug('cannot fetch package from zipball url, ' . $e->getMessage());
$exceptions[] = $e;
}
}
if($repositoryQueryResults->Files->TarballUrl !== null)
{
try
{
Console::outDebug(sprintf('fetching package %s from %s', $input->package, $repositoryQueryResults->Files->TarballUrl));
$archive = Functions::downloadGitServiceFile($repositoryQueryResults->Files->TarballUrl, $entry);
return PackageCompiler::tryCompile(Functions::extractArchive($archive), $repositoryQueryResults->Version);
}
catch(Exception $e)
{
Console::outDebug('cannot fetch package from tarball url, ' . $e->getMessage());
$exceptions[] = $e;
}
}
if($repositoryQueryResults->Files->PackageUrl !== null)
{
try
{
Console::outDebug(sprintf('fetching package %s from %s', $input->package, $repositoryQueryResults->Files->PackageUrl));
return Functions::downloadGitServiceFile($repositoryQueryResults->Files->PackageUrl, $entry);
}
catch(Exception $e)
{
Console::outDebug('cannot fetch package from package url, ' . $e->getMessage());
$exceptions[] = $e;
}
}
if($repositoryQueryResults->Files->GitHttpUrl !== null || $repositoryQueryResults->Files->GitSshUrl !== null)
{
try
{
Console::outDebug(sprintf('fetching package %s from %s', $input->package, $repositoryQueryResults->Files->GitHttpUrl ?? $repositoryQueryResults->Files->GitSshUrl));
$git_repository = GitClient::cloneRepository($repositoryQueryResults->Files->GitHttpUrl ?? $repositoryQueryResults->Files->GitSshUrl);
foreach(GitClient::getTags($git_repository) as $tag)
{
if(VersionComparator::compareVersion($tag, $repositoryQueryResults->Version) === 0)
{
GitClient::checkout($git_repository, $tag);
return PackageCompiler::tryCompile($git_repository, $repositoryQueryResults->Version);
}
}
Console::outDebug('cannot fetch package from git repository, no matching tag found');
}
catch(Exception $e)
{
Console::outDebug('cannot fetch package from git repository, ' . $e->getMessage());
$exceptions[] = $e;
}
}
// Recursively create an exception with the previous exceptions as the previous exception
$exception = null;
if(count($exceptions) > 0)
{
foreach($exceptions as $e)
{
if($exception === null)
{
$exception = new PackageFetchException($e->getMessage(), $e);
}
else
{
if($e->getMessage() === $exception->getMessage())
{
continue;
}
$exception = new PackageFetchException($e->getMessage(), $exception);
}
}
}
else
{
$exception = new PackageFetchException('Cannot fetch package from remote source, no assets found');
}
throw $exception;
}
throw new PackageFetchException(sprintf('Unknown remote source type %s', $remote_source_type));
}
/**
* Installs a package from a source syntax (vendor/package=version@source)
*
* @param string $source
* @param Entry|null $entry
* @param array $options
* @return string
* @throws InstallationException
*/
public function installFromSource(string $source, ?Entry $entry, array $options=[]): string
{
try
{
Console::outVerbose(sprintf('Installing package from source %s', $source));
$package = $this->fetchFromSource($source, $entry);
return $this->install($package, $entry, $options);
}
catch(Exception $e)
{
throw new InstallationException('Cannot install package from source, ' . $e->getMessage(), $e);
}
}
/**
* @param Dependency $dependency
* @param Package $package
* @param string $package_path
* @param Entry|null $entry
* @param array $options
* @return void
* @throws AccessDeniedException
* @throws IOException
* @throws InstallationException
* @throws InvalidPackageNameException
* @throws InvalidScopeException
* @throws MissingDependencyException
* @throws NotImplementedException
* @throws PackageAlreadyInstalledException
* @throws PackageLockException
* @throws PackageNotFoundException
* @throws PackageParsingException
* @throws PathNotFoundException
* @throws RunnerExecutionException
* @throws SymlinkException
* @throws UnsupportedCompilerExtensionException
* @throws VersionNotFoundException
* @throws PathNotFoundException
*/
private function processDependency(Dependency $dependency, Package $package, string $package_path, ?Entry $entry=null, array $options=[]): void
{
if(RuntimeCache::get(sprintf('dependency_installed.%s=%s', $dependency->Name, $dependency->Version ?? 'null')))
{
Console::outDebug(sprintf('dependency %s=%s already processed, skipping', $dependency->Name, $dependency->Version ?? 'null'));
return;
}
Console::outVerbose('processing dependency ' . $dependency->Name . ' (' . $dependency->Version . ')');
$dependent_package = $this->getPackage($dependency->Name);
$dependency_met = false;
if ($dependent_package !== null && $dependency->Version !== null && Validate::version($dependency->Version))
{
Console::outDebug('dependency has version constraint, checking if package is installed');
$dependent_version = $this->getPackageVersion($dependency->Name, $dependency->Version);
if ($dependent_version !== null)
{
$dependency_met = true;
}
}
elseif ($dependent_package !== null && $dependency->Version === null)
{
Console::outDebug(sprintf('dependency %s has no version specified, assuming dependency is met', $dependency->Name));
$dependency_met = true;
}
Console::outDebug('dependency met: ' . ($dependency_met ? 'true' : 'false'));
if ($dependency->SourceType !== null && !$dependency_met)
{
Console::outVerbose(sprintf('Installing dependency %s=%s for %s=%s', $dependency->Name, $dependency->Version, $package->Assembly->Package, $package->Assembly->Version));
switch ($dependency->SourceType)
{
case DependencySourceType::LOCAL:
Console::outDebug('installing from local source ' . $dependency->Source);
$basedir = dirname($package_path);
if (!file_exists($basedir . DIRECTORY_SEPARATOR . $dependency->Source))
{
throw new PathNotFoundException($basedir . DIRECTORY_SEPARATOR . $dependency->Source);
}
$this->install($basedir . DIRECTORY_SEPARATOR . $dependency->Source, null, $options);
RuntimeCache::set(sprintf('dependency_installed.%s=%s', $dependency->Name, $dependency->Version), true);
break;
case DependencySourceType::STATIC:
throw new PackageNotFoundException('Static linking not possible, package ' . $dependency->Name . ' is not installed');
case DependencySourceType::REMOTE:
Console::outDebug('installing from remote source ' . $dependency->Source);
$this->installFromSource($dependency->Source, $entry, $options);
RuntimeCache::set(sprintf('dependency_installed.%s=%s', $dependency->Name, $dependency->Version), true);
break;
default:
throw new NotImplementedException('Dependency source type ' . $dependency->SourceType . ' is not implemented');
}
}
elseif(!$dependency_met)
{
throw new MissingDependencyException(sprintf('The dependency %s=%s for %s=%s is not met', $dependency->Name, $dependency->Version, $package->Assembly->Package, $package->Assembly->Version));
}
}
/**
* Returns an existing package entry, returns null if no such entry exists
*
* @param string $package
* @return PackageEntry|null
* @throws PackageLockException
*/
public function getPackage(string $package): ?PackageEntry
{
Console::outDebug('getting package ' . $package);
return $this->getPackageLockManager()?->getPackageLock()?->getPackage($package);
}
/**
* Returns an existing version entry, returns null if no such entry exists
*
* @param string $package
* @param string $version
* @return VersionEntry|null
* @throws VersionNotFoundException
* @throws PackageLockException
*/
public function getPackageVersion(string $package, string $version): ?VersionEntry
{
Console::outDebug('getting package version ' . $package . '=' . $version);
return $this->getPackage($package)?->getVersion($version);
}
/**
* Returns the latest version of the package, or null if there is no entry
*
* @param string $package
* @return VersionEntry|null
* @throws VersionNotFoundException
* @throws PackageLockException
* @noinspection PhpUnused
*/
public function getLatestVersion(string $package): ?VersionEntry
{
Console::outDebug('getting latest version of package ' . $package);
return $this->getPackage($package)?->getVersion($this->getPackage($package)?->getLatestVersion());
}
/**
* Returns an array of all packages and their installed versions
*
* @return array
* @throws PackageLockException
* @throws PackageLockException
*/
public function getInstalledPackages(): array
{
return $this->getPackageLockManager()?->getPackageLock()?->getPackages() ?? [];
}
/**
* Returns a package tree representation
*
* @param array $tree
* @param string|null $package
* @return array
*/
public function getPackageTree(array $tree=[], ?string $package=null): array
{
// First build the packages to scan first
$packages = [];
if($package !== null)
{
// If it's coming from a selected package, query the package and process its dependencies
$exploded = explode('=', $package);
try
{
/** @noinspection CallableParameterUseCaseInTypeContextInspection */
$package = $this->getPackage($exploded[0]);
if($package === null)
{
throw new PackageNotFoundException('Package ' . $exploded[0] . ' not found');
}
$version = $package->getVersion($exploded[1]);
if($version === null)
{
throw new VersionNotFoundException('Version ' . $exploded[1] . ' not found for package ' . $exploded[0]);
}
foreach ($version->Dependencies as $dependency)
{
if(!in_array($dependency->PackageName . '=' . $dependency->Version, $tree, true))
{
$packages[] = $dependency->PackageName . '=' . $dependency->Version;
}
}
}
catch(Exception $e)
{
unset($e);
}
}
else
{
// If it's coming from nothing, start with the installed packages on the system
try
{
foreach ($this->getInstalledPackages() as $installed_package => $versions)
{
foreach ($versions as $version)
{
if (!in_array($installed_package . '=' . $version, $packages, true))
{
$packages[] = $installed_package . '=' . $version;
}
}
}
}
catch (PackageLockException $e)
{
unset($e);
}
}
// Go through each package
foreach($packages as $package_iter)
{
$package_e = explode('=', $package_iter);
try
{
$version_entry = $this->getPackageVersion($package_e[0], $package_e[1]);
if($version_entry === null)
{
Console::outWarning('Version ' . $package_e[1] . ' of package ' . $package_e[0] . ' not found');
}
else
{
$tree[$package_iter] = null;
if($version_entry->Dependencies !== null && count($version_entry->Dependencies) > 0)
{
$tree[$package_iter] = [];
foreach($version_entry->Dependencies as $dependency)
{
$dependency_name = sprintf('%s=%s', $dependency->PackageName, $dependency->Version);
$tree[$package_iter] = $this->getPackageTree($tree[$package_iter], $dependency_name);
}
}
}
}
catch(Exception $e)
{
unset($e);
}
}
return $tree;
}
/**
* Uninstalls a package version
*
* @param string $package
* @param string $version
* @return void
* @throws AccessDeniedException
* @throws IOException
* @throws PackageLockException
* @throws PackageNotFoundException
* @throws SymlinkException
* @throws VersionNotFoundException
*/
public function uninstallPackageVersion(string $package, string $version): void
{
if(Resolver::resolveScope() !== Scopes::SYSTEM)
{
throw new AccessDeniedException('Insufficient permission to uninstall packages');
}
$version_entry = $this->getPackageVersion($package, $version);
if($version_entry === null)
{
throw new PackageNotFoundException(sprintf('The package %s=%s was not found', $package, $version));
}
Console::out(sprintf('Uninstalling %s=%s', $package, $version));
Console::outVerbose(sprintf('Removing package %s=%s from PackageLock', $package, $version));
if(!$this->getPackageLockManager()?->getPackageLock()?->removePackageVersion($package, $version))
{
Console::outDebug('warning: removing package from package lock failed');
}
$this->getPackageLockManager()?->save();
Console::outVerbose('Removing package files');
$scanner = new DirectoryScanner();
$filesystem = new Filesystem();
if($filesystem->exists($version_entry->Location))
{
Console::outVerbose(sprintf('Removing package files from %s', $version_entry->Location));
/** @var SplFileInfo $item */
/** @noinspection PhpRedundantOptionalArgumentInspection */
foreach($scanner($version_entry->Location, true) as $item)
{
if(is_file($item->getPath()))
{
Console::outDebug('removing file ' . $item->getPath());
Console::outDebug(sprintf('deleting %s', $item->getPath()));
$filesystem->remove($item->getPath());
}
}
}
else
{
Console::outWarning(sprintf('warning: package location %s does not exist', $version_entry->Location));
}
$filesystem->remove($version_entry->Location);
if($version_entry->ExecutionUnits !== null && count($version_entry->ExecutionUnits) > 0)
{
Console::outVerbose('Uninstalling execution units');
$execution_pointer_manager = new ExecutionPointerManager();
foreach($version_entry->ExecutionUnits as $executionUnit)
{
if(!$execution_pointer_manager->removeUnit($package, $version, $executionUnit->execution_policy->Name))
{
Console::outDebug(sprintf('warning: removing execution unit %s failed', $executionUnit->execution_policy->Name));
}
}
}
$symlink_manager = new SymlinkManager();
$symlink_manager->sync();
}
/**
* Uninstalls all versions of a package
*
* @param string $package
* @return void
* @throws AccessDeniedException
* @throws PackageLockException
* @throws PackageNotFoundException
* @throws VersionNotFoundException
*/
public function uninstallPackage(string $package): void
{
if(Resolver::resolveScope() !== Scopes::SYSTEM)
{
throw new AccessDeniedException('Insufficient permission to uninstall packages');
}
$package_entry = $this->getPackage($package);
if($package_entry === null)
{
throw new PackageNotFoundException(sprintf('The package %s was not found', $package));
}
foreach($package_entry->getVersions() as $version)
{
$version_entry = $package_entry->getVersion($version);
if($version_entry === null)
{
Console::outDebug(sprintf('warning: version %s of package %s not found', $version, $package));
continue;
}
try
{
$this->uninstallPackageVersion($package, $version_entry->Version);
}
catch(Exception $e)
{
Console::outDebug(sprintf('warning: unable to uninstall package %s=%s, %s (%s)', $package, $version_entry->Version, $e->getMessage(), $e->getCode()));
}
}
}
/**
* @param Package $package
* @param InstallationPaths $paths
* @throws InstallationException
*/
private static function initData(Package $package, InstallationPaths $paths): void
{
Console::outVerbose(sprintf('Initializing data for %s', $package->Assembly->Name));
// Create data files
$dependencies = [];
foreach($package->Dependencies as $dependency)
{
$dependencies[] = $dependency->toArray(true);
}
$data_files = [
$paths->getDataPath() . DIRECTORY_SEPARATOR . 'assembly' =>
ZiProto::encode($package->Assembly->toArray(true)),
$paths->getDataPath() . DIRECTORY_SEPARATOR . 'ext' =>
ZiProto::encode($package->Header->CompilerExtension->toArray()),
$paths->getDataPath() . DIRECTORY_SEPARATOR . 'const' =>
ZiProto::encode($package->Header->RuntimeConstants),
$paths->getDataPath() . DIRECTORY_SEPARATOR . 'dependencies' =>
ZiProto::encode($dependencies),
];
foreach($data_files as $file => $data)
{
try
{
Console::outDebug(sprintf('generating data file %s', $file));
IO::fwrite($file, $data);
}
catch (IOException $e)
{
throw new InstallationException('Cannot write to file \'' . $file . '\', ' . $e->getMessage(), $e);
}
}
}
/**
* @return PackageLockManager|null
*/
private function getPackageLockManager(): ?PackageLockManager
{
if($this->package_lock_manager === null)
{
$this->package_lock_manager = new PackageLockManager();
}
return $this->package_lock_manager;
}
}