From 150ff9dee3345f001d55f9e9c16bc2a7846d6492 Mon Sep 17 00:00:00 2001 From: Netkas Date: Fri, 16 Dec 2022 01:15:12 -0500 Subject: [PATCH] Refactored \ncc\Classes\ComposerExtension > ComposerSourceBuiltin to allow for building from local packages --- .../ComposerSourceBuiltin.php | 465 +++++++++++------- 1 file changed, 288 insertions(+), 177 deletions(-) diff --git a/src/ncc/Classes/ComposerExtension/ComposerSourceBuiltin.php b/src/ncc/Classes/ComposerExtension/ComposerSourceBuiltin.php index 8fbd154..85f94f5 100644 --- a/src/ncc/Classes/ComposerExtension/ComposerSourceBuiltin.php +++ b/src/ncc/Classes/ComposerExtension/ComposerSourceBuiltin.php @@ -32,6 +32,8 @@ use ncc\Exceptions\UserAbortedOperationException; use ncc\Interfaces\ServiceSourceInterface; use ncc\Managers\ProjectManager; + use ncc\ncc; + use ncc\Objects\ComposerJson; use ncc\Objects\ComposerLock; use ncc\Objects\ProjectConfiguration; use ncc\Objects\RemotePackageInput; @@ -95,6 +97,68 @@ throw new RuntimeException(sprintf('Could not find package %s in the compiled packages', $packageInput->toStandard())); } + /** + * Works with a local composer.json file and attempts to compile the required packages + * and their dependencies, returns the path to the compiled package. + * + * @param string $path + * @return string + * @throws AccessDeniedException + * @throws BuildConfigurationNotFoundException + * @throws BuildException + * @throws ComposerDisabledException + * @throws ComposerException + * @throws ComposerNotAvailableException + * @throws DirectoryNotFoundException + * @throws FileNotFoundException + * @throws IOException + * @throws InternalComposerNotAvailableException + * @throws MalformedJsonException + * @throws PackageNotFoundException + * @throws PackagePreparationFailedException + * @throws ProjectConfigurationNotFoundException + * @throws UnsupportedCompilerExtensionException + * @throws UnsupportedRunnerException + * @throws UserAbortedOperationException + */ + public static function fromLocal(string $path): string + { + // Check if the file composer.json exists + if (!file_exists($path . DIRECTORY_SEPARATOR . 'composer.json')) + throw new FileNotFoundException(sprintf('File "%s" not found', $path . DIRECTORY_SEPARATOR . 'composer.json')); + + // Execute composer with options + $options = self::getOptions(); + $composer_exec = self::getComposerPath(); + $process = new Process([$composer_exec, 'install']); + self::prepareProcess($process, $path, $options); + + Console::outDebug(sprintf('executing %s', $process->getCommandLine())); + $process->run(function ($type, $buffer) { + Console::out($buffer, false); + }); + + if (!$process->isSuccessful()) + throw new ComposerException($process->getErrorOutput()); + + $filesystem = new Filesystem(); + if($filesystem->exists($path . DIRECTORY_SEPARATOR . 'build')) + $filesystem->remove($path . DIRECTORY_SEPARATOR . 'build'); + $filesystem->mkdir($path . DIRECTORY_SEPARATOR . 'build'); + + // Compile dependencies + self::compilePackages($path . DIRECTORY_SEPARATOR . 'composer.lock'); + + // Finally convert the main package's composer.json to package.json and compile it + ComposerSourceBuiltin::convertProject($path); + $project_manager = new ProjectManager($path); + $project_manager->load(); + $built_package = $project_manager->build(); + + RuntimeCache::setFileAsTemporary($built_package); + return $built_package; + } + /** * @param string $composer_lock_path * @return array @@ -133,154 +197,16 @@ foreach ($composer_lock->Packages as $package) { $package_path = $base_dir . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . $package->Name; - // Generate the package configuration - $project_configuration = ComposerSourceBuiltin::generateProjectConfiguration($package->Name, $composer_lock); - // Process the source files - if ($package->Autoload !== null) - { - $source_directory = $package_path . DIRECTORY_SEPARATOR . '.src'; - if ($filesystem->exists($source_directory)) - { - $filesystem->remove($source_directory); - } - $filesystem->mkdir($source_directory); - $source_directories = []; - $static_files = []; + // Load the composer lock file + $composer_package = $composer_lock->getPackage($package->Name); + if ($composer_package == null) + throw new PackageNotFoundException(sprintf('Package "%s" not found in composer lock file', $package->Name)); - // Extract all the source directories - if ($package->Autoload->Psr4 !== null && count($package->Autoload->Psr4) > 0) - { - Console::outVerbose('Extracting PSR-4 source directories'); - foreach ($package->Autoload->Psr4 as $namespace_pointer) - { - if ($namespace_pointer->Path !== null && !in_array($namespace_pointer->Path, $source_directories)) - { - $source_directories[] = $package_path . DIRECTORY_SEPARATOR . $namespace_pointer->Path; - } - } - } - - if ($package->Autoload->Psr0 !== null && count($package->Autoload->Psr0) > 0) - { - Console::outVerbose('Extracting PSR-0 source directories'); - foreach ($package->Autoload->Psr0 as $namespace_pointer) - { - if ($namespace_pointer->Path !== null && !in_array($namespace_pointer->Path, $source_directories)) - { - $source_directories[] = $package_path . DIRECTORY_SEPARATOR . $namespace_pointer->Path; - } - } - } - - if ($package->Autoload->Files !== null && count($package->Autoload->Files) > 0) - { - Console::outVerbose('Extracting static files'); - foreach ($package->Autoload->Files as $file) - { - $static_files[] = $package_path . DIRECTORY_SEPARATOR . $file; - } - } - - Console::outDebug(sprintf('source directories: %s', implode(', ', $source_directories))); - - // First scan the project files and create a file struct. - $DirectoryScanner = new DirectoryScanner(); - - // TODO: Implement exclude-class handling - try - { - $DirectoryScanner->unsetFlag(FilesystemIterator::FOLLOW_SYMLINKS); - } - catch (Exception $e) - { - throw new PackagePreparationFailedException('Cannot unset flag \'FOLLOW_SYMLINKS\' in DirectoryScanner, ' . $e->getMessage(), $e); - } - - // Include file components that can be compiled - $DirectoryScanner->setIncludes(ComponentFileExtensions::Php); - - foreach ($source_directories as $directory) - { - /** @var SplFileInfo $item */ - /** @noinspection PhpRedundantOptionalArgumentInspection */ - foreach ($DirectoryScanner($directory, True) as $item) - { - if (is_dir($item->getPathName())) - continue; - - $parsed_path = str_ireplace($package_path . DIRECTORY_SEPARATOR, '', $item->getPathName()); - - Console::outDebug(sprintf('copying file %s for package %s', $parsed_path, $package->Name)); - $filesystem->copy($item->getPathName(), $source_directory . DIRECTORY_SEPARATOR . $parsed_path); - } - } - - if (count($static_files) > 0) - { - $project_configuration->Project->Options['static_files'] = $static_files; - - foreach ($static_files as $file) - { - $parsed_path = str_ireplace($package_path . DIRECTORY_SEPARATOR, '', $file); - Console::outDebug(sprintf('copying file %s for package %s', $parsed_path, $package->Name)); - $filesystem->copy($file, $source_directory . DIRECTORY_SEPARATOR . $parsed_path); - } - unset($file); - } - - $project_configuration->toFile($package_path . DIRECTORY_SEPARATOR . 'project.json'); - } + // Convert it to a NCC project configuration + $project_configuration = self::convertProject($package_path, $composer_package); // Load the project - $license_files = [ - 'LICENSE', - 'license', - 'LICENSE.txt', - 'license.txt' - ]; - - foreach($license_files as $license_file) - { - if($filesystem->exists($package_path . DIRECTORY_SEPARATOR . $license_file)) - { - // Check configuration if composer.extension.display_licenses is set - if(Functions::cbool(Functions::getConfigurationProperty('composer.extension.display_licenses'))) - { - Console::out(sprintf('License for package %s:', $package->Name)); - Console::out(IO::fread($package_path . DIRECTORY_SEPARATOR . $license_file)); - } - } - } - - if(Functions::cbool(Functions::getConfigurationProperty('composer.extension.display_authors'))) - { - if($package->Authors !== null && count($package->Authors) > 0) - { - Console::out(sprintf('Authors for package %s:', $package->Name)); - foreach($package->Authors as $author) - { - Console::out(sprintf(' - %s', $author->Name)); - - if($author->Email !== null) - { - Console::out(sprintf(' %s', $author->Email)); - } - - if($author->Homepage !== null) - { - Console::out(sprintf(' %s', $author->Homepage)); - } - - if($author->Role !== null) - { - Console::out(sprintf(' %s', $author->Role)); - } - - } - } - } - $project_manager = new ProjectManager($package_path); $project_manager->load(); $built_package = $project_manager->build(); @@ -288,6 +214,7 @@ // Copy the project to the build directory $out_path = $base_dir . DIRECTORY_SEPARATOR . 'build' . DIRECTORY_SEPARATOR . sprintf('%s=%s.ncc', $project_configuration->Assembly->Package, $project_configuration->Assembly->Version); $filesystem->copy($built_package, $out_path); + $filesystem->remove($built_package); $built_packages[$project_configuration->Assembly->Package] = $out_path; } @@ -323,18 +250,12 @@ * Generates a project configuration from a package selection * from the composer.lock file * - * @param string $package_name - * @param ComposerLock $composer_lock + * @param ComposerJson $composer_package * @return ProjectConfiguration - * @throws PackageNotFoundException + * @throws Exception */ - private static function generateProjectConfiguration(string $package_name, ComposerLock $composer_lock): ProjectConfiguration + private static function generateProjectConfiguration(ComposerJson $composer_package): ProjectConfiguration { - // Load the composer lock file - $composer_package = $composer_lock->getPackage($package_name); - if ($composer_package == null) - throw new PackageNotFoundException(sprintf('Package "%s" not found in composer lock file', $package_name)); - // Generate a new project configuration object $project_configuration = new ProjectConfiguration(); @@ -345,14 +266,22 @@ if (isset($composer_package->Version)) $project_configuration->Assembly->Version = Functions::parseVersion($composer_package->Version); + if($project_configuration->Assembly->Version == null || $project_configuration->Assembly->Version == '') + $project_configuration->Assembly->Version = '1.0.0'; + $project_configuration->Assembly->UUID = Uuid::v1()->toRfc4122(); - $project_configuration->Assembly->Package = self::toPackageName($package_name); + $project_configuration->Assembly->Package = self::toPackageName($composer_package->Name); + + // Add the update source + $project_configuration->Project->UpdateSource = new ProjectConfiguration\UpdateSource(); + $project_configuration->Project->UpdateSource->Source = sprintf('%s=%s@composer', str_ireplace('\\', '/', $composer_package->Name), $composer_package->Version); + $project_configuration->Project->UpdateSource->Repository = null; // Process the dependencies foreach ($composer_package->Require as $item) { $package_name = self::toPackageName($item->PackageName); - $package_version = $composer_lock->getPackage($item->PackageName)?->Version; + $package_version = $composer_package->Version; if ($package_version == null) { $package_version = '1.0.0'; @@ -485,7 +414,6 @@ * @param string $vendor * @param string $package * @param string|null $version - * @param array $options * @return string * @throws AccessDeniedException * @throws ComposerDisabledException @@ -497,7 +425,7 @@ * @throws InvalidScopeException * @throws UserAbortedOperationException */ - private static function require(string $vendor, string $package, ?string $version = null, array $options = []): string + private static function require(string $vendor, string $package, ?string $version = null): string { if (Resolver::resolveScope() !== Scopes::System) throw new AccessDeniedException('Insufficient permissions to require'); @@ -530,26 +458,7 @@ // Execute composer with options $options = self::getOptions(); $process = new Process(array_merge([$composer_exec, 'require'], $options)); - $process->setWorkingDirectory($tmp_dir); - - // Check if scripts are enabled while running as root - if (!in_array('--no-scripts', $options) && Resolver::resolveScope() == Scopes::System) - { - Console::outWarning('composer scripts are enabled while running as root, this can allow malicious scripts to run as root'); - if (!isset($options['--no-interaction'])) - { - if (!Console::getBooleanInput('Do you want to continue?')) - throw new UserAbortedOperationException('The operation was aborted by the user'); - - // The user understands the risks and wants to continue - $process->setEnv(['COMPOSER_ALLOW_SUPERUSER' => 1]); - } - } - else - { - // Composer is running "safely". We can disable the superuser check - $process->setEnv(['COMPOSER_ALLOW_SUPERUSER' => 1]); - } + self::prepareProcess($process, $tmp_dir, $options); Console::outDebug(sprintf('executing %s', $process->getCommandLine())); $process->run(function ($type, $buffer) { @@ -606,4 +515,206 @@ throw new ComposerNotAvailableException('No composer executable path is configured'); } + + /** + * @param Process $process + * @param string $path + * @param array $options + * @return void + * @throws UserAbortedOperationException + */ + private static function prepareProcess(Process $process, string $path, array $options): void + { + $process->setWorkingDirectory($path); + + // Check if scripts are enabled while running as root + if (!in_array('--no-scripts', $options) && Resolver::resolveScope() == Scopes::System) + { + Console::outWarning('composer scripts are enabled while running as root, this can allow malicious scripts to run as root'); + if (!isset($options['--no-interaction'])) + { + if (!Console::getBooleanInput('Do you want to continue?')) + throw new UserAbortedOperationException('The operation was aborted by the user'); + + // The user understands the risks and wants to continue + $process->setEnv(['COMPOSER_ALLOW_SUPERUSER' => 1]); + } + } + else + { + // Composer is running "safely". We can disable the superuser check + $process->setEnv(['COMPOSER_ALLOW_SUPERUSER' => 1]); + } + } + + /** + * Converts a composer project to a NCC project + * + * @param mixed $composer_package + * @param string $package_path + * @return ProjectConfiguration + * @throws AccessDeniedException + * @throws FileNotFoundException + * @throws IOException + * @throws MalformedJsonException + * @throws PackagePreparationFailedException + * @throws Exception + */ + private static function convertProject(string $package_path, ?ComposerJson $composer_package=null): ProjectConfiguration + { + if($composer_package == null) + ComposerJson::fromArray(Functions::loadJsonFile($package_path . DIRECTORY_SEPARATOR . 'composer.json')); + + $project_configuration = ComposerSourceBuiltin::generateProjectConfiguration($composer_package); + $filesystem = new Filesystem(); + + // Process the source files + if ($composer_package->Autoload !== null) + { + $source_directory = $package_path . DIRECTORY_SEPARATOR . '.src'; + if ($filesystem->exists($source_directory)) + $filesystem->remove($source_directory); + $filesystem->mkdir($source_directory); + $source_directories = []; + $static_files = []; + + // Extract all the source directories + if ($composer_package->Autoload->Psr4 !== null && count($composer_package->Autoload->Psr4) > 0) + { + Console::outVerbose('Extracting PSR-4 source directories'); + foreach ($composer_package->Autoload->Psr4 as $namespace_pointer) + { + if ($namespace_pointer->Path !== null && !in_array($namespace_pointer->Path, $source_directories)) + { + $source_directories[] = $package_path . DIRECTORY_SEPARATOR . $namespace_pointer->Path; + } + } + } + + if ($composer_package->Autoload->Psr0 !== null && count($composer_package->Autoload->Psr0) > 0) + { + Console::outVerbose('Extracting PSR-0 source directories'); + foreach ($composer_package->Autoload->Psr0 as $namespace_pointer) + { + if ($namespace_pointer->Path !== null && !in_array($namespace_pointer->Path, $source_directories)) + { + $source_directories[] = $package_path . DIRECTORY_SEPARATOR . $namespace_pointer->Path; + } + } + } + + if ($composer_package->Autoload->Files !== null && count($composer_package->Autoload->Files) > 0) + { + Console::outVerbose('Extracting static files'); + foreach ($composer_package->Autoload->Files as $file) + { + $static_files[] = $package_path . DIRECTORY_SEPARATOR . $file; + } + } + + Console::outDebug(sprintf('source directories: %s', implode(', ', $source_directories))); + + // First scan the project files and create a file struct. + $DirectoryScanner = new DirectoryScanner(); + + // TODO: Implement exclude-class handling + try + { + $DirectoryScanner->unsetFlag(FilesystemIterator::FOLLOW_SYMLINKS); + } + catch (Exception $e) + { + throw new PackagePreparationFailedException('Cannot unset flag \'FOLLOW_SYMLINKS\' in DirectoryScanner, ' . $e->getMessage(), $e); + } + + // Include file components that can be compiled + $DirectoryScanner->setIncludes(ComponentFileExtensions::Php); + + foreach ($source_directories as $directory) + { + /** @var SplFileInfo $item */ + /** @noinspection PhpRedundantOptionalArgumentInspection */ + foreach ($DirectoryScanner($directory, True) as $item) + { + if (is_dir($item->getPathName())) + continue; + + $parsed_path = str_ireplace($package_path . DIRECTORY_SEPARATOR, '', $item->getPathName()); + + Console::outDebug(sprintf('copying file %s for package %s', $parsed_path, $composer_package->Name)); + $filesystem->copy($item->getPathName(), $source_directory . DIRECTORY_SEPARATOR . $parsed_path); + } + } + + if (count($static_files) > 0) + { + $project_configuration->Project->Options['static_files'] = $static_files; + + foreach ($static_files as $file) + { + $parsed_path = str_ireplace($package_path . DIRECTORY_SEPARATOR, '', $file); + Console::outDebug(sprintf('copying file %s for package %s', $parsed_path, $composer_package->Name)); + $filesystem->copy($file, $source_directory . DIRECTORY_SEPARATOR . $parsed_path); + } + unset($file); + } + + $project_configuration->toFile($package_path . DIRECTORY_SEPARATOR . 'project.json'); + } + + // This part simply displays the package information to the command-line interface + if(ncc::cliMode()) + { + $license_files = [ + 'LICENSE', + 'license', + 'LICENSE.txt', + 'license.txt' + ]; + + foreach($license_files as $license_file) + { + if($filesystem->exists($package_path . DIRECTORY_SEPARATOR . $license_file)) + { + // Check configuration if composer.extension.display_licenses is set + if(Functions::cbool(Functions::getConfigurationProperty('composer.extension.display_licenses'))) + { + Console::out(sprintf('License for package %s:', $composer_package->Name)); + Console::out(IO::fread($package_path . DIRECTORY_SEPARATOR . $license_file)); + break; + } + } + } + + if(Functions::cbool(Functions::getConfigurationProperty('composer.extension.display_authors'))) + { + if($composer_package->Authors !== null && count($composer_package->Authors) > 0) + { + Console::out(sprintf('Authors for package %s:', $composer_package->Name)); + foreach($composer_package->Authors as $author) + { + Console::out(sprintf(' - %s', $author->Name)); + + if($author->Email !== null) + { + Console::out(sprintf(' %s', $author->Email)); + } + + if($author->Homepage !== null) + { + Console::out(sprintf(' %s', $author->Homepage)); + } + + if($author->Role !== null) + { + Console::out(sprintf(' %s', $author->Role)); + } + + } + } + } + } + + return $project_configuration; + } } \ No newline at end of file