XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEServerManager

【Java】我的世界XFE服务器管理器

公开
关注 0 Fork 0 Star 0
UTF-8
<#
.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