Skip to content

zig cc: Respect Clang's -static and -dynamic flags #23572

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 26, 2025

Conversation

alexrp
Copy link
Member

@alexrp alexrp commented Apr 14, 2025

Hopefully more correct take on #12894.

Before:

❯ zig cc main.c -target x86_64-linux-musl && musl-ldd ./a.out
musl-ldd: ./a.out: Not a valid dynamic program
❯ zig cc main.c -target x86_64-linux-musl -static && musl-ldd ./a.out
musl-ldd: ./a.out: Not a valid dynamic program
❯ zig cc main.c -target x86_64-linux-musl -dynamic && musl-ldd ./a.out
musl-ldd: ./a.out: Not a valid dynamic program

After:

❯ zig cc main.c -target x86_64-linux-musl && musl-ldd ./a.out
musl-ldd: ./a.out: Not a valid dynamic program
❯ zig cc main.c -target x86_64-linux-musl -static && musl-ldd ./a.out
musl-ldd: ./a.out: Not a valid dynamic program
❯ zig cc main.c -target x86_64-linux-musl -dynamic && musl-ldd ./a.out
        /lib/ld-musl-x86_64.so.1 (0x72c10019e000)
        libc.so => /lib/ld-musl-x86_64.so.1 (0x72c10019e000)

Closes #11909.

Release Notes

zig cc now properly respects the -static and -dynamic flags. Most notably, this allows statically linking native glibc, and dynamically linking cross-compiled musl.

They are, themselves, static libraries even if the resulting artifact strictly
speaking requires dynamic linking to the corresponding system DLLs to run. Note,
though, that there's no libc-provided dynamic linker on Windows like on POSIX,
so this isn't particularly problematic.

This matches x86_64-w64-mingw32-gcc behavior.
@alexrp alexrp requested a review from andrewrk April 14, 2025 19:40
Before:

    ❯ zig cc main.c -target x86_64-linux-musl && musl-ldd ./a.out
    musl-ldd: ./a.out: Not a valid dynamic program
    ❯ zig cc main.c -target x86_64-linux-musl -static && musl-ldd ./a.out
    musl-ldd: ./a.out: Not a valid dynamic program
    ❯ zig cc main.c -target x86_64-linux-musl -dynamic && musl-ldd ./a.out
    musl-ldd: ./a.out: Not a valid dynamic program

After:

    ❯ zig cc main.c -target x86_64-linux-musl && musl-ldd ./a.out
    musl-ldd: ./a.out: Not a valid dynamic program
    ❯ zig cc main.c -target x86_64-linux-musl -static && musl-ldd ./a.out
    musl-ldd: ./a.out: Not a valid dynamic program
    ❯ zig cc main.c -target x86_64-linux-musl -dynamic && musl-ldd ./a.out
            /lib/ld-musl-x86_64.so.1 (0x72c10019e000)
            libc.so => /lib/ld-musl-x86_64.so.1 (0x72c10019e000)

Closes ziglang#11909.
@alexrp
Copy link
Member Author

alexrp commented Apr 26, 2025

I'll go ahead and merge this so we can see if it breaks anything in the wild once people start testing it in master builds. @andrewrk the requested review for you was just in case you wanted to have a look since you merged and reverted #12894 back in the day.

@alexrp alexrp merged commit 99a79f9 into ziglang:master Apr 26, 2025
17 of 18 checks passed
@alexrp alexrp deleted the zig-cc-static-dynamic branch April 26, 2025 13:06
@polarathene
Copy link

polarathene commented May 2, 2025

It would be nice if https://ziglang.org/download/ mentioned the release cadence for the master builds? I'm assuming weekly (seems to be on Sundays)?

The current master build is from Sunday 27th April, but was a couple commits before this one was merged:

image


I wanted to test this feature out and also verify that it'd work with glibc (I'm aware of the caveats of static glibc), but I'll need to wait until the next build is available it seems.

For future me, here is a copy/paste Dockerfile to automate testing this PR (UPDATE: Confirmed with associated follow-up PR, static glibc can be built with -static provided no -target is set and the required static libs are packaged by the distro, thus not compatible with Alpine as a build host):

ARG BASE_IMAGE=fedora
# Alternatively use a release version like `0.14.0`:
ARG ZIG_VERSION=master


# Separate stage (base image agnostic) for caching Zig releases:
FROM alpine AS get-zig
RUN apk --no-cache add curl jq
ARG ZIG_VERSION
RUN <<"HEREDOC"
  # Grab a Zig release (jq retrieves the URL from the JSON data, eg: `.master.x86_64-linux.tarball):
  ZIG_RELEASE=$(curl -fsSL https://ziglang.org/download/index.json | jq -rc ".[\"${ZIG_VERSION}\"][\"x86_64-linux\"].tarball")
  ZIG_DIR="/opt/zig-${ZIG_VERSION}"

  # Prefer mkdir with `--strip-components=1` otherwise archive top-level directory is the longer archive filename from URL
  # Use `--no-same-owner` because ownership is otherwise 1000:1000
  mkdir "${ZIG_DIR}"
  curl -fsSL "${ZIG_RELEASE}" | tar --extract --xz --directory "${ZIG_DIR}" --strip-components=1 --no-same-owner
HEREDOC


# Base images:
FROM fedora:42 AS base-fedora
# For `zig cc` to support static glibc builds these files are required:
# https://github.com/ziglang/zig/issues/4986#issuecomment-1982362679
# - `glibc-static` provides: /usr/lib64/libc.a
# - `gcc` provides: /usr/lib/gcc/x86_64-redhat-linux/15/libgcc_eh.a
# `musl-libc` provides the musl libc + interpreter to run the `musl-ldd` script
RUN dnf --setopt=install_weak_deps=0 install -yq file jq gcc glibc-static musl-libc

FROM debian:12-slim AS base-debian
ARG DEBIAN_FRONTEND=noninteractive
# `musl` for `musl-ldd` + dynamic link musl
# `gcc` + `libc6-dev` for static link glibc
RUN apt-get -qq update && apt-get -qq install file gcc libc6-dev musl

# NOTE: Alpine is not supported for static glibc builds:
FROM alpine:3.21 AS base-alpine
RUN apk add bash file


# Final image stage:
FROM base-"${BASE_IMAGE}"
ARG ZIG_VERSION
COPY --link --from=get-zig "/opt/zig-${ZIG_VERSION}" /opt/ziglang
RUN ln -s /opt/ziglang/zig /usr/local/bin/zig
# Defaults for convenience:
WORKDIR /example
CMD ["zig-test"]


# Remainder of this file embeds content:
# Example program to test with:
COPY <<"HEREDOC" /example/main.c
#include <stdio.h>
int main()
{
    printf("Hello World!\n");
}
HEREDOC

# Various shell scripts for convenience:
COPY --chmod=755 <<"HEREDOC" /usr/local/bin/zig-test
#!/bin/bash
echo "Testing builds via Zig $(zig version)"

# Basic build:
echo -e '\nRunning: zig cc main.c -o example'
zig cc main.c -o example
inspect-bin example
echo -e '\n'

# Build with specific target libc + `-static`/-dynamic` flag variants:
zig-test-arch musl
zig-test-arch gnu

# One final attempt with glibc static:
echo 'Running a glibc static build with explicit inputs to link for glibc-static'

echo -e "\n..with '-static':"
zig cc main.c -o example-gnu-inputs-static -target x86_64-linux-gnu -static \
  /usr/lib64/libc.a \
  /usr/lib/gcc/x86_64-redhat-linux/15/libgcc_eh.a
[[ ${?} -eq 0 ]] && inspect-bin example-gnu-inputs-static

echo -e "\n..without '-static':"
zig cc main.c -o example-gnu-inputs -target x86_64-linux-gnu \
  /usr/lib64/libc.a \
  /usr/lib/gcc/x86_64-redhat-linux/15/libgcc_eh.a
[[ ${?} -eq 0 ]] && inspect-bin example-gnu-inputs

# Build static libc from host (this one actually works):
# https://github.com/ziglang/zig/pull/23752#issuecomment-2849743043
# NOTE: Alpine/musl-based build host is not supported by zig for glibc static builds
# Regardless Alpine lacks the equivalent glibc-static package required.
echo -e "\n..with '-static' but without '-target' or inputs:"
echo 'Running: zig cc main.c -o example-static -static'
zig cc main.c -o example-static -static
[[ ${?} -eq 0 ]] && inspect-bin example-static
HEREDOC

COPY --chmod=755 <<"HEREDOC" /usr/local/bin/zig-test-arch
#!/bin/bash
TARGET_ARCH=${1?must provide an arch}
BUILD_TARGET="x86_64-linux-${TARGET_ARCH}"

for VARIANT in default static dynamic; do
  BUILD_FLAGS=()
  # Append the `-static` or `-dynamic` flags:
  [[ "${VARIANT}" != 'default' ]] && BUILD_FLAGS+=("-${VARIANT}")

  # NOTE: Command stored in a list for printing to stdout via echo before running it:
  # Example: `zig cc main.c -o example-gnu-static -target x86_64-linux-gnu -static`
  RUN_COMMAND=(zig cc main.c -o "example-${TARGET_ARCH}-${VARIANT}" -target "${BUILD_TARGET}" "${BUILD_FLAGS[@]}")
  echo "Running: ${RUN_COMMAND[@]}"
  "${RUN_COMMAND[@]}"

  # If status code is 0 (success) inspect the bin:
  [[ ${?} -eq 0 ]] && inspect-bin "example-${TARGET_ARCH}-${VARIANT}" "${TARGET_ARCH}"
  echo -e '\n'
done
HEREDOC

COPY --chmod=755 <<"HEREDOC" /usr/local/bin/inspect-bin
#!/bin/bash
BIN=${1?must provide path to binary}
TARGET_ARCH="${2:-gnu}"

case "${TARGET_ARCH}" in
  ( 'gnu'  ) export LDD='ldd' ;;
  ( 'musl' ) export LDD='musl-ldd' ;;
  ( '*'    ) echo 'Unsupported ARCH'; return 1 ;;
esac

function _inspect() {
  local BIN=${1}

  echo "Inspecting '${BIN}'"
  file "${BIN}"
  "${LDD}" "${BIN}"
}

_inspect $1
HEREDOC

COPY --chmod=755 <<"HEREDOC" /usr/local/bin/musl-ldd
#!/bin/sh
/lib/ld-musl-x86_64.so.1 --list "${@}"
HEREDOC
Original Dockerfile for this comment

This was the version used for output reported. The newer version of the Dockerfile is a bit more thorough in coverage and better handles output with build failures.

FROM fedora:42
RUN <<"HEREDOC"
  # Only `file` + `jq` are probably relevant?:
  dnf --setopt=install_weak_deps=0 install -yq file jq gcc glibc-static musl-libc nano

  # Install Zig (latest master branch build)
  ZIG_AMD64_MASTER=$(curl -fsSL https://ziglang.org/download/index.json | jq -rc '.master["x86_64-linux"].tarball')

  # Prefer mkdir with `--strip-components=1` otherwise archive top-level directory is the longer archive filename from URL
  # Use `--no-same-owner` because ownership is otherwise 1000:1000
  mkdir /opt/zig-master
  curl -fsSL "${ZIG_AMD64_MASTER}" | tar --extract --xz --directory /opt/zig-master --strip-components=1 --no-same-owner
  ln -s /opt/zig-master/zig /usr/local/bin/zig
HEREDOC
# Defaults for convenience:
WORKDIR /example
CMD ["zig-test"]

# Remainder of this file embeds content:
# Example program to test with:
COPY <<"HEREDOC" /example/main.c
#include <stdio.h>
int main()
{
    printf("Hello World!\n");
}
HEREDOC

# Various shell scripts for convenience:
COPY --chmod=755 <<HEREDOC /usr/local/bin/zig-test
#!/bin/sh
zig cc main.c -o example
inspect-bin example

zig-test-arch musl
zig-test-arch gnu
HEREDOC

COPY --chmod=755 <<"HEREDOC" /usr/local/bin/zig-test-arch
#!/bin/sh
TARGET_ARCH=${1?must provide an arch}
BUILD_TARGET="x86_64-linux-${TARGET_ARCH}"

zig cc main.c -o "example-${TARGET_ARCH}-default" -target "${BUILD_TARGET}"
zig cc main.c -o "example-${TARGET_ARCH}-static"  -target "${BUILD_TARGET}" -static
zig cc main.c -o "example-${TARGET_ARCH}-dynamic" -target "${BUILD_TARGET}" -dynamic

for VARIANT in default static dynamic; do
  inspect-bin "example-${TARGET_ARCH}-${VARIANT}" "${TARGET_ARCH}"
done
HEREDOC

COPY --chmod=755 <<"HEREDOC" /usr/local/bin/inspect-bin
#!/bin/bash
BIN=${1?must provide path to binary}
TARGET_ARCH="${2:-gnu}"

case "${TARGET_ARCH}" in
  ( 'gnu'  ) export LDD='ldd' ;;
  ( 'musl' ) export LDD='musl-ldd' ;;
  ( '*'    ) echo 'Unsupported ARCH'; return 1 ;;
esac

function _inspect() {
  local BIN=${1}

  echo "Inspecting '${BIN}'"
  file "${BIN}"
  "${LDD}" "${BIN}"
  echo -e '\n'
}

_inspect $1
HEREDOC

COPY --chmod=755 <<"HEREDOC" /usr/local/bin/musl-ldd
#!/bin/sh
/usr/lib/ld-musl-x86_64.so.1 --list "${@}"
HEREDOC

Prior to this PR, the current output from the container is:

$ docker build --tag localhost/zig:master .

# Just run this and it'll build all variants for musl and glibc:
$ docker run --rm -it localhost/zig:master

Inspecting 'example'
example: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, with debug_info, not stripped
        linux-vdso.so.1 (0x00007fbc0e34d000)
        libc.so.6 => /lib64/libc.so.6 (0x00007fbc0e153000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fbc0e34f000)


Inspecting 'example-musl-default'
example-musl-default: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped
/usr/lib/ld-musl-x86_64.so.1: example-musl-default: Not a valid dynamic program


Inspecting 'example-musl-static'
example-musl-static: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped
/usr/lib/ld-musl-x86_64.so.1: example-musl-static: Not a valid dynamic program


Inspecting 'example-musl-dynamic'
example-musl-dynamic: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped
/usr/lib/ld-musl-x86_64.so.1: example-musl-dynamic: Not a valid dynamic program


Inspecting 'example-gnu-default'
example-gnu-default: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.0.0, with debug_info, not stripped
        linux-vdso.so.1 (0x00007f417404c000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f4173e52000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f417404e000)


Inspecting 'example-gnu-static'
example-gnu-static: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.0.0, with debug_info, not stripped
        linux-vdso.so.1 (0x00007f99e7de2000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f99e7be8000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f99e7de4000)


Inspecting 'example-gnu-dynamic'
example-gnu-dynamic: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.0.0, with debug_info, not stripped
        linux-vdso.so.1 (0x00007f2601f2a000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f2601d30000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f2601f2c000)

@polarathene
Copy link

polarathene commented May 2, 2025

Making a separate comment just to highlight that slight difference from using -target or not with glibc:

# GNU/Linux 3.2.0
example: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, with debug_info, not stripped

vs

# GNU/Linux 2.0.0
example-gnu-default: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.0.0, with debug_info, not stripped

Not sure if that's relevant, but something must be slightly different for that difference 😅

@Rexicon226
Copy link
Contributor

It would be nice if ziglang.org/download mentioned the release cadence for the master builds? I'm assuming weekly (seems to be on Sundays)?

The release cadence is approximately every 8 hours, however the nightly CI has been broken for a few days:
https://github.com/ziglang/www.ziglang.org/actions/workflows/build-tarballs.yml

@alexrp
Copy link
Member Author

alexrp commented May 2, 2025

ziglang/zig-bootstrap@2fd9c31

@polarathene
Copy link

polarathene commented May 2, 2025

ziglang/zig-bootstrap@2fd9c31

Aren't these opt-in? (glibc => -static + musl => -dynamic)

The breakage isn't a surprise elsewhere AFAIK when you choose non-default linking for a target? Why require linking inputs manually instead? Is Zig unable to locate system libraries available itself?

I was hoping -static support would fix this bug with glibc target. It seems that current master builds are blocking using -static, and if I don't provide that flag, but provide the files to link as suggested, that still fails to build.


From the linked commit message:

This is problematic when targeting *-linux-gnu* and *-macos-none because Zig doesn't provide a static libc for these, resulting in early configure errors.
Just rely on the Zig compiler to infer the executable link mode from the link inputs instead.

For comparison with Rust, -gnu is dynamic by default and -musl is static by default, just like with Zig AFAIK. You can change that via -C target-feature flag as demonstrated below, that's akin to -static / -dynamic flags here.

Rust doesn't provide static libc for the linux gnu target either, so when you do the equivalent of -static you would need to ensure you provide the static libs on the system for the linker to find.

For musl, Rust provides files in it's toolchain to support that by the default but opt-in to dynamic linking likewise requires extra files as shown below.

Perhaps Zig differs a little here, I remember when I tried static linking glibc, I had to explicitly provide two files, it wasn't able to locate them like Rust does when they're available. Ideally -static would be sufficient if you can fix that 👍

Reproduction (Rust equivalents for reference)

Collapsed for brevity (Click to view)
# Build both Fedora (glibc) and Alpine (musl) image variants:
docker build --tag localhost/example:glibc --build-arg WITH_LIBC=glibc .
docker build --tag localhost/example:musl --build-arg WITH_LIBC=musl .
ARG WITH_LIBC=glibc

FROM fedora:42 AS rust-glibc
RUN <<HEREDOC
  # NOTE: `glibc-static` would be needed for static linking of glibc:
  dnf --setopt=install_weak_deps=0 install -yq file gcc nano rustup
  rustup-init -y --profile minimal --default-toolchain stable --target x86_64-unknown-linux-musl

  dnf clean all
HEREDOC


FROM alpine:3.21 AS rust-musl
RUN <<HEREDOC
  # NOTE: `musl-dev` is needed for `Scrt1.o` + `crti.o` files when dynamic linking:
  apk --no-cache add file gcc nano rustup
  rustup-init -y --profile minimal --default-toolchain stable --target x86_64-unknown-linux-musl
HEREDOC


FROM rust-${WITH_LIBC}
ENV PATH="/root/.cargo/bin:$PATH"
WORKDIR /example
RUN cargo init .

Rust musl dynamic

For dynamically linked musl, you would need to use -C target-feature=-crt-static (similar to -dynamic), and possibly -C link-self-contained=no, then provide musl-libc to link to.

# NOTE: The `example` volume is for transferring between glibc/musl containers
$ docker run --rm -it --volume example:/tmp/data localhost/example:glibc

$ Build with musl target and dynamic linking:
$ RUSTFLAGS='-C target-feature=-crt-static' cargo build --release --target x86_64-unknown-linux-musl
   Compiling example v0.1.0 (/example)
    Finished `release` profile [optimized] target(s) in 1.32s

# Links the same way as you'd get with glibc:
$ ldd target/x86_64-unknown-linux-musl/release/example
        linux-vdso.so.1 (0x00007f726514a000)
        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f72650bc000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f7264eca000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f726514c000)

# Interpreter is glibc:
$ file target/x86_64-unknown-linux-musl/release/example
target/x86_64-unknown-linux-musl/release/example: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ed074b72ac8f274ca3f836c7383db58178d46d3c, for GNU/Linux 3.2.0, not stripped

Resolving this will require patchelf:

$ dnf install -yq musl-libc patchelf
$ patchelf --set-interpreter /lib/ld-musl-x86_64.so.1 target/x86_64-unknown-linux-musl/release/example

# Corrected:
$ file target/x86_64-unknown-linux-musl/release/example
target/x86_64-unknown-linux-musl/release/example: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, BuildID[sha1]=ed074b72ac8f274ca3f836c7383db58178d46d3c, for GNU/Linux 3.2.0, not stripped

# The interpreter for some reason resolves to glibc dynamic loader 🤷‍♂️
$ ldd target/x86_64-unknown-linux-musl/release/example
        linux-vdso.so.1 (0x00007f15e2b2f000)
        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f15e2a9a000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f15e28a8000)
        /lib/ld-musl-x86_64.so.1 => /lib64/ld-linux-x86-64.so.2 (0x00007f15e2b31000)

