diff --git a/.github/workflows/verify.yaml b/.github/workflows/verify.yaml
index 8e1a3be..2cb15e8 100644
--- a/.github/workflows/verify.yaml
+++ b/.github/workflows/verify.yaml
@@ -15,4 +15,33 @@ jobs:
distribution: 'graalvm'
cache: maven
- run: ./mvnw clean verify
- # TODO: check native-image can build ice
+
+ native-build:
+ name: Build and test native image (${{ matrix.arch }})
+ runs-on: ${{ matrix.runner }}
+ strategy:
+ matrix:
+ include:
+ - arch: amd64
+ runner: ubuntu-24.04
+ profile: native
+ artifact: ice-native-amd64-dynamic
+ - arch: arm64
+ runner: ubuntu-24.04-arm
+ profile: native-arm64
+ artifact: ice-native-arm64-dynamic
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ java-version: '21'
+ distribution: 'graalvm'
+ cache: maven
+ - name: Build native image (dynamic linking - no musl required)
+ run: ./mvnw -P${{ matrix.profile }} -pl ice clean package -Dmaven.test.skip=true
+ - name: Upload native binary as artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ matrix.artifact }}
+ path: ice/target/ice
+ if-no-files-found: error
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 52ecf19..4ae9503 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+.idea/
target
/demo/data
/demo/clickhouse-udfs.xml
diff --git a/README.md b/README.md
index 91e9c0d..d9f7f45 100644
--- a/README.md
+++ b/README.md
@@ -10,8 +10,14 @@ Create/delete tables, insert data with `ice insert -p ns1.table1 file://example.
## Installation
-Pre-built binaries\* (+ links to Docker images for [ice](https://hub.docker.com/r/altinity/ice) and [ice-rest-catalog](https://hub.docker.com/r/altinity/ice-rest-catalog)) are available form [GitHub Releases](https://github.com/Altinity/ice/releases) page.
-> \* currently require `java` 21+ to run (available [here](https://adoptium.net/installation/)).
+Pre-built binaries (+ links to Docker images for [ice](https://hub.docker.com/r/altinity/ice) and [ice-rest-catalog](https://hub.docker.com/r/altinity/ice-rest-catalog)) are available from [GitHub Releases](https://github.com/Altinity/ice/releases) page.
+
+**Two types of binaries are available:**
+
+1. **Java-based binaries** - Require Java 21+ to run (available [here](https://adoptium.net/installation/))
+2. **Native binaries** - Standalone executables with no Java dependency
+ - `ice-native-amd64` - Static binary (musl) for x86_64 Linux (no dependencies)
+ - `ice-native-arm64` - Dynamic binary for ARM64 Linux (requires glibc)
## Usage
@@ -19,6 +25,8 @@ See [examples/](examples/).
## Development
+### Standard Build (Java-based)
+
Install [sdkman](https://sdkman.io/install), then
```shell
@@ -35,6 +43,24 @@ sdk env
./mvnw
```
+### Native Image Build (Standalone)
+
+Build standalone native binaries with no Java dependency:
+
+```shell
+# Install prerequisites
+sdk env # or ensure Java 21+ and GraalVM are available
+
+# For amd64 (static with musl - no dependencies)
+mvn -Pnative-amd64-static -pl ice clean package -Dmaven.test.skip=true
+For arm64
+mvn -Pnative-arm64 -pl ice clean package -Dmaven.test.skip=true
+
+# Docker builds
+docker build -f ice/Dockerfile.native-amd64-static -t ice-native:amd64 .
+
+docker build -f ice/Dockerfile.native-arm64 -t ice-native:arm64 .
+
## License
Copyright (c) 2025, Altinity Inc and/or its affiliates. All rights reserved.
diff --git a/ice/Dockerfile.native-amd64-static b/ice/Dockerfile.native-amd64-static
new file mode 100644
index 0000000..e8b4f27
--- /dev/null
+++ b/ice/Dockerfile.native-amd64-static
@@ -0,0 +1,33 @@
+# Dockerfile for building ICE native image (amd64, static with musl)
+# Usage: docker build -f ice/Dockerfile.native-amd64-static -t ice-native:amd64 .
+
+FROM ghcr.io/graalvm/native-image-community:21-muslib AS builder
+
+WORKDIR /workspace
+
+# Copy license header file (required by license-maven-plugin)
+COPY apache-header.txt .
+
+# Copy Maven files for dependency resolution
+COPY mvnw .
+COPY .mvn .mvn
+COPY pom.xml .
+COPY ice/pom.xml ice/
+COPY ice-rest-catalog/pom.xml ice-rest-catalog/
+
+# Download dependencies (cached layer)
+RUN ./mvnw dependency:go-offline -pl ice || true
+
+# Copy source code
+COPY ice/src ice/src
+
+# Build static native image with musl
+RUN ./mvnw -Pnative-amd64-static -pl ice clean package -Dmaven.test.skip=true
+
+# ============================================================================
+# Final stage: minimal scratch image (truly standalone binary)
+# ============================================================================
+FROM scratch
+COPY --from=builder /workspace/ice/target/ice /ice
+ENTRYPOINT ["/ice"]
+
diff --git a/ice/Dockerfile.native-arm64 b/ice/Dockerfile.native-arm64
new file mode 100644
index 0000000..4d997ec
--- /dev/null
+++ b/ice/Dockerfile.native-arm64
@@ -0,0 +1,33 @@
+# Dockerfile for building ICE native image (arm64, dynamic linking)
+# Usage: docker build -f ice/Dockerfile.native-arm64 -t ice-native:arm64 .
+
+FROM ghcr.io/graalvm/native-image-community:21 AS builder
+
+WORKDIR /workspace
+
+# Copy license header file (required by license-maven-plugin)
+COPY apache-header.txt .
+
+# Copy Maven files for dependency resolution
+COPY mvnw .
+COPY .mvn .mvn
+COPY pom.xml .
+COPY ice/pom.xml ice/
+COPY ice-rest-catalog/pom.xml ice-rest-catalog/
+
+# Download dependencies (cached layer)
+RUN ./mvnw dependency:go-offline -pl ice || true
+
+# Copy source code
+COPY ice/src ice/src
+
+# Build dynamic native image
+RUN ./mvnw -Pnative-arm64 -pl ice clean package -Dmaven.test.skip=true
+
+# ============================================================================
+# Final stage: distroless base (includes minimal glibc runtime)
+# ============================================================================
+FROM gcr.io/distroless/base-debian12:nonroot
+COPY --from=builder /workspace/ice/target/ice /ice
+ENTRYPOINT ["/ice"]
+
diff --git a/ice/pom.xml b/ice/pom.xml
index feb357a..81bd81e 100644
--- a/ice/pom.xml
+++ b/ice/pom.xml
@@ -618,5 +618,111 @@
+
+ native
+
+
+
+ org.graalvm.buildtools
+ native-maven-plugin
+ ${native.maven.plugin.version}
+ true
+
+
+ build-native
+
+ compile-no-fork
+
+ package
+
+
+
+ ice
+ com.altinity.ice.cli.Main
+
+ -H:ReflectionConfigurationFiles=${project.basedir}/src/main/resources/reflection-config.json
+ --initialize-at-run-time=io.netty
+ --initialize-at-run-time=ch.qos.logback
+ -H:+ReportExceptionStackTraces
+
+
+
+
+
+
+
+
+ native-amd64-static
+
+
+
+ org.graalvm.buildtools
+ native-maven-plugin
+ ${native.maven.plugin.version}
+ true
+
+
+ build-native
+
+ compile-no-fork
+
+ package
+
+
+
+ ice
+ com.altinity.ice.cli.Main
+
+
+ -H:ReflectionConfigurationFiles=${project.basedir}/src/main/resources/reflection-config.json
+ --initialize-at-run-time=io.netty
+ --initialize-at-run-time=ch.qos.logback
+
+ --static
+ --libc=musl
+
+ -H:+RemoveUnusedSymbols
+ -H:+ReportExceptionStackTraces
+
+
+
+
+
+
+
+ native-arm64
+
+
+
+ org.graalvm.buildtools
+ native-maven-plugin
+ ${native.maven.plugin.version}
+ true
+
+
+ build-native
+
+ compile-no-fork
+
+ package
+
+
+
+ ice
+ com.altinity.ice.cli.Main
+
+
+ -H:ReflectionConfigurationFiles=${project.basedir}/src/main/resources/reflection-config.json
+ --initialize-at-run-time=io.netty
+ --initialize-at-run-time=ch.qos.logback
+
+ -H:+RemoveUnusedSymbols
+ -H:+ReportExceptionStackTraces
+
+
+
+
+
+
diff --git a/ice/src/main/resources/reflection-config.json b/ice/src/main/resources/reflection-config.json
new file mode 100644
index 0000000..15a1a7b
--- /dev/null
+++ b/ice/src/main/resources/reflection-config.json
@@ -0,0 +1,112 @@
+[
+ {
+ "name": "ch.qos.logback.classic.AsyncAppender",
+ "allDeclaredConstructors": true,
+ "allPublicConstructors": true,
+ "allDeclaredMethods": true,
+ "allPublicMethods": true,
+ "allDeclaredFields": true,
+ "allPublicFields": true,
+ "allDeclaredClasses": true,
+ "allPublicClasses": true
+ },
+ {
+ "name": "ch.qos.logback.classic.encoder.PatternLayoutEncoder",
+ "allDeclaredConstructors": true,
+ "allPublicConstructors": true,
+ "allDeclaredMethods": true,
+ "allPublicMethods": true,
+ "allDeclaredFields": true,
+ "allPublicFields": true,
+ "allDeclaredClasses": true,
+ "allPublicClasses": true
+ },
+ {
+ "name": "ch.qos.logback.classic.pattern.DateConverter",
+ "allDeclaredConstructors": true,
+ "allPublicConstructors": true,
+ "allDeclaredMethods": true,
+ "allPublicMethods": true,
+ "allDeclaredFields": true,
+ "allPublicFields": true,
+ "allDeclaredClasses": true,
+ "allPublicClasses": true
+ },
+ {
+ "name": "ch.qos.logback.classic.pattern.LevelConverter",
+ "allDeclaredConstructors": true,
+ "allPublicConstructors": true,
+ "allDeclaredMethods": true,
+ "allPublicMethods": true,
+ "allDeclaredFields": true,
+ "allPublicFields": true,
+ "allDeclaredClasses": true,
+ "allPublicClasses": true
+ },
+ {
+ "name": "ch.qos.logback.classic.pattern.LineSeparatorConverter",
+ "allDeclaredConstructors": true,
+ "allPublicConstructors": true,
+ "allDeclaredMethods": true,
+ "allPublicMethods": true,
+ "allDeclaredFields": true,
+ "allPublicFields": true,
+ "allDeclaredClasses": true,
+ "allPublicClasses": true
+ },
+ {
+ "name": "ch.qos.logback.classic.pattern.LoggerConverter",
+ "allDeclaredConstructors": true,
+ "allPublicConstructors": true,
+ "allDeclaredMethods": true,
+ "allPublicMethods": true,
+ "allDeclaredFields": true,
+ "allPublicFields": true,
+ "allDeclaredClasses": true,
+ "allPublicClasses": true
+ },
+ {
+ "name": "ch.qos.logback.classic.pattern.MessageConverter",
+ "allDeclaredConstructors": true,
+ "allPublicConstructors": true,
+ "allDeclaredMethods": true,
+ "allPublicMethods": true,
+ "allDeclaredFields": true,
+ "allPublicFields": true,
+ "allDeclaredClasses": true,
+ "allPublicClasses": true
+ },
+ {
+ "name": "ch.qos.logback.classic.pattern.ThreadConverter",
+ "allDeclaredConstructors": true,
+ "allPublicConstructors": true,
+ "allDeclaredMethods": true,
+ "allPublicMethods": true,
+ "allDeclaredFields": true,
+ "allPublicFields": true,
+ "allDeclaredClasses": true,
+ "allPublicClasses": true
+ },
+ {
+ "name": "ch.qos.logback.core.ConsoleAppender",
+ "allDeclaredConstructors": true,
+ "allPublicConstructors": true,
+ "allDeclaredMethods": true,
+ "allPublicMethods": true,
+ "allDeclaredFields": true,
+ "allPublicFields": true,
+ "allDeclaredClasses": true,
+ "allPublicClasses": true
+ },
+ {
+ "name": "ch.qos.logback.core.FileAppender",
+ "allDeclaredConstructors": true,
+ "allPublicConstructors": true,
+ "allDeclaredMethods": true,
+ "allPublicMethods": true,
+ "allDeclaredFields": true,
+ "allPublicFields": true,
+ "allDeclaredClasses": true,
+ "allPublicClasses": true
+ }
+]