Skip to content

Conversation

@vadorovsky
Copy link
Member

@vadorovsky vadorovsky commented Nov 17, 2025

Publish statically linked libraries for:

  • aarch64-apple-darwin
  • aarch64-unknown-linux-musl
  • x86_64-apple-darwin
  • x86_64-unknown-linux-musl

Use libLLVM artifacts from Rust CI. To make sure that the release job does not regress, execute it in a "dry run" mode for each pull request

See individual commits for details.


This change is Reviewable

@vadorovsky vadorovsky force-pushed the binstall branch 30 times, most recently from 784db70 to f38cfb3 Compare November 21, 2025 20:52
Current libbpf works just fine without it. Furthermore, that package
is not available for non x86_64 architectures.
To make the configuration matrix less repetitive, move the toolchain
parameters directly under `matrix`, letting GitHub actions produce
combinations. Use `include` only to specify the parameters of a job
using LLVM from sources.

That will let us to add more platforms LLVM installation sources and
installation sources with less effort.
It's useful for release jobs as well. Extract it to a separate action.

Use `-d` instead of `--max-depth` to make it compatible with the BSD
implementation of `du` that's used on macOS. Do not check the APT
directory on macOS.
Copy link
Member Author

@vadorovsky vadorovsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 0 of 13 files reviewed, 42 unresolved discussions (waiting on @alessandrod, @copilot, and @tamird)


-- commits line 40 at r1:

Previously, tamird (Tamir Duberstein) wrote…

Nice!

Done.


-- commits line 58 at r13:

Previously, tamird (Tamir Duberstein) wrote…

consider putting these simpler commits ahead of the first commit to reduce rebase churn as we go through this review.

Done


build.rs line 15 at r1 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

https://developers.google.com/style/lists#numbered-lettered-bulleted-lists

Done.


build.rs line 194 at r1 (raw file):

Previously, vadorovsky (Michal R) wrote…

Emitting a warning sounds good.

Done.


build.rs line 257 at r1 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

Yeah, that's definitely simpler! I still think emitting some information when applying first-wins would be useful.

Done.


build.rs line 14 at r11 (raw file):

you could do w.write_all($bytes.as_ref()) in the body so that you can pass string literals rather than b"..."

Great idea! I indeed can pass string literals.

and <path>.as_os_str() rather than <path>.as_os_str().as_bytes().

That doesn't work, because OsStr does not implement AsRef<[u8]>. as_bytes is a part of a unix-specific OsStrExt. So I still need to call it explicitly.


build.rs line 19 at r11 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

consider calling io::stdout() fewer times since it acquires a lock

Done.


build.rs line 26 at r11 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

eh I don't think this is necessary

Done.


build.rs line 102 at r11 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

"failed to parse data of static library archive member {} as object file"

Done.


build.rs line 110 at r11 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

technically we got an error getting the name, "failed to ... in static library archive member {}"?

Done.


build.rs line 133 at r11 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

could avoid the allocations with a Cow!

Given that we also have the case where we have two C++ stdlib files (static LLVM libc++ comes in two files - libc++.a+ and libc++abi.a), I wrote my own enum.


build.rs line 151 at r11 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

could avoid the allocations with a Cow!

Done.


build.rs line 185 at r11 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

noble but idk if worth it to hardcode lengths

Done.


build.rs line 302 at r11 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

needs newline? i think you always want a newline, so you may as well name print_bytes something like println_bytes

Done.


build.rs line 343 at r11 (raw file):

Previously, tamird (Tamir Duberstein) wrote…
    let llvm_config = env::split_paths(&paths_os)
        .find_map(|dir| {
            let candidate = Path::new(&dir).join("llvm-config");
            candidate.try_exists().map(|exists| exists.then_some(candidate)).transpose()
        })
        .transpose()
        .with_context(|| format!("could not find llvm-config in {var_name}"))?;

I think?

Almost. Your llvm_config variable is still an Option, but I can handle it with or_or_else.

Also, your with_context applies the message to an error returned by try_exist, where I think a message like "failed to inspect the file {}" would be more correct. "could not find..." suits more as an error for handling an Option with ok_or_else.


build.rs line 367 at r11 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

isn't this just else? see comment below about avoiding duplication with what's in main

Done according to the comment below


build.rs line 388 at r11 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

i think this logic can be in link_llvm, something right at the top like

let link_fn = if cfg!(feature = "no-llvm-linking") {
  if cfg!(feature = "llvm-link-static") {
    // bail
  }
  return;
} else if cfg!(feature = "llvm-link-static") {
  link_llvm_static
} else {
  link_llvm_dynamic
};

or the logic can stay here and you pass the function into link_llvm.

With the first idea, we can just make the former link_llvm the main function.


Cargo.toml line 70 at r11 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

Let's put llvm-link-static above this one? and then we should say here that we're linking libLLVM's dependencies statically rather than enumerating them. How does that sound?

Done. I also put the version features and rust-* features first.


Cargo.toml line 117 at r12 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

Is this the last version that used LLVM 19? Add a comment.

Yes, Rust 1.86.0 is the last version that used LLVM 19.

But now I decided to just drop LLVM 19. Rust 1.87.0 gives us so many nice things, like chained if let and OsStr::display(). I initially added LLVM 19 support, because of Linux distros that are packaging mitmproxy. But given that for now it's just Arch, Gentoo and NixOS and all of them use LLVM >=20, I think it's fine to drop it. 😛


.github/workflows/ci.yml line 49 at r15 (raw file):
To be honest, I don't even understand this comment.

because we care about the apt packages available

We use old Ubuntu because we care about apt packages available? What? 😅

I guess the real reason why we test on older Ubuntu LTS, is that we want to support it, which is a fair goal. If you agree with this explanation, I will rephrase the comment.


.github/workflows/ci.yml line 111 at r15 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

🤔 why only on x86?

There is no gcc-multilib package on arm64.

Anyway, now I removed this block entirely. I don't think we need that package at all - libbpf seems to work just fine without it. And in theory, gcc-multilib should be only needed when building something for 32-bit x86. The fact that the <asm/types.h> headers weren’t available without it sounds pretty sus - it either was never the case, or there was some weird packaging bug.


.github/workflows/ci.yml line 73 at r19 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

this is broken, shared and dynamic are the same thing. i think you meant to say Apple and Linux musl come with static ones?

Yes, sorry, that's what I meant.


.github/workflows/ci.yml line 87 at r19 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

one thing that's a bummer about this representation is that we end up doing a bunch of repeated work e.g. we spin two jobs for x86 linux even though almost all the work done is the same. perhaps instead of this we could just say llvm-from "rust-ci" and then bake the knowledge of which target is shared vs static into the steps below (instead of baking it into the matrix)

Good idea, done.


.github/workflows/ci.yml line 186 at r19 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

we probably don't need to get this from the bleeding edge LLVM so we could keep the LLVM APT dance in the LLVM install stanza above.

I removed the step.


.github/workflows/ci.yml line 219 at r19 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

interesting, is this because >> $GITHUB_PATH puts things at the front of PATH?

also, can this just be unconditional on linux? we're already installing FileCheck above via apt

I removed this step now. After applying your suggestion about unifying rust-ci-{static,shared} into one, this is not needed - we can just use FileCheck from the GNU tarballs.


.github/workflows/ci.yml line 225 at r19 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

s/our GitHub/GitHub Actions/

Done.


.github/workflows/release.yml line 36 at r1 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

Let's come back to this after we resolve the other thread.

OK, since the ABI incompatibility myth is busted, I'm linking the deps too.


.github/workflows/release.yml line 46 at r20 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

are we gonna need releases for each llvm version we support?

The idea which @alessandrod and me are on board in is producing multiple binaries per supported LLVM version (bpf-linker-20, bpf-linker-21) and the main one (bpf-linker) would autodetect which LLVM version is needed for the inputs, then call the appropriate binary. This way, the main binary would work with different Rust versions.

The downside of having different releases per LLVM version would be a necessity to reinstall when switching toolchains. Currently, rustc just looks for bpf-linker in PATH.

I haven't started any work on this yet, I think I would prefer to land this PR first, then #321, and implement that idea afterwards.


xtask/src/main.rs line 221 at r17 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

you shouldn't need this; Command::ouput takes &mut self

https://doc.rust-lang.org/stable/std/process/struct.Command.html#method.output

