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

Feature selection in workspace depends on the set of packages compiled #4463

Open
matklad opened this Issue Sep 3, 2017 · 21 comments

Comments

Projects
None yet
6 participants
@matklad
Member

matklad commented Sep 3, 2017

Reproduction:

  1. Check out this commit: matklad/fall@3022be4

  2. Build some test with cargo test -p fall_test -p fall_test -p lang_rust -p lang_rust -p lang_json --verbose --no-run

  3. Build other tests with cargo test --all --verbose --no-run

  4. Run cargo test -p fall_test -p fall_test -p lang_rust -p lang_rust -p lang_json --verbose --no-run again and observe that memchr and some other dependencies are recompiled.

  5. Run cargo test --all --verbose --no-run and observe memchr recompiled again.

The verbose flag gives the following commands for memchr:

Running `rustc --crate-name memchr /home/matklad/trash/registry/src/github.com-1ecc6299db9ec823/memchr-1.0.1/src/lib.rs --crate-type lib --emit=dep-info,link -C debuginfo=2 --cfg 'feature="default"' --cfg 'feature="libc"' --cfg 'feature="use_std"' -C metadata=be49c4722e8b48bf -C extra-filename=-be49c4722e8b48bf --out-dir /home/matklad/trash/fall/target/debug/deps -L dependency=/home/matklad/trash/fall/target/debug/deps --extern libc=/home/matklad/trash/fall/target/debug/deps/liblibc-90ba32719d46f457.rlib --cap-lints allow -C target-cpu=native`
Running `rustc --crate-name memchr /home/matklad/trash/registry/src/github.com-1ecc6299db9ec823/memchr-1.0.1/src/lib.rs --crate-type lib --emit=dep-info,link -C debuginfo=2 --cfg 'feature="default"' --cfg 'feature="libc"' --cfg 'feature="use_std"' -C metadata=be49c4722e8b48bf -C extra-filename=-be49c4722e8b48bf --out-dir /home/matklad/trash/fall/target/debug/deps -L dependency=/home/matklad/trash/fall/target/debug/deps --extern libc=/home/matklad/trash/fall/target/debug/deps/liblibc-335251832eb2b7ec.rlib --cap-lints allow -C target-cpu=native`

Here's the single difference:

--extern libc=/home/matklad/trash/fall/target/debug/deps/liblibc-90ba32719d46f457.rlib 
--extern libc=/home/matklad/trash/fall/target/debug/deps/liblibc-335251832eb2b7ec.rlib 

Versions (whyyyyy cargo is 0.21 and rustc is 1.20??? This is soo confusing)

λ cargo --version --verbose
cargo 0.21.0 (5b4b8b2ae 2017-08-12)
release: 0.21.0
commit-hash: 5b4b8b2ae3f6a884099544ce66dbb41626110ece
commit-date: 2017-08-12

~/trash/fall master
λ rustc --version
rustc 1.20.0 (f3d6973f4 2017-08-27)

@matklad matklad added the C-bug label Sep 3, 2017

@matklad

This comment has been minimized.

Member

matklad commented Sep 3, 2017

So, it has to do with features. Namely, two cargo invocations produce two different libcs:

Running `rustc --crate-name libc /home/matklad/trash/registry/src/github.com-1ecc6299db9ec823/libc-0.2.30/src/lib.rs --crate-type lib --emit=dep-info,link -C debuginfo=2 --cfg 'feature="use_std"' -C metadata=335251832eb2b7ec -C extra-filename=-335251832eb2b7ec --out-dir /home/matklad/trash/fall/target/debug/deps -L dependency=/home/matklad/trash/fall/target/debug/deps --cap-lints allow -C target-cpu=native`
Running `rustc --crate-name libc /home/matklad/trash/registry/src/github.com-1ecc6299db9ec823/libc-0.2.30/src/lib.rs --crate-type lib --emit=dep-info,link -C debuginfo=2 --cfg 'feature="default"' --cfg 'feature="use_std"' -C metadata=90ba32719d46f457 -C extra-filename=-90ba32719d46f457 --out-dir /home/matklad/trash/fall/target/debug/deps -L dependency=/home/matklad/trash/fall/target/debug/deps --cap-lints allow -C target-cpu=native`