# This should be equivalent to `musl-ldd` but seems to be resolving for glibc?:
# NOTE: The musl dynamic loader is effectively checking for these as if you had
# run it with the ENV `LD_LIBRARY_PATH=/usr/x86_64-linux-musl/lib64`
$ /lib/ld-musl-x86_64.so.1 --list target/x86_64-unknown-linux-musl/release/example

        /lib/ld-musl-x86_64.so.1 (0x7f6bbd8d3000)
Error loading shared library libgcc_s.so.1: No such file or directory (needed by target/x86_64-unknown-linux-musl/release/example)
        libc.so.6 => /lib/ld-musl-x86_64.so.1 (0x7f6bbd8d3000)
Error loading shared library ld-linux-x86-64.so.2: No such file or directory (needed by target/x86_64-unknown-linux-musl/release/example)
Error relocating target/x86_64-unknown-linux-musl/release/example: _Unwind_Resume: symbol not found
Error relocating target/x86_64-unknown-linux-musl/release/example: _Unwind_Backtrace: symbol not found
Error relocating target/x86_64-unknown-linux-musl/release/example: _Unwind_GetRegionStart: symbol not found
Error relocating target/x86_64-unknown-linux-musl/release/example: _Unwind_GetTextRelBase: symbol not found
Error relocating target/x86_64-unknown-linux-musl/release/example: _Unwind_RaiseException: symbol not found
Error relocating target/x86_64-unknown-linux-musl/release/example: _Unwind_GetIPInfo: symbol not found
Error relocating target/x86_64-unknown-linux-musl/release/example: _Unwind_GetLanguageSpecificData: symbol not found
Error relocating target/x86_64-unknown-linux-musl/release/example: _Unwind_GetIP: symbol not found
Error relocating target/x86_64-unknown-linux-musl/release/example: _Unwind_GetDataRelBase: symbol not found
Error relocating target/x86_64-unknown-linux-musl/release/example: _Unwind_SetGR: symbol not found
Error relocating target/x86_64-unknown-linux-musl/release/example: _Unwind_DeleteException: symbol not found
Error relocating target/x86_64-unknown-linux-musl/release/example: _Unwind_SetIP: symbol not found

