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
.phpunit.result.cache
.DS_Store
phpstan.neon
phpstan.neon
/composer

View file

@ -13,10 +13,8 @@ class InitCommand
*/
public function handle(CommandCall $input)
{
$this->ensureRequiredExtensionInstalled(); // TODO - Ensure bookstack install deps are met?
// TODO - Check composer and git exists before running
// TODO - Potentially download composer?
echo "Checking system requirements...\n";
$this->ensureRequirementsMet();
$suggestedOutPath = $input->subcommand;
if ($suggestedOutPath === 'default') {
@ -25,13 +23,22 @@ class InitCommand
echo "Locating and checking install directory...\n";
$installDir = $this->getInstallDir($suggestedOutPath);
$this->ensureInstallDirEmpty($installDir);
$this->ensureInstallDirEmptyAndWritable($installDir);
echo "Cloning down BookStack project to install directory...\n";
$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";
$this->installComposerDependencies($installDir);
$this->installComposerDependencies($composer, $installDir);
echo "Creating .env file from .env.example...\n";
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.
* @throws CommandError
*/
protected function ensureRequiredExtensionInstalled(): void
protected function ensureRequirementsMet(): void
{
// if (!extension_loaded('zip')) {
// throw new CommandError('The "zip" PHP extension is required to run this command');
// }
$errors = [];
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
@ -80,12 +144,9 @@ class InitCommand
* Run composer install to download PHP dependencies.
* @throws CommandError
*/
protected function installComposerDependencies(string $installDir): void
protected function installComposerDependencies(ProgramRunner $composer, string $installDir): void
{
$errors = (new ProgramRunner('composer', '/usr/local/bin/composer'))
->withTimeout(300)
->withIdleTimeout(15)
->runCapturingStdErr([
$errors = $composer->runCapturingStdErr([
'install',
'--no-dev', '-n', '-q', '--no-progress',
'-d', $installDir
@ -122,12 +183,16 @@ class InitCommand
* Ensure that the installation directory is completely empty to avoid potential conflicts or issues.
* @throws CommandError
*/
protected function ensureInstallDirEmpty(string $installDir): void
protected function ensureInstallDirEmptyAndWritable(string $installDir): void
{
$contents = array_diff(scandir($installDir), ['..', '.']);
if (count($contents) > 0) {
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) {
throw new CommandError("Could not create directory [{$suggestedDir}] for install.");
}
$dir = $suggestedDir;
$dir = realpath($suggestedDir);
} else {
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 $idleTimeout = 15;
protected array $environment = [];
protected array $additionalProgramDirectories = [];
public function __construct(
protected string $program,
@ -35,6 +36,12 @@ class ProgramRunner
return $this;
}
public function withAdditionalPathLocation(string $directoryPath): static
{
$this->additionalProgramDirectories[] = $directoryPath;
return $this;
}
public function runCapturingAllOutput(array $args): string
{
$output = '';
@ -55,16 +62,30 @@ class ProgramRunner
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);
foreach ($process as $type => $data) {
if ($type === $process::ERR) {
$stdErrCallback($data);
if ($stdErrCallback) {
$stdErrCallback($data);
}
} 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
@ -80,7 +101,7 @@ class ProgramRunner
protected function resolveProgramPath(): string
{
$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)) {
throw new \Exception("Could not locate \"{$this->program}\" program.");