&1', $output, $status); return [0 === $status, implode("\n", $output)]; }; // Composer, in the order that works on a developer machine. $composer = null; foreach (['php '.escapeshellarg($root.'/composer.phar'), 'composer'] as $candidate) { [$ok] = $run($candidate.' --version'); if ($ok) { $composer = $candidate; break; } } if (null === $composer) { fwrite(STDERR, "Composer ist auf diesem Rechner nicht aufrufbar.\n"); fwrite(STDERR, "Ein Release wird auf einem Build-Rechner gebaut, nicht auf dem Hosting - genau deshalb gibt es dieses Skript.\n"); exit(1); } $staging = $outDirectory.'/kantsites-'.$version; $archive = $outDirectory.'/kantsites-'.$version.'.zip'; if (is_dir($staging) || is_file($archive)) { fwrite(STDERR, sprintf("Für Fassung %s gibt es schon ein Release. Erst entfernen oder VERSION erhöhen.\n", $version)); exit(1); } if (!is_dir($outDirectory) && !mkdir($outDirectory, 0o775, true)) { fwrite(STDERR, sprintf("%s ließ sich nicht anlegen.\n", $outDirectory)); exit(1); } echo "Fassung {$version}\n"; // What belongs into a release: the application, and nothing that only a developer needs. $include = ['bin', 'config', 'migrations', 'public', 'src', 'templates', 'composer.json', 'composer.lock', 'VERSION', 'README.md', 'LICENSE', '.env.example']; $skip = ['.git', '.github', 'node_modules', 'var', 'tests', 'scripts', 'reports', 'phpunit.xml.dist', '.php-cs-fixer.dist.php', 'package.json', 'package-lock.json']; $copy = static function (string $from, string $to) use (&$copy, $skip): int { $count = 0; if (is_file($from)) { @mkdir(dirname($to), 0o775, true); return @copy($from, $to) ? 1 : 0; } foreach (scandir($from) ?: [] as $entry) { if ('.' === $entry || '..' === $entry || in_array($entry, $skip, true)) { continue; } $count += $copy($from.'/'.$entry, $to.'/'.$entry); } return $count; }; $files = 0; foreach ($include as $entry) { if (file_exists($root.'/'.$entry)) { $files += $copy($root.'/'.$entry, $staging.'/'.$entry); } } // Die .env des Entwicklungsrechners gehört nicht in ein Release; gebraucht wird trotzdem eine, // denn ohne sie bootet die Anwendung nicht einmal so weit, dass der Installer erscheint. Also // wird die Vorlage zur .env des Releases - mit Produktionswerten und ohne Geheimnis. Das // Geheimnis erzeugt der erste Aufruf selbst, die Zugangsdaten schreibt der Installer nach // .env.local. copy($root.'/.env.example', $staging.'/.env'); ++$files; echo "Quelldateien: {$files}\n"; // The dependencies, without the development ones - that is the whole point of building here. $flags = isset($options['keep-dev']) ? '' : ' --no-dev'; [$ok, $output] = $run(sprintf( '%s install%s --optimize-autoloader --no-interaction --no-progress --working-dir=%s', $composer, $flags, escapeshellarg($staging), )); if (!$ok) { fwrite(STDERR, "composer install ist gescheitert:\n".$output."\n"); exit(1); } echo "vendor/ gebaut".($flags ? ' ohne Entwicklungspakete' : ' MIT Entwicklungspaketen')."\n"; // The environment file is not part of a release: it holds the secret of one installation. foreach (['.env.local', '.env.prod.local'] as $secret) { if (is_file($staging.'/'.$secret)) { unlink($staging.'/'.$secret); } } if (!class_exists(ZipArchive::class)) { fwrite(STDERR, "Die PHP-Erweiterung zip fehlt; ohne sie lässt sich kein Archiv schreiben.\n"); exit(1); } $zip = new ZipArchive(); if (true !== $zip->open($archive, ZipArchive::CREATE | ZipArchive::OVERWRITE)) { fwrite(STDERR, "Archiv ließ sich nicht anlegen.\n"); exit(1); } $entries = 0; $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($staging, FilesystemIterator::SKIP_DOTS)); foreach ($iterator as $file) { if ($file->isFile()) { // Paths inside the archive start with the version folder, so unpacking never scatters // files across an existing installation. $zip->addFile($file->getPathname(), 'kantsites-'.$version.'/'.str_replace('\\', '/', substr($file->getPathname(), strlen($staging) + 1))); ++$entries; } } $zip->close(); $hash = hash_file('sha256', $archive); file_put_contents($archive.'.sha256', $hash.' '.basename($archive)."\n"); printf("\nArchiv: %s\n", $archive); printf("Dateien: %d, Größe: %.1f MB\n", $entries, filesize($archive) / 1048576); printf("SHA-256: %s\n", $hash); // Und der einzelne Dateidownload: dieselbe install.php, nur mit eingetragener Adresse und // Prüfsumme. Ohne beides verweigert sie die Arbeit, deshalb entsteht sie erst hier. $installer = strtr((string) file_get_contents($root.'/scripts/install.php'), [ "const KANT_ARCHIVE_URL = '';" => sprintf("const KANT_ARCHIVE_URL = '%s';", $releaseBase.'/v'.$version.'/'.basename($archive)), "const KANT_ARCHIVE_SHA256 = '';" => sprintf("const KANT_ARCHIVE_SHA256 = '%s';", $hash), "const KANT_VERSION = '';" => sprintf("const KANT_VERSION = '%s';", $version), ]); file_put_contents($outDirectory.'/install.php', $installer); printf("Installer: %s (lädt %s)\n", $outDirectory.'/install.php', $releaseBase.'/v'.$version.'/'.basename($archive)); echo "\nBeides gehört auf die Releaseseite: das Archiv unter genau dieser Adresse, und install.php zum Herunterladen.\n"; echo "Die Prüfsumme steckt jetzt in install.php, damit ein manipuliertes Archiv nicht entpackt wird.\n";