Bring it over to Alpine:

# Transfer to the volume to make accessible to musl container:
$ cp target/x86_64-unknown-linux-musl/release/example /tmp/data/example
$ exit

# Run a musl container:
$ docker run --rm -it --volume example:/tmp/data localhost/example:musl

# Glibc interpreter still linked for some reason 🤷‍♂️
$ ldd /tmp/data/example
        /lib/ld-musl-x86_64.so.1 (0x7fe7076d0000)
        libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x7fe707643000)
        libc.so.6 => /lib/ld-musl-x86_64.so.1 (0x7fe7076d0000)
Error loading shared library ld-linux-x86-64.so.2: No such file or directory (needed by /tmp/data/example)

$ file /tmp/data/example
/tmp/data/example: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, BuildID[sha1]=ed074b72ac8f274ca3f836c7383db58178d46d3c, for GNU/Linux 3.2.0, not stripped

# Fix it by removing the invalid dependency:
$ apk add patchelf
$ patchelf --remove-needed ld-linux-x86-64.so.2 /tmp/data/example

# Now we have the expected interpreter `/lib/ld-musl-x86_64.so.1` working:
# NOTE: Alpine symlinks `/usr/lib/libc.so` to the interpreter.
$ ldd /tmp/data/example
        /lib/ld-musl-x86_64.so.1 (0x7fbd087d0000)
        libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x7fbd08743000)
        libc.so.6 => /lib/ld-musl-x86_64.so.1 (0x7fbd087d0000)

