Skip to content
Snippets Groups Projects
Commit 10950a57 authored by Adam G-H's avatar Adam G-H
Browse files

Issue #3296261 by tedbow, phenaproxima, drumm: Add the ability to map package...

Issue #3296261 by tedbow, phenaproxima, drumm: Add the ability to map package names to project names and vice-versa
parent 2b632894
No related branches found
No related tags found
No related merge requests found
Showing
with 359 additions and 1 deletion
......@@ -15,7 +15,7 @@
"drupal/core": "^9.3",
"php-tuf/composer-stager": "^1.0.0-beta2",
"composer/composer": "^2.2.12 || ^2.3.5",
"composer-runtime-api": "^2.0.9",
"composer-runtime-api": "^2.1",
"symfony/config": "^4.4 || ^6.1",
"php": ">=7.4.0"
},
......
......@@ -8,6 +8,7 @@ use Composer\IO\NullIO;
use Composer\Package\PackageInterface;
use Composer\Semver\Comparator;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Serialization\Yaml;
/**
* Defines a utility object to get information from Composer's API.
......@@ -190,4 +191,112 @@ class ComposerUtility {
return array_filter($packages, $filter, ARRAY_FILTER_USE_BOTH);
}
/**
* Returns installed package data from Composer's `installed.php`.
*
* @return array
* The installed package data as represented in Composer's `installed.php`,
* keyed by package name.
*/
private function getInstalledPackagesData(): array {
$installed_php = implode(DIRECTORY_SEPARATOR, [
// Composer returns the absolute path to the vendor directory by default.
$this->getComposer()->getConfig()->get('vendor-dir'),
'composer',
'installed.php',
]);
$data = include $installed_php;
return $data['versions'];
}
/**
* Returns the Drupal project name for a given Composer package.
*
* @param string $package_name
* The name of the package.
*
* @return string|null
* The name of the Drupal project installed by the package, or NULL if:
* - The package is not installed.
* - The package is not of a supported type (one of `drupal-module`,
* `drupal-theme`, or `drupal-profile`).
* - The package name does not begin with `drupal/`.
* - The project name could not otherwise be determined.
*/
public function getProjectForPackage(string $package_name): ?string {
$data = $this->getInstalledPackagesData();
if (array_key_exists($package_name, $data)) {
$package = $data[$package_name];
$supported_package_types = [
'drupal-module',
'drupal-theme',
'drupal-profile',
];
// Only consider packages which are packaged by drupal.org and will be
// known to the core Update module.
if (str_starts_with($package_name, 'drupal/') && in_array($package['type'], $supported_package_types, TRUE)) {
return $this->scanForProjectName($package['install_path']);
}
}
return NULL;
}
/**
* Returns the package name for a given Drupal project.
*
* @param string $project_name
* The name of the project.
*
* @return string|null
* The name of the Composer package which installs the project, or NULL if
* it could not be determined.
*/
public function getPackageForProject(string $project_name): ?string {
$installed = $this->getInstalledPackagesData();
// If we're lucky, the package name is the project name, prefixed with
// `drupal/`.
if (array_key_exists("drupal/$project_name", $installed)) {
return "drupal/$project_name";
}
$installed = array_keys($installed);
foreach ($installed as $package_name) {
if ($this->getProjectForPackage($package_name) === $project_name) {
return $package_name;
}
}
return NULL;
}
/**
* Scans a given path to determine the Drupal project name.
*
* The path will be scanned for `.info.yml` files containing a `project` key.
*
* @param string $path
* The path to scan.
*
* @return string|null
* The name of the project, as declared in the first found `.info.yml` which
* contains a `project` key, or NULL if none was found.
*/
private function scanForProjectName(string $path): ?string {
$iterator = new \RecursiveDirectoryIterator($path);
$iterator = new \RecursiveIteratorIterator($iterator);
$iterator = new \RegexIterator($iterator, '/.+\.info\.yml$/', \RecursiveRegexIterator::GET_MATCH);
foreach ($iterator as $match) {
$info = file_get_contents($match[0]);
$info = Yaml::decode($info);
if (is_string($info['project']) && !empty($info['project'])) {
return $info['project'];
}
}
return NULL;
}
}
{}
{}
{
"packages": [
{
"name": "drupal/package_project_match",
"version": "6.1.3",
"type": "drupal-module"
},
{
"name": "drupal/not_match_package",
"version": "6.1.3",
"type": "drupal-theme"
},
{
"name": "non_drupal/other_project",
"version": "6.1.3",
"type": "drupal-module"
},
{
"name": "drupal/nested_no_match_package",
"version": "6.1.3",
"type": "drupal-profile"
},
{
"name": "drupal/custom_module",
"version": "6.1.3",
"type": "drupal-custom-module"
}
]
}
<?php
/**
* @file
*/
$projects_dir = __DIR__ . '/../../web/projects';
return [
'versions' => [
'drupal/package_project_match' => [
'type' => 'drupal-module',
'install_path' => $projects_dir . '/package_project_match',
],
'drupal/not_match_package' => [
'type' => 'drupal-module',
'install_path' => $projects_dir . '/not_match_project',
],
'drupal/nested_no_match_package' => [
'type' => 'drupal-module',
'install_path' => $projects_dir . '/any_folder_name',
],
'non_drupal/other_project' => [
'type' => 'drupal-module',
'install_path' => $projects_dir . '/other_project',
],
'drupal/custom_module' => [
'type' => 'drupal-custom-module',
'install_path' => $projects_dir . '/custom_module',
],
],
];
# A test info.yml file where the folder names and info.yml file names do not match the project or package.
# Only the project key in this file need to match.
project: nested_no_match_project
......@@ -5,6 +5,9 @@ namespace Drupal\Tests\package_manager\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\package_manager\ComposerUtility;
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamDirectory;
use org\bovigo\vfs\vfsStreamFile;
use org\bovigo\vfs\visitor\vfsStreamAbstractVisitor;
/**
* @coversDefaultClass \Drupal\package_manager\ComposerUtility
......@@ -18,6 +21,45 @@ class ComposerUtilityTest extends KernelTestBase {
*/
protected static $modules = ['package_manager'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$fixture = vfsStream::newDirectory('fixture');
vfsStream::copyFromFileSystem(__DIR__ . '/../../fixtures/project_package_conversion', $fixture);
$this->vfsRoot->addChild($fixture);
// Strip the `.hide` suffix from all `.info.yml.hide` files. Drupal's coding
// standards don't allow info files to have the `project` key, but we need
// it to be present for testing.
vfsStream::inspect(new class () extends vfsStreamAbstractVisitor {
/**
* {@inheritdoc}
*/
public function visitFile(vfsStreamFile $file) {
$name = $file->getName();
if (str_ends_with($name, '.info.yml.hide')) {
$new_name = basename($name, '.hide');
$file->rename($new_name);
}
}
/**
* {@inheritdoc}
*/
public function visitDirectory(vfsStreamDirectory $dir) {
foreach ($dir->getChildren() as $child) {
$this->visit($child);
}
}
});
}
/**
* Tests that ComposerUtility disables automatic creation of .htaccess files.
*/
......@@ -91,4 +133,100 @@ class ComposerUtilityTest extends KernelTestBase {
$this->assertSame(['drupal/updated'], array_keys($updated));
}
/**
* @covers ::getProjectForPackage
*
* @param string $package
* The package name.
* @param string|null $expected_project
* The expected project if any, otherwise NULL.
*
* @dataProvider providerGetProjectForPackage
*/
public function testGetProjectForPackage(string $package, ?string $expected_project): void {
$dir = $this->vfsRoot->getChild('fixture')->url();
$this->assertSame($expected_project, ComposerUtility::createForDirectory($dir)->getProjectForPackage($package));
}
/**
* Data provider for ::testGetProjectForPackage().
*
* @return mixed[][]
* The test cases.
*/
public function providerGetProjectForPackage(): array {
return [
'package and project match' => [
'drupal/package_project_match',
'package_project_match',
],
'package and project do not match' => [
'drupal/not_match_package',
'not_match_project',
],
'vendor is not drupal' => [
'non_drupal/other_project',
NULL,
],
'missing package' => [
'drupal/missing',
NULL,
],
'nested_no_match' => [
'drupal/nested_no_match_package',
'nested_no_match_project',
],
'unsupported package type' => [
'drupal/custom_module',
NULL,
],
];
}
/**
* @covers ::getPackageForProject
*
* @param string $project
* The project name.
* @param string|null $expected_package
* The expected package if any, otherwise NULL.
*
* @dataProvider providerGetPackageForProject
*/
public function testGetPackageForProject(string $project, ?string $expected_package): void {
$dir = $this->vfsRoot->getChild('fixture')->url();
$this->assertSame($expected_package, ComposerUtility::createForDirectory($dir)->getPackageForProject($project));
}
/**
* Data provider for ::testGetPackageForProject().
*
* @return mixed[][]
* The test cases.
*/
public function providerGetPackageForProject(): array {
return [
'package and project match' => [
'package_project_match',
'drupal/package_project_match',
],
'package and project do not match' => [
'not_match_project',
'drupal/not_match_package',
],
'vendor is not drupal' => [
'other_project',
NULL,
],
'missing package' => [
'missing',
NULL,
],
'nested_no_match' => [
'nested_no_match_project',
'drupal/nested_no_match_package',
],
];
}
}
<?php
namespace Drupal\Tests\package_manager\Unit;
use Composer\Autoload\ClassLoader;
use Drupal\Tests\UnitTestCase;
/**
* Tests retrieval of package data from Composer's `installed.php`.
*
* ComposerUtility relies on the internal structure of `installed.php` for
* certain operations. This test is intended as an early warning if the file's
* internal structure changes in a way that would break our functionality.
*
* @group package_manager
*/
class InstalledPackagesDataTest extends UnitTestCase {
/**
* Tests that Composer's `installed.php` file looks how we expect.
*/
public function testinstalledPackagesData(): void {
$loaders = ClassLoader::getRegisteredLoaders();
$installed_php = key($loaders) . '/composer/installed.php';
$this->assertFileIsReadable($installed_php);
$data = include $installed_php;
// There should be a `versions` array whose keys are package names.
$this->assertIsArray($data['versions']);
$this->assertMatchesRegularExpression('|^[a-z0-9\-_]+/[a-z0-9\-_]+$|', key($data['versions']));
// The values of `versions` should be arrays of package information that
// includes a non-empty `install_path` string and a non-empty `type` string.
$package = reset($data['versions']);
$this->assertIsArray($package);
$this->assertNotEmpty($package['install_path']);
$this->assertIsString($package['install_path']);
$this->assertNotEmpty($package['type']);
$this->assertIsString($package['type']);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment