diff --git a/.github/workflows/gradle-build.yml b/.github/workflows/gradle-build.yml new file mode 100644 index 00000000..99ba0a7a --- /dev/null +++ b/.github/workflows/gradle-build.yml @@ -0,0 +1,93 @@ +name: Gradle Build + +on: + push: + branches: [ main, gradle-convert ] + pull_request: + branches: [ main ] + workflow_dispatch: + +jobs: + build: + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + with: + gradle-version: 8.5 + + - name: Install 7-Zip + run: | + choco install 7zip -y + echo "C:\Program Files\7-Zip" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Show build info + run: gradle buildInfo + + - name: List available versions + run: gradle listVersions + + - name: Verify bundle structure + run: gradle verifyBundle + + - name: Build release + run: gradle buildRelease + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: bearsampp-git-release + path: ../bearsampp-build/tools/git/**/*.7z + retention-days: 30 + + - name: Display build output + run: | + Write-Host "Build completed successfully!" + Write-Host "Output files:" + Get-ChildItem ../bearsampp-build/tools/git/ -Recurse -Filter *.7z | ForEach-Object { + Write-Host " - $($_.FullName) ($([math]::Round($_.Length / 1MB, 2)) MB)" + } + + build-all: + runs-on: windows-latest + if: github.event_name == 'workflow_dispatch' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + with: + gradle-version: 8.5 + + - name: Install 7-Zip + run: | + choco install 7zip -y + echo "C:\Program Files\7-Zip" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Build all releases + run: gradle buildAllReleases --parallel --max-workers=2 + + - name: Upload all artifacts + uses: actions/upload-artifact@v4 + with: + name: bearsampp-git-all-releases + path: ../bearsampp-build/tools/git/**/*.7z + retention-days: 30 diff --git a/.gitignore b/.gitignore index 207bc8a7..fbb472e8 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,15 @@ # ignore "current" directories /**/current +# Gradle +/.gradle +/build + # Qodo /.qodo + +# Gradle +.gradle/ + +# Build artifacts +build/ diff --git a/.gradle-docs/README.md b/.gradle-docs/README.md new file mode 100644 index 00000000..25126831 --- /dev/null +++ b/.gradle-docs/README.md @@ -0,0 +1,461 @@ +# Bearsampp Module Git - Gradle Build Documentation + +## Table of Contents + +- [Overview](#overview) +- [Quick Start](#quick-start) +- [Installation](#installation) +- [Build Tasks](#build-tasks) +- [Configuration](#configuration) +- [Architecture](#architecture) +- [Troubleshooting](#troubleshooting) +- [Migration Guide](#migration-guide) + +--- + +## Overview + +The Bearsampp Module Git project has been converted to a **pure Gradle build system**, replacing the legacy Ant build configuration. This provides: + +- **Modern Build System** - Native Gradle tasks and conventions +- **Better Performance** - Incremental builds and caching +- **Simplified Maintenance** - Pure Groovy/Gradle DSL +- **Enhanced Tooling** - IDE integration and dependency management +- **Cross-Platform Support** - Works on Windows, Linux, and macOS + +### Project Information + +| Property | Value | +|-------------------|------------------------------------------| +| **Project Name** | module-git | +| **Group** | com.bearsampp.modules | +| **Type** | Git Module Builder | +| **Build Tool** | Gradle 8.x+ | +| **Language** | Groovy (Gradle DSL) | + +--- + +## Quick Start + +### Prerequisites + +| Requirement | Version | Purpose | +|-------------------|---------------|------------------------------------------| +| **Java** | 8+ | Required for Gradle execution | +| **Gradle** | 8.0+ | Build automation tool | +| **7-Zip** | Latest | Archive extraction and compression | + +### Basic Commands + +```bash +# Display build information +gradle info + +# List all available tasks +gradle tasks + +# List available versions +gradle listVersions + +# Build a release (interactive) +gradle release + +# Build a specific version (non-interactive) +gradle release -PbundleVersion=2.51.2 + +# Clean build artifacts +gradle clean +``` + +--- + +## Installation + +### 1. Clone the Repository + +```bash +git clone https://github.com/bearsampp/module-git.git +cd module-git +``` + +### 2. Verify Environment + +Check that you have the required tools: + +```bash +# Check Java version +java -version + +# Check Gradle version +gradle --version + +# Check 7-Zip +7z +``` + +### 3. List Available Versions + +```bash +gradle listVersions +``` + +### 4. Build Your First Release + +```bash +# Interactive mode (prompts for version) +gradle release + +# Or specify version directly +gradle release -PbundleVersion=2.51.2 +``` + +--- + +## Build Tasks + +### Core Build Tasks + +| Task | Description | Example | +|-----------------------|--------------------------------------------------|------------------------------------------| +| `release` | Build and package release (interactive/non-interactive) | `gradle release -PbundleVersion=2.51.2` | +| `clean` | Clean build artifacts and temporary files | `gradle clean` | + +### Information Tasks + +| Task | Description | Example | +|---------------------|--------------------------------------------------|----------------------------| +| `info` | Display build configuration information | `gradle info` | +| `listVersions` | List available bundle versions in bin/ | `gradle listVersions` | + +### Task Groups + +| Group | Purpose | +|------------------|--------------------------------------------------| +| **build** | Build and package tasks | +| **help** | Help and information tasks | + +--- + +## Configuration + +### build.properties + +The main configuration file for the build: + +```properties +bundle.name = git +bundle.release = 2025.11.1 +bundle.type = tools +bundle.format = 7z +``` + +| Property | Description | Example Value | +|-------------------|--------------------------------------|----------------| +| `bundle.name` | Name of the bundle | `git` | +| `bundle.release` | Release version | `2025.11.1` | +| `bundle.type` | Type of bundle | `tools` | +| `bundle.format` | Archive format | `7z` | + +### releases.properties + +Maps Git versions to download URLs: + +```properties +2.51.2=https://github.com/Bearsampp/module-git/releases/download/2025.11.1/bearsampp-git-2.51.2-2025.11.1.7z +2.50.1=https://github.com/Bearsampp/module-git/releases/download/2025.7.10/bearsampp-git-2.50.1-2025.7.10.7z +``` + +If a version is not found in `releases.properties`, the build will check the remote untouched modules repository: +`https://raw.githubusercontent.com/Bearsampp/modules-untouched/main/modules/git.properties` + +### gradle.properties + +Gradle-specific configuration: + +```properties +# Gradle daemon configuration +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.caching=true + +# JVM settings +org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m +``` + +### Directory Structure + +``` +module-git/ +├── .gradle-docs/ # Gradle documentation +│ └── README.md # Main documentation +├── bin/ # Git version configurations +│ ├── git2.50.1/ +│ ├── git2.51.2/ +│ └── archived/ # Archived versions +│ └── git2.34.0/ +├── bearsampp-build/ # External build directory (outside repo) +│ ├── tmp/ # Temporary build files +│ │ ├── bundles_prep/tools/git/ # Prepared bundles +│ │ ├── downloads/git/ # Downloaded archives +│ │ └── extract/git/ # Extracted sources +│ └── tools/git/ # Final packaged archives +│ └── 2025.11.1/ # Release version +│ ├── bearsampp-git-2.51.2-2025.11.1.7z +│ ├── bearsampp-git-2.51.2-2025.11.1.7z.md5 +│ └── ... +├── build.gradle # Main Gradle build script +├── settings.gradle # Gradle settings +├── build.properties # Build configuration +└── releases.properties # Available Git releases +``` + +--- + +## Architecture + +### Build Process Flow + +``` +1. User runs: gradle release -PbundleVersion=2.51.2 + ↓ +2. Validate environment and version + ↓ +3. Resolve download URL from releases.properties or remote + ↓ +4. Download Git archive (or use cached version) + ↓ +5. Extract archive to tmp/extract/ + ↓ +6. Create preparation directory (tmp/bundles_prep/) + ↓ +7. Copy Git files (excluding doc/ directory) + ↓ +8. Copy custom configuration from bin/git{version}/ + - bearsampp.conf (with @RELEASE_VERSION@ replacement) + - repos.dat + - Other custom files + ↓ +9. Output prepared bundle to tmp/bundles_prep/ + ↓ +10. Package prepared folder into archive in bearsampp-build/tools/git/{bundle.release}/ + - The archive includes the top-level folder: git{version}/ +``` + +### Packaging Details + +- **Archive name format**: `bearsampp-git-{version}-{bundle.release}.{7z|zip}` +- **Location**: `bearsampp-build/tools/git/{bundle.release}/` + - Example: `bearsampp-build/tools/git/2025.11.1/bearsampp-git-2.51.2-2025.11.1.7z` +- **Content root**: The top-level folder inside the archive is `git{version}/` (e.g., `git2.51.2/`) +- **Structure**: The archive contains the Git version folder at the root with all Git files inside + +**Archive Structure Example**: +``` +bearsampp-git-2.51.2-2025.11.1.7z +└── git2.51.2/ ← Version folder at root + ├── bearsampp.conf + ├── repos.dat + ├── bin/ + │ ├── git.exe + │ ├── git-bash.exe + │ └── ... + ├── cmd/ + ├── etc/ + ├── mingw64/ + └── ... +``` + +**Verification Commands**: + +```bash +# List archive contents with 7z +7z l bearsampp-build/tools/git/2025.11.1/bearsampp-git-2.51.2-2025.11.1.7z | more + +# You should see entries beginning with: +# git2.51.2/bearsampp.conf +# git2.51.2/bin/git.exe +# git2.51.2/... + +# Extract and inspect with PowerShell +7z x bearsampp-build/tools/git/2025.11.1/bearsampp-git-2.51.2-2025.11.1.7z -o_inspect +Get-ChildItem _inspect\git2.51.2 | Select-Object Name + +# Expected output: +# bearsampp.conf +# repos.dat +# bin/ +# cmd/ +# etc/ +# ... +``` + +**Note**: This archive structure matches other Bearsampp modules where archives contain `{module}{version}/` at the root. This ensures consistency across all Bearsampp modules. + +**Hash Files**: Each archive is accompanied by hash sidecar files: +- `.md5` - MD5 checksum +- `.sha1` - SHA-1 checksum +- `.sha256` - SHA-256 checksum +- `.sha512` - SHA-512 checksum + +Example: +``` +bearsampp-build/tools/git/2025.11.1/ +├── bearsampp-git-2.51.2-2025.11.1.7z +├── bearsampp-git-2.51.2-2025.11.1.7z.md5 +├── bearsampp-git-2.51.2-2025.11.1.7z.sha1 +├── bearsampp-git-2.51.2-2025.11.1.7z.sha256 +└── bearsampp-git-2.51.2-2025.11.1.7z.sha512 +``` + +### Version Management + +Each Git version can have custom configuration files in `bin/git{version}/`: + +- **bearsampp.conf** - Module configuration (with `@RELEASE_VERSION@` placeholder) +- **repos.dat** - Repository configuration +- Any other custom files needed for the specific version + +--- + +## Troubleshooting + +### Common Issues + +#### Issue: "Version not found" + +**Symptom:** +``` +Version not found: 2.51.2 +``` + +**Solution:** +1. List available versions: `gradle listVersions` +2. Ensure the version directory exists in `bin/` or `bin/archived/` +3. Check that the version is in `releases.properties` or the remote untouched modules + +--- + +#### Issue: "7-Zip not found" + +**Symptom:** +``` +7-Zip not found. Please install 7-Zip or set 7Z_HOME environment variable. +``` + +**Solution:** +1. Install 7-Zip from https://www.7-zip.org/ +2. Or set `7Z_HOME` environment variable: + ```bash + set 7Z_HOME=C:\Program Files\7-Zip + ``` + +--- + +#### Issue: "Java version too old" + +**Symptom:** +``` +Java 8+ required +``` + +**Solution:** +1. Check Java version: `java -version` +2. Install Java 8 or higher from https://adoptium.net/ +3. Update JAVA_HOME environment variable + +--- + +#### Issue: "Download failed" + +**Symptom:** +``` +Failed to download from URL +``` + +**Solution:** +1. Check internet connection +2. Verify URL in `releases.properties` +3. Check if GitHub releases are accessible +4. Clear download cache: `gradle clean` + +--- + +### Debug Mode + +Run Gradle with debug output: + +```bash +gradle release -PbundleVersion=2.51.2 --info +gradle release -PbundleVersion=2.51.2 --debug +gradle release -PbundleVersion=2.51.2 --stacktrace +``` + +### Clean Build + +If you encounter issues, try a clean build: + +```bash +gradle clean +gradle release -PbundleVersion=2.51.2 +``` + +--- + +## Migration Guide + +### From Ant to Gradle + +The project has been fully migrated from Ant to Gradle. Here's what changed: + +#### Removed Files + +| File | Status | Replacement | +|-------------------|-----------|----------------------------| +| `build.xml` | ❌ Removed | `build.gradle` | + +#### Command Mapping + +| Ant Command | Gradle Command | +|--------------------------------------|---------------------------------------------| +| `ant release` | `gradle release` | +| `ant release -Dinput.bundle=2.51.2` | `gradle release -PbundleVersion=2.51.2` | +| `ant clean` | `gradle clean` | + +#### Key Differences + +| Aspect | Ant | Gradle | +|---------------------|------------------------------|----------------------------------| +| **Build File** | XML (build.xml) | Groovy DSL (build.gradle) | +| **Task Definition** | `` | `tasks.register('...')` | +| **Properties** | `` | `ext { ... }` | +| **Dependencies** | Manual downloads | Automatic with caching | +| **Caching** | None | Built-in incremental builds | +| **IDE Support** | Limited | Excellent (IntelliJ, Eclipse) | + +--- + +## Additional Resources + +- [Gradle Documentation](https://docs.gradle.org/) +- [Bearsampp Project](https://github.com/bearsampp/bearsampp) +- [Git for Windows](https://gitforwindows.org/) + +--- + +## Support + +For issues and questions: + +- **GitHub Issues**: https://github.com/bearsampp/module-git/issues +- **Bearsampp Issues**: https://github.com/bearsampp/bearsampp/issues +- **Documentation**: https://bearsampp.com/module/git + +--- + +**Last Updated**: 2025-01-31 +**Version**: 2025.11.1 +**Build System**: Pure Gradle (no wrapper, no Ant) + +Notes: +- This project deliberately does not ship the Gradle Wrapper. Install Gradle 8+ locally and run with `gradle ...`. +- Legacy Ant files (e.g., Eclipse `.launch` referencing `build.xml`) are deprecated and not used by the build. diff --git a/README.md b/README.md index 0a72d1a4..7c3fa2dc 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,36 @@ This is a module of [Bearsampp project](https://github.com/bearsampp/bearsampp) involving Git. +## Building + +This project uses Gradle for building. See [.gradle-docs/README.md](.gradle-docs/README.md) for complete documentation. + +### Quick Start + +```bash +# Display build information +gradle info + +# List all available tasks +gradle tasks + +# List available versions +gradle listVersions + +# Build a release (interactive) +gradle release + +# Build a specific version (non-interactive) +gradle release -PbundleVersion=2.51.2 + +# Clean build artifacts +gradle clean +``` + +### Documentation + +- **[Complete Guide](.gradle-docs/README.md)** - Full documentation including installation, configuration, and troubleshooting + ## Documentation and downloads https://bearsampp.com/module/git diff --git a/apache-reference.gradle b/apache-reference.gradle new file mode 100644 index 00000000..7a9e7f33 --- /dev/null +++ b/apache-reference.gradle @@ -0,0 +1,1356 @@ +/* + * Bearsampp Module Apache - Gradle Build + * + * Pure Gradle build configuration for Apache module packaging. + * This build script handles downloading, extracting, and packaging Apache binaries + * along with custom modules and configurations. + * + * Usage: + * gradle tasks - List all available tasks + * gradle info - Display build information + * gradle release -PbundleVersion=2.4.62 - Build release for specific version + * gradle clean - Clean build artifacts + * gradle verify - Verify build environment + */ + +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', 'apache')}" + +// 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', 'apache') + bundleRelease = buildProps.getProperty('bundle.release', '1.0.0') + bundleType = buildProps.getProperty('bundle.type', 'bins') + 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 for dependencies +repositories { + mavenCentral() +} + +// ============================================================================ +// HELPER FUNCTIONS +// ============================================================================ + +// Function to download from modules-untouched GitHub repository +def downloadFromModulesUntouched(String version) { + println "Checking modules-untouched repository on GitHub..." + + // GitHub raw content URL for modules-untouched (using main branch) + def githubBaseUrl = "https://github.com/Bearsampp/modules-untouched/raw/main/apache${version}" + + // Try to detect the archive file by checking common patterns + def possibleArchives = [ + "bearsampp-apache-${version}.7z", + "apache-${version}.7z", + "apache${version}.7z", + "bearsampp-apache-${version}.zip", + "apache-${version}.zip", + "apache${version}.zip" + ] + + def downloadDir = file(bundleTmpDownloadPath) + def extractDir = file(bundleTmpExtractPath) + downloadDir.mkdirs() + extractDir.mkdirs() + + def downloadedFile = null + def archiveUrl = null + + // Try to find which archive exists + for (archiveName in possibleArchives) { + def testUrl = "${githubBaseUrl}/${archiveName}" + println " Checking: ${testUrl}" + + try { + // Test if URL exists by attempting a HEAD request + def connection = new URL(testUrl).openConnection() + connection.setRequestMethod("HEAD") + connection.setConnectTimeout(5000) + connection.setReadTimeout(5000) + + def responseCode = connection.getResponseCode() + if (responseCode == 200) { + archiveUrl = testUrl + downloadedFile = file("${downloadDir}/modules-untouched-${archiveName}") + println " Found: ${archiveName}" + break + } + } catch (Exception e) { + // URL doesn't exist, try next + } + } + + if (!archiveUrl) { + println " Apache ${version} not found in modules-untouched repository" + return null + } + + // Download the archive if not already cached + if (!downloadedFile.exists()) { + println " Downloading from modules-untouched: ${archiveUrl}" + try { + ant.get(src: archiveUrl, dest: downloadedFile, verbose: true) + println " Download complete" + } catch (Exception e) { + println " Download failed: ${e.message}" + return null + } + } else { + println " Using cached file: ${downloadedFile.name}" + } + + // Extract the archive + println " Extracting archive..." + def extractPath = file("${extractDir}/modules-untouched-${version}") + if (extractPath.exists()) { + delete extractPath + } + extractPath.mkdirs() + + def filename = downloadedFile.name + + // Use 7zip or built-in extraction + if (filename.endsWith('.7z')) { + def sevenZipPath = find7ZipExecutable() + if (sevenZipPath) { + println " Using 7zip: ${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 -> + println " ${line}" + } + + def exitCode = process.waitFor() + if (exitCode != 0) { + println " 7zip extraction failed" + return null + } + } else { + println " 7-Zip not found, cannot extract .7z file" + return null + } + } else if (filename.endsWith('.zip')) { + copy { + from zipTree(downloadedFile) + into extractPath + } + } else { + println " Unsupported archive format: ${filename}" + return null + } + + println " Extraction complete" + + // Find the Apache directory in the extracted files + def apacheDir = findApacheDirectory(extractPath) + if (!apacheDir) { + println " Could not find Apache directory in extracted files" + return null + } + + println " Found Apache directory: ${apacheDir.name}" + println " Apache ${version} from modules-untouched ready at: ${apacheDir.absolutePath}" + + return apacheDir +} + +// Function to load remote apache.properties from modules-untouched +def loadRemoteApacheProperties() { + def remoteUrl = "https://raw.githubusercontent.com/Bearsampp/modules-untouched/main/modules/apache.properties" + + try { + println "Loading remote apache.properties from modules-untouched..." + def connection = new URL(remoteUrl).openConnection() + connection.setConnectTimeout(10000) + connection.setReadTimeout(10000) + + def remoteProps = new Properties() + connection.inputStream.withStream { stream -> + remoteProps.load(stream) + } + + println " Loaded ${remoteProps.size()} versions from remote apache.properties" + return remoteProps + } catch (Exception e) { + println " Warning: Could not load remote apache.properties: ${e.message}" + return new Properties() + } +} + +// Function to download and extract Apache binaries +def downloadAndExtractApache(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) + if (!downloadUrl) { + // Check remote apache.properties from modules-untouched + println "Version ${version} not found in releases.properties" + println "Checking remote apache.properties from modules-untouched..." + + def remoteProps = loadRemoteApacheProperties() + downloadUrl = remoteProps.getProperty(version) + + if (downloadUrl) { + println " Found version ${version} in remote apache.properties" + println " URL: ${downloadUrl}" + } else { + // Check modules-untouched GitHub repository as fallback + println "Version ${version} not found in remote apache.properties" + + def untouchedDir = downloadFromModulesUntouched(version) + if (untouchedDir) { + println "Using Apache ${version} from modules-untouched repository" + return untouchedDir + } + + throw new GradleException(""" + Version ${version} not found in releases.properties, remote apache.properties, or modules-untouched repository. + + Please either: + 1. Add the version to releases.properties with a download URL + 2. Add the version to https://github.com/Bearsampp/modules-untouched/blob/main/modules/apache.properties + 3. Upload Apache binaries to: https://github.com/Bearsampp/modules-untouched/tree/main/apache${version}/ + """.stripIndent()) + } + } + + println "Downloading Apache ${version} from:" + println " ${downloadUrl}" + + // Determine filename from URL + def filename = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1) + def downloadDir = file(bundleTmpDownloadPath) + def extractDir = file(bundleTmpExtractPath) + downloadDir.mkdirs() + extractDir.mkdirs() + + def 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 + println " Extracting archive..." + def extractPath = file("${extractDir}/${version}") + if (extractPath.exists()) { + delete extractPath + } + extractPath.mkdirs() + + // Use 7zip or built-in extraction + if (filename.endsWith('.7z')) { + // Try to use 7zip if available + def sevenZipPath = find7ZipExecutable() + if (sevenZipPath) { + println " Using 7zip: ${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() + + // Read output + process.inputStream.eachLine { line -> + println " ${line}" + } + + def exitCode = process.waitFor() + if (exitCode != 0) { + throw new GradleException("7zip extraction failed with exit code: ${exitCode}") + } + } else { + throw new GradleException("7-Zip not found. Please install 7-Zip 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 Apache directory in the extracted files + def apacheDir = findApacheDirectory(extractPath) + if (!apacheDir) { + throw new GradleException("Could not find Apache directory in extracted files") + } + + println " Found Apache directory: ${apacheDir.name}" + println " Apache ${version} ready at: ${apacheDir.absolutePath}" + + return apacheDir +} + +// 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 +} + +// Function to find Apache directory in extracted files +def findApacheDirectory(File extractPath) { + // Look for Apache directory (case-insensitive, various formats) + def apacheDirs = extractPath.listFiles()?.findAll { + it.isDirectory() && ( + it.name == 'Apache24' || + it.name == 'Apache2.4' || + it.name.toLowerCase().startsWith('apache') + ) + } + + if (apacheDirs && !apacheDirs.isEmpty()) { + // Prefer Apache24 or Apache2.4 if they exist + def preferred = apacheDirs.find { it.name == 'Apache24' || it.name == 'Apache2.4' } + return preferred ?: apacheDirs[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() && ( + it.name == 'Apache24' || + it.name == 'Apache2.4' || + it.name.toLowerCase().startsWith('apache') + ) + } + if (subDirs && !subDirs.isEmpty()) { + foundDir = subDirs[0] + } + } + } + + return foundDir +} + +// 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() +} + +// ============================================================================ +// GRADLE NATIVE 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 Apache - 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 listVersions - List available versions + gradle release -PbundleVersion=2.4.62 - Build release for version + gradle releaseAll - Build all available versions + gradle clean - Clean build artifacts + gradle verify - Verify build environment + """.stripIndent() + } +} + +// Task: Main release task +tasks.register('release') { + group = 'build' + description = 'Build release package (use -PbundleVersion=X.X.X to specify version)' + + // Capture property at configuration time to avoid deprecation warning + def bundleVersionProperty = project.findProperty('bundleVersion') + + doLast { + def versionToBuild = bundleVersionProperty + + // Interactive mode if no version specified + if (!versionToBuild) { + def versions = getAvailableVersions() + + if (versions.isEmpty()) { + throw new GradleException("No versions found in bin/ or bin/archived/ directories") + } + + println "" + println "=".multiply(70) + println "Available ${bundleName} versions:" + println "=".multiply(70) + + // Show versions with location tags + def binDir = file("${projectDir}/bin") + def archivedDir = file("${projectDir}/bin/archived") + + versions.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 "=".multiply(70) + println "" + println "Enter version number or full version string: " + 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: ${versions.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: ${versions.join(', ')} + """.stripIndent()) + } + + input = input.trim() + + // Check if input is a number (index selection) + if (input.isInteger()) { + def index = input.toInteger() - 1 + if (index >= 0 && index < versions.size()) { + versionToBuild = versions[index] + } else { + throw new GradleException("Invalid selection: ${input}. Please choose 1-${versions.size()}") + } + } else { + // Direct version string + versionToBuild = input + } + + println "" + println "Selected version: ${versionToBuild}" + } + + println "=".multiply(70) + println "Building release for ${bundleName} version ${versionToBuild}..." + println "=".multiply(70) + + // 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()) { + def allVersions = getAvailableVersions() + def availableVersionsList = allVersions.collect { + " - ${it}" + }.join('\n') ?: " (none found)" + + throw new GradleException(""" + Bundle version not found: ${bundleName}${versionToBuild} + + Available versions: + ${availableVersionsList} + """.stripIndent()) + } + + println "Bundle path: ${bundlePath}" + println "" + + // Get the untouched module source + def bundleFolder = bundlePath.name + def bundleVersion = bundleFolder.replace(bundleName, '') + + // Determine source paths - check bin directory first, then build directory + def bundleSrcDest = bundlePath + def bundleSrcFinal = null + + if (file("${bundleSrcDest}/Apache24").exists()) { + bundleSrcFinal = file("${bundleSrcDest}/Apache24") + } else if (file("${bundleSrcDest}/Apache2.4").exists()) { + bundleSrcFinal = file("${bundleSrcDest}/Apache2.4") + } else { + // Apache binaries not found in bin/ - check modules-untouched repository first + println "" + println "Apache binaries not found in bin/ directory" + + // Check modules-untouched GitHub repository + def untouchedDir = downloadFromModulesUntouched(bundleVersion) + if (untouchedDir) { + println "Using Apache ${bundleVersion} from modules-untouched repository" + bundleSrcFinal = untouchedDir + } else { + // Check if already downloaded to tmp extract path + def tmpExtractPath = file("${bundleTmpExtractPath}/${bundleVersion}") + def tmpApacheDir = findApacheDirectory(tmpExtractPath) + + if (tmpApacheDir && tmpApacheDir.exists()) { + println "Using cached Apache binaries from tmp extract directory" + bundleSrcFinal = tmpApacheDir + } else { + // Download and extract to tmp directory + println "Downloading Apache ${bundleVersion}..." + println "" + + try { + // Download and extract to tmp directory (or use modules-untouched as fallback) + bundleSrcFinal = downloadAndExtractApache(bundleVersion, file(bundleTmpExtractPath)) + } catch (Exception e) { + throw new GradleException(""" + Failed to obtain Apache binaries: ${e.message} + + You can manually: + 1. Download and extract Apache binaries to: ${bundleSrcDest}/Apache24/ + 2. Add version ${bundleVersion} to releases.properties with a download URL + 3. Upload Apache binaries to: https://github.com/Bearsampp/modules-untouched/tree/main/apache${bundleVersion}/ + """.stripIndent()) + } + } + } + } + + def httpdExe = file("${bundleSrcFinal}/bin/httpd.exe") + if (!httpdExe.exists()) { + throw new GradleException("httpd.exe not found at ${httpdExe}") + } + + println "Source folder: ${bundleSrcFinal}" + println "" + + // Prepare output directory + def apachePrepPath = file("${bundleTmpPrepPath}/${bundleName}${bundleVersion}") + if (apachePrepPath.exists()) { + delete apachePrepPath + } + apachePrepPath.mkdirs() + + // Copy readme if exists + def readmeFile = file("${bundleSrcDest}/readme_first.html") + if (readmeFile.exists()) { + copy { + from readmeFile + into apachePrepPath + } + } + + // Copy Apache files (excluding certain directories) + println "Copying Apache files..." + copy { + from bundleSrcFinal + into apachePrepPath + exclude 'cgi-bin/**' + exclude 'conf/original/**' + exclude 'conf/ssl/**' + exclude 'error/**' + exclude 'htdocs/**' + exclude 'icons/**' + exclude 'include/**' + exclude 'lib/**' + exclude 'logs/*' + exclude 'tools/**' + } + + // Copy bundle customizations + println "Copying bundle customizations..." + copy { + from bundlePath + into apachePrepPath + exclude 'Apache24/**' + exclude 'Apache2.4/**' + exclude 'readme_first.html' + } + + // Process modules if modules.properties exists + def modulesFile = file("${apachePrepPath}/modules.properties") + if (modulesFile.exists()) { + println "" + println "Processing modules..." + + def modules = new Properties() + modulesFile.withInputStream { modules.load(it) } + + def modulesContent = new StringBuilder() + + modules.each { moduleName, moduleUrl -> + println " Processing module: ${moduleName}" + + // Extract version from URL (if present) + def versionMatch = (moduleUrl =~ /apache-.*-.*-(.*)/) + def moduleVersion = versionMatch ? versionMatch[0][1] : 'unknown' + + // Download module (simulated - in real implementation would download) + // For now, we'll just document what would happen + println " URL: ${moduleUrl}" + println " Version: ${moduleVersion}" + + // Add to modules content for httpd.conf injection + modulesContent.append("#LoadModule ${moduleName}_module modules/mod_${moduleName}.so\n") + } + + // Inject modules into httpd.conf + def httpdConf = file("${bundlePath}/conf/httpd.conf") + def httpdConfBer = file("${bundlePath}/conf/httpd.conf.ber") + + if (httpdConf.exists()) { + println "" + println "Injecting modules into httpd.conf..." + def confContent = httpdConf.text.replace('@APACHE_MODULES@', modulesContent.toString()) + file("${apachePrepPath}/conf/httpd.conf").text = confContent + } + + if (httpdConfBer.exists()) { + println "Injecting modules into httpd.conf.ber..." + def confContent = httpdConfBer.text.replace('@APACHE_MODULES@', modulesContent.toString()) + file("${apachePrepPath}/conf/httpd.conf.ber").text = confContent + } + + // Remove modules.properties from output + modulesFile.delete() + } + + println "" + println "Preparing archive..." + + // Determine build output path following Bruno pattern + // bearsampp-build/{bundleType}/{bundleName}/{bundleRelease} + 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(apachePrepPath) + .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: apachePrepPath) + + 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 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('') +} + +// 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()) { + bundlePath = file("${projectDir}/bin/archived/${bundleName}${version}") + } + + if (!bundlePath.exists()) { + throw new GradleException("Bundle path not found: ${bundlePath}") + } + + println "Bundle path: ${bundlePath}" + println "" + + // Get the untouched module source + def bundleFolder = bundlePath.name + def bundleVersion = bundleFolder.replace(bundleName, '') + + // Determine source paths + def bundleSrcDest = bundlePath + def bundleSrcFinal = null + + if (file("${bundleSrcDest}/Apache24").exists()) { + bundleSrcFinal = file("${bundleSrcDest}/Apache24") + } else if (file("${bundleSrcDest}/Apache2.4").exists()) { + bundleSrcFinal = file("${bundleSrcDest}/Apache2.4") + } else { + throw new GradleException("Main folder not found in ${bundleSrcDest}") + } + + def httpdExe = file("${bundleSrcFinal}/bin/httpd.exe") + if (!httpdExe.exists()) { + throw new GradleException("httpd.exe not found at ${httpdExe}") + } + + println "Source folder: ${bundleSrcFinal}" + println "" + + // Prepare output directory + def apachePrepPath = file("${bundleTmpPrepPath}/${bundleName}${bundleVersion}") + if (apachePrepPath.exists()) { + delete apachePrepPath + } + apachePrepPath.mkdirs() + + // Copy readme if exists + def readmeFile = file("${bundleSrcDest}/readme_first.html") + if (readmeFile.exists()) { + copy { + from readmeFile + into apachePrepPath + } + } + + // Copy Apache files (excluding certain directories) + println "Copying Apache files..." + copy { + from bundleSrcFinal + into apachePrepPath + exclude 'cgi-bin/**' + exclude 'conf/original/**' + exclude 'conf/ssl/**' + exclude 'error/**' + exclude 'htdocs/**' + exclude 'icons/**' + exclude 'include/**' + exclude 'lib/**' + exclude 'logs/*' + exclude 'tools/**' + } + + // Copy bundle customizations + println "Copying bundle customizations..." + copy { + from bundlePath + into apachePrepPath + exclude 'Apache24/**' + exclude 'Apache2.4/**' + exclude 'readme_first.html' + } + + // Process modules if modules.properties exists + def modulesFile = file("${apachePrepPath}/modules.properties") + if (modulesFile.exists()) { + println "" + println "Processing modules..." + + def modules = new Properties() + modulesFile.withInputStream { modules.load(it) } + + def modulesContent = new StringBuilder() + + modules.each { moduleName, moduleUrl -> + println " Processing module: ${moduleName}" + + // Extract version from URL (if present) + def versionMatch = (moduleUrl =~ /apache-.*-.*-(.*)/) + def moduleVersion = versionMatch ? versionMatch[0][1] : 'unknown' + + println " URL: ${moduleUrl}" + println " Version: ${moduleVersion}" + + // Add to modules content for httpd.conf injection + modulesContent.append("#LoadModule ${moduleName}_module modules/mod_${moduleName}.so\n") + } + + // Inject modules into httpd.conf + def httpdConf = file("${bundlePath}/conf/httpd.conf") + def httpdConfBer = file("${bundlePath}/conf/httpd.conf.ber") + + if (httpdConf.exists()) { + println "" + println "Injecting modules into httpd.conf..." + def confContent = httpdConf.text.replace('@APACHE_MODULES@', modulesContent.toString()) + file("${apachePrepPath}/conf/httpd.conf").text = confContent + } + + if (httpdConfBer.exists()) { + println "Injecting modules into httpd.conf.ber..." + def confContent = httpdConfBer.text.replace('@APACHE_MODULES@', modulesContent.toString()) + file("${apachePrepPath}/conf/httpd.conf.ber").text = confContent + } + + // Remove modules.properties from output + modulesFile.delete() + } + + println "" + println "[SUCCESS] ${bundleName} ${version} completed" + println "Output: ${apachePrepPath}" + 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-apache..." + + 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.4.62 - 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 Apache 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 Apache modules configuration +tasks.register('checkModules') { + group = 'verification' + description = 'Check Apache modules configuration in bin directories' + + doLast { + def binDir = file("${projectDir}/bin") + if (!binDir.exists()) { + println "bin/ directory not found" + return + } + + println "\nChecking Apache modules configuration..." + println "-".multiply(80) + + def versions = binDir.listFiles() + ?.findAll { it.isDirectory() && it.name.startsWith(bundleName) } + ?.sort { it.name } ?: [] + + versions.each { versionDir -> + def modulesFile = new File(versionDir, 'modules.properties') + if (modulesFile.exists()) { + def modules = new Properties() + modulesFile.withInputStream { modules.load(it) } + println "\n${versionDir.name}:" + println " Modules file: Found (${modules.size()} modules)" + modules.each { name, url -> + println " - ${name}" + } + } else { + println "\n${versionDir.name}:" + println " Modules file: Not found" + } + } + println "-".multiply(80) + } +} + +// ============================================================================ +// BUILD LIFECYCLE HOOKS +// ============================================================================ + +gradle.taskGraph.whenReady { graph -> + println """ + ================================================================ + Bearsampp Module Apache - Gradle Build + ================================================================ + """.stripIndent() +} + +// ============================================================================ +// DEFAULT TASK +// ============================================================================ + +defaultTasks 'info' diff --git a/bin/archives/git2.34.0/bearsampp.conf b/bin/archived/git2.34.0/bearsampp.conf similarity index 100% rename from bin/archives/git2.34.0/bearsampp.conf rename to bin/archived/git2.34.0/bearsampp.conf diff --git a/bin/archives/git2.34.0/repos.dat b/bin/archived/git2.34.0/repos.dat similarity index 100% rename from bin/archives/git2.34.0/repos.dat rename to bin/archived/git2.34.0/repos.dat diff --git a/bin/archives/git2.37.0/bearsampp.conf b/bin/archived/git2.37.0/bearsampp.conf similarity index 100% rename from bin/archives/git2.37.0/bearsampp.conf rename to bin/archived/git2.37.0/bearsampp.conf diff --git a/bin/archives/git2.37.0/repos.dat b/bin/archived/git2.37.0/repos.dat similarity index 100% rename from bin/archives/git2.37.0/repos.dat rename to bin/archived/git2.37.0/repos.dat diff --git a/bin/archives/git2.37.2/bearsampp.conf b/bin/archived/git2.37.2/bearsampp.conf similarity index 100% rename from bin/archives/git2.37.2/bearsampp.conf rename to bin/archived/git2.37.2/bearsampp.conf diff --git a/bin/archives/git2.37.2/repos.dat b/bin/archived/git2.37.2/repos.dat similarity index 100% rename from bin/archives/git2.37.2/repos.dat rename to bin/archived/git2.37.2/repos.dat diff --git a/bin/archives/git2.37.3/bearsampp.conf b/bin/archived/git2.37.3/bearsampp.conf similarity index 100% rename from bin/archives/git2.37.3/bearsampp.conf rename to bin/archived/git2.37.3/bearsampp.conf diff --git a/bin/archives/git2.37.3/repos.dat b/bin/archived/git2.37.3/repos.dat similarity index 100% rename from bin/archives/git2.37.3/repos.dat rename to bin/archived/git2.37.3/repos.dat diff --git a/bin/archives/git2.38.1/bearsampp.conf b/bin/archived/git2.38.1/bearsampp.conf similarity index 100% rename from bin/archives/git2.38.1/bearsampp.conf rename to bin/archived/git2.38.1/bearsampp.conf diff --git a/bin/archives/git2.38.1/repos.dat b/bin/archived/git2.38.1/repos.dat similarity index 100% rename from bin/archives/git2.38.1/repos.dat rename to bin/archived/git2.38.1/repos.dat diff --git a/bin/archives/git2.39.1/bearsampp.conf b/bin/archived/git2.39.1/bearsampp.conf similarity index 100% rename from bin/archives/git2.39.1/bearsampp.conf rename to bin/archived/git2.39.1/bearsampp.conf diff --git a/bin/archives/git2.39.1/repos.dat b/bin/archived/git2.39.1/repos.dat similarity index 100% rename from bin/archives/git2.39.1/repos.dat rename to bin/archived/git2.39.1/repos.dat diff --git a/bin/archives/git2.40.1/bearsampp.conf b/bin/archived/git2.40.1/bearsampp.conf similarity index 100% rename from bin/archives/git2.40.1/bearsampp.conf rename to bin/archived/git2.40.1/bearsampp.conf diff --git a/bin/archives/git2.40.1/repos.dat b/bin/archived/git2.40.1/repos.dat similarity index 100% rename from bin/archives/git2.40.1/repos.dat rename to bin/archived/git2.40.1/repos.dat diff --git a/bin/archives/git2.41.0/bearsampp.conf b/bin/archived/git2.41.0/bearsampp.conf similarity index 100% rename from bin/archives/git2.41.0/bearsampp.conf rename to bin/archived/git2.41.0/bearsampp.conf diff --git a/bin/archives/git2.41.0/repos.dat b/bin/archived/git2.41.0/repos.dat similarity index 100% rename from bin/archives/git2.41.0/repos.dat rename to bin/archived/git2.41.0/repos.dat diff --git a/bin/archives/git2.42.0.2/bearsampp.conf b/bin/archived/git2.42.0.2/bearsampp.conf similarity index 100% rename from bin/archives/git2.42.0.2/bearsampp.conf rename to bin/archived/git2.42.0.2/bearsampp.conf diff --git a/bin/archives/git2.42.0.2/repos.dat b/bin/archived/git2.42.0.2/repos.dat similarity index 100% rename from bin/archives/git2.42.0.2/repos.dat rename to bin/archived/git2.42.0.2/repos.dat diff --git a/bin/archives/git2.44.0.1/bearsampp.conf b/bin/archived/git2.44.0.1/bearsampp.conf similarity index 100% rename from bin/archives/git2.44.0.1/bearsampp.conf rename to bin/archived/git2.44.0.1/bearsampp.conf diff --git a/bin/archives/git2.44.0.1/repos.dat b/bin/archived/git2.44.0.1/repos.dat similarity index 100% rename from bin/archives/git2.44.0.1/repos.dat rename to bin/archived/git2.44.0.1/repos.dat diff --git a/bin/archives/git2.45.0/bearsampp.conf b/bin/archived/git2.45.0/bearsampp.conf similarity index 100% rename from bin/archives/git2.45.0/bearsampp.conf rename to bin/archived/git2.45.0/bearsampp.conf diff --git a/bin/archives/git2.45.0/repos.dat b/bin/archived/git2.45.0/repos.dat similarity index 100% rename from bin/archives/git2.45.0/repos.dat rename to bin/archived/git2.45.0/repos.dat diff --git a/bin/archives/git2.45.1/bearsampp.conf b/bin/archived/git2.45.1/bearsampp.conf similarity index 100% rename from bin/archives/git2.45.1/bearsampp.conf rename to bin/archived/git2.45.1/bearsampp.conf diff --git a/bin/archives/git2.45.1/repos.dat b/bin/archived/git2.45.1/repos.dat similarity index 100% rename from bin/archives/git2.45.1/repos.dat rename to bin/archived/git2.45.1/repos.dat diff --git a/bin/archives/git2.45.2/bearsampp.conf b/bin/archived/git2.45.2/bearsampp.conf similarity index 100% rename from bin/archives/git2.45.2/bearsampp.conf rename to bin/archived/git2.45.2/bearsampp.conf diff --git a/bin/archives/git2.45.2/repos.dat b/bin/archived/git2.45.2/repos.dat similarity index 100% rename from bin/archives/git2.45.2/repos.dat rename to bin/archived/git2.45.2/repos.dat diff --git a/bin/archives/git2.46.0/bearsampp.conf b/bin/archived/git2.46.0/bearsampp.conf similarity index 100% rename from bin/archives/git2.46.0/bearsampp.conf rename to bin/archived/git2.46.0/bearsampp.conf diff --git a/bin/archives/git2.46.0/repos.dat b/bin/archived/git2.46.0/repos.dat similarity index 100% rename from bin/archives/git2.46.0/repos.dat rename to bin/archived/git2.46.0/repos.dat diff --git a/bin/archives/git2.47.0-rc1/bearsampp.conf b/bin/archived/git2.47.0-rc1/bearsampp.conf similarity index 100% rename from bin/archives/git2.47.0-rc1/bearsampp.conf rename to bin/archived/git2.47.0-rc1/bearsampp.conf diff --git a/bin/archives/git2.47.0-rc1/repos.dat b/bin/archived/git2.47.0-rc1/repos.dat similarity index 100% rename from bin/archives/git2.47.0-rc1/repos.dat rename to bin/archived/git2.47.0-rc1/repos.dat diff --git a/bin/archives/git2.47.0.2/bearsampp.conf b/bin/archived/git2.47.0.2/bearsampp.conf similarity index 100% rename from bin/archives/git2.47.0.2/bearsampp.conf rename to bin/archived/git2.47.0.2/bearsampp.conf diff --git a/bin/archives/git2.47.0.2/repos.dat b/bin/archived/git2.47.0.2/repos.dat similarity index 100% rename from bin/archives/git2.47.0.2/repos.dat rename to bin/archived/git2.47.0.2/repos.dat diff --git a/bin/archives/git2.47.0/bearsampp.conf b/bin/archived/git2.47.0/bearsampp.conf similarity index 100% rename from bin/archives/git2.47.0/bearsampp.conf rename to bin/archived/git2.47.0/bearsampp.conf diff --git a/bin/archives/git2.47.0/repos.dat b/bin/archived/git2.47.0/repos.dat similarity index 100% rename from bin/archives/git2.47.0/repos.dat rename to bin/archived/git2.47.0/repos.dat diff --git a/bin/archives/git2.47.1/bearsampp.conf b/bin/archived/git2.47.1/bearsampp.conf similarity index 100% rename from bin/archives/git2.47.1/bearsampp.conf rename to bin/archived/git2.47.1/bearsampp.conf diff --git a/bin/archives/git2.47.1/repos.dat b/bin/archived/git2.47.1/repos.dat similarity index 100% rename from bin/archives/git2.47.1/repos.dat rename to bin/archived/git2.47.1/repos.dat diff --git a/bin/archives/git2.48.0-rc2/bearsampp.conf b/bin/archived/git2.48.0-rc2/bearsampp.conf similarity index 100% rename from bin/archives/git2.48.0-rc2/bearsampp.conf rename to bin/archived/git2.48.0-rc2/bearsampp.conf diff --git a/bin/archives/git2.48.0-rc2/repos.dat b/bin/archived/git2.48.0-rc2/repos.dat similarity index 100% rename from bin/archives/git2.48.0-rc2/repos.dat rename to bin/archived/git2.48.0-rc2/repos.dat diff --git a/bin/archives/git2.48.1/bearsampp.conf b/bin/archived/git2.48.1/bearsampp.conf similarity index 100% rename from bin/archives/git2.48.1/bearsampp.conf rename to bin/archived/git2.48.1/bearsampp.conf diff --git a/bin/archives/git2.48.1/repos.dat b/bin/archived/git2.48.1/repos.dat similarity index 100% rename from bin/archives/git2.48.1/repos.dat rename to bin/archived/git2.48.1/repos.dat diff --git a/bin/archives/git2.49.0/bearsampp.conf b/bin/archived/git2.49.0/bearsampp.conf similarity index 100% rename from bin/archives/git2.49.0/bearsampp.conf rename to bin/archived/git2.49.0/bearsampp.conf diff --git a/bin/archives/git2.49.0/repos.dat b/bin/archived/git2.49.0/repos.dat similarity index 100% rename from bin/archives/git2.49.0/repos.dat rename to bin/archived/git2.49.0/repos.dat diff --git a/bin/archives/git2.50.0.2/bearsampp.conf b/bin/archived/git2.50.0.2/bearsampp.conf similarity index 100% rename from bin/archives/git2.50.0.2/bearsampp.conf rename to bin/archived/git2.50.0.2/bearsampp.conf diff --git a/bin/archives/git2.50.0.2/repos.dat b/bin/archived/git2.50.0.2/repos.dat similarity index 100% rename from bin/archives/git2.50.0.2/repos.dat rename to bin/archived/git2.50.0.2/repos.dat diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..8aabd7b4 --- /dev/null +++ b/build.gradle @@ -0,0 +1,742 @@ +/* + * Bearsampp Module Git - Gradle Build + * + * Pure Gradle build configuration for Git module packaging. + */ + +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', 'git')}" + +// 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', 'git') + bundleRelease = buildProps.getProperty('bundle.release', '1.0.0') + bundleType = buildProps.getProperty('bundle.type', 'tools') + bundleFormat = buildProps.getProperty('bundle.format', '7z') + + // Build paths + 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 + + // Download and extract paths + bundleTmpDownloadPath = file("${buildTmpPath}/downloads/${bundleName}").absolutePath + bundleTmpExtractPath = file("${buildTmpPath}/extract/${bundleName}").absolutePath +} + +// NOTE: Local releases.properties is deprecated for version resolution. +// As in the Bruno module, versions/URLs are sourced primarily from +// modules-untouched git.properties, with a constructed URL as fallback. +ext.releasesProps = new Properties() +if (file('releases.properties').exists()) { + file('releases.properties').withInputStream { ext.releasesProps.load(it) } +} + +// Load untouched module versions from GitHub +def loadRemoteGitProperties() { + def remoteUrl = "https://raw.githubusercontent.com/Bearsampp/modules-untouched/main/modules/git.properties" + + try { + println "Loading remote git.properties from modules-untouched..." + def connection = new URL(remoteUrl).openConnection() + connection.setConnectTimeout(10000) + connection.setReadTimeout(10000) + + def remoteProps = new Properties() + connection.inputStream.withStream { stream -> + remoteProps.load(stream) + } + + println " Loaded ${remoteProps.size()} versions from remote git.properties" + return remoteProps + } catch (Exception e) { + println " Warning: Could not load remote git.properties: ${e.message}" + return null + } +} + +// Function to download from modules-untouched repository (primary source) +def downloadFromModulesUntouched(String version, File destDir) { + println "Checking modules-untouched repository..." + + def remoteProps = loadRemoteGitProperties() + def untouchedUrl = null + + if (remoteProps) { + untouchedUrl = remoteProps.getProperty(version) + if (untouchedUrl) { + println "Found version ${version} in modules-untouched git.properties" + println "Downloading from:\n ${untouchedUrl}" + } else { + println "Version ${version} not found in modules-untouched git.properties" + println "Attempting to construct URL based on standard format..." + untouchedUrl = "https://github.com/Bearsampp/modules-untouched/releases/download/git-${version}/git-${version}-win64.7z" + println " ${untouchedUrl}" + } + } else { + println "Could not fetch git.properties, using standard URL format..." + untouchedUrl = "https://github.com/Bearsampp/modules-untouched/releases/download/git-${version}/git-${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}") + + if (!downloadedFile.exists()) { + println " Downloading to: ${downloadedFile}" + try { + new URL(untouchedUrl).withInputStream { input -> + downloadedFile.withOutputStream { output -> + def buffer = new byte[8192] + int bytesRead + while ((bytesRead = input.read(buffer)) != -1) { + output.write(buffer, 0, bytesRead) + } + } + } + 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 git.properties or matches format: git-{version}/git-{version}-win64.7z + 3. You have internet connectivity + """.stripIndent()) + } + } else { + println " Using cached file: ${downloadedFile}" + } + + return downloadedFile +} + +// 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 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() +} + +// Function to download and extract Git binaries (remote-first like Bruno) +def downloadAndExtractGit(String version, File destDir) { + // Always source versions/URLs from modules-untouched git.properties, + // falling back to the standard constructed URL when missing. + def extractDir = file(bundleTmpExtractPath) + extractDir.mkdirs() + + def downloadedFile = downloadFromModulesUntouched(version, destDir) + + // Determine filename from downloaded file + def filename = downloadedFile.name + + // Extract the archive + println " Extracting archive..." + def extractPath = file("${extractDir}/${version}") + if (extractPath.exists()) { + delete extractPath + } + extractPath.mkdirs() + + // Use 7zip or built-in extraction + // Support both plain .7z archives and 7-Zip self-extracting archives (.7z.exe) + def lowerName = filename.toLowerCase() + if (lowerName.endsWith('.7z') || lowerName.endsWith('.7z.exe')) { + def sevenZipPath = find7ZipExecutable() + if (sevenZipPath) { + println " Using 7zip: ${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("7-Zip not found. Please install 7-Zip or set 7Z_HOME environment variable.") + } + } else if (lowerName.endsWith('.zip')) { + copy { + from zipTree(downloadedFile) + into extractPath + } + } else { + throw new GradleException("Unsupported archive format: ${filename}") + } + + println " Extraction complete" + println " Git ${version} extracted to: ${extractPath.absolutePath}" + + return extractPath +} + +// Try to locate the PortableGit root directory inside an extraction folder. +// Heuristics: +// - A directory that contains either: +// usr/bin/git.exe OR mingw*/bin/git.exe OR cmd/git.exe OR git-bash.exe +// - Prefer a directory that has typical PortableGit structure: usr/, mingw*/ or cmd/ +def findGitDirectory(File extractPath) { + if (!extractPath?.exists()) return null + + def isGitRoot = { File dir -> + def paths = [ + new File(dir, 'usr/bin/git.exe'), + new File(dir, 'mingw64/bin/git.exe'), + new File(dir, 'mingw32/bin/git.exe'), + new File(dir, 'cmd/git.exe'), + new File(dir, 'bin/git.exe'), + new File(dir, 'git-bash.exe') + ] + return paths.any { it.exists() } + } + + // 1) Check the extract root itself + if (isGitRoot(extractPath)) return extractPath + + // 2) Breadth-first search subdirectories until we find a git root + def queue = new ArrayDeque() + extractPath.listFiles()?.findAll { it.isDirectory() }?.each { queue.add(it) } + + while (!queue.isEmpty()) { + def dir = queue.removeFirst() + if (isGitRoot(dir)) { + return dir + } + dir.listFiles()?.findAll { it.isDirectory() }?.each { queue.add(it) } + } + + 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('') +} + +// Task: Main release task +tasks.register('release') { + group = 'build' + description = 'Build release package (use -PbundleVersion=X.X.X to specify version)' + + // Capture property at configuration time + def bundleVersionProperty = project.findProperty('bundleVersion') + + doLast { + def versionToBuild = bundleVersionProperty + + // Interactive mode if no version specified + if (!versionToBuild) { + def versions = getAvailableVersions() + + if (versions.isEmpty()) { + throw new GradleException("No versions found in bin/ or bin/archived/ directories") + } + + println "" + println "=".multiply(70) + println "Available ${bundleName} versions:" + println "=".multiply(70) + + // Show versions with location tags + def binDir = file("${projectDir}/bin") + def archivedDir = file("${projectDir}/bin/archived") + + versions.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 "=".multiply(70) + println "" + println "Enter version number or full version string: " + println "" + + // Read 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: ${versions.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: ${versions.join(', ')} + """.stripIndent()) + } + + input = input.trim() + + // Check if input is a number (index selection) + if (input.isInteger()) { + def index = input.toInteger() - 1 + if (index >= 0 && index < versions.size()) { + versionToBuild = versions[index] + } else { + throw new GradleException("Invalid selection: ${input}. Please choose 1-${versions.size()}") + } + } else { + // Direct version string + versionToBuild = input + } + + println "" + println "Selected version: ${versionToBuild}" + } + + println "=".multiply(70) + println "Building release for ${bundleName} version ${versionToBuild}..." + println "=".multiply(70) + + // 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()) { + def allVersions = getAvailableVersions() + def availableVersionsList = allVersions.collect { + " - ${it}" + }.join('\n') ?: " (none found)" + + throw new GradleException(""" + Bundle version not found: ${bundleName}${versionToBuild} + + Available versions: + ${availableVersionsList} + """.stripIndent()) + } + + println "Bundle path: ${bundlePath}" + println "" + + // Get the untouched module source + def bundleFolder = bundlePath.name + def bundleVersion = bundleFolder.replace(bundleName, '') + + // Download and extract Git binaries + def gitExtractRoot = downloadAndExtractGit(bundleVersion, file(bundleTmpExtractPath)) + + // Locate actual PortableGit root folder (where git.exe lives) + def actualGitSrcDir = findGitDirectory(gitExtractRoot) + if (actualGitSrcDir == null) { + // Fallback: if a single subdirectory exists, use it; else fail + def children = gitExtractRoot.listFiles()?.findAll { it.isDirectory() } ?: [] + if (children.size() == 1) { + actualGitSrcDir = children[0] + println "Could not detect git root by markers; falling back to single folder: ${actualGitSrcDir.name}" + } else { + def listed = children.collect { " - ${it.name}" }.join('\n') + throw new GradleException(""" + Could not locate Git root directory after extraction. + Looked in: ${gitExtractRoot} + Subdirectories: +${listed} + Expected to find usr/bin/git.exe, mingw*/bin/git.exe, cmd/git.exe or git-bash.exe + """.stripIndent()) + } + } else { + println "Detected Git root: ${actualGitSrcDir.absolutePath}" + } + + // Prepare output directory + def gitPrepPath = file("${bundleTmpPrepPath}/${bundleFolder}") + if (gitPrepPath.exists()) { + delete gitPrepPath + } + gitPrepPath.mkdirs() + + // Copy Git files from detected source directory + println "Copying Git files from ${actualGitSrcDir} ..." + copy { + from actualGitSrcDir + into gitPrepPath + exclude 'doc/**' + } + + // Quick sanity check: ensure prep directory is not empty + def prepContents = (gitPrepPath.listFiles() ? gitPrepPath.listFiles().toList() : []) + if (prepContents.isEmpty()) { + println "[WARNING] Prep directory is empty after copy. Source was: ${actualGitSrcDir}" + // Try one more heuristic: if source contains a single directory, copy from it + def srcChildren = actualGitSrcDir.listFiles()?.findAll { it.isDirectory() } ?: [] + if (srcChildren.size() == 1) { + println "Retrying copy from nested single directory: ${srcChildren[0].name}" + copy { + from srcChildren[0] + into gitPrepPath + exclude 'doc/**' + } + prepContents = (gitPrepPath.listFiles() ? gitPrepPath.listFiles().toList() : []) + } + } + + if (prepContents.isEmpty()) { + throw new GradleException("Prep directory is empty at ${gitPrepPath}. Copy step failed.") + } + + // Copy bundle customizations + println "Copying bundle customizations..." + copy { + from bundlePath + into gitPrepPath + } + + // Replace @RELEASE_VERSION@ in bearsampp.conf + def bearsamppConf = file("${gitPrepPath}/bearsampp.conf") + if (bearsamppConf.exists()) { + def content = bearsamppConf.text + bearsamppConf.text = content.replace('@RELEASE_VERSION@', bundleRelease) + } + + println "" + println "Copying to bundles_build directory..." + def nonZipBuildPath = file("${bundleTmpBuildPath}/${bundleFolder}") + if (nonZipBuildPath.exists()) { + delete nonZipBuildPath + } + nonZipBuildPath.mkdirs() + copy { + from gitPrepPath + into nonZipBuildPath + } + println "Non-zip version available at: ${nonZipBuildPath}" + + println "" + println "Preparing archive..." + + // Determine build output path + def buildPath = file(buildBasePath) + def buildToolsPath = file("${buildPath}/${bundleType}/${bundleName}/${bundleRelease}") + buildToolsPath.mkdirs() + + // Build archive filename + def destFile = file("${buildToolsPath}/bearsampp-${bundleName}-${bundleVersion}-${bundleRelease}") + + // Compress based on format + if (bundleFormat == '7z') { + def archiveFile = file("${destFile}.7z") + if (archiveFile.exists()) { + delete archiveFile + } + + println "Compressing ${bundleName}${bundleVersion} to ${archiveFile.name}..." + + 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}" + + // To include the version folder at the root of the archive, + // run 7-Zip from the parent of the prep directory and add the folder name. + def command = [ + sevenZipExe, + 'a', + '-t7z', + archiveFile.absolutePath.toString(), + bundleFolder + ] + + def process = new ProcessBuilder(command as String[]) + .directory(file(bundleTmpPrepPath)) + .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 { + def archiveFile = file("${destFile}.zip") + if (archiveFile.exists()) { + delete archiveFile + } + + println "Compressing ${bundleName}${bundleVersion} to ${archiveFile.name}..." + + // Include the version folder at the root of the ZIP archive + ant.zip(destfile: archiveFile) { + zipfileset(dir: bundleTmpPrepPath, includes: "${bundleFolder}/**") + } + + 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: ${nonZipBuildPath}" + println "Archive: ${destFile}.${bundleFormat}" + println "=".multiply(70) + } +} + +// Task: List available bundle versions +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 { 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: Enhanced clean task +tasks.named('clean') { + group = 'build' + description = 'Clean build artifacts and temporary files' + + doLast { + def buildDir = file("${projectDir}/build") + if (buildDir.exists()) { + delete buildDir + } + + println "[SUCCESS] Build artifacts cleaned" + } +} + +// Task: Display build information +tasks.register('info') { + group = 'help' + description = 'Display build configuration information' + + def projectName = project.name + def projectVersion = project.version + def projectDescription = project.description + def bundleNameValue = bundleName + def bundleReleaseValue = bundleRelease + def bundleTypeValue = bundleType + def bundleFormatValue = bundleFormat + + doLast { + println """ + ================================================================ + Bearsampp Module Git - Build Info + ================================================================ + + Project: ${projectName} + Version: ${projectVersion} + Description: ${projectDescription} + + Bundle Properties: + Name: ${bundleNameValue} + Release: ${bundleReleaseValue} + Type: ${bundleTypeValue} + Format: ${bundleFormatValue} + + Quick Start: + gradle tasks - List all available tasks + gradle info - Show this information + gradle listVersions - List available versions + gradle release -PbundleVersion=2.51.2 - Build release for version + gradle clean - Clean build artifacts + """.stripIndent() + } +} + +defaultTasks 'info' diff --git a/build.xml b/build.xml deleted file mode 100644 index 2c0b660a..00000000 --- a/build.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..140401c6 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,9 @@ +# Gradle Configuration +org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.daemon=true +org.gradle.configureondemand=true + +# Kotlin +kotlin.code.style=official diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..e4985429 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,4 @@ +// Gradle settings (Groovy DSL) +// This project intentionally uses Groovy, not Kotlin, for build scripts. + +rootProject.name = 'module-git'