Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ DevTools explicitly rejects `shared-namespace-root`, Composer `autoload.files`,

Root mappings must also resolve each namespace to exactly the directory used by Axolotl-PM, including case. For example, with `src-namespace-prefix: MyPlugin`, `MyPlugin\\Extra\\` must map to `src/Extra/`. Mapping it to `src/extra/` or a namespace outside that plugin prefix is rejected, even though the directory lies under `src/`.

The existing DevTools self-build explicitly packages its PHP parser source and license. DevTools rewrites that bundled source into its private `AxolotlBucketLabs\DevTools\Vendor\PhpParser` namespace at load time, isolating the builder from other plugins that use another PHP-Parser release. That narrow self-packaging path is retained. Arbitrary `include-paths` on a user plugin do not turn off Composer validation or imply that a copied vendor directory will autoload.
The existing DevTools self-build explicitly packages its PHP parser source and license. Composer preparation recognizes that self-build configuration and returns no imported virions; the workflow still installs the locked packages so `include-paths` can package PHP Parser. DevTools rewrites that bundled source into its private `AxolotlBucketLabs\DevTools\Vendor\PhpParser` namespace at load time, isolating the builder from other plugins that use another PHP-Parser release. That narrow self-packaging path is retained. Arbitrary `include-paths` on a user plugin do not turn off Composer validation or imply that a copied vendor directory will autoload.

## Provenance and output safety

Expand Down
9 changes: 2 additions & 7 deletions src/Build/BuildConfigResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,8 @@ public function resolve(string $projectRoot, string $pluginName) : BuildConfig
}
}

