Skip to content

Conversation

@N6REJ
Copy link
Contributor

@N6REJ N6REJ commented Nov 14, 2025

PR Type

Enhancement, Documentation


Description

  • Converted build system from hybrid Ant/Gradle to pure Gradle implementation with comprehensive Groovy DSL configuration

  • Added complete build.gradle.bruno-reference with 1183 lines implementing version resolution, release tasks (release, releaseAll), verification, and helper functions for 7-Zip detection and hash generation

  • Created extensive documentation suite (9 new documents) covering configuration, tasks, migration guide, and design decisions

  • Added gradle.properties with performance optimizations (daemon, parallel execution, build caching, JVM tuning)

  • Removed legacy build.xml Apache Ant configuration file

  • Updated main README.md with Gradle quick start, prerequisites, and task reference

  • Included migration guide with command mapping from Ant to Gradle equivalents and benefits analysis


Diagram Walkthrough

flowchart LR
  A["Legacy Ant<br/>build.xml"] -->|"Convert to"| B["Pure Gradle<br/>build.gradle"]
  B --> C["Configuration<br/>gradle.properties<br/>build.properties"]
  B --> D["Tasks<br/>release<br/>releaseAll<br/>verify"]
  B --> E["Documentation<br/>README<br/>TASKS<br/>CONFIGURATION<br/>MIGRATION"]
  C --> F["Optimized<br/>Build System"]
  D --> F
  E --> F
Loading

File Walkthrough

Relevant files
Enhancement
1 files
build.gradle.bruno-reference
Complete Gradle build system for Bruno module packaging   

build.gradle.bruno-reference

  • Added comprehensive Gradle build configuration for Bruno module with
    1183 lines of build logic
  • Implements version resolution strategy with 3-tier fallback (local
    releases.properties, remote modules-untouched, standard URL format)
  • Includes core build tasks: release (single version), releaseAll
    (batch), clean, and verification tasks
  • Provides helper functions for 7-Zip detection, hash generation
    (MD5/SHA1/SHA256/SHA512), and Bruno directory discovery
  • Supports interactive and non-interactive build modes with environment
    variable configuration for build paths
+1183/-0
Documentation
11 files
CONFIGURATION.md
Complete configuration guide for Gradle build system         

.gradle-docs/CONFIGURATION.md

  • Created comprehensive 551-line configuration guide covering all build
    system settings
  • Documents build.properties, gradle.properties, releases.properties,
    and environment variables
  • Includes detailed property descriptions, build path configuration,
    archive naming conventions, and hash file formats
  • Provides configuration examples (standard, custom paths, ZIP format,
    development setup) and best practices
  • Contains troubleshooting section for common configuration issues
+551/-0 
README.md
Main Gradle build documentation and quick start guide       

.gradle-docs/README.md

  • Created 446-line main documentation with overview, quick start, and
    installation instructions
  • Includes build tasks reference table, configuration details, and
    architecture explanation with process flow diagrams
  • Documents packaging details with archive structure examples and
    verification commands
  • Covers modules-untouched integration, troubleshooting common issues,
    and migration guide from Ant
  • Provides additional resources and support information
+446/-0 
TASKS.md
Complete Gradle tasks reference and usage guide                   

.gradle-docs/TASKS.md

  • Created 620-line comprehensive task reference documenting all Gradle
    tasks with detailed descriptions
  • Includes task groups (build, verification, help), usage examples,
    parameters, and expected output for each task
  • Documents core tasks: release, releaseAll, clean, verify,
    validateProperties, checkModulesUntouched
  • Provides information tasks: info, listVersions, listReleases with
    example outputs
  • Includes task dependencies, execution order, global options, and best
    practices
+620/-0 
MIGRATION.md
Comprehensive Ant to Gradle migration guide with examples

.gradle-docs/MIGRATION.md

  • Created 511-line migration guide from Apache Ant to Gradle build
    system
  • Provides command mapping table showing Ant to Gradle equivalents for
    all build operations
  • Documents file changes, configuration changes, and task equivalents
    with before/after code examples
  • Includes benefits comparison table showing performance, developer
    experience, and maintainability improvements
  • Contains migration checklist and troubleshooting section for common
    migration issues
+511/-0 
DESIGN-DECISIONS.md
Design decisions documentation for build system architecture

DESIGN-DECISIONS.md

  • Created 371-line document explaining design decisions for the build
    system
  • Justifies use of -PbundleVersion parameter over -Pversion for clarity
    and separation of concerns
  • Explains directory display conventions with [bin] location indicators
    for consistency and extensibility
  • Documents comparison with other modules (Bruno, Apache) and rationale
    for pure Gradle approach
  • Includes property naming conventions, directory structure choices, and
    release naming format
+371/-0 
DOCS-UPDATE-SUMMARY.md
Documentation restructuring summary and verification guide

DOCS-UPDATE-SUMMARY.md

  • Created 141-line summary of documentation restructuring and updates
  • Documents consolidation from 12 files to 5 focused documents with
    elimination of redundancy
  • Lists files removed (QUICK_REFERENCE, INTERACTIVE_MODE,
    FEATURE_SUMMARY, etc.) and final documentation structure
  • Highlights key improvements in structure, content, and usability
  • References module-php gradle-convert branch as the documentation
    reference implementation
+141/-0 
BUILD-SYSTEM.md
Comprehensive Gradle build system specification and documentation

BUILD-SYSTEM.md

  • New comprehensive build system specification document detailing pure
    Gradle implementation with Groovy DSL
  • Documents Gradle version requirements (7.0+), file structure, and
    absence of wrapper files
  • Includes detailed configuration examples for build.gradle,
    settings.gradle, gradle.properties, and build.properties
  • Provides system requirements, installation instructions, build
    commands, and troubleshooting guide
+418/-0 
INDEX.md
Documentation index and navigation guide for Gradle build system

.gradle-docs/INDEX.md

  • New documentation index providing centralized navigation for all
    Gradle build documentation
  • Includes quick links, documentation structure, and getting started
    guides for new users and those migrating from Ant
  • Contains searchable keyword index and document summaries for README,
    TASKS, CONFIGURATION, and MIGRATION guides
  • Provides version history and contribution guidelines for documentation
    maintenance
+340/-0 
MIGRATION-SUMMARY.md
Migration summary from Ant to pure Gradle build system     

MIGRATION-SUMMARY.md

  • New summary document detailing migration from hybrid Ant/Gradle to
    pure Gradle build system
  • Documents removed Ant files, updated build configuration, and new
    comprehensive documentation structure
  • Lists new Gradle tasks (info, verify, listVersions, listReleases,
    validateProperties, generateDocs)
  • Includes command mapping from Ant to Gradle, benefits analysis, and
    verification testing results
+298/-0 
FINAL-SUMMARY.md
Final summary of Gradle build system conversion and consistency
verification

FINAL-SUMMARY.md

  • New final summary document confirming successful build system
    conversion matching Bruno and Apache module patterns
  • Documents key changes including clean ASCII terminal output,
    interactive release mode, and directory location display
  • Provides usage examples for interactive/non-interactive releases,
    version listing, and build information display
  • Includes consistency comparison tables with other modules and
    comprehensive testing results
+295/-0 
README.md
Updated README with Gradle build system quick start and task reference

README.md

  • Updated with new Build System section replacing legacy Ant
    documentation
  • Added Quick Start section with essential Gradle commands (gradle info,
    gradle release, gradle verify, etc.)
  • Included Prerequisites table specifying Java 8+, Gradle 8.0+, and
    7-Zip requirements
  • Added Available Tasks table documenting all Gradle tasks with
    descriptions and links to detailed documentation
+58/-2   
Configuration changes
1 files
gradle.properties
Gradle properties configuration for build performance optimization

gradle.properties

  • New Gradle configuration file with performance optimization settings
  • Enables Gradle daemon, parallel execution, build caching, and
    configuration on demand for faster builds
  • Configures JVM arguments (2GB max heap, 512MB initial heap, 512MB
    metaspace)
  • Sets console output to rich format, warning mode to all, and enables
    file system watching for Gradle 7.0+