sigh, it looks like reviewable completely wrecked your comments in this file :(

Not sure what do you mean. I'm assigning output to the variable, to be able to reuse it in the error message.


xtask/src/main.rs line 252 at r17 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

var_os

Done.


xtask/src/main.rs line 256 at r17 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

lmao do we need this

also this can be HeaderValue::from_static

https://docs.rs/reqwest/latest/reqwest/header/struct.HeaderValue.html#method.from_static

ditto below

though I think life is maybe simpler when using requestbuilder::json and requestbuilder::bearer_auth

https://docs.rs/reqwest/latest/reqwest/blocking/struct.RequestBuilder.html#method.bearer_auth

If you mean why I have to do into_iter().collect() - the reason is that HeaderMap implements FromIterator, but doesn't have any convenient From implementation. HeadersMap::insert returns an error, that's annoying - the advantage of collecting an iterator is that I don't have to sprinkle errors.

Comment on lines 218 to 220
let _cmd = rustc_cmd.arg(format!("+{toolchain}"));
}
let _cmd = rustc_cmd.args(["--version", "--verbose"]);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it's rustc, not clippy:

warning: unused result of type `&mut std::process::Command`
   --> xtask/src/main.rs:218:9
    |
218 |         rustc_cmd.arg(format!("+{toolchain}"));
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = note: requested on the command line with `-W unused-results`

warning: unused result of type `&mut std::process::Command`
   --> xtask/src/main.rs:220:5
    |
220 |     rustc_cmd.args(["--version", "--verbose"]);
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

warning: `xtask` (bin "xtask") generated 2 warnings
warning: `xtask` (bin "xtask" test) generated 2 warnings (2 duplicates)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.71s

@vadorovsky vadorovsky force-pushed the binstall branch 7 times, most recently from 248db33 to 2bc24ef Compare November 28, 2025 11:37
llvm-sys' build.rs[0] uses the output of `llvm-config` binary[1] to
figure out where libLLVM libraries live and which additional system
libraries to link. There are several problems with that approach.

The first one is the fact that the host system has to execute
`llvm-config`, which is not always possible. For instance, binaries in
LLVM tarballs provided by Rust community for `*-musl` targets are linked
dynamically to musl, which makes it impossible to execute on distros
with GNU userland. At the same time, libraries in the same tarball are
linked statically to musl (and dynamically to libstdc++), which means
that it's perfectly fine to use them even in GNU userland. Executing
binaries also makes cross-compilation hard.

Avoiding these problems by omitting the build.rs of llvm-sys was
suggested by upstream[2].

The second problem is that `llvm-config` includes all system libraries
that were used for all LLVM subprojects that were built together. Given
that the most of package providers build the whole LLVM monorepo once
and then split binary artifacts into different packages, there are
dependencies we don't need, like libpolly (which gives hard time to our
users, see aya-rs#29). The system libraries we need are: C++ standard library
and compression libraries (zlib and/or zstd, depends on the build).

Furthermore, llvm-sys hard codes the system library paths, making
assumptions about different operating systems[3]. We avoid that by
asking the C compiler about the library paths, which is more reliable.

Last but not least, llvm-sys differentiates between
`force-{dynamic,static}` and `prefer-{dynamic,static}` features, giving
uncertainity about the linkage. There is also no direct control over
how other system libraries are linked.

Our build.rs builds everything dynamically by default and honors two
features:

- `llvm-link-static`, triggering static linkage of libLLVM.
- `llvm-deps-link-static`, triggering static linkage of libraries that
  libLLVM depends on (zlib, zstd).

`llvm-deps-link-static` requires `llvm-link-static` - when libLLVM is
linked dynamically, static linkage of dependencies makes no sense.
Shared libLLVM libraries should come with `DT_NEEDED` referencing all
necessary dependencies. What's great is that these entries are limited
to C++ stdlib and compression libraries and libpolly is not included.

[0] https://gitlab.com/taricorp/llvm-sys.rs/-/blob/e2e64ff03193cd1b548942ac71ec46baa2ad17d8/build.rs
[1] https://gitlab.com/taricorp/llvm-sys.rs/-/blob/e2e64ff03193cd1b548942ac71ec46baa2ad17d8/build.rs#L224-231
[2] https://gitlab.com/taricorp/llvm-sys.rs/-/merge_requests/49#note_2130852482
[3] https://gitlab.com/taricorp/llvm-sys.rs/-/blob/e2e64ff03193cd1b548942ac71ec46baa2ad17d8/build.rs#L388-427

Fixes: aya-rs#29
1.90.0 is the latest version that uses LLVM 20. Use it instead of
1.89.0.

