前言

昨天,我在 Windows 11 系统中完成了 GraalVM 原生应用 构建能力的测试(相关细节可参考此前文章:《Spring Boot 3 + GraalVM 25 实战:基于 Windows11 系统编译原生镜像》)。今天有了新的想法,想构建可在 Linux 环境运行的原生可执行程序,过程中遇到了典型的系统兼容性问题,最终通过 Docker + ManyLinux 镜像方案解决,现将完整实践流程记录如下。

一、初步尝试:WSL 构建的局限与问题暴露

为快速搭建 Linux 构建环境,我首先启用了 Windows 自带的 WSL(Windows Subsystem for Linux)功能,并从微软应用商店下载安装了 Ubuntu 24.04 发行版。在该环境中配置 GraalVM 后,执行 native-image 命令构建原生镜像时,整个过程未出现任何报错,二进制程序在 Ubuntu 24.04 本地也能正常运行。

但当我将构建好的二进制文件复制到目标 Linux 环境(基于 GLIBC 2.28 版本)时,程序启动立即失败,终端抛出核心错误:“GLIBC 2.32 版本不存在”

通过检索与 AI 咨询得知,GLIBC(GNU C Library)是 Linux 系统的核心库,负责提供基础的系统调用与函数支持,其版本兼容性具有“向下不兼容”特性——即高版本 GLIBC 环境下编译的程序,无法在低版本 GLIBC 环境中运行。此次问题的根源正是:Ubuntu 24.04 预装的 GLIBC 版本(2.32+)高于目标环境的 GLIBC 2.28 版本,导致程序依赖的底层库无法找到。

进一步查阅 GraalVM 官方 Issues,确认了“高版本 Linux 构建的原生镜像无法兼容低版本系统”是普遍现象。但由于手头没有预装 GLIBC 2.28 的系统镜像(如 CentOS 7、Ubuntu 20.04 等),无法通过虚拟机直接搭建匹配的构建环境,并且只是为了构建程序,而运行一个完整的系统,效率不是很高,因此将解决方案聚焦到更灵活的 Docker 容器技术上。

二、方案选型:ManyLinux 镜像的核心优势

经过调研发现,ManyLinux 镜像 是解决 Linux 跨版本编译兼容性问题的最优选择。该镜像由 Python 软件基金会主导维护,本质是基于 CentOS 等低版本 Linux 系统构建的“兼容层镜像”,核心特点包括:

  1. 多版本 GLIBC 支持:内置从 2.12 到最新版本的 GLIBC 库,可满足不同低版本目标环境的依赖需求;
  2. 预装编译工具链:默认包含 gcc、g++、make、binutils 等全套编译工具,无需额外配置;
  3. 架构适配性:提供 x86_64、aarch64 等主流架构的镜像版本,可根据目标环境选择对应镜像;
  4. 轻量可移植:基于容器化技术,启动速度快,且可在 Windows(WSL 或 Docker Desktop)、macOS 等环境中运行,无需单独搭建 Linux 物理机/虚拟机。

最终确定方案:在 Windows 环境中通过 Docker 启动 ManyLinux 容器,在容器内配置 GraalVM 并构建原生镜像,利用 ManyLinux 的低版本 GLIBC 环境,确保生成的二进制程序可兼容目标系统(GLIBC 2.28)。

三、跨平台编译实战:从镜像构建到程序验证

3.1 步骤1:构建自定义 ManyLinux 镜像(集成 GraalVM、Maven)

ManyLinux 官方镜像仅包含基础编译环境,需自行集成 GraalVM 和 Maven。首先创建 Dockerfile,在官方镜像基础上安装 GraalVM,确保构建环境的一致性:

# GraalVM 25 + Maven 3.9.11 + glibc 2.28 精简版
FROM quay.io/pypa/manylinux_2_28_x86_64

# 复制并安装Maven
COPY apache-maven-3.9.11-bin.tar.gz /tmp/
RUN tar -xzf /tmp/apache-maven-3.9.11-bin.tar.gz -C /opt && \
    rm /tmp/apache-maven-3.9.11-bin.tar.gz

# 复制并安装GraalVM JDK 25
COPY graalvm-jdk-25_linux-x64_bin.tar.gz /tmp/
RUN tar -xzf /tmp/graalvm-jdk-25_linux-x64_bin.tar.gz -C /opt && \
    rm /tmp/graalvm-jdk-25_linux-x64_bin.tar.gz && \
    GRAALVM_DIR=$(ls -d /opt/graalvm-jdk-25*) && \
    ln -s $GRAALVM_DIR /opt/graalvm

# 设置环境变量
# 设置环境变量(先定义基础变量)
ENV JAVA_HOME=/opt/graalvm
ENV MAVEN_HOME=/opt/apache-maven-3.9.11

# 单独设置PATH,确保引用的变量已被定义
ENV PATH="$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH"

# 创建Maven仓库目录并设置工作目录
RUN mkdir -p /root/.m2
WORKDIR /project

# 验证命令
CMD ["bash", "-c", "java -version && echo && mvn -version && echo && native-image --version"]

编写完成后,在 Dockerfile 所在目录执行构建命令,生成集成自定义镜像(Dockerfile:Dockerfile.graalvm25-maven-glibc228):

# 构建镜像
docker build -f Dockerfile.graalvm25-maven-glibc228 -t graalvm25-maven-228 .

3.2 步骤2:挂载项目目录并启动容器

为避免在容器内重复拷贝项目代码,通过 Docker 的 目录挂载 功能,将 Windows 本地的项目目录以及 Maven 仓库映射到容器内,实现“本地修改、容器内编译”的便捷流程:

3.2.1 确认本地路径(Windows)

项目在 Windows 中的路径为:C:\Users\Gu\IdeaProjects\tmxk,对应容器中的项目目录 /project

3.2.2 启动容器并挂载目录

打开 Windows 终端(或 WSL 终端),执行以下命令启动容器:

# 启动容器,挂载本地项目目录到容器内的 /project 目录
# --rm:容器退出后自动删除,避免残留
# -it:以交互模式启动,便于进入容器执行命令
docker run -it --rm -v C:\Users\Gu\IdeaProjects\tmxk:/project -v C:\Users\Gu\.m2:/root/.m2 graalvm25-maven-228 bash

执行命令后,将自动进入容器的 /bin/bash 终端,此时容器内的 /project 目录与 Windows 本地的 C:\Users\Gu\IdeaProjects\tmxk 目录完全同步,本地修改的代码会实时同步到容器内。

3.3 步骤3:在容器内构建原生镜像

前台启动容器后,默认会在项目目录 /project,执行我预先提供好的构建脚本:

#!/usr/bin/env bash

# 构建 GraalVM 原生镜像的脚本(容器内执行,项目映射到 /project)

set -euo pipefail

# 设置环境变量,允许外部覆盖
export MAVEN_OPTS="${MAVEN_OPTS:-"-Xmx4g"}"
export MAVEN_ARGS="${MAVEN_ARGS:-"-DskipTests"}"

# 选择 Maven 命令(优先使用项目中的 mvnw)
MVN_CMD="mvn"
#if [[ -x "./mvnw" ]]; then
#  MVN_CMD="./mvnw"
#  printf "使用项目中的 Maven 包装器: %s\n" "$MVN_CMD"
#fi

# 打印当前环境信息
printf "当前环境信息:\n"
printf "Java版本: %s\n" "$(java -version 2>&1 | head -n 1)"
printf "Maven版本: %s\n" "$($MVN_CMD -version | head -n 1)"
printf "GraalVM Native Image版本: %s\n" "$(native-image --version | head -n 1)"
printf "项目目录: %s\n" "$(pwd)"
printf "当前用户: %s\n" "$(id -un)"

# 检查是否在/project目录
if [[ "$(pwd)" != "/project" ]]; then
  printf "警告:当前目录不是 /project,而是 %s\n" "$(pwd)"
  printf "尝试切换到 /project 目录...\n"
  cd /project || {
    printf "错误:无法切换到 /project 目录,请确保项目已正确映射\n" >&2
    exit 1
  }
fi

printf "\n开始构建原生镜像...\n"

# 检查pom.xml是否存在
if [[ ! -f "pom.xml" ]]; then
  printf "错误:在 /project 目录未找到 pom.xml\n" >&2
  printf "请确认项目已正确映射到容器的 /project 目录\n" >&2
  exit 1
fi

# 检查ops模块是否存在
if [[ ! -d "ops" ]]; then
  printf "错误:未找到 ops 模块目录 (/project/ops)\n" >&2
  exit 1
fi

# 执行指定的编译命令
printf "使用命令构建原生镜像: %s -pl ops -Pnative native:compile %s\n" "$MVN_CMD" "$MAVEN_ARGS"
$MVN_CMD -pl ops -Pnative native:compile $MAVEN_ARGS

# 查找构建产物(在ops模块的target目录中)
printf "\n检查构建结果...\n"
shopt -s nullglob
artifacts=()

# 搜索ops模块target目录下的可执行文件
for f in ops/target/*; do
  if [[ -f "$f" && -x "$f" && "$f" != *.jar && "$f" != *.so && "$f" != *.dll ]]; then
    artifacts+=("$f")
  fi
done

# 如果没找到,扩大搜索范围
if [[ ${#artifacts[@]} -eq 0 ]]; then
  printf "在 ops/target 中未找到可执行文件,扩大搜索范围...\n"
  for f in target/* ops/target/*-runner; do
    if [[ -f "$f" && -x "$f" && "$f" != *.jar ]]; then
      artifacts+=("$f")
    fi
  done
fi

if [[ ${#artifacts[@]} -gt 0 ]]; then
  BIN_PATH="${artifacts[0]}"
  printf "原生镜像构建成功!\n"
  ls -la "$BIN_PATH"
  ln -sf "$BIN_PATH" /project/native-app  # 在项目根目录创建符号链接
  printf "已创建符号链接: /project/native-app -> %s\n" "$BIN_PATH"
else
  printf "错误:未找到原生可执行产物。\n" >&2
  printf "请检查 ops 模块的构建配置和输出目录\n" >&2
  exit 1
fi

# 可选:运行原生镜像进行测试(通过环境变量控制)
if [[ "${RUN_TEST:-0}" == "1" ]]; then
  printf "\n运行原生镜像测试...\n"
  /project/native-app --version || /project/native-app --help || true
fi

printf "\n构建流程完成。\n"

构建过程中,native-image 会基于 ManyLinux 容器的 GLIBC 2.28 环境编译链接,生成的二进制程序 ops ,这会从根源上解决兼容性问题。

3.4 步骤4:二进制文件验证

因为是把项目目录挂载到容器中的,所以容器构建产物就会实时出现在项目的 target 目录中,直接保存即可。

3.4.1 目标环境验证

将容器内的 ops/target/ops 程序通过 SCP/FTP 等工具上传到目标 Linux 环境(GLIBC 2.28),执行以下命令测试:

# 上传后赋予可执行权限
chmod +x ops

# 运行程序
./ops

此时程序可正常启动,无“GLIBC 版本不存在”的错误,证明跨版本兼容性问题已解决。

四、总结

通过 Docker + ManyLinux 镜像的方案,无需搭建低版本 Linux 虚拟机,即可快速构建出兼容低版本 GLIBC 的 GraalVM 原生镜像,核心优势在于:

  1. 兼容性保障:利用 ManyLinux 的低版本 GLIBC 环境,从编译源头解决跨版本依赖问题;
  2. 效率提升:容器化环境启动快、配置可复用,避免重复搭建构建环境;
  3. 跨平台灵活:可在 Windows、macOS 等环境中运行,适配不同开发者的本地系统。

该方案不仅适用于 GraalVM 原生镜像构建,也可推广到 C/C++、Go 等其他语言的跨 Linux 版本编译场景,具有较强的通用性。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