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

Tracking issue for RFC 2282 - Cargo profile dependencies #48683

Open
Manishearth opened this Issue Mar 2, 2018 · 32 comments

Comments

Projects
None yet
8 participants
@Manishearth
Member

Manishearth commented Mar 2, 2018

This is a tracking issue for RFC 2282 (rust-lang/rfcs#2282).

Cargo issue: rust-lang/cargo#5298

Steps:

(cc @rust-lang/cargo for mentorship instructions)

Unresolved questions:

  • We can bikeshed the names of the new keys that were introduces
@Boscop

This comment has been minimized.

Boscop commented Apr 4, 2018

Any update on this? :)

@Manishearth

This comment has been minimized.

Member

Manishearth commented Apr 4, 2018

Someone needs to implement it.

@ehuss

This comment has been minimized.

Contributor

ehuss commented Apr 5, 2018

If you're looking for someone to work on this I'd be happy to help!

@Manishearth

This comment has been minimized.

Member

Manishearth commented Apr 5, 2018

Sure! I don't have the bandwidth to mentor this but feel free to try it out. Might want to file an issue on the cargo side to coordinate the implementation work.

@ehuss

This comment has been minimized.

Contributor

ehuss commented Apr 28, 2018

Some issues came up during implementation.

  • @alexcrichton mentioned that the package name could support full-spec syntax (crates.io/foo, etc.). It looks like it should be easy to support, just let me know if you'd like me to add it! (Currently it just matches the name.)
  • I briefly discussed with @matklad the possibility of having a dedicated profile used for scripts. It would be easy to add, and would remove "build-override" which is essentially a pseudo-profile (and seems a little confusing to me). It would also decouple building build scripts from the --release flag. Let me know if you want to consider this route!
@Manishearth

This comment has been minimized.

Member

Manishearth commented Apr 28, 2018

Full spec sounds great.

The problem with having a dedicated profile for scripts is that you may wish them to be compiled differently in debug vs release. build-override lets you do both.

The original proposal (not the one we RFCd) allowed nesting of profiles which would have made this work well (you declare a new profile, and link it in as the build profile for an existing one)

@Manishearth

This comment has been minimized.

Member

Manishearth commented Apr 28, 2018

(Also to me it seems like having a build profile just makes the whole profile vs workflow deal even messier -- we already are in a situation where the test/bench profiles are a bit weird)

@ehuss

This comment has been minimized.

Contributor

ehuss commented Apr 28, 2018

Yep! I was just meaning that it can probably be improved on without too much effort. I think there was some concern during the RFC process that shared dependencies would be a problem, but they shouldn't be. I think @matklad had more thoughts on the profile/workflow stuff, so I'll let him say more.

@matklad

This comment has been minimized.

Member

matklad commented May 2, 2018

may wish them [build scripts] to be compiled differently in debug vs release.

@Manishearth could you give an example for this? An important practical reason for compiling build scripts exactly the same in dev and release is to avoid compiling syn and friends twice, when you do , say, cargo test followed by cargo bench. You can achieve this with current design of build_overrides, but you'll have to explicitly override most of options for build profiles, because, by default, dev and release are quite different.

More broadly, I think the following properties make a dedicated build profile an interesting option to consider:

  • It makes sense to use a single profile for build scripts, so as to compile them once for all cargo invocations.

  • Semantically, it makes little sense to use the same profile for build script and library code. This is easily understood in the context of cross compilation: you compile library for the target arch, and the build script for host arch, and they are executed on completely different machines. The motivations for picking particular compiler flags must be different in these two separate worlds.

  • Similarly to the previous bullet, the build scripts profile settings do not affect the generated artifacts in any way.

  • The best defaults for the build scripts are likely to be different. It seems to me that we want something like the following:

    [profile.build]
    # low hanging optimization fruits which shouldn't affect build time much
    opt-level = 1 
    # save build time and disk space by not emitting huge amount of debug-info
    debug = false 
    # speed up both compile time and runtime
    debug-assertions = false
    overflow-checks = false
    # build scripts are small and rarely modified
    incremental = false

    this differs significantly from both dev and release profiles.