+30/-0   
Additional files
13 files
bearsampp.conf [link]   
bearsampp.conf [link]   
bearsampp.conf [link]   
bearsampp.conf [link]   
bearsampp.conf [link]   
bearsampp.conf [link]   
bearsampp.conf [link]   
bearsampp.conf [link]   
bearsampp.conf [link]   
bearsampp.conf [link]   
bearsampp.conf [link]   
bearsampp.conf [link]   
build.xml +0/-38   

@N6REJ N6REJ added the enhancement ✨ Improve program label Nov 14, 2025
@qodo-merge-pro
Copy link
Contributor

qodo-merge-pro bot commented Nov 14, 2025

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🔴
Weak hash algorithms

Description: The build generates and publishes MD5 and SHA1 hashes alongside stronger SHA256/512, which
encourages use of weak, collision-prone algorithms that are considered insecure for
integrity; avoid publishing MD5/SHA1.
build.gradle.bruno-reference [722-745]

Referred Code
// Generate MD5
def md5File = new File("${file.absolutePath}.md5")
def md5Hash = calculateHash(file, 'MD5')
md5File.text = "${md5Hash} ${file.name}\n"
println "  Created: ${md5File.name}"

// Generate SHA1
def sha1File = new File("${file.absolutePath}.sha1")
def sha1Hash = calculateHash(file, 'SHA-1')
sha1File.text = "${sha1Hash} ${file.name}\n"
println "  Created: ${sha1File.name}"

// Generate SHA256
def sha256File = new File("${file.absolutePath}.sha256")
def sha256Hash = calculateHash(file, 'SHA-256')
sha256File.text = "${sha256Hash} ${file.name}\n"
println "  Created: ${sha256File.name}"

// Generate SHA512
def sha512File = new File("${file.absolutePath}.sha512")
def sha512Hash = calculateHash(file, 'SHA-512')


 ... (clipped 3 lines)
Executable trust risk

Description: The 7-Zip executable is resolved from environment and hardcoded Windows paths then
executed with user-controlled inputs, which on non-Windows or PATH-hijack scenarios could
execute an unintended binary leading to command execution; restrict execution to trusted
paths or verify the binary signature.
build.gradle.bruno-reference [673-714]

Referred Code
// Helper function to find 7-Zip executable
def find7ZipExecutable() {
    // Check environment variable
    def sevenZipHome = System.getenv('7Z_HOME')
    if (sevenZipHome) {
        def exe = file("${sevenZipHome}/7z.exe")
        if (exe.exists()) {
            return exe.absolutePath
        }
    }

    // Check common installation paths
    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)


 ... (clipped 21 lines)
Supply chain trust

Description: The build fetches bruno.properties over HTTPS but does not verify authenticity beyond TLS
and accepts any content to decide download sources, enabling supply-chain poisoning if the
remote is compromised; add pinning, signature verification, or checksum validation.
build.gradle.bruno-reference [105-128]

