Skip to content
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

Cargo wrongly links std to no_std crate with conditional dependency on std, even when that dependency is not selected #6571

Closed
cbeck88 opened this issue Jan 20, 2019 · 13 comments
Labels
A-features Area: features — conditional compilation C-bug Category: bug

Comments

@cbeck88
Copy link

cbeck88 commented Jan 20, 2019

Problem

In the minimal test case (https://github.com/cbeck88/rust-cargo-bug), a library is marked no_std and which has no dependency on std is nevertheless linked to std by cargo, wrongly. This breaks the build, because it conflicts with provided lang items.

Yet, when a conditional dependency which is not even selected or built by cargo is commented out in Cargo.toml, cargo stops linking std to the first target, and the build succeeds.

This indicates that the logic that determines whether a crate transitively depends on std is broken and doesn't take into account conditional dependencies properly.

Steps

To reproduce the bug, use the minimal verifiable example provided here: https://github.com/cbeck88/rust-cargo-bug

  1. clone the repository
  2. cargo build, or cargo build --target=x86_64-unknown-linux-gnu if you are not on an x86_64 machine. Observe build failure
  3. git apply the patch, which comments out conditional dependency on rand
  4. Build again and observe that it succeeds.

Possible Solution(s)

I suspect that the logic that determines whether std is needed, ignores, or doesn't properly take into account, whether or not a conditional dependency was actually selected.

Notes

Output of cargo version:

Using stable cargo and rust on Ubuntu 18.04:

$ cargo --version
cargo 1.30.0
$ rustc --version
rustc 1.30.0
@cbeck88 cbeck88 added the C-bug Category: bug label Jan 20, 2019
@cbeck88
Copy link
Author

cbeck88 commented Jan 20, 2019

@dwijnand I don't see that any of those tickets has to do with my issue -- they are all about features, and there are no cfg features in my example, I am using cfg of target-arch

This issue specifically has to do with this syntax:

[target.'cfg(not(target_arch = "x86_64"))'.dependencies]
rand = { version = "0.6" }

Cargo is giving me std on x86_64 arch because these two lines are present, which doesn't make any sense, and when we comment them out, it stops doing that.

This has nothing to do with default features, or features being unioned across the build plan. rand is not a dependency of my crate on x86_64, nevertheless std gets linked in when I build. That's the bug.

@dwijnand
Copy link
Member

Let me premise by saying we can reopen if I'm wrong, but I think that's already covered by these comments in those issues:

@cbeck88
Copy link
Author

cbeck88 commented Jan 22, 2019

So, I'm skeptical that this is really the same issue, because when we change it from using target_arch in the cfg expressions to simply adding a feature, and making rand an optional dependency gated on that feature, it simply works, in our actual project in tree.

I can try to add such a demonstration in the minimal verifiable example.

This suggests that there is more to the story than e.g. @alexcrichton 's response to this comment: #2589 (comment)

When we build the resolution graph today it contains information about all targets, and the filtering per platform only happens at the very end when we're compiling.

otherwise why doesn't the same issue occur when we branch on features instead of target_arch?

Contrary to what is written there in 2016, It seems that cargo is successfully able to avoid building rand in x86_64, AND when using the cfg(not(target_arch = ...)) expression. The discrepancy in my example is specific to std and not any other crate.

@dwijnand
Copy link
Member

A minimal verificable example would be lovely, thank you.

@dwijnand dwijnand reopened this Jan 22, 2019
@ehuss ehuss added the A-features Area: features — conditional compilation label Jan 31, 2019
@lijinpei
Copy link

A little bit of digging into it shows the problem seems to be cargo decided to pass --cfg 'feature="std"' when compiling rand_core, see the following different calls of rustc(you can generate then with cargo build -vv)

LD_LIBRARY_PATH='/C/Development/rust-cargo-bug/target/debug/deps:/home/lijinpei/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib:/home/lijinpei/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib' CARGO_PKG_NAME=rand_core CARGO_PKG_DESCRIPTION='Core random number generator traits and tools for implementation. ' CARGO_PKG_VERSION_MINOR=4 CARGO_PKG_HOMEPAGE='https://crates.io/crates/rand_core' CARGO=/home/lijinpei/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/cargo CARGO_PKG_AUTHORS='The Rand Project Developers:The Rust Project Developers' CARGO_MANIFEST_DIR=/home/lijinpei/.cargo/registry/src/github.com-1ecc6299db9ec823/rand_core-0.4.0 CARGO_PKG_VERSION_MAJOR=0 CARGO_PKG_VERSION_PATCH=0 CARGO_PKG_VERSION_PRE= CARGO_PKG_VERSION=0.4.0 CARGO_PKG_REPOSITORY='https://github.com/rust-random/rand' rustc --crate-name rand_core /home/lijinpei/.cargo/registry/src/github.com-1ecc6299db9ec823/rand_core-0.4.0/src/lib.rs --color always --crate-type lib --emit=dep-info,link -C debuginfo=2 -C metadata=afbd3cacdc17ebd3 -C extra-filename=-afbd3cacdc17ebd3 --out-dir /C/Development/rust-cargo-bug/target/debug/deps -L dependency=/C/Development/rust-cargo-bug/target/debug/deps --cap-lints warn

LD_LIBRARY_PATH='/C/Development/rust-cargo-bug/target/debug/deps:/home/lijinpei/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib:/home/lijinpei/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib' CARGO_PKG_DESCRIPTION='Core random number generator traits and tools for implementation. ' CARGO_PKG_AUTHORS='The Rand Project Developers:The Rust Project Developers' CARGO_PKG_NAME=rand_core CARGO_PKG_VERSION_MINOR=4 CARGO_MANIFEST_DIR=/home/lijinpei/.cargo/registry/src/github.com-1ecc6299db9ec823/rand_core-0.4.0 CARGO_PKG_VERSION=0.4.0 CARGO_PKG_REPOSITORY='https://github.com/rust-random/rand' CARGO_PKG_VERSION_PATCH=0 CARGO_PKG_HOMEPAGE='https://crates.io/crates/rand_core' CARGO_PKG_VERSION_PRE= CARGO=/home/lijinpei/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/cargo CARGO_PKG_VERSION_MAJOR=0 rustc --crate-name rand_core /home/lijinpei/.cargo/registry/src/github.com-1ecc6299db9ec823/rand_core-0.4.0/src/lib.rs --color always --crate-type lib --emit=dep-info,link -C debuginfo=2 --cfg 'feature="alloc"' --cfg 'feature="std"' -C metadata=fac15169af3958da -C extra-filename=-fac15169af3958da --out-dir /C/Development/rust-cargo-bug/target/debug/deps -L dependency=/C/Development/rust-cargo-bug/target/debug/deps --cap-lints warn

(the first one is the correct one).

@lijinpei
Copy link

I think the problem is, when cargo computes Resolve(a data-structure contains which package are dependent by which, and which version and features), it treat take targets-specific dependencies as they are always needed. So rand's mere appearance (even through not later linked in) cause it's std feature, and therefore rand_core's std feature to be included, which cause problem when rand_core is linked with myenslave.
For example, when the crate doesn't compiles, Resolve contains the followings:
pkg "rand_core" version 0.3.1 features:
pkg "rand_isaac" version 0.1.1 features:
pkg "cloudabi" version 0.0.3 features: bitflags default
pkg "rand_os" version 0.1.2 features:
pkg "rand_chacha" version 0.1.1 features:
pkg "winapi-x86_64-pc-windows-gnu" version 0.4.0 features:
pkg "myenclave" version 1.0.0 features:
pkg "semver" version 0.9.0 features: default
pkg "autocfg" version 0.1.2 features:
pkg "rdrand" version 0.4.0 features: std default
pkg "winapi" version 0.3.6 features: ntsecapi minwindef profileapi winnt
pkg "winapi-i686-pc-windows-gnu" version 0.4.0 features:
pkg "semver-parser" version 0.7.0 features:
pkg "fuchsia-cprng" version 0.1.1 features:
pkg "rand_jitter" version 0.1.3 features: std rand_core
pkg "bitflags" version 1.0.4 features: default
pkg "rand" version 0.6.5 features: std alloc default rand_jitter rand_core rand_os
pkg "mcrand" version 1.0.0 features:
pkg "libc" version 0.2.48 features: use_std default
pkg "rand_pcg" version 0.1.1 features:
pkg "rand_hc" version 0.1.0 features:
pkg "rustc_version" version 0.2.3 features:
pkg "rand_core" version 0.4.0 features: alloc std
pkg "rand_xorshift" version 0.1.1 features:
If we write the following in Cargo.toml:
[target.'cfg(not(target_arch = "x86_64"))'.dependencies] rand = { version = "0.6", default-features = false }
then Resolve would be:
pkg "winapi" version 0.3.6 features: profileapi minwindef winnt ntsecapi
pkg "rand_xorshift" version 0.1.1 features:
pkg "rand_pcg" version 0.1.1 features:
pkg "rand_isaac" version 0.1.1 features:
pkg "winapi-i686-pc-windows-gnu" version 0.4.0 features:
pkg "rand_hc" version 0.1.0 features:
pkg "libc" version 0.2.48 features:
pkg "rand_core" version 0.4.0 features:
pkg "rand_jitter" version 0.1.3 features:
pkg "mcrand" version 1.0.0 features:
pkg "rand_chacha" version 0.1.1 features:
pkg "semver-parser" version 0.7.0 features:
pkg "rustc_version" version 0.2.3 features:
pkg "rand_core" version 0.3.1 features:
pkg "winapi-x86_64-pc-windows-gnu" version 0.4.0 features:
pkg "rand" version 0.6.5 features:
pkg "semver" version 0.9.0 features: default
pkg "autocfg" version 0.1.2 features:
pkg "myenclave" version 1.0.0 features:

and the project compiles.

@cbeck88
Copy link
Author

cbeck88 commented Feb 21, 2019

@lijinpei thank you for digging into this!

So my takeaway from this is that making something optional is somehow not as strong as making it `cfg(target_arch(...))`` selected -- that's confusing to me but if you think this behavior is reliable it's really helpful!

Do you think that this a "bug" or a design decision of the cargo team? (Or simply not clear which?)

@lijinpei
Copy link

lijinpei commented Feb 22, 2019

First, theoretically, whether core_rand's std feature enabled or not, it meets all the dependency requirements. A requirements problem may be satisfied by multiple version of a set of packages, or multiple sets of feature of packages, I don't know if cargo guarantees to return a specific set.

Secondly, currently, when computing dependencies, cargo treat all platform-specific dependencies as if they are always needed, it's only when the actual build starts the platform-specifics are filtered out. I personally thought this is a bug. But that needs to be cleared be cargo team. (I can see one reason for cargo to work this way is it needs to support cross-build while share the same Cargo.lock. But maybe constraints for different platforms may not be satisfied together?)

(A third problem would be features like std needs consensus between rustc and cargo, but cargo won't look into a .rs file's contents, it even doesn't know which set of files are needed to build a crate without rustc's help, while rustc doesn't know package dependencies, it needs cargo to provides them on the command line)

@RalfJung
Copy link
Member

Might be related to #2524?

@cbeck88
Copy link
Author

cbeck88 commented Aug 14, 2019

@lijinpei in any other build system like, scons, cmake, make, if unnecessary stuff is being built, that is a bug. All of these other systems are highly configurable so that you can make it build exactly what you need with exactly the flags you need.

It's crazy that in cargo you can have bugs where the wrong stuff is built, it takes hours to track down the reason, and then there's no fix, or maybe a hack requiring changes to everything, and the cargo devs question whether it's a bug! Building unnecessary stuff can mean slow builds or broken builds. Bugs like this in cargo (and there are many of them!), and the difficulty of debugging them, are a serious reason not to use rust at all, esp. in embedded development. In our company we have worked around it with a hack but it's pretty dirty.

You can choose not to define it as a bug, but this just means you have set the bar for cargo way below other similar tools.

I can see one reason for cargo to work this way is it needs to support cross-build while share the same Cargo.lock

I don't see a reason for this constraint, any more than it would make sense to require Cargo.lock to be the same no matter what features are enabled.

@ehuss
Copy link
Contributor

ehuss commented Jul 13, 2020

I'm going to close this as I believe it is resolved by #7914.

@ehuss ehuss closed this as completed Jul 13, 2020
cbeck88 added a commit to mobilecoinfoundation/mobilecoin that referenced this issue Sep 10, 2022
many years ago, we encountered this bug:
rust-lang/cargo#6571

This bug meant that if we wanted to have a single Rng type that worked
on all platforms, including SGX, then it could not use the standard
library on any platform, because cargo would evaluate dependencies
without respect to what platform you are on.

However, it was really important for our code that we had such an
abstraction layer. This was important for writing enclave impl code
that could run in SGX and also in unit tests for instance.

The way we solved this issue was that, we took the current version
of `ThreadRng` which is the generically recommendable cryptographic
RNG type from `rand` crate, and made the smallest possible changes
to it until it would build without the standard library. The main
change was replacing `std::thread_local!` with the `#[thread_local]`
attribute, which turned out to be straightforward. However, it creates
an annoying maintenance burden on us, and there has been a lot of
churn in the rand crate since then.

Since the `resolver = 2` stuff was stabilized, the original problem
is no longer the case. It's fine to have crates that pull in `std`
in one platform but not another. So we can now just use `ThreadRng`
as the no-RDRAND fallback, like we wanted all along.
cbeck88 added a commit to mobilecoinfoundation/mobilecoin that referenced this issue Sep 13, 2022
many years ago, we encountered this bug:
rust-lang/cargo#6571

This bug meant that if we wanted to have a single Rng type that worked
on all platforms, including SGX, then it could not use the standard
library on any platform, because cargo would evaluate dependencies
without respect to what platform you are on.

However, it was really important for our code that we had such an
abstraction layer. This was important for writing enclave impl code
that could run in SGX and also in unit tests for instance.

The way we solved this issue was that, we took the current version
of `ThreadRng` which is the generically recommendable cryptographic
RNG type from `rand` crate, and made the smallest possible changes
to it until it would build without the standard library. The main
change was replacing `std::thread_local!` with the `#[thread_local]`
attribute, which turned out to be straightforward. However, it creates
an annoying maintenance burden on us, and there has been a lot of
churn in the rand crate since then.

Since the `resolver = 2` stuff was stabilized, the original problem
is no longer the case. It's fine to have crates that pull in `std`
in one platform but not another. So we can now just use `ThreadRng`
as the no-RDRAND fallback, like we wanted all along.
cbeck88 added a commit to mobilecoinfoundation/mobilecoin that referenced this issue Sep 13, 2022
…2503)

