Problem/Motivation

When Drupal in installed in a non-standard installation structure, DRUPAL_ROOT is defined incorrectly and various parts of Drupal don't work.

This is important because the standard way of developing a Composer package, by defining a path repository in a project to symlink a git clone of the package, isn't currently possible with Drupal core.

Definitions

- project root: the folder where the root composer.json is
- app root (also drupal root): The folder where Drupal's entry-point index.php is. This is where the HTTP request to Drupal is made. Typically PROJECT_ROOT/web.
- scaffolding: The process which copies files into the app root when Drupal is installed with Composer. This is done by a Composer plugin within Drupal.
- Composer symlinking: The technique of using a 'path' repository for a package which points to a local git clone of the package. This causes Composer to symlink the package from the git clone into its location in the project. This is a method used to develop a package which needs to be used in the context of a Composer project -- such as Drupal core.

There are various kinds of installation structures for Drupal, depending on where and how Drupal core is installed:

- drupal/recommended-project: drupal scaffolded into the web/ folder.
- plain git clone of Drupal core, with `composer install` run at its root.
- drupal/legacy-project: drupal scaffolded into the project root.
- drupal core symlinked in by Composer, such as with the joachim-n/drupal-core-development-project Composer template. (This is the best way to develop a Composer package within a project -- see https://medium.com/pvtl/local-composer-package-development-47ac5c0e5bb4.)
- drupal installed in vendor/ - this is not possible because of other issues, and will not be fixed here, but this issue should aim to make DRUPAL_ROOT correct for this structure.

What causes the problem

We use Composer to install Drupal into the app root rather than /vendor, and Drupal
expects to find files like settings.php and extensions in the app root. Because of this, code in a Drupal project falls into several 'zones':

- Composer autoloader
- Composer vendor code
- Drupal core
- Drupal scaffolded files such as index.php and update.php
- Drupal settings, files, and extensions

These different zones don't all know how to get hold of code in the other zones:

- The autoloader always knows how to load Composer vendor code, and Drupal core code, because Composer installed it.
- Composer vendor code, e.g. Drush, knows the location of the Composer autoloader relative to itself, because it knows it's in vendor/foo/bar. But it has to make assumptions for the location of Drupal core or scaffolded files.
- Drupal core doesn't know the location of anything, because where it was installed is defined in composer.json. It has to make assumptions about how to get to the autoloader. It does that with a special autoload.php file at the root of the Drupal repository. (For the drupal/recommended-project template, an autoload.php is scaffolded into web/ so that it can go one level up and find the vendor folder. In drupal/legacy-project Drupal's include() loads the repository's autoload.php, and in drupal/recommended-project the same include() loads the scaffolded autoload.php.)
- Drupal core has to make assumptions about how to find its settings.
- Composer vendor code has to make assumptions about how to find Drupal settings. This affects things like Drush and Drupal Console.
- Drupal's scaffolded files can know about all code locations, since they are written by the scaffold plugin during Composer installation. During this process we can get the location of anything from Composer, and write it into the scaffolded files.
- Drupal settings.php file: knows the location of scaffolded index.php, as that is fixed relative to itself.

These assumptions all break in non-standard installation structures. In particular, the use of __DIR__ in these assumptions breaks when Drupal is symlinked into a project.

Fixing this is difficult because Drupal has MANY different entry points:

- HTTP request to the index.php file which is scaffolded into the app root
- HTTP request to scaffolded install.php
- HTTP request to scaffolded update.php
- HTTP request to core/rebuild.php, which is not scaffolded.
- tests run with phpunit
- tests run with run-tests.sh (this case doesn't really count, as run-tests.sh is only meant for Drupal CI, whose installation structure is a known quantity: see #3228531: document run-tests.sh as not intended for public consumption)
- code in a Composer package (e.g. Drush or Console) bookstraps Drupal
- scripts in core/scripts
- HTTP request to core/modules/statistics/statistics.php (yay! special case!)

Proposed resolution

Various approaches have been tried (see old branches in the issue fork). But ultimately, there's only one thing we can rely on: Composer knows the location of everything, because it installs everything.

Asking Composer runtime API every page load is potentially a performance hit, but it's simple to have Composer write a file containing the data we need during Composer installation.

So the plan is as follows:

1. For Composer vendor code which needs to find Drupal, add a new Composer plugin which is inside core, whose sole job is to write the locations class file into the plugin's own folder. Make the plugin required by core, so it's always present, even in projects that don't use the scaffold plugin. And nobody else needs to worry about defining it, as to change the location values you set them in composer.json.

The file is written as a class, so that it can always be loaded by Composer. (Making it a plain file registered as a Composer autoload 'file' item causes problems with package dependency hierarchy.) This class currently only defines one constant, but #3208975: split the concept of DRUPAL_ROOT/app root into app root and Drupal web root could expand on that.

This plugin should always be installed with Drupal, whether using the drupal/recommended-project or drupal/legacy-project templates, or running Composer on a git clone of core.

Deprecate the $app_root parameter to DrupalKernel, since DrupalKernel uses the plugin.

2. For code in Drupal core which needs to get to the autoloader, replace all uses of __DIR__ with code which uses the actual path of the executed file. This is to prevent PHP from resolving symlinks.

API changes

- New Composer plugin in Drupal core
- new DrupalLocations class, which 3rd party code can use to find Drupal's location
- DrupalKernel's $app_root parameter in various methods is deprecated
- DrupalKernel::guessApplicationRoot is deprecated and renamed. This is protected but Drush uses it, for instance.

Original summary

I usually link drupal inside my www directory like seen in the following ls output.
I think symlinking like that is not unusual, as it helps with version controlling of own projects.

core -> ../drupalcheckout/core
.htaccess
../drupalcheckout/index.php
../drupalcheckout/modules
profiles
robots.txt -> ../drupalcheckout/robots.txt
sites
themes -> ../drupalcheckout/themes

i've attached a script which will setup such an environment in the current directory.

With this setup BASE_ROOT in install.php will be set to the drupalcheckout directory.
Install then tries to find a settings.php in ../drupalcheckout/sites/default and not ./sites/default.

same Problem and fix should be considered for update.php and authorize.php

related discussions:
#1055856: Optimize settings.php discovery
#484554: Stop relying on Apache for determining the current path

CommentFileSizeAuthor
#198 1792310-nr-bot.txt150 bytesneeds-review-queue-bot
#133 interdiff.txt269 bytesKapilV
#133 1792310-133.patch1.85 KBKapilV
#130 composer-test-1792310.json_.txt2.46 KBjoachim
#94 CORE__core-link.1792310-interdiff-93-94.txt2.78 KBAdamPS
#94 CORE__core-link.1792310-94.patch9.67 KBAdamPS
#93 CORE__core-link.1792310-interdiff-92-93.txt1.11 KBAdamPS
#93 CORE__core-link.1792310-93.patch13.1 KBAdamPS
#92 CORE__core-link.1792310-interdiff-91-92.txt1.04 KBAdamPS
#92 CORE__core-link.1792310-92.patch13.03 KBAdamPS
#91 CORE__core-link.1792310-91.patch13.03 KBAdamPS
#90 CORE__core-link.1792310-interdiff-80-90.txt8.6 KBAdamPS
#90 CORE__core-link.1792310-90.patch6.96 KBAdamPS
#81 CORE__core-link.1792310-80.patch7.13 KBAdamPS
#80 CORE__core-link.1792310-interdiff-79-80.txt5.56 KBAdamPS
#79 CORE__core-link.1792310-interdiff-78-79.txt2.07 KBAdamPS
#79 CORE__core-link.1792310-79.patch2.95 KBAdamPS
#78 CORE__core-link.1792310-78.patch1.35 KBAdamPS
#75 drupal_root_chdir.1792310-75.patch3.96 KBAdamPS
#73 drupal_root_chdir.1792310-73.patch3.66 KBAdamPS
#56 patch_commit_8e1f5ca618b8.patch4.34 KBomega8cc
#50 1792310-50-drupal_root_chdir.patch2.47 KBderhasi
#43 1792310-43.patch1.53 KBtstoeckler
#34 1792310.patch1.8 KBRobLoach
#29 chdir_drupal_root-1792310-29.patch1.11 KBpatrickd
#27 01792310-authorizeinstallupdate.phpforsymlinkedcoredirectory_3.patch1.1 KBderEremit
#26 01792310-authorizeinstallupdate.phpforsymlinkedcoredirectory_3.patch1.1 KBderEremit
#25 01792310-authorizeinstallupdate.phpforsymlinkedcoredirectory_3.patch1.1 KBderEremit
#23 01792310-authorizeinstallupdate.phpforsymlinkedcoredirectory_2.patch1.29 KBderEremit
#15 0001-fix-authorizeinstallupdate.phpforsymlinkedcoredirectory_1.patch1.49 KBderEremit
#3 01792310-install.php-has-wrong-DRUPAL_ROOT.patch680 bytesderEremit
#2 01792310-install.php-has-wrong-DRUPAL_ROOT.patch680 bytesderEremit
#1 drupalsymlinksetup.txt342 bytesderEremit

Issue fork drupal-1792310

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

derEremit’s picture

FileSize
342 bytes
derEremit’s picture

Status: Needs review » Patch (to be ported)
FileSize
680 bytes
derEremit’s picture

Status: Patch (to be ported) » Needs review
FileSize
680 bytes

sorry, wrong issue state.
Fixed issue by using $_SERVER['SCRIPT_FILENAME'] as base for finding correct path for chdir.

patrickd’s picture

Title: install.php has wrong DRUPAL_ROOT when core directory is a symbolic link » install.php has wronhttp://www.youtube.com/watch?feature=player_embedded&v=Rg DRUPAL_ROOT when core directory is a symbolic link

Confirming the problem, patch corrects it

but I'm not sure whether this is the correct way to handle it

patrickd’s picture

Title: install.php has wronhttp://www.youtube.com/watch?feature=player_embedded&v=Rg DRUPAL_ROOT when core directory is a symbolic link » install.php has wrong DRUPAL_ROOT when core directory is a symbolic link

oops, pasted my clipboard contents into title,
correcting xD

leschekfm’s picture

Status: Needs review » Needs work

Hi,
I think you should at least add a comment to the patch, why you had to do this and what you do.
Another thing: why do you use '-17' as parameter? Is this constant? If so I would suggest to define one.

Nonetheless thanks for your patch :)

webflo’s picture

Please review the patch in #1540136: Simplify index.php by moving DRUPAL_ROOT to bootstrap.inc. Maybe it fixes this issue too.

Carsten Müller’s picture

I think #1540136: Simplify index.php by moving DRUPAL_ROOT to bootstrap.inc is sovling this issue too. I just tested it by using symlinks for the core files/folder in my setup and it worked fine. So i think this issue can be closed (duplicate)

leschekfm’s picture

With a setup as mentioned in #1 http://drupal.org/files/1540136_4.patch is not solving this issue.

Carsten Müller’s picture

leschekfm is right

@see also https://bugs.php.net/bug.php?id=46260

correct path is given by $_SERVER["SCRIPT_FILENAME"]

derEremit’s picture

Priority: Normal » Major
Status: Needs work » Patch (to be ported)

update:
issue still exists
patch still works.

can anybody tell if my patch could cause unwanted sideeffects or should be improved?

tim.plunkett’s picture

Priority: Major » Normal
Status: Patch (to be ported) » Needs review
Issue tags: +Needs backport to D7
derEremit’s picture

needs backport?
not with my setup
issue exists since install.php was moved to /core folder.
so no problem with drupal7

derEremit’s picture

Issue tags: -Needs backport to D7

ok, sorry i accidentally set the wrong patch status. Therefor the confusion

derEremit’s picture

Title: install.php has wrong DRUPAL_ROOT when core directory is a symbolic link » chdir to DRUPAL_ROOT not working when core directory is symbolic link
FileSize
1.49 KB

update issue title and patch to fix install.php authorize.php and update.php as they need no seperate discussion.

RobLoach’s picture

Note that there's also #1540136: Simplify index.php by moving DRUPAL_ROOT to bootstrap.inc, which cleans up some of this stuff. Might want to get that in before this one, since then the changes presented in #15 would just need to happen in one place rather than a few places.

derEremit’s picture

I agree:
when #1540136: Simplify index.php by moving DRUPAL_ROOT to bootstrap.inc gets commited this patch has to be restructured any way.
testet the patch in #1540136: Simplify index.php by moving DRUPAL_ROOT to bootstrap.inc and it makes things even worse for my setup mentioned in the issue summary.
I don't know if we can set this issue to critical.

derEremit’s picture

What we need now is test-results for different servers / operating systems.
apache, nginx, IIS..
As i was told $_server['scriptname'] could not be reliable across different platforms.
I tested successfully under various ubuntu versions with apache2.

RobLoach’s picture

#1540136: Simplify index.php by moving DRUPAL_ROOT to bootstrap.inc is in. We could probably simplify the chdir() usage here.

derEremit’s picture

my proposal for using script_filename to do directory detection could at least affect fastcgi setups

see:
http://serverfault.com/questions/15031/can-nginx-handle-php-or-similar-f...

seems with fastcgi you have to expicitly set script_filename, so questions:

- can we assume a fastcgi setup as advanced and mention setting up script_filename in install.txt
- can we have testcases for installing drupal on different environments (and with a symlinked setup)?

RobLoach’s picture

Status: Needs review » Needs work
derEremit’s picture

Status: Needs work » Needs review
FileSize
1.29 KB

just removed a newline in the patch so it could apply

Status: Needs review » Needs work
derEremit’s picture

derEremit’s picture

Status: Needs work » Needs review
FileSize
1.1 KB
derEremit’s picture

RobLoach’s picture

dirname(__DIR__) doesn't work?

patrickd’s picture

We can't use "__DIR__" because it works with symlinks the same way as ".." would

But using dirname() is a good idea to get rid of the ugly substr() stuff!

Status: Needs review » Needs work

The last submitted patch, chdir_drupal_root-1792310-29.patch, failed testing.

patrickd’s picture

Status: Needs work » Needs review

#29: chdir_drupal_root-1792310-29.patch queued for re-testing.

derEremit’s picture

thought about using dirname, beauty of code vs speed? actually don't know if dirname is slower than substr. but its one function call more ;)

patrickd’s picture

beauty (readability) wins over optimization and (if actually necessary and probably not in this case) follows later.

dirname() does not need hard drive access, it's basically just stripping away anything from the end of the string until the last slash.. so it performs probably quite similar to substr()

RobLoach’s picture

FileSize
1.8 KB

Probably could apply the same pattern to statistics.php too.

Status: Needs review » Needs work

The last submitted patch, 1792310.patch, failed testing.

patrickd’s picture

Status: Needs work » Needs review

#34: 1792310.patch queued for re-testing.

ultimateboy’s picture

Just confirming that the patch in #34 does indeed fix issues when core files are symlinked. The code looks sane to me, but I'll wait for another pair or two of eyes before RTBC.

ultimateboy’s picture

#34: 1792310.patch queued for re-testing.

brianmercer’s picture

The patch at #34 doesn't fix my problem with a symlink setup using nginx. I have a similar directory structure to the op. The problem on install is finding the settings.php file. The problem with symlinks is caused by moving from the use of cwd() in D7 to __DIR__ in D8. __DIR__ resolves symlinks.

Using

chdir(dirname(dirname($_SERVER['SCRIPT_FILENAME'])));

is fine for /core/install.php but it still can't find the settings.php file because that's found using get_conf_path which uses DRUPAL_ROOT which is set in bootstrap.inc using __DIR__ now.

If I change bootstrap.inc to

define('DRUPAL_ROOT', dirname(dirname($_SERVER['SCRIPT_FILENAME'])));

then it will work fine for /core/install.php but then when it moves to regular operation with /index.php it's one directory too high.

And that's just trying to get it to find settings.php. I haven't looked at how it finds the /files directory.

derEremit’s picture

there's a discussion of putting settings.php in drupal root. Perhaps that would help
#1757536: Move settings.php to /settings directory, fold sites.php into settings.php

tstoeckler’s picture

34: 1792310.patch queued for re-testing.

Status: Needs review » Needs work

The last submitted patch, 34: 1792310.patch, failed testing.

tstoeckler’s picture

Here's a re-roll.

Does not include the change to the require_once in statistics.php, as we do require_once using __DIR__ all over the place. So if that causes problems, we should be changing that everywhere else as well.

Status: Needs review » Needs work

The last submitted patch, 43: 1792310-43.patch, failed testing.

tstoeckler’s picture

Very strange, this might be a random fail:

ImageFieldDisplayTest.php, line 229
ImageFieldDisplayTest.php, line 251

array_flip(): Can only flip STRING and INTEGER values!
in FieldableDatabaseStorageController.php, line 212

tstoeckler’s picture

Status: Needs work » Needs review

43: 1792310-43.patch queued for re-testing.

tstoeckler’s picture

Issue tags: +Random fail

Anyone want to RTBC?

sun’s picture

Issue tags: -Random fail +Random test failure
sun’s picture

derhasi’s picture

FileSize
2.47 KB

I stumpled upon this issue too, when symlinking the core directory.

My structure looks like:

core -> ../drupal/core
index.php
sites

I tried to apply the patch from #43, but as update.php is not present anymore, that patch failed.

As mentioned by @brianmercer in #39 , the current DRUPAL_ROOT implementation (using __DIR__) is not working for the same reasons this issue exists. I see a solution in reclaiming getcwd() for setting DRUPAL_ROOT. This way the calling script (install.php, rebuild.php, ...) are responsible for setting the working directory to the drupal root (as they currently already try to do). I tried to document that in extending the comment on define('DRUPAL_ROOT', ... );.

So I attach a rerolled patch

  • without updatephp (as the file does not exist anymore)
  • with the changes to defining DRUPAL_ROOT
derhasi’s picture

tstoeckler’s picture

Status: Needs review » Reviewed & tested by the community

Yes, this in fact looks correct to me.

Looking back at the referenced issue this is how it was before, so - even if that has been closed for quite a while - the fact that this is how it is in previous Drupal versions should be proof enough that it's safe and works under any circumstances. The __DIR__ trick introduced there was neat but since it doesn't work with symlinks there's no reason we shouldn't just go back to what is proven to work.

The $_SERVER['SCRIPT_FILENAME'] has also been confirmed by enough people above.

So - although it obviously wouldn't for more people to take a look at this, as always - I see no reason why this shouldn't go in as is, for Rewiewed & tested by the community it truely is. :-)

