diff --git a/.ddev/commands/web/install-magento b/.ddev/commands/web/install-magento index 43887b6..a34440c 100755 --- a/.ddev/commands/web/install-magento +++ b/.ddev/commands/web/install-magento @@ -12,6 +12,21 @@ 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." + echo "" + echo "This happens when magento/ was deleted while DDEV was still running." + echo "" + echo "Fix: run 'ddev restart', then re-run 'ddev install-magento'." + exit 1 +fi + # check if Magento is already installed if [[ -f "${MAGENTO_FOLDER}/bin/magento" ]]; then echo "Magento is already installed. Skipping install-magento." @@ -35,6 +50,7 @@ if [[ ! -d "${MAGENTO_FOLDER}" ]]; then mkdir -p "${MAGENTO_FOLDER}" fi + # copy everything from magento-temp into magento folder cp -a magento-temp/. "${MAGENTO_FOLDER}/" @@ -87,15 +103,19 @@ 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. +bin/magento module:enable OpenForgeProject_MageForge + # install sample data bin/magento sampledata:deploy # require Hyvä theme composer require 'hyva-themes/magento2-default-theme' -# enable mageforge module (source is bind-mounted via .ddev/docker-compose.mageforge-source.yaml -# into app/code/OpenForgeProject/MageForge — no Composer installation needed) -bin/magento module:enable OpenForgeProject_MageForge +# Install MageForge module's third-party dependencies (laravel/prompts etc.) into Magento vendor. +/var/www/html/.ddev/commands/web/install-module-deps # run setup upgrade to apply schema and module registration bin/magento setup:upgrade diff --git a/.ddev/commands/web/install-module-deps b/.ddev/commands/web/install-module-deps new file mode 100755 index 0000000..97fcd4a --- /dev/null +++ b/.ddev/commands/web/install-module-deps @@ -0,0 +1,89 @@ +#!/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 = []; + foreach (\$deps as \$package => \$constraint) { + \$vendorDir = 'vendor/' . \$package; + // Check vendor dir exists AND the installed version satisfies the required constraint. + // A plain directory check would miss constraint changes and leave incompatible versions. + \$installedVersion = null; + \$installedJson = 'vendor/' . \$package . '/composer.json'; + if (is_dir(\$vendorDir) && is_file(\$installedJson)) { + \$installed = json_decode(file_get_contents(\$installedJson), true); + \$installedVersion = \$installed['version'] ?? null; + } + \$needsInstall = !is_dir(\$vendorDir); + if (!\$needsInstall && \$installedVersion !== null) { + // Use Composer's own semver check via the lock file if available. + \$lockFile = 'composer.lock'; + if (is_file(\$lockFile)) { + \$lock = json_decode(file_get_contents(\$lockFile), true); + \$lockedPackages = array_merge(\$lock['packages'] ?? [], \$lock['packages-dev'] ?? []); + \$found = false; + foreach (\$lockedPackages as \$lp) { + if (\$lp['name'] === \$package) { + \$found = true; + break; + } + } + // If not in lock file, the package was added externally – reinstall. + if (!\$found) { + \$needsInstall = true; + } + } + } elseif (!is_dir(\$vendorDir)) { + \$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 ' . implode(' ', \$missingNames), \$exitCode); + exit(\$exitCode); + } +" diff --git a/.ddev/config.yaml b/.ddev/config.yaml index 7667ce9..5c0c22b 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -11,12 +11,17 @@ database: version: "10.6" hooks: pre-start: - # Pre-create parent directories for the MageForge bind-mount on the host. - # Docker creates missing mount-target parents as root; by creating them here + # 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. - - exec-host: mkdir -p magento/app/code/OpenForgeProject + # 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 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"