Always use the latest stable, to be able to catch updates in CI.
Add a subcommand that prints the github.com/rust-lang/rust commit from
master that corresponds to an LLVM update for the current Rust
toolchain. The goal is to make downloading LLVM artifacts from Rust CI
easier.
@vadorovsky vadorovsky force-pushed the binstall branch 2 times, most recently from fbacf75 to 43867e9 Compare November 28, 2025 12:36
Copy link
Member Author

@vadorovsky vadorovsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 0 of 13 files reviewed, 42 unresolved discussions (waiting on @alessandrod and @tamird)


Cargo.toml line 86 at r1 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

Let's discuss in the other thread.

Removed.


.github/workflows/ci.yml line 146 at r16 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

this is slightly spooky, does it need a comment?

What's spooky here? This is exactly how it used to be.


.github/workflows/ci.yml line 80 at r19 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

leaving a placeholder here because this ABI compat claim might need revisiting after the first commit discussion is resolved

Removed.


.github/workflows/ci.yml line 207 at r19 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

use a switch-case here and explicitly blow up on an unexpected value (i think this is checked above but it might one day not be)

Removed the comment and I've managed to remove the libpolly requirement.


.github/workflows/ci.yml line 216 at r19 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

does this need a comment?

No, the requirement of libpolly is removed now.


xtask/src/main.rs line 270 at r17 (raw file):

Previously, tamird (Tamir Duberstein) wrote…

why not search commits? https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-commits

wouldn't that get you there in a single API call instead of two?

The problem is that you have to find the bors' merge commit that bumps LLVM, e.g.:

rust-lang/rust@5f666a6

The commit title is "Auto merge of... [branch-name]". The actual title of the PR - "Update LLVM..." - is embedded in the commit message. There is no search qualifier which allows full-text search of commit messages in /search/commits?q=.

@vadorovsky vadorovsky force-pushed the binstall branch 3 times, most recently from 7cb7256 to 861434b Compare November 28, 2025 17:21
We are planning to use libLLVM from Rust CI to produce static binaries.
This change makes sure that tests work with Rust's libLLVM artifacts
and libLLVM linked statically (from any source).

Rust CI ships only one flavor of libLLVM, shared or static, per target.
Linux GNU targets come with shared ones. Apple and Linux musl targets
come with dynamic ones.

To save disk space, run `cargo hack --feature-powerset` only when
shared libraries are used. Otherwise, run cargo with a single feature
set.
- Use LLVM from Rust CI instead of LLVM built from source.
- Support for macOS and Linux ARM.
- Add the disk usage reports.
- To make sure that the job does not regress, execute it in a "dry run"
  mode for each pull request.
Copy link
Member

@tamird tamird left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tamird reviewed 13 of 13 files at r21, 1 of 1 files at r22, 5 of 5 files at r23, 7 of 7 files at r24, 8 of 8 files at r32, 6 of 6 files at r33, 1 of 1 files at r34, 1 of 1 files at r35, 1 of 1 files at r36, 4 of 4 files at r37, 2 of 2 files at r40, 1 of 1 files at r41, 2 of 2 files at r43, 1 of 1 files at r44, 1 of 1 files at r45, 1 of 1 files at r46, all commit messages.
Reviewable status: all files reviewed, 18 unresolved discussions (waiting on @vadorovsky)


build.rs line 257 at r1 (raw file):

Previously, vadorovsky (Michal R) wrote…

Done.

this one is not done?


.github/workflows/ci.yml line 49 at r15 (raw file):

Previously, vadorovsky (Michal R) wrote…

To be honest, I don't even understand this comment.

because we care about the apt packages available

We use old Ubuntu because we care about apt packages available? What? 😅

I guess the real reason why we test on older Ubuntu LTS, is that we want to support it, which is a fair goal. If you agree with this explanation, I will rephrase the comment.

We used to care about the specific version of clang that was installed (but not in PATH) see aya-rs/aya@59a1530. I'm not sure if it mattered here in bpf-linker.


.github/workflows/ci.yml line 111 at r15 (raw file):

Previously, vadorovsky (Michal R) wrote…

There is no gcc-multilib package on arm64.

Anyway, now I removed this block entirely. I don't think we need that package at all - libbpf seems to work just fine without it. And in theory, gcc-multilib should be only needed when building something for 32-bit x86. The fact that the <asm/types.h> headers weren’t available without it sounds pretty sus - it either was never the case, or there was some weird packaging bug.

heh aya-rs/aya@2907bc6


.github/workflows/ci.yml line 146 at r16 (raw file):

