Skip to content

Commit

Permalink
add AddLinesConfigurator + updating PackageJsonSynchronizer for symfo…
Browse files Browse the repository at this point in the history
…ny/asset-mapper

Co-authored-by: Ryan Weaver <[email protected]>
  • Loading branch information
kbond and weaverryan committed May 26, 2023
1 parent 51077ed commit 0b70eb3
Show file tree
Hide file tree
Showing 10 changed files with 1,140 additions and 45 deletions.
29 changes: 25 additions & 4 deletions src/Configurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class Configurator
private $io;
private $options;
private $configurators;
private $postInstallConfigurators;
private $cache;

public function __construct(Composer $composer, IOInterface $io, Options $options)
Expand All @@ -45,6 +46,9 @@ public function __construct(Composer $composer, IOInterface $io, Options $option
'dockerfile' => Configurator\DockerfileConfigurator::class,
'docker-compose' => Configurator\DockerComposeConfigurator::class,
];
$this->postInstallConfigurators = [
'add-lines' => Configurator\AddLinesConfigurator::class,
];
}

public function install(Recipe $recipe, Lock $lock, array $options = [])
Expand All @@ -57,11 +61,25 @@ public function install(Recipe $recipe, Lock $lock, array $options = [])
}
}

/**
* Run after all recipes have been installed to run post-install configurators.
*/
public function postInstall(Recipe $recipe, Lock $lock, array $options = [])
{
$manifest = $recipe->getManifest();
foreach (array_keys($this->postInstallConfigurators) as $key) {
if (isset($manifest[$key])) {
$this->get($key)->configure($recipe, $manifest[$key], $lock, $options);
}
}
}

