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 + } +]