Skip to content

Commit

Permalink
wp-env: Xdebug support (#27346)
Browse files Browse the repository at this point in the history
* Install Xdebug 3 into the wp-env development service.
* Add flag to wp-env start for setting Xdebug mode.
* Add VS Code debugger configuration for Xdebug.
* Only write to internal wp-env docker files on wp-env start command.
* Fail if wp-env command is run without docker files set up.
* Update readme with all of the recent wp-env changes.
  • Loading branch information
noahtallen authored Dec 9, 2020
1 parent 7ae456c commit 454c708
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 15 deletions.
14 changes: 14 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"port": 9003,
"pathMappings": {
"/var/www/html/wp-content/plugins/gutenberg": "${workspaceRoot}/"
}
}
]
}
17 changes: 16 additions & 1 deletion packages/env/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,24 @@

## Unreleased

### Breaking Changes

- `wp-env start` is now the only command which writes to the docker configuration files. Previously, running any command would also parse the config and then write it to the correct location. Now, other commands still parse the config, but they will not overwrite the confugiration which was set by wp-env start. This allows parameters to be passed to wp-env start which can affect the configuration.

### Enhancement

- Update nodegit dependency to 0.27.0, the earlier version does not have pre-built binaries for Node 14.15.0 LTS. Upgrading provides support without requiring building nodegit locally.
- Update nodegit dependency to 0.27.0, the earlier version does not have pre-built binaries for Node 14.15.0 LTS. Upgrading provides support without requiring building nodegit locally.
- Allow WP_HOME wp-config value to be set to a custom port other than the default for the docker instance.
- Append the instance URL to output of `wp-env start`.

### New feature

- Add support for setting the PHP version used for the WordPress instance. For example, test PHP 8 with `"phpVersion": 8.0` in wp-env.json.
- Add Xdebug 3 to the development environment. You can enable Xdebug with `wp-env start --xdebug` (for debug mode) or `wp-env start --xdebug=develop,coverage` for custom modes.

### Bug Fixes

- ZIP-based plugin sources are now downloaded to a directory using the basename of the URL instead of the full URL path. This prevents HTML encoded characters in the URL (like "/") from being improperly encoded into the filesystem. This fixes the issue where many .zip sources broke because files with these badly formatted characters were not loaded as assets.

## 2.0.0 (2020-09-03)

Expand Down
57 changes: 55 additions & 2 deletions packages/env/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,51 @@ wp-env start --debug
...
```

## Using Xdebug

Xdebug is installed in the wp-env environment, but it is turned off by default. To enable Xdebug, you can use the `--xdebug` flag with the `wp-env start` command. Here is a reference to how the flag works:

```sh
# Sets the Xdebug mode to "debug" (for step debugging):
wp-env start --xdebug

# Sets the Xdebug mode to "off":
wp-env start

# Enables each of the Xdebug modes listed:
wp-env start --xdebug=profile,trace,debug
```

You can see a reference on each of the Xdebug modes and what they do in the [Xdebug documentation](https://xdebug.org/docs/all_settings#mode).

### Xdebug IDE support

To connect to Xdebug from your IDE, you can use these IDE settings. This bit of JSON was tested for VS Code's `launch.json` format (which you can [learn more about here](https://code.visualstudio.com/docs/editor/debugging#_launchjson-attributes)) along with [this PHP Debug extension](https://marketplace.visualstudio.com/items?itemName=felixfbecker.php-debug). Its path mapping also points to a specific plugin -- you should update this to point to the source you are working with inside of the wp-env instance.

You should only have to translate `port` and `pathMappings` to the format used by your own IDE.

```json
{
"name": "Listen for XDebug",
"type": "php",
"request": "launch",
"port": 9003,
"pathMappings": {
"/var/www/html/wp-content/plugins/gutenberg": "${workspaceRoot}/"
}
}
```

Once your IDEs Xdebug settings have been enabled, you should just have to launch the debugger, put a breakpoint on any line of PHP code, and then refresh your browser!

Here is a summary:

1. Start wp-env with xdebug enabled: `wp-env start --xdebug`
2. Install a suitable Xdebug extension for your IDE if it does not include one already.
3. Configure the IDE debugger to use port `9003` and the correct source files in wp-env.
4. Launch the debugger and put a breakpoint on any line of PHP code.
5. Refresh the URL wp-env is running at and the breakpoint should trigger.

## Command reference

`wp-env` creates generated files in the `wp-env` home directory. By default, this is `~/.wp-env`. The exception is Linux, where files are placed at `~/wp-env` [for compatibility with Snap Packages](https://github.com/WordPress/gutenberg/issues/20180#issuecomment-587046325). The `wp-env` home directory contains a subdirectory for each project named `/$md5_of_project_path`. To change the `wp-env` home directory, set the `WP_ENV_HOME` environment variable. For example, running `WP_ENV_HOME="something" wp-env start` will download the project files to the directory `./something/$md5_of_project_path` (relative to the current directory).
Expand All @@ -202,12 +247,20 @@ wp-env start
Starts WordPress for development on port 8888 (override with WP_ENV_PORT) and
tests on port 8889 (override with WP_ENV_TESTS_PORT). The current working
directory must be a WordPress installation, a plugin, a theme, or contain a
.wp-env.json file. After first install, use the '--update' flag to download updates
to mapped sources and to re-apply WordPress configuration options.
.wp-env.json file. After first install, use the '--update' flag to download
updates to mapped sources and to re-apply WordPress configuration options.

Options:
--help Show help [boolean]
--version Show version number [boolean]
--debug Enable debug output. [boolean] [default: false]
--update Download source updates and apply WordPress configuration.
[boolean] [default: false]
--xdebug Enables Xdebug. If not passed, Xdebug is turned off. If no modes
are set, uses "debug". You may set multiple Xdebug modes by passing
them in a comma-separated list: `--xdebug=develop,coverage`. See
https://xdebug.org/docs/all_settings#mode for information about
Xdebug modes. [string]
```

