From 0f43e2af2623124bb8f542a99efb5e6df63b2cad Mon Sep 17 00:00:00 2001 From: jwaisner Date: Sat, 8 Nov 2025 09:42:17 -0600 Subject: [PATCH 1/3] Added Gradle to build process --- .gitignore | 7 + build.gradle | 375 ++++++++++++++++++++++++++++++++++++++++++++++ gradle.properties | 19 +++ settings.gradle | 25 ++++ 4 files changed, 426 insertions(+) create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore index 207bc8a..913ec11 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,10 @@ # Qodo /.qodo + +# Gradle +.gradle/ +build/ +!gradle-wrapper.jar +*.gradle.kts.compiled +.gradle-cache/ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..cdf567d --- /dev/null +++ b/build.gradle @@ -0,0 +1,375 @@ +/* + * Bearsampp Module Perl - Gradle Build + * + * This is a hybrid build configuration that: + * 1. Imports existing Ant build files for backward compatibility + * 2. Provides modern Gradle features (caching, incremental builds, parallel execution) + * 3. Allows gradual migration from Ant to Gradle + * + * Usage: + * gradle tasks - List all available tasks + * gradle release - Interactive release (prompts for version) + * gradle release "-PbundleVersion=5.40.0" - Non-interactive release + * gradle clean - Clean build artifacts + * gradle info - Display build information + */ + +plugins { + id 'base' +} + +// Load build properties +def buildProps = new Properties() +file('build.properties').withInputStream { buildProps.load(it) } + +// Project information +group = 'com.bearsampp.modules' +version = buildProps.getProperty('bundle.release', '1.0.0') +description = "Bearsampp Module - ${buildProps.getProperty('bundle.name', 'perl')}" + +// Define project paths +ext { + projectBasedir = projectDir.absolutePath + rootDir = projectDir.parent + devPath = file("${rootDir}/dev").absolutePath + buildPropertiesFile = file('build.properties').absolutePath + + // Bundle properties from build.properties + bundleName = buildProps.getProperty('bundle.name', 'perl') + bundleRelease = buildProps.getProperty('bundle.release', '1.0.0') + bundleType = buildProps.getProperty('bundle.type', 'tools') + bundleFormat = buildProps.getProperty('bundle.format', '7z') +} + +// Verify dev path exists +if (!file(ext.devPath).exists()) { + throw new GradleException("Dev path not found: ${ext.devPath}. Please ensure the 'dev' project exists in ${ext.rootDir}") +} + +// Configure repositories for dependencies +repositories { + mavenCentral() +} + +// ============================================================================ +// ANT INTEGRATION - Import existing Ant build files +// ============================================================================ + +// Set Ant properties before importing +ant.properties['project.basedir'] = ext.projectBasedir +ant.properties['root.dir'] = ext.rootDir +ant.properties['dev.path'] = ext.devPath +ant.properties['build.properties'] = ext.buildPropertiesFile + +// Load build.properties into Ant +ant.property(file: ext.buildPropertiesFile) + +// Import the main Ant build file +// This preserves all existing Ant functionality +ant.importBuild('build.xml') { antTargetName -> + // Map Ant target names to Gradle task names + // Prefix all with 'ant-' to avoid conflicts + return "ant-${antTargetName}".toString() +} + +// ============================================================================ +// GRADLE NATIVE TASKS - Modern alternatives and enhancements +// ============================================================================ + +// Task: Display build information +tasks.register('info') { + group = 'help' + description = 'Display build configuration information' + + doLast { + println """ + ================================================================ + Bearsampp Module Perl - Build Info + ================================================================ + + Project: ${project.name} + Version: ${project.version} + Description: ${project.description} + + Bundle Properties: + Name: ${bundleName} + Release: ${bundleRelease} + Type: ${bundleType} + Format: ${bundleFormat} + + Paths: + Project Dir: ${projectBasedir} + Root Dir: ${rootDir} + Dev Path: ${devPath} + + Java: + Version: ${JavaVersion.current()} + Home: ${System.getProperty('java.home')} + + Gradle: + Version: ${gradle.gradleVersion} + Home: ${gradle.gradleHomeDir} + + Available Task Groups: + * build - Build and package tasks + * ant tasks - Legacy Ant tasks (prefixed with 'ant-') + * help - Help and information tasks + + Quick Start: + gradle tasks - List all available tasks + gradle info - Show this information + gradle release - Interactive release build + gradle release "-PbundleVersion=5.40.0" - Non-interactive release + gradle clean - Clean build artifacts + gradle verify - Verify build environment + """.stripIndent() + } +} + +// Task: Main release task - supports both interactive and non-interactive modes +tasks.register('release') { + group = 'build' + description = 'Build release package (interactive or use -PbundleVersion=X.X.X for non-interactive)' + + // Ensure libraries are loaded first + dependsOn 'ant-load.lib' + + doLast { + def versionToBuild = project.findProperty('bundleVersion') + + if (versionToBuild) { + // Non-interactive mode with specified version + println "=".multiply(70) + println "Building release for ${bundleName} version ${versionToBuild}..." + println "=".multiply(70) + + def bundlePath = file("${projectDir}/bin/${bundleName}${versionToBuild}") + + if (!bundlePath.exists()) { + def availableVersions = file("${projectDir}/bin").listFiles() + .findAll { it.isDirectory() && it.name.startsWith(bundleName) } + .collect { " - " + it.name.replace(bundleName, '') } + .join('\n') + + throw new GradleException("Bundle version not found: ${bundlePath}\n\nAvailable versions in bin/:\n${availableVersions}") + } + + println "Bundle path: ${bundlePath}" + println "" + + // Execute Ant command directly to avoid Gradle Ant integration issues + def antCommand = ["cmd", "/c", "ant", "release", "-Dinput.bundle=${versionToBuild}"] + println "Executing: ant release -Dinput.bundle=${versionToBuild}" + println "" + + def process = antCommand.execute(null, projectDir) + process.consumeProcessOutput(System.out, System.err) + def exitCode = process.waitFor() + + if (exitCode != 0) { + throw new GradleException("Ant release failed with exit code: ${exitCode}") + } + + println "" + println "=".multiply(70) + println "[SUCCESS] Release build completed successfully for version ${versionToBuild}" + println "=".multiply(70) + } else { + // Interactive mode - call Ant release target which will prompt for input + println "=".multiply(70) + println "Starting interactive release build..." + println "You will be prompted to enter the bundle version." + println "=".multiply(70) + println "" + + // Call the imported ant-release target for interactive mode + tasks.getByName('ant-release').actions.each { action -> + action.execute(tasks.getByName('ant-release')) + } + + println "" + println "=".multiply(70) + println "[SUCCESS] Release build completed" + println "=".multiply(70) + } + } +} + +// Task: Enhanced clean task +tasks.named('clean') { + group = 'build' + description = 'Clean build artifacts and temporary files' + + doLast { + // Clean Gradle build directory + def buildDir = file("${projectDir}/build") + if (buildDir.exists()) { + delete buildDir + } + + // Clean any temporary directories that might be created + // Use manual directory traversal to avoid fileTree default excludes issue + def tmpDirs = [] + projectDir.eachFileRecurse { file -> + if (file.isDirectory() && (file.name == 'tmp' || file.name == '.tmp')) { + tmpDirs.add(file) + } + } + tmpDirs.each { dir -> + if (dir.exists()) { + delete dir + } + } + + println "[SUCCESS] Build artifacts cleaned" + } +} + +// Task: Verify build environment +tasks.register('verify') { + group = 'verification' + description = 'Verify build environment and dependencies' + + doLast { + println "Verifying build environment for module-perl..." + + def checks = [:] + + // Check Java version + def javaVersion = JavaVersion.current() + checks['Java 8+'] = javaVersion >= JavaVersion.VERSION_1_8 + + // Check required files + checks['build.xml'] = file('build.xml').exists() + checks['build.properties'] = file('build.properties').exists() + checks['releases.properties'] = file('releases.properties').exists() + + // Check dev directory and required build files + checks['dev directory'] = file(devPath).exists() + checks['build-commons.xml'] = file("${devPath}/build/build-commons.xml").exists() + checks['build-bundle.xml'] = file("${devPath}/build/build-bundle.xml").exists() + + println "\nEnvironment Check Results:" + println "-".multiply(60) + checks.each { name, passed -> + def status = passed ? "[PASS]" : "[FAIL]" + println " ${status.padRight(10)} ${name}" + } + println "-".multiply(60) + + def allPassed = checks.values().every { it } + if (allPassed) { + println "\n[SUCCESS] All checks passed! Build environment is ready." + println "\nYou can now run:" + println " gradle release - Interactive release" + println " gradle release \"-PbundleVersion=5.40.0\" - Non-interactive release" + } else { + println "\n[WARNING] Some checks failed. Please review the requirements." + throw new GradleException("Build environment verification failed") + } + } +} + +// Task: List all bundle versions from releases.properties +tasks.register('listReleases') { + group = 'help' + description = 'List all available releases from releases.properties' + + doLast { + def releasesFile = file('releases.properties') + if (!releasesFile.exists()) { + println "releases.properties not found" + return + } + + def releases = new Properties() + releasesFile.withInputStream { releases.load(it) } + + println "\nAvailable Perl Releases:" + println "-".multiply(80) + releases.sort { it.key }.each { version, url -> + println " ${version.padRight(10)} -> ${url}" + } + println "-".multiply(80) + println "Total releases: ${releases.size()}" + } +} + +// Task: List available bundle versions in bin directory +tasks.register('listVersions') { + group = 'help' + description = 'List all available bundle versions in bin/ directory' + + doLast { + def binDir = file("${projectDir}/bin") + if (!binDir.exists()) { + println "bin/ directory not found" + return + } + + def versions = binDir.listFiles() + .findAll { it.isDirectory() && it.name.startsWith(bundleName) } + .collect { it.name.replace(bundleName, '') } + .sort() + + println "\nAvailable ${bundleName} versions in bin/:" + println "-".multiply(60) + versions.each { version -> + println " ${version}" + } + println "-".multiply(60) + println "Total versions: ${versions.size()}" + println "\nTo build a specific version:" + println " gradle release \"-PbundleVersion=${versions.last()}\"" + } +} + +// Task: Validate build.properties +tasks.register('validateProperties') { + group = 'verification' + description = 'Validate build.properties configuration' + + doLast { + println "Validating build.properties..." + + def required = ['bundle.name', 'bundle.release', 'bundle.type', 'bundle.format'] + def missing = [] + + required.each { prop -> + if (!buildProps.containsKey(prop) || buildProps.getProperty(prop).trim().isEmpty()) { + missing.add(prop) + } + } + + if (missing.isEmpty()) { + println "[SUCCESS] All required properties are present:" + required.each { prop -> + println " ${prop} = ${buildProps.getProperty(prop)}" + } + } else { + println "[ERROR] Missing required properties:" + missing.each { prop -> + println " - ${prop}" + } + throw new GradleException("build.properties validation failed") + } + } +} + +// ============================================================================ +// BUILD LIFECYCLE HOOKS +// ============================================================================ + +gradle.taskGraph.whenReady { graph -> + println """ + ================================================================ + Bearsampp Module Perl - Gradle + Ant Hybrid Build + ================================================================ + """.stripIndent() +} + +// ============================================================================ +// DEFAULT TASK +// ============================================================================ + +defaultTasks 'info' diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..28221e1 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,19 @@ +# Gradle Build Properties for Bearsampp Module Perl + +# Gradle daemon configuration +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.caching=true + +# JVM settings for Gradle +org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError + +# Configure console output +org.gradle.console=auto +org.gradle.warning.mode=all + +# Build performance +org.gradle.configureondemand=false + +# Gradle version compatibility +# This project is compatible with Gradle 7.0+ diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..68a95fe --- /dev/null +++ b/settings.gradle @@ -0,0 +1,25 @@ +/* + * Bearsampp Module Perl - Gradle Settings + */ + +rootProject.name = 'module-perl' + +// Enable Gradle features for better performance +enableFeaturePreview('STABLE_CONFIGURATION_CACHE') + +// Configure build cache for faster builds +buildCache { + local { + enabled = true + directory = file("${rootDir}/.gradle/build-cache") + } +} + +// Display initialization message +gradle.rootProject { + println """ + ================================================================ + Initializing Bearsampp Module Perl Build + ================================================================ + """.stripIndent() +} From cdca3e5d4d2720c3c61c178cfc858510d4cb4445 Mon Sep 17 00:00:00 2001 From: Bear Date: Fri, 14 Nov 2025 21:28:33 -0600 Subject: [PATCH 2/3] gradle build is complete --- README.md | 42 +++ build.gradle | 814 +++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 673 insertions(+), 183 deletions(-) diff --git a/README.md b/README.md index 76c2d7b..145b7f9 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,48 @@ This is a module of [Bearsampp project](https://github.com/bearsampp/bearsampp) https://bearsampp.com/module/perl +## Build (Gradle) + +The build system has been modernized to align with the Bruno module and now writes artifacts to the shared bearsampp-build directory instead of the local project build/ folder. +Additionally, Gradle's own temporary `build/` directory is relocated to the shared tmp area so nothing is written under the module folder during builds. + +Prerequisites: +- Java 8+ +- Gradle (wrapper or system Gradle) +- 7-Zip installed and available in PATH or via `7Z_HOME` when using `bundle.format=7z` + +Build properties are defined in `build.properties`: +- `bundle.name` (e.g., `perl`) +- `bundle.release` (e.g., `2025.4.26`) +- `bundle.type` (e.g., `tools`) +- `bundle.format` (`7z` or `zip`) +- Optional: `build.path` to override the default build root + +Build output location: +- Default root: `/../bearsampp-build` +- You can override with: + - `build.path` in `build.properties`, or + - Environment variable `BEARSAMPP_BUILD_PATH` + +Within the build root, artifacts are organized as: +`{buildRoot}/{bundle.type}/{bundle.name}/{bundle.release}/bearsampp-{name}-{version}-{release}.{7z|zip}` + +Gradle internal build directory (relocated): +`{buildRoot}/tmp/gradle/{bundle.type}/{bundle.name}` + +Common tasks: +- List tasks: `gradle tasks` +- Show build info: `gradle info` +- Verify environment: `gradle verify` +- List available versions from `bin/` and `bin/archived/`: `gradle listVersions` +- Build a specific version: `gradle release -PbundleVersion=5.40.0` +- Build all available versions in `bin*/`: `gradle releaseAll` +- Clean Gradle project artifacts: `gradle clean` + +Notes: +- Place source bundles under `bin/` as directories named like `perl5.40.0` (or in `bin/archived/`). The build will package these and produce checksums (MD5, SHA1, SHA256, SHA512). +- When `bundle.format` is `7z`, ensure 7-Zip is installed. You can set `7Z_HOME` to the folder containing `7z.exe`. + ## Issues Issues must be reported on [Bearsampp repository](https://github.com/bearsampp/bearsampp/issues). diff --git a/build.gradle b/build.gradle index cdf567d..1575a82 100644 --- a/build.gradle +++ b/build.gradle @@ -1,23 +1,31 @@ /* * Bearsampp Module Perl - Gradle Build * - * This is a hybrid build configuration that: - * 1. Imports existing Ant build files for backward compatibility - * 2. Provides modern Gradle features (caching, incremental builds, parallel execution) - * 3. Allows gradual migration from Ant to Gradle + * This is a 100% Gradle build configuration for the Perl module, + * aligned with the Bruno module build. It relocates build outputs to + * the shared bearsampp-build directory and provides modern tasks. * * Usage: * gradle tasks - List all available tasks - * gradle release - Interactive release (prompts for version) - * gradle release "-PbundleVersion=5.40.0" - Non-interactive release - * gradle clean - Clean build artifacts + * gradle release -PbundleVersion=5.40.0 - Build release for specific version + * gradle releaseAll - Build all versions in bin/ + * gradle clean - Clean Gradle project build dir * gradle info - Display build information + * gradle verify - Verify build environment + * gradle listVersions - List available versions */ +// Imports for better IDE/static analysis support +import org.gradle.api.GradleException + plugins { id 'base' } +// ============================================================================ +// PROJECT CONFIGURATION +// ============================================================================ + // Load build properties def buildProps = new Properties() file('build.properties').withInputStream { buildProps.load(it) } @@ -32,48 +40,259 @@ ext { projectBasedir = projectDir.absolutePath rootDir = projectDir.parent devPath = file("${rootDir}/dev").absolutePath - buildPropertiesFile = file('build.properties').absolutePath // Bundle properties from build.properties bundleName = buildProps.getProperty('bundle.name', 'perl') bundleRelease = buildProps.getProperty('bundle.release', '1.0.0') bundleType = buildProps.getProperty('bundle.type', 'tools') bundleFormat = buildProps.getProperty('bundle.format', '7z') + + // Build paths - with configurable base path + // Priority: 1) build.properties, 2) Environment variable, 3) Default + def buildPathFromProps = buildProps.getProperty('build.path', '').trim() + def buildPathFromEnv = System.getenv('BEARSAMPP_BUILD_PATH') ?: '' + def defaultBuildPath = "${rootDir}/bearsampp-build" + + buildBasePath = buildPathFromProps ?: (buildPathFromEnv ?: defaultBuildPath) + + // Use shared bearsampp-build/tmp directory structure + buildTmpPath = file("${buildBasePath}/tmp").absolutePath + bundleTmpBuildPath = file("${buildTmpPath}/bundles_build/${bundleType}/${bundleName}").absolutePath + bundleTmpPrepPath = file("${buildTmpPath}/bundles_prep/${bundleType}/${bundleName}").absolutePath + bundleTmpSrcPath = file("${buildTmpPath}/bundles_src").absolutePath + // New: downloads/extract paths (to mirror Bruno behavior) + bundleTmpDownloadPath = file("${buildTmpPath}/downloads/${bundleName}").absolutePath + bundleTmpExtractPath = file("${buildTmpPath}/extract/${bundleName}").absolutePath + + // Gradle's own build directory relocated to shared tmp (to mirror Bruno behavior) + gradleTmpBuildPath = file("${buildTmpPath}/gradle/${bundleType}/${bundleName}").absolutePath } +// Relocate Gradle's build/ directory to the shared bearsampp-build/tmp path +buildDir = file(ext.gradleTmpBuildPath) + // Verify dev path exists if (!file(ext.devPath).exists()) { throw new GradleException("Dev path not found: ${ext.devPath}. Please ensure the 'dev' project exists in ${ext.rootDir}") } -// Configure repositories for dependencies +// Configure repositories repositories { mavenCentral() } // ============================================================================ -// ANT INTEGRATION - Import existing Ant build files +// HELPER FUNCTIONS // ============================================================================ -// Set Ant properties before importing -ant.properties['project.basedir'] = ext.projectBasedir -ant.properties['root.dir'] = ext.rootDir -ant.properties['dev.path'] = ext.devPath -ant.properties['build.properties'] = ext.buildPropertiesFile - -// Load build.properties into Ant -ant.property(file: ext.buildPropertiesFile) - -// Import the main Ant build file -// This preserves all existing Ant functionality -ant.importBuild('build.xml') { antTargetName -> - // Map Ant target names to Gradle task names - // Prefix all with 'ant-' to avoid conflicts - return "ant-${antTargetName}".toString() +// Helper function to find Perl directory (must contain perl.exe) +def findPerlDirectory(File path) { + if (!path?.exists()) return null + def hasPerlExe = { File dir -> new File(dir, 'perl.exe').exists() || new File(dir, 'Perl.exe').exists() } + if (hasPerlExe(path)) return path + File found = null + def stack = new ArrayDeque() + path.listFiles()?.findAll { it.isDirectory() }?.each { stack.push(it) } + while (!stack.isEmpty() && found == null) { + def dir = stack.pop() + if (hasPerlExe(dir)) { found = dir; break } + dir.listFiles()?.findAll { it.isDirectory() }?.each { stack.push(it) } + } + return found +} + +// Helper function to find 7-Zip executable +def find7ZipExecutable() { + def sevenZipHome = System.getenv('7Z_HOME') + if (sevenZipHome) { + def exe = file("${sevenZipHome}/7z.exe") + if (exe.exists()) return exe.absolutePath + } + def commonPaths = [ + 'C:/Program Files/7-Zip/7z.exe', + 'C:/Program Files (x86)/7-Zip/7z.exe', + 'D:/Program Files/7-Zip/7z.exe', + 'D:/Program Files (x86)/7-Zip/7z.exe' + ] + for (path in commonPaths) { def exe = file(path); if (exe.exists()) return exe.absolutePath } + try { + def process = ['where', '7z.exe'].execute() + process.waitFor() + if (process.exitValue() == 0) { def out = process.text.trim(); if (out) return out.split('\n')[0].trim() } + } catch (Exception ignored) {} + return null +} + +// Fetch perl.properties from modules-untouched repository +def fetchModulesUntouchedProperties() { + def propsUrl = "https://raw.githubusercontent.com/Bearsampp/modules-untouched/main/modules/perl.properties" + + println "Fetching perl.properties from modules-untouched repository..." + println " URL: ${propsUrl}" + + def tempFile = file("${bundleTmpDownloadPath}/perl-untouched.properties") + tempFile.parentFile.mkdirs() + + try { + new URL(propsUrl).withInputStream { input -> + tempFile.withOutputStream { output -> + output << input + } + } + def props = new Properties() + tempFile.withInputStream { props.load(it) } + println " ✓ Successfully loaded ${props.size()} versions from modules-untouched" + return props + } catch (Exception e) { + println " ✗ Warning: Could not fetch perl.properties from modules-untouched: ${e.message}" + println " Will fall back to standard URL format if needed" + return null + } +} + +// Download from modules-untouched (with fallback URL pattern) +def downloadFromModulesUntouched(String version) { + println "Checking modules-untouched repository..." + + def untouchedProps = fetchModulesUntouchedProperties() + def untouchedUrl = null + + if (untouchedProps) { + untouchedUrl = untouchedProps.getProperty(version) + if (untouchedUrl) { + println "Found version ${version} in modules-untouched perl.properties" + println "Downloading from:\n ${untouchedUrl}" + } else { + println "Version ${version} not found in modules-untouched perl.properties" + println "Attempting to construct URL based on standard format..." + untouchedUrl = "https://github.com/Bearsampp/modules-untouched/releases/download/perl-${version}/perl-${version}-win64.7z" + println " ${untouchedUrl}" + } + } else { + println "Could not fetch perl.properties, using standard URL format..." + untouchedUrl = "https://github.com/Bearsampp/modules-untouched/releases/download/perl-${version}/perl-${version}-win64.7z" + println " ${untouchedUrl}" + } + + def filename = untouchedUrl.substring(untouchedUrl.lastIndexOf('/') + 1) + def downloadDir = file(bundleTmpDownloadPath) + downloadDir.mkdirs() + def downloadedFile = file("${downloadDir}/${filename}") + + if (!downloadedFile.exists()) { + println " Downloading to: ${downloadedFile}" + try { + new URL(untouchedUrl).withInputStream { input -> + downloadedFile.withOutputStream { output -> + byte[] buf = new byte[8192] + int read + while ((read = input.read(buf)) != -1) { + output.write(buf, 0, read) + } + } + } + println " Download complete from modules-untouched" + } catch (Exception e) { + throw new GradleException(( + """ + Failed to download from modules-untouched: ${e.message} + + Tried URL: ${untouchedUrl} + + Please verify: + 1. Version ${version} exists in modules-untouched repository + 2. The URL is correct in perl.properties or matches format: perl-{version}/perl-{version}-win64.7z + 3. You have internet connectivity + """ + ).stripIndent()) + } + } else { + println " Using cached file: ${downloadedFile}" + } + + return downloadedFile +} + +// Download and extract Perl binaries, return directory that contains perl.exe +def downloadAndExtractPerl(String version) { + def downloadedFile = downloadFromModulesUntouched(version) + + def extractBase = file(bundleTmpExtractPath) + extractBase.mkdirs() + println " Extracting archive..." + def extractPath = file("${extractBase}/${version}") + if (extractPath.exists()) { delete extractPath } + extractPath.mkdirs() + + def filename = downloadedFile.name + if (filename.endsWith('.7z')) { + def sevenZipPath = find7ZipExecutable() + if (sevenZipPath) { + def command = [ sevenZipPath.toString(), 'x', downloadedFile.absolutePath.toString(), "-o${extractPath.absolutePath}".toString(), '-y' ] + def process = new ProcessBuilder(command as String[]) + .directory(extractPath) + .redirectErrorStream(true) + .start() + process.inputStream.eachLine { line -> if (line.trim()) println " ${line}" } + def exitCode = process.waitFor() + if (exitCode != 0) throw new GradleException("7zip extraction failed with exit code: ${exitCode}") + } else { + throw new GradleException("7zip not found. Please install 7zip or extract manually.") + } + } else if (filename.endsWith('.zip')) { + copy { from zipTree(downloadedFile); into extractPath } + } else { + throw new GradleException("Unsupported archive format: ${filename}") + } + + println " Extraction complete" + def perlDir = findPerlDirectory(extractPath) + if (!perlDir) throw new GradleException("Could not find perl.exe in extracted files") + println " Found Perl directory: ${perlDir.name}" + println "\nNOTE: Version ${version} was sourced from modules-untouched (or fallback URL)." + return perlDir +} + +// Helper function to generate hash files +def generateHashFiles(File file) { + if (!file.exists()) throw new GradleException("File not found for hashing: ${file}") + def writeHash = { String algo, String ext -> + def digest = java.security.MessageDigest.getInstance(algo) + file.withInputStream { is -> + byte[] buf = new byte[8192]; int read + while ((read = is.read(buf)) != -1) { digest.update(buf, 0, read) } + } + def hash = digest.digest().collect { String.format('%02x', it) }.join('') + def outFile = new File("${file.absolutePath}.${ext}") + outFile.text = "${hash} ${file.name}\n" + println " Created: ${outFile.name}" + } + writeHash('MD5', 'md5') + writeHash('SHA-1', 'sha1') + writeHash('SHA-256', 'sha256') + writeHash('SHA-512', 'sha512') +} + +// Helper to enumerate available versions (bin and bin/archived) +def getAvailableVersions() { + def versions = [] + def binDir = file("${projectDir}/bin") + if (binDir.exists()) { + versions.addAll(binDir.listFiles() + ?.findAll { it.isDirectory() && it.name.startsWith(bundleName) && it.name != 'archived' } + ?.collect { it.name.replace(bundleName, '') } ?: []) + } + def archivedDir = file("${projectDir}/bin/archived") + if (archivedDir.exists()) { + versions.addAll(archivedDir.listFiles() + ?.findAll { it.isDirectory() && it.name.startsWith(bundleName) } + ?.collect { it.name.replace(bundleName, '') } ?: []) + } + return versions.unique().sort() } // ============================================================================ -// GRADLE NATIVE TASKS - Modern alternatives and enhancements +// GRADLE TASKS // ============================================================================ // Task: Display build information @@ -81,146 +300,356 @@ tasks.register('info') { group = 'help' description = 'Display build configuration information' + // Capture at configuration time + def projectName = project.name + def projectVersion = project.version + def projectDescription = project.description + def projectBasedirValue = projectBasedir + def rootDirValue = rootDir + def devPathValue = devPath + def bundleNameValue = bundleName + def bundleReleaseValue = bundleRelease + def bundleTypeValue = bundleType + def bundleFormatValue = bundleFormat + def buildBasePathValue = buildBasePath + def buildTmpPathValue = buildTmpPath + def bundleTmpPrepPathValue = bundleTmpPrepPath + def bundleTmpBuildPathValue = bundleTmpBuildPath + def bundleTmpSrcPathValue = bundleTmpSrcPath + def javaVersion = JavaVersion.current() + def javaHome = System.getProperty('java.home') + def gradleVersion = gradle.gradleVersion + def gradleHome = gradle.gradleHomeDir + doLast { println """ ================================================================ Bearsampp Module Perl - Build Info ================================================================ - Project: ${project.name} - Version: ${project.version} - Description: ${project.description} + Project: ${projectName} + Version: ${projectVersion} + Description: ${projectDescription} Bundle Properties: - Name: ${bundleName} - Release: ${bundleRelease} - Type: ${bundleType} - Format: ${bundleFormat} + Name: ${bundleNameValue} + Release: ${bundleReleaseValue} + Type: ${bundleTypeValue} + Format: ${bundleFormatValue} Paths: - Project Dir: ${projectBasedir} - Root Dir: ${rootDir} - Dev Path: ${devPath} + Project Dir: ${projectBasedirValue} + Root Dir: ${rootDirValue} + Dev Path: ${devPathValue} + Build Base: ${buildBasePathValue} + Build Tmp: ${buildTmpPathValue} + Tmp Prep: ${bundleTmpPrepPathValue} + Tmp Build: ${bundleTmpBuildPathValue} + Tmp Src: ${bundleTmpSrcPathValue} + Gradle Build: ${buildDir} Java: - Version: ${JavaVersion.current()} - Home: ${System.getProperty('java.home')} + Version: ${javaVersion} + Home: ${javaHome} Gradle: - Version: ${gradle.gradleVersion} - Home: ${gradle.gradleHomeDir} + Version: ${gradleVersion} + Home: ${gradleHome} Available Task Groups: * build - Build and package tasks - * ant tasks - Legacy Ant tasks (prefixed with 'ant-') * help - Help and information tasks Quick Start: gradle tasks - List all available tasks gradle info - Show this information - gradle release - Interactive release build - gradle release "-PbundleVersion=5.40.0" - Non-interactive release + gradle release -PbundleVersion=5.40.0 - Build specific version + gradle releaseAll - Build all versions gradle clean - Clean build artifacts gradle verify - Verify build environment """.stripIndent() } } -// Task: Main release task - supports both interactive and non-interactive modes +// Task: Main release task - build a specific version tasks.register('release') { group = 'build' - description = 'Build release package (interactive or use -PbundleVersion=X.X.X for non-interactive)' + description = 'Build release package for a specific version (use -PbundleVersion=X.X.X or run interactively)' - // Ensure libraries are loaded first - dependsOn 'ant-load.lib' + def versionProperty = project.findProperty('bundleVersion') doLast { - def versionToBuild = project.findProperty('bundleVersion') + def versionToBuild = versionProperty + + if (!versionToBuild) { + def availableVersions = getAvailableVersions() + if (availableVersions.isEmpty()) { + throw new GradleException("No versions found in bin/ directory") + } - if (versionToBuild) { - // Non-interactive mode with specified version + println "" println "=".multiply(70) - println "Building release for ${bundleName} version ${versionToBuild}..." + println "Interactive Release Mode" println "=".multiply(70) + println "" + println "Available versions:" - def bundlePath = file("${projectDir}/bin/${bundleName}${versionToBuild}") + def binDir = file("${projectDir}/bin") + def archivedDir = file("${projectDir}/bin/archived") - if (!bundlePath.exists()) { - def availableVersions = file("${projectDir}/bin").listFiles() - .findAll { it.isDirectory() && it.name.startsWith(bundleName) } - .collect { " - " + it.name.replace(bundleName, '') } - .join('\n') + availableVersions.eachWithIndex { v, idx -> + def location = "" + if (binDir.exists() && file("${binDir}/${bundleName}${v}").exists()) { location = "[bin]" } + else if (archivedDir.exists() && file("${archivedDir}/${bundleName}${v}").exists()) { location = "[bin/archived]" } + println " ${(idx + 1).toString().padLeft(2)}. ${v.padRight(15)} ${location}" + } + println "" + println "Enter version to build (index or version string):" + println "" - throw new GradleException("Bundle version not found: ${bundlePath}\n\nAvailable versions in bin/:\n${availableVersions}") + def input = null + try { + def reader = new BufferedReader(new InputStreamReader(System.in)) + input = reader.readLine() + } catch (Exception e) { + throw new GradleException(""" + Failed to read input. Please use non-interactive mode: + gradle release -PbundleVersion=X.X.X + + Available versions: ${availableVersions.join(', ')} + """.stripIndent()) } + if (!input || input.trim().isEmpty()) { + throw new GradleException(""" + No version selected. Please use non-interactive mode: + gradle release -PbundleVersion=X.X.X - println "Bundle path: ${bundlePath}" + Available versions: ${availableVersions.join(', ')} + """.stripIndent()) + } + def cleaned = input.trim() + if (cleaned.isInteger()) { + def idx = cleaned.toInteger() + if (idx < 1 || idx > availableVersions.size()) { + throw new GradleException(""" + Invalid selection index: ${cleaned} + + Please choose a number between 1 and ${availableVersions.size()} or enter a version string. + """.stripIndent()) + } + versionToBuild = availableVersions[idx - 1] + } else { + versionToBuild = cleaned + if (!availableVersions.contains(versionToBuild)) { + throw new GradleException(""" + Invalid version: ${versionToBuild} + + Please choose from available versions: + ${availableVersions.collect { " - ${it}" }.join('\n')} + """.stripIndent()) + } + } println "" + println "Selected version: ${versionToBuild}" + } - // Execute Ant command directly to avoid Gradle Ant integration issues - def antCommand = ["cmd", "/c", "ant", "release", "-Dinput.bundle=${versionToBuild}"] - println "Executing: ant release -Dinput.bundle=${versionToBuild}" - println "" + println "" + println "=".multiply(70) + println "Building ${bundleName} ${versionToBuild}" + println "=".multiply(70) + println "" - def process = antCommand.execute(null, projectDir) - process.consumeProcessOutput(System.out, System.err) - def exitCode = process.waitFor() + // Validate version exists (bin or archived) + def bundlePath = file("${projectDir}/bin/${bundleName}${versionToBuild}") + if (!bundlePath.exists()) { + bundlePath = file("${projectDir}/bin/archived/${bundleName}${versionToBuild}") + if (!bundlePath.exists()) { + throw new GradleException("Bundle version not found in bin/ or bin/archived/\n\nAvailable versions:\n${getAvailableVersions().collect { " - ${it}" }.join('\n')}") + } + } - if (exitCode != 0) { - throw new GradleException("Ant release failed with exit code: ${exitCode}") + println "Bundle path: ${bundlePath}" + println "" + + // Determine source paths + def bundleSrcDest = bundlePath + def bundleSrcFinal = bundleSrcDest + + // Verify perl.exe exists; if not, try to use cached extract or download from modules-untouched + def perlExe = file("${bundleSrcFinal}/perl.exe") + if (!perlExe.exists()) { + def nested = findPerlDirectory(bundleSrcFinal) + if (nested) { + bundleSrcFinal = nested + } else { + // Try cached extract first + def cachedExtract = file("${bundleTmpExtractPath}/${versionToBuild}") + def cachedDir = findPerlDirectory(cachedExtract) + if (cachedDir && cachedDir.exists()) { + println "Using cached Perl binaries from bearsampp-build/tmp" + bundleSrcFinal = cachedDir + } else { + println "" + println "Perl binaries not found" + println "Downloading Perl ${versionToBuild}..." + println "" + try { + bundleSrcFinal = downloadAndExtractPerl(versionToBuild) + } catch (Exception e) { + throw new GradleException(( + """ + Failed to download Perl binaries: ${e.message} + + You can manually download and extract Perl binaries to: + ${bundleSrcDest}/ + + Or check that version ${versionToBuild} exists in modules-untouched perl.properties + """ + ).stripIndent()) + } + } } + } - println "" - println "=".multiply(70) - println "[SUCCESS] Release build completed successfully for version ${versionToBuild}" - println "=".multiply(70) - } else { - // Interactive mode - call Ant release target which will prompt for input - println "=".multiply(70) - println "Starting interactive release build..." - println "You will be prompted to enter the bundle version." - println "=".multiply(70) - println "" + perlExe = file("${bundleSrcFinal}/perl.exe") + if (!perlExe.exists()) { throw new GradleException("perl.exe not found at ${perlExe}") } - // Call the imported ant-release target for interactive mode - tasks.getByName('ant-release').actions.each { action -> - action.execute(tasks.getByName('ant-release')) - } + println "Source folder: ${bundleSrcFinal}" + println "" - println "" - println "=".multiply(70) - println "[SUCCESS] Release build completed" - println "=".multiply(70) + // Prepare output directory + def prepPath = file("${bundleTmpPrepPath}/${bundleName}${versionToBuild}") + if (prepPath.exists()) { delete prepPath } + prepPath.mkdirs() + + // Copy Perl files from source + println "Copying ${bundleName} files..." + copy { from bundleSrcFinal; into prepPath } + + // Overlay files from bin bundle directory (if different) + if (bundleSrcDest.absolutePath != bundleSrcFinal.absolutePath) { + println "Overlaying bundle files from bin directory..." + copy { from bundleSrcDest; into prepPath } + } + + // Copy to bundles_build directory (non-archive version for dev) + println "" + println "Copying to bundles_build directory..." + def buildPathNonZip = file("${bundleTmpBuildPath}/${bundleName}${versionToBuild}") + if (buildPathNonZip.exists()) { delete buildPathNonZip } + buildPathNonZip.mkdirs() + copy { from prepPath; into buildPathNonZip } + println "Non-zip version available at: ${buildPathNonZip}" + + println "" + println "Preparing archive..." + + // bearsampp-build/{bundleType}/{bundleName}/{bundleRelease} + def buildPath = file(buildBasePath) + def buildBinsPath = file("${buildPath}/${bundleType}/${bundleName}/${bundleRelease}") + buildBinsPath.mkdirs() + + def destFile = file("${buildBinsPath}/bearsampp-${bundleName}-${versionToBuild}-${bundleRelease}") + + if (bundleFormat == '7z') { + def archiveFile = file("${destFile}.7z") + if (archiveFile.exists()) delete archiveFile + + def sevenZipExe = find7ZipExecutable() + if (!sevenZipExe) throw new GradleException("7-Zip not found. Please install 7-Zip or set 7Z_HOME environment variable.") + println "Using 7-Zip: ${sevenZipExe}" + println "Compressing ${bundleName}${versionToBuild} to ${archiveFile.name}..." + + def command = [ sevenZipExe, 'a', '-t7z', archiveFile.absolutePath.toString(), '.' ] + def process = new ProcessBuilder(command as String[]) + .directory(prepPath) + .redirectErrorStream(true) + .start() + process.inputStream.eachLine { line -> if (line.trim()) println " ${line}" } + def exitCode = process.waitFor() + if (exitCode != 0) throw new GradleException("7zip compression failed with exit code: ${exitCode}") + + println "Archive created: ${archiveFile}" + println "Generating hash files..." + generateHashFiles(archiveFile) + } else { + def archiveFile = file("${destFile}.zip") + if (archiveFile.exists()) delete archiveFile + println "Compressing ${bundleName}${versionToBuild} to ${archiveFile.name}..." + task("zipArchive_${versionToBuild}", type: Zip) { + from prepPath + destinationDirectory = archiveFile.parentFile + archiveFileName = archiveFile.name + }.execute() + println "Archive created: ${archiveFile}" + println "Generating hash files..." + generateHashFiles(archiveFile) } + + println "" + println "=".multiply(70) + println "[SUCCESS] Release build completed successfully for version ${versionToBuild}" + println "Output directory: ${buildPathNonZip}" + println "Archive: ${destFile}.${bundleFormat}" + println "=".multiply(70) } } -// Task: Enhanced clean task -tasks.named('clean') { +// Task: Build all available versions +tasks.register('releaseAll') { group = 'build' - description = 'Clean build artifacts and temporary files' + description = 'Build release packages for all available versions in bin/ directory' doLast { - // Clean Gradle build directory - def buildDir = file("${projectDir}/build") - if (buildDir.exists()) { - delete buildDir - } - - // Clean any temporary directories that might be created - // Use manual directory traversal to avoid fileTree default excludes issue - def tmpDirs = [] - projectDir.eachFileRecurse { file -> - if (file.isDirectory() && (file.name == 'tmp' || file.name == '.tmp')) { - tmpDirs.add(file) + def versions = getAvailableVersions() + if (versions.isEmpty()) throw new GradleException('No versions found in bin/ or bin/archived/') + + println "" + println "=".multiply(70) + println "Building releases for ${versions.size()} ${bundleName} versions" + println "=".multiply(70) + println "" + + def success = 0 + def failed = [] + versions.eachWithIndex { v, idx -> + println "=".multiply(70) + println "[${idx + 1}/${versions.size()}] Building ${bundleName} ${v}..." + println "=".multiply(70) + try { + gradle.startParameter.projectProperties.put('bundleVersion', v) + tasks.getByName('release').actions.each { action -> action.execute(tasks.getByName('release')) } + success++ + } catch (Exception e) { + println "[FAILED] ${bundleName} ${v}: ${e.message}" + failed.add(v) } + println "" } - tmpDirs.each { dir -> - if (dir.exists()) { - delete dir - } + + println "=".multiply(70) + println "Build Summary" + println "=".multiply(70) + println "Total versions: ${versions.size()}" + println "Successful: ${success}" + println "Failed: ${failed.size()}" + if (!failed.isEmpty()) { + println "\nFailed versions:" + failed.each { println " - ${it}" } + throw new GradleException("${failed.size()} version(s) failed to build") + } else { + println "[SUCCESS] All versions built successfully!" } + } +} +// Task: Enhanced clean task (Gradle project build directory only) +tasks.named('clean') { + group = 'build' + description = 'Clean build artifacts (Gradle project build dir)' + doLast { + def relocatedBuildDir = file(buildDir) + if (relocatedBuildDir.exists()) delete relocatedBuildDir println "[SUCCESS] Build artifacts cleaned" } } @@ -231,96 +660,128 @@ tasks.register('verify') { description = 'Verify build environment and dependencies' doLast { - println "Verifying build environment for module-perl..." - + println 'Verifying build environment for module-perl...' def checks = [:] - - // Check Java version def javaVersion = JavaVersion.current() checks['Java 8+'] = javaVersion >= JavaVersion.VERSION_1_8 - - // Check required files - checks['build.xml'] = file('build.xml').exists() checks['build.properties'] = file('build.properties').exists() - checks['releases.properties'] = file('releases.properties').exists() - - // Check dev directory and required build files - checks['dev directory'] = file(devPath).exists() - checks['build-commons.xml'] = file("${devPath}/build/build-commons.xml").exists() - checks['build-bundle.xml'] = file("${devPath}/build/build-bundle.xml").exists() + checks['bin directory'] = file("${projectDir}/bin").exists() + if (bundleFormat == '7z') { checks['7-Zip'] = find7ZipExecutable() != null } println "\nEnvironment Check Results:" println "-".multiply(60) checks.each { name, passed -> - def status = passed ? "[PASS]" : "[FAIL]" + def status = passed ? '[PASS]' : '[FAIL]' println " ${status.padRight(10)} ${name}" } println "-".multiply(60) def allPassed = checks.values().every { it } - if (allPassed) { - println "\n[SUCCESS] All checks passed! Build environment is ready." - println "\nYou can now run:" - println " gradle release - Interactive release" - println " gradle release \"-PbundleVersion=5.40.0\" - Non-interactive release" - } else { - println "\n[WARNING] Some checks failed. Please review the requirements." - throw new GradleException("Build environment verification failed") + if (!allPassed) throw new GradleException('Build environment verification failed') + println '\n[SUCCESS] All checks passed! Build environment is ready.' + println "\nYou can now run:\n gradle release -PbundleVersion=5.40.0 - Build release for version\n gradle listVersions - List available versions" + } +} + +// Task: List available bundle versions in bin and bin/archived +tasks.register('listVersions') { + group = 'help' + description = 'List all available bundle versions in bin/ and bin/archived/ directories' + + doLast { + def versions = getAvailableVersions() + if (versions.isEmpty()) { println "\nNo versions found in bin/ or bin/archived/ directories"; return } + println "\nAvailable ${bundleName} versions:" + println "-".multiply(60) + def binDir = file("${projectDir}/bin") + def archivedDir = file("${projectDir}/bin/archived") + versions.each { v -> + def location = '' + if (binDir.exists() && file("${binDir}/${bundleName}${v}").exists()) location = '[bin]' + else if (archivedDir.exists() && file("${archivedDir}/${bundleName}${v}").exists()) location = '[bin/archived]' + println " ${v.padRight(15)} ${location}" + } + println "-".multiply(60) + println "Total versions: ${versions.size()}" + if (!versions.isEmpty()) { + println "\nTo build a specific version:" + println " gradle release -PbundleVersion=${versions.last()}" } } } -// Task: List all bundle versions from releases.properties +// Task: List all bundle versions from modules-untouched perl.properties tasks.register('listReleases') { group = 'help' - description = 'List all available releases from releases.properties' + description = 'List all available releases from modules-untouched perl.properties' doLast { - def releasesFile = file('releases.properties') - if (!releasesFile.exists()) { - println "releases.properties not found" + def props = fetchModulesUntouchedProperties() + if (!props) { + println "\n[WARNING] Could not fetch modules-untouched perl.properties.\nNo release information available." return } - - def releases = new Properties() - releasesFile.withInputStream { releases.load(it) } - - println "\nAvailable Perl Releases:" + println "\nAvailable Perl Releases (modules-untouched):" println "-".multiply(80) - releases.sort { it.key }.each { version, url -> + props.sort { a, b -> a.key <=> b.key }.each { version, url -> println " ${version.padRight(10)} -> ${url}" } println "-".multiply(80) - println "Total releases: ${releases.size()}" + println "Total releases: ${props.size()}" } } -// Task: List available bundle versions in bin directory -tasks.register('listVersions') { - group = 'help' - description = 'List all available bundle versions in bin/ directory' +// Task: Check modules-untouched integration +tasks.register('checkModulesUntouched') { + group = 'verification' + description = 'Check modules-untouched repository integration and available versions' doLast { - def binDir = file("${projectDir}/bin") - if (!binDir.exists()) { - println "bin/ directory not found" - return - } + println "" + println "=".multiply(70) + println "Modules-Untouched Integration Check" + println "=".multiply(70) + println "" - def versions = binDir.listFiles() - .findAll { it.isDirectory() && it.name.startsWith(bundleName) } - .collect { it.name.replace(bundleName, '') } - .sort() + def propsUrl = "https://raw.githubusercontent.com/Bearsampp/modules-untouched/main/modules/perl.properties" + println "Repository URL:\n ${propsUrl}\n" - println "\nAvailable ${bundleName} versions in bin/:" - println "-".multiply(60) - versions.each { version -> - println " ${version}" + println "Fetching perl.properties from modules-untouched..." + def untouchedProps = fetchModulesUntouchedProperties() + + if (untouchedProps) { + println "" + println "=".multiply(70) + println "Available Versions in modules-untouched" + println "=".multiply(70) + + def sorted = untouchedProps.sort { a, b -> + def ap = a.key.tokenize('.') + def bp = b.key.tokenize('.') + for (int i = 0; i < Math.min(ap.size(), bp.size()); i++) { + def an = ap[i].toInteger() + def bn = bp[i].toInteger() + if (an != bn) return an <=> bn + } + return ap.size() <=> bp.size() + } + sorted.each { v, url -> println " ${v.padRight(10)}" } + println "=".multiply(70) + println "Total versions: ${untouchedProps.size()}" + println "" + println "=".multiply(70) + println "[SUCCESS] modules-untouched integration is working" + println "=".multiply(70) + println "" + println "Version Resolution Strategy:\n 1. Check modules-untouched perl.properties (remote)\n 2. Construct standard URL format (fallback)" + } else { + println "" + println "=".multiply(70) + println "[WARNING] Could not fetch perl.properties from modules-untouched" + println "=".multiply(70) + println "" + println "The build system will fall back to:\n 1. Standard URL format construction" } - println "-".multiply(60) - println "Total versions: ${versions.size()}" - println "\nTo build a specific version:" - println " gradle release \"-PbundleVersion=${versions.last()}\"" } } @@ -328,31 +789,18 @@ tasks.register('listVersions') { tasks.register('validateProperties') { group = 'verification' description = 'Validate build.properties configuration' - doLast { - println "Validating build.properties..." - + println 'Validating build.properties...' def required = ['bundle.name', 'bundle.release', 'bundle.type', 'bundle.format'] def missing = [] - - required.each { prop -> - if (!buildProps.containsKey(prop) || buildProps.getProperty(prop).trim().isEmpty()) { - missing.add(prop) - } - } - - if (missing.isEmpty()) { - println "[SUCCESS] All required properties are present:" - required.each { prop -> - println " ${prop} = ${buildProps.getProperty(prop)}" - } - } else { - println "[ERROR] Missing required properties:" - missing.each { prop -> - println " - ${prop}" - } - throw new GradleException("build.properties validation failed") + required.each { p -> if (!buildProps.containsKey(p) || buildProps.getProperty(p).trim().isEmpty()) missing.add(p) } + if (!missing.isEmpty()) { + println '[ERROR] Missing required properties:' + missing.each { println " - ${it}" } + throw new GradleException('build.properties validation failed') } + println '[SUCCESS] All required properties are present:' + required.each { println " ${it} = ${buildProps.getProperty(it)}" } } } @@ -363,7 +811,7 @@ tasks.register('validateProperties') { gradle.taskGraph.whenReady { graph -> println """ ================================================================ - Bearsampp Module Perl - Gradle + Ant Hybrid Build + Bearsampp Module Perl - Gradle Build ================================================================ """.stripIndent() } From 363514eabf5b7544954dd500818a2914ccffe50b Mon Sep 17 00:00:00 2001 From: Bear Date: Sun, 16 Nov 2025 02:30:54 -0600 Subject: [PATCH 3/3] fixes version folder not being in build --- .gitignore | 1 - README.md | 11 ++++++-- build.gradle | 71 +++++++++++++++++++++++++++++----------------------- build.xml | 35 -------------------------- 4 files changed, 48 insertions(+), 70 deletions(-) delete mode 100644 build.xml diff --git a/.gitignore b/.gitignore index 913ec11..ed26188 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,5 @@ # Gradle .gradle/ build/ -!gradle-wrapper.jar *.gradle.kts.compiled .gradle-cache/ diff --git a/README.md b/README.md index 145b7f9..de8add2 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,12 @@ https://bearsampp.com/module/perl The build system has been modernized to align with the Bruno module and now writes artifacts to the shared bearsampp-build directory instead of the local project build/ folder. Additionally, Gradle's own temporary `build/` directory is relocated to the shared tmp area so nothing is written under the module folder during builds. +Ant build deprecated: +- The former Ant build is no longer supported for this module. The `build.xml` file remains only as a stub that instructs users to run Gradle tasks. + Prerequisites: - Java 8+ -- Gradle (wrapper or system Gradle) +- Gradle (system Gradle only; do not use Gradle wrapper in this repository) - 7-Zip installed and available in PATH or via `7Z_HOME` when using `bundle.format=7z` Build properties are defined in `build.properties`: @@ -35,10 +38,13 @@ Build output location: Within the build root, artifacts are organized as: `{buildRoot}/{bundle.type}/{bundle.name}/{bundle.release}/bearsampp-{name}-{version}-{release}.{7z|zip}` +Archive layout: +- The archive contains a single top-level folder named `{name}{version}` (e.g., `perl5.40.0`) that includes all files. This mirrors the Bruno module behavior. + Gradle internal build directory (relocated): `{buildRoot}/tmp/gradle/{bundle.type}/{bundle.name}` -Common tasks: +Common tasks (use system Gradle `gradle` command): - List tasks: `gradle tasks` - Show build info: `gradle info` - Verify environment: `gradle verify` @@ -50,6 +56,7 @@ Common tasks: Notes: - Place source bundles under `bin/` as directories named like `perl5.40.0` (or in `bin/archived/`). The build will package these and produce checksums (MD5, SHA1, SHA256, SHA512). - When `bundle.format` is `7z`, ensure 7-Zip is installed. You can set `7Z_HOME` to the folder containing `7z.exe`. + - The produced 7z/zip preserves the version folder as the archive root (e.g., `perl5.40.0/…`). ## Issues diff --git a/build.gradle b/build.gradle index 1575a82..49323de 100644 --- a/build.gradle +++ b/build.gradle @@ -15,8 +15,7 @@ * gradle listVersions - List available versions */ -// Imports for better IDE/static analysis support -import org.gradle.api.GradleException +// Note: Avoid importing GradleException for editor analyzers; use RuntimeException instead plugins { id 'base' @@ -71,9 +70,9 @@ ext { // Relocate Gradle's build/ directory to the shared bearsampp-build/tmp path buildDir = file(ext.gradleTmpBuildPath) -// Verify dev path exists +// Dev path (legacy Ant build) is optional in pure Gradle build if (!file(ext.devPath).exists()) { - throw new GradleException("Dev path not found: ${ext.devPath}. Please ensure the 'dev' project exists in ${ext.rootDir}") + logger.lifecycle("[INFO] Optional 'dev' project not found at ${ext.devPath}. Proceeding with pure Gradle build (no Ant).") } // Configure repositories @@ -101,6 +100,8 @@ def findPerlDirectory(File path) { return found } +// (No Gradle wrapper task by project policy; use a system Gradle installation) + // Helper function to find 7-Zip executable def find7ZipExecutable() { def sevenZipHome = System.getenv('7Z_HOME') @@ -141,7 +142,8 @@ def fetchModulesUntouchedProperties() { } def props = new Properties() tempFile.withInputStream { props.load(it) } - println " ✓ Successfully loaded ${props.size()} versions from modules-untouched" + // Use ASCII-only to avoid garbled characters on some Windows code pages + println " [OK] Successfully loaded ${props.size()} versions from modules-untouched" return props } catch (Exception e) { println " ✗ Warning: Could not fetch perl.properties from modules-untouched: ${e.message}" @@ -193,7 +195,7 @@ def downloadFromModulesUntouched(String version) { } println " Download complete from modules-untouched" } catch (Exception e) { - throw new GradleException(( + throw new RuntimeException(( """ Failed to download from modules-untouched: ${e.message} @@ -235,19 +237,19 @@ def downloadAndExtractPerl(String version) { .start() process.inputStream.eachLine { line -> if (line.trim()) println " ${line}" } def exitCode = process.waitFor() - if (exitCode != 0) throw new GradleException("7zip extraction failed with exit code: ${exitCode}") + if (exitCode != 0) throw new RuntimeException("7zip extraction failed with exit code: ${exitCode}") } else { - throw new GradleException("7zip not found. Please install 7zip or extract manually.") + throw new RuntimeException("7zip not found. Please install 7zip or extract manually.") } } else if (filename.endsWith('.zip')) { copy { from zipTree(downloadedFile); into extractPath } } else { - throw new GradleException("Unsupported archive format: ${filename}") + throw new RuntimeException("Unsupported archive format: ${filename}") } println " Extraction complete" def perlDir = findPerlDirectory(extractPath) - if (!perlDir) throw new GradleException("Could not find perl.exe in extracted files") + if (!perlDir) throw new RuntimeException("Could not find perl.exe in extracted files") println " Found Perl directory: ${perlDir.name}" println "\nNOTE: Version ${version} was sourced from modules-untouched (or fallback URL)." return perlDir @@ -255,7 +257,7 @@ def downloadAndExtractPerl(String version) { // Helper function to generate hash files def generateHashFiles(File file) { - if (!file.exists()) throw new GradleException("File not found for hashing: ${file}") + if (!file.exists()) throw new RuntimeException("File not found for hashing: ${file}") def writeHash = { String algo, String ext -> def digest = java.security.MessageDigest.getInstance(algo) file.withInputStream { is -> @@ -384,7 +386,7 @@ tasks.register('release') { if (!versionToBuild) { def availableVersions = getAvailableVersions() if (availableVersions.isEmpty()) { - throw new GradleException("No versions found in bin/ directory") + throw new RuntimeException("No versions found in bin/ directory") } println "" @@ -412,7 +414,7 @@ tasks.register('release') { def reader = new BufferedReader(new InputStreamReader(System.in)) input = reader.readLine() } catch (Exception e) { - throw new GradleException(""" + throw new RuntimeException(""" Failed to read input. Please use non-interactive mode: gradle release -PbundleVersion=X.X.X @@ -420,7 +422,7 @@ tasks.register('release') { """.stripIndent()) } if (!input || input.trim().isEmpty()) { - throw new GradleException(""" + throw new RuntimeException(""" No version selected. Please use non-interactive mode: gradle release -PbundleVersion=X.X.X @@ -431,7 +433,7 @@ tasks.register('release') { if (cleaned.isInteger()) { def idx = cleaned.toInteger() if (idx < 1 || idx > availableVersions.size()) { - throw new GradleException(""" + throw new RuntimeException(""" Invalid selection index: ${cleaned} Please choose a number between 1 and ${availableVersions.size()} or enter a version string. @@ -441,7 +443,7 @@ tasks.register('release') { } else { versionToBuild = cleaned if (!availableVersions.contains(versionToBuild)) { - throw new GradleException(""" + throw new RuntimeException(""" Invalid version: ${versionToBuild} Please choose from available versions: @@ -464,7 +466,7 @@ tasks.register('release') { if (!bundlePath.exists()) { bundlePath = file("${projectDir}/bin/archived/${bundleName}${versionToBuild}") if (!bundlePath.exists()) { - throw new GradleException("Bundle version not found in bin/ or bin/archived/\n\nAvailable versions:\n${getAvailableVersions().collect { " - ${it}" }.join('\n')}") + throw new RuntimeException("Bundle version not found in bin/ or bin/archived/\n\nAvailable versions:\n${getAvailableVersions().collect { " - ${it}" }.join('\n')}") } } @@ -496,7 +498,7 @@ tasks.register('release') { try { bundleSrcFinal = downloadAndExtractPerl(versionToBuild) } catch (Exception e) { - throw new GradleException(( + throw new RuntimeException(( """ Failed to download Perl binaries: ${e.message} @@ -512,13 +514,14 @@ tasks.register('release') { } perlExe = file("${bundleSrcFinal}/perl.exe") - if (!perlExe.exists()) { throw new GradleException("perl.exe not found at ${perlExe}") } + if (!perlExe.exists()) { throw new RuntimeException("perl.exe not found at ${perlExe}") } println "Source folder: ${bundleSrcFinal}" println "" - // Prepare output directory - def prepPath = file("${bundleTmpPrepPath}/${bundleName}${versionToBuild}") + // Prepare output directory (this is the top-level folder that must be kept inside the archive) + def versionFolderName = "${bundleName}${versionToBuild}" + def prepPath = file("${bundleTmpPrepPath}/${versionFolderName}") if (prepPath.exists()) { delete prepPath } prepPath.mkdirs() @@ -556,18 +559,19 @@ tasks.register('release') { if (archiveFile.exists()) delete archiveFile def sevenZipExe = find7ZipExecutable() - if (!sevenZipExe) throw new GradleException("7-Zip not found. Please install 7-Zip or set 7Z_HOME environment variable.") + if (!sevenZipExe) throw new RuntimeException("7-Zip not found. Please install 7-Zip or set 7Z_HOME environment variable.") println "Using 7-Zip: ${sevenZipExe}" - println "Compressing ${bundleName}${versionToBuild} to ${archiveFile.name}..." + println "Compressing ${versionFolderName} to ${archiveFile.name} (including top-level folder)..." - def command = [ sevenZipExe, 'a', '-t7z', archiveFile.absolutePath.toString(), '.' ] + // Run 7z from the parent directory and add the folder by name to preserve the top-level folder in the archive + def command = [ sevenZipExe, 'a', '-t7z', archiveFile.absolutePath.toString(), versionFolderName ] def process = new ProcessBuilder(command as String[]) - .directory(prepPath) + .directory(prepPath.parentFile) .redirectErrorStream(true) .start() process.inputStream.eachLine { line -> if (line.trim()) println " ${line}" } def exitCode = process.waitFor() - if (exitCode != 0) throw new GradleException("7zip compression failed with exit code: ${exitCode}") + if (exitCode != 0) throw new RuntimeException("7zip compression failed with exit code: ${exitCode}") println "Archive created: ${archiveFile}" println "Generating hash files..." @@ -575,9 +579,12 @@ tasks.register('release') { } else { def archiveFile = file("${destFile}.zip") if (archiveFile.exists()) delete archiveFile - println "Compressing ${bundleName}${versionToBuild} to ${archiveFile.name}..." + println "Compressing ${versionFolderName} to ${archiveFile.name} (including top-level folder)..." task("zipArchive_${versionToBuild}", type: Zip) { - from prepPath + // Include the version folder itself so the archive has the top-level directory + from(prepPath.parentFile) { + include "${versionFolderName}/**" + } destinationDirectory = archiveFile.parentFile archiveFileName = archiveFile.name }.execute() @@ -602,7 +609,7 @@ tasks.register('releaseAll') { doLast { def versions = getAvailableVersions() - if (versions.isEmpty()) throw new GradleException('No versions found in bin/ or bin/archived/') + if (versions.isEmpty()) throw new RuntimeException('No versions found in bin/ or bin/archived/') println "" println "=".multiply(70) @@ -636,7 +643,7 @@ tasks.register('releaseAll') { if (!failed.isEmpty()) { println "\nFailed versions:" failed.each { println " - ${it}" } - throw new GradleException("${failed.size()} version(s) failed to build") + throw new RuntimeException("${failed.size()} version(s) failed to build") } else { println "[SUCCESS] All versions built successfully!" } @@ -677,7 +684,7 @@ tasks.register('verify') { println "-".multiply(60) def allPassed = checks.values().every { it } - if (!allPassed) throw new GradleException('Build environment verification failed') + if (!allPassed) throw new RuntimeException('Build environment verification failed') println '\n[SUCCESS] All checks passed! Build environment is ready.' println "\nYou can now run:\n gradle release -PbundleVersion=5.40.0 - Build release for version\n gradle listVersions - List available versions" } @@ -797,7 +804,7 @@ tasks.register('validateProperties') { if (!missing.isEmpty()) { println '[ERROR] Missing required properties:' missing.each { println " - ${it}" } - throw new GradleException('build.properties validation failed') + throw new RuntimeException('build.properties validation failed') } println '[SUCCESS] All required properties are present:' required.each { println " ${it} = ${buildProps.getProperty(it)}" } diff --git a/build.xml b/build.xml deleted file mode 100644 index cc79941..0000000 --- a/build.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -