Skip to content
This repository has been archived by the owner on Feb 13, 2023. It is now read-only.

Make Drupal VM into a Composer plugin that scaffolds a Vagrantfile #1256

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
38 changes: 28 additions & 10 deletions Vagrantfile
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :

require_relative 'lib/drupalvm/vagrant'
require 'json'

# Absolute paths on the host machine.
host_drupalvm_dir = File.dirname(File.expand_path(__FILE__))
host_project_dir = ENV['DRUPALVM_PROJECT_ROOT'] || host_drupalvm_dir
host_config_dir = ENV['DRUPALVM_CONFIG_DIR'] ? "#{host_project_dir}/#{ENV['DRUPALVM_CONFIG_DIR']}" : host_project_dir
drupalvm_env = ENV['DRUPALVM_ENV'] || 'vagrant'

# Absolute paths on the guest machine.
guest_project_dir = '/vagrant'
guest_drupalvm_dir = ENV['DRUPALVM_DIR'] ? "/vagrant/#{ENV['DRUPALVM_DIR']}" : guest_project_dir
guest_config_dir = ENV['DRUPALVM_CONFIG_DIR'] ? "/vagrant/#{ENV['DRUPALVM_CONFIG_DIR']}" : guest_project_dir
# Default paths when the project is based on Drupal VM.
host_project_dir = host_drupalvm_dir = host_config_dir = __dir__
guest_project_dir = guest_drupalvm_dir = guest_config_dir = '/vagrant'

drupalvm_env = ENV['DRUPALVM_ENV'] || 'vagrant'
if File.exist?("#{host_project_dir}/composer.json")
cconfig = {}
composer_conf = JSON.parse(File.read("#{host_project_dir}/composer.json"))
if composer_conf['extra'] && composer_conf['extra']['drupalvm']
cconfig = composer_conf['extra']['drupalvm']
end

# If Drupal VM is a Composer dependency set the correct path.
vendor_dir = ENV['COMPOSER_VENDOR_DIR'] || composer_conf.fetch('config', {}).fetch('vendor-dir', 'vendor')
drupalvm_path = "#{vendor_dir}/geerlingguy/drupal-vm"
Copy link
Collaborator Author

@oxyc oxyc Apr 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vendor_dir being a function seems very unclear to me coming from PHP. Rubocop complains if I add unnecessary parenthesis though. Maybe I should drop that lint?

Copy link
Collaborator Author

@oxyc oxyc Apr 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One problem here. There are some Windows developers who might prefer to code entirely within the VM right? This is why I'm avoiding calls to composer without the which function which is now in the lib we dont have access to before figuring out host_drupalvm_path (d'oh). As I'm avoiding calling composer config to retrieve the vendor-dir path, this could theoretically be wrong if the user somehow set a custom vendor path using a different method than the vendor-dir extra-field.

Also this setup requires the user to run composer install to download Drupal VM. That's probably alright, I mean all window devs using this setup would have composer installed, right?

Copy link
Collaborator Author

@oxyc oxyc Apr 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could also allow the user to specify the path to Drupal VM in the composer.json. So that it doesnt have to reside within the vendor-dir. But I doubt anyone would need to use that.

drupalvm_path = cconfig['path'] || "#{vendor_dir}/geerlingguy/drupal-vm"

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this better than depending on composer being available in the current session.

Also need to consider env vars -- https://getcomposer.org/doc/03-cli.md#composer-vendor-dir

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some Windows developers who might prefer to code entirely within the VM right?

Yes. (And the occasional Mac or Linux user who likes the isolation.)

Also this setup requires the user to run composer install to download Drupal VM. That's probably alright, I mean all window devs using this setup would have composer installed, right?

Preferably, yes. But right now there are a lot of Windows devs who neither want nor are allowed to set up PHP and/or Composer on Windows... but it could be possible we could make that a requirement for 5.0.0+...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But right now there are a lot of Windows devs who neither want nor are allowed to set up PHP and/or Composer on Windows.

Would these devs still be using Drupal VM as a composer dependency?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also the possibility that someone commits all dependencies and therefore not all Devs would need composer..

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So for example a tech lead might use composer on behalf of the rest of the team and track all dependencies - https://www.codeenigma.com/build/blog/do-you-really-need-composer-production

I personally prefer an isolated build process but some might use DVM as composer dep without composer

if Dir.exist?("#{host_project_dir}/#{drupalvm_path}")
host_drupalvm_dir = "#{host_project_dir}/#{drupalvm_path}"
guest_drupalvm_dir = "#{guest_project_dir}/#{drupalvm_path}"
end
Copy link
Collaborator Author

@oxyc oxyc Mar 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One annoying thing here is that this wouldn't support forks... any ideas?


# Read config_dir from composer.json if set.
if cconfig.include?('config_dir')
host_config_dir = "#{host_project_dir}/#{cconfig['config_dir']}"
guest_config_dir = "#{guest_project_dir}/#{cconfig['config_dir']}"
end
end

require "#{host_drupalvm_dir}/lib/drupalvm/vagrant"

default_config_file = "#{host_drupalvm_dir}/default.config.yml"
unless File.exist?(default_config_file)
Expand Down
14 changes: 12 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "geerlingguy/drupal-vm",
"type": "vm",
"type": "composer-plugin",
"description": "A VM for local Drupal development, built with Vagrant + Ansible",
"keywords": ["vagrant", "vm", "virtual machine", "drupal"],
"homepage": "https://www.drupalvm.com",
Expand All @@ -20,10 +20,20 @@
"source": "https://github.com/geerlingguy/drupal-vm",
"docs": "http://docs.drupalvm.com"
},
"require": {},
"require": {
"composer-plugin-api": "^1.0"
},
"config": {
"process-timeout": 1800
},
"autoload": {
"psr-4": {
"JJG\\DrupalVM\\": "composer/src/"
}
},
"extra": {
"class": "JJG\\DrupalVM\\Plugin"
},
"scripts": {
"run-tests": "./tests/run-tests.sh",
"docker-bake": "./provisioning/docker/bake.sh",
Expand Down
91 changes: 91 additions & 0 deletions composer/src/Plugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php
/**
* @file
* Contains JJG\DrupalVM|Plugin.
*/

namespace JJG\DrupalVM;

use Composer\Composer;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\Factory;
use Composer\IO\IOInterface;
use Composer\Plugin\PluginInterface;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;

class Plugin implements PluginInterface, EventSubscriberInterface {

/**
* @var \Composer\Composer
*/
protected $composer;

/**
* @var \Composer\IO\IOInterface
*/
protected $io;

/**
* {@inheritdoc}
*/
public function activate(Composer $composer, IOInterface $io) {
$this->composer = $composer;
$this->io = $io;
}

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return array(
ScriptEvents::POST_INSTALL_CMD => 'addVagrantfile',
ScriptEvents::POST_UPDATE_CMD => 'addVagrantfile',
);
}

/**
* Add/update project Vagrantfile.
*
* @param \Composer\Script\Event $event
*/
public function addVagrantfile(Event $event) {

$baseDir = dirname(Factory::getComposerFile());
$source = __DIR__ . '/../../Vagrantfile';
$target = $baseDir . '/Vagrantfile';

if (file_exists($source)) {
if (!file_exists($target) || md5_file($source) != md5_file($target)) {
$isLegacy = $this->isLegacyVagrantfile($target);

copy($source, $target);

$extra = $this->composer->getPackage()->getExtra();
if ($isLegacy && !isset($extra['drupalvm']['config_dir'])) {
$this->io->writeError(
'<warning>'
. 'Drupal VM has been updated and consequently written over your Vagrantfile which from now on will be managed by Drupal VM. '
. 'Due to this change, you are required to set the `config_dir` location in your composer.json file:'
. "\n"
. "\n $ composer config extra.drupalvm.config_dir <sub-directory>"
. "\n"
. '</warning>'
);
}
}
}
}

/**
* Return if the parent project is using the < 5.0.0 delegating Vagrantfile.
*
* @return bool
*/
private function isLegacyVagrantfile($vagrantfile) {
if (!file_exists($vagrantfile)) {
return false;
}
return strpos(file_get_contents($vagrantfile), '# Load the real Vagrantfile') !== false;
}
}
157 changes: 22 additions & 135 deletions docs/deployment/composer-dependency.md
Original file line number Diff line number Diff line change
@@ -1,151 +1,38 @@
To make future Drupal VM updates easier to integrate with an existing project, you might consider the more complex setup of installing Drupal VM as a `composer` dependency. Using a delegating `Vagrantfile` you are able to run `vagrant` commands anywhere in your project as well as separate your custom configuration files from Drupal VM's own files.
If you're setting up a new project you can get up and running quickly using [drupal-composer/drupal-project](https://github.com/drupal-composer/drupal-project) as a project template.

### Add Drupal VM as a Composer dependency

Add Drupal VM as a development dependency to your `composer.json`.

```
composer require --dev geerlingguy/drupal-vm
```

### Setup your configuration files

Add and configure the `config.yml` anywhere you like, in this example we place it in a `config/` directory.

_Note: This will be the directory where Drupal VM looks for other local configuration files as well. Such as `drupal_build_makefile` and `local.config.yml`._

```
├── composer.json
├── config/
│ ├── config.yml
│ ├── local.config.yml
│ └── Vagrantfile.local
├── docroot/
│ ├── ...
│ └── index.php
└── vendor/
├── ...
└── geerlingguy/
└── drupal-vm/
```

Change the [build strategy to use your `composer.json`](composer.md#using-composer-when-drupal-vm-is-a-composer-dependency-itself) file by setting:

```yaml
drupal_build_composer_project: false
drupal_build_composer: true
drupal_composer_path: false
drupal_composer_install_dir: "/var/www/drupalvm"
drupal_core_path: "{{ drupal_composer_install_dir }}/docroot"
```

If you intened to use the devel module, it must be added as a requirement to your `composer.json` file. Alternatively, if you do not intend to use it remove it from `drupal_enabled_modules` in your `config.yml` file:

```yaml
drupal_enabled_modules: []
```

If you're using `pre_provision_scripts` or `post_provision_scripts` you also need to adjust their paths to take into account the new directory structure. The examples used in `default.config.yml` assume the files are located in the Drupal VM directory. You can use the `config_dir` variable which is the absolute path of the directory where your `config.yml` is located.

```yaml
post_provision_scripts:
# The default provided in `default.config.yml`:
- "../../examples/scripts/configure-solr.sh"
# With Drupal VM as a Composer dependency:
- "{{ config_dir }}/../examples/scripts/configure-solr.sh"
```

### Create a delegating `Vagrantfile`

Create a delegating `Vagrantfile` that will catch all your `vagrant` commands and send them to Drupal VM's own `Vagrantfile`. Place this file in your project's root directory.

```ruby
# The absolute path to the root directory of the project. Both Drupal VM and
# the config file need to be contained within this path.
ENV['DRUPALVM_PROJECT_ROOT'] = "#{__dir__}"
# The relative path from the project root to the config directory where you
# placed your config.yml file.
ENV['DRUPALVM_CONFIG_DIR'] = "config"
# The relative path from the project root to the directory where Drupal VM is located.
ENV['DRUPALVM_DIR'] = "vendor/geerlingguy/drupal-vm"

# Load the real Vagrantfile
load "#{__dir__}/#{ENV['DRUPALVM_DIR']}/Vagrantfile"
```sh
composer create-project drupal-composer/drupal-project:8.x-dev <some-dir> --stability dev --no-interaction
```

When you issue `vagrant` commands anywhere in your project tree this file will be detected and used as a delegator for Drupal VM's own Vagrantfile.
### Add Drupal VM as a Composer dependency

Your project structure should now look like this:
Require Drupal VM as a development dependency to your project.

```
├── Vagrantfile
├── composer.json
├── config/
│ ├── config.yml
│ ├── local.config.yml
│ └── Vagrantfile.local
├── docroot/
│ ├── ...
│ └── index.php
└── vendor/
├── ...
└── geerlingguy/
└── drupal-vm/
```
composer require --dev geerlingguy/drupal-vm

### Provision the VM
_This command will scaffold a `Vagrantfile` in the root of your project. Feel free to add it to your `.gitignore` as the file is now managed by Drupal VM and will be reset each time you run `composer update`._

Finally provision the VM using the delegating `Vagrantfile`.
### Create a `config.yml` to configure the VM

```sh
vagrant up
```
Create and configure a config.yml file, eg. in `vm/config.yml`. You'll need at least the following variables:

_Important: you should never issue `vagrant` commands through Drupal VM's own `Vagrantfile` from now on. If you do, it will create a secondary VM in that directory._
# Change the build strategy to use a composer.json file.
drupal_build_composer: true
drupal_build_composer_project: false

## Drupal VM in a subdirectory without composer
# Tell Drupal VM that the composer.json file already exists and doesn't need to be transfered.
drupal_composer_path: false

If you're not using `composer` in your project you can still download Drupal VM (or add it as a git submodule) to any subdirectory in your project. As an example let's name that directory `box/`.
# Set the location of the composer.json file and Drupal core.
drupal_composer_install_dir: "/var/www/drupalvm"
drupal_core_path: "{{ drupal_composer_install_dir }}/web"
Copy link
Collaborator Author

@oxyc oxyc Jun 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be too much to print this in the console if Drupal VM is added and no prior Vagrantfile exists? It would just make getting started so much easier if you wouldn't have to visit the docs... Or scaffold the vm/config.yml file in case a Vagrantfile is missing.


```
├── docroot/
│ ├── ...
│ └── index.php
└── box/
├── ...
├── default.config.yml
└── Vagrantfile
```
_Note that `drupal_core_path` needs to match your `composer.json` configuration. `drupal-project` uses `web/` whereas Lightning and BLT uses `docroot/`_

Configure your `config.yml` as mentioned in the [`composer` section](#setup-your-configuration-files) above.
If you placed the `config.yml` file in a subdirectory, tell Drupal VM where by adding the location to your `composer.json`. If not, Drupal VM will look for all configuration files in the root of your project.

```yaml
post_provision_scripts:
# The default provided in `default.config.yml`:
- "../../examples/scripts/configure-solr.sh"
# With Drupal VM in a toplevel subdirectory
- "{{ config_dir }}/../examples/scripts/configure-solr.sh"
```
composer config extra.drupalvm.config_dir 'vm'

Your directory structure should now look like this:
## Patching Drupal VM

```
├── Vagrantfile
├── config/
│ ├── config.yml
│ ├── local.config.yml
│ └── Vagrantfile.local
├── docroot/
│ ├── ...
│ └── index.php
└── box/
├── ...
├── default.config.yml
└── Vagrantfile
```

Provision the VM using the delegating `Vagrantfile`.

```sh
vagrant up
```
If you need to patch something in Drupal VM that you're otherwise unable to configure, you can do so with the help of the `composer-patches` plugin. Read the [documentation on how to create and apply patches](../extending/patching.md).
16 changes: 3 additions & 13 deletions docs/deployment/composer.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
Drupal VM is configured to use `composer create-project` to build a Drupal 8 codebase by default but supports building Drupal from a custom `composer.json` file as well.

_Note that if you already have a project built using a `composer.json` file you should instead add [Drupal VM as a dependency](composer-dependency.md) of your project. The method described below will make it somewhat difficult to manage your `composer.json` together with Drupal VM as the file will be copied from `drupal_composer_path` into `drupal_composer_install_dir` as a secondary `composer.json`._
Copy link
Collaborator Author

@oxyc oxyc Apr 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you okay with this, or maybe too scary? Honestly I think we should default drupal_composer_path to no in favor of Drupal VM as a dependency. I think we've seen a few issues due to this particular build setup being a bit non intuitive and I dont see much use case for it other than a faster version of drupal_build_composer_project.

Copy link
Collaborator Author

@oxyc oxyc Apr 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or well that's a bit odd too. Maybe it should be renamed drupal_composer_copy_path so it makes more sense as to why you need to set it to false for the composer dependency setup.


1. Copy `example.drupal.composer.json` to `drupal.composer.json` and modify it to your liking.
2. Use the Composer build system by setting `drupal_build_composer: true` in your `config.yml` (make sure `drupal_build_makefile` and `drupal_build_composer_project` are set to `false`).
3. Ensure `drupal_core_path` points to the webroot directory: `drupal_core_path: {{ drupal_composer_install_dir }}/web`
Expand All @@ -11,18 +13,6 @@ drupal_build_composer: true
drupal_core_path: "{{ drupal_composer_install_dir }}/web"
```

_The file set in `drupal_composer_path` (which defaults to `drupal.composer.json`) will be copied from your host computer into the VM's `drupal_composer_install_dir` and renamed `composer.json`._

## Using Composer when [Drupal VM is a composer dependency itself](composer-dependency.md)

In the scenario where you have an existing `composer.json` in the root of your project, follow the usual steps for installing with a composer.json but instead of creating a `drupal.composer.json` file, disable the transfering of the file by setting `drupal_composer_path: false`, and change `drupal_composer_install_dir` to point to the the directory where it will be located. If `drupal_composer_path` is not truthy, Drupal VM assumes it already exists.

```yaml
drupal_build_composer_project: false
drupal_build_composer: true
drupal_composer_path: false
drupal_composer_install_dir: "/var/www/drupalvm"
drupal_core_path: "{{ drupal_composer_install_dir }}/docroot"
```
The file set in `drupal_composer_path` (which defaults to `drupal.composer.json`) will be copied from your host computer into the VM's `drupal_composer_install_dir` and renamed `composer.json`.

_Opting for composer based installs will most likely increase your VM's time to provision considerably. Find out how you can [improve composer build performance](../other/performance.md#improving-composer-build-performance)._
21 changes: 21 additions & 0 deletions docs/deployment/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
There are two supported methods of integrating Drupal VM with your site. You can either download Drupal VM and integrate your site inside the project, or you can add Drupal VM as a Composer dependency to your existing project.

## Download Drupal VM and integrate your project.

The easiest way to get started with Drupal VM is to download the [latest release](https://www.drupalvm.com/), open the terminal, `cd` to the directory, and type `vagrant up`.

Using this method you have various options of how your site will be built, or if it will be built by Drupal VM at all:

- [Build using a local codebase](../deployment/local-codebase.md)
- [Build using a Composer package](../deployment/composer-package.md) (default)
- [Build using a composer.json](../deployment/composer.md)
- [Build using a Drush Make file](../deployment/drush-make.md)
- [Deploy Drupal via Git](../deployment/git.md)

## Add Drupal VM as a Composer depedency to an existing project

_If you're using Composer to manage your project, having Drupal VM as dependency makes it easier to pull in future updates._

Using this method you only have one option of how your site will be built, it will be built using your parent project's `composer.json` file.

- [Add Drupal VM to your project using Composer](../deployment/composer-dependency.md)