Referred Code
// This is the primary source for version information when not in releases.properties
def fetchModulesUntouchedProperties() {
    def propsUrl = "https://raw.githubusercontent.com/Bearsampp/modules-untouched/main/modules/bruno.properties"

    println "Fetching bruno.properties from modules-untouched repository..."
    println "  URL: ${propsUrl}"

    def tempFile = file("${bundleTmpDownloadPath}/bruno-untouched.properties")
    tempFile.parentFile.mkdirs()

    try {
        ant.get(src: propsUrl, dest: tempFile, verbose: false, ignoreerrors: false)

        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 bruno.properties from modules-untouched: ${e.message}"
        println "  Will fall back to standard URL format if needed"


 ... (clipped 3 lines)
Unsigned binary download

Description: When a version is missing locally, the code constructs a release URL and downloads
binaries from GitHub without validating signatures or expected checksums before
extraction, risking malicious binaries; enforce checksum/signature verification prior to
use.
build.gradle.bruno-reference [130-189]

Referred Code
// Function to download from modules-untouched repository
def downloadFromModulesUntouched(String version, File destDir) {
    println "Version ${version} not found in releases.properties"
    println "Checking modules-untouched repository..."

    // First, try to fetch bruno.properties from modules-untouched
    def untouchedProps = fetchModulesUntouchedProperties()
    def untouchedUrl = null

    if (untouchedProps) {
        untouchedUrl = untouchedProps.getProperty(version)
        if (untouchedUrl) {
            println "Found version ${version} in modules-untouched bruno.properties"
            println "Downloading from:"
            println "  ${untouchedUrl}"
        } else {
            println "Version ${version} not found in modules-untouched bruno.properties"
            println "Attempting to construct URL based on standard format..."
            // Fallback to constructed URL
            untouchedUrl = "https://github.com/Bearsampp/modules-untouched/releases/download/bruno-${version}/bruno-${version}-win64.7z"
            println "  ${untouchedUrl}"


 ... (clipped 39 lines)
External process execution

Description: The script executes an external 7-Zip process with paths derived from downloaded archives;
while arguments are passed as an array (mitigating injection), lack of validation of paths
and reliance on system PATH may allow unintended binary execution; ensure path
canonicalization and trusted executable location.
build.gradle.bruno-reference [246-273]

Referred Code
// Use 7zip or built-in extraction
if (filename.endsWith('.7z')) {
    // Try to use 7zip if available
    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}"
        }



 ... (clipped 7 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing audit logs: The new build tasks perform critical actions (downloading, extracting, packaging) without
structured audit logging that captures user ID and standardized timestamps, relying mainly
on println outputs.

Referred Code
    println "  ${downloadUrl}"

    // Determine filename from URL
    def filename = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1)
    def downloadDir = file(bundleTmpDownloadPath)
    downloadDir.mkdirs()

    downloadedFile = file("${downloadDir}/${filename}")

    // Download if not already present
    if (!downloadedFile.exists()) {
        println "  Downloading to: ${downloadedFile}"
        ant.get(src: downloadUrl, dest: downloadedFile, verbose: true)
        println "  Download complete"
    } else {
        println "  Using cached file: ${downloadedFile}"
    }
}

// Extract the archive
def extractDir = file(bundleTmpExtractPath)


 ... (clipped 65 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Limited error context: Several GradleExceptions provide generic messages without including actionable context
like target URLs or paths in all cases, and some downloads use ant.get without try/catch,
risking unhandled failures.

Referred Code
    // Download if not already present
    if (!downloadedFile.exists()) {
        println "  Downloading to: ${downloadedFile}"
        ant.get(src: downloadUrl, dest: downloadedFile, verbose: true)
        println "  Download complete"
    } else {
        println "  Using cached file: ${downloadedFile}"
    }
}

// Extract the archive
def extractDir = file(bundleTmpExtractPath)
extractDir.mkdirs()
println "  Extracting archive..."
def extractPath = file("${extractDir}/${version}")
if (extractPath.exists()) {
    delete extractPath
}
extractPath.mkdirs()

// Determine filename from downloaded file


 ... (clipped 38 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Verbose errors: Error and warning outputs echo full filesystem paths and external URLs directly to
console, which may be user-facing in some contexts and could disclose internal details.

Referred Code
            // Download and extract to bearsampp-build/tmp
            bundleSrcFinal = downloadAndExtractBruno(bundleVersion, file(bundleTmpExtractPath))
        } catch (Exception e) {
            throw new GradleException("""
                Failed to download Bruno binaries: ${e.message}

                You can manually download and extract Bruno binaries to:
                  ${bundleSrcDest}/

                Or check that version ${bundleVersion} exists in releases.properties
            """.stripIndent())
        }
    }
}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Unsanitized inputs: External inputs like constructed URLs and version strings from properties/user input are
used for downloads and command execution without explicit validation against allowed
patterns or schemes.

Referred Code
def versionProperty = project.findProperty('bundleVersion')

doLast {
    def versionToBuild = versionProperty

    if (!versionToBuild) {
        // Interactive mode - prompt for version
        def availableVersions = getAvailableVersions()

        if (availableVersions.isEmpty()) {
            throw new GradleException("No versions found in bin/ directory")
        }

        println ""
        println "=".multiply(70)
        println "Interactive Release Mode"
        println "=".multiply(70)
        println ""
        println "Available versions:"

        // Show versions with location tags


 ... (clipped 51 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-merge-pro
Copy link
Contributor

qodo-merge-pro bot commented Nov 14, 2025

PR Code Suggestions ✨

Latest suggestions up to 14fc857

CategorySuggestion                                                                                                                                    Impact
Possible issue
Prevent 7z process hangs

Improve the 7-Zip extraction process by ensuring process streams are closed in a
finally block and adding a timeout to process.waitFor() to prevent potential
deadlocks or hangs.

build.gradle.bruno-reference [247-273]

 if (filename.endsWith('.7z')) {
-    // Try to use 7zip if available
     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}")
+        try {
+            process.inputStream.withReader { reader ->
+                reader.eachLine { line ->
+                    if (line?.trim()) println "    ${line}"
+                }
+            }
+            // Optional: enforce a max wait to avoid hangs
+            if (!process.waitFor(10, java.util.concurrent.TimeUnit.MINUTES)) {
+                process.destroyForcibly()
+                throw new GradleException("7zip extraction timed out after 10 minutes")
+            }
+            def exitCode = process.exitValue()
+            if (exitCode != 0) {
+                throw new GradleException("7zip extraction failed with exit code: ${exitCode}")
+            }
+        } finally {
+            try { process.inputStream?.close() } catch (ignore) {}
+            try { process.errorStream?.close() } catch (ignore) {}
+            try { process.outputStream?.close() } catch (ignore) {}
         }
     } else {
         throw new GradleException("7zip not found. Please install 7zip or extract manually.")
     }
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out a potential for process deadlocks by not closing streams and adds a timeout to prevent hangs, which are valid and important improvements for handling external processes robustly.

Medium
Validate build.properties presence

Add an explicit check for the existence of build.properties and wrap the file
loading in a try-catch block to provide a clearer error message if the file is
missing or unreadable.

build.gradle.bruno-reference [37-39]

-// Load build properties
+// Load build properties with validation
 def buildProps = new Properties()
-file('build.properties').withInputStream { buildProps.load(it) }
+def buildPropsFile = file('build.properties')
+if (!buildPropsFile.exists()) {
+    throw new GradleException("Required build.properties not found at ${buildPropsFile.absolutePath}. Please create it with bundle.name, bundle.release, bundle.type, and bundle.format.")
+}
+try {
+    buildPropsFile.withInputStream { buildProps.load(it) }
+} catch (Exception e) {
+    throw new GradleException("Failed to read build.properties: ${e.message}", e)
+}
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that the build will fail with an unhelpful error if build.properties is missing, and improves robustness by adding an explicit check and better error handling at the point of loading.

Low
General
Validate remote properties download

Add validation to the fetchModulesUntouchedProperties function to check that the
downloaded properties file is not empty and contains at least one property
before it is used.

build.gradle.bruno-reference [106-128]

 def fetchModulesUntouchedProperties() {
     def propsUrl = "https://raw.githubusercontent.com/Bearsampp/modules-untouched/main/modules/bruno.properties"
-    
+
     println "Fetching bruno.properties from modules-untouched repository..."
     println "  URL: ${propsUrl}"
-    
+
     def tempFile = file("${bundleTmpDownloadPath}/bruno-untouched.properties")
     tempFile.parentFile.mkdirs()
-    
+
     try {
         ant.get(src: propsUrl, dest: tempFile, verbose: false, ignoreerrors: false)
-        
+
+        if (!tempFile.exists() || tempFile.length() == 0) {
+            throw new IOException("Downloaded properties file is empty")
+        }
+
         def props = new Properties()
         tempFile.withInputStream { props.load(it) }
-        
+
+        if (props.isEmpty()) {
+            throw new IOException("No versions found in downloaded properties")
+        }
+
         println "  ✓ Successfully loaded ${props.size()} versions from modules-untouched"
         return props
     } catch (Exception e) {
-        println "  ✗ Warning: Could not fetch bruno.properties from modules-untouched: ${e.message}"
+        println "  ✗ Warning: Could not fetch usable bruno.properties from modules-untouched: ${e.message}"
         println "  Will fall back to standard URL format if needed"
         return null
     }
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: This suggestion improves the robustness of fetching remote properties by adding checks for an empty file and empty properties, preventing the build from proceeding with invalid data from a failed download.

Medium
  • More

Previous suggestions

Suggestions up to commit 520124b
CategorySuggestion                                                                                                                                    Impact
Possible issue
Quote paths in 7-Zip command

Quote the file and output paths in the 7-Zip extraction command to correctly
handle spaces and special characters.

build.gradle.bruno-reference [249-273]

 def sevenZipPath = find7ZipExecutable()
 if (sevenZipPath) {
+    // Quote paths to handle spaces/special characters safely
     def command = [
         sevenZipPath.toString(),
         'x',
-        downloadedFile.absolutePath.toString(),
-        "-o${extractPath.absolutePath}".toString(),
+        "\"${downloadedFile.absolutePath}\"",
+        "\"-o${extractPath.absolutePath}\"",
         '-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.")
 }
Suggestion importance[1-10]: 6

__

Why: This is a valid robustness improvement that prevents the build from failing if file paths contain spaces, which is a common scenario on Windows environments.

Low
Quote 7-Zip add command paths

Quote the archive and input paths in the 7-Zip compression command to correctly
handle spaces and special characters.

build.gradle.bruno-reference [596-644]

 // Build archive filename
 def destFile = file("${buildBinsPath}/bearsampp-${bundleName}-${bundleVersion}-${bundleRelease}")
 
-// Compress based on format
 if (bundleFormat == '7z') {
-    // 7z format
     def archiveFile = file("${destFile}.7z")
     if (archiveFile.exists()) {
         delete archiveFile
     }
 
     println "Compressing ${bundleName}${bundleVersion} to ${archiveFile.name}..."
 
-    // Find 7z executable
     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}"
 
-    // Create 7z archive
+    // Quote archive path and input path to handle spaces
     def command = [
-        sevenZipExe,
+        sevenZipExe.toString(),
         'a',
         '-t7z',
-        archiveFile.absolutePath.toString(),
-        '.'
+        "\"${archiveFile.absolutePath}\"",
+        "\".\""
     ]
 
     def process = new ProcessBuilder(command as String[])
         .directory(brunoPrepPath)
         .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}"
 
-    // Generate hash files
     println "Generating hash files..."
     generateHashFiles(archiveFile)
Suggestion importance[1-10]: 6

__

Why: This is a valid robustness improvement that prevents the build from failing if file paths contain spaces, which is a common scenario on Windows environments.

Low
General
Add network timeout to downloads

Add a maxtime attribute to the ant.get call to set a network timeout, preventing
the build from hanging indefinitely on network issues.

build.gradle.bruno-reference [106-128]

-// Function to fetch bruno.properties from modules-untouched repository
-// This is the primary source for version information when not in releases.properties
 def fetchModulesUntouchedProperties() {
     def propsUrl = "https://raw.githubusercontent.com/Bearsampp/modules-untouched/main/modules/bruno.properties"
     
     println "Fetching bruno.properties from modules-untouched repository..."
     println "  URL: ${propsUrl}"
     
     def tempFile = file("${bundleTmpDownloadPath}/bruno-untouched.properties")
     tempFile.parentFile.mkdirs()
     
     try {
-        ant.get(src: propsUrl, dest: tempFile, verbose: false, ignoreerrors: false)
+        // Set a max time (in seconds) to avoid indefinite hangs on bad networks
+        ant.get(src: propsUrl, dest: tempFile, verbose: false, ignoreerrors: false, maxtime: 60)
         
         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 bruno.properties from modules-untouched: ${e.message}"
         println "  Will fall back to standard URL format if needed"
         return null
     }
 }
Suggestion importance[1-10]: 5

__

Why: This suggestion improves the build's resilience to network issues by adding a timeout to the ant.get call, preventing indefinite hangs and making the build more robust.

Low
Suggestions up to commit ac3bd8a
CategorySuggestion                                                                                                                                    Impact
Possible issue
Enable fallback when local file missing

Modify the downloadAndExtractBruno function to handle a missing
releases.properties file gracefully, allowing the build to proceed with remote
fallback logic instead of throwing an exception.

build.gradle.bruno-reference [194-197]

+def releases = new Properties()
+def downloadUrl = null
+
 def releasesFile = file('releases.properties')
-if (!releasesFile.exists()) {
-    throw new GradleException("releases.properties not found")
+if (releasesFile.exists()) {
+    releasesFile.withInputStream { releases.load(it) }
+    downloadUrl = releases.getProperty(version)
+} else {
+    println "releases.properties not found, will attempt remote fallback (modules-untouched) and constructed URL."
 }
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical bug where the build fails if releases.properties is missing, which prevents the documented 3-tier fallback mechanism from working.

High
General
Add ZIP fallback if 7-Zip missing

In the release task, add a fallback to create a ZIP archive if the bundle.format
is 7z but the 7-Zip executable cannot be found, preventing a hard build failure.

build.gradle.bruno-reference [610-613]

 def sevenZipExe = find7ZipExecutable()
 if (!sevenZipExe) {
-    throw new GradleException("7-Zip not found. Please install 7-Zip or set 7Z_HOME environment variable.")
+    println "[WARNING] 7-Zip not found. Falling back to ZIP format for this build."
+    // Fallback to ZIP archive creation
+    def zipArchive = file("${destFile}.zip")
+    if (zipArchive.exists()) {
+        delete zipArchive
+    }
+    ant.zip(destfile: zipArchive, basedir: brunoPrepPath)
+    println "Archive created (fallback ZIP): ${zipArchive}"
+    println "Generating hash files..."
+    generateHashFiles(zipArchive)
+    return
 }
Suggestion importance[1-10]: 7

__

Why: This is a valuable suggestion that improves the build script's resilience by adding a fallback to ZIP compression if 7-Zip is not found, which is especially useful for non-Windows or CI environments.

Medium
Suggestions up to commit f9abd59
CategorySuggestion                                                                                                                                    Impact
High-level
Extract build logic into a reusable Gradle plugin

Extract the common build logic from the large, monolithic build script into a
reusable custom Gradle plugin. This will reduce code duplication and improve
maintainability across modules.

Examples:

build.gradle.bruno-reference [1-1183]
/*
 * Bearsampp Module Bruno - Gradle Build
 *
 * This is a 100% Gradle build configuration for the Bruno module.
 * It handles downloading, extracting, and packaging Bruno releases.
 *
 * VERSION RESOLUTION STRATEGY (3-tier fallback):
 *   1. Local releases.properties (primary source)
 *   2. Remote modules-untouched bruno.properties (automatic fallback)
 *      URL: https://github.com/Bearsampp/modules-untouched/blob/main/modules/bruno.properties

 ... (clipped 1173 lines)

Solution Walkthrough:

Before:

// In module-bruno/build.gradle.bruno-reference
plugins { id 'base' }

// ~100 lines of path and property configuration for bruno
ext {
  bundleName = 'bruno'
  // ... many other paths
}

// ~100 lines of helper functions for downloading/extracting bruno
def downloadAndExtractBruno(String version, File destDir) {
  // ... complex logic to download from github, fallback, etc.
}

// ~200 lines for the 'release' task for bruno
tasks.register('release') {
  doLast {
    // ... complex logic to find binaries, copy files, run 7zip
  }
}

// ~500 more lines for other tasks (info, verify, clean, etc.) for bruno
// ... and the same ~1000 lines are copied and adapted in other modules

After:

// In buildSrc/src/main/groovy/com.bearsampp.module-build.gradle
class ModuleBuildPlugin implements Plugin<Project> {
    void apply(Project project) {
        // ~1000 lines of generic build logic extracted here
        // - Generic download/extract functions
        // - Generic release task
        // - Generic info, verify tasks

        project.tasks.register('release', ReleaseTask) {
            // Task is configured by an extension
        }
    }
}

// In module-bruno/build.gradle
plugins {
    id 'com.bearsampp.module-build'
}

// Configure the plugin for this specific module
moduleBuild {
    bundleName = 'bruno'
    bundleType = 'tools'
    executableName = 'bruno.exe'
}
Suggestion importance[1-10]: 9

__

Why: This is a critical architectural suggestion that correctly identifies the major flaw of code duplication in the monolithic build script and proposes a scalable, maintainable solution using a custom Gradle plugin.

High
Security
Prevent path traversal security vulnerability

Add input validation for the bundleVersion project property to prevent a path
traversal security vulnerability. The validation should ensure the version
string contains only safe characters.

build.gradle.bruno-reference [422-427]

 doLast {
     def versionToBuild = versionProperty
+
+    if (versionToBuild && (versionToBuild.contains('..') || !versionToBuild.matches("^[\\w.-]+\$"))) {
+        throw new GradleException("Invalid characters in bundleVersion. Only alphanumeric characters, dots, and hyphens are allowed.")
+    }
 
     if (!versionToBuild) {
         // Interactive mode - prompt for version
         def availableVersions = getAvailableVersions()
Suggestion importance[1-10]: 9

__

Why: This is a valid and critical security suggestion that addresses a potential path traversal vulnerability when a malicious bundleVersion is passed, which could lead to file system manipulation outside the intended directories.

High
General
Improve executable discovery from PATH

Improve the reliability of finding the 7z.exe executable from the system PATH.
Instead of using the first result from the where command, iterate through all
returned paths and use the first one that executes successfully.

build.gradle.bruno-reference [701-708]

 def process = ['where', '7z.exe'].execute()
 process.waitFor()
 if (process.exitValue() == 0) {
     def output = process.text.trim()
     if (output) {
-        return output.split('\n')[0].trim()
+        // Find the first valid 7z.exe that can execute
+        for (String path in output.split('\n')) {
+            def candidate = path.trim()
+            try {
+                def testProcess = [candidate, '--help'].execute()
+                testProcess.waitFor()
+                if (testProcess.exitValue() == 0) {
+                    return candidate
+                }
+            } catch (Exception ignored) {
+                // Ignore if this path is not a valid executable
+            }
+        }
     }
 }
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies a potential issue where the first 7z.exe in the PATH might be non-functional and proposes a robust solution to iterate and test all found executables, improving the build script's reliability.

Low
Fix corrupted characters in documentation

Fix corrupted Unicode characters in an example output in MIGRATION-SUMMARY.md by
replacing them with ASCII characters, ensuring consistency with the project's
design decisions.

MIGRATION-SUMMARY.md [239-241]

-╔════════════════════════════════════════════════════════════════════════════╗
-║                    Build Environment Verification                          ║
-╚═══════════════════════════════════════════════════════════════════════════╝
+============================================================================
+|                    Build Environment Verification                          |
+============================================================================
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies corrupted Unicode characters in the documentation and proposes a fix that aligns with the project's stated goal of using ASCII for compatibility.

Low
Update documentation for consistency

Update an example output in .gradle-docs/README.md to use ASCII characters
instead of Unicode, ensuring it accurately reflects the build system's output
and maintains documentation consistency.

.gradle-docs/README.md [216-219]

 Expected output:
 

-╔════════════════════════════════════════════════════════════════════════════╗
-║ Build Environment Verification ║
-╚════════════════════════════════════════════════════════════════════════════╝
+============================================================================
+| Build Environment Verification |
+============================================================================







<details><summary>Suggestion importance[1-10]: 6</summary>

__

Why: The suggestion correctly points out an inconsistency in the documentation, as the example output uses Unicode characters while the project standard is ASCII, improving documentation accuracy.


</details></details></td><td align=center>Low

</td></tr><tr><td>



<details><summary>Use relative path in documentation<!-- not_implemented --></summary>

___

**Replace the hardcoded absolute path in the <code>README.md</code> example with a generic, <br>relative path to make the documentation universally applicable.**

[README.md [120-121]](https://github.com/Bearsampp/module-memcached/pull/23/files#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R120-R121)

```diff
 # 4. Test package
-7z t C:\Users\troy\Bearsampp-build\release\bearsampp-memcached-1.6.39-2025.8.20.7z
+# The release package is created in the 'build/release' directory
+7z t build/release/bearsampp-memcached-1.6.39-2025.8.20.7z
Suggestion importance[1-10]: 4

__

Why: The suggestion correctly identifies a hardcoded, user-specific path in the documentation, and replacing it with a generic path improves its usability for other developers.

Low

@qodo-merge-pro
Copy link
Contributor

qodo-merge-pro bot commented Nov 15, 2025

PR Reviewer Guide 🔍

(Review updated until commit 14fc857)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 No relevant tests
🔒 Security concerns

Supply chain integrity:
Remote downloads (modules-untouched URLs) are performed without integrity verification (no signature or checksum checks). This could allow tampered artifacts to be ingested. Add checksum validation (e.g., expected SHA256 from a trusted source) and enforce TLS certificate verification (Gradle/Ant does TLS, but verify algorithmically).

⚡ Recommended focus areas for review

Possible Issue

Hard-coded Windows-specific paths and commands (e.g., searching for 7z.exe via 'where', fixed "C:/Program Files/...") may break on non-Windows environments despite docs claiming cross-platform support. Consider OS detection and POSIX alternatives or graceful fallbacks.

// Helper function to find 7-Zip executable
def find7ZipExecutable() {
    // Check environment variable
    def sevenZipHome = System.getenv('7Z_HOME')
    if (sevenZipHome) {
        def exe = file("${sevenZipHome}/7z.exe")
        if (exe.exists()) {
            return exe.absolutePath
        }
    }

    // Check common installation paths
    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 to find in PATH
    try {
        def process = ['where', '7z.exe'].execute()
        process.waitFor()
        if (process.exitValue() == 0) {
            def output = process.text.trim()
            if (output) {
                return output.split('\n')[0].trim()
            }
        }
    } catch (Exception e) {
        // Ignore
    }

    return null
}
Robustness

Network fetch via ant.get for remote properties/downloads lacks retry/backoff and checksum verification; a transient failure or tampered file could cause flaky or unsafe builds. Consider adding retries and optional checksum validation against known hashes.

// Function to fetch bruno.properties from modules-untouched repository
// This is the primary source for version information when not in releases.properties
def fetchModulesUntouchedProperties() {
    def propsUrl = "https://raw.githubusercontent.com/Bearsampp/modules-untouched/main/modules/bruno.properties"

    println "Fetching bruno.properties from modules-untouched repository..."
    println "  URL: ${propsUrl}"

    def tempFile = file("${bundleTmpDownloadPath}/bruno-untouched.properties")
    tempFile.parentFile.mkdirs()

    try {
        ant.get(src: propsUrl, dest: tempFile, verbose: false, ignoreerrors: false)

        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 bruno.properties from modules-untouched: ${e.message}"
        println "  Will fall back to standard URL format if needed"
        return null
    }
}

// Function to download from modules-untouched repository
def downloadFromModulesUntouched(String version, File destDir) {
    println "Version ${version} not found in releases.properties"
    println "Checking modules-untouched repository..."

    // First, try to fetch bruno.properties from modules-untouched
    def untouchedProps = fetchModulesUntouchedProperties()
    def untouchedUrl = null

    if (untouchedProps) {
        untouchedUrl = untouchedProps.getProperty(version)
        if (untouchedUrl) {
            println "Found version ${version} in modules-untouched bruno.properties"
            println "Downloading from:"
            println "  ${untouchedUrl}"
        } else {
            println "Version ${version} not found in modules-untouched bruno.properties"
            println "Attempting to construct URL based on standard format..."
            // Fallback to constructed URL
            untouchedUrl = "https://github.com/Bearsampp/modules-untouched/releases/download/bruno-${version}/bruno-${version}-win64.7z"
            println "  ${untouchedUrl}"
        }
    } else {
        println "Could not fetch bruno.properties, using standard URL format..."
        // Fallback to constructed URL
        untouchedUrl = "https://github.com/Bearsampp/modules-untouched/releases/download/bruno-${version}/bruno-${version}-win64.7z"
        println "  ${untouchedUrl}"
    }

    // Determine filename from URL
    def filename = untouchedUrl.substring(untouchedUrl.lastIndexOf('/') + 1)
    def downloadDir = file(bundleTmpDownloadPath)
    downloadDir.mkdirs()

    def downloadedFile = file("${downloadDir}/${filename}")

    // Download if not already present
    if (!downloadedFile.exists()) {
        println "  Downloading to: ${downloadedFile}"
        try {
            ant.get(src: untouchedUrl, dest: downloadedFile, verbose: true)
            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 bruno.properties or matches format: bruno-{version}/bruno-{version}-win64.7z
                3. You have internet connectivity
            """.stripIndent())
        }
    } else {
        println "  Using cached file: ${downloadedFile}"
    }

    return downloadedFile
}
Consistency

Documentation files refer extensively to "Memcached" while this build script targets "bruno"; ensure naming, examples, and task outputs align across docs and script to avoid operator confusion.

/*
 * Bearsampp Module Bruno - Gradle Build
 *
 * This is a 100% Gradle build configuration for the Bruno module.
 * It handles downloading, extracting, and packaging Bruno releases.
 *
 * VERSION RESOLUTION STRATEGY (3-tier fallback):
 *   1. Local releases.properties (primary source)
 *   2. Remote modules-untouched bruno.properties (automatic fallback)
 *      URL: https://github.com/Bearsampp/modules-untouched/blob/main/modules/bruno.properties
 *   3. Standard URL format construction (last resort)
 *
 * DOCUMENTATION:
 *   All build documentation is located in /.gradle-docs/
 *   See /.gradle-docs/README.md for complete documentation index
 *
 * Usage:
 *   gradle tasks                              - List all available tasks
 *   gradle release -PbundleVersion=2.13.0     - Build release for specific version
 *   gradle releaseAll                         - Build all versions
 *   gradle clean                              - Clean build artifacts
 *   gradle info                               - Display build information
 *   gradle verify                             - Verify build environment
 *   gradle listVersions                       - List available versions
 *   gradle listReleases                       - List releases from properties
 *   gradle checkModulesUntouched              - Check modules-untouched integration
 */

plugins {
    id 'base'
}

// ============================================================================
// PROJECT CONFIGURATION
// ============================================================================

// 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', 'bruno')}"

// Define project paths
ext {
    projectBasedir = projectDir.absolutePath
    rootDir = projectDir.parent
    devPath = file("${rootDir}/dev").absolutePath

    // Bundle properties from build.properties
    bundleName = buildProps.getProperty('bundle.name', 'bruno')
    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 (same as Ant builds)
    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

    // Download and extract paths - use bearsampp-build/tmp instead of local build/
    bundleTmpDownloadPath = file("${buildTmpPath}/downloads/${bundleName}").absolutePath
    bundleTmpExtractPath = file("${buildTmpPath}/extract/${bundleName}").absolutePath
}

// 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
repositories {
    mavenCentral()
}

// ============================================================================
// HELPER FUNCTIONS
// ============================================================================

// Function to check if version exists in releases.properties
def versionExistsInReleases(String version) {
    def releasesFile = file('releases.properties')
    if (!releasesFile.exists()) {
        return false
    }

    def releases = new Properties()
    releasesFile.withInputStream { releases.load(it) }

    return releases.getProperty(version) != null
}

// Function to fetch bruno.properties from modules-untouched repository
// This is the primary source for version information when not in releases.properties
def fetchModulesUntouchedProperties() {
    def propsUrl = "https://raw.githubusercontent.com/Bearsampp/modules-untouched/main/modules/bruno.properties"

    println "Fetching bruno.properties from modules-untouched repository..."
    println "  URL: ${propsUrl}"

    def tempFile = file("${bundleTmpDownloadPath}/bruno-untouched.properties")
    tempFile.parentFile.mkdirs()

    try {
        ant.get(src: propsUrl, dest: tempFile, verbose: false, ignoreerrors: false)

        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 bruno.properties from modules-untouched: ${e.message}"
        println "  Will fall back to standard URL format if needed"
        return null
    }
}

// Function to download from modules-untouched repository
def downloadFromModulesUntouched(String version, File destDir) {
    println "Version ${version} not found in releases.properties"
    println "Checking modules-untouched repository..."

    // First, try to fetch bruno.properties from modules-untouched
    def untouchedProps = fetchModulesUntouchedProperties()
    def untouchedUrl = null

    if (untouchedProps) {
        untouchedUrl = untouchedProps.getProperty(version)
        if (untouchedUrl) {
            println "Found version ${version} in modules-untouched bruno.properties"
            println "Downloading from:"
            println "  ${untouchedUrl}"
        } else {
            println "Version ${version} not found in modules-untouched bruno.properties"
            println "Attempting to construct URL based on standard format..."
            // Fallback to constructed URL
            untouchedUrl = "https://github.com/Bearsampp/modules-untouched/releases/download/bruno-${version}/bruno-${version}-win64.7z"
            println "  ${untouchedUrl}"
        }
    } else {
        println "Could not fetch bruno.properties, using standard URL format..."
        // Fallback to constructed URL
        untouchedUrl = "https://github.com/Bearsampp/modules-untouched/releases/download/bruno-${version}/bruno-${version}-win64.7z"
        println "  ${untouchedUrl}"
    }

    // Determine filename from URL
    def filename = untouchedUrl.substring(untouchedUrl.lastIndexOf('/') + 1)
    def downloadDir = file(bundleTmpDownloadPath)
    downloadDir.mkdirs()

    def downloadedFile = file("${downloadDir}/${filename}")

    // Download if not already present
    if (!downloadedFile.exists()) {
        println "  Downloading to: ${downloadedFile}"
        try {
            ant.get(src: untouchedUrl, dest: downloadedFile, verbose: true)
            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 bruno.properties or matches format: bruno-{version}/bruno-{version}-win64.7z
                3. You have internet connectivity
            """.stripIndent())
        }
    } else {
        println "  Using cached file: ${downloadedFile}"
    }

    return downloadedFile
}

// Function to download and extract Bruno binaries
def downloadAndExtractBruno(String version, File destDir) {
    // Load releases.properties to get download URL
    def releasesFile = file('releases.properties')
    if (!releasesFile.exists()) {
        throw new GradleException("releases.properties not found")
    }

    def releases = new Properties()
    releasesFile.withInputStream { releases.load(it) }

    def downloadUrl = releases.getProperty(version)
    def downloadedFile = null

    // Check if version exists in releases.properties
    if (!downloadUrl) {
        println "Version ${version} not found in releases.properties"
        println "Fetching from modules-untouched repository..."

        // Try to download from modules-untouched
        downloadedFile = downloadFromModulesUntouched(version, destDir)
    } else {
        println "Downloading Bruno ${version} from:"
        println "  ${downloadUrl}"

        // Determine filename from URL
        def filename = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1)
        def downloadDir = file(bundleTmpDownloadPath)
        downloadDir.mkdirs()

        downloadedFile = file("${downloadDir}/${filename}")

        // Download if not already present
        if (!downloadedFile.exists()) {
            println "  Downloading to: ${downloadedFile}"
            ant.get(src: downloadUrl, dest: downloadedFile, verbose: true)
            println "  Download complete"
        } else {
            println "  Using cached file: ${downloadedFile}"
        }
    }

    // Extract the archive
    def extractDir = file(bundleTmpExtractPath)
    extractDir.mkdirs()
    println "  Extracting archive..."
    def extractPath = file("${extractDir}/${version}")
    if (extractPath.exists()) {
        delete extractPath
    }
    extractPath.mkdirs()

    // Determine filename from downloaded file
    def filename = downloadedFile.name

    // Use 7zip or built-in extraction
    if (filename.endsWith('.7z')) {
        // Try to use 7zip if available
        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"

    // Find the Bruno directory in the extracted files
    def brunoDir = findBrunoDirectory(extractPath)
    if (!brunoDir) {
        throw new GradleException("Could not find Bruno directory in extracted files")
    }

    println "  Found Bruno directory: ${brunoDir.name}"

    // If downloaded from modules-untouched, add a note
    if (!versionExistsInReleases(version)) {
        println ""
        println "NOTE: Version ${version} was downloaded from modules-untouched repository"
        println "      Consider adding it to releases.properties for future builds"
    }

    return brunoDir
}

// Function to find Bruno directory in extracted files
def findBrunoDirectory(File extractPath) {
    // Look for directory containing bruno.exe
    def brunoDirs = extractPath.listFiles()?.findAll {
        it.isDirectory() && file("${it}/bruno.exe").exists()
    }

    if (brunoDirs && !brunoDirs.isEmpty()) {
        return brunoDirs[0]
    }

    // If not found at top level, search one level deep
    def foundDir = null
    extractPath.listFiles()?.each { dir ->
        if (dir.isDirectory() && !foundDir) {
            def subDirs = dir.listFiles()?.findAll {
                it.isDirectory() && file("${it}/bruno.exe").exists()
            }
            if (subDirs && !subDirs.isEmpty()) {
                foundDir = subDirs[0]
            }
        }
    }

    return foundDir
}

// ============================================================================
// GRADLE TASKS
// ============================================================================

// Task: Display build information
tasks.register('info') {
    group = 'help'
    description = 'Display build configuration information'

    // Capture values at configuration time to avoid deprecation warnings
    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 bundleTmpDownloadPathValue = bundleTmpDownloadPath
    def bundleTmpExtractPathValue = bundleTmpExtractPath
    def javaVersion = JavaVersion.current()
    def javaHome = System.getProperty('java.home')
    def gradleVersion = gradle.gradleVersion
    def gradleHome = gradle.gradleHomeDir

    doLast {
        println """
        ================================================================
                  Bearsampp Module Bruno - Build Info
        ================================================================

        Project:        ${projectName}
        Version:        ${projectVersion}
        Description:    ${projectDescription}

        Bundle Properties:
          Name:         ${bundleNameValue}
          Release:      ${bundleReleaseValue}
          Type:         ${bundleTypeValue}
          Format:       ${bundleFormatValue}

        Paths:
          Project Dir:  ${projectBasedirValue}
          Root Dir:     ${rootDirValue}
          Dev Path:     ${devPathValue}
          Build Base:   ${buildBasePathValue}
          Build Tmp:    ${buildTmpPathValue}
          Tmp Prep:     ${bundleTmpPrepPathValue}
          Tmp Build:    ${bundleTmpBuildPathValue}
          Tmp Src:      ${bundleTmpSrcPathValue}
          Tmp Download: ${bundleTmpDownloadPathValue}
          Tmp Extract:  ${bundleTmpExtractPathValue}

        Java:
          Version:      ${javaVersion}
          Home:         ${javaHome}

        Gradle:
          Version:      ${gradleVersion}
          Home:         ${gradleHome}

        Available Task Groups:
          * build        - Build and package tasks
          * help         - Help and information tasks
          * verification - Verification tasks

        Quick Start:
          gradle tasks                              - List all available tasks
          gradle info                               - Show this information
          gradle release -PbundleVersion=2.13.0     - Build specific version
          gradle releaseAll                         - Build all versions
          gradle clean                              - Clean build artifacts
          gradle verify                             - Verify build environment
        """.stripIndent()
    }
}

// Task: Main release task - build a specific version
tasks.register('release') {
    group = 'build'
    description = 'Build release package for a specific version (use -PbundleVersion=X.X.X or run interactively)'

    // Capture property at configuration time to avoid deprecation warning
    def versionProperty = project.findProperty('bundleVersion')

    doLast {
        def versionToBuild = versionProperty

        if (!versionToBuild) {
            // Interactive mode - prompt for version
            def availableVersions = getAvailableVersions()

            if (availableVersions.isEmpty()) {
                throw new GradleException("No versions found in bin/ directory")
            }

            println ""
            println "=".multiply(70)
            println "Interactive Release Mode"
            println "=".multiply(70)
            println ""
            println "Available versions:"

            // Show versions with location tags
            def binDir = file("${projectDir}/bin")
            def archivedDir = file("${projectDir}/bin/archived")

            availableVersions.eachWithIndex { version, index ->
                def location = ""
                if (binDir.exists() && file("${binDir}/${bundleName}${version}").exists()) {
                    location = "[bin]"
                } else if (archivedDir.exists() && file("${archivedDir}/${bundleName}${version}").exists()) {
                    location = "[bin/archived]"
                }
                println "  ${(index + 1).toString().padLeft(2)}. ${version.padRight(15)} ${location}"
            }
            println ""
            println "Enter version number to build:"
            println ""

            // Read input using Gradle's standard input
            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

                    Available versions: ${availableVersions.join(', ')}
                """.stripIndent())
            }

            versionToBuild = input.trim()

            // Validate the entered version
            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}"
        }

        println ""
        println "=".multiply(70)
        println "Building ${bundleName} ${versionToBuild}"
        println "=".multiply(70)
        println ""

        // Validate version exists - check both bin and bin/archived directories
        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')}")
            }
        }

        println "Bundle path: ${bundlePath}"
        println ""

        // Get the bundle folder and version
        def bundleFolder = bundlePath.name
        def bundleVersion = bundleFolder.replace(bundleName, '')

        // Determine source paths
        def bundleSrcDest = bundlePath
        def bundleSrcFinal = bundleSrcDest

        // Check if bruno.exe exists in bin/ directory
        def brunoExe = file("${bundleSrcFinal}/bruno.exe")
        if (!brunoExe.exists()) {
            // Bruno binaries not found in bin/ - check if already downloaded to bearsampp-build/tmp
            def tmpExtractPath = file("${bundleTmpExtractPath}/${bundleVersion}")
            def tmpBrunoDir = findBrunoDirectory(tmpExtractPath)

            if (tmpBrunoDir && tmpBrunoDir.exists()) {
                println "Using cached Bruno binaries from bearsampp-build/tmp"
                bundleSrcFinal = tmpBrunoDir
            } else {
                // Download and extract to bearsampp-build/tmp
                println ""
                println "Bruno binaries not found"
                println "Downloading Bruno ${bundleVersion}..."
                println ""

                try {
                    // Download and extract to bearsampp-build/tmp
                    bundleSrcFinal = downloadAndExtractBruno(bundleVersion, file(bundleTmpExtractPath))
                } catch (Exception e) {
                    throw new GradleException("""
                        Failed to download Bruno binaries: ${e.message}

                        You can manually download and extract Bruno binaries to:
                          ${bundleSrcDest}/

                        Or check that version ${bundleVersion} exists in releases.properties
                    """.stripIndent())
                }
            }
        }

        // Verify bruno.exe exists
        brunoExe = file("${bundleSrcFinal}/bruno.exe")
        if (!brunoExe.exists()) {
            throw new GradleException("bruno.exe not found at ${brunoExe}")
        }

        println "Source folder: ${bundleSrcFinal}"
        println ""

        // Prepare output directory
        def brunoPrepPath = file("${bundleTmpPrepPath}/${bundleName}${bundleVersion}")
        if (brunoPrepPath.exists()) {
            delete brunoPrepPath
        }
        brunoPrepPath.mkdirs()

        // Copy Bruno binaries from extracted/downloaded location
        println "Copying Bruno files..."
        copy {
            from bundleSrcFinal
            into brunoPrepPath
        }

        // Copy configuration files from bin directory
        println "Copying configuration files..."
        copy {
            from bundleSrcDest
            into brunoPrepPath
            include 'bearsampp.conf'
        }

        println ""
        println "Preparing archive..."

        // Determine build output path following Apache pattern
        // bearsampp-build/tools/bruno/{bundleRelease} for tools
        // bearsampp-build/bins/apache/{bundleRelease} for bins
        def buildPath = file(buildBasePath)
        def buildBinsPath = file("${buildPath}/${bundleType}/${bundleName}/${bundleRelease}")
        buildBinsPath.mkdirs()

        // Build archive filename
        def destFile = file("${buildBinsPath}/bearsampp-${bundleName}-${bundleVersion}-${bundleRelease}")

        // Compress based on format
        if (bundleFormat == '7z') {
            // 7z format
            def archiveFile = file("${destFile}.7z")
            if (archiveFile.exists()) {
                delete archiveFile
            }

            println "Compressing ${bundleName}${bundleVersion} to ${archiveFile.name}..."

            // Find 7z executable
            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}"

            // Create 7z archive
            def command = [
                sevenZipExe,
                'a',
                '-t7z',
                archiveFile.absolutePath.toString(),
                '.'
            ]

            def process = new ProcessBuilder(command as String[])
                .directory(brunoPrepPath)
                .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}"

            // Generate hash files
            println "Generating hash files..."
            generateHashFiles(archiveFile)

        } else {
            // ZIP format
            def archiveFile = file("${destFile}.zip")
            if (archiveFile.exists()) {
                delete archiveFile
            }

            println "Compressing ${bundleName}${bundleVersion} to ${archiveFile.name}..."

            ant.zip(destfile: archiveFile, basedir: brunoPrepPath)

            println "Archive created: ${archiveFile}"

            // Generate hash files
            println "Generating hash files..."
            generateHashFiles(archiveFile)
        }

        println ""
        println "=".multiply(70)
        println "[SUCCESS] Release build completed successfully for version ${versionToBuild}"
        println "Output directory: ${buildBinsPath}"
        println "Archive: ${destFile}.${bundleFormat}"
        println "=".multiply(70)
    }
}

// Helper function to find 7-Zip executable
def find7ZipExecutable() {
    // Check environment variable
    def sevenZipHome = System.getenv('7Z_HOME')
    if (sevenZipHome) {
        def exe = file("${sevenZipHome}/7z.exe")
        if (exe.exists()) {
            return exe.absolutePath
        }
    }

    // Check common installation paths
    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 to find in PATH
    try {
        def process = ['where', '7z.exe'].execute()
        process.waitFor()
        if (process.exitValue() == 0) {
            def output = process.text.trim()
            if (output) {
                return output.split('\n')[0].trim()
            }
        }
    } catch (Exception e) {
        // Ignore
    }

    return null
}

// Helper function to generate hash files
def generateHashFiles(File file) {
    if (!file.exists()) {
        throw new GradleException("File not found for hashing: ${file}")
    }

    // Generate MD5
    def md5File = new File("${file.absolutePath}.md5")
    def md5Hash = calculateHash(file, 'MD5')
    md5File.text = "${md5Hash} ${file.name}\n"
    println "  Created: ${md5File.name}"

    // Generate SHA1
    def sha1File = new File("${file.absolutePath}.sha1")
    def sha1Hash = calculateHash(file, 'SHA-1')
    sha1File.text = "${sha1Hash} ${file.name}\n"
    println "  Created: ${sha1File.name}"

    // Generate SHA256
    def sha256File = new File("${file.absolutePath}.sha256")
    def sha256Hash = calculateHash(file, 'SHA-256')
    sha256File.text = "${sha256Hash} ${file.name}\n"
    println "  Created: ${sha256File.name}"

    // Generate SHA512
    def sha512File = new File("${file.absolutePath}.sha512")
    def sha512Hash = calculateHash(file, 'SHA-512')
    sha512File.text = "${sha512Hash} ${file.name}\n"
    println "  Created: ${sha512File.name}"
}

