<?php
namespace PhpBrew\Command;
use CLIFramework\Command;
use CLIFramework\ValueCollection;
use Exception;
use GetOptionKit\OptionCollection;
use PhpBrew\Build;
use PhpBrew\Config;
use PhpBrew\ConfigureParameters;
use PhpBrew\Distribution\DistributionUrlPolicy;
use PhpBrew\Downloader\DownloadFactory;
use PhpBrew\Exception\SystemCommandException;
use PhpBrew\ReleaseList;
use PhpBrew\Tasks\AfterConfigureTask;
use PhpBrew\Tasks\BeforeConfigureTask;
use PhpBrew\Tasks\BuildTask;
use PhpBrew\Tasks\ConfigureTask;
use PhpBrew\Tasks\DownloadTask;
use PhpBrew\Tasks\DSymTask;
use PhpBrew\Tasks\ExtractTask;
use PhpBrew\Tasks\InstallTask;
use PhpBrew\Tasks\MakeTask;
use PhpBrew\Tasks\PrepareDirectoryTask;
use PhpBrew\Tasks\TestTask;
use PhpBrew\VariantBuilder;
use PhpBrew\VariantParser;
use PhpBrew\VersionDslParser;
use SplFileInfo;
class InstallCommand extends Command
{
public function brief()
{
return 'Install php';
}
public function aliases()
{
return array('i', 'ins');
}
public function usage()
{
return 'phpbrew install [php-version] ([+variant...])';
}
public function arguments($args)
{
$args->add('version')->suggestions(function () {
$releaseList = ReleaseList::getReadyInstance();
$releases = $releaseList->getReleases();
$collection = new ValueCollection();
foreach ($releases as $major => $versions) {
$collection->group($major, "PHP $major", array_keys($versions));
}
$collection->group('pseudo', 'pseudo', array('latest', 'next'));
return $collection;
});
$args->add('variants')->multiple()->suggestions(function () {
$variants = new VariantBuilder();
$list = $variants->getVariantNames();
sort($list);
return array_map(function ($n) {
return '+' . $n;
}, $list);
});
}
public function parseSemanticOptions(array &$args)
{
$settings = array();
$definitions = array(
'as' => '*',
'like' => '*',
'using' => '*+',
);
foreach ($definitions as $k => $requirement) {
$idx = array_search($k, $args);
if ($idx !== false) {
if ($requirement == '*') {
list($key, $val) = array_splice($args, $idx, 2);
$settings[$key] = $val;
} elseif ($requirement == '*+') {
$values = array_splice($args, $idx, 2);
$key = array_shift($values);
$settings[$key] = $values;
}
}
}
return $settings;
}
* @param OptionCollection $opts
*/
public function options($opts)
{
$opts->add('test', 'Run tests after the installation.');
$opts->add(
'name:',
'The name of the installation. '
. 'By default the installed path is equal to the release version name (php-5.x.x), '
. 'however you can specify a custom name instead of the default `php-5.x.x`. For example, `myphp-5.3.2-dbg`'
)
->valueName('name');
$opts->add('post-clean', 'Run make clean after the installation.');
$opts->add(
'production',
'Use production configuration file. this installer will copy the php-production.ini into the etc directory.'
);
$opts->add('build-dir:', 'Specify the build directory. '
. 'the distribution tarball will be extracted to the directory you specified '
. 'instead of $PHPBREW_ROOT/build/{name}.')
->isa('dir')
;
$opts->add('root', 'Specify PHPBrew root instead of PHPBREW_ROOT');
$opts->add('home', 'Specify PHPBrew home instead of PHPBREW_HOME');
$opts->add('no-config-cache', 'Do not use config.cache for configure script.');
$opts->add('no-clean', 'Do not clean previously compiled objects before building PHP. '
. 'By default phpbrew will run `make clean` before running the configure script '
. 'to ensure everything is cleaned up.')
;
$opts->add('no-patch', 'Do not apply any patch');
$opts->add('no-configure', 'Do not run configure script');
$opts->add('no-install', 'Do not install, just run build the target');
$opts->add(
'n|nice:',
'Runs build processes at an altered scheduling priority. '
. 'The priority can be adjusted over a range of -20 (the highest) to 20 (the lowest).'
)
->valueName('priority');
$opts->add('patch+:', 'Apply patch before build.')
->isa('file');
$opts->add('old', 'Install phpbrew incompatible phps (< 5.3)');
$opts->add('user-config', 'Allow users create their own config file (php.ini or extension config init files)');
DownloadFactory::addOptionsForCommand($opts);
$opts->add('f|force', 'Force the installation (redownloads source).')
->defaultValue(false)
;
$opts->add('d|dryrun', 'Do not build, but run through all the tasks.');
$opts->add(
'like:',
'Inherit variants from an existing build. '
. 'This option would require an existing build directory from the {version}.'
)
->valueName('version');
$opts->add('j|jobs:', 'Specifies the number of jobs to run build simultaneously (make -jN).')
->valueName('concurrent job number')
;
$opts->add('stdout', 'Outputs install logs to stdout.');
$opts->add('sudo', 'sudo to run install command.');
}
public function execute($version)
{
if (extension_loaded('posix') && posix_getuid() === 0) {
$this->logger->warn(
"*WARNING* You're running phpbrew as root/sudo. Unless you're going to install "
. "system-wide phpbrew, this might cause problems."
);
sleep(3);
}
$distUrl = null;
$versionInfo = array();
$releaseList = ReleaseList::getReadyInstance($this->options);
$versionDslParser = new VersionDslParser();
$clean = new MakeTask($this->logger, $this->options);
$clean->setQuiet();
if ($root = $this->options->root) {
Config::setPhpbrewRoot($root);
}
if ($home = $this->options->home) {
Config::setPhpbrewHome($home);
}
if ('latest' === strtolower($version)) {
$version = $releaseList->getLatestVersion();
}
if ('next' === strtolower($version)) {
$version = 'github.com/php/php-src:master';
}
if ($info = $versionDslParser->parse($version)) {
$version = $info['version'];
$distUrl = $info['url'];
$this->options['force']->setValue(
empty($info['is_tag'])
);
} else {
$version = preg_replace('/^php-/', '', $version);
$versionInfo = $releaseList->getVersion($version);
if (!$versionInfo) {
throw new Exception("Version $version not found.");
}
$version = $versionInfo['version'];
$distUrlPolicy = new DistributionUrlPolicy();
$distUrl = $distUrlPolicy->buildUrl($version, $versionInfo['filename'], $versionInfo['museum']);
}
$args = func_get_args();
array_shift($args);
$semanticOptions = $this->parseSemanticOptions($args);
$buildAs = isset($semanticOptions['as']) ? $semanticOptions['as'] : $this->options->name;
$buildLike = isset($semanticOptions['like']) ? $semanticOptions['like'] : $this->options->like;
if ($this->options->patch) {
$patchPaths = array();
foreach ($this->options->patch as $patch) {
$patchPath = realpath($patch);
if ($patchPath !== false) {
$patchPaths[(string) $patch] = $patchPath;
}
}
$this->options->keys['patch']->value = $patchPaths;
}
$build = new Build($version, $buildAs);
$installPrefix = Config::getInstallPrefix() . DIRECTORY_SEPARATOR . $build->getName();
if (!file_exists($installPrefix)) {
if (!mkdir($installPrefix, 0755, true) && !is_dir($installPrefix)) {
throw new \RuntimeException(sprintf('Directory "%s" was not created', $installPrefix));
}
}
$build->setInstallPrefix($installPrefix);
if ($buildLike) {
if ($parentBuild = Build::findByName($buildLike)) {
$this->logger->info("===> Loading build settings from $buildLike");
$build->loadVariantInfo($parentBuild->settings->toArray());
}
}
$msg = "===> phpbrew will now build {$build->getVersion()}";
if ($buildLike) {
$msg .= ' using variants from ' . $buildLike;
}
if (isset($semanticOptions['using'])) {
$msg .= ' plus custom variants: ' . implode(', ', $semanticOptions['using']);
$args = array_merge($args, $semanticOptions['using']);
}
if ($buildAs) {
$msg .= ' as ' . $buildAs;
}
$this->logger->info($msg);
if (!empty($args)) {
$this->logger->debug("---> Parsing variants from command arguments '" . implode(' ', $args) . "'");
}
$variantInfo = VariantParser::parseCommandArguments($args);
$build->loadVariantInfo($variantInfo);
if (!$variantInfo['enabled_variants']) {
$build->settings->enableVariant('default');
$this->logger->notice(
"You haven't enabled any variants. The default variant will be enabled: "
);
$builder = new VariantBuilder();
$this->logger->notice('[' . implode(', ', $builder->virtualVariants['default']) . ']');
$this->logger->notice("Please run 'phpbrew variants' for more information." . PHP_EOL);
}
if (preg_match('/5\.3\./', $version)) {
$this->logger->notice('PHP 5.3 requires +intl, enabled by default.');
$build->enableVariant('intl');
}
if (!in_array('--without-pear', $variantInfo['extra_options'])) {
$build->enableVariant('xml');
}
$this->logger->info('===> Loading and resolving variants...');
$build->loadVariantInfo($variantInfo);
$prepareTask = new PrepareDirectoryTask($this->logger, $this->options);
$prepareTask->run($build);
$buildDir = $this->options->{'build-dir'} ?: Config::getBuildDir();
if (!file_exists($buildDir)) {
if (!mkdir($buildDir, 0755, true) && !is_dir($buildDir)) {
throw new \RuntimeException(sprintf('Directory "%s" was not created', $buildDir));
}
}
$parameters = new ConfigureParameters();
if (!$this->options->{'no-config-cache'}) {
$parameters = $parameters->withOption('--cache-file', Config::getCacheDir() . '/config.cache');
}
$prefix = $build->getInstallPrefix();
$parameters = $parameters
->withOption('--prefix', $prefix);
if ($build->compareVersion('5.4') == -1) {
$parameters = $parameters
->withOption('--enable-sqlite-utf8')
->withOption('--enable-zend-multibyte');
} elseif ($build->compareVersion('5.6') == -1) {
$parameters = $parameters->withOption('--enable-zend-signals');
}
$variantBuilder = new VariantBuilder();
$parameters = $variantBuilder->build($build, $parameters);
$pkgConfigPaths = getenv('PKG_CONFIG_PATH');
if ($pkgConfigPaths !== '' && $pkgConfigPaths !== false) {
foreach (explode(PATH_SEPARATOR, $pkgConfigPaths) as $pkgConfigPath) {
$parameters = $parameters->withPkgConfigPath($pkgConfigPath);
}
}
$distFileDir = Config::getDistFileDir();
$downloadTask = new DownloadTask($this->logger, $this->options);
$algo = 'md5';
$hash = null;
if (isset($versionInfo['sha256'])) {
$algo = 'sha256';
$hash = $versionInfo['sha256'];
} elseif (isset($versionInfo['md5'])) {
$algo = 'md5';
$hash = $versionInfo['md5'];
}
$targetFilePath = $downloadTask->download($distUrl, $distFileDir, $algo, $hash);
if (!file_exists($targetFilePath)) {
throw new SystemCommandException("Download failed, $targetFilePath does not exist.", $build);
}
unset($downloadTask);
$extractTask = new ExtractTask($this->logger, $this->options);
$targetDir = $extractTask->extract($build, $targetFilePath, $buildDir);
if (!file_exists($targetDir)) {
throw new SystemCommandException("Extract failed, $targetDir does not exist.", $build);
}
unset($extractTask);
$this->logger->debug('Source Directory: ' . realpath($targetDir));
$build->setSourceDirectory($targetDir);
if (!$this->options->{'no-clean'} && file_exists($targetDir . DIRECTORY_SEPARATOR . 'Makefile')) {
if ($build->isEnabledVariant('apxs2') || $build->isEnabledVariant('fpm')) {
$this->logger->info('You want to build several variants. No clean is not available.');
} else {
$this->logger->info(
'Found existing Makefile, running make clean to ensure everything will be rebuilt.'
);
$this->logger->info(
"You can append --no-clean option after the install command if you don't want to rebuild."
);
$clean->clean($build);
}
}
chdir($targetDir);
$variantInfoFile = $build->getInstallPrefix() . DIRECTORY_SEPARATOR . 'phpbrew.variants';
$this->logger->debug("Writing variant info to $variantInfoFile");
if (false === $build->writeVariantInfoFile($variantInfoFile)) {
$this->logger->warn("Can't store variant info.");
}
$targetPaths = array();
if (!$this->options->{'no-configure'}) {
$options = $parameters->getOptions();
$sapis = array('cli' => array(
'enable' => array('--enable-cli'),
'disable' => array('--disable-cli')
));
if ($build->isEnabledVariant('apxs2')) {
$sapis['apache2'] = array(
'enable' => array('--with-apxs2'),
'disable' => array(),
);
}
if ($build->isEnabledVariant('fpm')) {
$addedOptions = array('--enable-fpm');
if (PHP_OS === 'Linux') {
$addedOptions[] = '--with-fpm-systemd';
}
$sapis['fpm'] = array(
'enable' => $addedOptions,
'disable' => array(),
);
}
foreach ($sapis as $sapi => $enableDisable) {
$this->logger->info('Running make clean to ensure everything will be rebuilt.');
$clean->clean($build);
$addedOptions = $removedOptions = array();
foreach ($sapis as $sapiName => $sapiEnableDisable) {
list($addedOptions, $removedOptions) = $this->enableDisable(
$parameters,
$sapiEnableDisable,
$sapiName === $sapi,
$options
);
}
$parameters = $parameters
->withOption('--with-config-file-path', $prefix . '/etc/' . $sapi)
->withOption('--with-config-file-scan-dir', $prefix . '/var/db/' . $sapi);
$this->build($build, $parameters);
foreach ($addedOptions as $addedOption) {
$parameters = $parameters->withoutOption($addedOption);
}
foreach ($removedOptions as $removedOption) {
$parameters = $parameters->withOption(
$removedOption,
array_key_exists($removedOption, $options) ? $options[$removedOption] : null
);
}
{
$buildTask = new BuildTask($this->logger, $this->options);
$buildTask->run($build);
unset($buildTask);
}
if ($this->options->{'test'}) {
$testTask = new TestTask($this->logger, $this->options);
$testTask->run($build);
unset($testTask);
}
if (!$this->options->{'no-install'}) {
$installTask = new InstallTask($this->logger, $this->options);
$installTask->install($build);
unset($installTask);
}
if ($this->options->{'post-clean'}) {
$clean->clean($build);
}
{
$dsym = new DSymTask($this->logger, $this->options);
$dsym->patch($build, $this->options);
}
$etcDirectory = $build->getEtcDirectory();
if ('fpm' === $sapi) {
$this->logger->info('---> Creating php-fpm.conf');
$fpmUnixSocket = $build->getInstallPrefix() . "/var/run/php-fpm.sock";
$this->installAs("$etcDirectory/php-fpm.conf.default", "$etcDirectory/php-fpm.conf");
$this->installAs("$etcDirectory/php-fpm.d/www.conf.default", "$etcDirectory/php-fpm.d/www.conf");
$patchingFiles = array("$etcDirectory/php-fpm.d/www.conf", "$etcDirectory/php-fpm.conf");
foreach ($patchingFiles as $patchingFile) {
if (file_exists($patchingFile)) {
$this->logger->info("---> Found $patchingFile");
$ini = file_get_contents($patchingFile);
$this->logger->info("---> Patching default fpm pool listen path to $fpmUnixSocket");
$ini = preg_replace('/^listen = .*$/m', "listen = $fpmUnixSocket" . PHP_EOL, $ini);
file_put_contents($patchingFile, $ini);
break;
}
}
}
$this->logger->info('---> Creating php.ini');
$phpConfigPath = $build->getSourceDirectory()
. DIRECTORY_SEPARATOR . ($this->options->production ? 'php.ini-production' : 'php.ini-development');
$this->logger->info("---> Copying $phpConfigPath ");
if (file_exists($phpConfigPath) && !$this->options->dryrun) {
$targetConfigPath = $this->makeIniFile($build, $etcDirectory, $phpConfigPath, $sapi);
$targetPaths[] = ' ' . $targetConfigPath;
}
}
}
if ($build->isEnabledVariant('pear')) {
$this->logger->info('Initializing pear config...');
$home = Config::getHome();
@mkdir("$home/tmp/pear/temp", 0755, true);
@mkdir("$home/tmp/pear/cache_dir", 0755, true);
@mkdir("$home/tmp/pear/download_dir", 0755, true);
system("pear config-set temp_dir $home/tmp/pear/temp");
system("pear config-set cache_dir $home/tmp/pear/cache_dir");
system("pear config-set download_dir $home/tmp/pear/download_dir");
$this->logger->info('Enabling pear auto-discover...');
system('pear config-set auto_discover 1');
}
$this->logger->debug('Source directory: ' . $targetDir);
$buildName = $build->getName();
$this->logger->info("Congratulations! Now you have PHP with $version as $buildName");
if ($build->isEnabledVariant('pdo') && $build->isEnabledVariant('mysql')) {
echo <<<EOT
* We found that you enabled 'mysql' variant, you might need to setup your
'pdo_mysql.default_socket' or 'mysqli.default_socket' in your php.ini file.
EOT;
}
if (count($targetPaths)) {
$targetConfigPaths = implode(PHP_EOL, $targetPaths);
echo <<<EOT
* To configure your installed PHP further, you can edit the config file(s) at
$targetConfigPaths
EOT;
}
if (!file_exists(Config::getHome() . DIRECTORY_SEPARATOR . 'bashrc')) {
echo <<<EOT
* WARNING:
You haven't run 'phpbrew init' yet! Be sure to setup your phpbrew to use your own php(s)
Please run 'phpbrew init' to setup your phpbrew in place.
EOT;
}
if (!getenv('PHPBREW_HOME')) {
echo <<<EOT
* WARNING:
You haven't setup your .bashrc file to load phpbrew shell script yet!
Please run 'phpbrew init' to see the steps!
EOT;
}
echo <<<EOT
To use the newly built PHP, try the line(s) below:
$ phpbrew use $buildName
Or you can use switch command to switch your default php to $buildName:
$ phpbrew switch $buildName
Enjoy!
EOT;
}
protected function installAs($source, $target, $override = false)
{
if (file_exists($source)) {
if ($override || !file_exists($target)) {
return copy($source, $target);
} else {
$this->logger->notice("Found existing $target.");
return false;
}
}
}
* @param Build $build
* @param string $etcDirectory
* @param string $phpConfigPath
* @param string $sapi
* @return string
*/
private function makeIniFile(Build $build, $etcDirectory, $phpConfigPath, $sapi)
{
$sapiEtcDirectory = $etcDirectory . DIRECTORY_SEPARATOR . $sapi;
if (!is_dir($sapiEtcDirectory) && mkdir($sapiEtcDirectory) && !is_dir($sapiEtcDirectory)) {
$this->logger->error("Can't create config directory " . $sapiEtcDirectory);
}
$extensionsConfig = $etcDirectory . '/../var/db/' . $sapi;
if (!is_dir($extensionsConfig) && mkdir($extensionsConfig, 0777, true) && !is_dir($extensionsConfig)) {
$this->logger->error("Can't create config directory " . $sapiEtcDirectory);
}
$targetConfigPath = $sapiEtcDirectory . DIRECTORY_SEPARATOR . 'php.ini';
if (file_exists($targetConfigPath)) {
$this->logger->notice("Found existing $targetConfigPath.");
} else {
copy($phpConfigPath, $targetConfigPath);
}
if (!$this->options->{'no-patch'}) {
$config = parse_ini_file($targetConfigPath, true);
$configContent = file_get_contents($targetConfigPath);
if (!isset($config['date']['timezone'])) {
$this->logger->info('---> Found date.timezone is not set, patching...');
if ($timezone = ini_get('date.timezone')) {
$this->logger->info("---> Found date.timezone, patching config timezone with $timezone");
$configContent = preg_replace(
'/^;?date.timezone\s*=\s*.*/im',
"date.timezone = $timezone",
$configContent
);
}
}
if (!isset($config['phar']['readonly'])) {
$pharReadonly = ini_get('phar.readonly');
if (!$pharReadonly) {
$this->logger->info('---> Disabling phar.readonly option.');
$configContent = preg_replace(
'/^;?phar.readonly\s*=\s*.*/im',
'phar.readonly = 0',
$configContent
);
}
}
if ($build->compareVersion('5.4') < 0) {
$this->logger->info("---> Turn off detect_encoding for php 5.3.*");
$configContent = $configContent . PHP_EOL . "detect_unicode = Off" . PHP_EOL;
}
file_put_contents($targetConfigPath, $configContent);
}
return $targetConfigPath;
}
private function build(Build $build, ConfigureParameters $parameters)
{
$configureTask = new BeforeConfigureTask($this->logger, $this->options);
$configureTask->run($build, $parameters);
unset($configureTask);
$configureTask = new ConfigureTask($this->logger, $this->options);
$configureTask->run($build, $parameters);
unset($configureTask);
$configureTask = new AfterConfigureTask($this->logger, $this->options);
$configureTask->run($build);
unset($configureTask);
}
* @param ConfigureParameters $parameters
* @param array $sapiSettings
* @param bool $currentSapi
* @param array $defaults
*
* @return array
*/
private function enableDisable(&$parameters, $sapiSettings, $currentSapi, $defaults)
{
$addedOptions = $removedOptions = array();
$enableOptions = $currentSapi ? $sapiSettings['enable'] : $sapiSettings['disable'];
$disableOptions = $currentSapi ? $sapiSettings['disable'] : $sapiSettings['enable'];
foreach ($enableOptions as $configOption) {
$addedOptions[] = $configOption;
$parameters = $parameters->withOption(
$configOption,
array_key_exists($configOption, $defaults) ? $defaults[$configOption] : null
);
}
foreach ($disableOptions as $configOption) {
$removedOptions[] = $configOption;
$parameters = $parameters->withoutOption($configOption);
}
return array($addedOptions, $removedOptions);
}
}