Why is "--target" next to impossible to use right?

At a first glance, Rust's --target option seems like a very convenient way to build your project for various targets. But when you actually try to use it, it quickly turns out that this does not work most of the time for any --target other then than your current build platform :roll_eyes:

For example, I am building on a Linux (64-Bit) system and I want to use --target=x86_64-unknown-freebsd to create a FreeBSD binary, but this fails with:

error[E0463]: can't find crate for `core`
  |
  = note: the `x86_64-unknown-freebsd` target may not be installed
  = help: consider downloading the target with `rustup target add x86_64-unknown-freebsd`

Fair enough! So I do exactly what Rust told me and I run:
rustup target add x86_64-unknown-freebsd

Installation succeeded.

But build still fails! This time with a cryptic error message and no hint what to do:

error: linking with `cc` failed: exit status: 1
  |
  = note:  "cc" "-m64" "<1 object files omitted>" "-Wl,--as-needed" "-Wl,-Bstatic" "<sysroot>/lib/rustlib/x86_64-unknown-freebsd/lib/libcompiler_builtins-*.rlib" "-Wl,-Bdynamic" "-lrt" "-lutil" "-lexecinfo" "-lkvm" "-lmemstat" "-lkvm" "-lutil" "-lprocstat" "-lrt" "-ldevstat" "-lexecinfo" "-lpthread" "-lgcc_s" "-lc" "-lm" "-lrt" "-lpthread" "-lrt" "-lutil" "-lexecinfo" "-lkvm" "-lmemstat" "-lkvm" "-lutil" "-lprocstat" "-lrt" "-ldevstat" "-L" "/tmp/rustcvkABXZ/raw-dylibs" "-Wl,--eh-frame-hdr" "-Wl,-z,noexecstack" "-o" "/home/me/src/app/target/x86_64-unknown-freebsd/release/deps/app-e6f8145791fa06db" "-Wl,--gc-sections" "-pie" "-Wl,-z,relro,-z,now" "-Wl,-O1" "-Wl,--strip-all" "-nodefaultlibs"
  = note: some arguments are omitted. use `--verbose` to show all linker arguments
  = note: /usr/bin/ld: cannot find -lexecinfo: No such file or directory
          /usr/bin/ld: cannot find -lkvm: No such file or directory
          /usr/bin/ld: cannot find -lmemstat: No such file or directory
          /usr/bin/ld: cannot find -lkvm: No such file or directory
          /usr/bin/ld: cannot find -lprocstat: No such file or directory
          /usr/bin/ld: cannot find -ldevstat: No such file or directory
          /usr/bin/ld: cannot find -lexecinfo: No such file or directory
          collect2: error: ld returned 1 exit status

What does this mean? :confused:

Did I do something wrong? Did I encounter a bug in Rust? Impossible to know!

At this point I can only give up. Or look for bug reports on GitHub of people who ran into the same problem. I did the latter and, as it turns out, I need to install clang and I need to explicitly tell Rust to use it by adding -Clinker=clang to the RUSTFLAGS. Plus, I also need to pass the correct target to clang by adding -Clink-arg=--target=x86_64-unknown-freebsd too.

Why, if Rust needs to use clang for --target=x86_64-unknown-freebsd to work, it doesn't tell me so? Why it doesn't tell me that I need to install clang, just like it did tell me that I need to install the target via rustup? And why it doesn't automatically use clang, once it is installed? Finally, why do I manually need to add the completely redundant -Clink-arg=--target=x86_64-unknown-freebsd to RUSTFLAGS when I'm already building with --target=x86_64-unknown-freebsd and therefore Rust shall be able to forward the correct parameters to clang by itself?

And the journey doesn't end here! Even with all of the above, Rust still fails to build. The missing piece again is an information that can only ever be found by reading through bug reports on GitHub: We need to download a so-called "sysroot" from the FreeBSD web-site, extract it to our Linux system and add -Clink-arg=--sysroot=/sysroot/freebsd/amd64 to the RUSTFLAGS. Sorry, but how in the world can anybody be expected to figure out that? If building with --target=x86_64-unknown-freebsd necessarily requires the matching "sysroot", then why (why?) Rust doesn't tell me so? Or, even better, why rustup target add does not simply download + install the "sysroot" stuff that it is inevitably going to require anyway?

Conclusion: At this point Rust's --target option is a big mess that is next to impossible to use right. Error messages are absolutely cryptic, and hints on how to fix them are lacking. Much of this could easily be avoided if Rust would automatically be doing the "right" things that are inevitably required anyway – rather than forcing the user to figure out obscure workarounds, which can only ever be found by reading through the bug reports on GitHub...

Are there any chances that this will be improved ???

Cheers!

8 Likes

Yes. If someone would do something to improve that situation. But note that answers to all these questions have nothing to do with Rust, they would be the exact same for for any target that uses libc (the majority of non-embedded targets). Worse: for anyone who did cross-compilation with C the answers would be obvious: of course you need to specify sysroot, C compiler and linker… how can it work otherwise?