// Helper function to calculate hash
def calculateHash(File file, String algorithm) {
    def digest = java.security.MessageDigest.getInstance(algorithm)
    file.withInputStream { stream ->
        def buffer = new byte[8192]
        def bytesRead
        while ((bytesRead = stream.read(buffer)) != -1) {
            digest.update(buffer, 0, bytesRead)
        }
    }
    return digest.digest().collect { String.format('%02x', it) }.join('')
}

// Helper function to get available versions
def getAvailableVersions() {
    def versions = []

    // Check bin directory
    def binDir = file("${projectDir}/bin")
    if (binDir.exists()) {
        def binVersions = binDir.listFiles()
            ?.findAll { it.isDirectory() && it.name.startsWith(bundleName) && it.name != 'archived' }
            ?.collect { it.name.replace(bundleName, '') } ?: []
        versions.addAll(binVersions)
    }

    // Check bin/archived subdirectory
    def archivedDir = file("${projectDir}/bin/archived")
    if (archivedDir.exists()) {
        def archivedVersions = archivedDir.listFiles()
            ?.findAll { it.isDirectory() && it.name.startsWith(bundleName) }
            ?.collect { it.name.replace(bundleName, '') } ?: []
        versions.addAll(archivedVersions)
    }

    // Remove duplicates and sort
    return versions.unique().sort()
}

