Skip to content

Commit

Permalink
[!!!][FEATURE] Introduce project:create console command
Browse files Browse the repository at this point in the history
  • Loading branch information
eliashaeussler committed Feb 1, 2023
1 parent ae74706 commit def22f7
Show file tree
Hide file tree
Showing 25 changed files with 1,183 additions and 152 deletions.
8 changes: 3 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@ RUN apk update \
ARG PROJECT_BUILDER_VERSION=0.0.0
RUN composer config version "$PROJECT_BUILDER_VERSION" \
&& composer update --prefer-dist --no-dev --no-install \
&& git add -f composer.lock \
&& mkdir artifacts \
&& git stash \
&& git archive --format=tar --output="artifacts/project-builder-$PROJECT_BUILDER_VERSION.tar" "stash@{0}" \
&& git stash pop
&& composer global config repositories.project-builder path /project-builder \
&& composer global config allow-plugins.cpsit/project-builder true \
&& composer global require cpsit/project-builder:$PROJECT_BUILDER_VERSION

WORKDIR /app
ENTRYPOINT ["/project-builder/docker-entrypoint.sh"]
6 changes: 5 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "cpsit/project-builder",
"description": "Composer package to create new projects from project templates",
"license": "GPL-3.0-or-later",
"type": "library",
"type": "composer-plugin",
"authors": [
{
"name": "Elias Häußler",
Expand All @@ -16,6 +16,7 @@
"ext-filter": "*",
"ext-json": "*",
"ext-mbstring": "*",
"composer-plugin-api": "^2.1",
"composer-runtime-api": "^2.1",
"cocur/slugify": "^4.1",
"cuyz/valinor": "^1.0",
Expand Down Expand Up @@ -75,6 +76,9 @@
"sort-packages": true,
"vendor-dir": ".build/vendor"
},
"extra": {
"class": "CPSIT\\ProjectBuilder\\ProjectBuilderPlugin"
},
"scripts": {
"post-create-project-cmd": [
"CPSIT\\ProjectBuilder\\Bootstrap::createProject"
Expand Down
3 changes: 2 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 1 addition & 6 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
#!/usr/bin/env sh
set -e

composer create-project "$@" \
--repository='{"type":"artifact","url":"/project-builder/artifacts"}' \
--prefer-dist \
--no-dev \
cpsit/project-builder \
/app
composer project:create /app "$@"
10 changes: 5 additions & 5 deletions docs/build-steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,12 @@ must be in the project type's template folder.

* Identifier: **`mirrorProcessedFiles`**
* Implementation: [`Builder\Generator\Step\MirrorProcessedFilesStep`](../src/Builder/Generator/Step/MirrorProcessedFilesStep.php)
* Variants: _processing_, _stoppable_
* Variants: _processing_

This is typically one of the **last configured steps**. It asks for confirmation to
mirror all previously processed source files and shared source files to the target
project directory. It also takes care of cleaning up the target directory as
well as removing the previously generated temporary directory.
This is typically one of the **last configured steps**. It mirrors all previously
processed source files and shared source files to the target project directory.
It also takes care of cleaning up the target directory as well as removing the
previously generated temporary directory.

### Process source files

Expand Down
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ parameters:
bootstrapFiles:
- tests/bootstrap.php
symfony:
consoleApplicationLoader: tests/console-application.php
containerXmlPath: var/cache/test-container.xml
34 changes: 25 additions & 9 deletions src/Bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

use Composer\InstalledVersions;
use Composer\Script;
use Symfony\Component\Console as SymfonyConsole;
use Symfony\Component\Filesystem;

/**
Expand All @@ -50,15 +51,27 @@ public static function createProject(
string $targetDirectory = null,
bool $exitOnFailure = true,
): int {
$messenger = IO\Messenger::create($event->getIO());
$targetDirectory ??= Helper\FilesystemHelper::getWorkingDirectory();

// Early return if current environment is unsupported
if (self::runsOnAnUnsupportedEnvironment()) {
throw Exception\UnsupportedEnvironmentException::forOutdatedComposerInstallation();
}

$exitCode = self::createApplication($messenger, $targetDirectory)->run();
self::prepareEnvironment($targetDirectory);

// Initialize IO components
$io = Console\IO\AccessibleConsoleIO::fromIO($event->getIO());
$messenger = IO\Messenger::create($io);
$input = new SymfonyConsole\Input\ArrayInput([
'target-directory' => $targetDirectory,
'--force' => true,
]);
$input->setInteractive($io->isInteractive());

// Run project creation
$command = Console\Command\CreateProjectCommand::create($messenger);
$exitCode = $command->run($input, $io->getOutput());

$event->stopPropagation();

Expand Down Expand Up @@ -92,15 +105,18 @@ public static function simulateCreateProject(Script\Event $event): void
exit($exitCode);
}

private static function createApplication(IO\Messenger $messenger, string $targetDirectory): Console\Application
private static function prepareEnvironment(string $targetDirectory): void
{
return new Console\Application(
$messenger,
Builder\Config\ConfigReader::create(),
new Error\ErrorHandler($messenger),
new Filesystem\Filesystem(),
$targetDirectory,
// Mirror source files to build directory
$filesystem = new Filesystem\Filesystem();
$filesystem->mirror(
Filesystem\Path::join($targetDirectory, Paths::PROJECT_SOURCES),
Filesystem\Path::join($targetDirectory, '.build', Paths::PROJECT_SOURCES),
);

// Register modified class loader
$loader = Resource\Local\Composer::createClassLoader();
$loader->register(true);
}

private static function runsOnAnUnsupportedEnvironment(): bool
Expand Down
2 changes: 1 addition & 1 deletion src/Builder/Generator/Step/GenerateBuildArtifactStep.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public function run(Builder\BuildResult $buildResult): bool
$artifact = new Builder\Artifact\BuildArtifact(
$artifactFile->getRelativePathname(),
$buildResult,
Resource\Local\Composer::createComposer(Helper\FilesystemHelper::getProjectRootPath())->getPackage(),
Resource\Local\Composer::createComposer(Helper\FilesystemHelper::getPackageDirectory())->getPackage(),
);

$buildResult->setBuildArtifact($artifact);
Expand Down
17 changes: 1 addition & 16 deletions src/Builder/Generator/Step/MirrorProcessedFilesStep.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,12 @@
* @author Elias Häußler <e.haeussler@familie-redlich.de>
* @license GPL-3.0-or-later
*/
final class MirrorProcessedFilesStep extends AbstractStep implements ProcessingStepInterface, StoppableStepInterface
final class MirrorProcessedFilesStep extends AbstractStep implements ProcessingStepInterface
{
use ProcessingFilesTrait;

private const TYPE = 'mirrorProcessedFiles';

private bool $stopped = false;

public function __construct(
ExpressionLanguage\ExpressionLanguage $expressionLanguage,
Filesystem\Filesystem $filesystem,
Expand All @@ -61,14 +59,6 @@ public function run(Builder\BuildResult $buildResult): bool
{
$instructions = $buildResult->getInstructions();

if (!$this->messenger->confirmOverwrite($instructions->getTargetDirectory())) {
$buildResult->setMirrored(false);

$this->stopped = true;

return false;
}

$this->messenger->newLine(ComposerIO\IOInterface::VERBOSE);

foreach ($this->listFilesInTargetDirectory($instructions) as $file) {
Expand Down Expand Up @@ -96,11 +86,6 @@ public function run(Builder\BuildResult $buildResult): bool
return true;
}

public function isStopped(): bool
{
return $this->stopped;
}

public static function getType(): string
{
return self::TYPE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
/*
* This file is part of the Composer package "cpsit/project-builder".
*
* Copyright (C) 2022 Elias Häußler <e.haeussler@familie-redlich.de>
* Copyright (C) 2023 Elias Häußler <e.haeussler@familie-redlich.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand All @@ -21,30 +21,29 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

namespace CPSIT\ProjectBuilder\Console;
namespace CPSIT\ProjectBuilder\Console\Command;

use Composer\Command;
use CPSIT\ProjectBuilder\Builder;
use CPSIT\ProjectBuilder\DependencyInjection;
use CPSIT\ProjectBuilder\Error;
use CPSIT\ProjectBuilder\Exception;
use CPSIT\ProjectBuilder\Helper;
use CPSIT\ProjectBuilder\IO;
use CPSIT\ProjectBuilder\Paths;
use CPSIT\ProjectBuilder\Resource;
use CPSIT\ProjectBuilder\Template;
use Symfony\Component\Console;
use Symfony\Component\Filesystem;
use Throwable;

use function reset;

/**
* Application.
* CreateProjectCommand.
*
* @author Elias Häußler <e.haeussler@familie-redlich.de>
* @license GPL-3.0-or-later
*
* @internal
*/
final class Application
final class CreateProjectCommand extends Command\BaseCommand
{
private const SUCCESSFUL = 0;
private const FAILED = 1;
Expand All @@ -63,47 +62,68 @@ public function __construct(
private Builder\Config\ConfigReader $configReader,
private Error\ErrorHandler $errorHandler,
private Filesystem\Filesystem $filesystem,
private string $targetDirectory,
array $templateProviders = [],
) {
parent::__construct('project:create');

if ([] === $templateProviders) {
$templateProviders = $this->createDefaultTemplateProviders();
}

$this->templateProviders = $templateProviders;
}

public function run(): int
public static function create(IO\Messenger $messenger): self
{
return new self(
$messenger,
Builder\Config\ConfigReader::create(),
new Error\ErrorHandler($messenger),
new Filesystem\Filesystem(),
);
}

protected function configure(): void
{
$this->setDescription('Create a new project, based on a selected project template');

$this->addArgument(
'target-directory',
Console\Input\InputArgument::REQUIRED,
'Absolute path to a directory where to create the new project',
);

$this->addOption(
'force',
'f',
Console\Input\InputOption::VALUE_NONE,
'Force project creation even if target directory is not empty',
);
}

protected function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
if (!$this->messenger->isInteractive()) {
$this->messenger->error('This command cannot be run in non-interactive mode.');

return self::FAILED;
}

$this->mirrorSourceFiles();
$targetDirectory = Helper\FilesystemHelper::resolveRelativePath($input->getArgument('target-directory'));
$force = $input->getOption('force');

$loader = Resource\Local\Composer::createClassLoader();
$loader->register(true);

$this->messenger->clearScreen();
$this->messenger->welcome();
// Early return if target directory is not empty and should not be overwritten
if (!$force
&& !Helper\FilesystemHelper::isDirectoryEmpty($targetDirectory)
&& !$this->messenger->confirmOverwrite($targetDirectory)
) {
return self::ABORTED;
}

try {
// Select template source
$defaultTemplateProvider = reset($this->templateProviders);
$templateSource = $this->selectTemplateSource($defaultTemplateProvider);
$templateSource->getProvider()->installTemplateSource($templateSource);
$templateIdentifier = $templateSource->getPackage()->getName();

// Create container
$config = $this->configReader->readConfig($templateIdentifier);
$config->setTemplateSource($templateSource);
$container = $this->buildContainer($config);

// Run project generation
$generator = $container->get(Builder\Generator\Generator::class);
$result = $generator->run($this->targetDirectory);
$generator = $this->prepareTemplate();
$result = $generator->run($targetDirectory);

// Show project generation result
$this->messenger->decorateResult($result);
Expand All @@ -124,12 +144,23 @@ public function run(): int
return self::SUCCESSFUL;
}

private function mirrorSourceFiles(): void
private function prepareTemplate(): Builder\Generator\Generator
{
$this->filesystem->mirror(
Filesystem\Path::join($this->targetDirectory, Paths::PROJECT_SOURCES),
Filesystem\Path::join($this->targetDirectory, '.build', Paths::PROJECT_SOURCES),
);
$this->messenger->clearScreen();
$this->messenger->welcome();

// Select template source
$defaultTemplateProvider = reset($this->templateProviders);
$templateSource = $this->selectTemplateSource($defaultTemplateProvider);
$templateSource->getProvider()->installTemplateSource($templateSource);
$templateIdentifier = $templateSource->getPackage()->getName();

// Create container
$config = $this->configReader->readConfig($templateIdentifier);
$config->setTemplateSource($templateSource);
$container = $this->buildContainer($config);

return $container->get(Builder\Generator\Generator::class);
}

/**
Expand Down
Loading

0 comments on commit def22f7

Please sign in to comment.