$ /lib/ld-musl-x86_64.so.1 --list /tmp/data/example
        /lib/ld-musl-x86_64.so.1 (0x7fbd087d0000)
        libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x7fbd08743000)
        libc.so.6 => /lib/ld-musl-x86_64.so.1 (0x7fbd087d0000)

# It works:
$ /tmp/data/example
Hello, world!

Build it on Alpine instead

$ docker run --rm -it localhost/example:musl

$ Build with musl target and dynamic linking:
$ RUSTFLAGS='-C target-feature=-crt-static' cargo build --release --target x86_64-unknown-linux-musl
   Compiling example v0.1.0 (/example)

error: linking with `cc` failed: exit status: 1
  |
  = note:  "cc" "-m64" "/tmp/rustciFelTn/symbols.o" "<2 object files omitted>" "-Wl,--as-needed" "-Wl,-Bstatic" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib/{libstd-*,libpanic_unwind-*,libobject-*,libmemchr-*,libaddr2line-*,libgimli-*,librustc_demangle-*,libstd_detect-*,libhashbrown-*,librustc_std_workspace_alloc-*,libminiz_oxide-*,libadler2-*,libunwind-*,libcfg_if-*,liblibc-*,liballoc-*,librustc_std_workspace_core-*,libcore-*,libcompiler_builtins-*}.rlib" "-Wl,-Bdynamic" "-lgcc_s" "-lc" "-Wl,--eh-frame-hdr" "-Wl,-z,noexecstack" "-L" "<sysroot>/lib/rustlib/x86_64-unknown-linux-musl/lib" "-o" "/example/target/x86_64-unknown-linux-musl/release/deps/example-c9cd3c828926ce86" "-Wl,--gc-sections" "-pie" "-Wl,-z,relro,-z,now" "-Wl,-O1" "-Wl,--strip-debug" "-nodefaultlibs"
  = note: some arguments are omitted. use `--verbose` to show all linker arguments
  = note: /usr/lib/gcc/x86_64-alpine-linux-musl/14.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: cannot find Scrt1.o: No such file or directory
          /usr/lib/gcc/x86_64-alpine-linux-musl/14.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: cannot find crti.o: No such file or directory
          collect2: error: ld returned 1 exit status