// Task: Build all available versions
tasks.register('releaseAll') {
    group = 'build'
    description = 'Build release packages for all available versions in bin/ directory'

    doLast {
        def binDir = file("${projectDir}/bin")
        if (!binDir.exists()) {
            throw new GradleException("bin/ directory not found")
        }

        def versions = getAvailableVersions()

        if (versions.isEmpty()) {
            throw new GradleException("No versions found in bin/ directory")
        }

        println ""
        println "=".multiply(70)
        println "Building releases for ${versions.size()} ${bundleName} versions"
        println "=".multiply(70)
        println ""

        def successCount = 0
        def failedVersions = []

        versions.each { version ->
            println "=".multiply(70)
            println "[${successCount + 1}/${versions.size()}] Building ${bundleName} ${version}..."
            println "=".multiply(70)

            try {
                // Call the release task logic for this version
                def bundlePath = file("${projectDir}/bin/${bundleName}${version}")

                if (!bundlePath.exists()) {
                    throw new GradleException("Bundle path not found: ${bundlePath}")
                }

                println "Bundle path: ${bundlePath}"
                println ""

                // Get the bundle folder and version
                def bundleFolder = bundlePath.name
                def bundleVersion = bundleFolder.replace(bundleName, '')

                // Determine source paths
                def bundleSrcDest = bundlePath
                def bundleSrcFinal = bundleSrcDest

                // Check if bruno.exe exists
                def brunoExe = file("${bundleSrcFinal}/bruno.exe")
                if (!brunoExe.exists()) {
                    throw new GradleException("bruno.exe not found at ${brunoExe}")
                }

                println "Source folder: ${bundleSrcFinal}"
                println ""

                // Prepare output directory
                def brunoPrepPath = file("${bundleTmpPrepPath}/${bundleName}${bundleVersion}")
                if (brunoPrepPath.exists()) {
                    delete brunoPrepPath
                }
                brunoPrepPath.mkdirs()

                // Copy Bruno files
                println "Copying Bruno files..."
                copy {
                    from bundleSrcDest
                    into brunoPrepPath
                }

                println ""
                println "[SUCCESS] ${bundleName} ${version} completed"
                println "Output: ${brunoPrepPath}"
                successCount++

            } catch (Exception e) {
                println ""
                println "[FAILED] ${bundleName} ${version}: ${e.message}"
                failedVersions.add(version)
            }

            println ""
        }

        // Summary
        println "=".multiply(70)
        println "Build Summary"
        println "=".multiply(70)
        println "Total versions: ${versions.size()}"
        println "Successful:     ${successCount}"
        println "Failed:         ${failedVersions.size()}"

        if (!failedVersions.isEmpty()) {
            println ""
            println "Failed versions:"
            failedVersions.each { v ->
                println "  - ${v}"
            }
        }

        println "=".multiply(70)

        if (failedVersions.isEmpty()) {
            println "[SUCCESS] All versions built successfully!"
        } else {
            throw new GradleException("${failedVersions.size()} version(s) failed to build")
        }
    }
}