### `wp-env stop`
Expand Down
1 change: 1 addition & 0 deletions packages/env/lib/build-docker-compose-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ module.exports = function buildDockerComposeConfig( config ) {
volumes: [ 'mysql:/var/lib/mysql' ],
},
wordpress: {
build: '.',
depends_on: [ 'mysql' ],
image: developmentWpImage,
ports: [ developmentPorts ],
Expand Down
7 changes: 7 additions & 0 deletions packages/env/lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const terminalLink = require( 'terminal-link' );
* Internal dependencies
*/
const env = require( './env' );
const parseXdebugMode = require( './parse-xdebug-mode' );

// Colors
const boldWhite = chalk.bold.white;
Expand Down Expand Up @@ -99,6 +100,12 @@ module.exports = function cli() {
'Download source updates and apply WordPress configuration.',
default: false,
} );
args.option( 'xdebug', {
describe:
'Enables Xdebug. If not passed, Xdebug is turned off. If no modes are set, uses "debug". You may set multiple Xdebug modes by passing them in a comma-separated list: `--xdebug=develop,coverage`. See https://xdebug.org/docs/all_settings#mode for information about Xdebug modes.',
coerce: parseXdebugMode,
type: 'string',
} );
},
withSpinner( env.start )
);
Expand Down
10 changes: 8 additions & 2 deletions packages/env/lib/commands/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,18 @@ const CONFIG_CACHE_KEY = 'config_checksum';
* @param {Object} options.spinner A CLI spinner which indicates progress.
* @param {boolean} options.debug True if debug mode is enabled.
* @param {boolean} options.update If true, update sources.
* @param {string} options.xdebug The Xdebug mode to set.
*/
module.exports = async function start( { spinner, debug, update } ) {
module.exports = async function start( { spinner, debug, update, xdebug } ) {
spinner.text = 'Reading configuration.';
await checkForLegacyInstall( spinner );

const config = await initConfig( { spinner, debug } );
const config = await initConfig( {
spinner,
debug,
xdebug,
writeChanges: true,
} );

if ( ! config.detectedLocalConfig ) {
const { configDirectoryPath } = config;
Expand Down
81 changes: 71 additions & 10 deletions packages/env/lib/init-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
* External dependencies
*/
const path = require( 'path' );
const fs = require( 'fs' ).promises;
const { writeFile, mkdir } = require( 'fs' ).promises;
const { existsSync } = require( 'fs' );
const yaml = require( 'js-yaml' );
const os = require( 'os' );

/**
* Internal dependencies
Expand All @@ -20,23 +22,32 @@ const buildDockerComposeConfig = require( './build-docker-compose-config' );
* ./.wp-env.json, creates ~/.wp-env, and creates ~/.wp-env/docker-compose.yml.
*
* @param {Object} options
* @param {Object} options.spinner A CLI spinner which indicates progress.
* @param {boolean} options.debug True if debug mode is enabled.
*
* @param {Object} options.spinner A CLI spinner which indicates progress.
* @param {boolean} options.debug True if debug mode is enabled.
* @param {string} options.xdebug The Xdebug mode to set. Defaults to "off".
* @param {boolean} options.writeChanges If true, writes the parsed config to the
* required docker files like docker-compose
* and Dockerfile. By default, this is false
* and only the `start` command writes any
* changes.
* @return {WPConfig} The-env config object.
*/
module.exports = async function initConfig( { spinner, debug } ) {
module.exports = async function initConfig( {
spinner,
debug,
xdebug = 'off',
writeChanges = false,
} ) {
const configPath = path.resolve( '.wp-env.json' );
const config = await readConfig( configPath );
config.debug = debug;

await fs.mkdir( config.workDirectoryPath, { recursive: true } );
// Adding this to the config allows the start command to understand that the
// config has changed when only the xdebug param has changed. This is needed
// so that Docker will rebuild the image whenever the xdebug flag changes.
config.xdebug = xdebug;

const dockerComposeConfig = buildDockerComposeConfig( config );
await fs.writeFile(
config.dockerComposeConfigPath,
yaml.dump( dockerComposeConfig )
);

if ( config.debug ) {
spinner.info(
Expand All @@ -53,5 +64,55 @@ module.exports = async function initConfig( { spinner, debug } ) {
spinner.start();
}

/**
* We avoid writing changes most of the time so that we can better pass params
* to the start command. For example, say you start wp-env with Xdebug enabled.
* If you then run another command, like opening bash in the wp instance, it
* would turn off Xdebug in the Dockerfile because it wouldn't have the --xdebug
* arg. This basically makes it such that wp-env start is the only command
* which updates any of the Docker configuration.
*/
if ( writeChanges ) {
await mkdir( config.workDirectoryPath, { recursive: true } );

await writeFile(
config.dockerComposeConfigPath,
yaml.dump( dockerComposeConfig )
);

await writeFile(
path.resolve( config.workDirectoryPath, 'Dockerfile' ),
dockerFileContents(
dockerComposeConfig.services.wordpress.image,
xdebug
)
);
} else if ( ! existsSync( config.workDirectoryPath ) ) {
spinner.fail(
'wp-env has not yet been initalized. 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 );
}

return config;
};

function dockerFileContents( image, xdebugMode ) {
const isLinux = os.type() === 'Linux';
// Discover client host does not appear to work on macOS with Docker.
const clientDetectSettings = isLinux
? 'xdebug.discover_client_host=true'
: 'xdebug.client_host="host.docker.internal"';

return `FROM ${ image }
RUN apt -qy install $PHPIZE_DEPS \\
&& pecl install xdebug \\
&& docker-php-ext-enable xdebug
RUN touch /usr/local/etc/php/php.ini
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 '${ clientDetectSettings }' >> /usr/local/etc/php/php.ini
`;
}
47 changes: 47 additions & 0 deletions packages/env/lib/parse-xdebug-mode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// See https://xdebug.org/docs/all_settings#mode
const XDEBUG_MODES = [
'develop',
'coverage',
'debug',
'gcstats',
'profile',
'trace',
];

/**
* Custom parsing for the Xdebug mode set via yargs. This function ensures two things:
* 1. If the --xdebug flag was set by itself, default to 'debug'.
* 2. If the --xdebug flag includes modes, make sure they are accepted by Xdebug.
*
* Note: ideally, we would also have this handle the case where no xdebug flag
* is set (and then turn Xdebug off). However, yargs does not pass 'undefined'
* to the coerce callback, so we cannot handle that case here.
*
* @param {string} value The user-set mode of Xdebug
* @return {string} The Xdebug mode to use with defaults applied.
*/
module.exports = function parseXdebugMode( value ) {
if ( typeof value !== 'string' ) {
throwXdebugModeError( value );
}

if ( value.length === 0 ) {
return 'debug';
}

const modes = value.split( ',' );
modes.forEach( ( userMode ) => {
if ( ! XDEBUG_MODES.some( ( realMode ) => realMode === userMode ) ) {
throwXdebugModeError( userMode );
}
} );
return value;
};

function throwXdebugModeError( value ) {
throw new Error(
`"${ value }" is not a mode recognized by Xdebug. Valid modes are: ${ XDEBUG_MODES.join(
', '
) }`
);
}
34 changes: 34 additions & 0 deletions packages/env/test/parse-xdebug-mode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Internal dependencies
*/
const parseXdebugMode = require( '../lib/parse-xdebug-mode' );

describe( 'parseXdebugMode', () => {
it( 'throws an error if the passed value is not a string', () => {
expect( () => parseXdebugMode() ).toThrow(
'is not a mode recognized by Xdebug'
);
} );

it( 'sets the Xdebug mode to "debug" if no mode is specified', () => {
const result = parseXdebugMode( '' );
expect( result ).toEqual( 'debug' );
} );

it( 'throws an error if a given mode is not recognized, including the invalid mode in the output', () => {
const fakeMode = 'fake-mode-123';
expect.assertions( 2 );
// Single mode:
expect( () => parseXdebugMode( fakeMode ) ).toThrow( fakeMode );

// Many modes:
expect( () =>
parseXdebugMode( `debug,profile,${ fakeMode }` )
).toThrow( fakeMode );
} );

it( 'returns all modes passed', () => {
const result = parseXdebugMode( 'debug,profile,trace' );
expect( result ).toEqual( 'debug,profile,trace' );
} );
} );

0 comments on commit 454c708

Please sign in to comment.