Edit: I did upload a patch in this issue but that was a mere re-roll, so I don't think I'm violating any codex here...

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

This looks good to me - I was wondering about the scripts in core/scripts - I think they might need work too. They are tricky since you have no idea where the user is running them from :(

mikeytown2’s picture

If you have no idea where the scripts are running from but you do know it's inside of drupal and your trying to find the drupal root I use this bit of code in D7 to accomplish the task.

  $webroot = str_replace('\\', '/', realpath(dirname($_SERVER['SCRIPT_FILENAME'])));
  while (!empty($webroot)) {
    if (file_exists($webroot . '/index.php') && strpos(file_get_contents($webroot . '/index.php'), 'menu_execute_active_handler();') !== FALSE) {
      break;
    }
    $new_webroot = str_replace('\\', '/', dirname($webroot));
    if ($new_webroot == $webroot) {
      // At the top level, exit as we're outside of where we should be.
      exit;
    }
    $webroot = $new_webroot;
  }
  chdir($webroot);

I do think that the patch in #50 is ready to go and any additional issues can be handled in a followup. So my vote is to put this issue back to RTBC.

omega8cc’s picture

Status: Needs work » Needs review
FileSize
4.34 KB

More complete patch attached (tested already).

Status: Needs review » Needs work

The last submitted patch, 56: patch_commit_8e1f5ca618b8.patch, failed testing.

omega8cc’s picture

OK, so my extended patch may need further work, even if it worked in several tests with BOA/Aegir: https://github.com/omega8cc/drupal/commits/8.0.x-symlinks-support

RobLoach’s picture

+++ b/core/includes/bootstrap.inc
@@ -208,9 +208,16 @@
+define('DRUPAL_ROOT', getcwd());

Drupal might not always be run from the current working directly. Because of this, I'm against using getcwd().

Alternatively, you could use readlink() to resolve any symlinks. Here's an example:

define('DRUPAL_ROOT', readlink(dirname(dirname(__DIR__))));
omega8cc’s picture

It is an interesting idea to use readlink(foo)! But we should then probably wrap it within is_link(foo) to avoid problems? Here is a quick test on what happens when you use readlink(foo) against regular file and not a symlink:

quad:~# touch a
quad:~# ln -s a b
quad:~# ls -l {a,b}
-rw-r--r-- 1 root root 0 Dec 27 01:38 a
lrwxrwxrwx 1 root root 1 Dec 27 01:38 b -> a
quad:~# php -r "var_dump(readlink('b'));"
string(1) "a"
quad:~# php -r "var_dump(readlink('a'));"
bool(false)
quad:~# php -r "var_dump(readlink('c'));"
bool(false)
quad:~# php -v
PHP 5.5.20 (cli) (built: Dec 26 2014 21:49:06)
omega8cc’s picture

Unfortunately this doesn't work with symlinked /core

define('DRUPAL_ROOT', readlink(dirname(dirname(__DIR__))));

The reason is simple: it is not the root directory what can be symlinked, but rather /core, or /index.php etc. so we should rather try to detect if /core (or something else?) is symlinked:

if (is_link(getcwd() . '/core')) {
  define('DRUPAL_ROOT', getcwd());
}
else {
  define('DRUPAL_ROOT', dirname(dirname(__DIR__)));
}
omega8cc’s picture

Then also this is required to make it work with symlinked /core

    // Include our bootstrap file.
    if (is_link(getcwd() . '/core')) {
      $core_root = getcwd() . '/core';
    }
    else {
      $core_root = dirname(dirname(dirname(__DIR__)));
    }
omega8cc’s picture

Of course it doesn't make any sense to check if getcwd() . '/core' is a symlink if we don't want to trust getcwd() for some reason. But in my tests using getcwd() is much more reliable than unpredictable dirname(dirname(__DIR__)), or worse yet, ('..'), so why not to use it instead? It is already used in other places, like scripts and tests.

omega8cc’s picture

We could probably re-use some logic used in Drush to detect Drupal root in the symlinks-aware mode:

/**
 * Exhaustive depth-first search to try and locate the Drupal root directory.
 * This makes it possible to run drush from a subdirectory of the drupal root.
 *
 * @param
 *   Search start path. Defaults to current working directory.
 * @return
 *   A path to drupal root, or FALSE if not found.
 */
function drush_locate_root($start_path = NULL) {
  $drupal_root = FALSE;

  $start_path = empty($start_path) ? drush_cwd() : $start_path;
  foreach (array(TRUE, FALSE) as $follow_symlinks) {
    $path = $start_path;
    if ($follow_symlinks && is_link($path)) {
      $path = realpath($path);
    }
    // Check the start path.
    if (drush_valid_drupal_root($path)) {
      $drupal_root = $path;
      break;
    }
    else {
      // Move up dir by dir and check each.
      while ($path = _drush_shift_path_up($path)) {
        if ($follow_symlinks && is_link($path)) {
          $path = realpath($path);
        }
        if (drush_valid_drupal_root($path)) {
          $drupal_root = $path;
          break 2;
        }
      }
    }
  }

  return $drupal_root;
}
/**
 * Returns the current working directory.
 *
 * This is the directory as it was when drush was started, not the
 * directory we are currently in. For that, use getcwd() directly.
 */
function drush_cwd() {
  if ($path = drush_get_context('DRUSH_OLDCWD')) {
    return $path;
  }
  // We use PWD if available because getcwd() resolves symlinks, which
  // could take us outside of the Drupal root, making it impossible to find.
  // $_SERVER['PWD'] isn't set on windows and generates a Notice.
  $path = isset($_SERVER['PWD']) ? $_SERVER['PWD'] : '';
  if (empty($path)) {
    $path = getcwd();
  }

  // Convert windows paths.
  $path = _drush_convert_path($path);

  // Save original working dir case some command wants it.
  drush_set_context('DRUSH_OLDCWD', $path);

  return $path;
}
/**
 * Checks whether given path qualifies as a Drupal root.
 *
 * @param string
 *   Path to check.
 *
 * @return string
 *   The relative path to common.inc (varies by Drupal version), or FALSE if not
 *   a Drupal root.
 */
function drush_valid_drupal_root($path) {
  if (!empty($path) && is_dir($path) && file_exists($path . '/index.php')) {
    // Drupal 8 root. Additional check for the presence of core/composer.json to
    // grant it is not a Drupal 7 site with a base folder named "core".
    $candidate = 'core/includes/common.inc';
    if (file_exists($path . '/' . $candidate) && file_exists($path . '/core/misc/drupal.js') && file_exists($path . '/composer.json')) {
      return $candidate;
    }
    // Drupal 7 root. Additional check for the absence of core/composer.json to
    // grant $path is not core/ of a Drupal 8 root.
    $candidate = 'includes/common.inc';
    if (file_exists($path . '/' . $candidate) && file_exists($path . '/misc/drupal.js') && ((basename($path) != 'core') || !file_exists($path . '/../composer.json'))) {
      return $candidate;
    }
  }
  return FALSE;
}

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

nicholas.alipaz’s picture

wow, this has been languishing here for a while. I think with more d8 traction this is going to become much more of an issue for many.

As an example, I was changing all of my project's dependencies over to be installed via composer today, drupal/core being one of them, and attempting to move all the dependencies out of my site's git repo. In doing so, I need to symlink the core directory into place after I do a composer install during deployments. Works perfect, except for the fact that core doesn't discover the settings properly when it is symlinked from elsewhere on the filesystem.

I have tried out all the patches in this thread, none of which seem to resolve the issue for me. Here is my project's structure (truncating some of the output for brevity):

/var/www/html/current/
|-- composer.json
|-- composer.lock
|-- config
|   `-- prod
|-- docroot
|   |-- ...
|   |-- core -> /var/www/html/shared/docroot/core
|   |-- ...
|   |-- libraries -> /var/www/html/shared/docroot/libraries
|   |-- modules
|   |   |-- contrib -> /var/www/html/shared/docroot/modules/contrib
|   |-- profiles -> /var/www/html/shared/docroot/profiles
|   |-- robots.txt
|   |-- sites
|   |   |-- default
|   |   |   |-- files -> /var/www/html/shared/docroot/sites/default/files
|   |-- themes
|   |-- update.php
|   `-- web.config
|-- drush
|   `-- policy.drush.inc
|-- scripts
|   `-- composer
`-- vendor -> /var/www/html/shared/vendor

I am using https://github.com/drupal-composer/drupal-project for my composer template here, which allows me to get a lot of the code out of the git repo and share it across releases. Would be nice if we could do the same with drupal core.

memtkmcc’s picture

Adding related issue.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

AdamPS’s picture

Version: 8.6.x-dev » 8.8.x-dev
Status: Needs work » Needs review
FileSize
3.66 KB

Problem still exists. Updated patch also fixes DrupalKernel::guessApplicationRoot().

Related issue #1672986: Option to have all php files outside of web root. is less general than this one. It does not address the shared code scenario of #66.

Status: Needs review » Needs work

The last submitted patch, 73: drupal_root_chdir.1792310-73.patch, failed testing. View results

AdamPS’s picture

Tricky. This issue is probably far from ready to commit in the current form as there are too many open questions:

  • What about /core/scripts?
  • Problems with tests.
  • Increased reliance on getcwd which might not be set.
  • $_SERVER["SCRIPT_FILENAME"] might not be set for some web servers?

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

AdamPS’s picture

Title: chdir to DRUPAL_ROOT not working when core directory is symbolic link » Error when core directory is a symbolic link
Status: Needs work » Needs review
FileSize
1.35 KB

Here is a much simpler patch. As far as I know it has no downsides for existing sites. It adds support for symbolic links when running the site via index.php. With this patch, the DrupalKernel class now correctly obeys the $app_root parameter.

It doesn't work for update.php, authorize.php, install.php or scripts - but if you are trying to lock down a site as is a likely case for moving the core directory out of the web root then you should most likely delete all those files. Instead run database updates via drush, code installs via composer, etc.

It is likely impossible to add tests for this patch because the test bot cannot create the required scenario where core the directory is a symbolic link.

AdamPS’s picture

AdamPS’s picture

Title: Error when core directory is a symbolic link » Wrong DRUPAL_ROOT with non-standard code structure
Issue summary: View changes
FileSize
5.56 KB

Improved patch and issue summary update

AdamPS’s picture

voleger’s picture

Why not to try to use drupal/core-composer-scaffold approach where the web root property defined in the extras section.
That approach would be configurable per Composer template.

AdamPS’s picture

Why not to try to use drupal/core-composer-scaffold approach where the web root property defined in the extras section.

Thanks for the suggestion. However I think that solves a different problem. It seems to me that setting the web root property will move the web root to a different directory DD and then install core into DD. The idea of this issue is to take core out of the webroot.

I don't see how a composer plugin can solve this requirement. It's the job of composer to define where files will be installed. This issue is the next step: if the core files are installed outside of the webroot then core doesn't work.

Or have I missed something??

voleger’s picture

A lot of functionality tied up on the DRUPAL_ROOT: describes where the webroot and the core folder are located. For backward compatibility, we can define DRUPAL_ROOT constant based on the composer.json settings definitions. BUT that change also requires to provide additional info:

- web_root - related path to the folder where index.php, the other scripts, and other publicly accessible files are located. Currently, this value equal to the DRUPAL_ROOT;
- core_root - related path to the core package location. Currently, this value equal to the DRUPAL_ROOT. But if we move the core package to the vendor directory, then that will break paths for projects which directly relies on the core files, i.e., legacy include files;
- config_root - directory for site/multisite settings as they are currently located at sites/*/settings.php and related files (also yml config files can be stored here). It would be more securely if the configuration would be located out of web_root;
- assets_root - for files accessible for web request, contains the files located at sites/default/files (css, js, uploaded files)
- extentions_root - where the proper custom extensions (modules, profiles, themes) would be located. Extensions naming issues currently prevent moving extentions into the vendor directory, so this would be required first step in direction to move extensions out of web_root.

Benefits:
- definitions from composer file would be available immediately after including autoload file before initiating of kernel instance;
- more granular configuration which will allow moving the things around as that requires a particular project;

Conclusion: I know that the scope of the issue to change the way how DRUPAL_ROOT is defined, but I think Drupal has to introduce more specific location configurations to achieve that project flexibility as a lot of developers wants to achieve.

AdamPS’s picture

Conclusion: I know that the scope of the issue to change the way how DRUPAL_ROOT is defined, but I think Drupal has to introduce more specific location configurations to achieve that project flexibility as a lot of developers wants to achieve.

It seems like a fine idea but I would say it is a separate issue and a feature request - creating something new and more flexible likely needing many changes throughout the code and even perhaps in modules (maybe not BC).

This issue is a bug fix - taking the existing method and making it work better with a simple and safe change that can easily be committed now. It doesn't seem to in any way prevent what you want to do so perhaps you can support it?

AdamPS’s picture

In fact the suggestion in #84 seems much more like the related issue #1672986: Option to have all php files outside of web root..

voleger’s picture

Status: Needs review » Reviewed & tested by the community

Thanks for pointing that.
So, regarding the latest patch: I see that chdir calls no more uses related paths, and relay on the DRUPAL_ROOT value. And approach much simpler that in previous patches.
+1 for RTBC

AdamPS’s picture

Great thanks @voleger

alexpott’s picture

Status: Reviewed & tested by the community » Needs review
+++ b/core/assets/scaffold/files/index.php
@@ -11,6 +11,7 @@
+define('DRUPAL_ROOT', __DIR__);
 $autoloader = require_once 'autoload.php';
 
 $kernel = new DrupalKernel('prod', $autoloader);

+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -293,6 +293,9 @@ public function __construct($environment, $class_loader, $allow_dumping = TRUE,
     $this->root = $app_root;
+    if (!defined('DRUPAL_ROOT')) {
+      define('DRUPAL_ROOT', $app_root);
+    }

One thing that seems confusing is why propagate DRUPAL_ROOT everywhere? Given the changes to DrupalKernel - should we not be passing in the root to the DrupalKernel constructor. We'd then have way less defines around.

The other thing that reading this patch seems to indicate - is that we have two sources of truth as to the app root at the moment. DRUPAL_ROOT and the app root that's in the DrupalKernel - and can be set on construction. Having two sources of truth that have to be aligned makes this more painful than it should be. For example, I think that we really need to pass along the app root in \Drupal\Core\DrupalKernel::initializeSettings() to \Drupal\Core\DrupalKernel::findSitePath() - not doing that makes this more complex than it should be.

AdamPS’s picture

Having two sources of truth that have to be aligned makes this more painful than it should be

I absolutely agree with that! I just noticed we have a 3rd source of truth: \Drupal::root(). However it's not a situation created by this patch so we just have to deal with it as best we can.

One thing that seems confusing is why propagate DRUPAL_ROOT everywhere?

OK I get it now, thanks for explaining. The new patch minimises use of DRUPAL_ROOT.

AdamPS’s picture

The more I look the more instances I find!

This patch attempts to fix all places where $app_root has been defaulted ignoring tests and scripts.

AdamPS’s picture

AdamPS’s picture

Sorry for all the failures. I've done some more testing this time so hopefully it will work better.

AdamPS’s picture

This patch removes InstallCommand/ServerCommand because that's the same as scripts. I raised a separate issue for those #3176691: Allow scripts/tests to work with non-standard code structure.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

AdamPS’s picture

Status: Needs review » Reviewed & tested by the community

Great thanks @voleger I reviewed your reroll and agree this is RTBC.

joachim’s picture

Closed #3188703: index.php lets DrupalKernel guess the app root, which doesn't work if Drupal is being symlinked from a repo by Composer as a duplicate.

Tagging as DX, as fixing this will make it much simpler to work on Drupal core with the package symlinked in from a local repo by Composer -- see https://github.com/joachim-n/drupal-core-composer for details.

joachim’s picture

Status: Reviewed & tested by the community » Needs work

Couple of problems I can see:

1.

I don't think the change to core/modules/statistics/statistics.php will work:

> DrupalKernel::createFromRequest(Request::createFromGlobals(), $autoloader, 'prod', TRUE, realpath(''));

realpath() resolves symlinks. So when Drupal code is being symlinked into the webroot, realpath() gives us the real path of the core/modules/statistics/statistics.php file, and so the kernel is created with the wrong app root.

2.

FunctionalTestSetupTrait::prepareEnvironment() creates the test site folder inside the real path, and so the test uses the wrong app root:

  protected function prepareEnvironment() {
    // Bootstrap Drupal so we can use Drupal's built in functions.
    $this->classLoader = require __DIR__ . '/../../../../../autoload.php';
    $request = Request::createFromGlobals();
    $kernel = TestRunnerKernel::createFromRequest($request, $this->classLoader);
    // TestRunnerKernel expects the working directory to be DRUPAL_ROOT.
    chdir(DRUPAL_ROOT);
    $kernel->boot();

TestRunnerKernel::createFromRequest() is being called without an app root, so the app root will be deduced from the *real* location of the file, and won't be in the web root. The kernel then defines the app root based on that, and so the test site folder is created in the wrong place. The test then crashes because later on it *is* looking in the right place for a site folder, but can't find one.

This is important because symlinking Drupal core into a Composer project is a good way to work on core without Composer making changes to core files -- see https://github.com/joachim-n/drupal-core-composer

Other than that, this is working well with the setup from that Composer project.

joachim’s picture

Another problem I can see is that we have two different places that store the app root:

- the DRUPAL_ROOT constant, defined in core/includes/bootstrap.inc
- the $app_root used in DrupalKernel, which is either passed in or guessed, and which can be obtained with getAppRoot()

I'm looking to see if there's an issue anywhere to deprecated DRUPAL_ROOT in favour of getAppRoot(), and I can't see anything.

Adding #2590077: [meta] Decoupling drupal/core from predefined directory structure as a parent issue.

AdamPS’s picture

Another problem I can see is that we have two different places that store the app root:

I agree this is true, but it's not a new problem. This issue doesn't make things any worse than before so I don't see that it should stop this issue from moving forward.

I'm looking to see if there's an issue anywhere to deprecated DRUPAL_ROOT in favour of getAppRoot(), and I can't see anything.

The comment #89 from alexpott helped confirm for me that DRUPAL_ROOT should be considered intended for deprecation.

AdamPS’s picture

Issue summary: View changes

@voleger Thanks for the reroll. However as far as I can see it doesn't cover the comments in #100 so this issue is still "Needs work"

1. I don't think the change to core/modules/statistics/statistics.php will work:

Good point so this still needs fixing I think.

2. FunctionalTestSetupTrait::prepareEnvironment() creates the test site folder inside the real path, and so the test uses the wrong app root:

True. However this issue is intended not to cover changes to tests and I have updated the IS to make that clearer. There is a separate issue for that: #3176691: Allow scripts/tests to work with non-standard code structure. It's not obvious in that case even how to work out the app root.

AdamPS’s picture

Status: Needs work » Reviewed & tested by the community

Correction I think point 1 from #100 is in fact fine.

1. I don't think the change to core/modules/statistics/statistics.php will work:

I can see no way to make it work if statistics.php is accessed via a link.

See IS "proposed resolution":

A few key files will need to remain in the web root without the use of symbolic links - this allows working out the application root by calling __DIR__ within these pages.

joachim’s picture

> I can see no way to make it work if statistics.php is accessed via a link.

I had a thought about this -- we probably need a new PHP file that's scaffolded which acts as a broker for any PHP files inside modules that need to be linked to.

So the actual link would be web/direct_link.php, with a parameter that gets it to load statistics.php.

But that's going to be complex and I'm very happy to leave that to a follow-up!

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

Added some review comments to gitlab

joachim’s picture

> Do we need to require that root is always passed in? Should we deprecation not passing it in. This would allow contrib to update for this change too.

Yes, we should. Because in the situation where Drupal is being symlinked into the webroot, then the code in DrupalKernel (or indeed any file that isn't scaffolded into the webroot) has no way of discovering the webroot location. That's because PHP resolves symlinks for things like __DIR__ and __FILE__ and gives you their real location. (Well, I've just googled and found https://github.com/logical-and/symlink-detective/blob/master/src/Symlink... ... but let's not go there.)

And the consequence of that would be that guessApplicationRoot() is removed.

AdamPS’s picture

Many thanks @joachim and @alexpott. I'm going to drop out of following this one - I no longer have a project that needs it and you both know much more about it than I do.

I had a thought about this -- we probably need a new PHP file that's scaffolded which acts as a broker for any PHP files inside modules that need to be linked to. ... But that's going to be complex and I'm very happy to leave that to a follow-up!

That sounds clever. It could even be for more than just module code - potentially it could be used for any file that isn't already a scaffold file: install.php, authorize.php, etc. This would remove all restrictions, and even the entire core directory could be a symlink.

Do we need to require that root is always passed in? Should we deprecation not passing it in. This would allow contrib to update for this change too.

Yes, also I agree, however I expect we can't do it until after #3176691: Allow scripts/tests to work with non-standard code structure. The IS has a bullet for it under "related tasks", but I never got so far as to raise an issue.

joachim’s picture

> That sounds clever. It could even be for more than just module code - potentially it could be used for any file that isn't already a scaffold file: install.php, authorize.php, etc. This would remove all restrictions, and even the entire core directory could be a symlink.

Thanks! ... but I'm now wondering whether it's just inventing a whole secondary routing system! Which is bad. Given that the scaffolding system is already something that allows files to be declared to it, I think a better approach might be to allow modules to declare files for scaffolding. But this is all for a separate issue.

joachim’s picture

Aha! I've just discovered something interesting and useful!

  dump($class_loader->findFile(DrupalKernel::class));

This produces the location within the app root, that is, without resolving the symlink to its true location!

In my case, I get:

> "/Users/joachim/Sites/drupal-core-composer/vendor/composer/../../web/core/lib/Drupal/Core/DrupalKernel.php"

So anywhere we have the Composer autoloader, we can get the correct app root.

joachim’s picture

I've pushed a commit to the MR which fixes drupal_rebuild().

However, other parts of the code still need fixing.

I like using the class loader like this. It feels nicely elegant. It would mean we could do something like this in DrupalKernel:

      $app_root = static::guessApplicationRoot($class_loader);

and go back to the way the code looked like before this MR -- where most of the time, code lets DrupalKernel figure out the app root.

alexpott’s picture

#111 looks pretty promising to me.

I wonder how much we can rely on the Composer autoloader findFile() method. We only have one place in core that currently does and this is the \Drupal\TestTools\PhpUnitCompatibility\PhpUnit8\ClassWriter::mutateTestBase() so pretty obscure and not part of the regular runtime.

joachim’s picture

> I wonder how much we can rely on the Composer autoloader findFile() method.

Yeah... I was thinking that too, sadly.

All its docs say is:

> Finds the path to the file where the class is defined.

There's no detail as to whether a symlink is resolved or not. So the maintainers of Composer could legitimately say that symlinks being resolved or not is an implementation detail, and one that makes no practical difference to their API, since its purpose is to load files, and the OS won't care whether which version of the filepath you give it when loading a file.

I am also finding it quite complicated figuring out the DrupalKernel class -- it seems to have lots of different ways in, and static methods and instance methods that get called at various times.

For instance, here in drupal_rebuild(), a static method gets called when we've instantiated it:

  $kernel = new DrupalKernel('prod', $class_loader, TRUE, $app_root);
  $kernel->setSitePath(DrupalKernel::findSitePath($request));

We could pass the $app_root back in to findSitePath() as a parameter, which works.

But here in install_check_requirements(), we don't have a kernel instance:

      $site_path = empty($install_state['site_path']) ? DrupalKernel::findSitePath($request, FALSE, dirname(__DIR__, 2)) : $install_state['site_path'];

And then there are weird places like PublicStream:

        // If there is no kernel available yet, we call the static
        // findSitePath().
        $site_path = DrupalKernel::findSitePath(Request::createFromGlobals());

It would be helpful if the comment explained under what circumstances the kernel might not be available here!

All this means that out of the 3 places in DrupalKernel that static::guessApplicationRoot() is called, not all of them have access to the class loader.

However! I have a totally new plan!

We define the DRUPAL_ROOT constant in bootstrap.inc, using __DIR__ and the assumption that the file is in its standard place.

And bootstrap.inc is loaded by Composer's autoloader, by being specified as an autoload file in core\composer.json:

        "files": [ "includes/bootstrap.inc" ]

Suppose we add a new scaffolded file called 'drupal_root.inc'. It's scaffolded, so it gets put in to the /web folder when Drupal is installed by Composer. If it's defined as an autoload file to Composer, then it's loaded anytime the Composer autoloader is loaded (except while Composer plugins are running... you can't use dump() in a plugin, for instance, which is a total pain). So we could define DRUPAL_ROOT in that file, instead of in bootstrap.inc.

The thing is, I think it would need to be defined to Composer's autoload at the project level, because Composer will look for files relative to the package. And we don't want to load the package's copy of it, but the scaffolded copy. So that means that drupal/recommended-project would have to declared it as an autoload file.

I think this would work. Then what we'd have is:

1. Scaffolded drupal_root.inc declares DRUPAL_ROOT.
2. includes/bootstrap.inc declares DRUPAL_ROOT as a fallback, for projects that are set up with a non-standard composer.json
3. DrupalKernel relies on DRUPAL_ROOT if it's defined, and if not, guesses using the __DIR__ technique as currently

joachim’s picture

Argh, nope, that doesn't work.

Composer's autoload files get loaded with files from required packages coming first, and files from the project last -- see https://github.com/composer/composer/issues/7535#issuecomment-411992433

So in the generated Composer autoloader code, we get this:

    '2f69d3914119f042cca9e44442d5ce95' => $baseDir . '/web/core/includes/bootstrap.inc',
    '952683d815ff0a7bf322b93c0be7e4e4' => $vendorDir . '/chi-teck/drupal-code-generator/src/bootstrap.php',
    '801c31d8ed748cfa537fa45402288c95' => $vendorDir . '/psy/psysh/src/functions.php',
    '3917c79c5052b270641b5a200963dbc2' => $vendorDir . '/kint-php/kint/init.php',
    'cd774370d548d72500b76f2ed6e26769' => $baseDir . '/web/drupal_root.php',

and so bootstrap.inc gets loaded before drupal_root.php.

So drupal_root.php is workable, but we'd have to remove defining DRUPAL_ROOT from bootstrap.inc completely.

Or alternatively, maybe there's a way that it can be declared to the autoloader in drupal/core's composer.json? AutoloadGenerator::getPathCode() looks like there might be a way to do this, but WHY don't some developers write inline comments to explain their code???

joachim’s picture

Oh wait, I think this can work.

In core/composer.json:

        "files": [
          "../drupal_root.php",
          "includes/bootstrap.inc"
        ]

The files get autoloaded in the order we need them to be.

I've pushed a new MR with this approach: https://git.drupalcode.org/project/drupal/-/merge_requests/473

Though I think we can drop the fallback defining in bootstrap.inc, since both files are always going to be loaded.

joachim’s picture

joachim’s picture

Drat, but my MR doesn't work with @derEremit's script in #1, because in that setup, EVERYTHING is symlinked into the webroot, and all of Composer's files are outside it too.

In that setup, the only thing that's reliable is $_SERVER['SCRIPT_FILENAME']. But using that will mess up Drush. Though Drush crashes anyway when I run it in the webroot in @derEremit's setup!

alexpott’s picture

Given #116 works how about we put the definition of DRUPAL_ROOT in autoload.php - and deprecate it being defined in bootstrap,.inc

Then we don't need a new file or changes to composer.json and all front controllers are suppose to use it. Does Drush use it... yep it does - it has \Drush\Config\Environment::loadSiteAutoloader()

joachim’s picture

> Given #116 works how about we put the definition of DRUPAL_ROOT in autoload.php - and deprecate it being defined in bootstrap,.inc

It won't work in a file that isn't bootstrapped.

I *think* for this issue there are two use cases:

A. The Composer project has the webroot within it (typically at /web), and Drupal core is symlinked into the Composer project. Example at https://github.com/joachim-n/drupal-core-composer
B. The Composer project has Drupal core installed normally, and is wholly outside of the webroot, with parts of it symlinked into the webroot. Example in the script in #1.
[C. Both use cases could co-exist, but I think for this issue it reduces to case B.]

The issue summary needs updating to explain these use cases, but I'll wait and see if there are more I have missed.

Where I think we are with possible techniques to deal with this is:

- using __DIR__ in any non-scaffolded file fails, because PHP always resolves symlinks in filename-related magic constants
- using __DIR__ in a composer-autoloaded scaffolded file (#116) works in use case A, but not in the use case B.
- using $_SERVER['SCRIPT_FILENAME'] works, even if the file being run is over symlinks (e.g. core/install.php), but it's not going to work with Drush. It's also quite fiddly as it needs to be setup in every single file that's an entry-point to Drupal
- Other things I may have missed

alexpott’s picture

@joachim I don't understand why it wouldn't work in our weird autoload.php you have to load that file in order to bootstrap / autoload in the first place. It's always present in the Drupal's root. It's created by \Drupal\Composer\Plugin\Scaffold\GenerateAutoloadReferenceFile

Plus I wonder if we're over-thinking all of this. How about we have an environment variable that we check for and if that is set to something use that instead. That way was support all cases without having to adjust too much of the eco-system. Then we could make \Drupal\Core\DrupalKernel::guessApplicationRoot public and tell people to use that instead of any __DIR__ tricks.

joachim’s picture

> I don't understand why it wouldn't work in our weird autoload.php you have to load that file in order to bootstrap / autoload in the first place.

That would work in case A. I hadn't noticed that file -- it's hard to keep track of all the composer.jsons and autoload.phps! I'll make a MR for that later.

I don't think it would work in case B. Though I am increasingly thinking that we should say case B is unsolvable in the state as presented in the script in #1. I'm thinking instead, the sites folder needs to be kept in the Composer project folder, and Drupal's files folder needs to be explicitly placed in the webroot rather than inside sites/default. I'll experiment.

joachim’s picture

@alexpott are you sure it's [DRUPAL]/composer/Plugin/Scaffold/GenerateAutoloadReferenceFile.php?

I've done:

1. delete [WEBROOT]/autoload.php
2. put 'crash()' at the top of that file
3. done `composer update --lock`
4. the autoload file is restored. Nothing has crashed.

There is also vendor/drupal/core-composer-scaffold/GenerateAutoloadReferenceFile.php. Is it maybe that one? (I've not tried it yet; need to go make dinner.)

alexpott’s picture

@joachim the two are the same... in order to make changes locally you need to change both. The composer scaffold plugin is built from the code in /core but ends up in vendor. And yep, it is confusing.

alexpott’s picture

On a core development checkout you'll also need to change the root autoload.php for good measure. Luckily this file never changes :D

joachim’s picture

Ok! New MR https://git.drupalcode.org/project/drupal/-/merge_requests/483 with the approach of defining DRUPAL_ROOT in our own autoload.php, and having DrupalKernel use that.

This is working for me for case A.

For case B, I can get it working if I don't copy the sites folder into the webroot. But then the files folder won't be accessible over the web! I really don't what to do about this case. I think maybe the first thing is to get detailed instructions for setting up the whole project outside the webroot that are newer than 2013.

Also, this is going to be tricky to test, because of the changes in drupal/core-composer-scaffold.

> The composer scaffold plugin is built from the code in /core but ends up in vendor.

AFAICT, the drupal/core-composer-scaffold is being read from the d.org git repo. The base composer.json in Drupal defines local Composer repos in composer/Plugin/ProjectMessage and composer/Plugin/VendorHardening, but not composer/Plugin/Scaffold, which is what would make symlink it from the installed drupal core files. In my case, I added this to the project's root composer.json repositories section:

        {
          "type": "path",
          "url": "repos/drupal/composer/Plugin/Scaffold"
        }
joachim’s picture

Status: Needs work » Needs review

I *think* this is ready. (I mostly find time to work on this issue with kids TV in the background, my focus is not perfect.)

It fixes the case where Drupal is symlinked into the Composer project, which is important because it makes developing Drupal core itself much easier: see https://github.com/joachim-n/drupal-core-composer for reasons.

It doesn't fix the case with the script in #1, but the issue summary says:

> #1672986: Option to have all php files outside of web root. - this issue does not solve that case but is a pre-requisite.

Also, the #1 script doesn't satisfy the case of moving all PHP files out of the webroot, as you end up with a webroot with the core, modules, profiles, and themes folders symlinked in. So all the code is effectively accessible over the web.

Currently, moving all files outside of the webroot isn't possible. The problem is that the concept of 'app root' means too many things:

1. The folder beneath with Drupal can find the site folder and the settings.php file
2. The folder beneath which Drupal can find the public files.
3. The folder beneath which Drupal can find its core files, and contrib modules and themes

Moving PHP files out of the web root means that we need a concept of 'app root' which is used for 1 and 3, and a concept of 'web root' which is used for 2.

I think the next step is probably to unpick those two.

I thought I'd seen an issue about reading the locations of the web root and the app root from the Composer scaffold settings, which seemed like a good idea to me, but on rereading it, I misunderstood the issue (#3084369: Look up scaffold [web-root] from installation location of drupal/core).

joachim’s picture

Ah yes, testing this!

To test this with the symlinking set up, I had to tweak the project composer.json I use for symlinking core. This is because we need the drupal/core-vendor-hardening Composer plugin to come from the Drupal clone which is on this issue's branch, and by default, it will be downloaded from the main package repository. (Related issue: #3207499: source the drupal/core-vendor-hardening plugin from the drupal project, or document why not.)

Here's the composer.json file I used to set up the project. Instructions for setting it up are the same as for https://github.com/joachim-n/drupal-core-composer.

I have no idea how we go about writing automated tests for symlinking Drupal. Well, I do, but they seem complicated and fiddly to me: get PHPUnit to create a new project folder inside the test site folder (so it gets cleaned up after the test) and write a composer.json file there, which defines a Composer repository to symlink in the git clone of Drupal that we're running in. The do composer install in that folder, check that works, check Drupal's installation process can be run in browser.

joachim’s picture

Oops. Here's the file.

joachim’s picture

Status: Needs review » Needs work

Argh, nope, !483 / branch 1792310-drupal-root-in-autoload doesn't work with Drush. I'd tested it, but before I added back the guarded definition of DRUPAL_ROOT in includes/bootstrap.inc.

This is what goes wrong:

- Drush's entry point needs to bootstrap Drupal
- it starts off by loading Composer's autoload file in vendor/autoload.php
- because the drupal/core composer.json defines includes/bootstrap.inc as an autoload file, that gets loaded. DRUPAL_ROOT isn't defined yet, so the guarded code for defining it runs
- loadSiteAutoloader() in vendor/drush/drush/src/Config/Environment.php then includes the Drupal scaffolded web/autoload.php file, which then complains that DRUPAL_ROOT is already defined

We can't expect Drush, or indeed any external CLI script to load the scaffolded web/autoload.php file, because they shouldn't assume they know where Drupal is, and in order to know where Drupal is, they need to ask Composer, and to ask Composer they need to start up the autoloader.

@alexpott what cases are covered by the guarded definition you wanted in includes/bootstrap.inc?

alexpott’s picture

@joachim good question. Perhaps we can move the guard out of bootstrap.inc and into \Drupal\Core\DrupalKernel::__construct() after the app root is guessed. This way anyone who is booting Drupal will have this set for sure even if they've avoided our odd autoloader thingy (not using our odd autoloader is the case I'm concerned about).

KapilV’s picture

Status: Needs work » Needs review
FileSize
1.85 KB
269 bytes

Fixed Custom Commands Failed.

joachim’s picture

I think one of several follow-ups to this issue is going to be to define and document *one* correct way to run Drupal from a CLI.

Status: Needs review » Needs work

The last submitted patch, 133: 1792310-133.patch, failed testing. View results

joachim’s picture

> This way anyone who is booting Drupal will have this set for sure even if they've avoided our odd autoloader thingy (not using our odd autoloader is the case I'm concerned about).

Drush uses our weird autoloader, but to use our weird autoloader, Drush (or another script) has to first find it. So it's circular -- our autoloader will define DRUPAL_ROOT, but you need to know that to load it.

Drush uses webflo/drupal-finder which tries various things to figure our where Drupal is relative to Composer.

I was starting to think we need to go back to branch 1792310-drupal-root-scaffolded-file, the approach of having DRUPAL_ROOT defined in a file that Composer includes in the autoload process, which gets scaffolded to the web root. That way we know that we set it up as soon as a script interacts with Composer.

However, there's a chicken and egg situation there too: defining the drupal_root.php file in composer.json as "../drupal_root.php" is making the assumption that drupal/core sits one directory below the webroot.

Really, the only authority on where things are is Composer, since the project root's composer.json defines where files got put in the drupal-scaffold property. But how to get that out... arrgh!

alexpott’s picture

Really, the only authority on where things are is Composer, since the project root's composer.json defines where files got put in the drupal-scaffold property. But how to get that out... arrgh!

Yeah - this is one of those. I think the current solution is okay. Anything that has to find Drupal outside of Drupal has to locate our autoload.php and include that. Them's the rules. And that allows us to define DRUPAL_ROOT. This solution coupled with the other changes will allow you to symlink core and if you're doing it differently will you might not support symlinks but you'll be no worse off than HEAD today.

joachim’s picture

> I think the current solution is okay.

Yup, agreed. One step at a time. This feels like a really complex area to unravel.

I've filed follow-ups with some of the ideas I've had:

- #3208975: split the concept of DRUPAL_ROOT/app root into app root and Drupal web root
- #3208979: Provide a single entry-point to Drupal for CLI scripts

Thanks for the MR review @alexpott. Will have a look now.

joachim’s picture

Status: Needs work » Needs review

From the MR review:

> I think we shouldn't rename [guessApplicationRoot] in this issue. We can file a follow-up to deprecate and rename.

Making a note here of that here for another follow-up.

I've made both the requested changes to the MR.

Also tested the current code with:

- core/rebuild.php
- update.php
- Drush cr
- Drupal Console rr

to check that they are all correctly picking up the sites folder and the contrib modules with a Drupal Core symlinked setup.

joachim’s picture

Just an update about the Composer project I've been working on that installs Drupal core from a git clone over a symlink: I've made into a fully-fledged Composer project, so it can be installed simply with `composer create-project joachim-n/drupal-core-development-project`. There's a Composer script that takes care of cloning Drupal core, so it's a single command setup and then drush si to install Drupal.

I've also moved it to a new repo with a better name -- https://github.com/joachim-n/drupal-core-development-project

Spokje’s picture

Status: Needs review » Needs work

@joachim: I'm afraid the Build Successful return of TestBot isn't quite accurate (#3208931: TestBot returns with "Build Successful" when there are too many test failures to report).

If you look at the full log of console output of the test-run, there's not much reason to rejoice... :/

joachim’s picture

Urgh drat.

The tests are failing because:

1. PHPUnit loads the core/tests/bootstrap.php file specified in the phpunit.xml file. This is loaded from its real location, not symlinked, because PHPUnit's file loader resolves symlinks.
2. In that file, drupal_phpunit_populate_class_loader() does require __DIR__ . '/../../autoload.php'; Which in a symlinked setup, gets us the wrong file.

After that I'm not sure....

joachim’s picture

Assigned: Unassigned » joachim

New branch with a new approach coming...

joachim’s picture

The approach of defining DRUPAL_ROOT inside our special autoload.php has a problem because in some circumstances it's circular: our special autoload.php is scaffolded into the web root, and defines the location of the webroot.

If you're coming into Drupal with an HTTP request, then you (usually) start off in the webroot with index.php, that includes our special autoload.php, and then it's fine.

But if you're a script, or phpunit, you come in from your own package. Scripts such as Drush and Drupal Console have to guess at where the webroot is in order to load our special autoload.php. At which point, defining DRUPAL_ROOT is too late to be useful. Guessing the location of the webroot from outside of it, or guessing how to get to Composer from within the webroot, is the problem this issue needs to fix.

So, re-thinking this from first principles:

- the only authority on where packages are located is Composer, because it puts them there
- the only authority on where the webroot is located is the project root composer.json, which defines it, and also our scaffolding Composer plugin, because it reads it from composer.json and then puts scaffolded files there
- any entry point, whether it's an HTTP request, a script, or phpunit, always starts by loading the Composer autoloader.

So:

1. Write a .inc file during scaffolding which defines DRUPAL_ROOT
2. Ensure this gets loaded during Composer's autoloading.
3. The file has to be in drupal/core, not in webroot, because Composer doesn't know where the webroot is (at least, not without reading the composer.json file which we don't want to be doing every single request)

New MR for this approach: https://git.drupalcode.org/project/drupal/-/merge_requests/591

This approach also sets groundwork for #3208975: split the concept of DRUPAL_ROOT/app root into app root and Drupal web root, because we can easily define more constants in that file, and #3208979: Provide a single entry-point to Drupal for CLI scripts, because once scripts load the Composer autoloader, they can find the Drupal webroot.

I've run a few tests both on a symlinked set up and with a flat git clone on which `composer install` is run.

joachim’s picture

Status: Needs work » Needs review
joachim’s picture

Fixed the various coding style errors.

And cherry-pick-squashed all the DrupalKernel stuff from the other branch which I'd completely forgotten we need here too :/

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

joachim’s picture

> I have no idea how we go about writing automated tests for symlinking Drupal. Well, I do, but they seem complicated and fiddly to me: get PHPUnit to create a new project folder inside the test site folder (so it gets cleaned up after the test) and write a composer.json file there, which defines a Composer repository to symlink in the git clone of Drupal that we're running in. The do composer install in that folder, check that works, check Drupal's installation process can be run in browser.

Haha! Since I wrote that comment, I have noticed that there are tests in Drupal that do pretty much exactly this, to test the Composer project templates which are in core!!

andypost’s picture

mxr576 made their first commit to this issue’s fork.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

mglaman’s picture

It seems like killing DRUPAL_ROOT for the app.root service parameter is a better approach than yet another scaffolding file.

joachim’s picture

That's worth exploring in a new branch, definitely.

What I like about the scaffold file technique is that it preserves DRY - the app root is defined only once in composer.json.

But if Drupal is being used as a library by something else, the scaffolding plugin probably won't even be in use.

joachim’s picture

Thinking about it some more, I don't see how an app.root service parameter could work in the symlinked repo scenario.

In order to get the service parameter, you need the default.services.yml file. And in order to get that file, you need the sites folder. To know where the sites folder is, you need the app root.

joachim’s picture

New idea! I've found something quite nice in the Composer API (https://getcomposer.org/doc/07-runtime.md):

// returns an absolute path to the package installation location if vendor/package is installed,
// or null if it is provided/replaced, or the package is a metapackage
// or throws OutOfBoundsException if the package is not installed at all
\Composer\InstalledVersions::getInstallPath('vendor/package');

Which means that in DrupalKernel, we can do this:

// Returns "/Users/joachim/Sites/drupal-core-composer/vendor/composer/../../web/core"
$drupal_core_installed_path = \Composer\InstalledVersions::getInstallPath('drupal/core');
// Sets $app_root to "/Users/joachim/Sites/drupal-core-composer/vendor/composer/../../web/"
$app_root = dirname($drupal_core_installed_path);

This gets us **exactly** what we want! This is the location where Composer has installed drupal/core, and that location is PROJECT_ROOT/web, because that's the location that is defined in composer.json. (Admittedly, there are some '..' in the path, but those are fine, right?)

However... this API is only available in Composer 2.1 :(

But it does mean that there is a really simple fix:

- DrupalKernel checks whether Composer\InstalledVersions::getInstallPath() exists, and if so, uses that to determine app root / DRUPAL_ROOT
- If it doesn't, because Composer is old, use the current technique which assumes a standard code structure
- we say that if you want a non-standard code structure, you need to install Drupal with Composer ^2.1

Could we at some point require a minimum version of Composer? There doesn't seem to be any Composer version requirements at the moment.

shaal’s picture

@joachim yes please!
#156++

I found this blogpost hinting about Composer v1 EOL - https://blog.packagist.com/deprecating-composer-1-support/

We hinted in the release announcement that Composer 1.x was pretty much EOL

And some more info about Composer v1 EOL - https://github.com/composer/composer/issues/10340

joachim’s picture

I realised after posting that while this covers the 'Drupal symlinked into web/core' case, it doesn't help at all for the 'Drupal installed into vendor/' case.

There's no simple way to handle that one.

To recap/rubberduck, there are 3 4 kinds of entry point into Drupal:

- A. HTTP request to the index.php file which is scaffolded into the app root
- B. running tests
- C. code in a Composer package (e.g. Drush or Console) bookstraps Drupal
- D. HTTP request to core/modules/statistics/statistics.php (yay! special case!)

If Drupal is installed by Composer in web/core then in all cases, asking Composer 'where did you put Drupal?' is fine.

If Drupal is installed by Composer in vendor/, then the only piece of information about the location of the app root is the definition of the scaffold location in composer.json.

We can get that by:

- reading composer.json (ugly)
- writing a file to a fixed location during the scaffolding process (also ugly)
- installing some sort of package to the scaffold location, so we can ask Composer about that package.

I'm going to investigate the 3rd option. We currently put drupal/core into web/core/, but there's no package installed into web/. Could we have a dummy, empty package which does there? Or would Composer complain about web/core/ being inside web/?

webflo’s picture

I wrote a custom composer plugin as an experiment for drupal-finder a while ago. Its currently hosted on github: https://github.com/webflo/drupal-finder-plugin

It stores additional metadata as a PHP class, similar to InstalledVersions::getInstallPath() and https://github.com/Ocramius/PackageVersions. I think it possible to compute all necessary information during installation of this plugin. It reacts on composer install and update as well.

Give it a try. "composer require webflo/drupal-finder-plugin:1.x-dev".

The code: https://github.com/webflo/drupal-finder-plugin/blob/1.x/src/DrupalFinder...

The generated class is called \DrupalFinderPlugin\Info

joachim’s picture

Cool, thanks!

We could do the same thing with the drupal/core-composer-scaffold plugin.
It would introduce a foreign file into the git repository in the setup where core is symlinked in by Composer. I suppose that could be handled with a .gitignore that's in the repository as well.

For setups where the drupal/core-composer-scaffold plugin is absent, we could require that the project do its own thing to define that same class.

alexpott’s picture

However... this API is only available in Composer 2.1 :(

Looks like it's worth adding composer-runtime-api ^2.0 to core/composer.json given the docs say

 * This class is copied in every Composer installed project and available to all
 *
 * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
 *
 * To require its presence, you can require `composer-runtime-api ^2.0`

We should do this for Drupal 10 in another issue.

alexpott’s picture

Also - goes without saying but we need to performance test this. This is going to result in loading vendor/composer/installed.php on every request. Which with core alone is nearly 2000 lines long... and on a decent size project it is double that. Loading this file for every request doesn't feel right.

joachim’s picture

I've had a go at installing drupal/core in vendor/, with some quick hack in DrupalKernel to make it set DRUPAL_ROOT correctly.

It falls over pretty quickly, because DrupalKernel::compileContainer() assumes that the app root, that is, the place where index.php is scaffolded to, is also where the core folder is located:

    foreach (['Core', 'Component'] as $parent_directory) {
      $path = 'core/lib/Drupal/' . $parent_directory;
      $parent_namespace = 'Drupal\\' . $parent_directory;
      foreach (new \DirectoryIterator($this->root . '/' . $path) as $component) {

It's trying to take directory listings of web/core/lib/Drupal/Core, because we've defined $app_root as 'web/', but finding nothing, because the folder it's trying to list is at vendor/drupal/core/lib/Drupal/Core.

So, I think the scope of this issue needs to be changed: we can't fix the drupal/core in vendor case until later on, in #3208975: split the concept of DRUPAL_ROOT/app root into app root and Drupal web root. This issue should nonetheless allow for that case to be able to correctly set the app root, even if it's not useful yet.

joachim’s picture

My latest plan was this:

- scaffold plugin generates a PHP file during the scaffold, written to the plugin's folder
- this file is registered as a "file" autoload with Composer, via a wrapper file (because otherwise Composer will complain when it hasn't been written yet)
- the file defines a namespaced constant, \DrupalLocation\APP_ROOT. The scaffold plugin sets the value of the constant to the scaffold location, which it knows during the scaffold process
- DrupalKernel uses this constant to set $app_root if it's defined, and uses the current 'jump up 2 dirs' logic if it's not defined (in the case there the scaffold plugin is not present)

And how this works out for different setups:

- standard setup with scaffolding: the scaffolded constant is used
- core symlinked in by Composer: the scaffolded constant is used
- plain clone of core: 'jump up 2 dirs' is used
- weird nonstandard setup, such as core in vendor/ : it's up to you to define \DrupalLocation\APP_ROOT yourself

This was working nicely!

Then I started looking at how we deprecate DRUPAL_ROOT, and I got stuck on the problem that Composer doesn't guarantee the order of autoload files. We have:

- core/includes/bootstrap.inc, registered as an autoload file by drupal/core, defines DRUPAL_ROOT
- drupal_location_loader.php, registered as an autoload file by drupal/core-composer-scaffold, defines \DrupalLocation\APP_ROOT

and we don't know which one comes first, but we really need \DrupalLocation\APP_ROOT to get defined first!

So unless anyone has any other ideas, I am going to tweak this so the generated file defines a class. That means that in core/includes/bootstrap.inc we can do class_exists(), and get the APP_ROOT constant.

I liked the idea of making it a namespaced constant, because it means that when we get to #3208975: split the concept of DRUPAL_ROOT/app root into app root and Drupal web root we can define constants in the same namespace but in different parts of the code.

I'm tempted to cheat and put the constant definition in the class file but outside of the class. But that would be hacky, right???

joachim’s picture

More rubber-ducking...

We need to write something during scaffolding which records the location of Drupal's app root.

This needs to be:
- something that is either automatically loaded or loadable by Composer, so that things like Drush and Console get it
- something that loads before Drupal's core/includes/bootstrap.inc, because otherwise that will set DRUPAL_ROOT
- it has to have the locations as strings, not use magic constants like __DIR__ because those won't work with symlinks

The simplest thing is to have the scaffolding plugin write a file. (I've tried this in various branches - 1792310-drupal-root-in-scaffolded-include-file, and 1792310-drupal-root-scaffolded-file but both get it wrong.)

The location file can't be in a scaffolded location, because then Composer autoload can't find it -- we can't make assumptions in composer.json's autoload config about where it's been put.

So it has to be written into a package's folder, like @webflo's plugin (see #159), and the package can have a .gitignore which says to ignore it.

The options are:

- autoload file defining a constant: this is tricky because Composer includes autoload files in dependency order (see https://github.com/composer/composer/issues/10509). So the only way to get this included before core's core/includes/bootstrap.inc (which is also an autoloaded file) is to have it in something that drupal core requires. That rules out the scaffolding plugin, or putting it in the project's composer.json.
- autoload class: I wasn't sure about this because then a nonstandard project would have to define that class itself to set the locations.

However, instead of this location class being a new API for projects to use, I am now thinking we could simply use the existing API, which is the "locations" config in the project composer.json.

So my new plan is: we add a new Composer plugin which is inside core, whose sole job is to write the locations file into itself. Make the plugin required by core so that the autoload file loads before core/includes/bootstrap.inc. That means it's always present, even in projects that don't use the scaffold plugin. And nobody else needs to worry about defining it, as to change the location values you set them in composer.json.

alexpott’s picture

So my new plan is: we add a new Composer plugin which is inside core, whose sole job is to write the locations file into itself. Make the plugin required by core so that the autoload file loads before core/includes/bootstrap.inc. That means it's always present, even in projects that don't use the scaffold plugin. And nobody else needs to worry about defining it, as to change the location values you set them in composer.json.

Sounds reasonable.

One thing I'm wondering is are we using DRUPAL_ROOT etc too much. Maybe we don't need to use it - otherwise other applications would have this problem.

joachim’s picture

> One thing I'm wondering is are we using DRUPAL_ROOT etc too much. Maybe we don't need to use it - otherwise other applications would have this problem.

I had a quick look at other CMS's installation docs.

- Wordpress doesn't use Composer, so it controls its own file structure
- Joomla also doesn't use Composer
- Laravel installs with Composer, but I don't see any special locations declared, so it looks like its extensions just go in /vendor

The reason we have DRUPAL_ROOT is that we're supporting multiple ways of installing Drupal with different structures -- the recommended template, the legacy template, and plain git clone of core.

joachim’s picture

Issue summary: View changes

New MR ready for review: https://git.drupalcode.org/project/drupal/-/merge_requests/1922

I've set the old MRs which are mine as drafts.

And I've rewritten the IS to explain in depth the problem and how the MR works. I hope it's clear. This is a complex topic, which has taken me a long time to understand!

joachim’s picture

Component: install system » base system

Not sure which component is best for this, but probably not the installer.

joachim’s picture

Both of the failing tests are passing for me locally with a git clone of the branch, run like this:

1. clone the branch
2. do composer install in the root of the git clone
3. set up phpunit.xml, pointing it at an empty DB
4 vendor/bin/phpunit core/modules/system/tests/src/Functional/UpdateSystem/RebuildScriptTest.php or vendor/bin/phpunit core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php --filter=testMinimumStabilityStrictness

These two tests also pass if I run then with run-tests.sh:

1. clone the branch
2. do composer install in the root of the git clone
3. install Drupal
4. install simpletest module
5. php core/scripts/run-tests.sh --url http://localhost:8888/drupal-test-install-git-clone-1792310 --dburl mysql://drupal:password@localhost/drupal-1792310-clone --file core/modules/system/tests/src/Functional/UpdateSystem/RebuildScriptTest.php` or php core/scripts/run-tests.sh --url http://localhost:8888/drupal-test-install-git-clone-1792310 --dburl mysql://drupal:password@localhost/drupal-1792310-clone --file core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php

joachim’s picture

Since I can't reproduce the test failure in RebuildScriptTest locally, I've been trying to get debugging info out of Drupal CI using a testing issue -
#3271051: Testing issue for Joachim, ignore!.

It's not going well -- I'm chucking print_r() statements into patches, but not always getting results when the patch is tested :(

What I think I know is this:

This line fails:

> $this->drupalGet('/container_rebuild_test/module_test/module_test_system_info_alter');

because the page reports that the module_test module is still in its original location, in core/modules/system/tests/modules/module_test rather than in SITE_DIR/modules/module_test.

The copy command is this:

> $file_system->mirror('core/modules/system/tests/modules/module_test', $this->siteDirectory . '/modules/module_test');

It works locally, and IIRC on Drupal CI debugging shows that the module gets copied to the site directory for the test site.

Debugging in ExtensionDiscovery seems to show however that SITE_DIR/modules is NOT getting picked up as a module folder.

That's about as far as I've got.

Taran2L made their first commit to this issue’s fork.

Taran2L’s picture

OK, the previous patch does not work. The issue here is Apache webser behavior with mod_dir:

A "trailing slash" redirect is issued when the server receives a request for a URL http://servername/foo/dirname where dirname is a directory. Directories require a trailing slash, so mod_dir issues a redirect to http://servername/foo/dirname/.

rebuild.php is not adding a trailing slash when redirecting to a front page when website is run in a subdirectory (like on drupalci)

joachim’s picture

Note to eventual committer - please credit @Taran2L and @bbrala for lots of time figuring out the test failure on slack.

bbrala’s picture

After investigating a little, the problem with the tests is in rebuild.php.

Settings::initialize($app_root, DrupalKernel::findSitePath($request), $autoloader);

This breaks the test. In my local env i fixed this by making DrupalKernel::getApplicationRoot() public and using that as first argument in the initialize command. Suddenly it knows what to do. The fact it uses $app_root = dirname($_SERVER['SCRIPT_FILENAME'], 2); here is a problem in a symlinked env (as the ci is, and my local env is ;))

I'm not gonna push to the branch since i'm not sure making that public is feasable. I'll let that be decided by the people working on this :)

For completeness sake;

diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php
index de3f74889a..5dcf62b4f1 100644
index de3f74889a..5dcf62b4f1 100644
--- a/core/lib/Drupal/Core/DrupalKernel.php
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -340,7 +340,7 @@ protected static function guessApplicationRoot() {
    *
    * @see \Drupal\Composer\Plugin\Scaffold\GenerateAutoloadReferenceFile
    */
-  protected static function getApplicationRoot() {
+  public static function getApplicationRoot() {
     if (class_exists(DrupalLocation::class)) {
       return DrupalLocation::APP_ROOT;
     }
diff --git a/core/rebuild.php b/core/rebuild.php
index e957440905..0d83ce39c8 100644
--- a/core/rebuild.php
+++ b/core/rebuild.php
@@ -11,12 +11,14 @@
  */

 use Drupal\Component\Utility\Crypt;
+use Drupal\Composer\Plugin\Locations\DrupalLocation;
 use Drupal\Core\DrupalKernel;
 use Drupal\Core\Site\Settings;
 use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;

+
 // Use SCRIPT_FILENAME rather than the current filename so that symlinks are not
 // resolved.
 $app_root = dirname($_SERVER['SCRIPT_FILENAME'], 2);
@@ -32,7 +34,7 @@
 DrupalKernel::bootEnvironment();

 try {
-  Settings::initialize($app_root, DrupalKernel::findSitePath($request), $autoloader);
+  Settings::initialize(DrupalKernel::getApplicationRoot(), DrupalKernel::findSitePath($request), $autoloader);
 }
 catch (HttpExceptionInterface $e) {
joachim’s picture

@Taran2L figured it out on slack:

The problem is that when the WHOLE of the project is symlinked, and the site run from that symlink (which is how Drupal CI sets up Drupal, for historical reasons, see #2838194: Run out of real subdirectory, not symlink), TWO different cache IDs are getting used for the container!

- a load of index.php with a cold cache sets a cid of service_container:prod:9.4.0-dev::Darwin:a:1:{i:0;s:85:"/PATH/TO/SITE/sites/default/services.yml";}
- and rebuild.php creates a cid of service_container:prod:9.4.0-dev::Darwin:a:1:{i:0;s:98:"/PATH/TO/SITE/subdirectory/sites/default/services.yml";}

And there's a simple way to fix that.

So I think the MR might be green now!

Thank you SO MUCH to @Taran2L and @bbrala for their help on slack over the last few days!

joachim’s picture

Issue summary: View changes
joachim’s picture

Issue summary: View changes

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

voleger’s picture

Version: 9.5.x-dev » 10.0.x-dev
Status: Needs review » Needs work
Issue tags: +Needs reroll

It would be great to see this in drupal:10.0.0. Needs reroll.

joachim’s picture

Status: Needs work » Needs review

Made a new branch and rebased onto 10.0.x, opened a new MR.

voleger’s picture

Status: Needs review » Needs work
Issue tags: -Needs reroll

seems like changes require running the following command
composer update --lock

joachim’s picture

Status: Needs work » Needs review

Done! Thanks for the review!

voleger’s picture

Status: Needs review » Reviewed & tested by the community

Drupal\Tests\ComposerIntegrationTest passed!

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

I think this change is still going to be difficult. As far as I can see this change means that you have to run composer in production because it is writing absolute paths to the file. Not all projects are set up like this. It means your codebase is no longer portable without running composer commands. That's a very big change to make. Also writing to a code path like composer/Plugin/Locations/DrupalLocation.php is a big change.

joachim’s picture

> As far as I can see this change means that you have to run composer in production because it is writing absolute paths to the file

I'll change it to a relative path.

> Also writing to a code path like composer/Plugin/Locations/DrupalLocation.php is a big change

We have to write to a location that's loadable by class autoloading. Where would you rather it be written to?

joachim’s picture

I've had a quick play around with it, and I think that writing a relative path means that we have to do #3208975: split the concept of DRUPAL_ROOT/app root into app root and Drupal web root as part of this issue.

That's because sometimes code in Drupal wants the root to load things in itself, and sometimes code in Composer packages wants the Drupal root to load Drupal. Those two things work if the root is absolute. If it's relative, it breaks because those two things don't have the same working directory.

joachim’s picture

> Also writing to a code path like composer/Plugin/Locations/DrupalLocation.php is a big change

Thinking about this, the only options are:

- inside a package, because then Composer has it in its autoload
- in the root of the project, with an autoload entry in root composer.json
- in a dedicated subfolder of the root of the project, e.g. '/locations', with an autoload entry in root composer.json
- in the scaffolded Drupal, but then we have to violate DRY by having an autoload entry in root composer.json which repeats the extra>drupal-scaffold>locations value in the same file

@alexpott which do you prefer / dislike the least?

joachim’s picture

Rebased to 9.4 MR on 9.5.

joachim’s picture

Status: Needs work » Needs review

> @alexpott which do you prefer / dislike the least?

@alexpott wrote on Slack:

> My quick gut feel is: I'm not sure it is an easy choice, but Drupal root is at least consistent with other files. This makes me wonder if we should add this to the autoload.php that we write already.

I've updated both MRs to write to the Composer root.

Using the autoload.php that we write is fine for Drupal code to find our DrupalLocations class, but doesn't work for Composer packages such as Drush, which also need to have it loaded.

joachim’s picture

Status: Needs review » Needs work

The test failure is this:

22:15:53 In GenerateLocationsClass.php line 146:
22:15:53
22:15:53 file_put_contents(/var/www/html/DrupalLocation.php): Failed to open stream:
22:15:53 Permission denied
22:15:53

Looks like our CI isn't letting the Locations plugin write to the project root. Any ideas why?

neclimdul’s picture

There's a ton of documentation on how code interacts but what problem does this solve? How would I recreate this problem? The original issue summary sounds like a support request but the people working on this have me thinking its a bigger problem but I can't figure out what the expected outcome is.

joachim’s picture

> There's a ton of documentation on how code interacts but what problem does this solve?

Non-standard installations of Drupal which place core somewhere else, or install it in a different way.

Any thoughts on how to rewrite this line in the IS to be clearer?

This is important because the standard way of developing a Composer package, by defining a path repository in a project to symlink a git clone of the package, isn't currently possible with Drupal core.

> How would I recreate this problem?

Try to install drupal/core for development in a Composer project using the path repository technique: see https://medium.com/pvtl/local-composer-package-development-47ac5c0e5bb4

I made a Composer template to make this possible but it's basically a heap of hacks to work around the problem here. See https://github.com/joachim-n/drupal-core-development-project

joachim’s picture

Status: Needs work » Needs review

The D10 MR is passing tests.

The D9 MR is failing because it's not managing to write the DrupalLocations class into the Composer project root. I don't understand how the D10 branch is managing to do that where the D9 branch is failing. The core/drupalci.yml file is identical in both branches.

Unless anyone has any ideas for how to fix this, I'm tempted to just drop the D9 branch, as I doubt this issue is going to land on D9 anyway!!

DamienMcKenna’s picture

Agreed with #196, the D9 test run shows the following:

02:57:51  Drupal app root assumed to be the Composer project root, /var/www/html.
02:57:51  
02:57:51  In GenerateLocationsClass.php line 152:
02:57:51                                                  
02:57:51    The directory /var/www/html is not writable.  

and it goes downhill from there, whereas the D10 test run says the following:

05:47:57  Drupal app root assumed to be the Composer project root, /var/www/html.
05:47:57  Writing Drupal locations class to /var/www/html/DrupalLocation.php.

Thoughts, anyone?

needs-review-queue-bot’s picture

Status: Needs review » Needs work
FileSize
150 bytes

The Needs Review Queue Bot tested this issue. It either no longer applies to Drupal core, or fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

voleger’s picture

Version: 10.0.x-dev » 11.x-dev

joachim’s picture

Status: Needs work » Needs review

New branch & MR for 11.x

smustgrave’s picture

Status: Needs review » Needs work

Have not reviewed.

But appears to have composer error and can't trigger retest.

joachim’s picture

Status: Needs work » Needs review

Fixed the errors.

bbrala’s picture

Status: Needs review » Needs work

I only found one real question in regards of a missed dirname(__DIR__, 2); in one of the files. There is other places we might want to use our new found power instead of some of the references through __DIR__. Most seem in tests though which i'd not touch probably.

I've run through this whole issue and went through the MR intensely. This seems like an elegant solution that will enable us to take a step forward in how core is structured.

joachim’s picture

Status: Needs work » Needs review

Resolved all points.

voleger’s picture

The latest version of MR looks great. Added suggestions for improvement.
The only thing that I want to clarify is the upgrade path for the existing projects. I reviewed CR, and there is only mention of the new API, but no upgrade steps. As I understand, existing projects need to update the autoload section of the composer.json file, right?

joachim’s picture

> The only thing that I want to clarify is the upgrade path for the existing projects. I reviewed CR, and there is only mention of the new API, but no upgrade steps. As I understand, existing projects need to update the autoload section of the composer.json file, right?

Oh that's a good point!

In the MR, any code that uses the \Drupal\Composer\Plugin\Locations\DrupalLocation class also has a fallback for if that class is not found.

That fallback is for the benefit of existing installations, but also for plain git clones, where there is no drupal scaffolding done and so the DrupalLocation won't get written.

So actually, the CR needs to be changed, and possibly the docs as well, to say this: you have access to DrupalLocation IF you know you are in a project that uses drupal/core-drupal-locations -- in other words, in code that you own in your project, or in a Composer project template that requires drupal/core-drupal-locations and sets up scaffolding. Generally, in thid-party code, you should use DrupalKernel::getAppRoot().

voleger’s picture

Status: Needs review » Needs work
Issue tags: +Needs change record updates

Ok, so PR looks good. I set NW for CR updates.

voleger’s picture

Status: Needs work » Needs review
Issue tags: +Needs reroll

MR needs to rebase

voleger’s picture

Status: Needs review » Needs work
joachim’s picture

Status: Needs work » Needs review
Issue tags: -Needs change record updates, -Needs reroll

Updated the README and the CR.

voleger’s picture

Status: Needs review » Reviewed & tested by the community

GitlabCI (wow, 4 times faster than DrupalCI) was not happy with CSpell check. I added an entry to a dictionary. The changes look good to me for RTBC

joachim’s picture

Status: Reviewed & tested by the community » Needs work

'definining' is a typo, not a word :)

I'll fix.

joachim’s picture

Status: Needs work » Reviewed & tested by the community

Fixed.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

I think the BC layer in bootstrap.inc is not correct - it should use Drupal\Locations\DrupalLocation; and not use Drupal\Composer\Plugin\Locations\DrupalLocation; - also the comment in bootstrap.inc is out-of-date.

Plus I think people need to guidance about whether to deploy DrupalLocation.php or should it be it .gitgnore like the other scaffolded files.

joachim’s picture

> I think the BC layer in bootstrap.inc is not correct

Fixed, and same problem in DrupalKernel too.

> Plus I think people need to guidance about whether to deploy DrupalLocation.php or should it be it .gitgnore like the other scaffolded files.

It can be either -- it's documented in the code, but I'll add it to the README:

 * To ensure the project's codebase is portable, the generated class must not
 * use absolute paths. It should instead use the __DIR__ constant, which in the
 * generated class will give the project root.

> should it be it .gitgnore like the other scaffolded files

We could do that, but code to add files to .gitignore is in the drupal/core-composer-scaffold and it's marked @internal. Is it ok for the locations plugin to call the scaffold plugin given they're both Drupal core?

alexpott’s picture

We could do that, but code to add files to .gitignore is in the drupal/core-composer-scaffold and it's marked @internal. Is it ok for the locations plugin to call the scaffold plugin given they're both Drupal core?

Maybe this new composer plugin should be part of scaffold - as it is very similar to the autoload.php we're scaffolding.

joachim’s picture

I did look at doing that at one point, but I felt that its function was sufficiently different from Scaffold to count as feature creep - Scaffold just copies files from one place to another according to a recipe. This generates a class.

alexpott’s picture

Hiding all the patches.

joachim’s picture

DrupalCon Lille conversation -- things to change:

1. Merge new Composer plugin into Scaffold - we've agreed that Scaffold's purpose is generally 'Make the Composer project happy with Drupal's weirdnesses'. It already generates the autoload.php file.
2. Look at https://getcomposer.org/doc/04-schema.md#classmap instead of PSR autoload.
3. Be clearer in the CR about what existing sites need to or can do -- cover various different setups.

capysara made their first commit to this issue’s fork.

capysara’s picture

I rebased and fixed conflicts in composer.lock and core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php. In FunctionalTestSetupTrait.php, this line had been removed altogether: chdir(DRUPAL_ROOT), see 5ef2211afc. In this MR, the line was changed to chdir($app_root). I don't have much context for either issue so I kept the change from this issue.

I updated the error messaging for coding standards, but I didn't address any of the comments in the MR. I was just trying to get this back to green.

(Also, sorry Joachim, I didn't notice Assigned until after I started committing things.)

joachim’s picture

Assigned: joachim » Unassigned

Thanks!
I probably should have unassigned myself a while ago -- haven't got the mental space to dive back into one at the moment. Thanks for keeping it ticking over!

It might be worth seeing if we can do without the chdirs() which are removed in 5ef2211afc.

joachim’s picture

I'm trying to rebase this, and I'm not managing to update Composer from lock, and I don't remember how I did it previously!

> COMPOSER_ROOT_VERSION=11.0-dev composer update --lock

says:

Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Root composer.json requires drupal/core == 11.9999999.9999999.9999999-dev (exact version match), it is satisfiable by drupal/core[11.x-dev] from composer repo (https://repo.packagist.org) but drupal/core[11.0-dev] from path repo (core) has higher repository priority. The packages from the higher priority repository do not match your constraint and are therefore not installable. That repository is canonical so the lower priority repo's packages are not installable. See https://getcomposer.org/repoprio for details and assistance.

and

> COMPOSER_ROOT_VERSION=11.9999999.9999999.9999999-dev composer update --lock

works, but then the version set in composer.lock is all wrong.

Darren Oh’s picture

You have be in the 11.x branch to update the lock file.

joachim’s picture

So is the right way to do it:

1. merge 11.x into feature branch
2. Update lock file
3. switch to feature branch, commit changes
4. switch to 11.x and do a git reset --hard to put it back

?

joachim’s picture

Aha, that's worked. Thanks!!

joachim’s picture

> 2. Look at https://getcomposer.org/doc/04-schema.md#classmap instead of PSR autoload.

That's not going to work. I've tried it, and any Composer operation which happens before the DrupalLocation.php is written produces this error, multiple times:

> Could not scan for classes inside "DrupalLocation.php" which does not appear to be a file nor a folder

That happens when doing `composer drupal:scaffold` and probably will happen when first installing Drupal with Composer.

The problem is that 'classmap' autoloading looks in the given list of files to find classes to register. That's unlike 'psr-4' autoloading, which merely declares directories to Composer, and it will only look for files when a class is about to be loaded.

So with psr-4 it's fine to declare a file that doesn't exist yet, but with classmap it's not.

@alexpott - your concern was that the namespace could clash with other things. What if make it more specific?

Instead of:

> namespace Drupal\Locations;

we have:

> namespace Drupal\Composer\Locations;

The Drupal\Composer namespace is used by things in our /composer folder, nobody else should be using that namespace.

joachim’s picture

Moved all the code to the scaffold plugin.

I've started trying to get the gitignore to work and can't quite get it working :/