<#
.SYNOPSIS
Builds all four XFEServerManager Forge jars and writes them to dist/.
.EXAMPLE
.\build-all.cmd
.EXAMPLE
.\build-all.cmd -WithTests
.NOTES
Minecraft 1.20.1 and common tests use JDK 17. The other targets use JDK 21.
JDK discovery checks explicit parameters, versioned environment variables,
JAVA_HOME, PATH, and common Windows JDK installation directories.
#>
[CmdletBinding()]
param(
[Parameter(Position = 0)]
[string]$Version,
[string]$Java17Home,
[string]$Java21Home,
[string]$ToolJavaHome,
[switch]$WithTests,
[switch]$SkipTests,
[switch]$SkipNpmInstall,
[switch]$SkipWrapperCheck,
[switch]$NoClean,
[switch]$Offline,
[switch]$PreflightOnly
)
$ErrorActionPreference = 'Stop'
$repositoryRoot = Split-Path -Parent $PSScriptRoot
$artifactDirectory = Join-Path $repositoryRoot 'dist'
$runTests = $WithTests -and -not $SkipTests
function Write-Step {
param([string]$Message)
Write-Host "`n==> $Message" -ForegroundColor Cyan
}
function Get-Sha256 {
param([Parameter(Mandatory)][string]$Path)
$stream = [System.IO.File]::OpenRead($Path)
$algorithm = [System.Security.Cryptography.SHA256]::Create()
try {
return -join ($algorithm.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') })
}
finally {
$algorithm.Dispose()
$stream.Dispose()
}
}
function Get-GradleProperty {
param(
[Parameter(Mandatory)][string]$PropertiesFile,
[Parameter(Mandatory)][string]$Name
)
$escapedName = [regex]::Escape($Name)
$matchingLine = Get-Content -LiteralPath $PropertiesFile |
Where-Object { $_ -match "^\s*$escapedName\s*=" } |
Select-Object -First 1
if (-not $matchingLine) {
throw "Missing property '$Name' in $PropertiesFile."
}
return ($matchingLine -replace "^\s*$escapedName\s*=\s*", '').Trim()
}
function Get-JavaMajor {
param([Parameter(Mandatory)][string]$CandidateHome)
$javaExecutable = Join-Path $CandidateHome 'bin\java.exe'
$javacExecutable = Join-Path $CandidateHome 'bin\javac.exe'
if (-not (Test-Path -LiteralPath $javaExecutable -PathType Leaf) -or
-not (Test-Path -LiteralPath $javacExecutable -PathType Leaf)) {
return $null
}
$versionOutput = (& $javaExecutable --version | Out-String)
if ($LASTEXITCODE -ne 0) {
return $null
}
if ($versionOutput -match 'version\s+"1\.(\d+)') {
return [int]$Matches[1]
}
if ($versionOutput -match 'version\s+"(\d+)') {
return [int]$Matches[1]
}
if ($versionOutput -match '(?:java|openjdk)\s+(\d+)') {
return [int]$Matches[1]
}
return $null
}
function Resolve-JavaHome {
param(
[Parameter(Mandatory)][int]$Major,
[int]$MaximumMajor = $Major,
[string]$ExplicitHome
)
$candidates = [System.Collections.Generic.List[string]]::new()
if ($ExplicitHome) {
$candidates.Add($ExplicitHome)
}
foreach ($environmentName in @("JAVA_HOME_$Major", "JDK${Major}_HOME", 'JAVA_HOME')) {
$environmentValue = [Environment]::GetEnvironmentVariable($environmentName)
if ($environmentValue) {
$candidates.Add($environmentValue)
}
}
$javacOnPath = Get-Command 'javac.exe' -ErrorAction SilentlyContinue
if ($javacOnPath) {
$candidates.Add((Split-Path -Parent (Split-Path -Parent $javacOnPath.Source)))
}
$searchRoots = @(
(Join-Path $env:ProgramFiles 'Java'),
(Join-Path $env:ProgramFiles 'Eclipse Adoptium'),
(Join-Path $env:ProgramFiles 'Microsoft'),
(Join-Path $env:ProgramFiles 'Zulu'),
(Join-Path $env:ProgramFiles 'BellSoft'),
(Join-Path $env:USERPROFILE '.jdks')
)
foreach ($searchRoot in $searchRoots) {
if (Test-Path -LiteralPath $searchRoot -PathType Container) {
foreach ($directory in Get-ChildItem -LiteralPath $searchRoot -Directory -ErrorAction SilentlyContinue) {
$candidates.Add($directory.FullName)
}
}
}
$seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
foreach ($candidate in $candidates) {
try {
$resolvedCandidate = (Resolve-Path -LiteralPath $candidate -ErrorAction Stop).Path
}
catch {
continue
}
if (-not $seen.Add($resolvedCandidate)) {
continue
}
$candidateMajor = Get-JavaMajor -CandidateHome $resolvedCandidate
if ($candidateMajor -ge $Major -and $candidateMajor -le $MaximumMajor) {
return $resolvedCandidate
}
}
throw "JDK $Major was not found. Install it, set JAVA_HOME_$Major, or pass -Java${Major}Home <path>."
}
function Invoke-Checked {
param(
[Parameter(Mandatory)][string]$Executable,
[Parameter(Mandatory)][string[]]$Arguments,
[Parameter(Mandatory)][string]$FailureMessage
)
& $Executable @Arguments | Out-Host
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
throw "$FailureMessage (exit code $exitCode)."
}
}
function Initialize-LoopbackWorkaround {
param(
[Parameter(Mandatory)][string]$CompilerJavaHome,
[Parameter(Mandatory)][string[]]$JavaHomes
)
$agentProject = Join-Path $repositoryRoot 'build-tools\loopback-agent'
$agentBuild = Join-Path $repositoryRoot 'build\loopback-agent'
$agentClasses = Join-Path $agentBuild 'classes'
$sourceFile = Join-Path $agentProject 'src\com\xfestudio\xfeservermanager\tooling\ForceTcpLoopbackAgent.java'
$manifestFile = Join-Path $agentProject 'MANIFEST.MF'
$agentJar = Join-Path $agentBuild 'xfesm-force-tcp-loopback-agent.jar'
New-Item -ItemType Directory -Path $agentClasses -Force | Out-Null
Invoke-Checked -Executable (Join-Path $CompilerJavaHome 'bin\javac.exe') -Arguments @(
'--release', '17',
'-d', $agentClasses,
$sourceFile
) -FailureMessage 'Unable to compile the loopback compatibility probe'
Invoke-Checked -Executable (Join-Path $CompilerJavaHome 'bin\jar.exe') -Arguments @(
'--create',
'--file', $agentJar,
'--manifest', $manifestFile,
'-C', $agentClasses,
'.'
) -FailureMessage 'Unable to package the loopback compatibility agent'
$probeClass = 'com.xfestudio.xfeservermanager.tooling.ForceTcpLoopbackAgent'
foreach ($javaHome in ($JavaHomes | Select-Object -Unique)) {
& (Join-Path $javaHome 'bin\java.exe') -cp $agentClasses $probeClass
if ($LASTEXITCODE -ne 0) {
return $agentJar
}
}
return $null
}
function Invoke-WithJava {
param(
[Parameter(Mandatory)][string]$JavaHome,
[Parameter(Mandatory)][scriptblock]$Action
)
$previousJavaHome = $env:JAVA_HOME
$previousUniversalJavaOptions = $env:_JAVA_OPTIONS
$previousPath = $env:Path
try {
$env:JAVA_HOME = $JavaHome
if ($loopbackAgentPath) {
$agentOption = "-javaagent:`"$loopbackAgentPath`""
$env:_JAVA_OPTIONS = if ($previousUniversalJavaOptions) {
"$previousUniversalJavaOptions $agentOption"
}
else {
$agentOption
}
}
$env:Path = "$(Join-Path $JavaHome 'bin');$previousPath"
& $Action
}
finally {
$env:JAVA_HOME = $previousJavaHome
$env:_JAVA_OPTIONS = $previousUniversalJavaOptions
$env:Path = $previousPath
}
}
function Invoke-Gradle {
param(
[Parameter(Mandatory)][string]$ProjectDirectory,
[Parameter(Mandatory)][string]$JavaHome,
[Parameter(Mandatory)][string[]]$Tasks,
[string[]]$Properties = @()
)
$wrapper = Join-Path $ProjectDirectory 'gradlew.bat'
if (-not (Test-Path -LiteralPath $wrapper -PathType Leaf)) {
throw "Gradle wrapper not found: $wrapper"
}
$arguments = [System.Collections.Generic.List[string]]::new()
$arguments.Add('-p')
$arguments.Add($ProjectDirectory)
$arguments.Add('--no-daemon')
$arguments.Add('--stacktrace')
$arguments.Add('--no-configuration-cache')
# ForgeGradle redirects Java outputs below build/sourceSets. Reusing an incremental build-cache
# entry after clean can restore only the changed classes and produce an incomplete release jar.
$arguments.Add('--no-build-cache')
if ($Offline) {
$arguments.Add('--offline')
}
foreach ($task in $Tasks) {
$arguments.Add($task)
}
foreach ($property in $Properties) {
$arguments.Add($property)
}
Invoke-WithJava -JavaHome $JavaHome -Action {
Invoke-Checked -Executable $wrapper -Arguments $arguments.ToArray() -FailureMessage "Gradle failed in $ProjectDirectory"
}
}
function Build-ForgePlatform {
param(
[Parameter(Mandatory)][string]$DirectoryName,
[Parameter(Mandatory)][string]$MinecraftVersion,
[Parameter(Mandatory)][string]$JavaHome
)
$platformDirectory = Join-Path $repositoryRoot $DirectoryName
$tasks = [System.Collections.Generic.List[string]]::new()
if (-not $NoClean) {
$tasks.Add('clean')
}
if ($runTests) {
$tasks.Add('check')
}
$tasks.Add('assemble')
Write-Step "Building Minecraft $MinecraftVersion with JDK $(Get-JavaMajor -CandidateHome $JavaHome)"
Invoke-Gradle -ProjectDirectory $platformDirectory -JavaHome $JavaHome -Tasks $tasks.ToArray() -Properties $forgeBuildProperties
$jarName = "xfeservermanager-forge-$MinecraftVersion-$resolvedVersion.jar"
$sourceJar = Join-Path $platformDirectory "build\libs\$jarName"
if (-not (Test-Path -LiteralPath $sourceJar -PathType Leaf)) {
throw "Expected artifact was not produced: $sourceJar"
}
$destinationJar = Join-Path $artifactDirectory $jarName
Copy-Item -LiteralPath $sourceJar -Destination $destinationJar -Force
return Get-Item -LiteralPath $destinationJar
}
Write-Step 'Running build preflight'
$platformDirectories = @(
'platform\forge-1.20.1',
'platform\forge-1.20.6',
'platform\forge-1.21.1',
'platform\forge-1.21.11'
)
$configuredVersions = @($platformDirectories | ForEach-Object {
Get-GradleProperty -PropertiesFile (Join-Path $repositoryRoot "$_\gradle.properties") -Name 'mod_version'
} | Select-Object -Unique)
if ([string]::IsNullOrWhiteSpace($Version)) {
if ($configuredVersions.Count -ne 1) {
throw "The four platform gradle.properties files must use one mod_version. Found: $($configuredVersions -join ', ')"
}
$resolvedVersion = $configuredVersions[0]
$versionProperties = @()
}
else {
if ($Version -notmatch '^[0-9A-Za-z][0-9A-Za-z.+_-]*$') {
throw "Invalid version '$Version'."
}
$resolvedVersion = $Version
$versionProperties = @("-Pmod_version=$resolvedVersion")
}
$resolvedJava17 = Resolve-JavaHome -Major 17 -ExplicitHome $Java17Home
$resolvedJava21 = Resolve-JavaHome -Major 21 -ExplicitHome $Java21Home
try {
$resolvedToolJava = Resolve-JavaHome -Major 25 -MaximumMajor 99 -ExplicitHome $ToolJavaHome
}
catch {
throw 'ForgeGradle 7 requires a JDK 25+ runtime for Mavenizer. Install JDK 25 or newer, or pass -ToolJavaHome <path>.'
}
$toolJavaMajor = Get-JavaMajor -CandidateHome $resolvedToolJava
$forgeBuildProperties = @($versionProperties) + @("-Pxfesm_mavenizer_java_version=$toolJavaMajor")
$loopbackAgentPath = Initialize-LoopbackWorkaround -CompilerJavaHome $resolvedJava17 -JavaHomes @(
$resolvedJava17,
$resolvedJava21,
$resolvedToolJava
)
$npmCommand = Get-Command 'npm.cmd' -ErrorAction SilentlyContinue
if (-not $npmCommand) {
$npmCommand = Get-Command 'npm' -ErrorAction SilentlyContinue
}
if (-not $npmCommand) {
throw 'npm was not found on PATH. Install Node.js before building the embedded Web UI.'
}
Write-Host "JDK 17: $resolvedJava17"
Write-Host "JDK 21: $resolvedJava21"
Write-Host "Build tool JDK: $resolvedToolJava"
Write-Host "Loopback mode: $(if ($loopbackAgentPath) { 'TCP compatibility fallback' } else { 'system default' })"
Write-Host "npm: $($npmCommand.Source)"
Write-Host "Version: $resolvedVersion$(if ($Version) { ' (command-line override)' } else { ' (gradle.properties)' })"
if (-not $SkipWrapperCheck) {
$wrapperCheck = Join-Path $PSScriptRoot 'verify-wrappers.ps1'
& $wrapperCheck
}
if ($PreflightOnly) {
Write-Host "`nPreflight completed successfully." -ForegroundColor Green
exit 0
}
New-Item -ItemType Directory -Path $artifactDirectory -Force | Out-Null
Get-ChildItem -LiteralPath $artifactDirectory -Filter 'xfeservermanager-forge-*.jar' -File -ErrorAction SilentlyContinue |
Remove-Item -Force
$checksumFile = Join-Path $artifactDirectory 'SHA256SUMS'
if (Test-Path -LiteralPath $checksumFile -PathType Leaf) {
Remove-Item -LiteralPath $checksumFile -Force
}
$webUiDirectory = Join-Path $repositoryRoot 'web-ui'
Write-Step 'Building embedded Web UI'
if (-not $SkipNpmInstall) {
Invoke-Checked -Executable $npmCommand.Source -Arguments @('ci', '--prefix', $webUiDirectory) -FailureMessage 'npm ci failed'
}
elseif (-not (Test-Path -LiteralPath (Join-Path $webUiDirectory 'node_modules') -PathType Container)) {
throw '-SkipNpmInstall was used, but web-ui/node_modules does not exist.'
}
if ($runTests) {
Invoke-Checked -Executable $npmCommand.Source -Arguments @('test', '--prefix', $webUiDirectory) -FailureMessage 'Web UI tests failed'
}
Invoke-Checked -Executable $npmCommand.Source -Arguments @('run', 'build', '--prefix', $webUiDirectory) -FailureMessage 'Web UI build failed'
if ($runTests) {
Write-Step 'Checking common Java modules with JDK 17'
Invoke-Gradle -ProjectDirectory $repositoryRoot -JavaHome $resolvedJava17 -Tasks @('check')
}
$artifacts = [System.Collections.Generic.List[System.IO.FileInfo]]::new()
$artifacts.Add((Build-ForgePlatform -DirectoryName 'platform\forge-1.20.1' -MinecraftVersion '1.20.1' -JavaHome $resolvedJava17))
$artifacts.Add((Build-ForgePlatform -DirectoryName 'platform\forge-1.20.6' -MinecraftVersion '1.20.6' -JavaHome $resolvedJava21))
$artifacts.Add((Build-ForgePlatform -DirectoryName 'platform\forge-1.21.1' -MinecraftVersion '1.21.1' -JavaHome $resolvedJava21))
$artifacts.Add((Build-ForgePlatform -DirectoryName 'platform\forge-1.21.11' -MinecraftVersion '1.21.11' -JavaHome $resolvedJava21))
$checksumLines = foreach ($artifact in $artifacts) {
$hash = Get-Sha256 -Path $artifact.FullName
"$hash $($artifact.Name)"
}
Set-Content -LiteralPath $checksumFile -Value $checksumLines -Encoding ascii
Write-Step "Build complete: $artifactDirectory"
$artifacts | ForEach-Object {
$hash = Get-Sha256 -Path $_.FullName
[PSCustomObject]@{
Jar = $_.Name
SizeMiB = [math]::Round($_.Length / 1MB, 2)
SHA256 = $hash
}
} | Format-Table -AutoSize
Write-Host "Checksums: $checksumFile" -ForegroundColor Green
<#
.SYNOPSIS
Builds all four XFEServerManager Forge jars and writes them to dist/.
.EXAMPLE
.\build-all.cmd
.EXAMPLE
.\build-all.cmd -WithTests
.NOTES
Minecraft 1.20.1 and common tests use JDK 17. The other targets use JDK 21.
JDK discovery checks explicit parameters, versioned environment variables,
JAVA_HOME, PATH, and common Windows JDK installation directories.
#>
[CmdletBinding()]
param(
[Parameter(Position = 0)]
[string]$Version,
[string]$Java17Home,
[string]$Java21Home,
[string]$ToolJavaHome,
[switch]$WithTests,
[switch]$SkipTests,
[switch]$SkipNpmInstall,
[switch]$SkipWrapperCheck,
[switch]$NoClean,
[switch]$Offline,
[switch]$PreflightOnly
)
$ErrorActionPreference = 'Stop'
$repositoryRoot = Split-Path -Parent $PSScriptRoot
$artifactDirectory = Join-Path $repositoryRoot 'dist'
$runTests = $WithTests -and -not $SkipTests
function Write-Step {
param([string]$Message)
Write-Host "`n==> $Message" -ForegroundColor Cyan
}
function Get-Sha256 {
param([Parameter(Mandatory)][string]$Path)
$stream = [System.IO.File]::OpenRead($Path)
$algorithm = [System.Security.Cryptography.SHA256]::Create()
try {
return -join ($algorithm.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') })
}
finally {
$algorithm.Dispose()
$stream.Dispose()
}
}
function Get-GradleProperty {
param(
[Parameter(Mandatory)][string]$PropertiesFile,
[Parameter(Mandatory)][string]$Name
)
$escapedName = [regex]::Escape($Name)
$matchingLine = Get-Content -LiteralPath $PropertiesFile |
Where-Object { $_ -match "^\s*$escapedName\s*=" } |
Select-Object -First 1
if (-not $matchingLine) {
throw "Missing property '$Name' in $PropertiesFile."
}
return ($matchingLine -replace "^\s*$escapedName\s*=\s*", '').Trim()
}
function Get-JavaMajor {
param([Parameter(Mandatory)][string]$CandidateHome)
$javaExecutable = Join-Path $CandidateHome 'bin\java.exe'
$javacExecutable = Join-Path $CandidateHome 'bin\javac.exe'
if (-not (Test-Path -LiteralPath $javaExecutable -PathType Leaf) -or
-not (Test-Path -LiteralPath $javacExecutable -PathType Leaf)) {
return $null
}
$versionOutput = (& $javaExecutable --version | Out-String)
if ($LASTEXITCODE -ne 0) {
return $null
}
if ($versionOutput -match 'version\s+"1\.(\d+)') {
return [int]$Matches[1]
}
if ($versionOutput -match 'version\s+"(\d+)') {
return [int]$Matches[1]
}
if ($versionOutput -match '(?:java|openjdk)\s+(\d+)') {
return [int]$Matches[1]
}
return $null
}
function Resolve-JavaHome {
param(
[Parameter(Mandatory)][int]$Major,
[int]$MaximumMajor = $Major,
[string]$ExplicitHome
)
$candidates = [System.Collections.Generic.List[string]]::new()
if ($ExplicitHome) {
$candidates.Add($ExplicitHome)
}
foreach ($environmentName in @("JAVA_HOME_$Major", "JDK${Major}_HOME", 'JAVA_HOME')) {
$environmentValue = [Environment]::GetEnvironmentVariable($environmentName)
if ($environmentValue) {
$candidates.Add($environmentValue)
}
}
$javacOnPath = Get-Command 'javac.exe' -ErrorAction SilentlyContinue
if ($javacOnPath) {
$candidates.Add((Split-Path -Parent (Split-Path -Parent $javacOnPath.Source)))
}
$searchRoots = @(
(Join-Path $env:ProgramFiles 'Java'),
(Join-Path $env:ProgramFiles 'Eclipse Adoptium'),
(Join-Path $env:ProgramFiles 'Microsoft'),
(Join-Path $env:ProgramFiles 'Zulu'),
(Join-Path $env:ProgramFiles 'BellSoft'),
(Join-Path $env:USERPROFILE '.jdks')
)
foreach ($searchRoot in $searchRoots) {
if (Test-Path -LiteralPath $searchRoot -PathType Container) {
foreach ($directory in Get-ChildItem -LiteralPath $searchRoot -Directory -ErrorAction SilentlyContinue) {
$candidates.Add($directory.FullName)
}
}
}
$seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
foreach ($candidate in $candidates) {
try {
$resolvedCandidate = (Resolve-Path -LiteralPath $candidate -ErrorAction Stop).Path
}
catch {
continue
}
if (-not $seen.Add($resolvedCandidate)) {
continue
}
$candidateMajor = Get-JavaMajor -CandidateHome $resolvedCandidate
if ($candidateMajor -ge $Major -and $candidateMajor -le $MaximumMajor) {
return $resolvedCandidate
}
}
throw "JDK $Major was not found. Install it, set JAVA_HOME_$Major, or pass -Java${Major}Home <path>."
}
function Invoke-Checked {
param(
[Parameter(Mandatory)][string]$Executable,
[Parameter(Mandatory)][string[]]$Arguments,
[Parameter(Mandatory)][string]$FailureMessage
)
& $Executable @Arguments | Out-Host
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
throw "$FailureMessage (exit code $exitCode)."
}
}
function Initialize-LoopbackWorkaround {
param(
[Parameter(Mandatory)][string]$CompilerJavaHome,
[Parameter(Mandatory)][string[]]$JavaHomes
)
$agentProject = Join-Path $repositoryRoot 'build-tools\loopback-agent'
$agentBuild = Join-Path $repositoryRoot 'build\loopback-agent'
$agentClasses = Join-Path $agentBuild 'classes'
$sourceFile = Join-Path $agentProject 'src\com\xfestudio\xfeservermanager\tooling\ForceTcpLoopbackAgent.java'
$manifestFile = Join-Path $agentProject 'MANIFEST.MF'
$agentJar = Join-Path $agentBuild 'xfesm-force-tcp-loopback-agent.jar'
New-Item -ItemType Directory -Path $agentClasses -Force | Out-Null
Invoke-Checked -Executable (Join-Path $CompilerJavaHome 'bin\javac.exe') -Arguments @(
'--release', '17',
'-d', $agentClasses,
$sourceFile
) -FailureMessage 'Unable to compile the loopback compatibility probe'
Invoke-Checked -Executable (Join-Path $CompilerJavaHome 'bin\jar.exe') -Arguments @(
'--create',
'--file', $agentJar,
'--manifest', $manifestFile,
'-C', $agentClasses,
'.'
) -FailureMessage 'Unable to package the loopback compatibility agent'
$probeClass = 'com.xfestudio.xfeservermanager.tooling.ForceTcpLoopbackAgent'
foreach ($javaHome in ($JavaHomes | Select-Object -Unique)) {
& (Join-Path $javaHome 'bin\java.exe') -cp $agentClasses $probeClass
if ($LASTEXITCODE -ne 0) {
return $agentJar
}
}
return $null
}
function Invoke-WithJava {
param(
[Parameter(Mandatory)][string]$JavaHome,
[Parameter(Mandatory)][scriptblock]$Action
)
$previousJavaHome = $env:JAVA_HOME
$previousUniversalJavaOptions = $env:_JAVA_OPTIONS
$previousPath = $env:Path
try {
$env:JAVA_HOME = $JavaHome
if ($loopbackAgentPath) {
$agentOption = "-javaagent:`"$loopbackAgentPath`""
$env:_JAVA_OPTIONS = if ($previousUniversalJavaOptions) {
"$previousUniversalJavaOptions $agentOption"
}
else {
$agentOption
}
}
$env:Path = "$(Join-Path $JavaHome 'bin');$previousPath"
& $Action
}
finally {
$env:JAVA_HOME = $previousJavaHome
$env:_JAVA_OPTIONS = $previousUniversalJavaOptions
$env:Path = $previousPath
}
}
function Invoke-Gradle {
param(
[Parameter(Mandatory)][string]$ProjectDirectory,
[Parameter(Mandatory)][string]$JavaHome,
[Parameter(Mandatory)][string[]]$Tasks,
[string[]]$Properties = @()
)
$wrapper = Join-Path $ProjectDirectory 'gradlew.bat'
if (-not (Test-Path -LiteralPath $wrapper -PathType Leaf)) {
throw "Gradle wrapper not found: $wrapper"
}
$arguments = [System.Collections.Generic.List[string]]::new()
$arguments.Add('-p')
$arguments.Add($ProjectDirectory)
$arguments.Add('--no-daemon')
$arguments.Add('--stacktrace')
$arguments.Add('--no-configuration-cache')
# ForgeGradle redirects Java outputs below build/sourceSets. Reusing an incremental build-cache
# entry after clean can restore only the changed classes and produce an incomplete release jar.
$arguments.Add('--no-build-cache')
if ($Offline) {
$arguments.Add('--offline')
}
foreach ($task in $Tasks) {
$arguments.Add($task)
}
foreach ($property in $Properties) {
$arguments.Add($property)
}
Invoke-WithJava -JavaHome $JavaHome -Action {
Invoke-Checked -Executable $wrapper -Arguments $arguments.ToArray() -FailureMessage "Gradle failed in $ProjectDirectory"
}
}
function Build-ForgePlatform {
param(
[Parameter(Mandatory)][string]$DirectoryName,
[Parameter(Mandatory)][string]$MinecraftVersion,
[Parameter(Mandatory)][string]$JavaHome
)
$platformDirectory = Join-Path $repositoryRoot $DirectoryName
$tasks = [System.Collections.Generic.List[string]]::new()
if (-not $NoClean) {
$tasks.Add('clean')
}
if ($runTests) {
$tasks.Add('check')
}
$tasks.Add('assemble')
Write-Step "Building Minecraft $MinecraftVersion with JDK $(Get-JavaMajor -CandidateHome $JavaHome)"
Invoke-Gradle -ProjectDirectory $platformDirectory -JavaHome $JavaHome -Tasks $tasks.ToArray() -Properties $forgeBuildProperties
$jarName = "xfeservermanager-forge-$MinecraftVersion-$resolvedVersion.jar"
$sourceJar = Join-Path $platformDirectory "build\libs\$jarName"
if (-not (Test-Path -LiteralPath $sourceJar -PathType Leaf)) {
throw "Expected artifact was not produced: $sourceJar"
}
$destinationJar = Join-Path $artifactDirectory $jarName
Copy-Item -LiteralPath $sourceJar -Destination $destinationJar -Force
return Get-Item -LiteralPath $destinationJar
}
Write-Step 'Running build preflight'
$platformDirectories = @(
'platform\forge-1.20.1',
'platform\forge-1.20.6',
'platform\forge-1.21.1',
'platform\forge-1.21.11'
)
$configuredVersions = @($platformDirectories | ForEach-Object {
Get-GradleProperty -PropertiesFile (Join-Path $repositoryRoot "$_\gradle.properties") -Name 'mod_version'
} | Select-Object -Unique)
if ([string]::IsNullOrWhiteSpace($Version)) {
if ($configuredVersions.Count -ne 1) {
throw "The four platform gradle.properties files must use one mod_version. Found: $($configuredVersions -join ', ')"
}
$resolvedVersion = $configuredVersions[0]
$versionProperties = @()
}
else {
if ($Version -notmatch '^[0-9A-Za-z][0-9A-Za-z.+_-]*$') {
throw "Invalid version '$Version'."
}
$resolvedVersion = $Version
$versionProperties = @("-Pmod_version=$resolvedVersion")
}
$resolvedJava17 = Resolve-JavaHome -Major 17 -ExplicitHome $Java17Home
$resolvedJava21 = Resolve-JavaHome -Major 21 -ExplicitHome $Java21Home
try {
$resolvedToolJava = Resolve-JavaHome -Major 25 -MaximumMajor 99 -ExplicitHome $ToolJavaHome
}
catch {
throw 'ForgeGradle 7 requires a JDK 25+ runtime for Mavenizer. Install JDK 25 or newer, or pass -ToolJavaHome <path>.'
}
$toolJavaMajor = Get-JavaMajor -CandidateHome $resolvedToolJava
$forgeBuildProperties = @($versionProperties) + @("-Pxfesm_mavenizer_java_version=$toolJavaMajor")
$loopbackAgentPath = Initialize-LoopbackWorkaround -CompilerJavaHome $resolvedJava17 -JavaHomes @(
$resolvedJava17,
$resolvedJava21,
$resolvedToolJava
)
$npmCommand = Get-Command 'npm.cmd' -ErrorAction SilentlyContinue
if (-not $npmCommand) {
$npmCommand = Get-Command 'npm' -ErrorAction SilentlyContinue
}
if (-not $npmCommand) {
throw 'npm was not found on PATH. Install Node.js before building the embedded Web UI.'
}
Write-Host "JDK 17: $resolvedJava17"
Write-Host "JDK 21: $resolvedJava21"
Write-Host "Build tool JDK: $resolvedToolJava"
Write-Host "Loopback mode: $(if ($loopbackAgentPath) { 'TCP compatibility fallback' } else { 'system default' })"
Write-Host "npm: $($npmCommand.Source)"
Write-Host "Version: $resolvedVersion$(if ($Version) { ' (command-line override)' } else { ' (gradle.properties)' })"
if (-not $SkipWrapperCheck) {
$wrapperCheck = Join-Path $PSScriptRoot 'verify-wrappers.ps1'
& $wrapperCheck
}
if ($PreflightOnly) {
Write-Host "`nPreflight completed successfully." -ForegroundColor Green
exit 0
}
New-Item -ItemType Directory -Path $artifactDirectory -Force | Out-Null
Get-ChildItem -LiteralPath $artifactDirectory -Filter 'xfeservermanager-forge-*.jar' -File -ErrorAction SilentlyContinue |
Remove-Item -Force
$checksumFile = Join-Path $artifactDirectory 'SHA256SUMS'
if (Test-Path -LiteralPath $checksumFile -PathType Leaf) {
Remove-Item -LiteralPath $checksumFile -Force
}
$webUiDirectory = Join-Path $repositoryRoot 'web-ui'
Write-Step 'Building embedded Web UI'
if (-not $SkipNpmInstall) {
Invoke-Checked -Executable $npmCommand.Source -Arguments @('ci', '--prefix', $webUiDirectory) -FailureMessage 'npm ci failed'
}
elseif (-not (Test-Path -LiteralPath (Join-Path $webUiDirectory 'node_modules') -PathType Container)) {
throw '-SkipNpmInstall was used, but web-ui/node_modules does not exist.'
}
if ($runTests) {
Invoke-Checked -Executable $npmCommand.Source -Arguments @('test', '--prefix', $webUiDirectory) -FailureMessage 'Web UI tests failed'
}
Invoke-Checked -Executable $npmCommand.Source -Arguments @('run', 'build', '--prefix', $webUiDirectory) -FailureMessage 'Web UI build failed'
if ($runTests) {
Write-Step 'Checking common Java modules with JDK 17'
Invoke-Gradle -ProjectDirectory $repositoryRoot -JavaHome $resolvedJava17 -Tasks @('check')
}
$artifacts = [System.Collections.Generic.List[System.IO.FileInfo]]::new()
$artifacts.Add((Build-ForgePlatform -DirectoryName 'platform\forge-1.20.1' -MinecraftVersion '1.20.1' -JavaHome $resolvedJava17))
$artifacts.Add((Build-ForgePlatform -DirectoryName 'platform\forge-1.20.6' -MinecraftVersion '1.20.6' -JavaHome $resolvedJava21))
$artifacts.Add((Build-ForgePlatform -DirectoryName 'platform\forge-1.21.1' -MinecraftVersion '1.21.1' -JavaHome $resolvedJava21))
$artifacts.Add((Build-ForgePlatform -DirectoryName 'platform\forge-1.21.11' -MinecraftVersion '1.21.11' -JavaHome $resolvedJava21))
$checksumLines = foreach ($artifact in $artifacts) {
$hash = Get-Sha256 -Path $artifact.FullName
"$hash $($artifact.Name)"
}
Set-Content -LiteralPath $checksumFile -Value $checksumLines -Encoding ascii
Write-Step "Build complete: $artifactDirectory"
$artifacts | ForEach-Object {
$hash = Get-Sha256 -Path $_.FullName
[PSCustomObject]@{
Jar = $_.Name
SizeMiB = [math]::Round($_.Length / 1MB, 2)
SHA256 = $hash
}
} | Format-Table -AutoSize
Write-Host "Checksums: $checksumFile" -ForegroundColor Green