e19ca4d7a9
Add cli/joomla_build.php — standalone build tool that detects all Joomla extension types from the XML manifest and builds accordingly: - plugin, module, component, template, library, file: flat ZIP - package: nested ZIPs for each sub-extension in packages/ Update both workflows to call joomla_build.php via the moko-platform PHP API instead of inlining bash build logic. Also extends joomla_release.php with: - typePrefix() for correct naming (plg_, mod_, com_, tpl_, pkg_, lib_) - buildPackageZip() for multi-extension package assembly - copyDir() helper Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
296 lines
10 KiB
PHP
296 lines
10 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* FILE INFORMATION
|
|
* DEFGROUP: moko-platform.CLI
|
|
* INGROUP: moko-platform
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
* PATH: /cli/joomla_build.php
|
|
* VERSION: 05.00.01
|
|
* BRIEF: Build a Joomla extension ZIP from manifest — all types supported
|
|
* NOTE: Called by pre-release and auto-release workflows.
|
|
*
|
|
* USAGE
|
|
* php joomla_build.php --path . --version 02.01.24
|
|
* php joomla_build.php --path . --version 02.01.24 --suffix -dev
|
|
* php joomla_build.php --path . --version 02.01.24 --output build --github-output
|
|
*
|
|
* Supports: plugin, module, component, template, package, library, file
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
// ── Argument parsing ────────────────────────────────────────────────────
|
|
$path = '.';
|
|
$version = '';
|
|
$suffix = '';
|
|
$outputDir = 'build';
|
|
$ghOutput = false;
|
|
|
|
foreach ($argv as $i => $arg) {
|
|
if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1];
|
|
if ($arg === '--version' && isset($argv[$i + 1])) $version = $argv[$i + 1];
|
|
if ($arg === '--suffix' && isset($argv[$i + 1])) $suffix = $argv[$i + 1];
|
|
if ($arg === '--output' && isset($argv[$i + 1])) $outputDir = $argv[$i + 1];
|
|
if ($arg === '--github-output') $ghOutput = true;
|
|
}
|
|
|
|
if ($version === '') {
|
|
fwrite(STDERR, "::error::--version is required\n");
|
|
exit(1);
|
|
}
|
|
|
|
$path = realpath($path) ?: $path;
|
|
|
|
// ── Find source directory ──────────────────────────────────────────────
|
|
$srcDir = null;
|
|
foreach (['src', 'htdocs'] as $d) {
|
|
if (is_dir("{$path}/{$d}")) { $srcDir = "{$path}/{$d}"; break; }
|
|
}
|
|
if ($srcDir === null) {
|
|
fwrite(STDERR, "::error::No src/ or htdocs/ directory in {$path}\n");
|
|
exit(1);
|
|
}
|
|
|
|
// ── Find manifest ──────────────────────────────────────────────────────
|
|
$manifest = findManifest($srcDir);
|
|
if ($manifest === null) {
|
|
fwrite(STDERR, "::error::No Joomla manifest found in {$srcDir}\n");
|
|
exit(1);
|
|
}
|
|
|
|
fwrite(STDERR, "Manifest: {$manifest}\n");
|
|
|
|
// ── Parse manifest ─────────────────────────────────────────────────────
|
|
$meta = parseManifest($manifest);
|
|
|
|
// Resolve language-key names (e.g. PLG_SYSTEM_MOKOWAAS -> "System - Moko WaaS")
|
|
if (preg_match('/^[A-Z_]+$/', $meta['name'])) {
|
|
$resolved = resolveLanguageKey($srcDir, $meta['name']);
|
|
if ($resolved !== null) { $meta['name'] = $resolved; }
|
|
}
|
|
|
|
$prefix = typePrefix($meta);
|
|
$zipName = "{$prefix}{$meta['element']}-{$version}{$suffix}.zip";
|
|
$zipPath = "{$outputDir}/{$zipName}";
|
|
|
|
fwrite(STDERR, "=== Joomla Build: {$meta['type']} — {$meta['element']} {$version}{$suffix} ===\n");
|
|
fwrite(STDERR, " Type: {$meta['type']}\n");
|
|
fwrite(STDERR, " Element: {$meta['element']}\n");
|
|
fwrite(STDERR, " Group: " . ($meta['group'] ?: 'n/a') . "\n");
|
|
fwrite(STDERR, " Name: {$meta['name']}\n");
|
|
fwrite(STDERR, " Output: {$zipName}\n");
|
|
|
|
// ── Build ──────────────────────────────────────────────────────────────
|
|
if (!is_dir($outputDir)) { mkdir($outputDir, 0755, true); }
|
|
|
|
if ($meta['type'] === 'package') {
|
|
buildPackageZip($srcDir, $zipPath);
|
|
} else {
|
|
buildZip($srcDir, $zipPath);
|
|
}
|
|
|
|
$sha256 = hash_file('sha256', $zipPath);
|
|
$size = filesize($zipPath);
|
|
|
|
fwrite(STDERR, "Package: {$zipPath} ({$size} bytes, SHA: " . substr($sha256, 0, 16) . "...)\n");
|
|
|
|
// ── Output variables ───────────────────────────────────────────────────
|
|
$vars = [
|
|
'zip_name' => $zipName,
|
|
'zip_path' => $zipPath,
|
|
'sha256' => $sha256,
|
|
'ext_type' => $meta['type'],
|
|
'ext_element' => $meta['element'],
|
|
'ext_name' => $meta['name'],
|
|
'ext_group' => $meta['group'],
|
|
'type_prefix' => $prefix,
|
|
];
|
|
|
|
if ($ghOutput && ($ghFile = getenv('GITHUB_OUTPUT')) !== false && $ghFile !== '') {
|
|
$fh = fopen($ghFile, 'a');
|
|
foreach ($vars as $k => $v) { fwrite($fh, "{$k}={$v}\n"); }
|
|
fclose($fh);
|
|
fwrite(STDERR, "Wrote " . count($vars) . " outputs to GITHUB_OUTPUT\n");
|
|
} else {
|
|
foreach ($vars as $k => $v) { echo "{$k}={$v}\n"; }
|
|
}
|
|
|
|
exit(0);
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// Functions
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
function findManifest(string $dir): ?string
|
|
{
|
|
// Priority: pkg_*.xml (packages), then any *.xml with <extension>
|
|
foreach (glob("{$dir}/pkg_*.xml") ?: [] as $f) { return $f; }
|
|
foreach (glob("{$dir}/*.xml") ?: [] as $f) {
|
|
if (str_contains((string) file_get_contents($f), '<extension')) { return $f; }
|
|
}
|
|
// Broader nested search
|
|
$iter = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
|
|
RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
foreach ($iter as $item) {
|
|
if ($item->isFile() && $item->getExtension() === 'xml') {
|
|
if (str_contains((string) file_get_contents($item->getPathname()), '<extension')) {
|
|
return $item->getPathname();
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function parseManifest(string $file): array
|
|
{
|
|
$xml = simplexml_load_file($file);
|
|
$name = (string) ($xml->name ?? '');
|
|
$type = (string) ($xml->attributes()->type ?? 'component');
|
|
$element = (string) ($xml->element ?? '');
|
|
$group = (string) ($xml->attributes()->group ?? '');
|
|
|
|
// Fallback element detection
|
|
if ($element === '') { $element = (string) ($xml->attributes()->plugin ?? ''); }
|
|
if ($element === '') { $element = (string) ($xml->attributes()->module ?? ''); }
|
|
if ($element === '') {
|
|
$element = strtolower(basename($file, '.xml'));
|
|
if (in_array($element, ['templatedetails', 'manifest'], true)) {
|
|
$element = strtolower(basename(dirname($file)));
|
|
}
|
|
}
|
|
if ($name === '') { $name = $element; }
|
|
|
|
return compact('name', 'type', 'element', 'group');
|
|
}
|
|
|
|
function typePrefix(array $meta): string
|
|
{
|
|
return match ($meta['type']) {
|
|
'plugin' => "plg_{$meta['group']}_",
|
|
'module' => 'mod_',
|
|
'component' => 'com_',
|
|
'template' => 'tpl_',
|
|
'package' => 'pkg_',
|
|
'library' => 'lib_',
|
|
default => '',
|
|
};
|
|
}
|
|
|
|
function resolveLanguageKey(string $srcDir, string $key): ?string
|
|
{
|
|
$iter = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($srcDir, FilesystemIterator::SKIP_DOTS)
|
|
);
|
|
foreach ($iter as $item) {
|
|
if ($item->isFile() && str_ends_with($item->getFilename(), '.sys.ini')) {
|
|
foreach (file($item->getPathname()) as $line) {
|
|
if (preg_match('/^' . preg_quote($key, '/') . '="(.+)"/', trim($line), $m)) {
|
|
return $m[1];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function isExcluded(string $name): bool
|
|
{
|
|
if ($name === '.ftpignore') return true;
|
|
if (str_starts_with($name, 'sftp-config')) return true;
|
|
if (str_starts_with($name, '.env')) return true;
|
|
if (str_starts_with($name, '.build-trigger')) return true;
|
|
$ext = pathinfo($name, PATHINFO_EXTENSION);
|
|
return in_array($ext, ['ppk', 'pem', 'key', 'local'], true);
|
|
}
|
|
|
|
function buildZip(string $srcDir, string $outPath): void
|
|
{
|
|
$zip = new ZipArchive();
|
|
if ($zip->open($outPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
|
fwrite(STDERR, "::error::Cannot create ZIP: {$outPath}\n");
|
|
exit(1);
|
|
}
|
|
$iter = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($srcDir, FilesystemIterator::SKIP_DOTS),
|
|
RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
foreach ($iter as $file) {
|
|
$local = str_replace('\\', '/', substr($file->getPathname(), strlen($srcDir) + 1));
|
|
if (isExcluded(basename($local))) continue;
|
|
$file->isDir() ? $zip->addEmptyDir($local) : $zip->addFile($file->getPathname(), $local);
|
|
}
|
|
$zip->close();
|
|
}
|
|
|
|
function buildPackageZip(string $srcDir, string $outPath): void
|
|
{
|
|
fwrite(STDERR, "Building Joomla package (multi-extension)...\n");
|
|
$staging = sys_get_temp_dir() . '/moko_pkg_' . uniqid();
|
|
mkdir($staging, 0755, true);
|
|
|
|
// 1. Zip each sub-extension in packages/
|
|
$packagesDir = "{$srcDir}/packages";
|
|
if (is_dir($packagesDir)) {
|
|
foreach (glob("{$packagesDir}/*", GLOB_ONLYDIR) as $extDir) {
|
|
$subManifest = findManifest($extDir);
|
|
if ($subManifest) {
|
|
$sub = parseManifest($subManifest);
|
|
$subPrefix = typePrefix($sub);
|
|
$subZipName = "{$subPrefix}{$sub['element']}.zip";
|
|
} else {
|
|
$subZipName = basename($extDir) . '.zip';
|
|
}
|
|
|
|
fwrite(STDERR, " Sub-extension: {$subZipName}\n");
|
|
buildZip($extDir, "{$staging}/{$subZipName}");
|
|
}
|
|
}
|
|
|
|
// 2. Copy package-level files (manifest, script, language)
|
|
foreach (glob("{$srcDir}/*.xml") ?: [] as $f) copy($f, "{$staging}/" . basename($f));
|
|
foreach (glob("{$srcDir}/*.php") ?: [] as $f) copy($f, "{$staging}/" . basename($f));
|
|
foreach (['language', 'administrator'] as $d) {
|
|
if (is_dir("{$srcDir}/{$d}")) {
|
|
copyTree("{$srcDir}/{$d}", "{$staging}/{$d}");
|
|
}
|
|
}
|
|
|
|
// 3. Create outer zip
|
|
buildZip($staging, $outPath);
|
|
|
|
// Cleanup
|
|
rmTree($staging);
|
|
}
|
|
|
|
function copyTree(string $src, string $dst): void
|
|
{
|
|
if (!is_dir($dst)) mkdir($dst, 0755, true);
|
|
$iter = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($src, FilesystemIterator::SKIP_DOTS),
|
|
RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
foreach ($iter as $item) {
|
|
$target = "{$dst}/" . $iter->getSubPathname();
|
|
$item->isDir() ? (is_dir($target) || mkdir($target, 0755, true)) : copy($item->getPathname(), $target);
|
|
}
|
|
}
|
|
|
|
function rmTree(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);
|
|
}
|