Added dep check and composer auto-install to init command

This commit is contained in:
Dan Brown 2023-03-04 19:23:44 +00:00
parent 21db0ebf46
commit 4d9d591792
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
3 changed files with 109 additions and 22 deletions

3
.gitignore vendored
View File

@ -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

View File

@ -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.");
} }

View File

@ -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.");