Previously, vadorovsky (Michal R) wrote…

What's spooky here? This is exactly how it used to be.

DYLD_LIBRARY_PATH is new, isn't it?


.github/workflows/ci.yml line 216 at r19 (raw file):

Previously, vadorovsky (Michal R) wrote…

No, the requirement of libpolly is removed now.

I think you need to click on the r19 at the top right corner of this comment box (and others) to properly see what I was commenting on.

In this case it was on LD_LIBRARY_PATH


xtask/src/main.rs line 221 at r17 (raw file):

Previously, vadorovsky (Michal R) wrote…

sigh, it looks like reviewable completely wrecked your comments in this file :(

Not sure what do you mean. I'm assigning output to the variable, to be able to reuse it in the error message.

It was a comment on cmd_for_error which seems to be gone now.


xtask/src/main.rs line 256 at r17 (raw file):

Previously, vadorovsky (Michal R) wrote…

If you mean why I have to do into_iter().collect() - the reason is that HeaderMap implements FromIterator, but doesn't have any convenient From implementation. HeadersMap::insert returns an error, that's annoying - the advantage of collecting an iterator is that I don't have to sprinkle errors.

No this comment was anchored on USER_AGENT.

But see the other comments, maybe RequestBuilder is better here?


xtask/src/main.rs line 270 at r17 (raw file):

Previously, vadorovsky (Michal R) wrote…

The problem is that you have to find the bors' merge commit that bumps LLVM, e.g.:

rust-lang/rust@5f666a6

The commit title is "Auto merge of... [branch-name]". The actual title of the PR - "Update LLVM..." - is embedded in the commit message. There is no search qualifier which allows full-text search of commit messages in /search/commits?q=.

Ah it turns out the issue is actually that github doesn't index commits for big repos like rust. drat.


.github/workflows/ci.yml line 135 at r41 (raw file):

        # [1] https://github.com/llvm/llvm-project/commit/dc1c43d
        run: |
          set -euxo pipefail

why this change?


.github/workflows/ci.yml line 310 at r41 (raw file):

        #
        # libLLVM from homebrew requires zstd.
        run: |

i think we need the set -euxo pipefail any time we have multiline scripts like this


build.rs line 106 at r33 (raw file):

            OsStr::from_bytes(first).display().fmt(f)?;
        }
        for lib in libs {

easier with .enumerate, no?


build.rs line 260 at r33 (raw file):

        false
    };
    let zstd_found = if needs_zstd {

needs_zstd && emit_search_path_if_defined(stdout, "LIBZSTD_PATH")?? same elsewhere


build.rs line 321 at r33 (raw file):

        for ld_path in env::split_paths(ld_paths) {
            let mut found_any = false;
            if !cxxstdlib_found && let Some(ref mut cxxstdlib_paths) = cxxstdlib_paths {

why if !cxxstdlib_found? i think this is the same as the let


build.rs line 333 at r33 (raw file):

            }
            if needs_zlib
                && !zlib_found

sam here


build.rs line 345 at r33 (raw file):

            }
            if needs_zstd
                && !zstd_found

and here


build.rs line 358 at r33 (raw file):

            if found_any {
                write_bytes!(
                    io::stdout(),

stdout


build.rs line 393 at r33 (raw file):

                            write_bytes!(stdout, ", ", path.as_os_str().as_bytes())?;
                        }
                        write_bytes!(stdout, "\n")?;

ditto, easier with enumerate? also i think you can just use write! for all of it so that you can bake the newline into write_bytes

Code quote:

                        let mut paths = paths.iter();
                        if let Some(first) = paths.next() {
                            write_bytes!(stdout, first.as_os_str().as_bytes())?;
                        }
                        for path in paths {
                            write_bytes!(stdout, ", ", path.as_os_str().as_bytes())?;
                        }
                        write_bytes!(stdout, "\n")?;

.github/workflows/ci.yml line 118 at r33 (raw file):

          sudo apt update
          sudo apt -y install llvm-${{ matrix.llvm-version }}-dev
          echo /usr/lib/llvm-${{ matrix.llvm-version }}/bin >> $GITHUB_PATH

leftover?

@vadorovsky
Copy link
Member Author

@tamird We have one build failure, which I understand why is happening, but I'm unsure which solution is the least terrible.

We are facing this error when trying to link statically LLVM from sources:

https://github.com/aya-rs/bpf-linker/actions/runs/19768633299/job/56647522117#step:27:38

  = note: some arguments are omitted. use `--verbose` to show all linker arguments
  = note: rust-lld: error: undefined symbol: std::__glibcxx_assert_fail(char const*, int, char const*, char const*)
          >>> referenced by DominanceFrontierImpl.h:0 (/home/runner/work/bpf-linker/bpf-linker/llvm-project/llvm/include/llvm/Analysis/DominanceFrontierImpl.h:0)
          >>>               MachineDominanceFrontier.cpp.o:(llvm::ForwardDominanceFrontierBase<llvm::MachineBasicBlock>::calculate(llvm::DominatorTreeBase<llvm::MachineBasicBlock, false> const&, llvm::DomTreeNodeBase<llvm::MachineBasicBlock> const*)) in archive /home/runner/work/bpf-linker/bpf-linker/target/debug/deps/libbpf_linker-31eec614a7a1be06.rlib
          >>> referenced by stl_vector.h:1319 (/usr/bin/../lib/gcc/x86_64-linux-gnu/12/../../../../include/c++/12/bits/stl_vector.h:1319)
          >>>               MachineDominanceFrontier.cpp.o:(llvm::ForwardDominanceFrontierBase<llvm::MachineBasicBlock>::calculate(llvm::DominatorTreeBase<llvm::MachineBasicBlock, false> const&, llvm::DomTreeNodeBase<llvm::MachineBasicBlock> const*)) in archive /home/runner/work/bpf-linker/bpf-linker/target/debug/deps/libbpf_linker-31eec614a7a1be06.rlib
          >>> referenced by optional:477 (/usr/bin/../lib/gcc/x86_64-linux-gnu/12/../../../../include/c++/12/optional:477)
          >>>               MachineDominanceFrontier.cpp.o:(llvm::MachineDominanceFrontier::runOnMachineFunction(llvm::MachineFunction&)) in archive /home/runner/work/bpf-linker/bpf-linker/target/debug/deps/libbpf_linker-31eec614a7a1be06.rlib
          >>> referenced 2921 more times
          collect2: error: ld returned 1 exit status

The error suggests that the libLLVM was linked to stdlibc++ 12 (dynamically), but now we are trying to link stdlibc++ 11. stdlibc++ is forwards, but not backwards compatible. libLLVM has undefined std::__glibcxx_assert_fail symbol from 12 which is not there in 11.

But the question is - why the heck was our custom LLVM build linked to a newer libstdc++?

The answer is - we build LLVM with clang:

"-DCMAKE_C_COMPILER=clang",
"-DCMAKE_CXX_COMPILER=clang++",

that reports the following library directories:

runner@runnervmfxdz0:~/work/bpf-linker/bpf-linker$ clang -print-search-dirs
programs: =/usr/lib/llvm-15/bin:/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/bin
libraries: =/usr/lib/llvm-15/lib/clang/15.0.7:/usr/lib/gcc/x86_64-linux-gnu/12:/usr/lib/gcc/x86_64-linux-gnu/12/../../../../lib64:/lib/x86_64-linux-gnu:/lib/../lib64:/usr/lib/x86_64-linux-gnu:/usr/lib/../lib64:/lib:/usr/lib

The second library directory - /usr/lib/gcc/x86_64-linux-gnu/12 - is the reason why libstdc++ 12 is picked.

The default C compiler, however, is GCC 11:

runner@runnervmfxdz0:~/work/bpf-linker/bpf-linker$ cc --version
cc (Ubuntu 11.4.0-1ubuntu1~22.04.2) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

runner@runnervmfxdz0:~/work/bpf-linker/bpf-linker$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04.2) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Which reports the following directories:

runner@runnervmfxdz0:~/work/bpf-linker/bpf-linker$ cc -print-search-dirs
install: /usr/lib/gcc/x86_64-linux-gnu/11/
programs: =/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../../x86_64-linux-gnu/bin/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../../x86_64-linux-gnu/bin/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../../x86_64-linux-gnu/bin/
libraries: =/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../../x86_64-linux-gnu/lib/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../../x86_64-linux-gnu/lib/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../../x86_64-linux-gnu/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../../lib/:/lib/x86_64-linux-gnu/11/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/11/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../../x86_64-linux-gnu/lib/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../:/lib/:/usr/lib/

/usr/lib/gcc/x86_64-linux-gnu/11/ - that's the reason why build.rs tries to link libstdc++11!

That said, GCC 12 called explicitly reports its own directories:

runner@runnervmfxdz0:~/work/bpf-linker/bpf-linker$ gcc-12 -print-search-dirs
install: /usr/lib/gcc/x86_64-linux-gnu/12/
programs: =/usr/lib/gcc/x86_64-linux-gnu/12/:/usr/lib/gcc/x86_64-linux-gnu/12/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/12/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/bin/x86_64-linux-gnu/12/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/bin/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/bin/
libraries: =/usr/lib/gcc/x86_64-linux-gnu/12/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/lib/x86_64-linux-gnu/12/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/lib/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/12/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../../lib/:/lib/x86_64-linux-gnu/12/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/12/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/lib/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../:/lib/:/usr/lib/

And therefore, building bpf-linker with CC=gcc-12 on that exact runner works fine:

runner@runnervmfxdz0:~/work/bpf-linker/bpf-linker$ CC=gcc-12 cargo build --no-default-features --features llvm-21,llvm-link-static
[...]
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.09s

Same with CC=clang:

runner@runnervmfxdz0:~/work/bpf-linker/bpf-linker$ CC=clang cargo build --no-default-features --features llvm-21,llvm-link-static
[...]
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.09s

So now it looks like we have the following options to solve the problem:

  1. Make sure that our custom LLVM is built with stdlibc++ 11.
    a) We could try to enforce the GCC 11 library dir through LDFLAGS="-L /usr/lib/gcc/x86_64-linux-gnu/11/lib". But that might get messy.
    b) We could build LLVM with the default GCC by not providing -DCMAKE_C_COMPILER. But that would likely increase the build time.
  2. Define CC=gcc-12 or CC=clang. Easy fix, but will require messy dance if GitHub action predicates to apply it only matrix.llvm-from = "source".
  3. Skip the static build steps for matrix.llvm-from = 'source' and just live with the fact we test static linking just for tarballs and packages.

I'm personally leaning towards 1) - starting from a) and falling back to b) if it gets too messy. Any preferences from your side?

@tamird
Copy link
Member

tamird commented Nov 28, 2025

@tamird We have one build failure, which I understand why is happening, but I'm unsure which solution is the least terrible.

We are facing this error when trying to link statically LLVM from sources:

https://github.com/aya-rs/bpf-linker/actions/runs/19768633299/job/56647522117#step:27:38

  = note: some arguments are omitted. use `--verbose` to show all linker arguments
  = note: rust-lld: error: undefined symbol: std::__glibcxx_assert_fail(char const*, int, char const*, char const*)
          >>> referenced by DominanceFrontierImpl.h:0 (/home/runner/work/bpf-linker/bpf-linker/llvm-project/llvm/include/llvm/Analysis/DominanceFrontierImpl.h:0)
          >>>               MachineDominanceFrontier.cpp.o:(llvm::ForwardDominanceFrontierBase<llvm::MachineBasicBlock>::calculate(llvm::DominatorTreeBase<llvm::MachineBasicBlock, false> const&, llvm::DomTreeNodeBase<llvm::MachineBasicBlock> const*)) in archive /home/runner/work/bpf-linker/bpf-linker/target/debug/deps/libbpf_linker-31eec614a7a1be06.rlib
          >>> referenced by stl_vector.h:1319 (/usr/bin/../lib/gcc/x86_64-linux-gnu/12/../../../../include/c++/12/bits/stl_vector.h:1319)
          >>>               MachineDominanceFrontier.cpp.o:(llvm::ForwardDominanceFrontierBase<llvm::MachineBasicBlock>::calculate(llvm::DominatorTreeBase<llvm::MachineBasicBlock, false> const&, llvm::DomTreeNodeBase<llvm::MachineBasicBlock> const*)) in archive /home/runner/work/bpf-linker/bpf-linker/target/debug/deps/libbpf_linker-31eec614a7a1be06.rlib
          >>> referenced by optional:477 (/usr/bin/../lib/gcc/x86_64-linux-gnu/12/../../../../include/c++/12/optional:477)
          >>>               MachineDominanceFrontier.cpp.o:(llvm::MachineDominanceFrontier::runOnMachineFunction(llvm::MachineFunction&)) in archive /home/runner/work/bpf-linker/bpf-linker/target/debug/deps/libbpf_linker-31eec614a7a1be06.rlib
          >>> referenced 2921 more times
          collect2: error: ld returned 1 exit status

