mirror of
http://124.126.16.154:8888/singularity/hdk-skeleton.git
synced 2026-01-15 03:35:06 +08:00
631 lines
22 KiB
PHP
Executable File
631 lines
22 KiB
PHP
Executable File
<?php
|
||
|
||
declare(strict_types=1);
|
||
/**
|
||
* This file is part of Hyperf.
|
||
*
|
||
* @link https://www.hyperf.io
|
||
* @document https://hyperf.wiki
|
||
* @contact group@hyperf.io
|
||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||
*/
|
||
|
||
namespace Installer;
|
||
|
||
use Composer\Composer;
|
||
use Composer\Factory;
|
||
use Composer\IO\IOInterface;
|
||
use Composer\Json\JsonFile;
|
||
use Composer\Package\BasePackage;
|
||
use Composer\Package\Link;
|
||
use Composer\Package\RootPackageInterface;
|
||
use Composer\Package\Version\VersionParser;
|
||
use FilesystemIterator;
|
||
use RecursiveDirectoryIterator;
|
||
use RecursiveIteratorIterator;
|
||
|
||
class OptionalPackages
|
||
{
|
||
/**
|
||
* @const string Regular expression for matching package name and version
|
||
*/
|
||
public const PACKAGE_REGEX = '/^(?P<name>[^:]+\/[^:]+)([:]*)(?P<version>.*)$/';
|
||
public string $projectName;
|
||
/**
|
||
* @var IOInterface
|
||
*/
|
||
public IOInterface $io;
|
||
|
||
/**
|
||
* Assets to remove during cleanup.
|
||
*
|
||
* @var string[]
|
||
*/
|
||
private array $assetsToRemove = [
|
||
'.travis.yml',
|
||
];
|
||
|
||
/**
|
||
* @var array
|
||
*/
|
||
private array $config;
|
||
|
||
/**
|
||
* @var Composer
|
||
*/
|
||
private Composer $composer;
|
||
|
||
/**
|
||
* @var array
|
||
*/
|
||
private array $composerDefinition;
|
||
|
||
/**
|
||
* @var JsonFile
|
||
*/
|
||
private JsonFile $composerJson;
|
||
|
||
/**
|
||
* @var Link[]
|
||
*/
|
||
private array $composerRequires;
|
||
|
||
/**
|
||
* @var Link[]
|
||
*/
|
||
private array $composerDevRequires;
|
||
|
||
/**
|
||
* @var string[] Dev dependencies to remove after install is complete
|
||
*/
|
||
private array $devDependencies = [
|
||
'composer/composer',
|
||
];
|
||
|
||
/**
|
||
* @var string path to this file
|
||
*/
|
||
private string $installerSource;
|
||
|
||
/**
|
||
* @var string
|
||
*/
|
||
private string $projectRoot;
|
||
|
||
/**
|
||
* @var RootPackageInterface
|
||
*/
|
||
private RootPackageInterface $rootPackage;
|
||
|
||
/**
|
||
* @var int[]
|
||
*/
|
||
private array $stabilityFlags;
|
||
|
||
/** @var int */
|
||
private int $port;
|
||
private string $description = '清锋的又一个软件项目';
|
||
|
||
public function __construct(IOInterface $io, Composer $composer, string $projectRoot = null)
|
||
{
|
||
$this->io = $io;
|
||
$this->composer = $composer;
|
||
// Get composer.json location
|
||
$composerFile = Factory::getComposerFile();
|
||
// Calculate project root from composer.json, if necessary
|
||
$this->projectRoot = $projectRoot ?: realpath(dirname($composerFile));
|
||
$this->projectRoot = rtrim($this->projectRoot, '/\\') . '/';
|
||
|
||
// Parse the composer.json
|
||
$this->parseComposerDefinition($composer, $composerFile);
|
||
// Get optional packages configuration
|
||
$this->config = require __DIR__ . '/config.php';
|
||
// Source path for this file
|
||
$this->installerSource = realpath(__DIR__) . '/';
|
||
}
|
||
|
||
/**
|
||
* Parses the composer file and populates internal data.
|
||
*/
|
||
private function parseComposerDefinition(Composer $composer, string $composerFile): void
|
||
{
|
||
$this->composerJson = new JsonFile($composerFile);
|
||
$this->composerDefinition = $this->composerJson->read();
|
||
// Get root package or root alias package
|
||
$this->rootPackage = $composer->getPackage();
|
||
// Get required packages
|
||
$this->composerRequires = $this->rootPackage->getRequires();
|
||
$this->composerDevRequires = $this->rootPackage->getDevRequires();
|
||
// Get stability flags
|
||
$this->stabilityFlags = $this->rootPackage->getStabilityFlags();
|
||
}
|
||
|
||
public function installHyperfScript(): void
|
||
{
|
||
$default_timezone = ini_get('TIME_ZONE');
|
||
$default_timezone = empty($default_timezone) ? 'Asia/Shanghai' : $default_timezone;
|
||
$ask[] = "\n <question>What time zone do you want to set up ?</question>\n";
|
||
$ask[] = " [<comment>n</comment>] Default time zone for php.ini\n";
|
||
$ask[] = " Make your selection or type a time zone name, like Asia/Shanghai ($default_timezone):\n ";
|
||
$answer = $this->io->askAndValidate(
|
||
implode('', $ask),
|
||
function ($value) {
|
||
if ($value === 'y' || $value === 'yes') {
|
||
throw new \InvalidArgumentException(
|
||
'You should type a time zone name, like Asia/Shanghai. Or type n to skip.'
|
||
);
|
||
}
|
||
|
||
return trim($value);
|
||
},
|
||
null,
|
||
$default_timezone
|
||
);
|
||
|
||
if ($answer != 'n') {
|
||
$content = file_get_contents($this->installerSource . '/resources/bin/hyperf.stub');
|
||
$content = str_replace('%TIME_ZONE%', $answer, $content);
|
||
$target_path = $this->projectRoot . 'bin/hyperf.php';
|
||
if (!is_dir(dirname($target_path))) {
|
||
mkdir(dirname($target_path));
|
||
}
|
||
file_put_contents($this->projectRoot . 'bin/hyperf.php', $content);
|
||
}
|
||
}
|
||
|
||
public function setupProject(): void
|
||
{
|
||
$this->projectName = $this->io->askAndValidate(
|
||
"\n <question>这个项目的项目名是什么?</question> (必填项,格式类似 lux-studio)\n ",
|
||
function ($value) {
|
||
$pattern = '/^\s*lux(-([a-z0-9])+)+\s*$/';
|
||
preg_match($pattern, $value, $matches);
|
||
if (count($matches) <= 0) {
|
||
throw new \InvalidArgumentException('项目名非法!合法格式为:/^lux(-([a-z0-9])+)+$/');
|
||
}
|
||
|
||
return trim($matches[0]);
|
||
},
|
||
null,
|
||
''
|
||
);
|
||
$this->description = $this->io->askAndValidate(
|
||
"\n <question>一句话描述下这个项目吧,中文可能导致报错</question>(默认:{$this->description})\n ",
|
||
function ($value) {
|
||
return trim($value);
|
||
},
|
||
null,
|
||
$this->description
|
||
);
|
||
|
||
$ports_map = require __DIR__ . '/portMap.php';
|
||
$available_ports = array_keys($ports_map);
|
||
$max_port = max(...$available_ports);
|
||
$default_port = intval($max_port / 10) + 1 . '1';
|
||
|
||
$this->port = $this->io->askAndValidate(
|
||
"\n <question>你想使用哪个端口监听这个项目?</question> (默认:" . $default_port . ") \n ",
|
||
function ($input) use ($available_ports, $ports_map) {
|
||
if (in_array($input, $available_ports)) {
|
||
$error = <<<'ERROR'
|
||
|
||
端口已使用!
|
||
|
||
已用端口:
|
||
ERROR;
|
||
foreach ($ports_map as $port => $info) {
|
||
['project' => $project_name, 'type' => $type] = $info;
|
||
$error .= <<<ERROR
|
||
|
||
[:$port] $project_name 的 $type
|
||
ERROR;
|
||
}
|
||
throw new \InvalidArgumentException($error);
|
||
}
|
||
|
||
return (int)$input;
|
||
},
|
||
null,
|
||
$default_port
|
||
);
|
||
|
||
$composer_project_name = sprintf('web-service/%s', $this->projectName);
|
||
$this->composerDefinition['name'] = $composer_project_name;
|
||
$this->composerDefinition['description'] = $this->description;
|
||
}
|
||
|
||
public function updateDockerEnv(): void
|
||
{
|
||
$content = file_get_contents($this->projectRoot . 'installer/resources/scripts/docker-env.sh');
|
||
$lines = explode(PHP_EOL, $content);
|
||
|
||
$exports = [
|
||
'',
|
||
"project=$this->projectName",
|
||
"port=$this->port",
|
||
];
|
||
|
||
array_splice($lines, 1, 0, $exports);
|
||
|
||
file_put_contents($this->projectRoot . 'scripts/docker-env.sh', join(PHP_EOL, $lines));
|
||
}
|
||
|
||
public function updateJenkinsShell(): void
|
||
{
|
||
$content = file_get_contents($this->projectRoot . 'installer/resources/scripts/jenkins.sh');
|
||
$lines = explode(PHP_EOL, $content);
|
||
|
||
$exports = [
|
||
'',
|
||
"project=$this->projectName",
|
||
];
|
||
|
||
array_splice($lines, 1, 0, $exports);
|
||
|
||
file_put_contents($this->projectRoot . 'jenkins.sh', join(PHP_EOL, $lines));
|
||
}
|
||
|
||
/**
|
||
* Create data and cache directories, if not present.
|
||
*
|
||
* Also sets up appropriate permissions.
|
||
*/
|
||
public function setupRuntimeDir(): void
|
||
{
|
||
$this->io->write('<info>初始化缓存目录</info>');
|
||
$runtimeDir = $this->projectRoot . '/runtime';
|
||
|
||
if (!is_dir($runtimeDir)) {
|
||
mkdir($runtimeDir, 0775, true);
|
||
chmod($runtimeDir, 0775);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Cleanup development dependencies.
|
||
*
|
||
* The dev dependencies should be removed from the stability flags,
|
||
* require-dev and the composer file.
|
||
*/
|
||
public function removeDevDependencies(): void
|
||
{
|
||
$this->io->write('<info>移除安装器依赖</info>');
|
||
foreach ($this->devDependencies as $devDependency) {
|
||
unset($this->stabilityFlags[$devDependency], $this->composerDevRequires[$devDependency], $this->composerDefinition['require-dev'][$devDependency]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Prompt for each optional installation package.
|
||
*
|
||
* @codeCoverageIgnore
|
||
*/
|
||
public function promptForOptionalPackages(): void
|
||
{
|
||
foreach ($this->config['questions'] as $questionName => $question) {
|
||
$this->promptForOptionalPackage($questionName, $question);
|
||
}
|
||
|
||
/*foreach (['sso', 'auth'] as $item) {
|
||
if ($this->composerDefinition['extra']['optional-packages'][$item] === 'y') {
|
||
foreach ($this->config['questions'][$item]['options']['y']['packages'] ?? [] as $package) {
|
||
if ($this->config['packages'][$package]['publish'] ?? false) {
|
||
$this->composerDefinition['scripts']['post-root-package-install'][] = '@php bin/hyperf.php vendor:publish '.$package;
|
||
}
|
||
}
|
||
}
|
||
}*/
|
||
}
|
||
|
||
/**
|
||
* Prompt for a single optional installation package.
|
||
*
|
||
* @param string $questionName Name of question
|
||
* @param array $question Question details from configuration
|
||
*/
|
||
public function promptForOptionalPackage(string $questionName, array $question): void
|
||
{
|
||
$defaultOption = $question['default'] ?? 1;
|
||
if (isset($this->composerDefinition['extra']['optional-packages'][$questionName])) {
|
||
// Skip question, it's already answered
|
||
return;
|
||
}
|
||
// Get answer
|
||
$answer = $this->askQuestion($question, $defaultOption);
|
||
// Process answer
|
||
$this->processAnswer($question, $answer);
|
||
// Save user selected option
|
||
$this->composerDefinition['extra']['optional-packages'][$questionName] = $answer;
|
||
// Update composer definition
|
||
$this->composerJson->write($this->composerDefinition);
|
||
}
|
||
|
||
/**
|
||
* Prepare and ask questions and return the answer.
|
||
*
|
||
* @param int|string $defaultOption
|
||
* @return bool|int|string
|
||
* @codeCoverageIgnore
|
||
*/
|
||
private function askQuestion(array $question, $defaultOption)
|
||
{
|
||
// Construct question
|
||
$ask = [
|
||
sprintf("\n <question>%s</question>\n", $question['question']),
|
||
];
|
||
$defaultText = $defaultOption;
|
||
foreach ($question['options'] as $key => $option) {
|
||
$defaultText = ($key === $defaultOption) ? $option['name'] : $defaultText;
|
||
$ask[] = sprintf(" [<comment>%s</comment>] %s\n", $key, $option['name']);
|
||
}
|
||
if ($question['required'] !== true) {
|
||
$ask[] = " [<comment>n</comment>] None of the above\n";
|
||
}
|
||
$ask[] = ($question['custom-package'] === true)
|
||
? sprintf(
|
||
' Make your selection or type a composer package name and version <comment>(%s)</comment>: ',
|
||
$defaultText
|
||
)
|
||
: sprintf(' Make your selection <comment>(%s)</comment>: ', $defaultText);
|
||
while (true) {
|
||
// Ask for user input
|
||
$answer = $this->io->ask(implode($ask), (string)$defaultOption);
|
||
// Handle none of the options
|
||
if ($answer === 'n' && $question['required'] !== true) {
|
||
return 'n';
|
||
}
|
||
// Handle numeric options
|
||
if (is_numeric($answer) && isset($question['options'][(int)$answer])) {
|
||
return (int)$answer;
|
||
}
|
||
// Handle string options
|
||
if (isset($question['options'][$answer])) {
|
||
return $answer;
|
||
}
|
||
// Search for package
|
||
if ($question['custom-package'] === true && preg_match(self::PACKAGE_REGEX, $answer, $match)) {
|
||
$packageName = $match['name'];
|
||
$packageVersion = $match['version'];
|
||
if (!$packageVersion) {
|
||
$this->io->write('<error>No package version specified</error>');
|
||
continue;
|
||
}
|
||
$this->io->write(sprintf(' - Searching for <info>%s:%s</info>', $packageName, $packageVersion));
|
||
$optionalPackage = $this->composer->getRepositoryManager()->findPackage($packageName, $packageVersion);
|
||
if ($optionalPackage === null) {
|
||
$this->io->write(sprintf('<error>Package not found %s:%s</error>', $packageName, $packageVersion));
|
||
continue;
|
||
}
|
||
return sprintf('%s:%s', $packageName, $packageVersion);
|
||
}
|
||
$this->io->write('<error>Invalid answer</error>');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Process the answer of a question.
|
||
*
|
||
* @param bool|int|string $answer
|
||
*/
|
||
public function processAnswer(array $question, $answer): bool
|
||
{
|
||
if (isset($question['options'][$answer])) {
|
||
// Add packages to install
|
||
if (isset($question['options'][$answer]['packages'])) {
|
||
foreach ($question['options'][$answer]['packages'] as $packageName) {
|
||
$packageData = $this->config['packages'][$packageName];
|
||
$this->addPackage($packageName, $packageData['version'], $packageData['whitelist'] ?? []);
|
||
}
|
||
}
|
||
// Copy files
|
||
if (isset($question['options'][$answer])) {
|
||
$force = !empty($question['force']);
|
||
foreach ($question['options'][$answer]['resources'] as $resource => $target) {
|
||
$this->copyResource($resource, $target, $force);
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
if ($question['custom-package'] === true && preg_match(self::PACKAGE_REGEX, (string)$answer, $match)) {
|
||
$this->addPackage($match['name'], $match['version'], []);
|
||
if (isset($question['custom-package-warning'])) {
|
||
$this->io->write(sprintf(' <warning>%s</warning>', $question['custom-package-warning']));
|
||
}
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Add a package.
|
||
*/
|
||
public function addPackage(string $packageName, string $packageVersion, array $whitelist = []): void
|
||
{
|
||
$this->io->write(
|
||
sprintf(
|
||
' - Adding package <info>%s</info> (<comment>%s</comment>)',
|
||
$packageName,
|
||
$packageVersion
|
||
)
|
||
);
|
||
// Get the version constraint
|
||
$versionParser = new VersionParser();
|
||
$constraint = $versionParser->parseConstraints($packageVersion);
|
||
// Create package link
|
||
$link = new Link('__root__', $packageName, $constraint, 'requires', $packageVersion);
|
||
// Add package to the root package and composer.json requirements
|
||
if (in_array($packageName, $this->config['require-dev'], true)) {
|
||
unset($this->composerDefinition['require'][$packageName], $this->composerRequires[$packageName]);
|
||
|
||
$this->composerDefinition['require-dev'][$packageName] = $packageVersion;
|
||
$this->composerDevRequires[$packageName] = $link;
|
||
} else {
|
||
unset($this->composerDefinition['require-dev'][$packageName], $this->composerDevRequires[$packageName]);
|
||
|
||
$this->composerDefinition['require'][$packageName] = $packageVersion;
|
||
$this->composerRequires[$packageName] = $link;
|
||
}
|
||
// Set package stability if needed
|
||
switch (VersionParser::parseStability($packageVersion)) {
|
||
case 'dev':
|
||
$this->stabilityFlags[$packageName] = BasePackage::STABILITY_DEV;
|
||
break;
|
||
case 'alpha':
|
||
$this->stabilityFlags[$packageName] = BasePackage::STABILITY_ALPHA;
|
||
break;
|
||
case 'beta':
|
||
$this->stabilityFlags[$packageName] = BasePackage::STABILITY_BETA;
|
||
break;
|
||
case 'RC':
|
||
$this->stabilityFlags[$packageName] = BasePackage::STABILITY_RC;
|
||
break;
|
||
}
|
||
// Whitelist packages for the component installer
|
||
foreach ($whitelist as $package) {
|
||
if (!in_array($package, $this->composerDefinition['extra']['zf']['component-whitelist'], true)) {
|
||
$this->composerDefinition['extra']['zf']['component-whitelist'][] = $package;
|
||
$this->io->write(sprintf(' - Whitelist package <info>%s</info>', $package));
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Copy a file to its final destination in the skeleton.
|
||
*
|
||
* @param string $resource resource file
|
||
* @param string $target destination
|
||
* @param bool $force whether or not to copy over an existing file
|
||
*/
|
||
public function copyResource(string $resource, string $target, bool $force = false): void
|
||
{
|
||
// Copy file
|
||
if ($force === false && is_file($this->projectRoot . $target)) {
|
||
return;
|
||
}
|
||
$destinationPath = dirname($this->projectRoot . $target);
|
||
if (!is_dir($destinationPath)) {
|
||
mkdir($destinationPath, 0775, true);
|
||
}
|
||
$this->io->write(sprintf(' - Copying <info>%s</info>', $target));
|
||
copy($this->installerSource . $resource, $this->projectRoot . $target);
|
||
}
|
||
|
||
/**
|
||
* Update the root package based on current state.
|
||
*/
|
||
public function updateRootPackage(): void
|
||
{
|
||
$this->rootPackage->setRequires($this->composerRequires);
|
||
$this->rootPackage->setDevRequires($this->composerDevRequires);
|
||
$this->rootPackage->setStabilityFlags($this->stabilityFlags);
|
||
$this->rootPackage->setAutoload($this->composerDefinition['autoload']);
|
||
$this->rootPackage->setDevAutoload($this->composerDefinition['autoload-dev']);
|
||
$this->rootPackage->setExtra($this->composerDefinition['extra'] ?? []);
|
||
}
|
||
|
||
/**
|
||
* Remove the installer from the composer definition.
|
||
*/
|
||
public function removeInstallerFromDefinition(): void
|
||
{
|
||
$this->io->write('<info>Remove installer</info>');
|
||
// Remove installer script autoloading rules
|
||
unset(
|
||
$this->composerDefinition['autoload']['psr-4']['Installer\\'],
|
||
$this->composerDefinition['autoload-dev']['psr-4']['InstallerTest\\'],
|
||
$this->composerDefinition['extra']['branch-alias'],
|
||
$this->composerDefinition['extra']['hyperf'],
|
||
$this->composerDefinition['extra']['optional-packages'],
|
||
$this->composerDefinition['scripts']['pre-update-cmd'],
|
||
$this->composerDefinition['scripts']['pre-install-cmd'],
|
||
$this->composerDefinition['version']
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Finalize the package.
|
||
*
|
||
* Writes the current JSON state to composer.json, clears the
|
||
* composer.lock file, and cleans up all files specific to the
|
||
* installer.
|
||
*
|
||
* @codeCoverageIgnore
|
||
*/
|
||
public function finalizePackage(): void
|
||
{
|
||
// Update composer definition
|
||
$this->composerJson->write($this->composerDefinition);
|
||
$this->clearComposerLockFile();
|
||
$this->cleanUp();
|
||
}
|
||
|
||
/**
|
||
* Removes composer.lock file from gitignore.
|
||
*
|
||
* @codeCoverageIgnore
|
||
*/
|
||
private function clearComposerLockFile(): void
|
||
{
|
||
$this->io->write('<info>Removing composer.lock from .gitignore</info>');
|
||
$ignoreFile = sprintf('%s/.gitignore', $this->projectRoot);
|
||
$content = $this->removeLinesContainingStrings(['composer.lock'], file_get_contents($ignoreFile));
|
||
file_put_contents($ignoreFile, $content);
|
||
}
|
||
|
||
/**
|
||
* Clean up/remove installer classes and assets.
|
||
*
|
||
* On completion of install/update, removes the installer classes (including
|
||
* this one) and assets (including configuration and templates).
|
||
*
|
||
* @codeCoverageIgnore
|
||
*/
|
||
private function cleanUp(): void
|
||
{
|
||
$this->io->write('<info>Removing Expressive installer classes, configuration, tests and docs</info>');
|
||
foreach ($this->assetsToRemove as $target) {
|
||
$target = $this->projectRoot . $target;
|
||
if (file_exists($target)) {
|
||
unlink($target);
|
||
}
|
||
}
|
||
$this->recursiveRmdir($this->installerSource);
|
||
}
|
||
|
||
/**
|
||
* Remove lines from string content containing words in array.
|
||
*/
|
||
public function removeLinesContainingStrings(array $entries, string $content): string
|
||
{
|
||
$entries = implode(
|
||
'|',
|
||
array_map(function ($word) {
|
||
return preg_quote($word, '/');
|
||
}, $entries)
|
||
);
|
||
return preg_replace('/^.*%na' . $entries . "me.*$(?:\r?\n)?/m", '', $content);
|
||
}
|
||
|
||
/**
|
||
* Recursively remove a directory.
|
||
*
|
||
* @codeCoverageIgnore
|
||
*/
|
||
private function recursiveRmdir(string $directory): void
|
||
{
|
||
if (!is_dir($directory)) {
|
||
return;
|
||
}
|
||
$rdi = new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS);
|
||
$rii = new RecursiveIteratorIterator($rdi, RecursiveIteratorIterator::CHILD_FIRST);
|
||
foreach ($rii as $filename => $fileInfo) {
|
||
if ($fileInfo->isDir()) {
|
||
rmdir($filename);
|
||
continue;
|
||
}
|
||
unlink($filename);
|
||
}
|
||
rmdir($directory);
|
||
}
|
||
}
|