error: could not compile `example` (bin "example") due to 1 previous error

To avoid that failure, add musl-dev:

$ RUSTFLAGS='-C target-feature=-crt-static' cargo build --release --target x86_64-unknown-linux-musl
   Compiling example v0.1.0 (/example)
    Finished `release` profile [optimized] target(s) in 0.31s

# NOTE: Rather than `libc.so` we have `libc.musl-x86_64.so.1` which is also a symlink to the musl interpreter.
$ ldd target/x86_64-unknown-linux-musl/release/example
        /lib/ld-musl-x86_64.so.1 (0x7f74ab741000)
        libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x7f74ab6b6000)
        libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7f74ab741000)

Rust glibc static

For statically linked glibc, you would need to use -C target-feature=+crt-static (similar to -static) and this would fail without a static glibc to build with. On Fedora this is available via glibc-static package and if needed you can specifically provide that to link.

$ docker run --rm -it localhost/example:glibc
$ RUSTFLAGS='-C target-feature=+crt-static' cargo build --release --target x86_64-unknown-linux-gnu

   Compiling example v0.1.0 (/example)
error: linking with `cc` failed: exit status: 1
  |
  = note:  "cc" "-m64" "/tmp/rustcEOi7A9/symbols.o" "<2 object files omitted>" "-Wl,--as-needed" "-Wl,-Bstatic" "<sysroot>/lib/rustlib/x86_64-unknown-linux-gnu/lib/{libstd-*,libpanic_unwind-*,libobject-*,libmemchr-*,libaddr2line-*,libgimli-*,librustc_demangle-*,libstd_detect-*,libhashbrown-*,librustc_std_workspace_alloc-*,libminiz_oxide-*,libadler2-*,libunwind-*,libcfg_if-*,liblibc-*}.rlib" "-lutil" "-lrt" "-lpthread" "-lm" "-ldl" "-lc" "-lgcc_eh" "-lgcc" "-lc" "<sysroot>/lib/rustlib/x86_64-unknown-linux-gnu/lib/{liballoc-*,librustc_std_workspace_core-*,libcore-*,libcompiler_builtins-*}.rlib" "-Wl,-Bdynamic" "-Wl,--eh-frame-hdr" "-Wl,-z,noexecstack" "-L" "<sysroot>/lib/rustlib/x86_64-unknown-linux-gnu/lib" "-o" "/example/target/x86_64-unknown-linux-gnu/release/deps/example-92a7e02a6848a45b" "-Wl,--gc-sections" "-static-pie" "-Wl,-z,relro,-z,now" "-Wl,-O1" "-Wl,--strip-debug" "-nodefaultlibs"
  = note: some arguments are omitted. use `--verbose` to show all linker arguments
  = note: /usr/sbin/ld: cannot find -lm: No such file or directory
          /usr/sbin/ld: have you installed the static version of the m library ?
          /usr/sbin/ld: cannot find -lc: No such file or directory
          /usr/sbin/ld: have you installed the static version of the c library ?
          /usr/sbin/ld: cannot find -lc: No such file or directory
          /usr/sbin/ld: have you installed the static version of the c library ?
          collect2: error: ld returned 1 exit status


