From 989e84c44cb0d176ac5477a00e65ba2dc3fc874b Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 25 May 2026 19:34:13 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20package=5Fbuild.php=20=E2=80=94=20correc?= =?UTF-8?q?t=20Joomla=20package=20extension=20builds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs fixed: 1. Double prefix: pkg_pkg_mokogallery → pkg_mokogallery (strip prefix from element name when it already matches the type prefix) 2. Package structure: sub-extension ZIPs now placed in packages/ subdir (was putting them in root), language/ directory now included Correct ZIP structure: pkg_mokogallery-XX.XX.XX.zip pkg_mokogallery.xml script.php language/ packages/ com_mokogallery.zip mod_mokogallery.zip plg_*.zip Closes #92 Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 1 + cli/package_build.php | 388 ++++++++++++++++++++++++------------------ 2 files changed, 226 insertions(+), 163 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a3fa75..09187cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Version format: `XX.YY.ZZ` (zero-padded semver). ### Fixed - `release_cascade.php`: accept `release-candidate` as stability value (was only accepting `rc`, causing cascade to silently skip) - PHPStan bumped from level 0 to level 2 — fixed 67 type errors (undefined variables, missing methods, wrong signatures, dead code) +- `package_build.php`: fix 0-byte ZIP for Joomla package extensions — sub-zips now in `packages/` subdir, no double `pkg_pkg_` prefix, includes `language/` dir (closes #92) ## [06.00.00] - 2026-05-25 diff --git a/cli/package_build.php b/cli/package_build.php index 1d18182..82dc838 100644 --- a/cli/package_build.php +++ b/cli/package_build.php @@ -1,5 +1,6 @@ #!/usr/bin/env php * * SPDX-License-Identifier: GPL-3.0-or-later @@ -37,17 +38,29 @@ $elementOverride = null; $githubOutput = 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 === '--output-dir' && isset($argv[$i + 1])) $outputDir = $argv[$i + 1]; - if ($arg === '--type-prefix' && isset($argv[$i + 1])) $typePrefixOverride = $argv[$i + 1]; - if ($arg === '--element' && isset($argv[$i + 1])) $elementOverride = $argv[$i + 1]; - if ($arg === '--github-output') $githubOutput = true; + if ($arg === '--path' && isset($argv[$i + 1])) { + $path = $argv[$i + 1]; + } + if ($arg === '--version' && isset($argv[$i + 1])) { + $version = $argv[$i + 1]; + } + if ($arg === '--output-dir' && isset($argv[$i + 1])) { + $outputDir = $argv[$i + 1]; + } + if ($arg === '--type-prefix' && isset($argv[$i + 1])) { + $typePrefixOverride = $argv[$i + 1]; + } + if ($arg === '--element' && isset($argv[$i + 1])) { + $elementOverride = $argv[$i + 1]; + } + if ($arg === '--github-output') { + $githubOutput = true; + } } if ($version === null) { - fwrite(STDERR, "Usage: package_build.php --path . --version XX.YY.ZZ [--output-dir /tmp]\n"); - exit(1); + fwrite(STDERR, "Usage: package_build.php --path . --version XX.YY.ZZ [--output-dir /tmp]\n"); + exit(1); } $root = realpath($path) ?: $path; @@ -55,15 +68,15 @@ $root = realpath($path) ?: $path; // -- Determine source directory ----------------------------------------------- $sourceDir = null; foreach (['src', 'htdocs'] as $candidate) { - if (is_dir("{$root}/{$candidate}")) { - $sourceDir = "{$root}/{$candidate}"; - break; - } + if (is_dir("{$root}/{$candidate}")) { + $sourceDir = "{$root}/{$candidate}"; + break; + } } if ($sourceDir === null) { - fwrite(STDERR, "No src/ or htdocs/ directory found in {$root}\n"); - exit(1); + fwrite(STDERR, "No src/ or htdocs/ directory found in {$root}\n"); + exit(1); } // -- Determine element and type prefix from manifest -------------------------- @@ -73,54 +86,80 @@ $extType = ''; $isPackage = false; if ($extElement === null || $typePrefixOverride === null) { - // Find manifest - $manifest = null; - foreach (glob("{$sourceDir}/pkg_*.xml") ?: [] as $f) { - if (strpos(file_get_contents($f), '([^<]+)<\/element>/', $xml, $m)) $extElement = $m[1]; - elseif (preg_match('/plugin="([^"]+)"/', $xml, $m)) $extElement = $m[1]; - elseif (preg_match('/module="([^"]+)"/', $xml, $m)) $extElement = $m[1]; - else $extElement = strtolower(pathinfo($manifest, PATHINFO_FILENAME)); - } + if ($extElement === null) { + if (preg_match('/([^<]+)<\/element>/', $xml, $m)) { + $extElement = $m[1]; + } elseif (preg_match('/plugin="([^"]+)"/', $xml, $m)) { + $extElement = $m[1]; + } elseif (preg_match('/module="([^"]+)"/', $xml, $m)) { + $extElement = $m[1]; + } else { + $extElement = strtolower(pathinfo($manifest, PATHINFO_FILENAME)); + } + } - if (preg_match('/]*type="([^"]+)"/', $xml, $m)) $extType = $m[1]; - $extFolder = ''; - if (preg_match('/]*group="([^"]+)"/', $xml, $m)) $extFolder = $m[1]; + if (preg_match('/]*type="([^"]+)"/', $xml, $m)) { + $extType = $m[1]; + } + $extFolder = ''; + if (preg_match('/]*group="([^"]+)"/', $xml, $m)) { + $extFolder = $m[1]; + } - if ($typePrefixOverride === null) { - switch ($extType) { - case 'plugin': $typePrefix = "plg_{$extFolder}_"; break; - case 'module': $typePrefix = 'mod_'; break; - case 'component': $typePrefix = 'com_'; break; - case 'template': $typePrefix = 'tpl_'; break; - case 'library': $typePrefix = 'lib_'; break; - case 'package': $typePrefix = 'pkg_'; break; - } - } + if ($typePrefixOverride === null) { + switch ($extType) { + case 'plugin': + $typePrefix = "plg_{$extFolder}_"; + break; + case 'module': + $typePrefix = 'mod_'; + break; + case 'component': + $typePrefix = 'com_'; + break; + case 'template': + $typePrefix = 'tpl_'; + break; + case 'library': + $typePrefix = 'lib_'; + break; + case 'package': + $typePrefix = 'pkg_'; + break; + } + } - $isPackage = ($extType === 'package' && is_dir("{$sourceDir}/packages")); - } + $isPackage = ($extType === 'package' && is_dir("{$sourceDir}/packages")); + } } if ($extElement === null) { - $extElement = strtolower(basename($root)); + $extElement = strtolower(basename($root)); +} + +// Prevent double prefix (e.g. pkg_pkg_mokogallery) +if ($typePrefix !== '' && str_starts_with($extElement, rtrim($typePrefix, '_'))) { + $extElement = substr($extElement, strlen(rtrim($typePrefix, '_')) + 1); } $zipName = "{$typePrefix}{$extElement}-{$version}.zip"; @@ -130,87 +169,106 @@ $tarPath = "{$outputDir}/{$tarName}"; // -- Exclude patterns --------------------------------------------------------- $excludePatterns = [ - '.ftpignore', - 'sftp-config*', - '*.ppk', - '*.pem', - '*.key', - '.env*', + '.ftpignore', + 'sftp-config*', + '*.ppk', + '*.pem', + '*.key', + '.env*', ]; // -- Build packages ----------------------------------------------------------- if ($isPackage) { - echo "=== Building Joomla PACKAGE (multi-extension) ===\n"; + echo "=== Building Joomla PACKAGE (multi-extension) ===\n"; - $stagingDir = sys_get_temp_dir() . '/moko-pkg-' . uniqid(); - mkdir($stagingDir, 0755, true); + $stagingDir = sys_get_temp_dir() . '/moko-pkg-' . uniqid(); + $packagesDir = "{$stagingDir}/packages"; + mkdir($packagesDir, 0755, true); - // ZIP each sub-extension - foreach (glob("{$sourceDir}/packages/*/") ?: [] as $extDir) { - $subName = basename($extDir); - echo " Packaging sub-extension: {$subName}\n"; + // ZIP each sub-extension into packages/ + foreach (glob("{$sourceDir}/packages/*/") ?: [] as $extDir) { + $subName = basename($extDir); + echo " Packaging sub-extension: {$subName}\n"; - $subZip = new ZipArchive(); - $subZipPath = "{$stagingDir}/{$subName}.zip"; - if ($subZip->open($subZipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { - fwrite(STDERR, "Failed to create ZIP for {$subName}\n"); - continue; - } + $subZip = new ZipArchive(); + $subZipPath = "{$packagesDir}/{$subName}.zip"; + if ($subZip->open($subZipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { + fwrite(STDERR, "Failed to create ZIP for {$subName}\n"); + continue; + } - addDirectoryToZip($subZip, $extDir, '', $excludePatterns); - $subZip->close(); - } + addDirectoryToZip($subZip, $extDir, '', $excludePatterns); + $subZip->close(); + echo " -> packages/{$subName}.zip (" . filesize($subZipPath) . " bytes)\n"; + } - // Copy package-level files - foreach (array_merge(glob("{$sourceDir}/*.xml") ?: [], glob("{$sourceDir}/*.php") ?: []) as $f) { - copy($f, "{$stagingDir}/" . basename($f)); - } + // Copy package-level files (manifest, script.php, etc.) + foreach (array_merge(glob("{$sourceDir}/*.xml") ?: [], glob("{$sourceDir}/*.php") ?: []) as $f) { + copy($f, "{$stagingDir}/" . basename($f)); + } - // Create ZIP from staging - $zip = new ZipArchive(); - if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { - fwrite(STDERR, "Failed to create ZIP: {$zipPath}\n"); - exit(1); - } - addDirectoryToZip($zip, $stagingDir, '', []); - $zip->close(); + // Copy language directory if present + if (is_dir("{$sourceDir}/language")) { + $langDest = "{$stagingDir}/language"; + mkdir($langDest, 0755, true); + $langIterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator("{$sourceDir}/language", RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST + ); + foreach ($langIterator as $item) { + $target = $langDest . '/' . substr($item->getPathname(), strlen("{$sourceDir}/language") + 1); + if ($item->isDir()) { + mkdir($target, 0755, true); + } else { + copy($item->getPathname(), $target); + } + } + } - // Create tar.gz — all arguments are escaped via escapeshellarg() - $tarCmd = sprintf( - 'tar -czf %s -C %s .', - escapeshellarg($tarPath), - escapeshellarg($stagingDir) - ); - passthru($tarCmd, $tarReturn); + // Create ZIP from staging + $zip = new ZipArchive(); + if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { + fwrite(STDERR, "Failed to create ZIP: {$zipPath}\n"); + exit(1); + } + addDirectoryToZip($zip, $stagingDir, '', []); + $zip->close(); - // Cleanup staging - $cleanCmd = sprintf('rm -rf %s', escapeshellarg($stagingDir)); - passthru($cleanCmd); + // Create tar.gz — all arguments are escaped via escapeshellarg() + $tarCmd = sprintf( + 'tar -czf %s -C %s .', + escapeshellarg($tarPath), + escapeshellarg($stagingDir) + ); + passthru($tarCmd, $tarReturn); + // Cleanup staging + $cleanCmd = sprintf('rm -rf %s', escapeshellarg($stagingDir)); + passthru($cleanCmd); } else { - echo "=== Building standard extension package ===\n"; + echo "=== Building standard extension package ===\n"; - // ZIP - $zip = new ZipArchive(); - if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { - fwrite(STDERR, "Failed to create ZIP: {$zipPath}\n"); - exit(1); - } - addDirectoryToZip($zip, $sourceDir, '', $excludePatterns); - $zip->close(); + // ZIP + $zip = new ZipArchive(); + if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { + fwrite(STDERR, "Failed to create ZIP: {$zipPath}\n"); + exit(1); + } + addDirectoryToZip($zip, $sourceDir, '', $excludePatterns); + $zip->close(); - // tar.gz — all arguments are escaped via escapeshellarg() - $excludeArgs = ''; - foreach ($excludePatterns as $pattern) { - $excludeArgs .= ' --exclude=' . escapeshellarg($pattern); - } - $tarCmd = sprintf( - 'tar -czf %s -C %s%s .', - escapeshellarg($tarPath), - escapeshellarg($sourceDir), - $excludeArgs - ); - passthru($tarCmd, $tarReturn); + // tar.gz — all arguments are escaped via escapeshellarg() + $excludeArgs = ''; + foreach ($excludePatterns as $pattern) { + $excludeArgs .= ' --exclude=' . escapeshellarg($pattern); + } + $tarCmd = sprintf( + 'tar -czf %s -C %s%s .', + escapeshellarg($tarPath), + escapeshellarg($sourceDir), + $excludeArgs + ); + passthru($tarCmd, $tarReturn); } // -- Calculate SHA-256 -------------------------------------------------------- @@ -224,29 +282,31 @@ echo "\n"; echo "ZIP: {$zipName} ({$zipSize} bytes)\n"; echo " SHA-256: {$sha256Zip}\n"; if ($tarSize > 0) { - echo "TAR: {$tarName} ({$tarSize} bytes)\n"; - echo " SHA-256: {$sha256Tar}\n"; + echo "TAR: {$tarName} ({$tarSize} bytes)\n"; + echo " SHA-256: {$sha256Tar}\n"; } // -- Export to GITHUB_OUTPUT -------------------------------------------------- if ($githubOutput) { - $ghOutput = getenv('GITHUB_OUTPUT'); - $lines = [ - "zip_name={$zipName}", - "tar_name={$tarName}", - "zip_path={$zipPath}", - "tar_path={$tarPath}", - "sha256_zip={$sha256Zip}", - "sha256_tar={$sha256Tar}", - "type_prefix={$typePrefix}", - "ext_element={$extElement}", - ]; - if ($ghOutput) { - file_put_contents($ghOutput, implode("\n", $lines) . "\n", FILE_APPEND); - fwrite(STDERR, "Exported " . count($lines) . " fields to GITHUB_OUTPUT\n"); - } else { - foreach ($lines as $line) echo "{$line}\n"; - } + $ghOutput = getenv('GITHUB_OUTPUT'); + $lines = [ + "zip_name={$zipName}", + "tar_name={$tarName}", + "zip_path={$zipPath}", + "tar_path={$tarPath}", + "sha256_zip={$sha256Zip}", + "sha256_tar={$sha256Tar}", + "type_prefix={$typePrefix}", + "ext_element={$extElement}", + ]; + if ($ghOutput) { + file_put_contents($ghOutput, implode("\n", $lines) . "\n", FILE_APPEND); + fwrite(STDERR, "Exported " . count($lines) . " fields to GITHUB_OUTPUT\n"); + } else { + foreach ($lines as $line) { + echo "{$line}\n"; + } + } } exit(0); @@ -256,33 +316,35 @@ exit(0); // ============================================================================= function addDirectoryToZip(ZipArchive $zip, string $dir, string $prefix, array $excludes): void { - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), - RecursiveIteratorIterator::SELF_FIRST - ); + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST + ); - foreach ($iterator as $file) { - $filePath = $file->getPathname(); - $relativePath = $prefix . substr($filePath, strlen($dir) + 1); + foreach ($iterator as $file) { + $filePath = $file->getPathname(); + $relativePath = $prefix . substr($filePath, strlen($dir) + 1); - // Check excludes - $basename = basename($filePath); - $skip = false; - foreach ($excludes as $pattern) { - if (fnmatch($pattern, $basename)) { - $skip = true; - break; - } - } - if ($skip) continue; + // Check excludes + $basename = basename($filePath); + $skip = false; + foreach ($excludes as $pattern) { + if (fnmatch($pattern, $basename)) { + $skip = true; + break; + } + } + if ($skip) { + continue; + } - // Normalize path separators for ZIP - $relativePath = str_replace('\\', '/', $relativePath); + // Normalize path separators for ZIP + $relativePath = str_replace('\\', '/', $relativePath); - if ($file->isDir()) { - $zip->addEmptyDir($relativePath); - } else { - $zip->addFile($filePath, $relativePath); - } - } + if ($file->isDir()) { + $zip->addEmptyDir($relativePath); + } else { + $zip->addFile($filePath, $relativePath); + } + } }