mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Added dep check and composer auto-install to init command
This commit is contained in:
parent
21db0ebf46
commit
4d9d591792
3
.gitignore
vendored
3
.gitignore
vendored
@ -25,4 +25,5 @@ nbproject
|
|||||||
webpack-stats.json
|
webpack-stats.json
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
.DS_Store
|
.DS_Store
|
||||||
phpstan.neon
|
phpstan.neon
|
||||||
|
/composer
|
@ -13,10 +13,8 @@ class InitCommand
|
|||||||
*/
|
*/
|
||||||
public function handle(CommandCall $input)
|
public function handle(CommandCall $input)
|
||||||
{
|
{
|
||||||
$this->ensureRequiredExtensionInstalled(); // TODO - Ensure bookstack install deps are met?
|
echo "Checking system requirements...\n";
|
||||||
|
$this->ensureRequirementsMet();
|
||||||
// TODO - Check composer and git exists before running
|
|
||||||
// TODO - Potentially download composer?
|
|
||||||
|
|
||||||
$suggestedOutPath = $input->subcommand;
|
$suggestedOutPath = $input->subcommand;
|
||||||
if ($suggestedOutPath === 'default') {
|
if ($suggestedOutPath === 'default') {
|
||||||
@ -25,13 +23,22 @@ class InitCommand
|
|||||||
|
|
||||||
echo "Locating and checking install directory...\n";
|
echo "Locating and checking install directory...\n";
|
||||||
$installDir = $this->getInstallDir($suggestedOutPath);
|
$installDir = $this->getInstallDir($suggestedOutPath);
|
||||||
$this->ensureInstallDirEmpty($installDir);
|
$this->ensureInstallDirEmptyAndWritable($installDir);
|
||||||
|
|
||||||
echo "Cloning down BookStack project to install directory...\n";
|
echo "Cloning down BookStack project to install directory...\n";
|
||||||
$this->cloneBookStackViaGit($installDir);
|
$this->cloneBookStackViaGit($installDir);
|
||||||
|
|
||||||
|
echo "Checking composer exists...\n";
|
||||||
|
$composer = $this->getComposerProgram($installDir);
|
||||||
|
try {
|
||||||
|
$composer->ensureFound();
|
||||||
|
} catch (\Exception $exception) {
|
||||||
|
echo "Composer does not exist, downloading a local copy...\n";
|
||||||
|
$this->downloadComposerToInstall($installDir);
|
||||||
|
}
|
||||||
|
|
||||||
echo "Installing application dependencies using composer...\n";
|
echo "Installing application dependencies using composer...\n";
|
||||||
$this->installComposerDependencies($installDir);
|
$this->installComposerDependencies($composer, $installDir);
|
||||||
|
|
||||||
echo "Creating .env file from .env.example...\n";
|
echo "Creating .env file from .env.example...\n";
|
||||||
copy($installDir . DIRECTORY_SEPARATOR . '.env.example', $installDir . DIRECTORY_SEPARATOR . '.env');
|
copy($installDir . DIRECTORY_SEPARATOR . '.env.example', $installDir . DIRECTORY_SEPARATOR . '.env');
|
||||||
@ -53,11 +60,68 @@ class InitCommand
|
|||||||
* Ensure the required PHP extensions are installed for this command.
|
* Ensure the required PHP extensions are installed for this command.
|
||||||
* @throws CommandError
|
* @throws CommandError
|
||||||
*/
|
*/
|
||||||
protected function ensureRequiredExtensionInstalled(): void
|
protected function ensureRequirementsMet(): void
|
||||||
{
|
{
|
||||||
// if (!extension_loaded('zip')) {
|
$errors = [];
|
||||||
// throw new CommandError('The "zip" PHP extension is required to run this command');
|
|
||||||
// }
|
if (version_compare(PHP_VERSION, '8.0.2') < 0) {
|
||||||
|
$errors[] = "PHP >= 8.0.2 is required to install BookStack.";
|
||||||
|
}
|
||||||
|
|
||||||
|
$requiredExtensions = ['bcmath', 'curl', 'gd', 'iconv', 'libxml', 'mbstring', 'mysqlnd', 'xml'];
|
||||||
|
foreach ($requiredExtensions as $extension) {
|
||||||
|
if (!extension_loaded($extension)) {
|
||||||
|
$errors[] = "The \"{$extension}\" PHP extension is required by not active.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
(new ProgramRunner('git', '/usr/bin/git'))->ensureFound();
|
||||||
|
(new ProgramRunner('php', '/usr/bin/php'))->ensureFound();
|
||||||
|
} catch (\Exception $exception) {
|
||||||
|
$errors[] = $exception->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($errors) > 0) {
|
||||||
|
throw new CommandError("Requirements failed with following errors:\n" . implode("\n", $errors));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function downloadComposerToInstall(string $installDir): void
|
||||||
|
{
|
||||||
|
$setupPath = $installDir . DIRECTORY_SEPARATOR . 'composer-setup.php';
|
||||||
|
$signature = file_get_contents('https://composer.github.io/installer.sig');
|
||||||
|
copy('https://getcomposer.org/installer', $setupPath);
|
||||||
|
$checksum = hash_file('sha384', $setupPath);
|
||||||
|
|
||||||
|
if ($signature !== $checksum) {
|
||||||
|
unlink($setupPath);
|
||||||
|
throw new CommandError("Could not install composer, checksum validation failed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = (new ProgramRunner('php', '/usr/bin/php'))
|
||||||
|
->runWithoutOutputCallbacks([
|
||||||
|
$setupPath, '--quiet',
|
||||||
|
"--install-dir={$installDir}",
|
||||||
|
"--filename=composer",
|
||||||
|
]);
|
||||||
|
|
||||||
|
unlink($setupPath);
|
||||||
|
|
||||||
|
if ($status !== 0) {
|
||||||
|
throw new CommandError("Could not install composer, composer-setup script run failed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the composer program.
|
||||||
|
*/
|
||||||
|
protected function getComposerProgram(string $installDir): ProgramRunner
|
||||||
|
{
|
||||||
|
return (new ProgramRunner('composer', '/usr/local/bin/composer'))
|
||||||
|
->withTimeout(300)
|
||||||
|
->withIdleTimeout(15)
|
||||||
|
->withAdditionalPathLocation($installDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function generateAppKey(string $installDir): void
|
protected function generateAppKey(string $installDir): void
|
||||||
@ -80,12 +144,9 @@ class InitCommand
|
|||||||
* Run composer install to download PHP dependencies.
|
* Run composer install to download PHP dependencies.
|
||||||
* @throws CommandError
|
* @throws CommandError
|
||||||
*/
|
*/
|
||||||
protected function installComposerDependencies(string $installDir): void
|
protected function installComposerDependencies(ProgramRunner $composer, string $installDir): void
|
||||||
{
|
{
|
||||||
$errors = (new ProgramRunner('composer', '/usr/local/bin/composer'))
|
$errors = $composer->runCapturingStdErr([
|
||||||
->withTimeout(300)
|
|
||||||
->withIdleTimeout(15)
|
|
||||||
->runCapturingStdErr([
|
|
||||||
'install',
|
'install',
|
||||||
'--no-dev', '-n', '-q', '--no-progress',
|
'--no-dev', '-n', '-q', '--no-progress',
|
||||||
'-d', $installDir
|
'-d', $installDir
|
||||||
@ -122,12 +183,16 @@ class InitCommand
|
|||||||
* Ensure that the installation directory is completely empty to avoid potential conflicts or issues.
|
* Ensure that the installation directory is completely empty to avoid potential conflicts or issues.
|
||||||
* @throws CommandError
|
* @throws CommandError
|
||||||
*/
|
*/
|
||||||
protected function ensureInstallDirEmpty(string $installDir): void
|
protected function ensureInstallDirEmptyAndWritable(string $installDir): void
|
||||||
{
|
{
|
||||||
$contents = array_diff(scandir($installDir), ['..', '.']);
|
$contents = array_diff(scandir($installDir), ['..', '.']);
|
||||||
if (count($contents) > 0) {
|
if (count($contents) > 0) {
|
||||||
throw new CommandError("Expected install directory to be empty but existing files found in [{$installDir}] target location.");
|
throw new CommandError("Expected install directory to be empty but existing files found in [{$installDir}] target location.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!is_writable($installDir)) {
|
||||||
|
throw new CommandError("Target install directory [{$installDir}] is not writable.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -148,7 +213,7 @@ class InitCommand
|
|||||||
if (!$created) {
|
if (!$created) {
|
||||||
throw new CommandError("Could not create directory [{$suggestedDir}] for install.");
|
throw new CommandError("Could not create directory [{$suggestedDir}] for install.");
|
||||||
}
|
}
|
||||||
$dir = $suggestedDir;
|
$dir = realpath($suggestedDir);
|
||||||
} else {
|
} else {
|
||||||
throw new CommandError("Could not resolve provided [{$suggestedDir}] path to an existing folder.");
|
throw new CommandError("Could not resolve provided [{$suggestedDir}] path to an existing folder.");
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ class ProgramRunner
|
|||||||
protected int $timeout = 240;
|
protected int $timeout = 240;
|
||||||
protected int $idleTimeout = 15;
|
protected int $idleTimeout = 15;
|
||||||
protected array $environment = [];
|
protected array $environment = [];
|
||||||
|
protected array $additionalProgramDirectories = [];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected string $program,
|
protected string $program,
|
||||||
@ -35,6 +36,12 @@ class ProgramRunner
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function withAdditionalPathLocation(string $directoryPath): static
|
||||||
|
{
|
||||||
|
$this->additionalProgramDirectories[] = $directoryPath;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function runCapturingAllOutput(array $args): string
|
public function runCapturingAllOutput(array $args): string
|
||||||
{
|
{
|
||||||
$output = '';
|
$output = '';
|
||||||
@ -55,16 +62,30 @@ class ProgramRunner
|
|||||||
return $err;
|
return $err;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function runWithoutOutputCallbacks(array $args, callable $stdOutCallback, callable $stdErrCallback): void
|
public function runWithoutOutputCallbacks(array $args, callable $stdOutCallback = null, callable $stdErrCallback = null): int
|
||||||
{
|
{
|
||||||
$process = $this->startProcess($args);
|
$process = $this->startProcess($args);
|
||||||
foreach ($process as $type => $data) {
|
foreach ($process as $type => $data) {
|
||||||
if ($type === $process::ERR) {
|
if ($type === $process::ERR) {
|
||||||
$stdErrCallback($data);
|
if ($stdErrCallback) {
|
||||||
|
$stdErrCallback($data);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$stdOutCallback($data);
|
if ($stdOutCallback) {
|
||||||
|
$stdOutCallback($data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $process->getExitCode() ?? 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function ensureFound(): void
|
||||||
|
{
|
||||||
|
$this->resolveProgramPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function startProcess(array $args): Process
|
protected function startProcess(array $args): Process
|
||||||
@ -80,7 +101,7 @@ class ProgramRunner
|
|||||||
protected function resolveProgramPath(): string
|
protected function resolveProgramPath(): string
|
||||||
{
|
{
|
||||||
$executableFinder = new ExecutableFinder();
|
$executableFinder = new ExecutableFinder();
|
||||||
$path = $executableFinder->find($this->program, $this->defaultPath);
|
$path = $executableFinder->find($this->program, $this->defaultPath, $this->additionalProgramDirectories);
|
||||||
|
|
||||||
if (is_null($path) || !is_file($path)) {
|
if (is_null($path) || !is_file($path)) {
|
||||||
throw new \Exception("Could not locate \"{$this->program}\" program.");
|
throw new \Exception("Could not locate \"{$this->program}\" program.");
|
||||||
|
Loading…
Reference in New Issue
Block a user