The only difference is --cfg 'feature="default"'.

So, I get two different libcs in target:

λ ls target/debug/deps | grep liblibc
.rw-r--r-- 982k matklad  3 Sep 14:06 liblibc-90ba32719d46f457.rlib
.rw-r--r-- 982k matklad  3 Sep 14:03 liblibc-335251832eb2b7ec.rlib

But I get a single memchr:

λ ls target/debug/deps | grep libmemchr
.rw-r--r-- 186k matklad  3 Sep 14:09 libmemchr-be49c4722e8b48bf.rlib

The file name is the same for both cargo commands, but the actual contents differs.

@matklad

This comment has been minimized.

Member

matklad commented Sep 3, 2017

Hm, so this looks like more serious then spurious rebuild!

Depending on what -p options you pass, you might end up with different final artifacts for the same package. This should not happen, right?

@matklad

This comment has been minimized.

Member

matklad commented Sep 3, 2017

Minimized example here: https://github.com/matklad/workspace-vs-feaures

@matklad matklad changed the title from Spurious rebuilds when testing different packages of a workspace to Feature selection in workspace depends on the set of packages compiled Sep 5, 2017

@matklad

This comment has been minimized.

Member

matklad commented Sep 5, 2017

@alexcrichton continuing discussion here, instead of #4469 which is somewhat orthogonal, as you've rightly pointed out!

I don't think this'd be too hard to implement, but I'm not sure if this is what we'd want implemented per se. If one target of a workspace doesn't want a particular feature activated, wouldn't it be surprising if some other target present in a workspace far away activated the feature?

Yeah, it looks like what we ideally want here is that each final artifact gets the minimal set of features. And this should work even withing a single package: currently, activating feature in dev-dependecy will activate it for usual dependency as well. This is also something to keep in mind if we go the route of binary-only (or per-target) dependencies.

Though such fine-grained feature activation will cause more compilation work overall, so using union of featues might be a pragmatic choice, as long as we keep features additive, and it sort of makes sense, because crates in workspace share dependencies anyway. And seems better then definitely some random unrelated target activating features for you depending on the command line flags.

@alexcrichton

This comment has been minimized.

Member

alexcrichton commented Sep 5, 2017

I think one of the main problems right now is that we're doing feature resolution far too soon, during the crate graph resolution. Instead what we should be doing is assuming all features are activated until we actually start compiling crates. That way if you have multiple targets all requesting different sets of features they'll all get separately compiled copies with the correct set of features.

Does that make sense? Or perhaps solving a different problem?

@matklad

This comment has been minimized.

Member

matklad commented Sep 5, 2017

Does that make sense? Or perhaps solving a different problem?

Yeah, totally, "they'll all get separately compiled copies with the correct set of features" is the perfect solution here, and it could be implemented by moving feature selection after the dependency resolution.

But I am really worried about additional work to get separately compiled copies, because it is multiplicative. Let's say you have a workspace with the following layout:

  1. leaf crates A and B, which transitively depend on external crate libc with different features
  2. A large number of intermediate crates, on which A and B also depend
  3. An ubiquitous utils crate, that depends on libc and is a dependency of any other crate.

Because A and B require different features from libc, and because libc happens to be at the bottom of the dependency graph, that means that for cargo build --all we will compile every crate twice. Moreover, editing utils and then doing cargo build --all again recompiles everything two times.

So it's not that only libc will get duplicated, the whole graph may be duplicated in the worst case.

@nipunn1313

This comment has been minimized.

Contributor

nipunn1313 commented Sep 5, 2017

If we assume that features are additive (as intended), then the innermost crate could be compiled once with the union of all features.

Additive features are a bit of a subtle point though (see #3620). Recompiling is the safest way, though expensive.

@alexcrichton

This comment has been minimized.

Member

alexcrichton commented Sep 5, 2017

@matklad yeah you're definitely right that the more aggressively we cache the more we end up caching :). @nipunn1313 you're also right that it should be safe for features to be unioned, but they often come with runtime or linkage implications. For example if a workspace has a no_std project and an executable, compiling both you wouldn't want to enable the standard library in the dependencies of the no_std project by accident!

I basically see this as there's a specification of what Cargo should be doing here. We've got, for example, two crates in a workspace, each which activates various sets of features in shared dependencies. Today Cargo does the "thing that caches too much" if you compile each separately (and also suffers a bug when you switch between projects it recompiles too much). Cargo also does the "union all the features" if you build both crates simultaneously (e.g. cargo build --all). Basically Cargo's not consistent!

I'd advocate that Cargo should try to stick to the "caches too much" solution as it's following the letter of the law of what you wrote down for a workspace. It also means that crates in a workspace don't need to worry too much about interfering with other crates in a workspace. Projects that run into problems of the "too much is cached" nature I'd imagine could then do the investigation to figure out what features are turned on where, and try to get each workspace member to share more dependencies by unifying the features.

@matklad

This comment has been minimized.

Member

matklad commented Sep 6, 2017

Projects that run into problems of the "too much is cached" nature I'd imagine could then do the investigation to figure out what features are turned on where, and try to get each workspace member to share more dependencies by unifying the features.

This somewhat resolves my concern about build times, but not entirely. I am worried that it might not be easy to unify features manually, if they are turned on by private transitive dependencies. It would be possible to do by adding this private transitive dependency as an explicit and unused dependency, but this looks accidental.

But now I too lean towards fine-grained features solution.

@nipunn1313

This comment has been minimized.

Contributor

nipunn1313 commented Sep 6, 2017

@SimonSapin

This comment has been minimized.

Contributor

SimonSapin commented Sep 19, 2017

Servo relies on the current behavior to some extent: two "top-level" crates (one executable and one C-compatible static library) depend on a shared library crate but enable different Cargo features. These features are mutually exclusive, enabling the union would not work.

Maybe the "right" thing to do here is to have separate workspaces for the different top-level things? Does it make sense for shared path dependencies to be members of two separate workspaces?

(Servo’s build system sets $CARGO_TARGET_DIR to different directories for the two top-level things so that they don’t overwrite each other. They also happen to be built with different compiler versions (some nightly v.s. some stable release).)

@nipunn1313

This comment has been minimized.

Contributor

nipunn1313 commented Sep 21, 2017

I would be in support of cargo build --all building the same dependency multiple times rather than resolving a feature union. This would be equivalent of running cargo build in a loop over each crate in the workspace. This would prevent multiple crates within a workspace from interfering with each other (or multiple dependencies in the dep-chain interfering). I believe it would cover Servo's case as well.

What makes this problem so insidious is that there's no way to enforce or even encourage the union property of features. If a project pulls in even one dependency that doesn't obey this property, it could potentially create an incorrect binary.

In @SimonSapin's case with Servo, I think Servo is lucky that the feature'd crate (style) is only one-level in from the top level crate. If you had a dep chain like

evenbiggerproject -> servo -> style[featA]
                  -> geckolib -> style[featB]

then I believe that compiling evenbiggerproject with cargo would select the union of features for style and use it for both geckolib and servo. This would be an incorrect binary w.r.t. the intent of the servo/geckolib Cargo.tomls

Our project at Dropbox ran into a similar issue with itertools -> libeither, where libeither was compiled with two different features. Lucky for us, libeither's features are union-safe, so the code was correct, but it did create spurious recompiles depending on which sub-crate we were compiling.

@djc

This comment has been minimized.

Contributor

djc commented Oct 4, 2017

I agree with @nipunn1313 -- I think cargo build --all should build all crates exactly as they would be if you had run cargo build for each crate separately. If that requires us to recompile some crates, so be it.

@SimonSapin

This comment has been minimized.

Contributor

SimonSapin commented Oct 13, 2017

This all sounds like agreement on what should happen. @alexcrichton, what code changes need to happen (on a high level) to get there?

@djc

This comment has been minimized.

Contributor

djc commented Oct 14, 2017

That's what I was discussing with @alexcrichton at the RustFest impl days, and I have a bunch of refactoring done that I'm still tweaking. Will post a PR ASAP. Do you have a particular dependency/urgency relating to Gecko or Servo on this?

@SimonSapin

This comment has been minimized.

Contributor

SimonSapin commented Oct 14, 2017

Nothing urgent. I thought this bug could cause spurious rebuilds after selectively building a crate with -p, but I couldn’t reproduce. Anyway, thanks for working on this!

@nipunn1313

This comment has been minimized.

Contributor

nipunn1313 commented Oct 15, 2017

@djc

This comment has been minimized.

Contributor

djc commented Oct 16, 2017

@nipunn1313 for my understanding, can you point me at a commit or otherwise elaborate on what problems you've had due to this issue?

@nipunn1313

This comment has been minimized.

Contributor

nipunn1313 commented Oct 16, 2017

Here's an example of a problem we had to work around
#3620 (comment)

In that particular case, either and itertools were both present in our workspace.
We ended up internally forking itertools to ask for a wider set of features from libeither, so there was consistency across the workspace.

@alexcrichton

This comment has been minimized.

Member

alexcrichton commented Oct 18, 2017

@SimonSapin taking on this issue will require a relatively significant refactoring of Cargo's backend. Right now feature resolution happens during crate graph resolution, but we need to defer it all the way until the very end when we're actually compiling crates.

SimonSapin added a commit to servo/servo that referenced this issue Dec 4, 2017

Use workspace.default-members to specify default crates for 'cargo bu…
…ild'

… and 'cargo test', etc. Include Servo and its unit tests,
but not Stylo because that would try to compile the style
crate with incompatible feature flags:
rust-lang/cargo#4463

`workspace.default-members` was added in
rust-lang/cargo#4743.
Older Cargo versions ignore it.

SimonSapin added a commit to servo/servo that referenced this issue Dec 4, 2017

Use workspace.default-members to specify default crates for 'cargo bu…
…ild'

… and 'cargo test', etc. Include Servo and its unit tests,
but not Stylo because that would try to compile the style
crate with incompatible feature flags:
rust-lang/cargo#4463

`workspace.default-members` was added in
rust-lang/cargo#4743.
Older Cargo versions ignore it.

SimonSapin added a commit to servo/servo that referenced this issue Dec 4, 2017

Use workspace.default-members to specify default crates for 'cargo bu…
…ild'

… and 'cargo test', etc. Include Servo and its unit tests,
but not Stylo because that would try to compile the style
crate with incompatible feature flags:
rust-lang/cargo#4463

`workspace.default-members` was added in
rust-lang/cargo#4743.
Older Cargo versions ignore it.

SimonSapin added a commit to servo/servo that referenced this issue Dec 5, 2017

Use workspace.default-members to specify default crates for 'cargo bu…
…ild'

… and 'cargo test', etc. Include Servo and its unit tests,
but not Stylo because that would try to compile the style
crate with incompatible feature flags:
rust-lang/cargo#4463

`workspace.default-members` was added in
rust-lang/cargo#4743.
Older Cargo versions ignore it.

bors-servo added a commit to servo/servo that referenced this issue Dec 5, 2017

Auto merge of #19476 - servo:default-members, r=nox
 Use workspace.default-members to specify default crates for cargo build

… and 'cargo test', etc. Include Servo and its unit tests, but not Stylo because that would try to compile the style crate with incompatible feature flags: rust-lang/cargo#4463

`workspace.default-members` was added in rust-lang/cargo#4743. Older Cargo versions ignore it.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/19476)
<!-- Reviewable:end -->

bors-servo added a commit to servo/servo that referenced this issue Dec 5, 2017

Auto merge of #19476 - servo:default-members, r=nox
 Use workspace.default-members to specify default crates for cargo build

… and 'cargo test', etc. Include Servo and its unit tests, but not Stylo because that would try to compile the style crate with incompatible feature flags: rust-lang/cargo#4463

`workspace.default-members` was added in rust-lang/cargo#4743. Older Cargo versions ignore it.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/19476)
<!-- Reviewable:end -->

SimonSapin added a commit to servo/servo that referenced this issue Dec 5, 2017

Use workspace.default-members to specify default crates for 'cargo bu…
…ild'

… and 'cargo test', etc. Include Servo and its unit tests,
but not Stylo because that would try to compile the style
crate with incompatible feature flags:
rust-lang/cargo#4463

`workspace.default-members` was added in
rust-lang/cargo#4743.
Older Cargo versions ignore it.

SimonSapin added a commit to servo/servo that referenced this issue Dec 5, 2017

Use workspace.default-members to specify default crates for 'cargo bu…
…ild'

… and 'cargo test', etc. Include Servo and its unit tests,
but not Stylo because that would try to compile the style
crate with incompatible feature flags:
rust-lang/cargo#4463

`workspace.default-members` was added in
rust-lang/cargo#4743.
Older Cargo versions ignore it.

SimonSapin added a commit to servo/servo that referenced this issue Dec 5, 2017

Use workspace.default-members to specify default crates for 'cargo bu…
…ild'

… and 'cargo test', etc. Include Servo and its unit tests,
but not Stylo because that would try to compile the style
crate with incompatible feature flags:
rust-lang/cargo#4463

`workspace.default-members` was added in
rust-lang/cargo#4743.
Older Cargo versions ignore it.

SimonSapin added a commit to servo/servo that referenced this issue Dec 7, 2017

Use workspace.default-members to specify default crates for 'cargo bu…
…ild'

… and 'cargo test', etc. Include Servo and its unit tests,
but not Stylo because that would try to compile the style
crate with incompatible feature flags:
rust-lang/cargo#4463

`workspace.default-members` was added in
rust-lang/cargo#4743.
Older Cargo versions ignore it.

bors-servo added a commit to servo/servo that referenced this issue Dec 7, 2017

Auto merge of #19476 - servo:default-members, r=nox
 Use workspace.default-members to specify default crates for cargo build

… and 'cargo test', etc. Include Servo and its unit tests, but not Stylo because that would try to compile the style crate with incompatible feature flags: rust-lang/cargo#4463

`workspace.default-members` was added in rust-lang/cargo#4743. Older Cargo versions ignore it.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/19476)
<!-- Reviewable:end -->

SimonSapin added a commit to servo/servo that referenced this issue Dec 7, 2017

Use workspace.default-members to specify default crates for 'cargo bu…
…ild'

… and 'cargo test', etc. Include Servo and its unit tests,
but not Stylo because that would try to compile the style
crate with incompatible feature flags:
rust-lang/cargo#4463

`workspace.default-members` was added in
rust-lang/cargo#4743.
Older Cargo versions ignore it.

bors-servo added a commit to servo/servo that referenced this issue Dec 7, 2017

Auto merge of #19476 - servo:default-members, r=nox
 Use workspace.default-members to specify default crates for cargo build

… and 'cargo test', etc. Include Servo and its unit tests, but not Stylo because that would try to compile the style crate with incompatible feature flags: rust-lang/cargo#4463

`workspace.default-members` was added in rust-lang/cargo#4743. Older Cargo versions ignore it.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/19476)
<!-- Reviewable:end -->

moz-v2v-gh pushed a commit to mozilla/gecko-dev that referenced this issue Dec 8, 2017

servo: Merge #19476 - Use workspace.default-members to specify defaul…
…t crates for cargo build (from servo:default-members); r=nox

… and 'cargo test', etc. Include Servo and its unit tests, but not Stylo because that would try to compile the style crate with incompatible feature flags: rust-lang/cargo#4463

`workspace.default-members` was added in rust-lang/cargo#4743. Older Cargo versions ignore it.

Source-Repo: https://github.com/servo/servo
Source-Revision: df68eea3f21cc3bbf24d5bbb66be42c4e3a9e427

--HG--
rename : servo/tests/unit/stylo/Cargo.toml => servo/ports/geckolib/tests/Cargo.toml
rename : servo/tests/unit/stylo/build.rs => servo/ports/geckolib/tests/build.rs
rename : servo/tests/unit/stylo/lib.rs => servo/ports/geckolib/tests/lib.rs
rename : servo/tests/unit/stylo/servo_function_signatures.rs => servo/ports/geckolib/tests/servo_function_signatures.rs
rename : servo/tests/unit/stylo/size_of.rs => servo/ports/geckolib/tests/size_of.rs
rename : servo/tests/unit/stylo/specified_values.rs => servo/ports/geckolib/tests/specified_values.rs
extra : subtree_source : https%3A//hg.mozilla.org/projects/converted-servo-linear
extra : subtree_revision : 0939a7049dc771e9d1b4f45f6e3ade2866266fa4

xeonchen pushed a commit to xeonchen/gecko-cinnabar that referenced this issue Dec 8, 2017

servo: Merge #19476 - Use workspace.default-members to specify defaul…
…t crates for cargo build (from servo:default-members); r=nox

… and 'cargo test', etc. Include Servo and its unit tests, but not Stylo because that would try to compile the style crate with incompatible feature flags: rust-lang/cargo#4463

`workspace.default-members` was added in rust-lang/cargo#4743. Older Cargo versions ignore it.

Source-Repo: https://github.com/servo/servo
Source-Revision: df68eea3f21cc3bbf24d5bbb66be42c4e3a9e427

aethanyc pushed a commit to aethanyc/gecko-dev that referenced this issue Dec 19, 2017

servo: Merge #19476 - Use workspace.default-members to specify defaul…
…t crates for cargo build (from servo:default-members); r=nox

… and 'cargo test', etc. Include Servo and its unit tests, but not Stylo because that would try to compile the style crate with incompatible feature flags: rust-lang/cargo#4463

`workspace.default-members` was added in rust-lang/cargo#4743. Older Cargo versions ignore it.

Source-Repo: https://github.com/servo/servo
Source-Revision: df68eea3f21cc3bbf24d5bbb66be42c4e3a9e427

hcpl added a commit to hcpl/mtproto-rs that referenced this issue Dec 30, 2017

Remove `parsing` feature from `tl_codegen`
Now `tl_codegen` already requires parsing capabilities from `syn` to
have more readable code.

The reason why this slipped through builds _after_ `tl_codegen` began to
use `syn/parsing` feature is because builds in the project root unify
features from `syn` in the dependency graph, so the overall build
succeeds. But builds in `tl_codegen/` don't have the `parsing` feature
of `syn` enabled, so here it fails.

P.S. Kinda scary to think how much more complicated it can get when
there are even more intertwined features with different sets of them
enabled in different situations.

Very likely to be related to
rust-lang/cargo#4463.
@driftluo

This comment has been minimized.

driftluo commented Mar 27, 2018

Today, I encountered a problem with the overall compilation of the workspace and the inconsistent compilation of each crate.

We have a public logger crate, the default is output to a file, open console features will be output to the terminal.

Some crates in the workspace have this feature turned on, while others do not need to be turned on.

But cargo bulid --all will cause the entire workspace's crates to be output to the terminal.

This behavior makes me very confused.

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