// 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
        }

        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-bruno..."

        def checks = [:]

        // Check Java version
        def javaVersion = JavaVersion.current()
        checks['Java 8+'] = javaVersion >= JavaVersion.VERSION_1_8

        // Check required files
        checks['build.properties'] = file('build.properties').exists()
        checks['releases.properties'] = file('releases.properties').exists()

        // Check dev directory
        checks['dev directory'] = file(devPath).exists()

        // Check bin directory
        checks['bin directory'] = file("${projectDir}/bin").exists()

        // Check 7-Zip if format is 7z
        if (bundleFormat == '7z') {
            checks['7-Zip'] = find7ZipExecutable() != null
        }

        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 -PbundleVersion=2.13.0   - Build release for version"
            println "  gradle listVersions                     - List available versions"
        } 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 Bruno 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 and bin/archived directories
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)

        // Show which directory each version is in
        def binDir = file("${projectDir}/bin")
        def archivedDir = file("${projectDir}/bin/archived")

        versions.each { version ->
            def location = ""
            if (binDir.exists() && file("${binDir}/${bundleName}${version}").exists()) {
                location = "[bin]"
            } else if (archivedDir.exists() && file("${archivedDir}/${bundleName}${version}").exists()) {
                location = "[bin/archived]"
            }
            println "  ${version.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: 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")
        }
    }
}

