feat: jlink打包脚本 - 创建最小JRE发行版(Windows/Linux/macOS)
- packaging/build-dist.ps1: PowerShell跨平台构建脚本 - packaging/build-dist.sh: Bash构建脚本 - BUILD.md: 完整构建安装指南 - 自动jdeps分析 + jlink精简JRE(~49MB)+ fat jar + 启动脚本 - 发行包约120MB,免安装JDK直接运行 - 删除旧run.bat/run.ps1 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env pwsh
|
||||
<#
|
||||
.SYNOPSIS
|
||||
构建 Claude Code Java 发行版(jlink 最小 JRE + fat jar)
|
||||
.DESCRIPTION
|
||||
生成可分发包:bin/ 启动脚本 + lib/ fat jar + runtime/ 精简 JRE
|
||||
用户只需将 bin/ 目录加入 PATH 即可使用 claude-code 命令
|
||||
.PARAMETER JavaHome
|
||||
JDK 25 安装路径(默认使用 JAVA_HOME 环境变量)
|
||||
.PARAMETER OutputDir
|
||||
输出目录(默认 dist/)
|
||||
.PARAMETER SkipBuild
|
||||
跳过 Maven 构建(使用已存在的 jar)
|
||||
.EXAMPLE
|
||||
.\packaging\build-dist.ps1
|
||||
.\packaging\build-dist.ps1 -JavaHome "C:\Dev\jdk-25.0.2"
|
||||
.\packaging\build-dist.ps1 -SkipBuild
|
||||
#>
|
||||
param(
|
||||
[string]$JavaHome = $env:JAVA_HOME,
|
||||
[string]$OutputDir = "dist",
|
||||
[switch]$SkipBuild
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
||||
|
||||
# ─── 验证环境 ───
|
||||
$javaExe = if ($IsWindows -or $env:OS -eq "Windows_NT") { "java.exe" } else { "java" }
|
||||
$jlinkExe = if ($IsWindows -or $env:OS -eq "Windows_NT") { "jlink.exe" } else { "jlink" }
|
||||
$jdepsExe = if ($IsWindows -or $env:OS -eq "Windows_NT") { "jdeps.exe" } else { "jdeps" }
|
||||
|
||||
if (-not $JavaHome) {
|
||||
Write-Error "JAVA_HOME not set. Use -JavaHome parameter or set JAVA_HOME environment variable."
|
||||
exit 1
|
||||
}
|
||||
$javaBinPath = Join-Path $JavaHome "bin"
|
||||
if (-not (Test-Path (Join-Path $javaBinPath $javaExe))) {
|
||||
Write-Error "JDK not found at $JavaHome. Ensure JDK 25 is installed."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$env:JAVA_HOME = $JavaHome
|
||||
$env:PATH = "$javaBinPath$([IO.Path]::PathSeparator)$env:PATH"
|
||||
|
||||
$javaVersion = & (Join-Path $javaBinPath $javaExe) --version 2>&1 | Select-Object -First 1
|
||||
Write-Host ""
|
||||
Write-Host " Claude Code Java - Distribution Builder" -ForegroundColor Cyan
|
||||
Write-Host " ========================================" -ForegroundColor Cyan
|
||||
Write-Host " JDK: $javaVersion" -ForegroundColor Gray
|
||||
Write-Host " Output: $OutputDir" -ForegroundColor Gray
|
||||
|
||||
# ─── 检测平台 ───
|
||||
$isWin = $IsWindows -or ($env:OS -eq "Windows_NT")
|
||||
$isMac = $IsMacOS
|
||||
if ($isWin) { $platform = "windows-x64" }
|
||||
elseif ($isMac) {
|
||||
$arch = & uname -m 2>&1
|
||||
$platform = if ($arch -match "arm64") { "macos-aarch64" } else { "macos-x64" }
|
||||
} else {
|
||||
$arch = & uname -m 2>&1
|
||||
$platform = if ($arch -match "aarch64") { "linux-aarch64" } else { "linux-x64" }
|
||||
}
|
||||
Write-Host " Platform: $platform" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
|
||||
# ─── Step 1: Maven 构建 ───
|
||||
Push-Location $ProjectRoot
|
||||
try {
|
||||
if (-not $SkipBuild) {
|
||||
Write-Host "[1/4] Building fat jar with Maven..." -ForegroundColor Yellow
|
||||
& mvn package -q -DskipTests 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Maven build failed"
|
||||
exit 1
|
||||
}
|
||||
Write-Host " OK Build succeeded" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[1/4] Skipping build (using existing jar)" -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
# 查找 fat jar
|
||||
$jarFile = Get-ChildItem "target/*.jar" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -notmatch "original" } |
|
||||
Select-Object -First 1
|
||||
if (-not $jarFile) {
|
||||
Write-Error "No jar found in target/. Run without -SkipBuild."
|
||||
exit 1
|
||||
}
|
||||
$jarSize = [math]::Round($jarFile.Length / 1MB, 1)
|
||||
Write-Host " Jar: $($jarFile.Name) ($jarSize MB)" -ForegroundColor Gray
|
||||
|
||||
# ─── Step 2: jdeps 分析模块 ───
|
||||
Write-Host "[2/4] Analyzing module dependencies with jdeps..." -ForegroundColor Yellow
|
||||
$detected = & (Join-Path $javaBinPath $jdepsExe) --print-module-deps --ignore-missing-deps --multi-release 25 $jarFile.FullName 2>&1
|
||||
if ($LASTEXITCODE -ne 0 -or -not $detected) {
|
||||
Write-Host " Warning: jdeps failed, using default modules" -ForegroundColor DarkYellow
|
||||
$detected = "java.base,java.desktop,java.management,java.net.http"
|
||||
}
|
||||
Write-Host " Detected: $detected" -ForegroundColor Gray
|
||||
|
||||
# Spring Boot 运行时必要模块
|
||||
$required = @(
|
||||
"java.naming", # JNDI (Spring)
|
||||
"java.xml", # XML processing (Spring)
|
||||
"java.sql", # JDBC
|
||||
"java.instrument", # Java agent / instrumentation
|
||||
"java.compiler", # Annotation processing
|
||||
"java.scripting", # Script engines
|
||||
"jdk.unsupported", # sun.misc.Unsafe
|
||||
"jdk.crypto.ec", # HTTPS/TLS ECDH
|
||||
"jdk.zipfs", # ZIP filesystem provider
|
||||
"jdk.jfr", # Flight Recorder
|
||||
"jdk.net" # Extended socket options (RestClient)
|
||||
)
|
||||
$allSet = [System.Collections.Generic.HashSet[string]]::new()
|
||||
foreach ($m in ($detected -split ",")) { [void]$allSet.Add($m.Trim()) }
|
||||
foreach ($m in $required) { [void]$allSet.Add($m) }
|
||||
$modules = ($allSet | Sort-Object) -join ","
|
||||
Write-Host " Final modules: $modules" -ForegroundColor Gray
|
||||
|
||||
# ─── Step 3: jlink ───
|
||||
Write-Host "[3/4] Creating minimal JRE with jlink..." -ForegroundColor Yellow
|
||||
$distDir = Join-Path $ProjectRoot $OutputDir
|
||||
$runtimeDir = Join-Path $distDir "runtime"
|
||||
|
||||
if (Test-Path $distDir) { Remove-Item $distDir -Recurse -Force }
|
||||
New-Item -ItemType Directory -Path $distDir -Force | Out-Null
|
||||
|
||||
& (Join-Path $javaBinPath $jlinkExe) `
|
||||
--add-modules $modules `
|
||||
--output $runtimeDir `
|
||||
--strip-debug `
|
||||
--compress zip-6 `
|
||||
--no-header-files `
|
||||
--no-man-pages 2>&1
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "jlink failed"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$runtimeSize = [math]::Round(
|
||||
(Get-ChildItem -Recurse $runtimeDir | Measure-Object -Property Length -Sum).Sum / 1MB, 1
|
||||
)
|
||||
Write-Host " OK Runtime: $runtimeSize MB" -ForegroundColor Green
|
||||
|
||||
# ─── Step 4: 组装 ───
|
||||
Write-Host "[4/4] Assembling distribution..." -ForegroundColor Yellow
|
||||
|
||||
# lib/
|
||||
$libDir = Join-Path $distDir "lib"
|
||||
New-Item -ItemType Directory -Path $libDir -Force | Out-Null
|
||||
Copy-Item $jarFile.FullName (Join-Path $libDir "claude-code-java.jar")
|
||||
|
||||
# bin/
|
||||
$binDir = Join-Path $distDir "bin"
|
||||
New-Item -ItemType Directory -Path $binDir -Force | Out-Null
|
||||
|
||||
# ── Windows launcher (claude-code.cmd) ──
|
||||
$cmdContent = @'
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
chcp 65001 >nul 2>&1
|
||||
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "APP_HOME=%SCRIPT_DIR%.."
|
||||
set "RUNTIME=%APP_HOME%\runtime"
|
||||
set "JAR=%APP_HOME%\lib\claude-code-java.jar"
|
||||
|
||||
if not exist "%RUNTIME%\bin\java.exe" (
|
||||
echo Error: Runtime not found at %RUNTIME%
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
"%RUNTIME%\bin\java.exe" ^
|
||||
--enable-preview ^
|
||||
--enable-native-access=ALL-UNNAMED ^
|
||||
--sun-misc-unsafe-memory-access=allow ^
|
||||
-Xmx512m ^
|
||||
-Djava.net.preferIPv4Stack=true ^
|
||||
-Dstdout.encoding=UTF-8 ^
|
||||
-Dstderr.encoding=UTF-8 ^
|
||||
-Dfile.encoding=UTF-8 ^
|
||||
-jar "%JAR%" ^
|
||||
%*
|
||||
'@
|
||||
Set-Content -Path (Join-Path $binDir "claude-code.cmd") -Value $cmdContent -Encoding ASCII
|
||||
|
||||
# ── Unix launcher (claude-code) ──
|
||||
$shContent = @'
|
||||
#!/bin/sh
|
||||
# Claude Code Java launcher
|
||||
SCRIPT="$0"
|
||||
while [ -L "$SCRIPT" ]; do
|
||||
DIR="$(cd "$(dirname "$SCRIPT")" && pwd)"
|
||||
SCRIPT="$(readlink "$SCRIPT")"
|
||||
[ "${SCRIPT#/}" = "$SCRIPT" ] && SCRIPT="$DIR/$SCRIPT"
|
||||
done
|
||||
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd)"
|
||||
APP_HOME="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
exec "$APP_HOME/runtime/bin/java" \
|
||||
--enable-preview \
|
||||
--enable-native-access=ALL-UNNAMED \
|
||||
--sun-misc-unsafe-memory-access=allow \
|
||||
-Xmx512m \
|
||||
-Djava.net.preferIPv4Stack=true \
|
||||
-Dstdout.encoding=UTF-8 \
|
||||
-Dstderr.encoding=UTF-8 \
|
||||
-Dfile.encoding=UTF-8 \
|
||||
-jar "$APP_HOME/lib/claude-code-java.jar" \
|
||||
"$@"
|
||||
'@
|
||||
$shContent = $shContent -replace "`r`n", "`n"
|
||||
[IO.File]::WriteAllText(
|
||||
(Join-Path $binDir "claude-code"),
|
||||
$shContent,
|
||||
[Text.UTF8Encoding]::new($false)
|
||||
)
|
||||
|
||||
# ── 总计 ──
|
||||
$totalSize = [math]::Round(
|
||||
(Get-ChildItem -Recurse $distDir | Measure-Object -Property Length -Sum).Sum / 1MB, 1
|
||||
)
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " Distribution built successfully!" -ForegroundColor Green
|
||||
Write-Host " ================================" -ForegroundColor Green
|
||||
Write-Host " Location : $distDir" -ForegroundColor White
|
||||
Write-Host " Size : $totalSize MB (Runtime $runtimeSize MB + Jar $jarSize MB)" -ForegroundColor White
|
||||
Write-Host " Platform : $platform" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host " Quick start:" -ForegroundColor Cyan
|
||||
if ($isWin) {
|
||||
Write-Host " set AI_API_KEY=your-key" -ForegroundColor Gray
|
||||
Write-Host " $binDir\claude-code.cmd" -ForegroundColor Gray
|
||||
} else {
|
||||
Write-Host " chmod +x $binDir/claude-code" -ForegroundColor Gray
|
||||
Write-Host " export AI_API_KEY=your-key" -ForegroundColor Gray
|
||||
Write-Host " $binDir/claude-code" -ForegroundColor Gray
|
||||
}
|
||||
Write-Host ""
|
||||
Write-Host " Or add bin/ to PATH for global access:" -ForegroundColor Cyan
|
||||
if ($isWin) {
|
||||
Write-Host " setx PATH `"%PATH%;$(Resolve-Path $binDir)`"" -ForegroundColor Gray
|
||||
} else {
|
||||
Write-Host " export PATH=`"${binDir}:`$PATH`"" -ForegroundColor Gray
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 构建 Claude Code Java 发行版(jlink 最小 JRE + fat jar)
|
||||
#
|
||||
# 用法:
|
||||
# ./packaging/build-dist.sh # 使用 JAVA_HOME
|
||||
# JAVA_HOME=/path/to/jdk25 ./packaging/build-dist.sh # 指定 JDK
|
||||
# ./packaging/build-dist.sh --skip-build # 跳过 Maven 构建
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
OUTPUT_DIR="${OUTPUT_DIR:-dist}"
|
||||
SKIP_BUILD=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--skip-build) SKIP_BUILD=true ;;
|
||||
--help|-h)
|
||||
echo "Usage: $0 [--skip-build]"
|
||||
echo " Environment: JAVA_HOME (required), OUTPUT_DIR (default: dist)"
|
||||
exit 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ─── 验证环境 ───
|
||||
if [ -z "${JAVA_HOME:-}" ] || [ ! -x "$JAVA_HOME/bin/java" ]; then
|
||||
echo "Error: JAVA_HOME not set or JDK not found."
|
||||
echo " export JAVA_HOME=/path/to/jdk-25"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
JAVA_VERSION=$("$JAVA_HOME/bin/java" --version 2>&1 | head -1)
|
||||
|
||||
echo ""
|
||||
echo " Claude Code Java - Distribution Builder"
|
||||
echo " ========================================"
|
||||
echo " JDK: $JAVA_VERSION"
|
||||
echo " Output: $OUTPUT_DIR"
|
||||
|
||||
# 检测平台
|
||||
OS="$(uname -s)"
|
||||
ARCH="$(uname -m)"
|
||||
case "$OS" in
|
||||
Darwin)
|
||||
PLATFORM="macos-$([ "$ARCH" = "arm64" ] && echo "aarch64" || echo "x64")" ;;
|
||||
Linux)
|
||||
PLATFORM="linux-$([ "$ARCH" = "aarch64" ] && echo "aarch64" || echo "x64")" ;;
|
||||
*)
|
||||
PLATFORM="unknown-$OS-$ARCH" ;;
|
||||
esac
|
||||
echo " Platform: $PLATFORM"
|
||||
echo ""
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# ─── Step 1: Maven 构建 ───
|
||||
if [ "$SKIP_BUILD" = false ]; then
|
||||
echo "[1/4] Building fat jar with Maven..."
|
||||
mvn package -q -DskipTests
|
||||
echo " OK Build succeeded"
|
||||
else
|
||||
echo "[1/4] Skipping build (using existing jar)"
|
||||
fi
|
||||
|
||||
JAR_FILE=$(find target -maxdepth 1 -name "*.jar" ! -name "*original*" | head -1)
|
||||
if [ -z "$JAR_FILE" ]; then
|
||||
echo "Error: No jar found in target/. Run without --skip-build."
|
||||
exit 1
|
||||
fi
|
||||
JAR_SIZE=$(du -m "$JAR_FILE" | cut -f1)
|
||||
echo " Jar: $(basename "$JAR_FILE") (${JAR_SIZE} MB)"
|
||||
|
||||
# ─── Step 2: jdeps 分析 ───
|
||||
echo "[2/4] Analyzing module dependencies with jdeps..."
|
||||
DETECTED=$("$JAVA_HOME/bin/jdeps" --print-module-deps --ignore-missing-deps --multi-release 25 "$JAR_FILE" 2>/dev/null || echo "")
|
||||
if [ -z "$DETECTED" ]; then
|
||||
echo " Warning: jdeps failed, using defaults"
|
||||
DETECTED="java.base,java.desktop,java.management,java.net.http"
|
||||
fi
|
||||
echo " Detected: $DETECTED"
|
||||
|
||||
# 追加 Spring Boot 运行时模块
|
||||
EXTRA="java.naming,java.xml,java.sql,java.instrument,java.compiler,java.scripting,jdk.unsupported,jdk.crypto.ec,jdk.zipfs,jdk.jfr,jdk.net"
|
||||
ALL_MODULES=$(echo "$DETECTED,$EXTRA" | tr ',' '\n' | sort -u | tr '\n' ',' | sed 's/,$//')
|
||||
echo " Final: $ALL_MODULES"
|
||||
|
||||
# ─── Step 3: jlink ───
|
||||
echo "[3/4] Creating minimal JRE with jlink..."
|
||||
DIST_DIR="$PROJECT_ROOT/$OUTPUT_DIR"
|
||||
RUNTIME_DIR="$DIST_DIR/runtime"
|
||||
|
||||
rm -rf "$DIST_DIR"
|
||||
mkdir -p "$DIST_DIR"
|
||||
|
||||
"$JAVA_HOME/bin/jlink" \
|
||||
--add-modules "$ALL_MODULES" \
|
||||
--output "$RUNTIME_DIR" \
|
||||
--strip-debug \
|
||||
--compress zip-6 \
|
||||
--no-header-files \
|
||||
--no-man-pages
|
||||
|
||||
RUNTIME_SIZE=$(du -sm "$RUNTIME_DIR" | cut -f1)
|
||||
echo " OK Runtime: ${RUNTIME_SIZE} MB"
|
||||
|
||||
# ─── Step 4: 组装 ───
|
||||
echo "[4/4] Assembling distribution..."
|
||||
|
||||
# lib/
|
||||
mkdir -p "$DIST_DIR/lib"
|
||||
cp "$JAR_FILE" "$DIST_DIR/lib/claude-code-java.jar"
|
||||
|
||||
# bin/
|
||||
mkdir -p "$DIST_DIR/bin"
|
||||
|
||||
# Unix launcher
|
||||
cat > "$DIST_DIR/bin/claude-code" << 'LAUNCHER'
|
||||
#!/bin/sh
|
||||
SCRIPT="$0"
|
||||
while [ -L "$SCRIPT" ]; do
|
||||
DIR="$(cd "$(dirname "$SCRIPT")" && pwd)"
|
||||
SCRIPT="$(readlink "$SCRIPT")"
|
||||
[ "${SCRIPT#/}" = "$SCRIPT" ] && SCRIPT="$DIR/$SCRIPT"
|
||||
done
|
||||
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd)"
|
||||
APP_HOME="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
exec "$APP_HOME/runtime/bin/java" \
|
||||
--enable-preview \
|
||||
--enable-native-access=ALL-UNNAMED \
|
||||
--sun-misc-unsafe-memory-access=allow \
|
||||
-Xmx512m \
|
||||
-Djava.net.preferIPv4Stack=true \
|
||||
-Dstdout.encoding=UTF-8 \
|
||||
-Dstderr.encoding=UTF-8 \
|
||||
-Dfile.encoding=UTF-8 \
|
||||
-jar "$APP_HOME/lib/claude-code-java.jar" \
|
||||
"$@"
|
||||
LAUNCHER
|
||||
chmod +x "$DIST_DIR/bin/claude-code"
|
||||
|
||||
# Windows launcher
|
||||
cat > "$DIST_DIR/bin/claude-code.cmd" << 'LAUNCHER'
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
chcp 65001 >nul 2>&1
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "APP_HOME=%SCRIPT_DIR%.."
|
||||
"%APP_HOME%\runtime\bin\java.exe" ^
|
||||
--enable-preview ^
|
||||
--enable-native-access=ALL-UNNAMED ^
|
||||
--sun-misc-unsafe-memory-access=allow ^
|
||||
-Xmx512m ^
|
||||
-Djava.net.preferIPv4Stack=true ^
|
||||
-Dstdout.encoding=UTF-8 ^
|
||||
-Dstderr.encoding=UTF-8 ^
|
||||
-Dfile.encoding=UTF-8 ^
|
||||
-jar "%APP_HOME%\lib\claude-code-java.jar" ^
|
||||
%*
|
||||
LAUNCHER
|
||||
|
||||
# 统计
|
||||
TOTAL_SIZE=$(du -sm "$DIST_DIR" | cut -f1)
|
||||
echo ""
|
||||
echo " Distribution built successfully!"
|
||||
echo " ================================"
|
||||
echo " Location : $DIST_DIR"
|
||||
echo " Size : ${TOTAL_SIZE} MB (Runtime ${RUNTIME_SIZE} MB + Jar ${JAR_SIZE} MB)"
|
||||
echo " Platform : $PLATFORM"
|
||||
echo ""
|
||||
echo " Quick start:"
|
||||
echo " export AI_API_KEY=your-key"
|
||||
echo " $DIST_DIR/bin/claude-code"
|
||||
echo ""
|
||||
echo " Or add to PATH:"
|
||||
echo " export PATH=\"$DIST_DIR/bin:\$PATH\""
|
||||
echo ""
|
||||
Reference in New Issue
Block a user