Commit 573702f6 authored by larowlan's avatar larowlan

Issue #2911319 by alexpott, dawehner, mglaman, mradcliffe, heddn, pbirk,...

Issue #2911319 by alexpott, dawehner, mglaman, mradcliffe, heddn, pbirk, danquah, maxstarkenburg, geerlingguy, vaplas, Chelsee, ressa, Chi, jonathanshaw, Mile23, andypost, larowlan, phenaproxima, droffats, kim.pepper, scotty, dsnopek, cashwilliams, borisson_, Mixologic, gerzenstl, David Radcliffe: Provide a single command to install & run Drupal
parent b607eb2a
......@@ -6,6 +6,17 @@ To use SQLite with your Drupal installation, the following requirements must be
met: Server has PHP 5.3.10 or later with PDO, and the PDO SQLite driver must be
enabled.
If you have not pdo_sqlite available depending on your system there are different ways to install it.
Windows
-------
Read more about it on http://www.php.net/manual/en/pdo.installation.php
Linux
-----
sudo apt-get install php-sqlite3
SQLITE DATABASE CREATION
------------------------
......
......@@ -2,6 +2,7 @@
CONTENTS OF THIS FILE
---------------------
* Quickstart
* Requirements and notes
* Optional server requirements
* Installation
......@@ -10,6 +11,35 @@ CONTENTS OF THIS FILE
* Multisite configuration
* Multilingual configuration
QUICKSTART
----------------------
Prerequisites:
- PHP 5.5.9 (or greater) (https://php.net).
In the instructions below, replace the version x.y.z with the specific version
you wish to download. Example: 8.6.0.zip. You can find the latest stable version
at https://www.drupal.org/project/drupal.
Download and extract the Drupal package:
- curl -sS https://ftp.drupal.org/files/projects/drupal-x.y.z.zip --output drupal-x.y.z.zip
- unzip drupal-x.y.z.zip
- cd /path/to/drupal-x.y.z
- php core/scripts/drupal quick-start
Wait… installation can take a minute or two. A successful installation will
result in opening the new site in your browser.
Run the following command for a list of available options that you may need to
configure quick-start:
- php core/scripts/drupal quick-start --help
Follow the instructions in the REINSTALL section below to start over.
NOTE: This quick start solution uses PHP's built-in web server and is not
intended for production use. Read more about how to run Drupal in a production
environment below.
REQUIREMENTS AND NOTES
----------------------
......
......@@ -89,10 +89,13 @@
* page request (optimized for the command line) and not send any output
* intended for the web browser. See install_state_defaults() for a list of
* elements that are allowed to appear in this array.
* @param callable $callback
* (optional) A callback to allow command line processes to update a progress
* bar. The callback is passed the $install_state variable.
*
* @see install_state_defaults()
*/
function install_drupal($class_loader, $settings = []) {
function install_drupal($class_loader, $settings = [], callable $callback = NULL) {
// Support the old way of calling this function with just a settings array.
// @todo Remove this when Drush is updated in the Drupal testing
// infrastructure in https://www.drupal.org/node/2389243
......@@ -114,7 +117,7 @@ function install_drupal($class_loader, $settings = []) {
install_begin_request($class_loader, $install_state);
// Based on the installation state, run the remaining tasks for this page
// request, and collect any output.
$output = install_run_tasks($install_state);
$output = install_run_tasks($install_state, $callback);
}
catch (InstallerException $e) {
// In the non-interactive installer, exceptions are always thrown directly.
......@@ -289,6 +292,8 @@ function install_state_defaults() {
* @param $install_state
* An array of information about the current installation state. This is
* modified with information gleaned from the beginning of the page request.
*
* @see install_drupal()
*/
function install_begin_request($class_loader, &$install_state) {
$request = Request::createFromGlobals();
......@@ -328,7 +333,7 @@ function install_begin_request($class_loader, &$install_state) {
date_default_timezone_set('Australia/Sydney');
}
$site_path = DrupalKernel::findSitePath($request, FALSE);
$site_path = empty($install_state['site_path']) ? DrupalKernel::findSitePath($request, FALSE) : $install_state['site_path'];
Settings::initialize(dirname(dirname(__DIR__)), $site_path, $class_loader);
// Ensure that procedural dependencies are loaded as early as possible,
......@@ -539,11 +544,14 @@ function install_begin_request($class_loader, &$install_state) {
* @param $install_state
* An array of information about the current installation state. This is
* passed along to each task, so it can be modified if necessary.
* @param callable $callback
* (optional) A callback to allow command line processes to update a progress
* bar. The callback is passed the $install_state variable.
*
* @return
* HTML output from the last completed task.
*/
function install_run_tasks(&$install_state) {
function install_run_tasks(&$install_state, callable $callback = NULL) {
do {
// Obtain a list of tasks to perform. The list of tasks itself can be
// dynamic (e.g., some might be defined by the installation profile,
......@@ -575,6 +583,9 @@ function install_run_tasks(&$install_state) {
\Drupal::state()->set('install_task', $install_state['installation_finished'] ? 'done' : $task_name);
}
}
if ($callback) {
$callback($install_state);
}
// Stop when there are no tasks left. In the case of an interactive
// installation, also stop if we have some output to send to the browser,
// the URL parameters have changed, or an end to the page request was
......
This diff is collapsed.
<?php
namespace Drupal\Core\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Installs a Drupal site and starts a webserver for local testing/development.
*
* Wraps 'install' and 'server' commands.
*
* @internal
* This command makes no guarantee of an API for Drupal extensions.
*
* @see \Drupal\Core\Command\InstallCommand
* @see \Drupal\Core\Command\ServerCommand
*/
class QuickStartCommand extends Command {
/**
* {@inheritdoc}
*/
protected function configure() {
$this->setName('quick-start')
->setDescription('Installs a Drupal site and runs a web server. This is not meant for production and might be too simple for custom development. It is a quick and easy way to get Drupal running.')
->addArgument('install-profile', InputArgument::OPTIONAL, 'Install profile to install the site in.')
->addOption('langcode', NULL, InputOption::VALUE_OPTIONAL, 'The language to install the site in. Defaults to en.', 'en')
->addOption('site-name', NULL, InputOption::VALUE_OPTIONAL, 'Set the site name. Defaults to Drupal.', 'Drupal')
->addOption('host', NULL, InputOption::VALUE_OPTIONAL, 'Provide a host for the server to run on. Defaults to 127.0.0.1.', '127.0.0.1')
->addOption('port', NULL, InputOption::VALUE_OPTIONAL, 'Provide a port for the server to run on. Will be determined automatically if none supplied.')
->addOption('suppress-login', 's', InputOption::VALUE_NONE, 'Disable opening a login URL in a browser.')
->addUsage('demo_umami --langcode fr')
->addUsage('standard --site-name QuickInstall --host localhost --port 8080')
->addUsage('minimal --host my-site.com --port 80');
parent::configure();
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output) {
$command = $this->getApplication()->find('install');
$arguments = [
'command' => 'install',
'install-profile' => $input->getArgument('install-profile'),
'--langcode' => $input->getOption('langcode'),
'--site-name' => $input->getOption('site-name'),
];
$installInput = new ArrayInput($arguments);
$returnCode = $command->run($installInput, $output);
if ($returnCode === 0) {
$command = $this->getApplication()->find('server');
$arguments = [
'command' => 'server',
'--host' => $input->getOption('host'),
'--port' => $input->getOption('port'),
];
if ($input->getOption('suppress-login')) {
$arguments['--suppress-login'] = TRUE;
}
$serverInput = new ArrayInput($arguments);
$returnCode = $command->run($serverInput, $output);
}
return $returnCode;
}
}
<?php
namespace Drupal\Core\Command;
use Drupal\Core\Database\ConnectionNotDefinedException;
use Drupal\Core\DrupalKernel;
use Drupal\Core\DrupalKernelInterface;
use Drupal\Core\Site\Settings;
use Drupal\user\Entity\User;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\PhpProcess;
use Symfony\Component\Process\Process;
/**
* Runs the PHP webserver for a Drupal site for local testing/development.
*
* @internal
* This command makes no guarantee of an API for Drupal extensions.
*/
class ServerCommand extends Command {
/**
* The class loader.
*
* @var object
*/
protected $classLoader;
/**
* Constructs a new ServerCommand command.
*
* @param object $class_loader
* The class loader.
*/
public function __construct($class_loader) {
parent::__construct('server');
$this->classLoader = $class_loader;
}
/**
* {@inheritdoc}
*/
protected function configure() {
$this->setDescription('Starts up a webserver for a site.')
->addOption('host', NULL, InputOption::VALUE_OPTIONAL, 'Provide a host for the server to run on.', '127.0.0.1')
->addOption('port', NULL, InputOption::VALUE_OPTIONAL, 'Provide a port for the server to run on. Will be determined automatically if none supplied.')
->addOption('suppress-login', 's', InputOption::VALUE_NONE, 'Disable opening a login URL in a browser.')
->addUsage('--host localhost --port 8080')
->addUsage('--host my-site.com --port 80');
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output) {
$io = new SymfonyStyle($input, $output);
$host = $input->getOption('host');
$port = $input->getOption('port');
if (!$port) {
$port = $this->findAvailablePort($host);
}
if (!$port) {
$io->getErrorStyle()->error('Unable to automatically determine a port. Use the --port to hardcode an available port.');
}
try {
$kernel = $this->boot();
}
catch (ConnectionNotDefinedException $e) {
$io->getErrorStyle()->error("No installation found. Use the 'install' command.");
return 1;
}
return $this->start($host, $port, $kernel, $input, $io);
}
/**
* Boots up a Drupal environment.
*
* @return \Drupal\Core\DrupalKernelInterface
* The Drupal kernel.
*
* @throws \Exception
* Exception thrown if kernel does not boot.
*/
protected function boot() {
$kernel = new DrupalKernel('prod', $this->classLoader, FALSE);
$kernel::bootEnvironment();
$kernel->setSitePath($this->getSitePath());
Settings::initialize($kernel->getAppRoot(), $kernel->getSitePath(), $this->classLoader);
$kernel->boot();
// Some services require a request to work. For example, CommentManager.
// This is needed as generating the URL fires up entity load hooks.
$kernel->getContainer()
->get('request_stack')
->push(Request::createFromGlobals());
return $kernel;
}
/**
* Finds an available port.
*
* @param string $host
* The host to find a port on.
*
* @return int|false
* The available port or FALSE, if no available port found,
*/
protected function findAvailablePort($host) {
$port = 8888;
while ($port >= 8888 && $port <= 9999) {
$connection = @fsockopen($host, $port);
if (is_resource($connection)) {
// Port is being used.
fclose($connection);
}
else {
// Port is available.
return $port;
}
$port++;
}
return FALSE;
}
/**
* Opens a URL in your system default browser.
*
* @param string $url
* The URL to browser to.
* @param \Symfony\Component\Console\Style\SymfonyStyle $io
* The IO.
*/
protected function openBrowser($url, SymfonyStyle $io) {
$is_windows = defined('PHP_WINDOWS_VERSION_BUILD');
if ($is_windows) {
// Handle escaping ourselves.
$cmd = 'start "web" "' . $url . '""';
}
else {
$url = escapeshellarg($url);
}
$is_linux = (new Process('which xdg-open'))->run();
$is_osx = (new Process('which open'))->run();
if ($is_linux === 0) {
$cmd = 'xdg-open ' . $url;
}
elseif ($is_osx === 0) {
$cmd = 'open ' . $url;
}
if (empty($cmd)) {
$io->getErrorStyle()
->error('No suitable browser opening command found, open yourself: ' . $url);
return;
}
if ($io->isVerbose()) {
$io->writeln("<info>Browser command:</info> $cmd");
}
// Need to escape double quotes in the command so the PHP will work.
$cmd = str_replace('"', '\"', $cmd);
// Sleep for 2 seconds before opening the browser. This allows the command
// to start up the PHP built-in webserver in the meantime. We use a
// PhpProcess so that Windows powershell users also get a browser opened
// for them.
$php = "<?php sleep(2); passthru(\"$cmd\"); ?>";
$process = new PhpProcess($php);
$process->start();
return;
}
/**
* Gets a one time login URL for user 1.
*
* @return string
* The one time login URL for user 1.
*/
protected function getOneTimeLoginUrl() {
$user = User::load(1);
\Drupal::moduleHandler()->load('user');
return user_pass_reset_url($user);
}
/**
* Starts up a webserver with a running Drupal.
*
* @param string $host
* The hostname of the webserver.
* @param int $port
* The port to start the webserver on.
* @param \Drupal\Core\DrupalKernelInterface $kernel
* The Drupal kernel.
* @param \Symfony\Component\Console\Input\InputInterface $input
* The input.
* @param \Symfony\Component\Console\Style\SymfonyStyle $io
* The IO.
*
* @return int
* The exit status of the PHP in-built webserver command.
*/
protected function start($host, $port, DrupalKernelInterface $kernel, InputInterface $input, SymfonyStyle $io) {
$finder = new PhpExecutableFinder();
$binary = $finder->find();
if ($binary === FALSE) {
throw new \RuntimeException('Unable to find the PHP binary.');
}
$io->writeln("<info>Drupal development server started:</info> <http://{$host}:{$port}>");
$io->writeln('<info>This server is not meant for production use.</info>');
$one_time_login = "http://$host:$port{$this->getOneTimeLoginUrl()}/login";
$io->writeln("<info>One time login url:</info> <$one_time_login>");
$io->writeln('Press Ctrl-C to quit.');
if (!$input->getOption('suppress-login')) {
if ($this->openBrowser("$one_time_login?destination=" . urlencode("/"), $io) === 1) {
$io->error('Error while opening up a one time login URL');
}
}
// Use the Process object to construct an escaped command line.
$process = new Process([
$binary,
'-S',
$host . ':' . $port,
'.ht.router.php',
], $kernel->getAppRoot(), [], NULL, NULL);
if ($io->isVerbose()) {
$io->writeln("<info>Server command:</info> {$process->getCommandLine()}");
}
// Carefully manage output so we can display output only in verbose mode.
$descriptors = [];
$descriptors[0] = STDIN;
$descriptors[1] = ['pipe', 'w'];
$descriptors[2] = ['pipe', 'w'];
$server = proc_open($process->getCommandLine(), $descriptors, $pipes, $kernel->getAppRoot());
if (is_resource($server)) {
if ($io->isVerbose()) {
// Write a blank line so that server output and the useful information are
// visually separated.
$io->writeln('');
}
$server_status = proc_get_status($server);
while ($server_status['running']) {
if ($io->isVerbose()) {
fpassthru($pipes[2]);
}
sleep(1);
$server_status = proc_get_status($server);
}
}
return proc_close($server);
}
/**
* Gets the site path.
*
* Defaults to 'sites/default'. For testing purposes this can be overridden
* using the DRUPAL_DEV_SITE_PATH environment variable.
*
* @return string
* The site path to use.
*/
protected function getSitePath() {
return getenv('DRUPAL_DEV_SITE_PATH') ?: 'sites/default';
}
}
......@@ -6,6 +6,7 @@
use Composer\Script\Event;
use Composer\Installer\PackageEvent;
use Composer\Semver\Constraint\Constraint;
use Composer\Util\ProcessExecutor;
/**
* Provides static functions for composer script events.
......@@ -269,6 +270,13 @@ protected static function findPackageKey($package_name) {
return $package_key;
}
/**
* Removes Composer's timeout so that scripts can run indefinitely.
*/
public static function removeTimeout() {
ProcessExecutor::setTimeout(0);
}
/**
* Helper method to remove directories and the files they contain.
*
......
......@@ -3,6 +3,7 @@
namespace Drupal\Core\Installer;
use Drupal\Core\DrupalKernel;
use Symfony\Component\HttpFoundation\Request;
/**
* Extend DrupalKernel to handle force some kernel behaviors.
......@@ -66,4 +67,15 @@ public function getInstallProfile() {
return $profile;
}
/**
* {@inheritdoc}
*/
public static function createFromRequest(Request $request, $class_loader, $environment, $allow_dumping = TRUE, $app_root = NULL) {
// This override exists because we don't need to initialize the settings
// again as they already are in install_begin_request().
$kernel = new static($environment, $class_loader, $allow_dumping, $app_root);
static::bootEnvironment($app_root);
return $kernel;
}
}
#!/usr/bin/env php
<?php
/**
* @file
* Provides CLI commands for Drupal.
*/
use Drupal\Core\Command\QuickStartCommand;
use Drupal\Core\Command\InstallCommand;
use Drupal\Core\Command\ServerCommand;
use Symfony\Component\Console\Application;
if (PHP_SAPI !== 'cli') {
return;
}
$classloader = require_once __DIR__ . '/../../autoload.php';
$application = new Application('drupal', \Drupal::VERSION);
$application->add(new QuickStartCommand());
$application->add(new InstallCommand($classloader));
$application->add(new ServerCommand($classloader));
$application->run();
<?php
namespace Drupal\Tests\Core\Command;
use Drupal\Core\Test\TestDatabase;
use Drupal\Tests\BrowserTestBase;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;
/**
* Tests the quick-start commands.
*
* These tests are run in a separate process because they load Drupal code via
* an include.
*
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*
* @group Command
*/
class QuickStartTest extends TestCase {
/**
* The PHP executable path.
*
* @var string
*/
protected $php;
/**
* A test database object.
*
* @var \Drupal\Core\Test\TestDatabase
*/
protected $testDb;
/**
* The Drupal root directory.
*
* @var string
*/
protected $root;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$php_executable_finder = new PhpExecutableFinder();
$this->php = $php_executable_finder->find();
$this->root = dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__))));
chdir($this->root);
if (!is_writable("{$this->root}/sites/simpletest")) {
$this->markTestSkipped('This test requires a writable sites/simpletest directory');
}
// Get a lock and a valid site path.
$this->testDb = new TestDatabase();
}
/**
* {@inheritdoc}
*/
public function tearDown() {
if ($this->testDb) {
$test_site_directory = $this->root . DIRECTORY_SEPARATOR . $this->testDb->getTestSitePath();
if (file_exists($test_site_directory)) {
// @todo use the tear down command from
// https://www.drupal.org/project/drupal/issues/2926633
// Delete test site directory.
$this->fileUnmanagedDeleteRecursive($test_site_directory, [
BrowserTestBase::class,
'filePreDeleteCallback'
]);
}
}
parent::tearDown();
}
/**
* Tests the quick-start command.
*/
public function testQuickStartCommand() {
// Install a site using the standard profile to ensure the one time login
// link generation works.
$install_command = "{$this->php} core/scripts/drupal quick-start standard --site-name='Test site {$this->testDb->getDatabasePrefix()}' --suppress-login";
$process = new Process($install_command, NULL, ['DRUPAL_DEV_SITE_PATH' => $this->testDb->getTestSitePath()]);
$process->inheritEnvironmentVariables();
$process->setTimeout(500);
$process->start();
$guzzle = new Client();
$port = FALSE;
while ($process->isRunning()) {
if (preg_match('/127.0.0.1:(\d+)/', $process->getOutput(), $match)) {
$port = $match[1];
break;
}
// Wait for more output.
sleep(1);
}
// The progress bar uses STDERR to write messages.
$this->assertContains('Congratulations, you installed Drupal!', $process->getErrorOutput());
$this->assertNotFalse($port, "Web server running on port $port");
$this->assertContains("127.0.0.1:$port/user/reset/1/", $process->getOutput());
// Give the server a couple of seconds to be ready.
sleep(2);
// Generate a cookie so we can make a request against the installed site.
include $this->root . '/core/includes/bootstrap.inc';
define('DRUPAL_TEST_IN_CHILD_SITE', FALSE);
chmod($this->testDb->getTestSitePath(), 0755);
$cookieJar = CookieJar::fromArray([
'SIMPLETEST_USER_AGENT' => drupal_generate_test_ua($this->testDb->getDatabasePrefix())
], '127.0.0.1');
$response = $guzzle->get('http://127.0.0.1:' . $port, ['cookies' => $cookieJar]);
$content = (string) $response->getBody();
$this->assertContains('Test site ' . $this->testDb->getDatabasePrefix(), $content);
// Stop the web server.
$process->stop();
}
/**
* Tests the quick-start commands.
*/
public function testQuickStartInstallAndServerCommands() {
// Install a site.
$install_command = "{$this->php} core/scripts/drupal install testing --site-name='Test site {$this->testDb->getDatabasePrefix()}'";
$install_process = new Process($install_command, NULL, ['DRUPAL_DEV_SITE_PATH' => $this->testDb->getTestSitePath()]);
$install_process->inheritEnvironmentVariables();
$install_process->setTimeout(500);
$result = $install_process->run();
// The progress bar uses STDERR to write messages.
$this->assertContains('Congratulations, you installed Drupal!', $install_process->getErrorOutput());
$this->assertSame(0, $result);
// Run the PHP built-in webserver.
$server_command = "{$this->php} core/scripts/drupal server --suppress-login";
$server_process = new Process($server_command, NULL, ['DRUPAL_DEV_SITE_PATH' => $this->testDb->getTestSitePath()]);
$server_process->inheritEnvironmentVariables();
$server_process->start();
$guzzle = new Client();
$port = FALSE;
while ($server_process->isRunning()) {
if (preg_match('/127.0.0.1:(\d+)/', $server_process->getOutput(), $match)) {
$port = $match[1];
break;
}
// Wait for more output.
sleep(1);
}
$this->assertEquals('', $server_process->getErrorOutput());
$this->assertContains("127.0.0.1:$port/user/reset/1/", $server_process->getOutput());
$this->assertNotFalse($port, "Web server running on port $port");
// Give the server a couple of seconds to be ready.
sleep(2);
// Generate a cookie so we can make a request against the installed site.
include $this->root . '/core/includes/bootstrap.inc';
define('DRUPAL_TEST_IN_CHILD_SITE', FALSE);