SITE_ORIGIN alone decides whether the site is www or not: CanonicalHostSubscriber sends the other spelling there with 301, cms:site:origin writes the setting, the system screen shows it. The roles now match the role help (editor: drafts, publisher: live content, admin: design, settings, users, system). The installer no longer reopens on an unreachable database, the session cookie follows ADMIN_PATH, percent-encoded credentials no longer break the container, consent only accepts same-site decisions, JSON-LD is HTML-safe, own CSS stays in its block, plus the editor, updater, download and public-output findings from the full review (ADR 099, ADR 100). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
167 lines
7.4 KiB
PHP
167 lines
7.4 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Builds a release archive that runs on hosting without Composer.
|
|
*
|
|
* Umut is right: shared hosting usually has no Composer. A release therefore has to carry
|
|
* `vendor/` with it, and that folder has to be built by somebody - here, on a machine that does
|
|
* have Composer. The archive is what `install.php` downloads and what a host without Composer
|
|
* unpacks; the Git repository stays free of `vendor/`.
|
|
*
|
|
* Run: php scripts/build-release.php [--out=var/releases]
|
|
*
|
|
* The script refuses to build rather than produce something misleading: without Composer there is
|
|
* no way to get a dependency tree without development packages, and shipping the development one
|
|
* would mean an archive with PHPUnit in it.
|
|
*/
|
|
$root = dirname(__DIR__);
|
|
$options = getopt('', ['out::', 'keep-dev', 'url::']);
|
|
// Wohin das Archiv beim Veröffentlichen gelegt wird. Steht fest in install.php, deshalb muss
|
|
// sie hier bekannt sein - eine Adresse, die erst im Browser abgefragt wird, wäre eine Lücke.
|
|
$releaseBase = rtrim($options['url'] ?? 'https://git.kant-it.de/kantarci/KantSites/releases/download', '/');
|
|
$outDirectory = $root.'/'.trim($options['out'] ?? 'var/releases', '/');
|
|
$version = trim((string) file_get_contents($root.'/VERSION'));
|
|
if (1 !== preg_match('/^\d+\.\d+\.\d+$/D', $version)) {
|
|
fwrite(STDERR, "VERSION enthält keine Fassung der Form 1.2.3.\n");
|
|
exit(1);
|
|
}
|
|
|
|
/** Runs a command and returns [ok, output]; the shell is used because this is a build tool. */
|
|
$run = static function (string $command) use ($root): array {
|
|
$output = [];
|
|
$status = 0;
|
|
exec('cd '.escapeshellarg($root).' && '.$command.' 2>&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.
|
|
// migrations.php und cli-config.php gehoeren dazu: ohne die erste findet Doctrine beim
|
|
// Einrichten keine Migrationsliste und bricht mit "configuration file does not exist" ab - genau
|
|
// dort, wo eine frische Installation ihre Tabellen anlegen will.
|
|
$include = ['bin', 'config', 'migrations', 'public', 'src', 'templates', 'composer.json', 'composer.lock', 'migrations.php', 'cli-config.php', 'VERSION', 'README.md', '.env.example'];
|
|
$skip = ['.git', '.github', '.claude', '.gitattributes', '.gitignore', '.env.lock', '.php-cs-fixer.cache', 'node_modules', 'var', 'tests', 'scripts', 'reports', 'phpunit.xml.dist', '.php-cs-fixer.dist.php', 'package.json', 'package-lock.json', 'AGENTS.md', 'ENTWICKLUNGSPLAN.md', 'docs', 'composer.phar', '.venv', 'vendor', 'phpunit.cache', 'migrations.php.bak'];
|
|
|
|
$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.
|
|
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";
|