Skip to content

Commit b5904c5

Browse files
jgillisclaude
andcommitted
x64: switch CI to docker-run + docker-commit; fix TARGET_ARCH env collision
The multi-stage Dockerfile build was tripping GNU make's built-in implicit-rule variable TARGET_ARCH. Make's default `%.o: %.c` recipe is `$(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c`, so an inherited `TARGET_ARCH=x86_64` env var splices `x86_64` as a positional gcc argument inside any sub-make that relies on the built-in pattern rule (e.g. Julia's bundled libwhich). Failure surfaces as `gcc: error: x86_64: linker input file not found` and looks like a parallel-make race. Fix: rename to JULIA_TARGET_ARCH everywhere + defensively `unset TARGET_ARCH` at the top of build-julia.sh. Also switch the workflow from docker/build-push-action to plain `docker run` + `docker commit` per user request — same env contract as the proven local build, no BuildKit-induced surprises. Local build of the renamed script: 77m53s, multi-target sysimage, no libunwind, smoketest green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6624257 commit b5904c5

4 files changed

Lines changed: 177 additions & 154 deletions

File tree

.github/workflows/build-image.yml

Lines changed: 86 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ on:
44
push:
55
branches: [docker-manylinux_2_28-x64]
66
paths:
7-
- Dockerfile
87
- scripts/**
98
- .github/workflows/build-image.yml
109
workflow_dispatch:
@@ -31,23 +30,34 @@ concurrency:
3130
env:
3231
IMAGE: ghcr.io/${{ github.repository_owner }}/manylinux_2_28-x64-polyglot
3332
BASE_IMAGE: ghcr.io/jgillis/manylinux_2_28-x64:production
34-
TARGET_ARCH: x86_64
35-
# BUILD_JOBS=1 to bypass the BuildKit `docker build` + parallel-make race
36-
# in Julia's deps phase (`gcc: error: x86_64: linker input file not found`
37-
# from configure scripts under -j>1). ~25 min deps phase vs ~12 with -j4.
38-
BUILD_JOBS: 1
33+
JULIA_TARGET_ARCH: x86_64
34+
# Local proven build runs at -j6 in ~78 min. GitHub-hosted ubuntu-latest is
35+
# 4-core/16 GB; -j4 keeps LLVM linking under the OOM line and matches the
36+
# build-julia.sh default. Going lower trades wall time for nothing.
37+
BUILD_JOBS: 4
3938

4039
jobs:
4140
build:
4241
runs-on: ubuntu-latest
42+
timeout-minutes: 180
4343
steps:
4444
- uses: actions/checkout@v4
45+
4546
- uses: docker/login-action@v3
4647
with:
4748
registry: ghcr.io
4849
username: ${{ github.actor }}
4950
password: ${{ secrets.GITHUB_TOKEN }}
50-
- uses: docker/setup-buildx-action@v3
51+
52+
- name: Free up disk space
53+
# ubuntu-latest ships ~14 GB free; LLVM build + container layers easily
54+
# push past that. Strip the largest known-unused payloads up front.
55+
run: |
56+
set -eux
57+
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \
58+
/opt/hostedtoolcache/CodeQL
59+
sudo docker image prune -af
60+
df -h /
5161
5262
- name: Resolve tags
5363
id: tags
@@ -61,21 +71,72 @@ jobs:
6171
echo "tag_jl=${{ env.IMAGE }}:julia${JULIA_VERSION}"
6272
} >> "$GITHUB_OUTPUT"
6373
64-
- name: Build (and conditionally push) image
65-
uses: docker/build-push-action@v6
66-
with:
67-
context: .
68-
file: Dockerfile
69-
build-args: |
70-
BASE_IMAGE=${{ env.BASE_IMAGE }}
71-
TARGET_ARCH=${{ env.TARGET_ARCH }}
72-
JULIA_VERSION=${{ steps.tags.outputs.julia_version }}
73-
BUILD_JOBS=${{ env.BUILD_JOBS }}
74-
push: ${{ github.event.inputs.push != 'false' }}
75-
tags: |
76-
${{ steps.tags.outputs.tag_prod }}
77-
${{ steps.tags.outputs.tag_sha }}
78-
${{ steps.tags.outputs.tag_jl }}
79-
cache-from: type=gha,scope=manylinux_2_28-x64-polyglot
80-
cache-to: type=gha,scope=manylinux_2_28-x64-polyglot,mode=max
81-
provenance: false
74+
- name: Pull base image
75+
run: docker pull "$BASE_IMAGE"
76+
77+
- name: Build polyglot inside container (docker run)
78+
# docker run + docker commit (NOT docker build). The multi-stage
79+
# Dockerfile approach + BuildKit was fighting the same GNU make
80+
# TARGET_ARCH name-collision bug that hit local builds, and gave
81+
# confusing failures because BuildKit obscures the env. docker run
82+
# is plain: same env contract as the proven local build script.
83+
run: |
84+
set -eux
85+
docker run \
86+
--name polyglot-build \
87+
-v "$PWD/scripts:/work/scripts:ro" \
88+
-e JULIA_VERSION="${{ steps.tags.outputs.julia_version }}" \
89+
-e JULIA_TARGET_ARCH="${{ env.JULIA_TARGET_ARCH }}" \
90+
-e BUILD_JOBS="${{ env.BUILD_JOBS }}" \
91+
"$BASE_IMAGE" \
92+
bash /work/scripts/run-in-container.sh
93+
94+
- name: Commit container into polyglot image
95+
# Bake metadata (ENV, WORKDIR, LABEL) via --change since docker commit
96+
# otherwise just snapshots the filesystem. Keep the base image's PATH
97+
# and prepend /opt/julia/bin + /opt/rustup/cargo/bin.
98+
run: |
99+
set -eux
100+
docker commit \
101+
--change 'ENV JULIA_HOME=/opt/julia' \
102+
--change 'ENV JULIA_PATH=/opt/julia' \
103+
--change 'ENV JULIA_VERSION=${{ steps.tags.outputs.julia_version }}' \
104+
--change 'ENV CARGO_HOME=/opt/rustup/cargo' \
105+
--change 'ENV RUSTUP_HOME=/opt/rustup' \
106+
--change 'ENV PATH=/opt/julia/bin:/opt/rustup/cargo/bin:/opt/rh/gcc-toolset-14/root/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' \
107+
--change 'ENV CARGO_BUILD_TARGET=${{ env.JULIA_TARGET_ARCH }}-unknown-linux-gnu' \
108+
--change 'WORKDIR /work' \
109+
--change 'LABEL org.opencontainers.image.title=manylinux_2_28 polyglot (Julia + Rust + C/C++)' \
110+
--change 'LABEL org.opencontainers.image.source=https://github.com/${{ github.repository }}' \
111+
--change 'LABEL casadi.polyglot.julia.version=${{ steps.tags.outputs.julia_version }}' \
112+
--change 'LABEL casadi.polyglot.arch=${{ env.JULIA_TARGET_ARCH }}' \
113+
polyglot-build \
114+
"${{ steps.tags.outputs.tag_prod }}"
115+
docker tag "${{ steps.tags.outputs.tag_prod }}" "${{ steps.tags.outputs.tag_sha }}"
116+
docker tag "${{ steps.tags.outputs.tag_prod }}" "${{ steps.tags.outputs.tag_jl }}"
117+
118+
- name: Smoketest committed image
119+
run: |
120+
set -eux
121+
docker run --rm "${{ steps.tags.outputs.tag_prod }}" bash -lc '
122+
set -e
123+
julia --version
124+
julia -e "println(\"Julia \", VERSION, \" — LLVM \", Base.libllvm_version, \" — CPU \", Sys.CPU_NAME)"
125+
rustc --version
126+
cargo --version
127+
cbindgen --version
128+
test ! -e /opt/julia/lib/libunwind.so
129+
echo "polyglot post-commit OK"
130+
'
131+
132+
- name: Push (conditional)
133+
if: github.event_name == 'push' || github.event.inputs.push != 'false'
134+
run: |
135+
set -eux
136+
docker push "${{ steps.tags.outputs.tag_prod }}"
137+
docker push "${{ steps.tags.outputs.tag_sha }}"
138+
docker push "${{ steps.tags.outputs.tag_jl }}"
139+
140+
- name: Cleanup builder container
141+
if: always()
142+
run: docker rm -f polyglot-build 2>/dev/null || true

Dockerfile

Lines changed: 0 additions & 112 deletions
This file was deleted.

scripts/build-julia.sh

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,25 @@
77
# into src/{signals-unix.c,stackwalk.c}
88
#
99
# Args: $1 = path to extracted Julia source tree (containing Make.inc)
10-
# Env: TARGET_ARCH (x86_64 | aarch64) — defaults to native
10+
# Env: JULIA_TARGET_ARCH (x86_64 | aarch64) — defaults to native
1111
# BUILD_JOBS — make parallelism (default 4)
1212
set -euo pipefail
1313
SRC="${1:?Julia source tree path required}"
14-
TARGET_ARCH="${TARGET_ARCH:-$(uname -m)}"
14+
JULIA_TARGET_ARCH="${JULIA_TARGET_ARCH:-$(uname -m)}"
1515
BUILD_JOBS="${BUILD_JOBS:-4}"
1616

17+
# GNU make's built-in implicit recipe for `%.o: %.c` is
18+
# $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c $<
19+
# so an inherited TARGET_ARCH env var (e.g. "x86_64") leaks in as a positional
20+
# gcc arg via Julia's sub-makes (libwhich, OpenBLAS, ...), failing with
21+
# `gcc: error: x86_64: linker input file not found`. Strip it defensively.
22+
unset TARGET_ARCH
23+
1724
cd "$SRC"
1825

1926
# JULIA_CPU_TARGET — multi-target string matching JuliaCI's official binary
2027
# builds (utilities/build_envs.sh).
21-
case "$TARGET_ARCH" in
28+
case "$JULIA_TARGET_ARCH" in
2229
x86_64)
2330
JULIA_CPU_TARGET="generic;sandybridge,-xsaveopt,clone_all;haswell,-rdrnd,base(1);x86-64-v4,-rdrnd,base(1)"
2431
;;
@@ -27,7 +34,7 @@ case "$TARGET_ARCH" in
2734
JULIA_CPU_TARGET="generic"
2835
;;
2936
*)
30-
echo "Unsupported TARGET_ARCH=$TARGET_ARCH" >&2; exit 1
37+
echo "Unsupported JULIA_TARGET_ARCH=$JULIA_TARGET_ARCH" >&2; exit 1
3138
;;
3239
esac
3340

@@ -36,18 +43,6 @@ USE_BINARYBUILDER=0
3643
JULIA_CPU_TARGET=${JULIA_CPU_TARGET}
3744
DISABLE_LIBUNWIND:=1
3845
EOF
39-
# HOSTCC must be a single token. Make.inc line 445 unconditionally does
40-
# `HOSTCC = \$(CC)` which then gets `-m\$(BINARY)` appended → HOSTCC becomes
41-
# the two-word "gcc -m64". libwhich.mk's recipe `\$(MAKE) -C scratch/...
42-
# CC="\$(HOSTCC)" libwhich` then expands to `make ... CC="gcc -m64" libwhich`.
43-
# Under BuildKit's docker build with -j>1, the sub-make's MAKEFLAGS-driven
44-
# re-tokenization splits this and a stray ARCH token ("x86_64" or "aarch64")
45-
# leaks in as a positional arg to gcc — `gcc: error: x86_64: linker input
46-
# file not found`. Setting HOSTCC in Make.user does NOT work because line
47-
# 445 overwrites it. The fix is to pass HOSTCC/HOSTCXX on the make command
48-
# line so they win over makefile assignments.
49-
MAKE_VARS=(HOSTCC=gcc HOSTCXX=g++)
50-
5146
echo "=== Make.user ==="
5247
cat Make.user
5348
echo
@@ -137,7 +132,7 @@ echo
137132

138133
echo "=== Build ==="
139134
date
140-
time make -j"$BUILD_JOBS" "${MAKE_VARS[@]}"
135+
time make -j"$BUILD_JOBS"
141136
date
142137
echo
143138

scripts/run-in-container.sh

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/bin/bash
2+
# Orchestrate the full polyglot image build inside the base manylinux container.
3+
# Invoked by the CI workflow under `docker run --name polyglot-build ...`;
4+
# the workflow then `docker commit`s the resulting container into the image.
5+
#
6+
# Required env vars:
7+
# JULIA_VERSION e.g. 1.12.6
8+
# JULIA_TARGET_ARCH x86_64 | aarch64 (NOT named TARGET_ARCH — that name
9+
# collides with GNU make's built-in
10+
# implicit-rule variable and poisons
11+
# the `%.o: %.c` recipe — see skill
12+
# gnu-make-target-arch-env-collision)
13+
# BUILD_JOBS make parallelism (CI defaults to 4)
14+
set -euo pipefail
15+
16+
: "${JULIA_VERSION:?JULIA_VERSION required}"
17+
: "${JULIA_TARGET_ARCH:?JULIA_TARGET_ARCH required}"
18+
: "${BUILD_JOBS:=4}"
19+
20+
# Defensive: this var must NEVER reach `make`. See skill referenced above.
21+
unset TARGET_ARCH
22+
23+
echo "=== Install build-time packages ==="
24+
# flex — not pre-installed in manylinux_2_28 / pypa bases
25+
# perl-core — OpenSSL Configure needs Data::Dumper + IPC::Cmd
26+
( dnf install -y flex perl-core 2>&1 | tail -5 ) \
27+
|| ( yum install -y flex perl-core 2>&1 | tail -5 )
28+
( dnf clean all 2>/dev/null || yum clean all ) >/dev/null 2>&1 || true
29+
rm -rf /var/cache/dnf /var/cache/yum 2>/dev/null || true
30+
31+
echo
32+
echo "=== Fetch Julia ${JULIA_VERSION} source ==="
33+
mkdir -p /build
34+
cd /build
35+
curl -fsSL -o "julia-${JULIA_VERSION}-full.tar.gz" \
36+
"https://github.com/JuliaLang/julia/releases/download/v${JULIA_VERSION}/julia-${JULIA_VERSION}-full.tar.gz"
37+
tar xzf "julia-${JULIA_VERSION}-full.tar.gz"
38+
rm "julia-${JULIA_VERSION}-full.tar.gz"
39+
40+
echo
41+
echo "=== Build Julia (delegates to scripts/build-julia.sh) ==="
42+
JULIA_TARGET_ARCH="$JULIA_TARGET_ARCH" BUILD_JOBS="$BUILD_JOBS" \
43+
/work/scripts/build-julia.sh "/build/julia-${JULIA_VERSION}"
44+
45+
echo
46+
echo "=== Move julia install into /opt/julia ==="
47+
mv "/build/julia-${JULIA_VERSION}/usr" /opt/julia
48+
# Strip everything else — sources, srccache, scratch, intermediate artifacts.
49+
# Leaves /opt/julia as the only thing committed into the final image.
50+
rm -rf "/build/julia-${JULIA_VERSION}" /build/*.tar.gz
51+
rmdir /build 2>/dev/null || true
52+
53+
echo
54+
echo "=== Install Rust + cbindgen (idempotent: skips if already present) ==="
55+
/work/scripts/install-rust.sh
56+
57+
# Add the per-target Rust triple.
58+
case "${JULIA_TARGET_ARCH}" in
59+
x86_64) RUSTUP_HOME=/opt/rustup CARGO_HOME=/opt/rustup/cargo rustup target add x86_64-unknown-linux-gnu ;;
60+
aarch64) RUSTUP_HOME=/opt/rustup CARGO_HOME=/opt/rustup/cargo rustup target add aarch64-unknown-linux-gnu ;;
61+
esac
62+
63+
echo
64+
echo "=== Drop cargo profile shim into /etc/profile.d ==="
65+
UP=$(echo "${JULIA_TARGET_ARCH}_unknown_linux_gnu" | tr '[:lower:]' '[:upper:]')
66+
GCC=$( command -v "${CC:-gcc}" 2>/dev/null || command -v gcc )
67+
printf 'export CARGO_TARGET_%s_LINKER=%s\nexport CARGO_TARGET_%s_RUSTFLAGS=-C prefer-dynamic\n' \
68+
"$UP" "$GCC" "$UP" > /etc/profile.d/polyglot-cargo.sh
69+
70+
echo
71+
echo "=== In-container smoketest ==="
72+
export PATH=/opt/julia/bin:/opt/rustup/cargo/bin:$PATH
73+
julia --version
74+
julia -e 'println("Julia ", VERSION, " — LLVM ", Base.libllvm_version, " — CPU ", Sys.CPU_NAME)'
75+
rustc --version
76+
cargo --version
77+
cbindgen --version
78+
test ! -e /opt/julia/lib/libunwind.so
79+
echo "polyglot pre-commit OK"

0 commit comments

Comments
 (0)