// Task: Check modules-untouched integration
tasks.register('checkModulesUntouched') {
    group = 'verification'
    description = 'Check modules-untouched repository integration and available versions'

    doLast {
        println ""
        println "=".multiply(70)
        println "Modules-Untouched Integration Check"
        println "=".multiply(70)
        println ""

        def propsUrl = "https://raw.githubusercontent.com/Bearsampp/modules-untouched/main/modules/bruno.properties"
        println "Repository URL:"
        println "  ${propsUrl}"
        println ""

        println "Fetching bruno.properties from modules-untouched..."
        def untouchedProps = fetchModulesUntouchedProperties()

        if (untouchedProps) {
            println ""
            println "=".multiply(70)
            println "Available Versions in modules-untouched"
            println "=".multiply(70)

            def sortedVersions = untouchedProps.sort { a, b ->
                // Simple version comparison
                def aParts = a.key.tokenize('.')
                def bParts = b.key.tokenize('.')
                for (int i = 0; i < Math.min(aParts.size(), bParts.size()); i++) {
                    def aNum = aParts[i].toInteger()
                    def bNum = bParts[i].toInteger()
                    if (aNum != bNum) return aNum <=> bNum
                }
                return aParts.size() <=> bParts.size()
            }

            sortedVersions.each { version, url ->
                def inReleases = versionExistsInReleases(version) ? "[in releases.properties]" : ""
                println "  ${version.padRight(10)} ${inReleases}"
            }

            println "=".multiply(70)
            println "Total versions: ${untouchedProps.size()}"
            println ""

            // Compare with releases.properties
            def releasesFile = file('releases.properties')
            if (releasesFile.exists()) {
                def releases = new Properties()
                releasesFile.withInputStream { releases.load(it) }

                def onlyInUntouched = untouchedProps.keySet() - releases.keySet()
                def onlyInReleases = releases.keySet() - untouchedProps.keySet()

                if (!onlyInUntouched.isEmpty()) {
                    println "Versions only in modules-untouched (${onlyInUntouched.size()}):"
                    onlyInUntouched.sort().each { version ->
                        println "  - ${version}"
                    }
                    println ""
                }

                if (!onlyInReleases.isEmpty()) {
                    println "Versions only in releases.properties (${onlyInReleases.size()}):"
                    onlyInReleases.sort().each { version ->
                        println "  - ${version}"
                    }
                    println ""
                }

                def inBoth = untouchedProps.keySet().intersect(releases.keySet())
                println "Versions in both sources: ${inBoth.size()}"
            }

            println ""
            println "=".multiply(70)
            println "[SUCCESS] modules-untouched integration is working"
            println "=".multiply(70)
            println ""
            println "Version Resolution Strategy:"
            println "  1. Check releases.properties (local)"
            println "  2. Check modules-untouched bruno.properties (remote)"
            println "  3. Construct standard URL format (fallback)"
            println ""
            println "Documentation: /.gradle-docs/MODULES_UNTOUCHED_INTEGRATION.md"

        } else {
            println ""
            println "=".multiply(70)
            println "[WARNING] Could not fetch bruno.properties from modules-untouched"
            println "=".multiply(70)
            println ""
            println "This may be due to:"
            println "  - Network connectivity issues"
            println "  - Repository access problems"
            println "  - File not available at expected location"
            println ""
            println "The build system will fall back to:"
            println "  1. releases.properties (if version exists)"
            println "  2. Standard URL format construction"
        }
    }
}

// ============================================================================
// BUILD LIFECYCLE HOOKS
// ============================================================================

gradle.taskGraph.whenReady { graph ->
    println """
    ================================================================
      Bearsampp Module Bruno - Gradle Build
    ================================================================
    """.stripIndent()
}

// ============================================================================
// DEFAULT TASK
// ============================================================================

defaultTasks 'info'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants