#!/usr/bin/env php * * 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 foreach (glob("{$dir}/pkg_*.xml") ?: [] as $f) { return $f; } foreach (glob("{$dir}/*.xml") ?: [] as $f) { if (str_contains((string) file_get_contents($f), 'isFile() && $item->getExtension() === 'xml') { if (str_contains((string) file_get_contents($item->getPathname()), '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 ?? ''); // For packages, prefer as the clean element (avoids pkg_pkg_ duplication) if ($type === 'package' && $element === '') { $packageName = (string) ($xml->packagename ?? ''); if ($packageName !== '') { $element = $packageName; } } // 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))); } } // Strip existing type prefix to prevent duplication (e.g. pkg_mokowaas → mokowaas) $element = preg_replace('/^(pkg_|com_|mod_|plg_\w+_|tpl_|lib_)/', '', $element); 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); }