The interaction with workflows is also interesting here: in a nutshell, a workflow would allow one to compile and run some custom rust code on the host to do execute custom tasks, like cargo subcommands, but locally to the project. And this code needs to be compiled with some optimization settings and such :)

I envision the build profile as the profile for all of build scripts, proc macros and workflow tasks. All three are compiled on the host, could be shared across cargo build and cargo build --release and have similar runtime/compile-time trade offs.

It is true that test, bench and especially doc profile (which exists and does nothing) are messy today. But it seems that, unlike those profiles, the suggested build one actually has compelling reasons to exist, as discussed above. Also, if we compare build_overrides with the build profile, I would argue that the latter has less overall complexity, because it reuses the existing concept, while the former is one is a special case with a special syntax and such.

@Manishearth

This comment has been minimized.

Member

Manishearth commented May 3, 2018

could you give an example for this?

My dev workflow may be one where I'm recompiling often (but I don't edit the build scripts) so I need the build scripts to be fast, but my release workflow may be one where I only run it occasionally (incremental builds are less common), so build scripts should be compiled in dev.

This isn't everyone, but this could be someone.

An important practical reason for compiling build scripts exactly the same in dev and release is to avoid compiling

This is an argument for why the defaults should be such that build profiles are the same , not an argument for why we shouldn't expose this functionality.

Firefox, for example, would probably want an optimized build script with an unoptimized libsyntax/syn/whatever.

you compile library for the target arch, and the build script for host arch, and they are executed on completely different machines. The motivations for picking particular compiler flags must be different in these two separate worlds.

IMO it's not relevant to bring cross compilation into the picture here because cross compilation works differently -- dependencies are not shared. If cargo was rearchitected so that shared deps between build scripts and the main build may be compiled into separate artifacts, this makes more sense, but as it stands, these are shared, and IMO it's easier to reason about these shared dependencies when you have a single profile instead of two.

@ehuss

This comment has been minimized.

Contributor

ehuss commented May 3, 2018

these are shared

Not anymore. Shared deps are now compiled separately. (The graph for features/versions is still unified, though.)

@ehuss

This comment has been minimized.

Contributor

ehuss commented May 6, 2018

I had some questions on how to feature-gate profiles in config files. I can imagine a variety of ways to implement it:

  • Command-line option (-Z profile-config)?
  • Feature flag in Cargo.toml (cargo-features = ["profile-config"])?
  • Some kind of flag in the config file itself?
  • Should it ignore entries in config if you don't specify the flag? Or warn? Or error?

I can imagine if you put a profile in a global config it could be really fussy. Let me know if you have any ideas.

@Manishearth

This comment has been minimized.

Member

Manishearth commented May 6, 2018

I'd have some kind of flag in the config itself.

@Manishearth

This comment has been minimized.

Member

Manishearth commented May 6, 2018

But that's not a strong opinion

@ehuss

This comment has been minimized.

Contributor

ehuss commented May 7, 2018

I have some more questions (cc @alexcrichton @matklad).

  • I had some questions/doubts about warnings for config files. In general, it looks like config files never warn about misspelled/unused keys. Is that intentional for some sort of forward-compatibility?
    • My implementation currently does not warn about unused override package names in config files ([profile.dev.overrides.foo]). I was thinking someone might place an entry in a global config, which would cause warnings in all projects not using that package. Sound reasonable?
    • Let me know if I should add warnings or errors for the following:
      • Unknown profile names ([profile.misspelled]).
      • Unsupported profile names (test, bench, doc).
      • Unknown keys in a profile.
  • Config files do not support merging values with mixed data types. For example, if you have lto = true in one config and lto = 'thin' in another, it fails. How much of a problem is this? (This affects lto, opt-level, and debug.)
  • If we go the route of adding unstable feature flags to config files, there are some design questions I have. To start with:
    • Should it be an error to specify a feature flag when using a stable release? Or should it warn and ignore the new settings? Or maybe just silently ignore? My concern is that it would be very cumbersome to use unstable features in a global config file (since running on stable would always fail or print annoying warnings).
    • Should unknown feature names be ignored, warn, or error?
  • (EDIT) Another idea for handling warnings would be to silently ignore unless you run with verbose.
@alexcrichton

This comment has been minimized.

Member

alexcrichton commented May 8, 2018

Aha some excellent questions @ehuss! Lemme see if I can dig in...

  • It's true yeah that we're quite lenient with config files. I don't think we can warn about unused (aka misspelled) keys here though in all situations. Not all builds may use all configuration, so we'd probably be going a bit too far out of our way to get this working. Now that being said I think profiles specifically could probably helpfully reject any configuration other than release/dev or any other obvious missteps, I think it's only possible to reject configuration that couldn't possibly be used though rather than "this isn't used for this particular build"

  • Hm... We may just want to tweak how configs are merged and say that a string can override a string or something like that. I don't think there's necessarily a downside to that approach I think, but do you forsee any problems with that?

  • I think it's probably safe to just require features to be enabled in the local cargo build (via the CLI maybe?) rather than the configuration. If the local features aren't enabled but configuration is detected then we could print a warning or something like that saying that the features aren't adequately enabled.

@ehuss

This comment has been minimized.

Contributor

ehuss commented May 8, 2018

profiles specifically could probably helpfully reject any configuration other than release/dev or any other obvious missteps,

My concern with rejecting with an error is that if in the future cargo supports additional profile names, it would make it impossible to use the new names or keys in a global config and continue using older versions of cargo. I would feel more comfortable with just warnings (profile `foo` is not supported or profile key `bar` is not supported).

string can override a string

Do you mean string can override an int? If so, that sounds fine to me.

via the CLI maybe?

That seems fine with me, too. Would this be a command-line flag that is only necessary during the unstable period?

My concern with unconditionally warning if you have a profile in a global config and you don't specify the command-line option is that you will end up with seeing that warning everywhere you don't specify the flag, which would be rather noisy. I kinda like the idea of only warning with -v since that would avoid the noise. That may not be very discoverable, though. ☹️ How common do we think profiles in global configs will be? If it's extremely rare, maybe the noisiness doesn't matter.

I had another question: What should the precedence be? This comment suggests it should be manifest over config. However, incremental uses config over manifest. The RFC doesn't specify. I was actually thinking it would be config over manifest to give the user control over what happens. With manifest over config their only alternative would be to edit the manifest. If config takes precedence, I wouldn't worry too much about a user globally specifying an important flag like panic (that just seems weird to me).

@alexcrichton

This comment has been minimized.

Member

alexcrichton commented May 8, 2018

I would feel more comfortable with just warnings

An excellent point and sounds good to me!

Do you mean string can override an int? If so, that sounds fine to me.

Oops yes, indeed!

Would this be a command-line flag that is only necessary during the unstable period?

Correct!

I'd also be ok not warning for now. It may lead to some confusion but you're right in that it's likely less than otherwise

What should the precedence be?

I definitely agree it's config over manifest because you modify your local config as opposed to the manifest which is shared amongst all users.

@ehuss

This comment has been minimized.

Contributor

ehuss commented Jun 9, 2018

Part 2 that includes config profiles has landed on nightly. The (minimal) documentation is at: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#config-profiles

@Centril

This comment has been minimized.

Contributor

Centril commented Nov 11, 2018

@Manishearth uhm -- why is this T-lang? was that a mistake?

@Manishearth Manishearth added T-cargo and removed T-lang labels Nov 11, 2018

@Manishearth

This comment has been minimized.

Member

Manishearth commented Nov 11, 2018

uh.....

yes 😄

oops

@jrmuizel

This comment has been minimized.

Contributor

jrmuizel commented Nov 20, 2018

We're suffering bad codegen issues in webrender with bincode that this would help to solve. Any chance someone can estimate a timeline for stabilization? Or should we try to look for alternative ways to solve this?

@ehuss

This comment has been minimized.

Contributor

ehuss commented Nov 22, 2018

@jrmuizel Unfortunately there currently is no timeline. I have a concern about the design, and I'll follow up here to try to spur discussion.

@ehuss

This comment has been minimized.

Contributor

ehuss commented Nov 22, 2018

The current implementation has some drawbacks regarding build script profiles and shared dependencies. Some of this is likely due to misunderstanding/miscommunication on my part. It centers around this statement in the RFC:

It is not possible to have the same crate compiled in different modes as a
build dependency and a regular dependency within the same profile when not
cross compiling. (This is a current limitation in Cargo, but it would be
nice if we could fix this)

This limitations was lifted, but has some possibly unintended consequences, and highlights some drawbacks. There might be some weird interactions with precedence, too. The current implementation merges overrides in the following order:

  1. Default hard-coded profile
  2. Base profile (like [profile.dev])
  3. build-override
  4. "*" override
  5. Package-specific override

An example of where this might be an issue: Build-override settings can cause shared dependencies to be built multiple times. This may be desirable in some cases, and in some it causes unwanted longer initial build times. Example:

[profile.dev.build-override]
debug = false

This will cause a shared dependency to be built once with debug=true (as a normal dependency) and once with debug=false (as a build dependency). You can override the shared dependency to make it compile once, but that is awkward.

How much of a problem is that?

There are some other hypothetical concerns about the lack of control:

  • If you have a package-specific override, there's no way to say if it is for a build-dependency or a normal dependency (it overrides in both cases). Probably ok?
  • Configuring proc-macros can be tedious. You need to target each individual one, and any non-shared dependency. You can't separately control shared dependencies of proc-macros with normal dependencies. This may be particularly weird when cross-compiling.

Implementation-wise I think these are easy to change, but UI/UX-wise it's not clear what's the best design. I would appreciate any feedback from anyone following here. Is there a better way to control normal dependencies shared with build scripts/proc-macro? Should it be easier to configure proc-macros? Are there any other concerns before stabilizing?

@alexcrichton

This comment has been minimized.

Member

alexcrichton commented Nov 28, 2018

I think I'm not personally too terribly concerned about multiple builds. Over time I think minimizing the number of builds for non-cross builds has ended up being a mistake as it just ends up causing a lot of problems here and there that amount to a large papercut. In practice these duplicated crates are always very small and at the bottom of the dependency graph where there's a lot of parallelism, so even practically I don't think it has a terrible amount of impact.

I think having package-specific overrides apply for both build/normal use cases is fine for now, we can always add more tweaks later! It's a pretty straightforward way to interpret it in the near term..

For procedural macros, I think I'd probably expect uniform handling of both procedural macros and build script dependencies (e.g. both should probably have debuginfo turned off by default). It's possible to pretty easily fold in proc-macro deps into the category of build-override I think?

@matklad

This comment has been minimized.

Member

matklad commented Nov 28, 2018

Over time I think minimizing the number of builds for non-cross builds has ended up being a mistake as it just ends up causing a lot of problems here and there that amount to a large papercut.

If we go a route of not deduping between dev/non-dev dependencies, then I feel a dedicated build profile (which, unlike build-override, is the same for --relase and --debug) is a better solution. More about build profile in this comment. I am out of the loop wrt current state of the art though, so might miss something obvious :)

@alexcrichton

This comment has been minimized.

Member

alexcrichton commented Nov 28, 2018

Yeah that seems reasonable to me, and such a profile we could define as "used for artifacts that are only transiently used during the build process itself", ranging from build scripts to procedural macros.

@ehuss

This comment has been minimized.

Contributor

ehuss commented Dec 9, 2018

fold in proc-macro deps into the category of build-override

I like the idea of including proc-macro/plugins in build-override. @Manishearth do you have any thoughts for or against doing that?

a dedicated build profile is a better solution.

I was originally in this camp, but I'm not convinced that it is necessarily better.

Pros for build-override:

  • Allows separate control of dev vs release settings.
  • Allows opportunistic reuse of shared dependencies (when using default settings).

Pros for dedicated build profile:

  • Avoid recompile of build scripts when switching between dev and release.
  • Only one place to specify settings (simpler to understand?).

The benefits to a dedicated profile do not seem very strong to me. @Manishearth gave explicit use cases where the current design is more powerful. Does anyone have any thoughts about that?

I was curious how common shared build dependencies are. I scanned all the crates on crates.io (all features enabled). Of the 8288 crates that have a build dependency anywhere in their graph, 2426 have at least one shared build dependency. The top 20 most shared crates are:

1949  libc
1872  winapi
1462  winapi-x86_64-pc-windows-gnu
1462  winapi-i686-pc-windows-gnu
1461  bitflags
1151  rand_core
955   fuchsia-zircon-sys
955   fuchsia-zircon
846   rand
846   cfg-if
738   phf_shared
737   siphasher
591   unicase
581   cloudabi
528   log
398   lazy_static
358   unicode-xid
264   quote
259   serde
239   syn

The rustc repo has an unusually large number of shared build dependencies (30).

@Mark-Simulacrum

This comment has been minimized.

Member

Mark-Simulacrum commented Dec 9, 2018

Given the inclusion of proc-macros and plugins, the build-override having a separate profile could make it feasible to have build scripts, proc macros, etc. compiled in release mode by-default (even on cargo build giving us overall faster compilation speeds. I don't think we have concrete numbers, but it does seem feasible that compiling essentially parts of the compiler (proc macros) in release mode would be advantageous.

@matklad

This comment has been minimized.

Member

matklad commented Dec 9, 2018

@Manishearth gave explicit use cases where the current design is more powerful. Does anyone have any thoughts about that?

Agree that having dev/release gives more power, but I'd expect that, in common case, one does need such power. If one indeed runs release/dev workflows on different machines (so that sharing compiled build scripts is not benefitial), then, on release machine, one can override build profile to match release via env-vars/cargo config. If you do dev and release on the same machine (overwhelmingly common case I suppose), then don't recompiling build scripts/proc macros seems to be a plain win?

Avoid recompile of build scripts when switching between dev and release.

A slightly different spin here is avoiding recompiling builds/proc macros when tweaking profiles. For example, my software is slow, so I want to build a flame graph, so I add debug = true to my release profile, and here I'd like to avoid recompiling syn.

Only one place to specify settings (simpler to understand?).

To me, a separate profile for "everything compiled for host" seems significantly simpler then a layer of overrides. So, I'd personally give this benefit a relatively heavy weight.

TL;DR: I think we need to weigh "power" against "common case": overrides win on power, dedicated profile wins on "common case"

@alexcrichton

This comment has been minimized.

Member

alexcrichton commented Dec 12, 2018

I think I'd personally be more of a fan of a dedicated profile, and we could figure out later how to have cascading overrides perhaps or something like that. Having a dedicated profile seems best for configuration and simplicity from the get-go

@Manishearth

This comment has been minimized.

Member

Manishearth commented Dec 12, 2018

Here's an example of Firefox's use case that may make some needs clear:

Firefox has a build time dependency on bindgen. Bindgen reads a crapload of c++ headers, does a bunch of analysis, and spits out rust code.

In debug mode, bindgen should be compiled in release mode because otherwise it's super slow.

However, the rust generation step is not slow. Compiling bindgen in release mode means compiling syn in release mode, which takes forever and is not really necessary.

I think a separate profile works with this? You can still support per-dep overrides with this.

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