From c086051a5bfd6586a14fcc62e520e35f1f502200 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Fri, 31 Jan 2025 11:26:59 -0500
Subject: [PATCH 1/5] Add an event subscriber for config export

---
 .../package_manager.services.yml              |  1 +
 .../src/EventSubscriber/ConfigSubscriber.php  | 47 +++++++++++++++++++
 2 files changed, 48 insertions(+)
 create mode 100644 core/modules/package_manager/src/EventSubscriber/ConfigSubscriber.php

diff --git a/core/modules/package_manager/package_manager.services.yml b/core/modules/package_manager/package_manager.services.yml
index 88a837631012..2d1cc482ad89 100644
--- a/core/modules/package_manager/package_manager.services.yml
+++ b/core/modules/package_manager/package_manager.services.yml
@@ -43,6 +43,7 @@ services:
     arguments:
       $appRoot: '%app.root%'
   Drupal\package_manager\FailureMarker: {}
+  Drupal\package_manager\EventSubscriber\ConfigSubscriber: {}
   Drupal\package_manager\EventSubscriber\UpdateDataSubscriber: {}
   Drupal\package_manager\EventSubscriber\ChangeLogger:
     calls:
diff --git a/core/modules/package_manager/src/EventSubscriber/ConfigSubscriber.php b/core/modules/package_manager/src/EventSubscriber/ConfigSubscriber.php
new file mode 100644
index 000000000000..e4392cef331b
--- /dev/null
+++ b/core/modules/package_manager/src/EventSubscriber/ConfigSubscriber.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\package_manager\EventSubscriber;
+
+use Drupal\Core\Config\ConfigEvents;
+use Drupal\Core\Config\StorageTransformEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Event subscriber to remove executable paths from exported config.
+ *
+ * @internal
+ *   This is an internal part of Package Manager and may be changed or removed
+ *   at any time without warning. External code should not interact with this
+ *   class.
+ */
+final class ConfigSubscriber implements EventSubscriberInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents(): array {
+    return [
+      ConfigEvents::STORAGE_TRANSFORM_EXPORT => 'onExport',
+    ];
+  }
+
+  /**
+   * Removes executable paths from exported config.
+   *
+   * @param \Drupal\Core\Config\StorageTransformEvent $event
+   *   The event object.
+   */
+  public function onExport(StorageTransformEvent $event): void {
+    $storage = $event->getStorage();
+
+    $settings = $storage->read('package_manager.settings');
+    $storage['executables'] = [
+      'composer' => NULL,
+      'rsync' => NULL,
+    ];
+    $storage->write('package_manager.settings', $settings);
+  }
+
+}
-- 
GitLab


From cf60f88f73ad883de1e939045038e21e52c764f1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Fri, 31 Jan 2025 11:32:55 -0500
Subject: [PATCH 2/5] Add install hook

---
 .../package_manager/package_manager.install   | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/core/modules/package_manager/package_manager.install b/core/modules/package_manager/package_manager.install
index 22d42a346ead..840eaf5b0c2d 100644
--- a/core/modules/package_manager/package_manager.install
+++ b/core/modules/package_manager/package_manager.install
@@ -12,8 +12,27 @@
 use Drupal\package_manager\Exception\StageFailureMarkerException;
 use Drupal\package_manager\FailureMarker;
 use PhpTuf\ComposerStager\API\Exception\ExceptionInterface;
+use PhpTuf\ComposerStager\API\Exception\LogicException;
 use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface;
 
+/**
+ * Implements hook_install().
+ */
+function package_manager_install(): void {
+  $executable_finder = \Drupal::service(ExecutableFinderInterface::class);
+
+  $config = \Drupal::configFactory()->getEditable('package_manager.settings');
+  foreach (['composer', 'rsync'] as $executable_name) {
+    try {
+      $config->set("executables.$executable_name", $executable_finder->find($executable_name));
+    }
+    catch (LogicException) {
+      // Couldn't find the executable; don't change the config.
+    }
+  }
+  $config->save();
+}
+
 /**
  * Implements hook_requirements().
  */
-- 
GitLab


From df0b089b8b9e154bd05f038d2f9d1378d48385cf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Fri, 31 Jan 2025 11:51:46 -0500
Subject: [PATCH 3/5] Fix DefaultConfigTest

---
 core/tests/Drupal/KernelTests/Config/DefaultConfigTest.php | 1 +
 1 file changed, 1 insertion(+)

diff --git a/core/tests/Drupal/KernelTests/Config/DefaultConfigTest.php b/core/tests/Drupal/KernelTests/Config/DefaultConfigTest.php
index fcdf72d1b8c3..13946e62a624 100644
--- a/core/tests/Drupal/KernelTests/Config/DefaultConfigTest.php
+++ b/core/tests/Drupal/KernelTests/Config/DefaultConfigTest.php
@@ -43,6 +43,7 @@ class DefaultConfigTest extends KernelTestBase {
    */
   public static $skippedConfig = [
     'locale.settings' => ['path: '],
+    'package_manager.settings' => ['composer: ', 'rsync: '],
     'syslog.settings' => ['facility: '],
   ];
 
-- 
GitLab


From bd04ab4a1bb6e9ddb4b30d8d51ae232c24820e12 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Fri, 31 Jan 2025 12:03:44 -0500
Subject: [PATCH 4/5] Add a settings form

---
 .../package_manager.links.menu.yml            |  5 ++
 .../package_manager.routing.yml               |  6 ++
 .../package_manager/src/Form/SettingsForm.php | 64 +++++++++++++++++++
 3 files changed, 75 insertions(+)
 create mode 100644 core/modules/package_manager/package_manager.links.menu.yml
 create mode 100644 core/modules/package_manager/package_manager.routing.yml
 create mode 100644 core/modules/package_manager/src/Form/SettingsForm.php

diff --git a/core/modules/package_manager/package_manager.links.menu.yml b/core/modules/package_manager/package_manager.links.menu.yml
new file mode 100644
index 000000000000..65fe5e04146e
--- /dev/null
+++ b/core/modules/package_manager/package_manager.links.menu.yml
@@ -0,0 +1,5 @@
+package_manager.settings:
+  title: 'Package Manager'
+  parent: system.admin_config_development
+  description: 'Configure Package Manager settings.'
+  route_name: package_manager.settings
diff --git a/core/modules/package_manager/package_manager.routing.yml b/core/modules/package_manager/package_manager.routing.yml
new file mode 100644
index 000000000000..e0e347c23b35
--- /dev/null
+++ b/core/modules/package_manager/package_manager.routing.yml
@@ -0,0 +1,6 @@
+package_manager.settings:
+  path: '/admin/config/development/package-manager'
+  defaults:
+    _form: 'Drupal\package_manager\Form\SettingsForm'
+  requirements:
+    _permission: 'administer software updates'
diff --git a/core/modules/package_manager/src/Form/SettingsForm.php b/core/modules/package_manager/src/Form/SettingsForm.php
new file mode 100644
index 000000000000..0b690c70cd95
--- /dev/null
+++ b/core/modules/package_manager/src/Form/SettingsForm.php
@@ -0,0 +1,64 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\package_manager\Form;
+
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\ConfigTarget;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Defines a form to configure Package Manager settings.
+ *
+ * @internal
+ *   This is an internal part of Package Manager and may be changed or removed
+ *   at any time without warning. External code should not interact with this
+ *   class.
+ */
+final class SettingsForm extends ConfigFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId(): string {
+    return 'package_manager_settings_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames(): array {
+    return ['package_manager.settings'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state): array {
+    $value_or_null = fn (string $value): ?string => trim($value) ?: NULL;
+
+    $form['executables']['composer'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Path to Composer'),
+      '#description' => $this->t('The full path to the Composer executable (usually named <code>composer</code> or <code>composer.phar</code>. Leave blank to auto-detect.'),
+      '#config_target' => new ConfigTarget(
+        'package_manager.settings',
+        'executables.composer',
+        toConfig: $value_or_null,
+      ),
+    ];
+    $form['executables']['rsync'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Path to rsync'),
+      '#description' => $this->t('The full path to the <code>rsync</code> executable. Leave blank to auto-detect.'),
+      '#config_target' => new ConfigTarget(
+        'package_manager.settings',
+        'executables.rsync',
+        toConfig: $value_or_null,
+      ),
+    ];
+    return parent::buildForm($form, $form_state);
+  }
+
+}
-- 
GitLab


From 9da807205e14a4d29a618ac2164d8e0dea4a0875 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Fri, 31 Jan 2025 13:57:46 -0500
Subject: [PATCH 5/5] Add test of install hook and config export

---
 .../src/EventSubscriber/ConfigSubscriber.php  |  2 +-
 .../tests/src/Kernel/ConfigTest.php           | 59 +++++++++++++++++++
 2 files changed, 60 insertions(+), 1 deletion(-)
 create mode 100644 core/modules/package_manager/tests/src/Kernel/ConfigTest.php

diff --git a/core/modules/package_manager/src/EventSubscriber/ConfigSubscriber.php b/core/modules/package_manager/src/EventSubscriber/ConfigSubscriber.php
index e4392cef331b..0155ded18883 100644
--- a/core/modules/package_manager/src/EventSubscriber/ConfigSubscriber.php
+++ b/core/modules/package_manager/src/EventSubscriber/ConfigSubscriber.php
@@ -37,7 +37,7 @@ public function onExport(StorageTransformEvent $event): void {
     $storage = $event->getStorage();
 
     $settings = $storage->read('package_manager.settings');
-    $storage['executables'] = [
+    $settings['executables'] = [
       'composer' => NULL,
       'rsync' => NULL,
     ];
diff --git a/core/modules/package_manager/tests/src/Kernel/ConfigTest.php b/core/modules/package_manager/tests/src/Kernel/ConfigTest.php
new file mode 100644
index 000000000000..6e7b97c2bfad
--- /dev/null
+++ b/core/modules/package_manager/tests/src/Kernel/ConfigTest.php
@@ -0,0 +1,59 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\package_manager\Kernel;
+
+use Drupal\Core\Config\StorageManagerInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface;
+
+/**
+ * @group package_manager
+ * @internal
+ */
+class ConfigTest extends PackageManagerKernelTestBase {
+
+  /**
+   * Tests that Package Manager auto-detects executable paths on install.
+   */
+  public function testExecutablePathsAutoDetection(): void {
+    $this->container->set(ExecutableFinderInterface::class, new class () implements ExecutableFinderInterface {
+
+      /**
+       * {@inheritdoc}
+       */
+      public function find(string $name): string {
+        return match ($name) {
+          'composer' => '/fake/path/to/composer',
+          'rsync' => '/fake/path/to/rsync',
+        };
+      }
+
+    });
+
+    $executables = $this->config('package_manager.settings')
+      ->get('executables');
+    $this->assertNull($executables['composer']);
+    $this->assertNull($executables['rsync']);
+
+    $this->container->get(ModuleHandlerInterface::class)
+      ->loadInclude('package_manager', 'install');
+    package_manager_install();
+
+    $executables = $this->config('package_manager.settings')
+      ->get('executables');
+    $this->assertSame('/fake/path/to/composer', $executables['composer']);
+    $this->assertSame('/fake/path/to/rsync', $executables['rsync']);
+
+    // The executable paths should be removed from exported config.
+    /** @var \Drupal\Core\Config\StorageInterface $export */
+    $export = $this->container->get(StorageManagerInterface::class)
+      ->getStorage();
+    $exported_settings = $export->read('package_manager.settings');
+    $this->assertIsArray($exported_settings);
+    $this->assertNull($exported_settings['executables']['composer']);
+    $this->assertNull($exported_settings['executables']['rsync']);
+  }
+
+}
-- 
GitLab