UNPKG

@wordpress/env

Version:

A zero-config, self contained local WordPress environment for development and testing.

371 lines (319 loc) 13.1 kB
'use strict'; /** * External dependencies */ const path = require( 'path' ); const { writeFile, mkdir } = require( 'fs' ).promises; const { existsSync } = require( 'fs' ); const yaml = require( 'js-yaml' ); /** * Internal dependencies */ const { ValidationError } = require( '../../config' ); const buildDockerComposeConfig = require( './build-docker-compose-config' ); /** * @typedef {import('../../config').WPConfig} WPConfig */ /** * Writes Docker configuration files (docker-compose.yml and Dockerfiles) for * a fully resolved config. Called only by the `start` command. * * The config must already have ports resolved, and `debug`, `xdebug`, and * `spx` properties set before calling this function. * * @param {WPConfig} config A fully resolved wp-env config object. * @return {WPConfig} The config object (unchanged). */ async function writeDockerFiles( config ) { const dockerComposeConfig = buildDockerComposeConfig( config ); await mkdir( config.workDirectoryPath, { recursive: true } ); await writeFile( config.dockerComposeConfigPath, yaml.dump( dockerComposeConfig ) ); // Write four Dockerfiles for each service we provided. // (WordPress and CLI services, then a development and test environment for each.) for ( const imageType of [ 'WordPress', 'CLI' ] ) { for ( const envType of [ 'development', 'tests' ] ) { await writeFile( path.resolve( config.workDirectoryPath, `${ envType === 'tests' ? 'Tests-' : '' }${ imageType }.Dockerfile` ), imageType === 'WordPress' ? wordpressDockerFileContents( envType, config ) : cliDockerFileContents( envType, config ) ); } } return config; } /** * Verifies that the Docker environment has been initialized (i.e. `wp-env * start` has been run at least once). Exits with an error message when the * work directory does not exist. * * @param {WPConfig} config The wp-env config object. * @param {Object} spinner A CLI spinner which indicates progress. */ function ensureDockerInitialized( config, spinner ) { if ( ! existsSync( config.workDirectoryPath ) ) { spinner.fail( 'wp-env has not yet been initialized. Please run `wp-env start` to install the WordPress instance before using any other commands. This is only necessary to set up the environment for the first time; it is typically not necessary for the instance to be running after that in order to use other commands.' ); process.exit( 1 ); } } /** * Generates the Dockerfile used by wp-env's `wordpress` and `tests-wordpress` instances. * * @param {string} env The environment we're installing -- development or tests. * @param {WPConfig} config The configuration object. * @return {string} The dockerfile contents. */ function wordpressDockerFileContents( env, config ) { const phpVersion = config.env[ env ].phpVersion ? ':php' + config.env[ env ].phpVersion : ''; return `FROM wordpress${ phpVersion } # Update apt sources for archived versions of Debian. # stretch (https://lists.debian.org/debian-devel-announce/2023/03/msg00006.html) RUN touch /etc/apt/sources.list RUN sed -i 's|deb.debian.org/debian stretch|archive.debian.org/debian stretch|g' /etc/apt/sources.list RUN sed -i 's|security.debian.org/debian-security stretch|archive.debian.org/debian-security stretch|g' /etc/apt/sources.list RUN sed -i '/stretch-updates/d' /etc/apt/sources.list # buster (https://lists.debian.org/debian-devel-announce/2025/06/msg00001.html) RUN sed -i 's|deb.debian.org/debian buster|archive.debian.org/debian buster|g' /etc/apt/sources.list RUN sed -i 's|security.debian.org/debian-security buster/updates|archive.debian.org/debian-security buster/updates|g' /etc/apt/sources.list RUN sed -i '/buster-updates/d' /etc/apt/sources.list # Create the host's user so that we can match ownership in the container. ARG HOST_USERNAME ARG HOST_UID ARG HOST_GID # When the IDs are already in use we can still safely move on. RUN groupadd -o -g $HOST_GID $HOST_USERNAME || true RUN useradd -mlo -u $HOST_UID -g $HOST_GID $HOST_USERNAME || true # Install any dependencies we need in the container. ${ installDependencies( 'wordpress', env, config ) } ${ getLoopbackPortConfig( config.env[ env ].port ) }`; } /** * Generates the Dockerfile used by wp-env's `cli` and `tests-cli` instances. * * @param {string} env The environment we're installing -- development or tests. * @param {WPConfig} config The configuration object. * @return {string} The dockerfile contents. */ function cliDockerFileContents( env, config ) { const phpVersion = config.env[ env ].phpVersion ? '-php' + config.env[ env ].phpVersion : ''; return `FROM wordpress:cli${ phpVersion } # Switch to root so we can create users. USER root # Create the host's user so that we can match ownership in the container. ARG HOST_USERNAME ARG HOST_UID ARG HOST_GID # When the IDs are already in use we can still safely move on. RUN addgroup -g $HOST_GID $HOST_USERNAME || true RUN adduser -h /home/$HOST_USERNAME -G $( getent group $HOST_GID | cut -d: -f1 ) -u $HOST_UID $HOST_USERNAME || true # Install any dependencies we need in the container. ${ installDependencies( 'cli', env, config ) } # Switch back to the original user now that we're done. USER www-data # Have the container sleep infinitely to keep it alive for us to run commands on it. CMD [ "/bin/sh", "-c", "while true; do sleep 2073600; done" ] `; } /** * Generates content for the Dockerfile to install dependencies. * * @param {string} service The kind of service that we're installing dependencies on ('wordpress' or 'cli'). * @param {string} env The environment we're installing dependencies for ('development' or 'tests'). * @param {WPConfig} config The configuration object. * @return {string} The Dockerfile content for installing dependencies. */ function installDependencies( service, env, config ) { let dockerFileContent = ''; // At times we may need to evaluate the environment. This is because the // WordPress image uses Ubuntu while the CLI image uses Alpine. // Start with some environment-specific dependency installations. switch ( service ) { case 'wordpress': { dockerFileContent += ` # Make sure we're working with the latest packages. RUN apt-get clean RUN apt-get -qy update # Install some basic PHP dependencies. RUN apt-get -qy install $PHPIZE_DEPS && touch /usr/local/etc/php/php.ini # Install git RUN apt-get -qy install git # Set up sudo so they can have root access. RUN apt-get -qy install sudo RUN echo "#$HOST_UID ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers`; break; } case 'cli': { dockerFileContent += ` # Make sure we're working with the latest packages. RUN apk update # Install some basic PHP dependencies. RUN apk --no-cache add $PHPIZE_DEPS && touch /usr/local/etc/php/php.ini # Set up sudo so they can have root access. RUN apk --no-cache add sudo linux-headers RUN echo "#$HOST_UID ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers`; break; } default: { throw new Error( `Invalid service "${ service }" given` ); } } dockerFileContent += getXdebugConfig( config.xdebug, config.env[ env ].phpVersion ); dockerFileContent += getSpxConfig( config.spx, config.env[ env ].phpVersion, service ); // Add better PHP settings. dockerFileContent += ` RUN echo 'upload_max_filesize = 1G' >> /usr/local/etc/php/php.ini RUN echo 'post_max_size = 1G' >> /usr/local/etc/php/php.ini`; // Make sure Composer is available for use in all services. dockerFileContent += ` RUN curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php RUN export COMPOSER_HASH=\`curl -sS https://composer.github.io/installer.sig\` && php -r "if (hash_file('SHA384', '/tmp/composer-setup.php') === '$COMPOSER_HASH') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('/tmp/composer-setup.php'); } echo PHP_EOL;" RUN php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer RUN rm /tmp/composer-setup.php`; // Install any Composer packages we might need globally. // Make sure to do this as the user and ensure the binaries are available in the $PATH. dockerFileContent += ` USER $HOST_UID:$HOST_GID ENV PATH="\${PATH}:/home/$HOST_USERNAME/.composer/vendor/bin" RUN composer global require --dev phpunit/phpunit:"^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0" USER root`; return dockerFileContent; } /** * Gets the Xdebug config based on the options in the config object. * * @param {string} xdebugMode The Xdebug mode set in the config. * @param {string} phpVersion The php version set in the environment. * @return {string} The Xdebug config -- can be an empty string when it's not used. */ function getXdebugConfig( xdebugMode = 'off', phpVersion ) { if ( xdebugMode === 'off' ) { return ''; } let xdebugVersion = 'xdebug'; if ( phpVersion ) { const versionTokens = phpVersion.split( '.' ); const majorVer = parseInt( versionTokens[ 0 ] ); const minorVer = parseInt( versionTokens[ 1 ] ); if ( isNaN( majorVer ) || isNaN( minorVer ) ) { throw new ValidationError( 'Something went wrong when parsing the PHP version.' ); } // Throw an error if someone tries to use Xdebug with an unsupported PHP version. // Xdebug 3 only supports 7.2 and higher. if ( majorVer < 7 || ( majorVer === 7 && minorVer < 2 ) ) { throw new ValidationError( `Cannot use XDebug 3 with PHP < 7.2. Your PHP version is ${ phpVersion }.` ); } // For now, we support PHP 7 by installing the final version of Xdebug to // support PHP 7 when the environment uses that version. By default, use the // latest version. if ( majorVer === 7 ) { xdebugVersion = 'xdebug-3.1.6'; } } return ` RUN if [ -z "$(pecl list | grep ${ xdebugVersion })" ] ; then pecl install ${ xdebugVersion } ; fi RUN docker-php-ext-enable xdebug RUN echo 'xdebug.start_with_request=yes' >> /usr/local/etc/php/php.ini RUN echo 'xdebug.mode=${ xdebugMode }' >> /usr/local/etc/php/php.ini RUN echo 'xdebug.client_host="host.docker.internal"' >> /usr/local/etc/php/php.ini`; } /** * Gets the SPX config based on the options in the config object. * * @param {string} spxMode The SPX mode set in the config. * @param {string} phpVersion The php version set in the environment. * @param {string} service The service name. * @return {string} The SPX config -- can be an empty string when it's not used. */ function getSpxConfig( spxMode = 'off', phpVersion, service ) { if ( spxMode === 'off' || service === 'cli' ) { return ''; } if ( phpVersion ) { const versionTokens = phpVersion.split( '.' ); const majorVer = parseInt( versionTokens[ 0 ] ); const minorVer = parseInt( versionTokens[ 1 ] ); if ( isNaN( majorVer ) || isNaN( minorVer ) ) { throw new ValidationError( 'Something went wrong when parsing the PHP version.' ); } // SPX requires PHP 5.4 or higher if ( majorVer < 5 || ( majorVer === 5 && minorVer < 4 ) ) { throw new ValidationError( `Cannot use SPX with PHP < 5.4. Your PHP version is ${ phpVersion }.` ); } } return ` # Install SPX profiler RUN apt-get update -qy RUN apt-get install -qy git zlib1g-dev RUN cd /tmp && git clone https://github.com/NoiseByNorthwest/php-spx.git RUN cd /tmp/php-spx && git checkout release/latest RUN cd /tmp/php-spx && phpize && ./configure && make && make install RUN docker-php-ext-enable spx RUN echo 'spx.http_enabled=1' >> /usr/local/etc/php/php.ini RUN echo 'spx.http_key="dev"' >> /usr/local/etc/php/php.ini RUN echo 'spx.http_ip_whitelist="*"' >> /usr/local/etc/php/php.ini RUN echo 'spx.data_dir="/tmp/spx"' >> /usr/local/etc/php/php.ini RUN mkdir -p /tmp/spx && chmod 777 /tmp/spx`; } /** * Generates Dockerfile RUN steps that make Apache also listen on the * host-mapped wp-env port, so PHP loopback requests inside the container * (WP-Cron, REST API loopback, Site Health) can reach WordPress at * WP_HOME = http://localhost:<port>. * * @see https://github.com/WordPress/gutenberg/issues/20569 * @see https://github.com/docker-library/wordpress/issues/611#issuecomment-1378316911 * * @param {number} port The host-side port wp-env exposes WordPress on. * @return {string} The Dockerfile fragment, or '' when no change is needed. */ function getLoopbackPortConfig( port ) { // Apache already listens on 80 by default and wp-env never // configures SSL, so 80 and 443 are no-ops. Mirrors the same // short-circuit in lib/config/add-or-replace-port.js. if ( port === 80 || port === 443 ) { return ''; } return ` # Make Apache listen on the wp-env-mapped port in addition to 80 so # PHP loopback requests (WP-Cron, REST API, Site Health) inside the # container reach WordPress at WP_HOME = http://localhost:${ port }. # See https://github.com/WordPress/gutenberg/issues/20569 RUN echo 'Listen ${ port }' >> /etc/apache2/ports.conf RUN sed -i 's|<VirtualHost \\*:80>|<VirtualHost *:80 *:${ port }>|' /etc/apache2/sites-enabled/000-default.conf`; } module.exports = { writeDockerFiles, ensureDockerInitialized, // Exported for testing. wordpressDockerFileContents, getLoopbackPortConfig, };