The answer to the how can it work otherwise is obvious: if someone with enough free time would add that logic to rust then it would be able to do more concise error messages, offer to use clang or maybe to use it automatically, etc.

Sadly most of people who can do such work are also satisfied with the status quo: they are using cargo cross and don't even think about what happens below…

If someone would sink few man-years of work (yes, I'm not joking, with the wast matrix of host x target it's few years of work to make it even remotely usable) into that then sure… do you volunteer?

1 Like

A perhaps quicker solution for a smaller improvement would be to just improve the error message, perhaps to link to a page about sysroot, compiler flags etc. For people who don't have a C/C++ background I can see how this can be quite confusing.

So even just having a message like "cross compiling can be complicated, see this link for a high level overview" would be a good improvement, that doesn't require man years.

I think such a page should mention that third party projects like cargo-cross and cargo-zigbuild exist (I believe there is one for cross compiling to windows too, I have not tried that).

5 Likes

I disagree.

There is an implementation detail that Rust/Cargo would like to think it's not their problem, but the reality is that the compilation is broken.

This is Rust's problem in the sense that user tells Rust to build a binary, and Rust doesn't build a working binary. Rust congratulates itself for building part of it, and then gives up. That's a lousy unfinished job.

9 Likes

I fell into the same error days ago (from MacOS to Linux), and simply gave up due to how hard to understand the error looks.

It was fine for embedded though, but I did use some configs.

When working with C, we are dealing with the "low-level" tools like the C compiler and the linker directly, unless we use something like Autotools or CMake. This obviously makes cross-building much more complicated, but also more explicit. I certainly don't want to say that the situation in C is better than in Rust when it comes to cross-building. Actually quite the opposite.

But the "problem" with Rust/Cargo is that it usually hides all that "low-level" stuff from you. And that is awesome for as long as it does work. But that quickly falls apart when we try to use the --target option! And then we have to deal with a weird "mix" of Cargo pretending to be the "high-level" tool that handles all the "low-level" stuff for us, but because it is failing miserably to get things right in this situation, we have to work around what Cargo is doing, by injecting various "low-level" workarounds through the back door, i.e., via RUSTFLAGS.

This is simply a very confusing situation, especially with those unhelpful error messages...

1 Like

I can't find it, but I seem to recall a discussion of improving this type of error. The problem is that doing so would require either parsing the error output of the linker, which varies by platform, or having Rust ship a linker of its own that handles all targets. Without one of these, there is no way to give better advice because rustc & cargo only know that the linker failed, not why.

1 Like

The original hint that you need to add a target with rustup could be improved to also say something like: "note: this may not be enough if you also have C code or target a non-embedded platform, see LINK for more information"

3 Likes

I also remember how much of a pain it was to figure but what targets there are and what they are for. The best documentation on this probably was a specific episode of SDR. I know there is a list in the Rust book, but that is probably the only place where they are all listed?

Not to mention also trying to figure out what target corresponds to a random machine on GitHub Actions or Hetzner or wherever. Even if realistically it will be impossible to cross-compile for that target anyway.

It is a tricky and messy integration point, but this doesn't make it any less of a problem.

Rust can do much more here. For example, when cross-compiling it's very rare for the default cc to support other targets, so Rust could warn about the linker woes whenever the default linker fails in cross-compilation. Rust could even run a trivial test link (with an empty or hello world binary) to see if the linker works at all, and then it'd be able to tell whether it's a bad linker or issue with the current arguments.

It could parse the output too. There's a finite amount of linkers in the world, and supporting the top few would go a long way towards making linking less daunting. It doesn't have to understand every edge case message, but it could be much more helpful in cases it can parse. Rust already has to support different linker flavors to set appropriate flags, so it's not linker-agnostic.

Rust/Cargo could also be smarter by detecting a better default linker automatically. It can't be done universally, but it would be massively helpful where it could be done, e.g. Debian has predictable paths for cross builds, and Rust/Cargo could make it work smoothly out of the box (making lots of people cross-building for Raspberry PIs happy).

I think it's all doable, and the buggest obstacle here is not technical, the mindset of "it's not Rust's fault, not Rust's problem, it's has always been bad, it's even worse in C, so it's ok to let it be bad in Rust too". Which is disappointing, because Rust/Cargo already work around a ton of things that are chronically broken in C toolchains.

5 Likes

Well, yes. I did not mean that the idea was rejected, but that the discussion highlighted the complexity of it.

There are many many simple things that could be done that would improve the situation, without tackling the full complex problem.

Don't let perfect be the enemy of good. I have noticed the Rust project has a tendency of doing this, many many times. (Why is the allocator API still nowhere near stabilisation for example?)

Part of the problem is that, if you're not careful, good becomes the enemy of good. If they stabilized the allocator API in a sub-par state, we may well be stuck with a bad API forever. Similar issues probably apply to many parts of cross-comp in Rust.

1 Like

It's notable that even zig and Go which both put a lot of effort into supporting cross compilation both still have issues, and the instant you have any FFI you're adding back the whole problem again. Zig choosing to take on the role of being the modern cross platform toolchain might be it's biggest legacy.

3 Likes