error: could not compile `example` (bin "example") due to 1 previous error

To avoid that failure, add glibc-static:

$ dnf install -yq glibc-static
$ RUSTFLAGS='-C target-feature=+crt-static' cargo build --release --target x86_64-unknown-linux-gnu

   Compiling example v0.1.0 (/example)
    Finished `release` profile [optimized] target(s) in 1.51s

@alexrp
Copy link
Member Author

alexrp commented May 2, 2025

Aren't these opt-in? (glibc => -static + musl => -dynamic)

Yes, and zig-bootstrap builds LLVM statically. But LLVM conflates library and executable linkage, hence that patch being necessary.

The breakage isn't a surprise elsewhere AFAIK when you choose non-default linking for a target? Why require linking inputs manually instead? Is Zig unable to locate system libraries available itself?

I don't really understand this question. But I'm referring to this logic in the compiler:

const link_mode = b: {
const explicitly_exe_or_dyn_lib = switch (options.output_mode) {
.Obj => false,
.Lib => (options.link_mode orelse .static) == .dynamic,
.Exe => true,
};
if (target_util.cannotDynamicLink(target)) {
if (options.link_mode == .dynamic) return error.TargetCannotDynamicLink;
break :b .static;
}
if (explicitly_exe_or_dyn_lib and link_libc and
(target.isGnuLibC() or target_util.osRequiresLibC(target)))
{
if (options.link_mode == .static) return error.LibCRequiresDynamicLinking;
break :b .dynamic;
}
// When creating a executable that links to system libraries, we
// require dynamic linking, but we must not link static libraries
// or object files dynamically!
if (options.any_dyn_libs and options.output_mode == .Exe) {
if (options.link_mode == .static) return error.SharedLibrariesRequireDynamicLinking;
break :b .dynamic;
}
if (options.link_mode) |link_mode| break :b link_mode;
if (explicitly_exe_or_dyn_lib and link_libc and
options.resolved_target.is_native_abi and target.abi.isMusl())
{
// If targeting the system's native ABI and the system's libc is
// musl, link dynamically by default.
break :b .dynamic;
}
// Static is generally a better default. Fight me.
break :b .static;
};

@polarathene
Copy link

polarathene commented May 2, 2025

TL;DR: Since Zig 0.15.0 there is a block on -static with a -gnu / glibc target.

  • If that was not blocked, could this PR enable -static for glibc as well? (I know you rarely would want to do this, but it would be helpful for supporting Eyra target in Rust which hijacks Zig -gnu target)
  • You commit comment refers to avoiding -static flag due to missing libs with -gnu target, but then says it should be possible to static link by providing the inputs:

    Just rely on the Zig compiler to infer the executable link mode from the link inputs instead.


Static glibc attempt

I don't really understand this question

I linked to a comment for context, basically I have this:

main.c:

#include <stdio.h>
int main()
{
    printf("Hello World!\n");
}
# Build static glibc:
zig cc main.c -static -target x86_64-linux-gnu -o example
error: libc of the specified target requires dynamic linking

# Ok... you said I just need to provide the link inputs instead so...:
$ zig cc main.c -static -target x86_64-linux-gnu -o example /usr/lib64/libc.a /usr/lib/gcc/x86_64-redhat-linux/15/libgcc_eh.a
error: libc of the specified target requires dynamic linking

# Ok... without `-static` flag then? (on Zig 0.14.0, this is also the output with `-static` flag):
$ zig cc main.c -target x86_64-linux-gnu -o example /usr/lib64/libc.a /usr/lib/gcc/x86_64-redhat-linux/15/libgcc_eh.a

ld.lld: error: undefined hidden symbol: _init
>>> referenced by libc-start.o:(__libc_start_main_impl) in archive /usr/lib64/libc.a

ld.lld: error: undefined hidden symbol: _fini
>>> referenced by libc-start.o:(call_fini) in archive /usr/lib64/libc.a

It is perhaps an exception, but it will not allow me to build that basic "hello world" example statically with glibc, even though that should be possible? (it is possible with Rust at least when I ensure I have glibc-static package installed)

In Zig 0.13.0 that build error didn't occur, instead it claimed to have built a static glibc executable (but it wrongly had an interpreter set causing segfault):

$ ./example
Segmentation fault (core dumped)

$ ldd example
        statically linked

$ file example
example: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.0.0, with debug_info, not stripped, too many notes (256)

Musl + -dynamic works vs glibc + -static failing

From my earlier reproduction to test this PR, I also get the following with the latest master build (a843be4):

# Build the Dockerfile shared and run it
# (`--no-cache` is required for updating `master` builds if image was previously built):
$ docker build --tag localhost/example --no-cache .
$ docker run --rm -it localhost/example bash

$ zig version
0.15.0-dev.451+a843be44a

# Run build tests:
$ zig-test

Results follow:

# This is glibc (no target specified)
Inspecting 'example'
example: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, with debug_info, not stripped
        linux-vdso.so.1 (0x00007f90b51a3000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f90b4fa9000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f90b51a5000)

musl-libc:

# No `-static`/`-dynamic` flag provided:
Inspecting 'example-musl-default'
example-musl-default: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped
/usr/lib/ld-musl-x86_64.so.1: example-musl-default: Not a valid dynamic program


Inspecting 'example-musl-static'
example-musl-static: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped
/usr/lib/ld-musl-x86_64.so.1: example-musl-static: Not a valid dynamic program


Inspecting 'example-musl-dynamic'
example-musl-dynamic: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, with debug_info, not stripped
        /lib/ld-musl-x86_64.so.1 (0x7f7a668b6000)
        libc.so => /lib/ld-musl-x86_64.so.1 (0x7f7a668b6000)

glibc:

# No `-static`/`-dynamic` flag provided:
Inspecting 'example-gnu-default'
example-gnu-default: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.0.0, with debug_info, not stripped
        linux-vdso.so.1 (0x00007f06c531f000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f06c5125000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f06c5321000)

# Build failed:
Inspecting 'example-gnu-static'
example-gnu-static: cannot open `example-gnu-static' (No such file or directory)
ldd: ./example-gnu-static: No such file or directory


Inspecting 'example-gnu-dynamic'
example-gnu-dynamic: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.0.0, with debug_info, not stripped
        linux-vdso.so.1 (0x00007f929b1e3000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f929afe9000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f929b1e5000)

@alexrp
Copy link
Member Author

alexrp commented May 2, 2025

It is perhaps an exception, but it will not allow me to build that basic "hello world" example statically with glibc, even though that should be possible?

Adding support for static glibc was not a goal of this PR. That will require changes in other places, likely to do with which CRT objects need to be linked in:

.linux => switch (mode) {
.dynamic_lib => .{
.crti = "crti.o",
.crtn = "crtn.o",
},
.dynamic_exe => .{
.crt0 = "crt1.o",
.crti = "crti.o",
.crtn = "crtn.o",
},
.dynamic_pie => .{
.crt0 = "Scrt1.o",
.crti = "crti.o",
.crtn = "crtn.o",
},
.static_exe => .{
.crt0 = "crt1.o",
.crti = "crti.o",
.crtn = "crtn.o",
},
.static_pie => .{
.crt0 = "rcrt1.o",
.crti = "crti.o",
.crtn = "crtn.o",
},
},

The check above that actually emits the error would also need to be tightened to only happen when using Zig-provided glibc.

I'm not opposed to supporting the static glibc use case when using non-Zig-provided glibc, but that just isn't what this PR was about; it was about fixing the dynamic musl use case which was broken both for native and cross builds.

In Zig 0.13.0 that build error didn't occur, instead it claimed to have built a static glibc executable (but it wrongly had an interpreter set causing segfault):

I would at least consider the new behavior strictly better since you don't get the false impression that it worked.

@polarathene
Copy link

polarathene commented May 2, 2025

likely to do with which CRT objects need to be linked in

That'd be fantastic! ❤️

UPDATE: This may have been handled by #23752 (comment) (I've not yet verified a Rust build with Eyra)


That's also another failure I recall (possible two scenarios I encountered with something like Zig adding Scrt1.o when it wasn't expected, or both Zig and Rust providing their own causing a conflict).

One of those failures I think was possibly with a Eyra "hello world" example (or maybe the rustix one), or maybe when I tried to do the equivalent with zig cc directly with a C program but the -gnu target on Zig lacked support for Rust's option -C link-arg=-nostartfiles which removed the need to link those CRT objects? (Eyra provides it's own, so it'd probably run into the same problem).

I think when I tried to look into how to do this on Zig it was not obvious to me. I believe there was a different type of target that had to be used but I was unsuccessful figuring out what I needed to do.

I remember I was attempting to compare how small of a "hello world" I could get with C / Zig without it being overly complicated. I had a 344 bytes hello world (456 bytes with text output) as the smallest result in Rust:

#![no_std]
#![no_main]

#[no_mangle]
pub extern "C" fn _start() -> ! {
  exit(); // +8 bytes to size vs using `loop() {}`
}

fn exit() -> ! { unsafe { rustix::runtime::exit_thread(42) } }

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} }

I don't seem to have the equivalent C / Zig code on me. Might have been Zig from some community member.

Anyway, if you make any improvements on that front I'd be happy to reproduce the problems again and verify if zig cc can better support building those projects.

@alexrp alexrp added the release notes This PR should be mentioned in the release notes. label May 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release notes This PR should be mentioned in the release notes.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[zig cc] unable to dynamically link musl executables
3 participants