$selfPackaging = strtolower($pluginName) === 'devtools'
&& in_array('vendor/nikic/php-parser/lib', $includePaths, true)
&& in_array('vendor/nikic/php-parser/LICENSE', $includePaths, true);
if (!$selfPackaging) {
foreach ((new ComposerVirionPlan())->requirements($projectRoot) as $requirement) {
$this->addRequirement($requirement, $virions);
}
foreach ((new ComposerVirionPlan())->requirements($projectRoot) as $requirement) {
$this->addRequirement($requirement, $virions);
}

return new BuildConfig(array_values($virions), array_values($includePaths));
Expand Down
3 changes: 3 additions & 0 deletions src/Build/ComposerVirionPlan.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ public function packages(string $projectRoot) : array
if (!is_file($path)) {
return [];
}
if (DevToolsSelfPackaging::matchesProject($projectRoot)) {
return [];
}
$composer = $this->readJson($path);
$rootAutoload = $composer['autoload'] ?? [];
if (!is_array($rootAutoload) || array_diff(array_keys($rootAutoload), ['psr-4', 'psr-0']) !== []) {
Expand Down
49 changes: 49 additions & 0 deletions src/Build/DevToolsSelfPackaging.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace AxolotlBucketLabs\DevTools\Build;

use AxolotlBucketLabs\DevTools\Support\ArtifactFile;
use AxolotlBucketLabs\DevTools\Support\YamlReader;
use AxolotlBucketLabs\DevTools\Validation\PluginManifestReader;
use function in_array;
use function is_array;
use function is_file;
use function strtolower;
use const DIRECTORY_SEPARATOR;

final class DevToolsSelfPackaging
{
private const PLUGIN_NAME = 'devtools';
private const PHP_PARSER_SOURCE_PATH = 'vendor/nikic/php-parser/lib';
private const PHP_PARSER_LICENSE_PATH = 'vendor/nikic/php-parser/LICENSE';

public static function matches(string $pluginName, mixed $includePaths) : bool
{
return strtolower($pluginName) === self::PLUGIN_NAME
&& is_array($includePaths)
&& in_array(self::PHP_PARSER_SOURCE_PATH, $includePaths, true)
&& in_array(self::PHP_PARSER_LICENSE_PATH, $includePaths, true);
}

public static function matchesProject(string $projectRoot) : bool
{
$manifestPath = $projectRoot . DIRECTORY_SEPARATOR . ArtifactFile::PLUGIN_MANIFEST;
$configPath = $projectRoot . DIRECTORY_SEPARATOR . ArtifactFile::DEVTOOLS_CONFIG;
if (!is_file($manifestPath) || !is_file($configPath)) {
return false;
}

$yamlReader = new YamlReader();
$config = $yamlReader->read($configPath);
$includePaths = $config['include-paths'] ?? null;
if (!self::matches(self::PLUGIN_NAME, $includePaths)) {
return false;
}

$manifest = (new PluginManifestReader($yamlReader))->read($manifestPath);

return self::matches($manifest->description->getName(), $includePaths);
}
}
27 changes: 27 additions & 0 deletions tests/Build/ComposerVirionPreparerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,33 @@ public function test_simple_project_prepare_is_noop() : void
self::assertDirectoryDoesNotExist($destination);
}

public function test_devtools_self_packaging_prepare_keeps_its_locked_php_parser_include_paths() : void
{
$project = $this->copyFixture('BuildPlugin');
$manifest = file_get_contents($project . '/plugin.yml');
self::assertIsString($manifest);
file_put_contents($project . '/plugin.yml', str_replace('name: BuildFixture', 'name: DevTools', $manifest));
$this->json($project . '/composer.json', [
'name' => 'axolotlbucketlabs/devtools',
'require' => ['nikic/php-parser' => '^5.6'],
'autoload' => ['files' => ['src/Build/php-parser-bootstrap.php']],
]);
$this->json($project . '/composer.lock', ['packages' => []]);
file_put_contents($project . '/devtools.yml', "include-paths:\n - vendor/nikic/php-parser/lib\n - vendor/nikic/php-parser/LICENSE\n");
$this->filesystem->ensureDirectory($project . '/vendor/nikic/php-parser/lib/PhpParser');
file_put_contents($project . '/vendor/nikic/php-parser/lib/PhpParser/Parser.php', '<?php final class Parser {}');
file_put_contents($project . '/vendor/nikic/php-parser/LICENSE', 'Fixture license');

$destination = $this->temporaryDirectory . '/prepared';
self::assertSame([], (new ComposerVirionPreparer($this->filesystem))->prepare($project, $destination));
self::assertDirectoryDoesNotExist($destination);

$result = $this->builder()->build($project, $destination, $project . '/build');
$phar = new Phar($result->outputPath);
self::assertTrue(isset($phar['vendor/nikic/php-parser/lib/PhpParser/Parser.php']));
self::assertTrue(isset($phar['vendor/nikic/php-parser/LICENSE']));
}

public function test_root_mapping_under_src_must_match_axolotl_namespace_layout() : void
{
$project = $this->copyFixture('BuildPlugin');
Expand Down
9 changes: 6 additions & 3 deletions tests/Validation/RepositoryMetadataTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ public function test_cli_documentation_matches_relative_extract_paths() : void
self::assertStringNotContainsString('selects `plugins/` or `virions/` when the destination is relative to a project', $contents);
}

public function test_workflow_actions_use_stable_release_tags_and_checkouts_do_not_persist_tokens() : void
public function test_workflow_actions_use_pinned_refs_and_checkouts_do_not_persist_tokens() : void
{
$root = dirname(__DIR__, 2);
$paths = array_merge(
Expand All @@ -329,9 +329,12 @@ public function test_workflow_actions_use_stable_release_tags_and_checkouts_do_n
foreach ($paths as $path) {
$contents = file_get_contents($path);
self::assertIsString($contents);
$checkoutCount += preg_match_all('/uses:\s*actions\\/checkout@v[0-9]+\.[0-9]+\.[0-9]+/', $contents);
$checkoutCount += preg_match_all('/uses:\s*actions\\/checkout@(?:v[0-9]+\.[0-9]+\.[0-9]+|[a-f0-9]{40})(?:\s|$)/', $contents);
$credentialOptOutCount += preg_match_all('/persist-credentials:\s*false/', $contents);
self::assertDoesNotMatchRegularExpression('/uses:\s*actions\\/(?:checkout|cache|upload-artifact|download-artifact)@[a-f0-9]{40}/', $contents);
preg_match_all('/uses:\s*actions\\/(?:checkout|cache|upload-artifact|download-artifact)@([^\s#]+)/', $contents, $matches);
foreach ($matches[1] as $reference) {
self::assertMatchesRegularExpression('/^(?:v[0-9]+\.[0-9]+\.[0-9]+|[a-f0-9]{40})$/', $reference);
}
}

self::assertGreaterThan(0, $checkoutCount);
Expand Down