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
-
-