public function populateUpdate(RecipeUpdate $recipeUpdate): void
{
$originalManifest = $recipeUpdate->getOriginalRecipe()->getManifest();
$newManifest = $recipeUpdate->getNewRecipe()->getManifest();
foreach (array_keys($this->configurators) as $key) {
$allConfigurators = array_merge($this->configurators, $this->postInstallConfigurators);
foreach (array_keys($allConfigurators) as $key) {
if (!isset($originalManifest[$key]) && !isset($newManifest[$key])) {
continue;
}
Expand All @@ -73,7 +91,10 @@ public function populateUpdate(RecipeUpdate $recipeUpdate): void
public function unconfigure(Recipe $recipe, Lock $lock)
{
$manifest = $recipe->getManifest();
foreach (array_keys($this->configurators) as $key) {

$allConfigurators = array_merge($this->configurators, $this->postInstallConfigurators);

foreach (array_keys($allConfigurators) as $key) {
if (isset($manifest[$key])) {
$this->get($key)->unconfigure($recipe, $manifest[$key], $lock);
}
Expand All @@ -82,15 +103,15 @@ public function unconfigure(Recipe $recipe, Lock $lock)

private function get($key): AbstractConfigurator
{
if (!isset($this->configurators[$key])) {
if (!isset($this->configurators[$key]) && !isset($this->postInstallConfigurators[$key])) {
throw new \InvalidArgumentException(sprintf('Unknown configurator "%s".', $key));
}

if (isset($this->cache[$key])) {
return $this->cache[$key];
}

$class = $this->configurators[$key];
$class = isset($this->configurators[$key]) ? $this->configurators[$key] : $this->postInstallConfigurators[$key];

return $this->cache[$key] = new $class($this->composer, $this->io, $this->options);
}
Expand Down
4 changes: 2 additions & 2 deletions src/Configurator/AbstractConfigurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ abstract public function unconfigure(Recipe $recipe, $config, Lock $lock);

abstract public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void;

protected function write($messages)
protected function write($messages, $verbosity = IOInterface::VERBOSE)
{
if (!\is_array($messages)) {
$messages = [$messages];
}
foreach ($messages as $i => $message) {
$messages[$i] = ' '.$message;
}
$this->io->writeError($messages, true, IOInterface::VERBOSE);
$this->io->writeError($messages, true, $verbosity);
}

protected function isFileMarked(Recipe $recipe, string $file): bool
Expand Down
230 changes: 230 additions & 0 deletions src/Configurator/AddLinesConfigurator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
<?php

namespace Symfony\Flex\Configurator;

use Composer\IO\IOInterface;
use Symfony\Flex\Lock;
use Symfony\Flex\Recipe;
use Symfony\Flex\Update\RecipeUpdate;

/**
* @author Kevin Bond <[email protected]>
* @author Ryan Weaver <[email protected]>
*/
class AddLinesConfigurator extends AbstractConfigurator
{
private const POSITION_TOP = 'top';
private const POSITION_BOTTOM = 'bottom';
private const POSITION_AFTER_TARGET = 'after_target';

private const VALID_POSITIONS = [
self::POSITION_TOP,
self::POSITION_BOTTOM,
self::POSITION_AFTER_TARGET,
];

public function configure(Recipe $recipe, $config, Lock $lock, array $options = []): void
{
foreach ($config as $patch) {
if (!isset($patch['file'])) {
$this->write(sprintf('The "file" key is required for the "add-lines" configurator for recipe "%s". Skipping', $recipe->getName()));

continue;
}

if (isset($patch['requires']) && !$this->isPackageInstalled($patch['requires'])) {
continue;
}

if (!isset($patch['content'])) {
$this->write(sprintf('The "content" key is required for the "add-lines" configurator for recipe "%s". Skipping', $recipe->getName()));

continue;
}
$content = $patch['content'];

$file = $this->path->concatenate([$this->options->get('root-dir'), $patch['file']]);
$warnIfMissing = isset($patch['warn_if_missing']) && $patch['warn_if_missing'];
if (!is_file($file)) {
$this->write([
sprintf('Could not add lines to file <info>%s</info> as it does not exist. Missing lines:', $patch['file']),
'<comment>"""</comment>',
$content,
'<comment>"""</comment>',
'',
], $warnIfMissing ? IOInterface::NORMAL : IOInterface::VERBOSE);

continue;
}

$this->write(sprintf('Patching file "%s"', $patch['file']));

if (!isset($patch['position'])) {
$this->write(sprintf('The "position" key is required for the "add-lines" configurator for recipe "%s". Skipping', $recipe->getName()));

continue;
}
$position = $patch['position'];
if (!\in_array($position, self::VALID_POSITIONS, true)) {
$this->write(sprintf('The "position" key must be one of "%s" for the "add-lines" configurator for recipe "%s". Skipping', implode('", "', self::VALID_POSITIONS), $recipe->getName()));

continue;
}

if (self::POSITION_AFTER_TARGET === $position && !isset($patch['target'])) {
$this->write(sprintf('The "target" key is required when "position" is "%s" for the "add-lines" configurator for recipe "%s". Skipping', self::POSITION_AFTER_TARGET, $recipe->getName()));

continue;
}
$target = isset($patch['target']) ? $patch['target'] : null;

$this->patchFile($file, $content, $position, $target, $warnIfMissing);
}
}

public function unconfigure(Recipe $recipe, $config, Lock $lock): void
{
foreach ($config as $patch) {
if (!isset($patch['file'])) {
$this->write(sprintf('The "file" key is required for the "add-lines" configurator for recipe "%s". Skipping', $recipe->getName()));

continue;
}

// Ignore "requires": the target packages may have just become uninstalled.
// Checking for a "content" match is enough.

$file = $this->path->concatenate([$this->options->get('root-dir'), $patch['file']]);
if (!is_file($file)) {
continue;
}

if (!isset($patch['content'])) {
$this->write(sprintf('The "content" key is required for the "add-lines" configurator for recipe "%s". Skipping', $recipe->getName()));

continue;
}
$value = $patch['content'];

$this->unPatchFile($file, $value);
}
}

public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void
{
$originalConfig = array_filter($originalConfig, function ($item) {
return !isset($item['requires']) || $this->isPackageInstalled($item['requires']);
});
$newConfig = array_filter($newConfig, function ($item) {
return !isset($item['requires']) || $this->isPackageInstalled($item['requires']);
});

$filterDuplicates = function (array $sourceConfig, array $comparisonConfig) {
$filtered = [];
foreach ($sourceConfig as $sourceItem) {
$found = false;
foreach ($comparisonConfig as $comparisonItem) {
if ($sourceItem['file'] === $comparisonItem['file'] && $sourceItem['content'] === $comparisonItem['content']) {
$found = true;
break;
}
}
if (!$found) {
$filtered[] = $sourceItem;
}
}

return $filtered;
};

// remove any config where the file+value is the same before & after
$filteredOriginalConfig = $filterDuplicates($originalConfig, $newConfig);
$filteredNewConfig = $filterDuplicates($newConfig, $originalConfig);

$this->unconfigure($recipeUpdate->getOriginalRecipe(), $filteredOriginalConfig, $recipeUpdate->getLock());
$this->configure($recipeUpdate->getNewRecipe(), $filteredNewConfig, $recipeUpdate->getLock());
}

private function patchFile(string $file, string $value, string $position, ?string $target, bool $warnIfMissing)
{
$fileContents = file_get_contents($file);

if (false !== strpos($fileContents, $value)) {
return; // already includes value, skip
}

switch ($position) {
case self::POSITION_BOTTOM:
$fileContents .= "\n".$value;

break;
case self::POSITION_TOP:
$fileContents = $value."\n".$fileContents;

break;
case self::POSITION_AFTER_TARGET:
$lines = explode("\n", $fileContents);
$targetFound = false;
foreach ($lines as $key => $line) {
if (false !== strpos($line, $target)) {
array_splice($lines, $key + 1, 0, $value);
$targetFound = true;

break;
}
}
$fileContents = implode("\n", $lines);

if (!$targetFound) {
$this->write([
sprintf('Could not add lines after "%s" as no such string was found in "%s". Missing lines:', $target, $file),
'<comment>"""</comment>',
$value,
'<comment>"""</comment>',
'',
], $warnIfMissing ? IOInterface::NORMAL : IOInterface::VERBOSE);
}

break;
}

file_put_contents($file, $fileContents);
}

private function unPatchFile(string $file, $value)
{
$fileContents = file_get_contents($file);

if (false === strpos($fileContents, $value)) {
return; // value already gone!
}

if (false !== strpos($fileContents, "\n".$value)) {
$value = "\n".$value;
} elseif (false !== strpos($fileContents, $value."\n")) {
$value = $value."\n";
}

$position = strpos($fileContents, $value);
$fileContents = substr_replace($fileContents, '', $position, \strlen($value));

file_put_contents($file, $fileContents);
}

private function isPackageInstalled($packages): bool
{
if (\is_string($packages)) {
$packages = [$packages];
}

$installedRepo = $this->composer->getRepositoryManager()->getLocalRepository();

foreach ($packages as $packageName) {
if (null === $installedRepo->findPackage($packageName, '*')) {
return false;
}
}

return true;
}
}
21 changes: 12 additions & 9 deletions src/Flex.php
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ public function install(Event $event)
$installContribs = $this->composer->getPackage()->getExtra()['symfony']['allow-contrib'] ?? false;
$manifest = null;
$originalComposerJsonHash = $this->getComposerJsonHash();
$postInstallRecipes = [];
foreach ($recipes as $recipe) {
if ('install' === $recipe->getJob() && !$installContribs && $recipe->isContrib()) {
$warning = $this->io->isInteractive() ? 'WARNING' : 'IGNORING';
Expand Down Expand Up @@ -519,6 +520,7 @@ function ($value) {

switch ($recipe->getJob()) {
case 'install':
$postInstallRecipes[] = $recipe;
$this->io->writeError(sprintf(' - Configuring %s', $this->formatOrigin($recipe)));
$this->configurator->install($recipe, $this->lock, [
'force' => $event instanceof UpdateEvent && $event->force(),
Expand All @@ -542,6 +544,12 @@ function ($value) {
}
}

foreach ($postInstallRecipes as $recipe) {
$this->configurator->postInstall($recipe, $this->lock, [
'force' => $event instanceof UpdateEvent && $event->force(),
]);
}

if (null !== $manifest) {
array_unshift(
$this->postInstallOutput,
Expand Down Expand Up @@ -572,17 +580,12 @@ private function synchronizePackageJson(string $rootDir)
$rootDir = realpath($rootDir);
$vendorDir = trim((new Filesystem())->makePathRelative($this->config->get('vendor-dir'), $rootDir), '/');

$synchronizer = new PackageJsonSynchronizer($rootDir, $vendorDir);
$executor = new ScriptExecutor($this->composer, $this->io, $this->options);
$synchronizer = new PackageJsonSynchronizer($rootDir, $vendorDir, $executor);

if ($synchronizer->shouldSynchronize()) {
$lockData = $this->composer->getLocker()->getLockData();

if (method_exists($synchronizer, 'addPackageJsonLink') && 'string' === (new \ReflectionParameter([$synchronizer, 'addPackageJsonLink'], 'phpPackage'))->getType()->getName()) {
// support for smooth upgrades from older flex versions
$lockData['packages'] = array_column($lockData['packages'] ?? [], 'name');
$lockData['packages-dev'] = array_column($lockData['packages-dev'] ?? [], 'name');
}

if ($synchronizer->synchronize(array_merge($lockData['packages'] ?? [], $lockData['packages-dev'] ?? []))) {
$this->io->writeError('<info>Synchronizing package.json with PHP packages</>');
$this->io->writeError('<warning>Don\'t forget to run npm install --force or yarn install --force to refresh your JavaScript dependencies!</>');
Expand Down Expand Up @@ -773,7 +776,7 @@ public function fetchRecipes(array $operations, bool $reset): array
$job = method_exists($operation, 'getOperationType') ? $operation->getOperationType() : $operation->getJobType();

if (!isset($manifests[$name]) && isset($data['conflicts'][$name])) {
$this->io->writeError(sprintf(' - Skipping recipe for %s: all versions of the recipe conflict with your package versions.', $name), true, IOInterface::VERBOSE);
$this->io->writeError(sprintf(' - Skipping recipe for %s: all versions of the recipe conflict with your package versions.', $name));
continue;
}

Expand All @@ -784,7 +787,7 @@ public function fetchRecipes(array $operations, bool $reset): array

if (!isset($newManifests[$name])) {
// no older recipe found
$this->io->writeError(sprintf(' - Skipping recipe for %s: all versions of the recipe conflict with your package versions.', $name), true, IOInterface::VERBOSE);
$this->io->writeError(sprintf(' - Skipping recipe for %s: all versions of the recipe conflict with your package versions.', $name));

continue 2;
}
Expand Down
Loading

0 comments on commit 0b70eb3

Please sign in to comment.