The error suggests that the libLLVM was linked to stdlibc++ 12 (dynamically), but now we are trying to link stdlibc++ 11. stdlibc++ is forwards, but not backwards compatible. libLLVM has undefined std::__glibcxx_assert_fail symbol from 12 which is not there in 11.

But the question is - why the heck was our custom LLVM build linked to a newer libstdc++?

The answer is - we build LLVM with clang:

"-DCMAKE_C_COMPILER=clang",
"-DCMAKE_CXX_COMPILER=clang++",

that reports the following library directories:

runner@runnervmfxdz0:~/work/bpf-linker/bpf-linker$ clang -print-search-dirs
programs: =/usr/lib/llvm-15/bin:/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/bin
libraries: =/usr/lib/llvm-15/lib/clang/15.0.7:/usr/lib/gcc/x86_64-linux-gnu/12:/usr/lib/gcc/x86_64-linux-gnu/12/../../../../lib64:/lib/x86_64-linux-gnu:/lib/../lib64:/usr/lib/x86_64-linux-gnu:/usr/lib/../lib64:/lib:/usr/lib

The second library directory - /usr/lib/gcc/x86_64-linux-gnu/12 - is the reason why libstdc++ 12 is picked.

The default C compiler, however, is GCC 11:

runner@runnervmfxdz0:~/work/bpf-linker/bpf-linker$ cc --version
cc (Ubuntu 11.4.0-1ubuntu1~22.04.2) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