* delete some terrible code that was written to work around cargo bugs

many years ago, we encountered this bug:
rust-lang/cargo#6571

This bug meant that if we wanted to have a single Rng type that worked
on all platforms, including SGX, then it could not use the standard
library on any platform, because cargo would evaluate dependencies
without respect to what platform you are on.

However, it was really important for our code that we had such an
abstraction layer. This was important for writing enclave impl code
that could run in SGX and also in unit tests for instance.

The way we solved this issue was that, we took the current version
of `ThreadRng` which is the generically recommendable cryptographic
RNG type from `rand` crate, and made the smallest possible changes
to it until it would build without the standard library. The main
change was replacing `std::thread_local!` with the `#[thread_local]`
attribute, which turned out to be straightforward. However, it creates
an annoying maintenance burden on us, and there has been a lot of
churn in the rand crate since then.

Since the `resolver = 2` stuff was stabilized, the original problem
is no longer the case. It's fine to have crates that pull in `std`
in one platform but not another. So we can now just use `ThreadRng`
as the no-RDRAND fallback, like we wanted all along.

* fixup from review comments

* remove unnecessary dependencies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-features Area: features — conditional compilation C-bug Category: bug
Projects
None yet
Development

No branches or pull requests

6 participants
@ehuss @RalfJung @dwijnand @cbeck88 @lijinpei and others