addOption( name: 'config-file', shortcut: ['c'], mode: InputOption::VALUE_REQUIRED, description: 'The path to the project\'s build.yaml file', default: 'build.yaml', ) ->addOption( name: 'output-dir', shortcut: ['o'], mode: InputOption::VALUE_REQUIRED, description: 'The directory to create files in', default: '.', ) ; } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $configFile = $input->getOption(name: 'config-file'); $outputDir = $input->getOption(name: 'output-dir'); $configurationData = array_merge( Yaml::parseFile(filename: __DIR__ . '/../../resources/build.defaults.yaml'), Yaml::parseFile(filename: $configFile), ); // Convert the input to a configuration data object. $normalizer = new ObjectNormalizer(null, new CamelCaseToSnakeCaseNameConverter()); $serializer = new Serializer([$normalizer], [new JsonEncoder()]); $configurationDataObject = $serializer->deserialize(json_encode($configurationData), Config::class, 'json'); $validator = Validation::createValidatorBuilder()->enableAnnotationMapping()->getValidator(); $violations = $validator->validate($configurationDataObject); if (0 < $violations->count()) { $io->error('Configuration is invalid.'); $io->listing( collect($violations) ->map(fn (ConstraintViolationInterface $v) => "{$v->getPropertyPath()} - {$v->getMessage()}") ->toArray() ); return Command::FAILURE; } if (isset($configurationData['docker-compose'])) { $configurationData['dockerCompose'] = $configurationData['docker-compose']; $configurationData['docker-compose'] = null; } $configurationData['managedText'] = 'Do not edit this file. It is automatically generated by https://www.oliverdavies.uk/build-configs.'; $filesToGenerate = $this->getFiles(configurationData: $configurationData); $io->info("Building configuration for {$configurationData['name']}."); $io->write('Generated files:'); $io->listing($this->getListOfFiles(filesToGenerate: $filesToGenerate)->toArray()); $this->generateFiles( configurationData: $configurationData, filesToGenerate: $filesToGenerate, outputDir: $outputDir, ); // return Command::SUCCESS; } /** * @param array $configurationData * @param Collection $filesToGenerate */ function generateFiles( Collection $filesToGenerate, string $outputDir, array $configurationData, ): void { $filesystem = new Filesystem(); $twig = new Environment(new FilesystemLoader([__DIR__ . '/../../templates'])); $filesToGenerate->each(function(TemplateFile $templateFile) use ($configurationData, $filesystem, $outputDir, $twig): void { if ($templateFile->path !== null) { if (!$filesystem->exists($templateFile->path)) { $filesystem->mkdir("{$outputDir}/{$templateFile->path}"); } } $sourceFile = "{$templateFile->data}.twig"; $outputFile = collect([ $outputDir, $templateFile->path, $templateFile->name, ])->filter()->implode('/'); $filesystem->dumpFile($outputFile, $twig->render($sourceFile, $configurationData)); }); // If the Docker entrypoint file is generated, ensure it is executable. if ($filesystem->exists("{$outputDir}/tools/docker/images/php/root/usr/local/bin/docker-entrypoint-php")) { $filesystem->chmod("{$outputDir}/tools/docker/images/php/root/usr/local/bin/docker-entrypoint-php", 0755); } if ($filesystem->exists("{$outputDir}/.githooks/pre-push")) { $filesystem->chmod("{$outputDir}/.githooks/pre-push", 0755); } } function getFiles(array $configurationData): Collection { /** @var Collection */ $filesToGenerate = collect([ new TemplateFile(data: 'common/.dockerignore', name: '.dockerignore'), new TemplateFile(data: 'common/.hadolint.yaml', name: '.hadolint.yaml'), new TemplateFile(data: 'env.example', name: '.env.example'), ]); $extraDatabases = Arr::get($configurationData, 'database.extra_databases', []); if (count($extraDatabases) > 0) { $filesToGenerate[] = new TemplateFile( data: 'extra-databases.sql', name: 'extra-databases.sql', path: 'tools/docker/images/database/root/docker-entrypoint-initdb.d', ); } if (false !== Arr::get($configurationData, "justfile", true)) { $filesToGenerate[] = new TemplateFile(data: 'justfile', name: 'justfile'); } if (isset($configurationData['dockerCompose']) && $configurationData['dockerCompose'] !== null) { $filesToGenerate[] = new TemplateFile(data: 'docker-compose.yaml', name: 'docker-compose.yaml'); } if (static::isPhp(Arr::get($configurationData, 'language'))) { $filesToGenerate[] = new TemplateFile(data: 'php/Dockerfile', name: 'Dockerfile'); $filesToGenerate[] = new TemplateFile(data: 'php/phpcs.xml', name: 'phpcs.xml.dist'); $filesToGenerate[] = new TemplateFile(data: 'php/phpunit.xml', name: 'phpunit.xml.dist'); $filesToGenerate[] = new TemplateFile( data: 'php/docker-entrypoint-php', name: 'docker-entrypoint-php', path: 'tools/docker/images/php/root/usr/local/bin', ); if (Arr::has(array: $configurationData, keys: 'php.phpstan')) { $filesToGenerate[] = new TemplateFile(data: 'php/phpstan.neon', name: 'phpstan.neon.dist'); } } if (static::isNode(Arr::get($configurationData, 'language'))) { $filesToGenerate[] = new TemplateFile(data: 'node/.yarnrc', name: '.yarnrc'); $filesToGenerate[] = new TemplateFile(data: 'node/Dockerfile', name: 'Dockerfile'); } if (static::isCaddy(Arr::get($configurationData, 'web.type'))) { $filesToGenerate[] = new TemplateFile( data: 'web/caddy/Caddyfile', name: 'Caddyfile', path: 'tools/docker/images/web/root/etc/caddy', ); } if (static::isNginx(Arr::get($configurationData, 'web.type'))) { $filesToGenerate[] = new TemplateFile( data: 'web/nginx/default.conf', name: 'default.conf', path: 'tools/docker/images/web/root/etc/nginx/conf.d', ); } if ('drupal-project' === Arr::get($configurationData, 'type')) { // Add a Drupal version of phpunit.xml.dist. $filesToGenerate[] = new TemplateFile(data: 'drupal-project/phpunit.xml.dist', name: 'phpunit.xml.dist'); } if (Arr::get($configurationData, 'experimental.createGitHubActionsConfiguration', false) === true) { $filesToGenerate[] = new TemplateFile( data: 'ci/github-actions/ci.yml', name: 'ci.yml', path: '.github/workflows', ); } if (Arr::get($configurationData, 'experimental.runGitHooksBeforePush', false) === true) { $filesToGenerate[] = new TemplateFile( data: 'git-hooks/pre-push', name: 'pre-push', path: '.githooks', ); } return $filesToGenerate; } function getListOfFiles(Collection $filesToGenerate): Collection { return $filesToGenerate ->map(fn (TemplateFile $templateFile): string => collect([$templateFile->path, $templateFile->name])->filter()->implode('/')) ->unique() ->sort(); } function isCaddy(?string $webServer): bool { if (is_null($webServer)) { return false; } return strtoupper($webServer) === WebServer::CADDY->name; } function isNginx(?string $webServer): bool { if (is_null($webServer)) { return false; } return strtoupper($webServer) === WebServer::NGINX->name; } function isNode(?string $language): bool { if (is_null($language)) { return false; } return strtoupper($language) === Language::NODE->name; } function isPhp(?string $language): bool { if (is_null($language)) { return false; } return strtoupper($language) === Language::PHP->name; } }