diff --git a/.env b/.env new file mode 100644 index 0000000..528ad70 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +# Auto-generated by build\tools\generate-dotenv.ps1 +SNOW_VERSION=0.7.0 diff --git a/.run/build-release-all.ps1.run.xml b/.run/build-release-all.ps1.run.xml new file mode 100644 index 0000000..2a862b4 --- /dev/null +++ b/.run/build-release-all.ps1.run.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.run/release-linux.ps1.run.xml b/.run/release-linux.ps1.run.xml new file mode 100644 index 0000000..06f300e --- /dev/null +++ b/.run/release-linux.ps1.run.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.run/release-windows.ps1.run.xml b/.run/release-windows.ps1.run.xml new file mode 100644 index 0000000..eb5a30c --- /dev/null +++ b/.run/release-windows.ps1.run.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bfc25e7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,57 @@ +# Stage 1: 官方 GraalVM 社区版(已含 native-image) +FROM ghcr.io/graalvm/native-image-community:24.0.2 AS builder + +RUN microdnf install -y \ + gcc gcc-c++ make git wget tar gzip which findutils maven \ + && microdnf clean all + +# ---------- 构建 musl ---------- +ARG MUSL_VER=1.2.5 +WORKDIR /tmp +RUN wget -q https://musl.libc.org/releases/musl-${MUSL_VER}.tar.gz \ + && tar -xzf musl-${MUSL_VER}.tar.gz \ + && cd musl-${MUSL_VER} \ + && ./configure --prefix=/opt/musl-${MUSL_VER} --disable-shared \ + && make -j"$(nproc)" \ + && make install \ + && ln -s /opt/musl-${MUSL_VER} /opt/musl \ + && cd / && rm -rf /tmp/musl-${MUSL_VER}* + +RUN ln -s /opt/musl/bin/musl-gcc /usr/local/bin/x86_64-linux-musl-gcc \ + && ln -s /opt/musl/bin/musl-gcc /usr/local/bin/x86_64-linux-musl-cc + +ENV PATH="/opt/musl/bin:${PATH}" +ENV CC="musl-gcc" +ENV C_INCLUDE_PATH="/opt/musl/include" +ENV LIBRARY_PATH="/opt/musl/lib" + +# ---------- 静态 zlib ---------- +ARG ZLIB_VERSION=1.3.1 +WORKDIR /tmp +RUN wget -q https://zlib.net/zlib-${ZLIB_VERSION}.tar.gz \ + && tar -xzf zlib-${ZLIB_VERSION}.tar.gz \ + && cd zlib-${ZLIB_VERSION} \ + && CC=musl-gcc ./configure --static --prefix=/opt/musl \ + && make -j"$(nproc)" \ + && make install \ + && cd / && rm -rf /tmp/zlib-${ZLIB_VERSION}* + +# ---------- Maven 缓存优化 ---------- +WORKDIR /app +COPY pom.xml ./ + +# 先拉依赖并缓存 +RUN mvn -B -P native-linux dependency:go-offline + +# ---------- 复制源码 ---------- +COPY . /app + +# ---------- 编译 native image ---------- +RUN mvn -P native-linux -DskipTests clean package + +# ------------------------------------------------------------ +# Stage 2: 输出产物镜像(可以直接 cp 出二进制) +# ------------------------------------------------------------ +FROM busybox AS export +WORKDIR /export +COPY --from=builder /app/org.jcnc.snow.cli.SnowCLI /export/Snow \ No newline at end of file diff --git a/build/build-project2tar.ps1 b/build/build-project2tar.ps1 new file mode 100644 index 0000000..2ee7146 --- /dev/null +++ b/build/build-project2tar.ps1 @@ -0,0 +1,48 @@ +# Set the tar package name +$tarName = "Snow.tar" + +# Get the script's current directory (build folder) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition + +# Get the parent directory (the project root) +$parentDir = Split-Path -Parent $scriptDir + +# Set the full path to the tar package +$tarPath = Join-Path $parentDir $tarName + +# Output message: starting to create tar package +Write-Output "Starting to create tar package: $tarName in $parentDir ..." + +# Remove old tar package if it exists +if (Test-Path $tarPath) { + Write-Output "Found an old $tarName, removing it..." + Remove-Item $tarPath -Force +} + +# Make sure the tar command is available +$tarCommand = "tar" +if (-not (Get-Command $tarCommand -ErrorAction SilentlyContinue)) { + Write-Error "❌ 'tar' command is not available. Please make sure 'tar' is installed and can be run from PowerShell." + exit 1 +} + +# Execute tar: change to org\jcnc directory and compress the snow folder +try { + # Build the command and run it + $tarCommandArgs = "-cf", $tarPath, "-C", "$scriptDir\..\src\main\java\org\jcnc", "snow" + Write-Output "Running tar command: tar $tarCommandArgs" + + & $tarCommand @tarCommandArgs +} catch { + Write-Error "❌ Failed to create tar package. Error: $_" + exit 1 +} + +# Check if tar package was created successfully +if (Test-Path $tarPath) { + Write-Output "✅ Successfully created $tarName" + exit 0 +} else { + Write-Error "❌ Creation failed. Please check the tar command and paths." + exit 1 +} diff --git a/build/build-release-all.ps1 b/build/build-release-all.ps1 new file mode 100644 index 0000000..3c42adb --- /dev/null +++ b/build/build-release-all.ps1 @@ -0,0 +1,129 @@ +param( + [string]$LogDir = (Join-Path $PSScriptRoot 'target\parallel-logs') +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$winScript = Join-Path $PSScriptRoot 'release-windows.ps1' +$linScript = Join-Path $PSScriptRoot 'release-linux.ps1' + +if (-not (Test-Path $winScript)) { throw "File not found: $winScript" } +if (-not (Test-Path $linScript)) { throw "File not found: $linScript" } + +$winLogOut = [System.IO.Path]::GetTempFileName() +$winLogErr = [System.IO.Path]::GetTempFileName() +$linLogOut = [System.IO.Path]::GetTempFileName() +$linLogErr = [System.IO.Path]::GetTempFileName() + +$winProc = Start-Process powershell.exe -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File',"`"$winScript`"") ` + -RedirectStandardOutput $winLogOut -RedirectStandardError $winLogErr -NoNewWindow -PassThru +$linProc = Start-Process powershell.exe -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File',"`"$linScript`"") ` + -RedirectStandardOutput $linLogOut -RedirectStandardError $linLogErr -NoNewWindow -PassThru + +$winPosOut = 0 +$winPosErr = 0 +$linPosOut = 0 +$linPosErr = 0 + +Write-Host "===== Build Started =====" +while (-not $winProc.HasExited -or -not $linProc.HasExited) { + # windows-release stdout + if (Test-Path $winLogOut) { + $size = (Get-Item $winLogOut).Length + if ($size -gt $winPosOut) { + $fs = [System.IO.File]::Open($winLogOut, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) + $fs.Position = $winPosOut + $sr = New-Object System.IO.StreamReader($fs) + while (!$sr.EndOfStream) { + $line = $sr.ReadLine() + if ($line) { Write-Host "[windows-release][OUT] $line" } + } + $winPosOut = $fs.Position + $sr.Close() + $fs.Close() + } + } + # windows-release stderr + if (Test-Path $winLogErr) { + $size = (Get-Item $winLogErr).Length + if ($size -gt $winPosErr) { + $fs = [System.IO.File]::Open($winLogErr, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) + $fs.Position = $winPosErr + $sr = New-Object System.IO.StreamReader($fs) + while (!$sr.EndOfStream) { + $line = $sr.ReadLine() + if ($line) { Write-Host "[windows-release][ERR] $line" -ForegroundColor Red } + } + $winPosErr = $fs.Position + $sr.Close() + $fs.Close() + } + } + # linux-release stdout + if (Test-Path $linLogOut) { + $size = (Get-Item $linLogOut).Length + if ($size -gt $linPosOut) { + $fs = [System.IO.File]::Open($linLogOut, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) + $fs.Position = $linPosOut + $sr = New-Object System.IO.StreamReader($fs) + while (!$sr.EndOfStream) { + $line = $sr.ReadLine() + if ($line) { Write-Host "[linux-release][OUT] $line" } + } + $linPosOut = $fs.Position + $sr.Close() + $fs.Close() + } + } + # linux-release stderr + if (Test-Path $linLogErr) { + $size = (Get-Item $linLogErr).Length + if ($size -gt $linPosErr) { + $fs = [System.IO.File]::Open($linLogErr, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) + $fs.Position = $linPosErr + $sr = New-Object System.IO.StreamReader($fs) + while (!$sr.EndOfStream) { + $line = $sr.ReadLine() + if ($line) { Write-Host "[linux-release][ERR] $line" -ForegroundColor Red } + } + $linPosErr = $fs.Position + $sr.Close() + $fs.Close() + } + } + Start-Sleep -Milliseconds 200 +} + +# After processes exit, print any remaining output +$tasks = @( + @{proc=$winProc; log=$winLogOut; tag='windows-release'; type='OUT'; skip=$winPosOut}, + @{proc=$winProc; log=$winLogErr; tag='windows-release'; type='ERR'; skip=$winPosErr}, + @{proc=$linProc; log=$linLogOut; tag='linux-release'; type='OUT'; skip=$linPosOut}, + @{proc=$linProc; log=$linLogErr; tag='linux-release'; type='ERR'; skip=$linPosErr} +) +foreach ($item in $tasks) { + if (Test-Path $item.log) { + $fs = [System.IO.File]::Open($item.log, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) + $fs.Position = $item.skip + $sr = New-Object System.IO.StreamReader($fs) + while (!$sr.EndOfStream) { + $line = $sr.ReadLine() + if ($line) { + if ($item.type -eq 'ERR') { + Write-Host "[$($item.tag)][ERR] $line" -ForegroundColor Red + } else { + Write-Host "[$($item.tag)][OUT] $line" + } + } + } + $sr.Close() + $fs.Close() + } +} + +Write-Host "" +Write-Host "All tasks completed successfully." -ForegroundColor Green + +Remove-Item $winLogOut, $winLogErr, $linLogOut, $linLogErr -Force +exit 0 diff --git a/build/build_project2tar.ps1 b/build/build_project2tar.ps1 deleted file mode 100644 index 81b5d69..0000000 --- a/build/build_project2tar.ps1 +++ /dev/null @@ -1,47 +0,0 @@ -# 设定 tar 包的名称 -$tarName = "Snow.tar" - -# 获取脚本当前目录(build文件夹) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition - -# 获取上一级目录(snow 根目录) -$parentDir = Split-Path -Parent $scriptDir - -# 设置 tar 包的完整路径 -$tarPath = Join-Path $parentDir $tarName - -# 输出开始创建 tar 包的消息 -Write-Output "开始创建 tar 包: $tarName 到 $parentDir ..." - -# 如果存在旧 tar 包,先删除它 -if (Test-Path $tarPath) { - Write-Output "发现旧的 $tarName,正在删除..." - Remove-Item $tarPath -Force -} - -# 确保 tar 命令可用 -$tarCommand = "tar" -if (-not (Get-Command $tarCommand -ErrorAction SilentlyContinue)) { - Write-Error "❌ tar 命令不可用。请确保 tar 已安装并可在 PowerShell 中执行。" - exit 1 -} - -# 执行打包操作: 切换到 org\jcnc 目录下再压缩 snow 文件夹 -try { - # 构建命令并执行 - $tarCommandArgs = "-cf", $tarPath, "-C", "$scriptDir\..\src\main\java\org\jcnc", "snow" - Write-Output "执行 tar 命令: tar $tarCommandArgs" - - & $tarCommand @tarCommandArgs -} catch { - Write-Error "❌ 创建 tar 包失败。错误信息: $_" - exit 1 -} - -# 检查 tar 包是否创建成功 -if (Test-Path $tarPath) { - Write-Output "✅ 成功创建 $tarName" -} else { - Write-Error "❌ 创建失败,请检查 tar 命令和路径是否正确。" - exit 1 -} diff --git a/build/release-linux.ps1 b/build/release-linux.ps1 new file mode 100644 index 0000000..5503242 --- /dev/null +++ b/build/release-linux.ps1 @@ -0,0 +1,55 @@ +# run-linux-snow-export.ps1 +# Build and package linux-snow-export, version read from SNOW_VERSION in .env + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# Import shared dotenv parser function +. "$PSScriptRoot\tools\dotenv.ps1" + +Write-Host "Step 0: Generate .env..." +try { + & "$PSScriptRoot\tools\generate-dotenv.ps1" -ErrorAction Stop +} catch { + Write-Error "Failed to generate .env: $( $_.Exception.Message )" + exit 1 +} + +Write-Host "Step 1: Build and run linux-snow-export..." +docker compose run --build --rm linux-snow-export +if ($LASTEXITCODE -ne 0) { + Write-Error "Build & Run failed, exiting script." + exit $LASTEXITCODE +} + +Write-Host "Step 2: Run linux-snow-export without rebuild..." +docker compose run --rm linux-snow-export +if ($LASTEXITCODE -ne 0) { + Write-Error "Run without rebuild failed, exiting script." + exit $LASTEXITCODE +} + +# ===== Step 3: Read version from .env ===== +$projectRoot = Resolve-Path (Join-Path $PSScriptRoot "..") +$dotenvPath = Join-Path $projectRoot ".env" + +if (-not (Test-Path -LiteralPath $dotenvPath)) { + Write-Error ".env not found at: $dotenvPath" + exit 1 +} + +$version = Read-DotEnvValue -FilePath $dotenvPath -Key 'SNOW_VERSION' +if (-not $version) { + Write-Error "SNOW_VERSION not found in .env" + exit 1 +} + +# ===== Step 4: Define output paths ===== +$targetDir = Join-Path $projectRoot "target\release" +$outDir = Join-Path $targetDir "Snow-v$version-linux-x64" +$tgzPath = Join-Path $targetDir "Snow-v$version-linux-x64.tgz" + +Write-Host ">>> Package ready!" -ForegroundColor Green +Write-Host "Version : $version" +Write-Host "Output Dir : $outDir" +Write-Host "Tgz File : $tgzPath" \ No newline at end of file diff --git a/build/release-windows.ps1 b/build/release-windows.ps1 new file mode 100644 index 0000000..f3c24f7 --- /dev/null +++ b/build/release-windows.ps1 @@ -0,0 +1,117 @@ +# release-windows.ps1 + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' +Set-StrictMode -Version Latest + +# Import shared dotenv parser function +. "$PSScriptRoot\tools\dotenv.ps1" + +# ===== Utility Functions ===== +function Find-PomUpwards([string]$startDir) { + $dir = Resolve-Path $startDir + while ($true) { + $pom = Join-Path $dir "pom.xml" + if (Test-Path $pom) { return $pom } + $parent = Split-Path $dir -Parent + if ($parent -eq $dir -or [string]::IsNullOrEmpty($parent)) { return $null } + $dir = $parent + } +} + +# ===== Step 0: Generate .env ===== +Write-Host "Step 0: Generate .env..." +try { + & "$PSScriptRoot\tools\generate-dotenv.ps1" -ErrorAction Stop +} catch { + Write-Error "Failed to generate .env: $($_.Exception.Message)" + exit 1 +} + +# ===== Step 1: Locate project root & build ===== +Write-Host "Step 1: Locate project root and build..." +$pom = Find-PomUpwards -startDir $PSScriptRoot +if (-not $pom) { + Write-Error "pom.xml not found. Please run this script within the project." + exit 1 +} + +$projectRoot = Split-Path $pom -Parent +Push-Location $projectRoot +try { + Write-Host "→ Running: mvn clean package" + mvn clean package + if ($LASTEXITCODE -ne 0) { + Write-Error "Maven build failed, exiting script." + exit $LASTEXITCODE + } + + # ===== Step 2: Read SNOW_VERSION ===== + Write-Host "Step 2: Read SNOW_VERSION from .env..." + $dotenvPath = Join-Path $projectRoot ".env" + $snowVersion = Read-DotEnvValue -FilePath $dotenvPath -Key "SNOW_VERSION" + if (-not $snowVersion) { + Write-Host "SNOW_VERSION not found in .env, using placeholder 0.0.0." -ForegroundColor Yellow + $snowVersion = "0.0.0" + } + Write-Host "SNOW_VERSION = $snowVersion" + + # ===== Step 3: Prepare release directory structure ===== + Write-Host "Step 3: Prepare release directory structure..." + $targetDir = Join-Path $projectRoot "target" + $exePath = Join-Path $targetDir "Snow.exe" + if (-not (Test-Path $exePath)) { + Write-Error "Expected build artifact not found: $exePath" + exit 1 + } + + $verName = "Snow-v${snowVersion}-windows-x64" + $releaseRoot = Join-Path $targetDir "release" + $outDir = Join-Path $releaseRoot $verName + $binDir = Join-Path $outDir "bin" + $libDir = Join-Path $outDir "lib" + + # Clean old directory + if (Test-Path $outDir) { + Write-Host "→ Cleaning previous output directory..." + Remove-Item $outDir -Recurse -Force + } + + New-Item -ItemType Directory -Force -Path $binDir | Out-Null + Copy-Item -Path $exePath -Destination (Join-Path $binDir "Snow.exe") -Force + Write-Host ">>> Collected Snow.exe" + + # Optional lib + $projectLib = Join-Path $projectRoot "lib" + if (Test-Path $projectLib) { + New-Item -ItemType Directory -Force -Path $libDir | Out-Null + Copy-Item -Path (Join-Path $projectLib "*") -Destination $libDir -Recurse -Force + Write-Host ">>> Copied lib directory" + } else { + Write-Host ">>> lib directory not found, skipping." -ForegroundColor Yellow + } + + # ===== Step 4: Create zip ===== + Write-Host "Step 4: Create release zip..." + New-Item -ItemType Directory -Force -Path $releaseRoot | Out-Null + $zipPath = Join-Path $releaseRoot ("{0}.zip" -f $verName) + if (Test-Path $zipPath) { + Write-Host "→ Removing existing zip: $zipPath" + Remove-Item $zipPath -Force + } + + try { + Compress-Archive -Path $outDir -DestinationPath $zipPath -Force + } catch { + Write-Error "Failed to create zip: $($_.Exception.Message)" + exit 1 + } + + Write-Host ">>> Package ready!" -ForegroundColor Green + Write-Host "Version : $snowVersion" + Write-Host "Output Dir : $outDir" + Write-Host "Zip File : $zipPath" +} +finally { + Pop-Location +} diff --git a/build/tools/dotenv.ps1 b/build/tools/dotenv.ps1 new file mode 100644 index 0000000..f3c1eb1 --- /dev/null +++ b/build/tools/dotenv.ps1 @@ -0,0 +1,53 @@ +# tools/dotenv.ps1 +# Unified .env reader function: +# - Supports `KEY=VAL` and `export KEY=VAL` +# - Skips blank lines and comments +# - Handles quoted values (single or double quotes) +# - Allows inline comments at the end of a line (space + #) +# - If the same KEY is defined multiple times, the last one takes precedence + +Set-StrictMode -Version Latest + +function Read-DotEnvValue { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)][string]$FilePath, + [Parameter(Mandatory=$true)][string]$Key + ) + + if (-not (Test-Path -LiteralPath $FilePath)) { return $null } + + # Match the target key (escaped), allowing optional "export" prefix + $pattern = '^(?:\s*export\s+)?(?' + [regex]::Escape($Key) + ')\s*=\s*(?.*)$' + $value = $null + + # Read line by line for large file compatibility + Get-Content -LiteralPath $FilePath | ForEach-Object { + $line = $_ + + # Skip blank lines and full-line comments + if ($line -match '^\s*$') { return } + if ($line -match '^\s*#') { return } + + if ($line -match $pattern) { + $v = $matches['v'] + + # Remove surrounding quotes if present + $trimmed = $v.Trim() + if ($trimmed -match '^\s*"(.*)"\s*$') { + $v = $matches[1] + } elseif ($trimmed -match "^\s*'(.*)'\s*$") { + $v = $matches[1] + } else { + # Strip inline comments (space + # …), ignoring escaped \# + if ($v -match '^(.*?)(? - + native-linux @@ -82,25 +78,39 @@ unix + org.graalvm.buildtools native-maven-plugin ${native.maven.plugin.version} - true + + + org.jcnc.snow.cli.SnowCLI + Snow + ${project.build.directory} + + --static + --libc=musl + --emit=build-report + -O2 + -H:Class=org.jcnc.snow.cli.SnowCLI + -H:CCompilerPath=/opt/musl/bin/musl-gcc + -H:CLibraryPath=/opt/musl/lib + + + - build-native - compile-no-fork package - + test-native @@ -109,24 +119,6 @@ test - - - - --static - - --libc=musl - - --emit build-report - - -O2 - - - - /opt/musl-1.2.5/bin:${env.PATH} - /opt/musl-1.2.5/include - /opt/musl-1.2.5/lib - -