From 8e30fa54b3c2f945e856e0e5a371f49a7b3556ee Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Fri, 26 Jun 2026 19:26:54 -0500 Subject: [PATCH 1/2] feat: support nested Joomla packages in release_package.php When a sub-package directory contains its own pkg_*.xml manifest and packages/ directory (e.g. a git submodule), recursively build each sub-extension into ZIPs before assembling the outer package ZIP. --- cli/release_package.php | 56 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/cli/release_package.php b/cli/release_package.php index c5f83f4..fa0dc5d 100644 --- a/cli/release_package.php +++ b/cli/release_package.php @@ -270,12 +270,66 @@ class ReleasePackageCli extends CliFramework } } + // Check if sub-source is itself a Joomla package (nested package) + $nestedPkgManifests = glob("{$subSourceDir}/pkg_*.xml") ?: []; + $isNestedPackage = !empty($nestedPkgManifests) && is_dir("{$subSourceDir}/packages"); + $subZip = new \ZipArchive(); if ($subZip->open($subZipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { $this->log('ERROR', "Failed to create sub-package ZIP: {$subZipPath}"); continue; } - $this->addDirToZip($subZip, $subSourceDir, '', $this->excludePatterns); + + if ($isNestedPackage) { + // Build nested package: zip each sub-extension, then assemble + echo " Building nested package: {$subName}\n"; + $nestedPkgDirs = glob("{$subSourceDir}/packages/*", GLOB_ONLYDIR) ?: []; + + // Read nested manifest to filter only listed sub-extensions + $nestedManifested = []; + foreach ($nestedPkgManifests as $npmf) { + $npmXml = @simplexml_load_file($npmf); + if ($npmXml && isset($npmXml->files)) { + foreach ($npmXml->files->file as $fn) { + $nzn = pathinfo((string) $fn, PATHINFO_FILENAME); + if (!empty($nzn)) { + $nestedManifested[$nzn] = true; + } + } + } + } + + foreach ($nestedPkgDirs as $npd) { + $nestedSubName = basename($npd); + if (!empty($nestedManifested) && !isset($nestedManifested[$nestedSubName])) { + continue; + } + $nestedSubZipPath = "{$outputDir}/{$nestedSubName}.zip"; + $nsZip = new \ZipArchive(); + if ($nsZip->open($nestedSubZipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { + continue; + } + $this->addDirToZip($nsZip, $npd, '', $this->excludePatterns); + $nsZip->close(); + $subZip->addFile($nestedSubZipPath, "packages/{$nestedSubName}.zip"); + echo " Nested sub: {$nestedSubName}.zip\n"; + } + + // Add top-level files (manifest, script, language) + $nestedTopFiles = array_merge( + glob("{$subSourceDir}/*.xml") ?: [], + glob("{$subSourceDir}/*.php") ?: [] + ); + foreach ($nestedTopFiles as $ntf) { + $subZip->addFile($ntf, basename($ntf)); + } + $nestedLangDir = "{$subSourceDir}/language"; + if (is_dir($nestedLangDir)) { + $this->addDirToZip($subZip, $nestedLangDir, 'language', $this->excludePatterns); + } + } else { + $this->addDirToZip($subZip, $subSourceDir, '', $this->excludePatterns); + } $subZip->close(); $zip->addFile($subZipPath, "packages/{$subName}.zip"); -- 2.52.0 From 3c420284e320e87d302c7b35c7333ee48832b8ed Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Fri, 26 Jun 2026 21:11:46 -0500 Subject: [PATCH 2/2] fix: harden nested package build against collisions and missing assets - Write nested sub-extension ZIPs to subdirectory to prevent filename collisions with outer package ZIPs (deferred ZipArchive::addFile reads) - Apply exclude patterns to nested top-level files (parity with outer) - Patch nested manifest with folder="packages" so Joomla finds sub-ZIPs - Include all top-level directories (not just language/) in nested packages Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG --- cli/release_package.php | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/cli/release_package.php b/cli/release_package.php index fa0dc5d..665aaaf 100644 --- a/cli/release_package.php +++ b/cli/release_package.php @@ -285,7 +285,6 @@ class ReleasePackageCli extends CliFramework echo " Building nested package: {$subName}\n"; $nestedPkgDirs = glob("{$subSourceDir}/packages/*", GLOB_ONLYDIR) ?: []; - // Read nested manifest to filter only listed sub-extensions $nestedManifested = []; foreach ($nestedPkgManifests as $npmf) { $npmXml = @simplexml_load_file($npmf); @@ -299,12 +298,16 @@ class ReleasePackageCli extends CliFramework } } + // Use a subdirectory to avoid filename collisions with outer ZIPs + $nestedOutputDir = "{$outputDir}/nested_{$subName}"; + @mkdir($nestedOutputDir, 0755, true); + foreach ($nestedPkgDirs as $npd) { $nestedSubName = basename($npd); if (!empty($nestedManifested) && !isset($nestedManifested[$nestedSubName])) { continue; } - $nestedSubZipPath = "{$outputDir}/{$nestedSubName}.zip"; + $nestedSubZipPath = "{$nestedOutputDir}/{$nestedSubName}.zip"; $nsZip = new \ZipArchive(); if ($nsZip->open($nestedSubZipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { continue; @@ -315,17 +318,38 @@ class ReleasePackageCli extends CliFramework echo " Nested sub: {$nestedSubName}.zip\n"; } - // Add top-level files (manifest, script, language) + // Patch nested manifest: ensure folder="packages" so Joomla finds sub-ZIPs + foreach ($nestedPkgManifests as $npmf) { + $npmContent = file_get_contents($npmf); + if ($npmContent !== false + && strpos($npmContent, '') !== false + && strpos($npmContent, 'folder="packages"') === false + ) { + $npmContent = str_replace('', '', $npmContent); + file_put_contents($npmf, $npmContent); + echo " Fixed: added folder=\"packages\" to " . basename($npmf) . "\n"; + } + } + + // Add top-level files (manifest, script, etc.) with exclude filter $nestedTopFiles = array_merge( glob("{$subSourceDir}/*.xml") ?: [], glob("{$subSourceDir}/*.php") ?: [] ); foreach ($nestedTopFiles as $ntf) { - $subZip->addFile($ntf, basename($ntf)); + if (!$this->isExcluded(basename($ntf), $this->excludePatterns)) { + $subZip->addFile($ntf, basename($ntf)); + } } - $nestedLangDir = "{$subSourceDir}/language"; - if (is_dir($nestedLangDir)) { - $this->addDirToZip($subZip, $nestedLangDir, 'language', $this->excludePatterns); + + // Add all top-level directories except packages/ + $nestedTopDirs = glob("{$subSourceDir}/*", GLOB_ONLYDIR) ?: []; + foreach ($nestedTopDirs as $ntd) { + $ndName = basename($ntd); + if ($ndName === 'packages') { + continue; + } + $this->addDirToZip($subZip, $ntd, $ndName, $this->excludePatterns); } } else { $this->addDirToZip($subZip, $subSourceDir, '', $this->excludePatterns); -- 2.52.0