1d87be7d5e
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
- Update REPO: from MokoStandards-API to moko-platform in 125 files - Fix wrong org path (mokoconsulting-tech → MokoConsulting) in 10 files - Fix SPDX-LICENSE-IDENTIFIER case in 2 template files - Add missing REPO: field to 3 files Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
217 lines
7.9 KiB
PHP
217 lines
7.9 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
/**
|
|
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* This file is part of a Moko Consulting project.
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* FILE INFORMATION
|
|
* DEFGROUP: MokoStandards.Templates.Scripts.Release
|
|
* INGROUP: MokoStandards.Templates
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
* PATH: /templates/scripts/release/package_joomla.php
|
|
* BRIEF: Build a distributable ZIP package for a Joomla component
|
|
* NOTE: Deployed to bin/build_package.php in governed WaaS component repos.
|
|
* Run: php bin/build_package.php [--version X.Y.Z] [--dry-run] [--output DIR]
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
// Deployed to bin/build_package.php in org repos — vendor/ is one level up (repo root)
|
|
require_once __DIR__ . '/../vendor/autoload.php';
|
|
|
|
use MokoEnterprise\CliFramework;
|
|
|
|
/**
|
|
* Builds a release-ready ZIP package from a Joomla component repository.
|
|
*
|
|
* What it does:
|
|
* 1. Reads VERSION from composer.json (or --version override)
|
|
* 2. Resolves the component name from the XML manifest
|
|
* 3. Copies tracked files into a temp staging directory, excluding dev artefacts
|
|
* 4. Writes <ComponentName>-<version>.zip into the output directory
|
|
* 5. Generates SHA-256 and MD5 checksums alongside the ZIP
|
|
*/
|
|
class PackageJoomla extends CliFramework
|
|
{
|
|
protected function configure(): void
|
|
{
|
|
$this->setDescription('Build a distributable ZIP package for a Joomla component');
|
|
$this->addArgument('--path', 'Repository root to package', '.');
|
|
$this->addArgument('--version', 'Override version (default: composer.json)', '');
|
|
$this->addArgument('--output', 'Output directory for the ZIP', 'build');
|
|
$this->addArgument('--dry-run', 'Preview without writing files', false);
|
|
}
|
|
|
|
protected function run(): int
|
|
{
|
|
$path = rtrim((string) $this->getArgument('--path'), '/\\');
|
|
$version = (string) $this->getArgument('--version');
|
|
$outDir = $path . '/' . ltrim((string) $this->getArgument('--output'), '/\\');
|
|
$dryRun = (bool) $this->getArgument('--dry-run');
|
|
|
|
if (!is_dir($path)) {
|
|
$this->error("Repository path does not exist: {$path}");
|
|
}
|
|
|
|
// ── 1. Resolve version ────────────────────────────────────────────
|
|
if ($version === '') {
|
|
$composerFile = $path . '/composer.json';
|
|
if (!file_exists($composerFile)) {
|
|
$this->error('Cannot determine version: composer.json not found and --version not set.');
|
|
}
|
|
$composer = json_decode((string) file_get_contents($composerFile), true);
|
|
$version = $composer['version'] ?? '';
|
|
if ($version === '') {
|
|
$this->error('composer.json has no "version" field. Pass --version explicitly.');
|
|
}
|
|
}
|
|
$this->log('INFO', "Version: {$version}");
|
|
|
|
// ── 2. Resolve component name ─────────────────────────────────────
|
|
$componentName = $this->resolveComponentName($path);
|
|
$this->log('INFO', "Component: {$componentName}");
|
|
|
|
// ── 3. Determine output paths ─────────────────────────────────────
|
|
$zipName = "{$componentName}-{$version}.zip";
|
|
$zipPath = "{$outDir}/{$zipName}";
|
|
|
|
if ($dryRun) {
|
|
$this->log('INFO', "[DRY-RUN] Would create: {$zipPath}");
|
|
$this->log('INFO', '[DRY-RUN] No files written.');
|
|
return 0;
|
|
}
|
|
|
|
// ── 4. Stage files ────────────────────────────────────────────────
|
|
if (!is_dir($outDir) && !mkdir($outDir, 0755, true)) {
|
|
$this->error("Cannot create output directory: {$outDir}");
|
|
}
|
|
|
|
$stagingDir = sys_get_temp_dir() . '/moko_pkg_' . uniqid();
|
|
mkdir($stagingDir, 0755, true);
|
|
|
|
$this->section('Staging files');
|
|
$exclude = [
|
|
'build', 'tests', '.git', '.github', 'vendor',
|
|
'node_modules', 'composer.lock', 'phpunit.xml',
|
|
'phpcs.xml', 'phpstan.neon', 'psalm.xml',
|
|
'package.json', 'package-lock.json', '.env',
|
|
];
|
|
$copied = $this->copyTree($path, $stagingDir, $exclude);
|
|
$this->log('INFO', "Staged {$copied} files");
|
|
|
|
// ── 5. Create ZIP ─────────────────────────────────────────────────
|
|
$this->section('Creating package');
|
|
|
|
if (!class_exists('ZipArchive')) {
|
|
$this->error('PHP ZipArchive extension is required but not loaded.');
|
|
}
|
|
|
|
$zip = new \ZipArchive();
|
|
if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
|
|
$this->error("Cannot create ZIP: {$zipPath}");
|
|
}
|
|
|
|
$this->addDirectoryToZip($zip, $stagingDir, '');
|
|
$zip->close();
|
|
|
|
// Clean up staging dir
|
|
$this->removeTree($stagingDir);
|
|
|
|
$this->log('INFO', "Created: {$zipPath}");
|
|
|
|
// ── 6. Checksums ──────────────────────────────────────────────────
|
|
file_put_contents("{$zipPath}.sha256", hash_file('sha256', $zipPath) . " {$zipName}\n");
|
|
file_put_contents("{$zipPath}.md5", hash_file('md5', $zipPath) . " {$zipName}\n");
|
|
$this->log('INFO', "Checksums written: {$zipName}.sha256 / .md5");
|
|
|
|
$this->log('INFO', 'Package complete.');
|
|
return 0;
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────
|
|
|
|
private function resolveComponentName(string $path): string
|
|
{
|
|
// Look for XML manifest at root or in administrator/components/com_*/
|
|
$candidates = array_merge(
|
|
glob($path . '/*.xml') ?: [],
|
|
glob($path . '/administrator/components/com_*/*.xml') ?: []
|
|
);
|
|
foreach ($candidates as $xml) {
|
|
$content = (string) file_get_contents($xml);
|
|
if (preg_match('/<extension[^>]+type=["\']component["\']/', $content)) {
|
|
if (preg_match('/<name>([^<]+)<\/name>/', $content, $m)) {
|
|
return preg_replace('/[^A-Za-z0-9_-]/', '', $m[1]);
|
|
}
|
|
}
|
|
}
|
|
// Fall back to repository directory name
|
|
return basename($path);
|
|
}
|
|
|
|
private function copyTree(string $src, string $dst, array $exclude): int
|
|
{
|
|
$copied = 0;
|
|
$iter = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($src, \FilesystemIterator::SKIP_DOTS),
|
|
\RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
foreach ($iter as $item) {
|
|
$rel = substr($item->getPathname(), strlen($src) + 1);
|
|
$topSegment = explode(DIRECTORY_SEPARATOR, $rel)[0];
|
|
if (in_array($topSegment, $exclude, true)) {
|
|
continue;
|
|
}
|
|
$target = $dst . DIRECTORY_SEPARATOR . $rel;
|
|
if ($item->isDir()) {
|
|
if (!is_dir($target)) {
|
|
mkdir($target, 0755, true);
|
|
}
|
|
} else {
|
|
copy($item->getPathname(), $target);
|
|
$copied++;
|
|
}
|
|
}
|
|
return $copied;
|
|
}
|
|
|
|
private function addDirectoryToZip(\ZipArchive $zip, string $dir, string $prefix): void
|
|
{
|
|
$iter = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
|
|
\RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
foreach ($iter as $item) {
|
|
$rel = substr($item->getPathname(), strlen($dir) + 1);
|
|
$entry = $prefix !== '' ? $prefix . '/' . $rel : $rel;
|
|
$entry = str_replace('\\', '/', $entry);
|
|
if ($item->isDir()) {
|
|
$zip->addEmptyDir($entry);
|
|
} else {
|
|
$zip->addFile($item->getPathname(), $entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function removeTree(string $dir): void
|
|
{
|
|
if (!is_dir($dir)) {
|
|
return;
|
|
}
|
|
$iter = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
|
|
\RecursiveIteratorIterator::CHILD_FIRST
|
|
);
|
|
foreach ($iter as $item) {
|
|
$item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname());
|
|
}
|
|
rmdir($dir);
|
|
}
|
|
}
|
|
|
|
$script = new PackageJoomla('build_package', 'Build a distributable ZIP package for a Joomla component');
|
|
exit($script->execute());
|