Skip to content
Open
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
26 changes: 23 additions & 3 deletions .ddev/commands/web/install-magento
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand All @@ -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}/"

Expand Down Expand Up @@ -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

Comment on lines +106 to +110
# 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
89 changes: 89 additions & 0 deletions .ddev/commands/web/install-module-deps
Original file line number Diff line number Diff line change
@@ -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';
Comment on lines +44 to +56
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);
}
"
11 changes: 8 additions & 3 deletions .ddev/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading