diff --git a/.env.slic b/.env.slic index c686328..521e106 100644 --- a/.env.slic +++ b/.env.slic @@ -29,10 +29,18 @@ SLIC_GIT_HANDLE=stellarwp # The path where `slic here` was executed. # SLIC_HERE_DIR= +# The WordPress content directory inside the containers. +SLIC_WP_CONTENT_CONTAINER_DIR=/var/www/html/wp-content # The path from which to read plugins from. SLIC_PLUGINS_DIR=./_plugins +# The site's plugins path when a target-local override changes the effective Docker mount. Managed by `slic here`. +SLIC_SITE_PLUGINS_DIR= +# The path from which to read must-use plugins from. +SLIC_MU_PLUGINS_DIR=./_wordpress/wp-content/mu-plugins # The path from which to read themes from. SLIC_THEMES_DIR=./_wordpress/wp-content/themes +# The site's themes path when the effective Docker mount uses Slic's bundled themes. Managed by `slic here`. +SLIC_SITE_THEMES_DIR= # The path from which to read WordPress core code from. SLIC_WP_DIR=./_wordpress # The path where mounted scripts will live diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e33d5d2..e96303c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,10 +19,10 @@ export PATH=$PATH:path/to/slic When we prep a release, we follow these steps: -1. Create a branch with the version number of the release, e.g. `1.0.0` +1. Create a branch named for the release, e.g. `release/1.0.0` 2. Merge changes into that branch. This will trigger the Docker images to build. 3. Ensure that the `CLI_VERSION` has been updated in `slic.php` to reflect the version number of the release. 4. Ensure that the `changelog.md` file has been updated. 5. Merge the release branch into `main`. 6. Delete the release branch. -7. Tag the release with the changelog.md contents for that release. This will trigger the final Docker images to build. \ No newline at end of file +7. Publish a GitHub Release using the changelog contents for that release. This will trigger the final Docker images to build. diff --git a/README.md b/README.md index 26db97d..d289131 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,19 @@ slic here By running `slic here` at the site level, this allows you to set plugins, themes, or the site itself as the location from which to run tests. This also has the benefit of running tests within the WP version that your site uses. +`slic here` mounts the complete site root at `/var/www/html`. WordPress core remains at `/var/www/html/wp` when the +project installs it in `wp/`, while root installations remain at `/var/www/html`. The detected `content/` or +`wp-content/` directory is mounted at the matching container path. + +When a project installs WordPress in `wp/`, selecting an individual plugin or theme uses Slic's isolated WordPress +installation instead. This preserves existing component test configurations that expect core at `/var/www/html`. +Plugin targets use Slic's bundled default themes, while theme targets mount the project's themes directory. Selecting +`site` switches back to the complete project root. Slic recreates running PHP containers when these mounts change and +leaves the database and other services running. + +`slic here` also disables subdirectory builds for sites and enables them for plugin and theme directories. You can +override the detected setting with `slic build-subdir on|off`. + ![slic here](docs/images/slic-here-wp.gif) ### Preparing your project diff --git a/changelog.md b/changelog.md index b805e8c..23be290 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# [2.4.0] - 2026-08-06 +- Added - First-class support for site-mode projects that use `content/` instead of `wp-content/`, including plugin and theme container paths. +- Changed - `slic here` now mounts the complete project root for every WordPress site, including projects that install core into `wp/`. +- Changed - `slic here` now disables subdirectory builds for sites and enables them for plugin and theme directories. +- Fixed - Site mode no longer creates an unused `wp-content/` directory when the project uses a custom content directory. +- Fixed - Plugin and theme targets in projects that install WordPress in `wp/` continue to use Slic's isolated WordPress installation. +- Fixed - Switching between site and isolated component targets recreates only the running PHP containers so changed mounts take effect without resetting the test database. +- Fixed - Site theme discovery remains stable while plugin and theme targets use their required theme mounts. +- Fixed - Target-local mount overrides remain authoritative when Slic selects a site layout. +- Changed - Multi-target commands reject mixed site, plugin, and theme targets when their WordPress mounts are incompatible. +- Changed - Multi-target commands reject targets with different target-local bind-mount settings. +- Fixed - Target-local Composer cache and Slic scripts bind mounts now recreate running PHP containers and persist their runtime state. +- Fixed - Target switching now stops without persisting new mount state when Docker cannot determine which services are running. +- Fixed - Test containers now disable the object-cache drop-in without modifying the project file. +- Fixed - Airplane mode now uses and creates the configured must-use plugin directory. + # [2.3.0] - 2026-06-25 - Added - PCOV support to the slic and WordPress containers to make code coverage generation faster than Xdebug-backed coverage. - Added - The `slic pcov on|off|status` command to configure PCOV and apply it to running containers and checks if xdebug is running and asks the user if they want to disable it. diff --git a/containers/slic/php.ini b/containers/slic/php.ini index 7a1a249..66265ef 100644 --- a/containers/slic/php.ini +++ b/containers/slic/php.ini @@ -15,6 +15,9 @@ post_max_size=128M ;Allow long request. max_execution_time=300 +;Disable project object-cache drop-ins without modifying host files. +auto_prepend_file=/slic-disable-object-cache.php + ;If XDebug is active, it should always start. xdebug.start_with_request=yes xdebug.mode=develop,debug,coverage @@ -27,4 +30,3 @@ opcache.validate_timestamps=1 opcache.max_accelerated_files=25000 opcache.memory_consumption=128 opcache.interned_strings_buffer=16 - diff --git a/containers/wordpress/disable-object-cache.php b/containers/wordpress/disable-object-cache.php new file mode 100644 index 0000000..83860aa --- /dev/null +++ b/containers/wordpress/disable-object-cache.php @@ -0,0 +1,26 @@ + static function () { + return false; + }, + 'accepted_args' => 0, +]; diff --git a/containers/wordpress/php.ini b/containers/wordpress/php.ini index 9384500..66265ef 100644 --- a/containers/wordpress/php.ini +++ b/containers/wordpress/php.ini @@ -15,6 +15,9 @@ post_max_size=128M ;Allow long request. max_execution_time=300 +;Disable project object-cache drop-ins without modifying host files. +auto_prepend_file=/slic-disable-object-cache.php + ;If XDebug is active, it should always start. xdebug.start_with_request=yes xdebug.mode=develop,debug,coverage diff --git a/docs/xdebug.md b/docs/xdebug.md index 00449f4..b8ba7b8 100644 --- a/docs/xdebug.md +++ b/docs/xdebug.md @@ -29,7 +29,7 @@ In PHPStorm settings: 5. Set the `Host` to whatever `slic xdebug status` returns. (typically `http://localhost:8888` ... yes, put the whole thing in the `Host` field) 6. Set the `Port` to `80` 7. Check the `Use path mappings` checkbox -8. Find the `wp-content/plugins` directory and set the `Absolute path on the server` to `/var/www/html/wp-content/plugins` +8. Find the plugins directory and set the `Absolute path on the server` to the path reported by `slic xdebug status` 9. If you've added the `slic` directory to your workspace, find the `slic/_wordpress` directory and set the `Absolute path on the server` to `/var/www/html` Screenshot from PhpStorm's video: @@ -143,4 +143,4 @@ If breakpoints still don't work: slic xdebug on ``` -4. **Verify the port**: Ensure port 9001 is not blocked by a firewall and that your code editor is listening on that port. \ No newline at end of file +4. **Verify the port**: Ensure port 9001 is not blocked by a firewall and that your code editor is listening on that port. diff --git a/slic-stack.site.yml b/slic-stack.site.yml index fb64db2..9d1660a 100644 --- a/slic-stack.site.yml +++ b/slic-stack.site.yml @@ -11,4 +11,4 @@ services: slic: environment: # Move to the target directory before running the command from the plugins directory. - CODECEPTION_PROJECT_DIR: /var/www/html + CODECEPTION_PROJECT_DIR: ${SLIC_CURRENT_PROJECT_CONTAINER_PATH:-/var/www/html} diff --git a/slic-stack.yml b/slic-stack.yml index 10be2a9..df00d17 100644 --- a/slic-stack.yml +++ b/slic-stack.yml @@ -110,8 +110,9 @@ services: volumes: # Paths are relative to the directory that contains this file, NOT the current working directory. - ${SLIC_WP_DIR}:/var/www/html - - ${SLIC_PLUGINS_DIR}:/var/www/html/wp-content/plugins - - ${SLIC_THEMES_DIR}:/var/www/html/wp-content/themes + - ${SLIC_PLUGINS_DIR}:${SLIC_WP_CONTENT_CONTAINER_DIR}/plugins + - ${SLIC_THEMES_DIR}:${SLIC_WP_CONTENT_CONTAINER_DIR}/themes + - ./containers/wordpress/disable-object-cache.php:/slic-disable-object-cache.php:ro - ${COMPOSER_CACHE_DIR:-./.cache}:/composer-cache - ./containers/wordpress/php.ini:/usr/local/etc/php/conf.d/zz-docker.ini healthcheck: # Apache service should be running correctly. @@ -170,7 +171,7 @@ services: # XDH=$(ip route | grep docker0 | awk '{print $9}') docker compose ... XDEBUG_CONFIG: "idekey=${XDK:-slic} remote_enable=${XDE:-1} remote_host=${XDH:-host.docker.internal} remote_port=${XDP:-9001} client_host=${XDH:-host.docker.internal} client_port=${XDP:-9001}" # Move to the target directory before running Codeception commands. - CODECEPTION_PROJECT_DIR: ${SLIC_CURRENT_PROJECT_CONTAINER_PATH:-/var/www/html/wp-content/plugins/test} + CODECEPTION_PROJECT_DIR: ${SLIC_CURRENT_PROJECT_CONTAINER_PATH:-${SLIC_WP_CONTENT_CONTAINER_DIR}/plugins/test} # When running the container in shell mode (using the slic `shell` command), then use this CC configuration. CODECEPTION_SHELL_CONFIG: "-c codeception.slic.yml" # Whether to disable the XDebug extension in the Codeception container completely or not. @@ -194,9 +195,11 @@ services: # Paths are relative to the directory that contains this file, NOT the current working directory. # Share the WordPress core installation files in the `_wordpress` directory. - ${SLIC_WP_DIR}:/var/www/html - # Share the plugins in the `/var/www/hmtl/wp-content/plugins` directory. - - ${SLIC_PLUGINS_DIR}:/var/www/html/wp-content/plugins - - ${SLIC_THEMES_DIR}:/var/www/html/wp-content/themes + # Share plugins and themes in the configured WordPress content directory. + - ${SLIC_PLUGINS_DIR}:${SLIC_WP_CONTENT_CONTAINER_DIR}/plugins + - ${SLIC_THEMES_DIR}:${SLIC_WP_CONTENT_CONTAINER_DIR}/themes + # Disable the project's persistent object-cache drop-in without modifying the host file. + - ./containers/wordpress/disable-object-cache.php:/slic-disable-object-cache.php:ro # In some plugins we use function-mocker and set it up to cache in `/tmp/function-mocker`. # To avoid a long re-caching on each run, let's cache in a docker volume, caching on the host # filesystem would be a worse cure than the disease. diff --git a/slic.php b/slic.php index ca2964a..60f072c 100644 --- a/slic.php +++ b/slic.php @@ -54,7 +54,7 @@ ] ); $cli_name = 'slic'; -const CLI_VERSION = '2.3.0'; +const CLI_VERSION = '2.4.0'; // If the run-time option `-q`, for "quiet", is specified, then do not print the header. if ( in_array( '-q', $argv, true ) || ( in_array( 'exec', $argv, true ) && ! in_array( 'help', $argv, true ) ) ) { diff --git a/src/commands/airplane-mode.php b/src/commands/airplane-mode.php index 4c459f4..07a0942 100644 --- a/src/commands/airplane-mode.php +++ b/src/commands/airplane-mode.php @@ -47,8 +47,8 @@ $plugin_dir = $mu_plugins_dir . DIRECTORY_SEPARATOR . 'airplane-mode'; if ( - ! is_dir( dirname( $mu_plugins_dir ) ) - && ! mkdir( $concurrentDirectory = dirname( $mu_plugins_dir ), 0755, true ) + ! is_dir( $mu_plugins_dir ) + && ! mkdir( $concurrentDirectory = $mu_plugins_dir, 0755, true ) && ! is_dir( $concurrentDirectory ) ) { echo magenta( "Failed to create mu-plugins directory {$mu_plugins_dir}." ); diff --git a/src/commands/here.php b/src/commands/here.php index 803c425..ee0ccda 100644 --- a/src/commands/here.php +++ b/src/commands/here.php @@ -35,9 +35,10 @@ $sub_args = args( [ 'reset' ], $args( '...' ), 0 ); $reset = $sub_args( 'reset', false ); -$wp_dir = SLIC_ROOT_DIR . '/_wordpress'; -$plugins_dir = SLIC_ROOT_DIR . '/_plugins'; -$themes_dir = SLIC_ROOT_DIR . '/_wordpress/wp-content/themes'; +$wp_dir = SLIC_ROOT_DIR . '/_wordpress'; +$plugins_dir = SLIC_ROOT_DIR . '/_plugins'; +$mu_plugins_dir = SLIC_ROOT_DIR . '/_wordpress/wp-content/mu-plugins'; +$themes_dir = SLIC_ROOT_DIR . '/_wordpress/wp-content/themes'; if ( empty( $reset ) ) { $here_dir = getcwd(); @@ -50,7 +51,13 @@ } $has_wp_config = dir_has_wp_config( $here_dir ); -$env_values = []; +$env_values = [ + 'SLIC_BUILD_SUBDIR' => 1, + 'SLIC_MU_PLUGINS_DIR' => $mu_plugins_dir, + 'SLIC_SITE_PLUGINS_DIR' => '', + 'SLIC_SITE_THEMES_DIR' => '', + 'SLIC_WP_CONTENT_CONTAINER_DIR' => '/var/www/html/wp-content', +]; if ( $has_wp_config ) { if ( file_exists( "{$here_dir}/wp-content" ) ) { @@ -58,20 +65,20 @@ } elseif ( file_exists( "{$here_dir}/content" ) ) { $wp_content_dir = "{$here_dir}/content"; } else { - echo magenta( "Cannot locate the wp-content directory. If you have a custom wp-content location, you will need to set the SLIC_WP_DIR, SLIC_PLUGINS_DIR, and SLIC_THEMES_DIR manually in slic's .env.slic.run file." ); + echo magenta( "Cannot locate the WordPress content directory." ); exit( 1 ); } $env_values['SLIC_HERE_DIR'] = $here_dir; - // Support WP skeleton. - if ( file_exists( "{$here_dir}/wp" ) ) { - $here_dir .= '/wp'; - } - - $env_values['SLIC_WP_DIR'] = $here_dir; - $env_values['SLIC_PLUGINS_DIR'] = "{$wp_content_dir}/plugins"; - $env_values['SLIC_THEMES_DIR'] = "{$wp_content_dir}/themes"; + $env_values['SLIC_BUILD_SUBDIR'] = 0; + $env_values['SLIC_MU_PLUGINS_DIR'] = "{$wp_content_dir}/mu-plugins"; + $env_values['SLIC_PLUGINS_DIR'] = "{$wp_content_dir}/plugins"; + $env_values['SLIC_SITE_PLUGINS_DIR'] = "{$wp_content_dir}/plugins"; + $env_values['SLIC_SITE_THEMES_DIR'] = "{$wp_content_dir}/themes"; + $env_values['SLIC_THEMES_DIR'] = "{$wp_content_dir}/themes"; + $env_values['SLIC_WP_DIR'] = $here_dir; + $env_values['SLIC_WP_CONTENT_CONTAINER_DIR'] = '/var/www/html/' . basename( $wp_content_dir ); } else { $env_values['SLIC_HERE_DIR'] = $here_dir; $env_values['SLIC_WP_DIR'] = $wp_dir; @@ -92,6 +99,12 @@ } } +foreach ( slic_target_mount_base_keys() as $key => $base_key ) { + if ( array_key_exists( $key, $env_values ) ) { + $env_values[ $base_key ] = $env_values[ $key ]; + } +} + // When changing the here target, clear the currently selected project. $env_values['SLIC_CURRENT_PROJECT'] = ''; $env_values['SLIC_CURRENT_PROJECT_CONTAINER_PATH'] = ''; diff --git a/src/commands/run.php b/src/commands/run.php index 56e4051..9c92b55 100644 --- a/src/commands/run.php +++ b/src/commands/run.php @@ -58,17 +58,6 @@ // rather than always falling back to SLIC_PLUGINS_DIR. $root = get_project_local_path(); -// Object-cache is disruptive in the context of tests; remove the object cache drop-in before running the tests. -$object_cache_dropin = slic_wp_dir( 'wp-content/object-cache.php' ); -if ( file_exists( $object_cache_dropin ) ) { - echo "Removing the object cache drop-in file before tests..." . PHP_EOL; - if ( ! unlink( $object_cache_dropin ) ) { - echo magenta( "Failed to remove the {$object_cache_dropin} file." . PHP_EOL ); - exit( 1 ); - } - echo "Object cache drop-in file removed." . PHP_EOL; -} - /* * Check what configuration files we've got available. * Depending on the what we have apply them in this order: dist, local, slic. diff --git a/src/commands/target.php b/src/commands/target.php index 771bc71..20b8e95 100644 --- a/src/commands/target.php +++ b/src/commands/target.php @@ -32,6 +32,56 @@ } while ( ! empty( $last_target ) ); $targets = array_unique( $targets ); +$target_types = array_unique( array_map( __NAMESPACE__ . '\\get_target_content_type', $targets ) ); + +if ( + slic_here_is_site() + && site_wordpress_is_in_subdirectory( realpath( getenv( 'SLIC_HERE_DIR' ) ) ) + && count( $target_types ) > 1 +) { + echo magenta( 'Site, plugin, and theme targets use different mounts and cannot be mixed in one target command.' . PHP_EOL ); + exit( 1 ); +} + +if ( count( $targets ) > 1 ) { + $mount_keys = slic_target_mount_keys(); + $original_environment = []; + + foreach ( array_merge( $mount_keys, [ 'SLIC_MU_PLUGINS_DIR' ] ) as $key ) { + $original_environment[ $key ] = getenv( $key ); + } + + $profiles = array_map( static function ( $target ) use ( $mount_keys ) { + foreach ( slic_target_mount_base_keys() as $key => $base_key ) { + putenv( "{$key}=" . getenv( $base_key ) ); + } + + slic_site_target_environment( explode( '/', $target )[0] ); + + $profile = []; + foreach ( $mount_keys as $key ) { + $profile[ $key ] = getenv( $key ); + } + + $file = get_project_local_path( $target ) . '/.env.slic.local'; + $profile = array_merge( + $profile, + is_file( $file ) ? array_intersect_key( read_env_file( $file ), array_flip( $mount_keys ) ) : [] + ); + ksort( $profile ); + + return serialize( $profile ); + }, $targets ); + + foreach ( $original_environment as $key => $value ) { + false === $value ? putenv( $key ) : putenv( "{$key}={$value}" ); + } + + if ( count( array_unique( $profiles ) ) > 1 ) { + echo magenta( 'Targets with different bind-mount settings cannot be mixed in one target command.' . PHP_EOL ); + exit( 1 ); + } +} $command_lines = []; @@ -86,4 +136,3 @@ slic_switch_target( $previous_target ); exit( $status ); - diff --git a/src/docker.php b/src/docker.php index 579c590..b1a2794 100644 --- a/src/docker.php +++ b/src/docker.php @@ -159,7 +159,7 @@ function slic_stack_array( $filenames_only = false ) { $base_stack = stack(); $stack_array = [ $file_prefix, $quote . $base_stack . $quote ]; - if ( slic_here_is_site() ) { + if ( slic_uses_site_root() ) { $stack_array[] = $file_prefix; $stack_array[] = $quote . stack( '.site' ) . $quote; } @@ -260,4 +260,3 @@ function docker_compose_realtime( array $options = [] ) { function docker_compose_bin(): string { return (string) getenv( 'SLIC_DOCKER_COMPOSE_BIN' ) ?: 'docker compose'; } - diff --git a/src/project.php b/src/project.php index 54278b0..e132984 100644 --- a/src/project.php +++ b/src/project.php @@ -108,14 +108,15 @@ function get_project_local_path( $target = null ) { * @return string The container target path. */ function get_project_container_path( $target = null ) { - $target = $target ?: slic_target(); + $target = $target ?: slic_target(); + $content_dir = getenv( 'SLIC_WP_CONTENT_CONTAINER_DIR' ) ?: '/var/www/html/wp-content'; if ( empty( $target ) ) { switch ( get_project_type() ) { case 'plugin': - return '/var/www/html/wp-content/plugins'; + return $content_dir . '/plugins'; case 'theme': - return '/var/www/html/wp-content/themes'; + return $content_dir . '/themes'; default: return '/var/www/html'; } @@ -123,9 +124,9 @@ function get_project_container_path( $target = null ) { switch ( get_target_content_type( $target ) ) { case 'plugin': - return '/var/www/html/wp-content/plugins/' . $target; + return $content_dir . '/plugins/' . $target; case 'theme': - return '/var/www/html/wp-content/themes/' . $target; + return $content_dir . '/themes/' . $target; case 'site': return '/var/www/html'; } diff --git a/src/services.php b/src/services.php index d91dc4e..8439e8e 100644 --- a/src/services.php +++ b/src/services.php @@ -98,7 +98,8 @@ function service_running( string $service ) { $ps = slic_process()( [ 'ps', '--services', '--filter', '"status=running"' ] ); $ps_status = $ps( 'status' ); if ( $ps_status !== 0 ) { - return false; + echo magenta( 'Could not determine which Slic services are running.' . PHP_EOL ); + exit( 1 ); } $running_services = explode( "\n", $ps( 'string_output' ) ); slic_cache_set( 'running_services', $running_services ); diff --git a/src/slic.php b/src/slic.php index 2698760..0816abc 100644 --- a/src/slic.php +++ b/src/slic.php @@ -76,10 +76,27 @@ function cli_header( $cli_name, $full = false, $extra = null ) { * @return bool */ function slic_here_is_site() { - $env_wp_dir = getenv( 'SLIC_WP_DIR' ); + $here_dir = getenv( 'SLIC_HERE_DIR' ); - return SLIC_ROOT_DIR . '/_wordpress' !== $env_wp_dir - && './_wordpress' !== $env_wp_dir; + if ( empty( $here_dir ) ) { + return false; + } + + $here_dir = realpath( $here_dir ); + $plugins_dir = realpath( getenv( 'SLIC_SITE_PLUGINS_DIR' ) ?: getenv( 'SLIC_PLUGINS_DIR' ) ); + $themes_dir = realpath( getenv( 'SLIC_SITE_THEMES_DIR' ) ?: getenv( 'SLIC_THEMES_DIR' ) ); + + return $here_dir && $here_dir !== $plugins_dir && $here_dir !== $themes_dir; +} + +/** + * Returns whether the complete site root is mounted as WordPress. + * + * @return bool + */ +function slic_uses_site_root() { + return slic_here_is_site() + && realpath( getenv( 'SLIC_HERE_DIR' ) ) === realpath( getenv( 'SLIC_WP_DIR' ) ); } /** @@ -229,8 +246,8 @@ function get_target_relative_path( $target ) { return ''; } - $plugin_dir = getenv( 'SLIC_PLUGINS_DIR' ); - $theme_dir = getenv( 'SLIC_THEMES_DIR' ); + $plugin_dir = slic_plugins_dir(); + $theme_dir = slic_themes_dir(); if ( file_exists( "{$plugin_dir}/{$target}" ) ) { $parent_path = $plugin_dir; @@ -285,9 +302,16 @@ function setup_slic_env( $root_dir, $reset = false ) { load_env_file( $root_dir . '/.env.slic.local' ); } + $configured_mounts = []; + foreach ( slic_target_mount_base_keys() as $key => $base_key ) { + $configured_mounts[ $key ] = getenv( $key ); + } + $configured_mounts['COMPOSER_CACHE_DIR'] = $configured_mounts['COMPOSER_CACHE_DIR'] ?: cache( '/composer' ); + // Load the current session configuration file. $run_env_file = $root_dir . '/.env.slic.run'; $staged_php_version = null; + $run_env = []; if ( file_exists( $run_env_file ) ) { load_env_file( $run_env_file ); @@ -301,6 +325,26 @@ function setup_slic_env( $root_dir, $reset = false ) { } } + // Restore target-overridable mounts to their global values before loading the selected target's overrides. + foreach ( slic_target_mount_base_keys() as $key => $base_key ) { + if ( 'SLIC_SCRIPTS' === $key ) { + $base_value = $configured_mounts[ $key ]; + } elseif ( 'COMPOSER_CACHE_DIR' === $key ) { + $legacy_cache = ! isset( $run_env[ $base_key ] ) + && isset( $run_env[ $key ] ) + && ! array_key_exists( 'SLIC_COMPOSER_CACHE_IS_EXPLICIT', $run_env ); + $explicit_cache = '1' === ( $run_env['SLIC_COMPOSER_CACHE_IS_EXPLICIT'] ?? '' ); + $base_value = $explicit_cache || $legacy_cache + ? ( $run_env[ $base_key ] ?? $run_env[ $key ] ) + : $configured_mounts[ $key ]; + putenv( 'SLIC_COMPOSER_CACHE_IS_EXPLICIT=' . ( $explicit_cache || $legacy_cache ? '1' : '0' ) ); + } else { + $base_value = $run_env[ $base_key ] ?? $run_env[ $key ] ?? $configured_mounts[ $key ]; + } + putenv( "{$base_key}={$base_value}" ); + putenv( "{$key}={$base_value}" ); + } + /* * Special handling of the `use` command. * @@ -315,6 +359,10 @@ function setup_slic_env( $root_dir, $reset = false ) { putenv( "SLIC_CURRENT_PROJECT={$target}" ); } + if ( slic_here_is_site() && getenv( 'SLIC_CURRENT_PROJECT' ) ) { + slic_site_target_environment( getenv( 'SLIC_CURRENT_PROJECT' ) ); + } + $target_path = get_project_local_path(); if( ! empty( $target_path ) ) { // Load the local overrides from the target. @@ -352,14 +400,14 @@ function setup_slic_env( $root_dir, $reset = false ) { if ( $wp_dir === './_wordpress' || $wp_dir === $default_wp_dir ) { // Default WordPress directory, inside slic. $wp_dir = ensure_dir( $default_wp_dir ); + ensure_dir( $wp_dir . '/wp-content' ); } else if ( ! is_dir( $wp_dir ) ) { // Custom WordPress directory, it falls on the user to have it set up correctly. echo magenta( "WordPress directory $wp_dir does not exist; is it initialized?" ); exit( 1 ); } - $wp_content_dir = ensure_dir( $wp_dir . '/wp-content' ); - $wp_themes_dir = $wp_content_dir . '/themes'; + $wp_themes_dir = $wp_dir . '/wp-content/themes'; putenv( 'SLIC_WP_DIR=' . $wp_dir ); putenv( 'SLIC_PLUGINS_DIR=' . ensure_dir( getenv( 'SLIC_PLUGINS_DIR' ) ?: root( '_plugins' ) ) ); @@ -474,7 +522,40 @@ function slic_target( $require = true ) { } /** - * Switches the current `use` target. + * Returns environment variables whose values determine target-specific bind mounts. + * + * @return string[] Bind-mount environment variable names. + */ +function slic_target_mount_keys() { + return [ + 'SLIC_WP_DIR', + 'SLIC_PLUGINS_DIR', + 'SLIC_THEMES_DIR', + 'SLIC_WP_CONTENT_CONTAINER_DIR', + 'COMPOSER_CACHE_DIR', + 'SLIC_COMPOSER_CACHE_IS_EXPLICIT', + 'SLIC_SCRIPTS', + ]; +} + +/** + * Returns target-overridable bind mounts that must restore a global value when targets change. + * + * @return array Effective variable names mapped to their runtime base variable names. + */ +function slic_target_mount_base_keys() { + return [ + 'SLIC_WP_DIR' => 'SLIC_BASE_WP_DIR', + 'SLIC_PLUGINS_DIR' => 'SLIC_BASE_PLUGINS_DIR', + 'SLIC_THEMES_DIR' => 'SLIC_BASE_THEMES_DIR', + 'SLIC_WP_CONTENT_CONTAINER_DIR' => 'SLIC_BASE_WP_CONTENT_CONTAINER_DIR', + 'COMPOSER_CACHE_DIR' => 'SLIC_BASE_COMPOSER_CACHE_DIR', + 'SLIC_SCRIPTS' => 'SLIC_BASE_SCRIPTS', + ]; +} + +/** + * Switches the active target and recreates running PHP services when its bind mounts differ. * * @param string $target Target to switch to. */ @@ -483,6 +564,29 @@ function slic_switch_target( $target ) { $run_settings_file = "{$root}/.env.slic.run"; $target_relative_path = ''; $subdir = ''; + $full_target = $target; + $run_environment = is_file( $run_settings_file ) ? read_env_file( $run_settings_file ) : []; + $mount_keys = slic_target_mount_keys(); + $previous_mounts = []; + $base_mounts = []; + + foreach ( $mount_keys as $key ) { + $previous_mounts[ $key ] = $run_environment[ $key ] ?? getenv( $key ); + } + + foreach ( slic_target_mount_base_keys() as $key => $base_key ) { + $base_value = getenv( $base_key ); + if ( + ! in_array( $key, [ 'COMPOSER_CACHE_DIR', 'SLIC_SCRIPTS' ], true ) + || ( 'COMPOSER_CACHE_DIR' === $key && '1' === getenv( 'SLIC_COMPOSER_CACHE_IS_EXPLICIT' ) ) + ) { + $base_mounts[ $base_key ] = $base_value; + if ( 'COMPOSER_CACHE_DIR' === $key ) { + $base_mounts['SLIC_COMPOSER_CACHE_IS_EXPLICIT'] = 1; + } + } + putenv( "{$key}={$base_value}" ); + } if ( slic_here_is_site() ) { $target_relative_path = get_target_relative_path( $target ); @@ -492,16 +596,134 @@ function slic_switch_target( $target ) { list( $target, $subdir ) = explode( '/', $target ); } - $env_values = [ + $env_values = slic_site_target_environment( $target ); + $target_path = get_project_local_path( $full_target ); + + // Automatic site-layout values are defaults; target-local configuration remains authoritative. + if ( file_exists( $target_path . '/.env.slic.local' ) ) { + load_env_file( $target_path . '/.env.slic.local' ); + } + + foreach ( $mount_keys as $key ) { + $env_values[ $key ] = getenv( $key ); + } + + $env_values = array_merge( $env_values, $base_mounts, [ 'SLIC_CURRENT_PROJECT' => $target, 'SLIC_CURRENT_PROJECT_CONTAINER_PATH' => get_project_container_path( $target . ( $subdir ? '/' . $subdir : '' ) ), 'SLIC_CURRENT_PROJECT_RELATIVE_PATH' => $target_relative_path, 'SLIC_CURRENT_PROJECT_SUBDIR' => $subdir, - ]; + ] ); + + foreach ( $env_values as $key => $value ) { + putenv( "{$key}={$value}" ); + } + + /* + * Site, plugin, and theme targets in `wp/` skeletons can require different WordPress and theme bind mounts. Docker + * cannot apply those changes with a normal container restart, so recreate only the PHP services that are already + * running. `--no-deps` keeps the database and the rest of the stack intact. + */ + $current_mounts = []; + foreach ( $mount_keys as $key ) { + $current_mounts[ $key ] = getenv( $key ); + } + + if ( $previous_mounts !== $current_mounts ) { + $running_services = array_values( array_filter( array_keys( php_services() ), static function ( $service ) { + return service_running( $service ); + } ) ); + + if ( $running_services ) { + if ( 0 !== slic_realtime()( array_merge( [ 'rm', '--stop', '--force' ], $running_services ) ) ) { + echo magenta( 'Could not remove the PHP services after their bind mounts changed.' . PHP_EOL ); + exit( 1 ); + } + + if ( 0 !== slic_realtime()( array_merge( [ 'up', '--wait', '--no-deps' ], $running_services ) ) ) { + echo magenta( 'Could not recreate the PHP services with their new bind mounts.' . PHP_EOL ); + exit( 1 ); + } + } + } write_env_file( $run_settings_file, $env_values, true ); +} - setup_slic_env( $root ); +/** + * Selects the WordPress mount used by a site target. + * + * Plugins and themes in a site that installs WordPress below its root retain Slic's isolated WordPress installation; + * their existing WPLoader configurations expect core at `/var/www/html`. The site target still mounts the complete + * project so its root configuration and custom content directory remain available. + * + * @param string $target Target to switch to. + * + * @return array Environment values to persist. + */ +function slic_site_target_environment( $target ) { + if ( ! slic_here_is_site() ) { + return []; + } + + $site_dir = realpath( getenv( 'SLIC_HERE_DIR' ) ); + $target_type = get_target_content_type( $target ); + $isolated = 'site' !== $target_type && site_wordpress_is_in_subdirectory( $site_dir ); + $plugins_dir = getenv( 'SLIC_SITE_PLUGINS_DIR' ) ?: getenv( 'SLIC_PLUGINS_DIR' ); + $themes_dir = getenv( 'SLIC_SITE_THEMES_DIR' ) ?: dirname( $plugins_dir ) . '/themes'; + + if ( $isolated ) { + $wp_dir = ensure_dir( root( '/_wordpress' ) ); + $wp_content_dir = ensure_dir( $wp_dir . '/wp-content' ); + $content_path = '/var/www/html/wp-content'; + $themes_dir = 'plugin' === $target_type ? ensure_dir( $wp_content_dir . '/themes' ) : $themes_dir; + } else { + $wp_dir = $site_dir; + $wp_content_dir = dirname( $plugins_dir ); + $content_path = '/var/www/html/' . basename( $wp_content_dir ); + } + + $env_values = [ + 'SLIC_MU_PLUGINS_DIR' => $wp_content_dir . '/mu-plugins', + 'SLIC_PLUGINS_DIR' => $plugins_dir, + 'SLIC_WP_CONTENT_CONTAINER_DIR' => $content_path, + 'SLIC_WP_DIR' => $wp_dir, + 'SLIC_THEMES_DIR' => $themes_dir, + ]; + + foreach ( $env_values as $key => $value ) { + putenv( "{$key}={$value}" ); + } + + return $env_values; +} + +/** + * Returns whether a site installs WordPress below its project root. + * + * @param string $site_dir Site root directory. + * + * @return bool + */ +function site_wordpress_is_in_subdirectory( $site_dir ) { + if ( file_exists( $site_dir . '/wp-load.php' ) ) { + return false; + } + + if ( file_exists( $site_dir . '/wp/wp-load.php' ) ) { + return true; + } + + try { + $composer_json = project_get_composer( $site_dir ); + } catch ( \Exception $e ) { + return false; + } + + $requires_wordpress = ! empty( $composer_json['require']['johnpbloch/wordpress-core'] ); + $install_dir = $composer_json['extra']['wordpress-install-dir'] ?? ''; + + return $requires_wordpress && is_string( $install_dir ) && '' !== trim( $install_dir, './\\' ); } /** @@ -619,6 +841,12 @@ function start_all_services() { * */ function slic_plugins_dir( $path = '' ) { + if ( slic_here_is_site() ) { + $plugins_dir = getenv( 'SLIC_SITE_PLUGINS_DIR' ) ?: slic_content_type_dir( 'plugins' ); + + return empty( $path ) ? $plugins_dir : $plugins_dir . '/' . ltrim( $path, '\\/' ); + } + return slic_content_type_dir( 'plugins', $path ); } @@ -631,6 +859,12 @@ function slic_plugins_dir( $path = '' ) { * */ function slic_themes_dir( $path = '' ) { + if ( slic_here_is_site() ) { + $themes_dir = getenv( 'SLIC_SITE_THEMES_DIR' ) ?: dirname( slic_plugins_dir() ) . '/themes'; + + return empty( $path ) ? $themes_dir : $themes_dir . '/' . ltrim( $path, '\\/' ); + } + return slic_content_type_dir( 'themes', $path ); } @@ -654,7 +888,7 @@ function slic_mu_plugins_dir( $path = '' ) { * */ function slic_content_type_dir( $content_type = 'plugins', $path = '' ) { - $content_type_dir = getenv( 'SLIC_' . strtoupper( $content_type ) . '_DIR' ); + $content_type_dir = getenv( 'SLIC_' . str_replace( '-', '_', strtoupper( $content_type ) ) . '_DIR' ); $root_dir = root(); if ( 'plugins' === $content_type ) { @@ -668,7 +902,7 @@ function slic_content_type_dir( $content_type = 'plugins', $path = '' ) { if ( empty( $content_type_dir ) ) { // Use the default directory in slic repository. $dir = $root_dir . $default_path; - } elseif ( is_dir( $content_type_dir ) ) { + } elseif ( is_dir( $content_type_dir ) || 0 === strpos( $content_type_dir, '/' ) ) { // Use the specified directory. $dir = $content_type_dir; } else { @@ -861,6 +1095,12 @@ function slic_info() { 'CI', 'TRAVIS_CI', 'COMPOSER_CACHE_DIR', + 'SLIC_BASE_COMPOSER_CACHE_DIR', + 'SLIC_BASE_PLUGINS_DIR', + 'SLIC_BASE_SCRIPTS', + 'SLIC_BASE_THEMES_DIR', + 'SLIC_BASE_WP_CONTENT_CONTAINER_DIR', + 'SLIC_BASE_WP_DIR', 'CONTINUOUS_INTEGRATION', 'GITHUB_ACTION', 'SLIC_PHP_VERSION', @@ -876,6 +1116,11 @@ function slic_info() { 'SLIC_GIT_DOMAIN', 'SLIC_GIT_HANDLE', 'SLIC_HERE_DIR', + 'SLIC_MU_PLUGINS_DIR', + 'SLIC_SITE_PLUGINS_DIR', + 'SLIC_SITE_THEMES_DIR', + 'SLIC_SCRIPTS', + 'SLIC_WP_CONTENT_CONTAINER_DIR', 'SLIC_PLUGINS_DIR', 'SLIC_THEMES_DIR', 'SLIC_WP_DIR', @@ -996,9 +1241,16 @@ function slic_handle_composer_cache( callable $args ) { if ( 'unset' === $toggle ) { // Pick it up from env, if possible, or use the default one. $value = env_var_backup( 'COMPOSER_CACHE_DIR', cache( '/composer' ) ); - - write_env_file( $run_settings_file, [ 'COMPOSER_CACHE_DIR' => $value ], true ); } + $explicit = 'unset' !== $toggle; + + write_env_file( $run_settings_file, [ + 'COMPOSER_CACHE_DIR' => $value, + 'SLIC_BASE_COMPOSER_CACHE_DIR' => $value, + 'SLIC_COMPOSER_CACHE_IS_EXPLICIT' => $explicit ? 1 : 0, + ], true ); + putenv( "SLIC_BASE_COMPOSER_CACHE_DIR={$value}" ); + putenv( 'SLIC_COMPOSER_CACHE_IS_EXPLICIT=' . ( $explicit ? '1' : '0' ) ); echo 'Composer cache directory: ' . ( $value ? light_cyan( $value ) : magenta( 'not set' ) ); @@ -1122,7 +1374,9 @@ function xdebug_status() { echo 'Host: ' . light_cyan( 'http://localhost' . ( $localhost_port === '80' ? '' : ':' . $localhost_port ) ) . PHP_EOL; echo colorize( 'Path mapping (host => server): ' . slic_plugins_dir() - . ' => /var/www/html/wp-content/plugins' ) . PHP_EOL; + . ' => ' + . ( getenv( 'SLIC_WP_CONTENT_CONTAINER_DIR' ) ?: '/var/www/html/wp-content' ) + . '/plugins' ) . PHP_EOL; echo colorize( 'Path mapping (host => server): ' . slic_wp_dir() . ' => /var/www/html' );