#!/usr/bin/env php * * 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_dolibarr.php * BRIEF: Build a distributable ZIP package for a Dolibarr module * NOTE: Deployed to bin/build_package.php in governed Dolibarr module 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 Dolibarr module repository. * * What it does: * 1. Reads VERSION from composer.json (or --version override) * 2. Resolves the module name from core/modules/mod*.class.php * 3. Copies tracked files into a temp staging directory, excluding dev artefacts * 4. Writes -.zip into the output directory * 5. Generates SHA-256 and MD5 checksums alongside the ZIP */ class PackageDolibarr extends CliFramework { protected function configure(): void { $this->setDescription('Build a distributable ZIP package for a Dolibarr module'); $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 module name ──────────────────────────────────────── $moduleName = $this->resolveModuleName($path); $this->log('INFO', "Module: {$moduleName}"); // ── 3. Determine output paths ───────────────────────────────────── $zipName = "{$moduleName}-{$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 resolveModuleName(string $path): string { $pattern = $path . '/src/core/modules/mod*.class.php'; $matches = glob($pattern) ?: []; if (!empty($matches)) { return basename($matches[0], '.class.php'); } // 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); // Skip excluded top-level entries $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 PackageDolibarr('build_package', 'Build a distributable ZIP package for a Dolibarr module'); exit($script->execute());