runner@runnervmfxdz0:~/work/bpf-linker/bpf-linker$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04.2) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Which reports the following directories:

runner@runnervmfxdz0:~/work/bpf-linker/bpf-linker$ cc -print-search-dirs
install: /usr/lib/gcc/x86_64-linux-gnu/11/
programs: =/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../../x86_64-linux-gnu/bin/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../../x86_64-linux-gnu/bin/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../../x86_64-linux-gnu/bin/
libraries: =/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../../x86_64-linux-gnu/lib/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../../x86_64-linux-gnu/lib/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../../x86_64-linux-gnu/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../../lib/:/lib/x86_64-linux-gnu/11/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/11/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../../x86_64-linux-gnu/lib/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../:/lib/:/usr/lib/

/usr/lib/gcc/x86_64-linux-gnu/11/ - that's the reason why build.rs tries to link libstdc++11!

That said, GCC 12 called explicitly reports its own directories:

runner@runnervmfxdz0:~/work/bpf-linker/bpf-linker$ gcc-12 -print-search-dirs
install: /usr/lib/gcc/x86_64-linux-gnu/12/
programs: =/usr/lib/gcc/x86_64-linux-gnu/12/:/usr/lib/gcc/x86_64-linux-gnu/12/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/12/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/bin/x86_64-linux-gnu/12/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/bin/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/bin/
libraries: =/usr/lib/gcc/x86_64-linux-gnu/12/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/lib/x86_64-linux-gnu/12/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/lib/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/12/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../../lib/:/lib/x86_64-linux-gnu/12/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/12/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/lib/:/usr/lib/gcc/x86_64-linux-gnu/12/../../../:/lib/:/usr/lib/

And therefore, building bpf-linker with CC=gcc-12 on that exact runner works fine:

runner@runnervmfxdz0:~/work/bpf-linker/bpf-linker$ CC=gcc-12 cargo build --no-default-features --features llvm-21,llvm-link-static
[...]
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.09s

Same with CC=clang:

runner@runnervmfxdz0:~/work/bpf-linker/bpf-linker$ CC=clang cargo build --no-default-features --features llvm-21,llvm-link-static
[...]
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.09s

So now it looks like we have the following options to solve the problem:

  1. Make sure that our custom LLVM is built with stdlibc++ 11.
    a) We could try to enforce the GCC 11 library dir through LDFLAGS="-L /usr/lib/gcc/x86_64-linux-gnu/11/lib". But that might get messy.
    b) We could build LLVM with the default GCC by not providing -DCMAKE_C_COMPILER. But that would likely increase the build time.
  2. Define CC=gcc-12 or CC=clang. Easy fix, but will require messy dance if GitHub action predicates to apply it only matrix.llvm-from = "source".
  3. Skip the static build steps for matrix.llvm-from = 'source' and just live with the fact we test static linking just for tarballs and packages.

I'm personally leaning towards 1) - starting from a) and falling back to b) if it gets too messy. Any preferences from your side?

Oof. What about 2) but just do it unconditionally?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants