From 1d0c59e60f5f2b8870845290fb89d56efea0d0c3 Mon Sep 17 00:00:00 2001 From: Thomas Hauschild <7961978+Morgy93@users.noreply.github.com> Date: Fri, 12 Jun 2026 21:30:41 +0200 Subject: [PATCH] fix!: install module via Composer path repository instead of app/code bind-mount --- .ddev/commands/web/install-magento | 36 ++++++----- .ddev/commands/web/install-module-deps | 75 ---------------------- .ddev/config.yaml | 13 ++-- .ddev/docker-compose.mageforge-source.yaml | 12 +++- docs/development.md | 37 +++++++---- 5 files changed, 56 insertions(+), 117 deletions(-) delete mode 100755 .ddev/commands/web/install-module-deps diff --git a/.ddev/commands/web/install-magento b/.ddev/commands/web/install-magento index b3c69092..2ead7166 100755 --- a/.ddev/commands/web/install-magento +++ b/.ddev/commands/web/install-magento @@ -12,14 +12,13 @@ cd /var/www/html || exit 1 # global config MAGENTO_FOLDER="magento" -# Guard: verify the MageForge bind-mount is active. -# The Docker bind-mount (../src → .../MageForge) is a kernel-level mount established -# when the containers start. If magento/ was deleted while DDEV was running, the mount -# becomes orphaned: its inode is gone, and recreating the directory produces a new inode -# not covered by the old mount. The module source would then be invisible to Magento. -# The only fix is a container restart, which re-establishes the mount on the new inode. -if [[ ! -f "${MAGENTO_FOLDER}/app/code/OpenForgeProject/MageForge/registration.php" ]]; then - echo "ERROR: The MageForge bind-mount is not active." +# Guard: verify the module source mount is active. +# The mount (repo root → magento/mageforge-source, read-only) is established when the +# containers start. If magento/ was deleted while DDEV was running, the mount can become +# orphaned: its inode is gone, and a recreated directory is not covered by the old mount. +# A container restart re-establishes the mount on the new inode. +if [[ ! -f "${MAGENTO_FOLDER}/mageforge-source/composer.json" ]]; then + echo "ERROR: The MageForge source mount is not active." echo "" echo "This happens when magento/ was deleted while DDEV was still running." echo "" @@ -72,17 +71,22 @@ composer config repositories.hyva-themes/magento2-theme-fallback git https://git composer config repositories.hyva-themes/magento2-order-cancellation-webapi git https://github.com/hyva-themes/magento2-order-cancellation-webapi.git composer config repositories.hyva-themes/magento2-email-module git https://github.com/hyva-themes/magento2-email-module.git -# Remove *.sample extension -find . -name "*.sample" -type f -exec sh -c 'mv "$1" "${1%.sample}"' _ {} \; +# Remove *.sample extension (skip the read-only module source mount) +find . -path ./mageforge-source -prune -o -name "*.sample" -type f -exec sh -c 'mv "$1" "${1%.sample}"' _ {} \; rm -f package-lock.json # remove basic package-lock.json to avoid conflicts # create missing local-themes.js file for grunt tasks echo 'module.exports = {};' >dev/tools/grunt/configs/local-themes.js -# Require Hyvä theme and install MageForge module deps before setup:install so that -# both are included in the initial schema-upgrade pass (avoiding a redundant second pass). -composer require 'hyva-themes/magento2-default-theme' -/var/www/html/.ddev/commands/web/install-module-deps +# Register the module source mount as a Composer path repository. Composer symlinks +# vendor/openforgeproject/mageforge → ../../mageforge-source, which resolves within the +# Magento root (satisfying Magento's path validator), and resolves the module's +# third-party dependencies (e.g. laravel/prompts) like any regular package. +composer config repositories.mageforge '{"type": "path", "url": "mageforge-source", "options": {"symlink": true}}' + +# Require Hyvä theme and MageForge before setup:install so that both are included +# in the initial schema-upgrade pass (avoiding a redundant second pass). +composer require 'hyva-themes/magento2-default-theme' 'openforgeproject/mageforge:@dev' # install magento bin/magento setup:install \ @@ -108,9 +112,7 @@ bin/magento deploy:mode:set developer # disable 2FA bin/magento module:disable Magento_TwoFactorAuth Magento_AdminAdobeImsTwoFactorAuth -# Enable MageForge: the module is bind-mounted under app/code/ and therefore not registered -# via Composer autoload. bin/magento module:enable discovers it through the component registrar -# (registration.php), writes the entry to app/etc/config.php, and exits non-zero on any error. +# Enable MageForge (Composer-installed modules are disabled until enabled in app/etc/config.php) bin/magento module:enable OpenForgeProject_MageForge # install sample data diff --git a/.ddev/commands/web/install-module-deps b/.ddev/commands/web/install-module-deps deleted file mode 100755 index a180bd7f..00000000 --- a/.ddev/commands/web/install-module-deps +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash - -## Description: Install MageForge module dependencies into the Magento vendor directory -## Usage: install-module-deps -## Example: ddev install-module-deps - -# The module source is bind-mounted (not Composer-installed), so its third-party -# dependencies are not resolved automatically. Read them from the module's composer.json -# and install the non-Magento/non-PHP ones into the Magento vendor. - -MAGENTO_DIR="/var/www/html/magento" -MODULE_COMPOSER_JSON="/var/www/html/composer.json" - -# Skip if Magento is not yet installed (e.g. on initial ddev start before install-magento) -if [[ ! -f "${MAGENTO_DIR}/vendor/autoload.php" ]]; then - exit 0 -fi - -cd "${MAGENTO_DIR}" || exit 1 - -php -r " - \$manifest = json_decode(file_get_contents('${MODULE_COMPOSER_JSON}'), true); - if (\$manifest === null) { - fwrite(STDERR, 'ERROR: Could not parse module composer.json at ${MODULE_COMPOSER_JSON}' . PHP_EOL); - exit(1); - } - \$deps = []; - foreach (\$manifest['require'] ?? [] as \$package => \$constraint) { - // Skip php, magento/*, and Composer platform packages (ext-*, lib-*, composer-*-api). - // Platform packages have no vendor/ directory and would cause repeated failed installs. - if (\$package === 'php' - || str_starts_with(\$package, 'magento/') - || str_starts_with(\$package, 'ext-') - || str_starts_with(\$package, 'lib-') - || \$package === 'composer-runtime-api' - || \$package === 'composer-plugin-api') { - continue; - } - \$deps[\$package] = \$constraint; - } - \$missingConstraints = []; - \$missingNames = []; - // Read Magento's composer.json once to compare declared constraints. - \$magentoManifest = is_file('composer.json') - ? json_decode(file_get_contents('composer.json'), true) - : null; - foreach (\$deps as \$package => \$constraint) { - \$vendorDir = 'vendor/' . \$package; - \$needsInstall = !is_dir(\$vendorDir); - if (!\$needsInstall) { - // Check whether Magento's composer.json already declares the exact required - // constraint. This detects constraint bumps in the module (e.g. ^0.3 → ^0.4) - // and triggers a re-install when the constraint changes. - \$declaredConstraint = \$magentoManifest['require'][\$package] - ?? \$magentoManifest['require-dev'][\$package] - ?? null; - if (\$declaredConstraint !== \$constraint) { - \$needsInstall = true; - } - } - if (\$needsInstall) { - \$missingConstraints[] = escapeshellarg(\"\$package:\$constraint\"); - \$missingNames[] = escapeshellarg(\$package); - } - } - if (\$missingConstraints) { - echo 'Installing MageForge module dependencies: ' . implode(', ', \$missingNames) . PHP_EOL; - // Add constraints without resolving first, then update only the new packages. - // This prevents Composer from touching unrelated Magento core dependencies. - passthru('composer require --no-interaction --no-update ' . implode(' ', \$missingConstraints), \$exitCode); - if (\$exitCode !== 0) { exit(\$exitCode); } - passthru('composer update --no-interaction --with-dependencies ' . implode(' ', \$missingNames), \$exitCode); - exit(\$exitCode); - } -" diff --git a/.ddev/config.yaml b/.ddev/config.yaml index 5c0c22bc..a25e9f99 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -11,17 +11,12 @@ database: version: "10.6" hooks: pre-start: - # Pre-create the MageForge bind-mount target directory on the host. - # Docker creates missing mount-target paths as root; by creating them here - # (as the host user) they get the correct ownership for the DDEV web container. - # MageForge/ is the actual bind-mount target (../src → .../MageForge), so we - # must create it explicitly – creating only the parent is not enough. - - exec-host: mkdir -p magento/app/code/OpenForgeProject/MageForge + # Pre-create the mageforge-source mount target on the host. Docker creates + # missing mount-target paths as root; creating it here (as the host user) + # gives it the correct ownership for the DDEV web container. + - exec-host: mkdir -p magento/mageforge-source post-start: - exec-host: ddev npx skills experimental_install - # Install MageForge module dependencies (e.g. laravel/prompts) that are not - # resolved automatically because the module is bind-mounted, not Composer-installed. - - exec: /var/www/html/.ddev/commands/web/install-module-deps use_dns_when_possible: true composer_root: magento composer_version: "2" diff --git a/.ddev/docker-compose.mageforge-source.yaml b/.ddev/docker-compose.mageforge-source.yaml index f01ddc72..32f4b154 100644 --- a/.ddev/docker-compose.mageforge-source.yaml +++ b/.ddev/docker-compose.mageforge-source.yaml @@ -1,6 +1,12 @@ services: web: volumes: - # Bind-mount the module source directly into app/code/ so realpath() returns - # a path within the Magento root, bypassing Magento's filesystem path validator. - - ../src:/var/www/html/magento/app/code/OpenForgeProject/MageForge:cached + # Mount the module repository read-only inside the Magento root. The Composer + # path-repository symlink (vendor/openforgeproject/mageforge → ../../mageforge-source) + # then resolves to a path within the Magento root, satisfying Magento's path + # validator. Read-only also protects the module source from an accidental + # `rm -rf magento` inside the container: deletion stops at the mount. + - ..:/var/www/html/magento/mageforge-source:ro,cached + # The repository contains magento/ itself; shadow it with an anonymous volume + # so the mount does not recurse (mageforge-source/magento/mageforge-source/…). + - /var/www/html/magento/mageforge-source/magento diff --git a/docs/development.md b/docs/development.md index c6502ecb..e87ce289 100644 --- a/docs/development.md +++ b/docs/development.md @@ -43,7 +43,7 @@ Welcome to the MageForge development repository. This guide covers everything yo > > - All module development happens in `/src/` — this is where you write code. > - Testing happens in `/magento/` — a full Magento 2 installation wired up for local use. -> - `/src/` is **bind-mounted by Docker** into `/magento/app/code/OpenForgeProject/MageForge/`. Changes are visible instantly; no Composer step needed. The mount is established on `ddev start` / `ddev restart`. +> - The repository is **mounted read-only by Docker** into `/magento/mageforge-source/` and installed into Magento as a regular Composer package (path repository with symlink). Changes in `/src/` are visible instantly; no Composer step needed. The mount is established on `ddev start` / `ddev restart`. > - `/magento/` is never included in a release. It exists solely for local development. --- @@ -83,12 +83,12 @@ Basic familiarity with Magento 2 module development is assumed. This script will: - Install a fresh Magento 2 instance inside `/magento/` - - Install third-party module dependencies via `ddev install-module-deps` + - Install MageForge via Composer (path repository pointing at the mounted module source) - Install Magento sample data - Enable the MageForge module (`bin/magento module:enable`) - Set developer mode and disable 2FA - > **Note:** `ddev start` must run before this command to establish the Docker bind-mount. + > **Note:** `ddev start` must run before this command to establish the Docker mount. 4. **Verify the installation:** @@ -100,15 +100,26 @@ You now have a fully functional local development environment. --- -## How the Bind-Mount Works +## How the Module Is Installed -`/src/` is bind-mounted by Docker (`.ddev/docker-compose.mageforge-source.yaml`) into the Magento app/code directory: +The repository root is bind-mounted **read-only** by Docker (`.ddev/docker-compose.mageforge-source.yaml`) into the Magento root: ``` -../src → magento/app/code/OpenForgeProject/MageForge/ +.. → magento/mageforge-source/ (read-only) ``` -A bind-mount is used instead of a Composer symlink because Magento's path validator rejects paths that resolve outside the Magento root. +`ddev install-magento` registers this directory as a Composer **path repository** and installs the module with `composer require openforgeproject/mageforge:@dev`. Composer creates a symlink: + +``` +magento/vendor/openforgeproject/mageforge → ../../mageforge-source +``` + +Why this construction? + +- **Magento's path validator** rejects paths that resolve (via `realpath()`) outside the Magento root. A plain Composer symlink to the repository root would fail; the mount point lives *inside* the Magento root, so validation passes. +- **Read-only protects the source**: an accidental `rm -rf magento` inside the container stops at the mount instead of deleting the repository through it. +- **Real Composer install**: third-party dependencies of the module (e.g. `laravel/prompts`) are resolved by Composer like for any end-user installation — no extra sync scripts needed. +- An anonymous volume shadows `mageforge-source/magento/` so the mount does not recurse into itself. **The mount is (re-)established every time the containers start.** Always run `ddev start` or `ddev restart` when: @@ -116,10 +127,10 @@ A bind-mount is used instead of a Composer symlink because Magento's path valida - pulling config changes from the repository - `/magento/` was deleted while DDEV was running (see [Common Issues](#common-issues)) -**Third-party dependencies** (e.g. `laravel/prompts`) are not resolved automatically for bind-mounted modules. A `post-start` hook runs `ddev install-module-deps` on every start to keep them in sync. You can also trigger it manually: +**Changed module dependencies** (`composer.json` in the repository root) are picked up with: ```bash -ddev install-module-deps +ddev composer update openforgeproject/mageforge ``` --- @@ -233,25 +244,25 @@ ddev poweroff ddev start ``` -**MageForge bind-mount not active / `registration.php` not found:** +**MageForge source mount not active / `mageforge-source` empty:** This happens when `/magento/` was deleted while DDEV was still running. Docker holds the old inode; the new directory is not covered by the mount. ```bash -ddev restart # Re-establishes the bind-mount on the new inode +ddev restart # Re-establishes the mount on the new inode ddev install-magento # Re-installs Magento with the mount now active ``` **Module dependencies missing after updating `composer.json`:** ```bash -ddev install-module-deps # Installs / updates third-party deps declared in src/composer.json +ddev composer update openforgeproject/mageforge # Re-resolves the module's dependencies ``` **Need to reinstall Magento from scratch:** ```bash -ddev restart # Ensure the bind-mount is fresh first +ddev restart # Ensure the source mount is fresh first ddev install-magento # Handles cleanup automatically ```