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

Profile Overrides (RFC #2282 Part 1) #5384

Merged
merged 23 commits into from
Apr 27, 2018
Merged

Conversation

ehuss
Copy link
Contributor

@ehuss ehuss commented Apr 18, 2018

Profile Overrides (RFC #2282 Part 1)

WIP: Putting this up before I dig into writing tests, but should be mostly complete. I also have a variety of questions below.

This implements the ability to override profiles for dependencies and build scripts. This includes a general rework of how profiles work internally. Closes #5298.

Profile overrides are available with profile-overrides set in cargo-features in the manifest.

Part 2 is to implement profiles in config files (to be in a separate PR).

General overview of changes:

  • Profiles moved to core/profiles.rs. All profile selection is centralized there.
  • Removed Profile flags test, doc, run_custom_build, and check. Added CompileMode to Unit to replace them.
  • Profile is no longer a reference in Unit.
  • Also removed rustc_args/rustdoc_args from Profile and place them in Context. This is currently not very elegant because it is a special case, but it works. An alternate solution I considered was to leave them in the Profile and add a special uber-override layer. Let me know if you think it should change.
  • Did some general cleanup in generate_targets.

Misc Fixes

  • cargo check now honors the --release flag. Fixes Cargo check does not respect the --release flag #5218.
  • cargo build --test will set panic correctly for dependences. Fixes cargo build for tests builds dependencies with panic #5369.
  • cargo check --tests will no longer include bins twice (once as a normal check, once as a --test check). It only does --test check now.
    • Similarly, cargo check --test name no longer implicitly checks bins.
  • Examples are no longer considered a "test". (See cargo check --tests fails to compile macros in examples #5397). Consequences:
    • cargo test will continue to build examples as a regular build (no change).
    • cargo test --tests will no longer build examples at all.
    • cargo test --all-targets will no longer build examples as tests, but instead build them as a regular build (now matches cargo test behavior).
    • cargo check --all-targets will no longer check examples twice (once as
      normal, once as --test). It now only checks it once as a normal
      target.

Questions

  • Thumbs up/down on the general approach?
  • The method to detect if a package is a member of a workspace should probably be redone. I'm uncertain of the best approach. Maybe Workspace.members could be a set?
  • Hash and PartialEq are implemented manually for Profile only to avoid matching on the name field. The name field is only there for debug purposes. Is it worth it to keep name? Maybe useful for future use (like Internally storing profiles as special-cased booleans #4140)?
  • I'm unhappy with the Finished line summary that displays [unoptimized + debuginfo]. It doesn't actually show what was compiled. Currently it just picks the base "dev" or "release" profile. I'm not sure what a good solution is (to be accurate it would need to potentially display a list of different options). Is it ok? (See also Internally storing profiles as special-cased booleans #4140 for the wrong profile name being printed.)
  • Build-dependencies use different profiles based on whether or not --release flag is given. This means that if you want build-dependencies to always use a specific set of settings, you have to specify both [profile.dev.build_override] and [profile.release.build_override]. Is that reasonable (for now)? I've noticed some issues (like Single profile for build dependencies #1774, cargo bench builds build dependencies in release mode #2234, Feature request: compile build.rs scripts in release mode even when doing a debug build #2424) discussing having more control over how build-dependencies are handled.
  • build --bench xxx or --benches builds dependencies with dev profile, which may be surprising. --release does the correct thing. Perhaps print a warning when using cargo build that builds benchmark deps in dev mode?
  • Should it warn/error if you have an override for a package that does not exist?
  • Should it warn/error if you attempt to set panic on the test or bench profile?

TODO

  • I have a long list of tests to add.
  • Address a few "TODO" comments left behind.

Copy link
Member

@matklad matklad left a comment

Choose a reason for hiding this comment

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

Holy cow, this looks awesome @ehuss !

opt-level = 3

# All dependencies (but not this crate itself) will be compiled
# with -Copt-level=2 . This includes build dependencies.
Copy link
Member

Choose a reason for hiding this comment

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

This probably should mention workspaces?

[RUNNING] `rustc --crate-name foo [..]`
[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]",
)
// TODO: does_not_contain does not support patterns!
Copy link
Member

Choose a reason for hiding this comment

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

I think it does support [..]? Looks like [COMPILING] is missing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think it does, see:

MatchKind::NotPresent => {
if !actual.contains(out) {
Ok(())
} else {
Err(format!(
"expected not to find:\n\
{}\n\n\
but found in output:\n\
{}",
out, actual
))
}

I mentioned it on IRC, I'll take a look at fixing it.

@@ -1649,6 +1649,7 @@ fn test_run_implicit_test_target() {
)
.build();

// TODO FIXME: This needs to better verify that examples are not built.
Copy link
Member

Choose a reason for hiding this comment

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

This can be done by putting a compile-time error in the example.

@@ -1742,13 +1742,13 @@ fn test_run_implicit_example_target() {
)
.build();

// TODO FIXME - verify example does NOT get run.
Copy link
Member

Choose a reason for hiding this comment

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

Example could panic with runtime error to detect this.

@@ -347,20 +369,65 @@ impl<'de> de::Deserialize<'de> for U32OrBool {
}

#[derive(Deserialize, Serialize, Clone, Debug, Default)]
#[serde(rename_all = "kebab-case")]
Copy link
Member

Choose a reason for hiding this comment

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

❤️

bail!("Profile overrides cannot be nested.");
}
if self.panic.is_some() {
bail!("`panic` may not be specified in a build override.")
Copy link
Member

Choose a reason for hiding this comment

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

s/build/profile/ ?

Check { test: bool },
/// A target being built for a benchmark.
Bench,
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we can merge Test and Bench, or perhaps move Bench to ProfileFor? From compiler pov, I think test & bench are indistinguishable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

An interesting idea. I'll think about it some more, but it may complicate cargo_compile since it needs to know the overall mode we're in (like selecting default targets, etc.).

// ancestor's profile. However `cargo clean -p` can hit this
// path.
// TODO: I think `cargo clean -p xxx` is not cleaning out
// the "OUT_DIR" directory. This is not a new bug.
Copy link
Member

Choose a reason for hiding this comment

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

OUT_DIR here is the --out-dir? It can't be cleaned by clean at all I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think so. I was thinking of the build_script_out_dir which is something like target/debug/build/pkgname-hash/out. But I was mistaken, I was actually just seeing the output, stderr, and root-output directories left behind. Probably not too important, and unrelated to these changes.

cx: &Context<'a, 'cfg>,
map: &mut HashMap<Unit<'a>, Profile>,
) {
if !map.contains_key(unit) {
Copy link
Member

Choose a reason for hiding this comment

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

It's slightly better to use early returns here, to save indentation and keep mental stack shallow:

if map.contains_key(unit) {
    return;
}

Ideally, I think we should just top-sort the units once and avoid writing recursive functions all over the place, but this definitely shouldn't be done in this PR.

let unit_deps = compute_deps(unit, cx, deps, profile_for)?;
let to_insert: Vec<_> = unit_deps.iter().map(|&(unit, _)| unit).collect();
deps.insert(*unit, to_insert);
for (unit, profile_for) in unit_deps {
Copy link
Member

Choose a reason for hiding this comment

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

If I add assert_eq!(unit.profile_for, profile_for); it is not triggered, so, perhaps, we don't need to return pairs after all?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There was a bug. Since units no longer include ProfileFor, this is no longer the case.

@matklad
Copy link
Member

matklad commented Apr 18, 2018

Thumbs up/down on the general approach?

👍 👍 👍

The method to detect if a package is a member of a workspace should probably be redone.

I think current implementation is OK: linear scan is perfectly fine until you have hundreds of members.

Is it worth it to keep name?

I have no opinion here :)

I'm unhappy with the Finished line summary

It still works correctly when no overrides are present, and otherwise it is a minor detail, so I suggest to punt on this for know: current solution seems at least OK to me!

Build-dependencies use different profiles based on whether or not --release flag is given.

Yeah, that seems generally wrong, but OK for now, because it more or less matches the current behavior. I would say that this is probably an instance where we do need a separate, not dev/not release profile, but let's deal with that later.

build --bench xxx or --benches builds dependencies with dev profile,

Hm, is this the current behavior? It definitely is a bug!

Should it warn/error if you have an override for a package that does not exist?

Let's give at least a warning if it's not too hard: typoes are likely in the names of packages, especially with - vs _ distinction.

Should it warn/error if you attempt to set panic on the test or bench profile?

It is allowed now, right? Then I think error is probably not feasible, but the warning seems useful!

}

impl ProfileFor {
pub fn all_values() -> Vec<ProfileFor> {
Copy link
Member

Choose a reason for hiding this comment

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

Mincooptimization, but here and in CompileMode::all_modes, we could return &'static [Stuff], to save some allocations.

/// command-line for `cargo rustc` and `cargo rustdoc`. These commands
/// only support one target, but we don't want the args passed to any
/// dependencies.
pub extra_compiler_args: HashMap<Unit<'a>, Vec<String>>,
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps we can use Option<(Unit, Vec<String>)> instead? Let's also document that this is overloaded in the sense that this can be arguments for different tools, rustc or rustdoc, depending on the command.

// Helper for creating a Unit struct.
let new_unit =
|pkg: &'a Package, target: &'a Target, mode: CompileMode, profile_for: ProfileFor| {
let actual_profile_for = if profile_for != ProfileFor::Any {
Copy link
Member

Choose a reason for hiding this comment

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

Just a personal taste, but I would name this just profile_for to shadow the input parameter, so that, down the line, we can't use the wrong profile_for.

@ehuss
Copy link
Contributor Author

ehuss commented Apr 18, 2018

Thanks @matklad for doing such a detailed review so quickly!

Unfortunately, I've discovered a pretty fundamental flaw in how it works. Units are not being properly de-duplicated. Example: test→lib and bin→lib, the lib gets compiled twice (once as a regular dependency, and again as a test dependency). But if you don't have panic set they get compiled with the exact same settings!

I think Profile really must be a part of Unit. I had a previous attempt that had it part of the Unit, but I was concerned about the huge amount of copying. However, I was using Strings before (instead of InternedString), and it seemed much worse at the time (require lots of clone() calls). I'm going to rewrite it to use this approach. Profile is only 24 bytes on my system, so it shouldn't be too bad. On the plus side, it should simplify some things.

Sorry for the churn, I'll try to fix this today and address all your comments.

@matklad
Copy link
Member

matklad commented Apr 18, 2018

But if you don't have panic set they get compiled with the exact same settings!

Wow, that's interesting! I quite like the current approach actually, but yeah, looks like we'd better to store raw flags to avoid duplication 😿

@ehuss
Copy link
Contributor Author

ehuss commented Apr 19, 2018

I've pushed the update that now embeds the profile inside Unit. It works quite a bit better now. I fear there are still some hidden gotchas, so I'll need to spend a fair amount of time making tests. I left a long comment in there about a subtle issue with cargo test, that I think I'm going to revisit because it's not quite right. Getting proper panic support is turning out to be very tricky. Doc tests are also turning out to be very tricky, because it greedily links against any libs it finds in Context.libraries.

@bors
Copy link
Collaborator

bors commented Apr 19, 2018

☔ The latest upstream changes (presumably #5392) made this pull request unmergeable. Please resolve the merge conflicts.

// cleared, and avoid building the lib thrice (once with `panic`, once
// without, once for --test). In particular, the lib included for
// doctests and examples are `Build` mode here.
let profile_for = if unit.mode.is_any_test() || cx.build_config.test {
Copy link
Member

Choose a reason for hiding this comment

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

Hm, I am genuinely not sure what the correct behavior would be here? Suppose we are compiling the integration test, which only executes the binary in the same package. It is reasonable to assume that the binary might be compiled with panic=abort, but the test itself should be compiled with panic=unwind, so common dependencies should be compiled twice.

Perhaps the best path forward is to preserve the current, buggy, behavior, land this PR, and then fix the behavior in a follow-up PR?

// `panic` set.
//
// As a consequence, Examples and Binaries get compiled
// without `panic` set. This probably isn't a bad deal.
Copy link
Member

Choose a reason for hiding this comment

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

Ah yeah, this is exactly what I was thinking about... Hm, compiling binaries with different panic setting actually seems like a big deal to me...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea, it's a tricky issue, I was going to ask about it. The immediate problem is that doc tests attempt to link with everything in Context.libraries. I'm uncertain what the easiest way to fix that would be. One thought I had would be to change run_doc_tests to only grab the dependencies for the CompileMode::Doctest unit and remove Context.libraries. There would be some other minor things to accommodate that, but I think it should work. I can do it now or later, let me know. It would remove this weird special-case.

Copy link
Member

Choose a reason for hiding this comment

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

Would it be difficult to preserve exactly the behavior (and bugs :) ) we have today? I think this would be the best way forward for now, because changes which both add unstable features and fix/modify stable behavior are tricky...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea, it should be preserving the old behavior. I have a few changes I haven't pushed, yet, that ensure that. I'm keeping a list of behavior changes, and I'll post it soon. A few are clearly bug fixes, but a couple are questionable. We can review them when I post it.

@ehuss
Copy link
Contributor Author

ehuss commented Apr 22, 2018

The CI failures are due to the introduction of rust-lang/rust#49289 (starting with nightly 2018-04-15). cargo check will now emit rmeta files for all targets. This will actually address #3624. This means that running cargo check twice will no longer re-check binaries. However, I'm uncertain how the test should be structured now. It's trying to verify the "freshness", but the behavior has changed between rust versions. How do you typically accommodate different rustc versions?

@ehuss
Copy link
Contributor Author

ehuss commented Apr 22, 2018

(I've updated the first comment with some other Misc Fixes that probably aren't too controversial, but please review them. Sorry for the long comment, I just wanted to be thorough.)

The latest push includes some tests (profile_targets.rs) that illustrate what I was asking about before in regards to a way to verify exactly which commands get run, but ignoring the order. I don't think it would be possible to achieve the same level of verification with the existing functions (in particular, verifying what doesn't get run). Let me know if you would prefer it to be done in a different way, though. One alternative thought I had would be to have an option for with_json to ignore the order of messages, and add some more information to ArtifactProfile to be able to distinguish which profile is used.

Questions

  • The doc profile does not seem to be used. Internally it is assigned to running rustdoc, but rustdoc does not use any flags from the profile. Is that intended? Perhaps we could deprecate it (or maybe just remove it from the docs)? There are about 24 crates on crates.io that set it for whatever reason.
  • I just want to double-check... I've been using the term "profile override" in a few places, but I'm starting to feel that might not be clear (compared to [profile.dev] is overriding the default dev profile). "package profile override" would be more accurate, but is maybe a little wordy?

Behavior Changes to Discuss

The following are new changes in behavior that I'm uncertain if we should keep or change.

  • cargo build --release --all-targets now compiles libs and bins twice (once for benchmarks, once for unit-tests). Previously the de-dupe logic would only compile them once (since they are essentially identical). This is because with CompileMode in Unit it distinguishes between a Bench and Test, so it doesn't get de-duped. I could maybe add a special case for this?

  • If a build.rs is needed for different profiles, it will now be run separately for each one. For example, build --all-targets will run build.rs once for the dev targets, and once for the bench targets (because the opt-level is different between them). I think this is more-correct behavior, but may be a surprising change to some. Thoughts?

Panic Changes and Issues

Getting panic correct has been quite tricky. This is the list of changes and issues I'm currently aware of. These assume you have panic set in some profile.

  • cargo build with tests or benches was previously failing (error: the crate `foo` is compiled with the panic strategy `abort` which is incompatible with this crate's strategy of `unwind` ). This is now fixed.

  • cargo test builds binaries without panic set. This is a tricky issue. If this is fixed, then the lib would get built three times (once without panic for tests, once with panic for bins, once with --test as a unit-test). The bigger problem is that rustdoc for doc-tests would fail to run because it would pick up both copies of the lib. See comment previously about needing to change Context.libraries to fix this. Leave as-is? Fix now/later?

  • build.rs scripts and their dependencies have panic cleared because the panic-clearing logic thinks it is a compiler plugin (see build_base_args). However, I don't think build.rs scripts should be treated the same as compiler plugins/proc-macros. Would it be worth it to fix this? The change is fairly easy -- I would probably add for_plugin (for_compiler?) to Target and adjust walk_used_in_plugin_map to use that instead of for_host. This is a minor issue, but I was a little surprised to see it in one of my test cases.
    The consequences are:

    • build.rs scripts are always built without panic.
    • If you have a dependency that is shared as a regular dependency and a build dependency, and you run cargo test or cargo bench, the dependency will be built twice, but with the same settings. Internally it thinks one is "dev without panic" (for tests) and the other is regular "dev" (for build.rs). It works fine, but slows things down since the shared dependency is built twice.

Known Bugs

  • DEBUG env var in build.rs is incorrect if profile debug=0. DEBUG env var in build.rs is incorrect if profile debug=0 #5370. Will fix in a separate PR.
  • Having a global build_config.release flag causes some issues. I'm uncertain how difficult this is to fix. Generally getting rid of the global build_config.release would be the correct approach. Thoughts? Address now or later? I think this only affects cargo build, and the workaround is to pass --release to it. Consequences:
  • cargo check only ever uses dev or release profile. For example, cargo check --test foo uses dev instead of test. I'm uncertain how any of the profile flags might affect rustc when running check.

@matklad
Copy link
Member

matklad commented Apr 22, 2018

The latest push includes some tests (profile_targets.rs) that illustrate what I was asking about before in regards to a way to verify exactly which commands get run, but ignoring the order. I don't think it would be possible to achieve the same level of verification with the existing functions (in particular, verifying what doesn't get run)

Yeah, that seems reasonable. Let's add with_stderr_unordered perhaps which compares sets of lines? (EDIT: and now matklad actually reads the diff and sees with_stderr_unordered :) )

@matklad
Copy link
Member

matklad commented Apr 22, 2018

However, I'm uncertain how the test should be structured now. It's trying to verify the "freshness", but the behavior has changed between rust versions. How do you typically accommodate different rustc versions?

I think we can add

// If you are reading this, and 1.27.0 is already stable, feel free to just remove this if!
if !is_nightly() { return; } 

@matklad
Copy link
Member

matklad commented Apr 22, 2018

The doc profile does not seem to be used. Internally it is assigned to running rustdoc, but rustdoc does not use any flags from the profile. Is that intended? Perhaps we could deprecate it (or maybe just remove it from the docs)? There are about 24 crates on crates.io that set it for whatever reason.

Yep, I believe we can safely deprecate this profile. That is, leave the syntax in Cargo.toml, give a warning about it, and remove traces of doc profile from the code. Eventually I think we'll do the same for test and bench profiles, but they are used, so we'll deal with that separately.

"package profile override" would be more accurate, but is maybe a little wordy?

I personally think about it as just "package profile". We need override in the surface syntax just to namespace this feature. I wonder if a sweeter syntax is possible? [profiles.dev.for.image] perhaps?

cargo build --release --all-targets now compiles libs and bins twice

I think it is important to remove this duplication. Could we get rid of the Bench compile mode?

If a build.rs is needed for different profiles, it will now be run separately for each one. For example, build --all-targets will run build.rs once for the dev targets, and once for the bench targets (because the opt-level is different between them). I think this is more-correct behavior, but may be a surprising change to some. Thoughts?

Hm, so currently build_scripts are compiled only with dev or release profiles. I suggest we keep this behavior, for three reasons:

  • No change means that we won't break someone's code,
  • If we remove all profiles except dev and test, we'll effectively have to change it back,
  • Test fiddle with panic setting, and this I think should not affect build scripts.

Long term, I believe the current behavior of using both dev and release for build scripts is also wrong. I think we need a separate use-visible profile for build scripts and their dependencies, so that cargo build && cargo build --release does not compile build scripts twice.

I don't think there should be any sharing between the build script and the main code. Especially for cross-compilation, it seems unreasonable to use the same profile for both.

@matklad
Copy link
Member

matklad commented Apr 22, 2018

cargo test builds binaries without panic set. This is a tricky issue. If this is fixed, then the lib would get built three times (once without panic for tests, once with panic for bins, once with --test as a unit-test)

This is the current behavior, right? I believe this is very wrong, but fixing it indeed adds even more compilations. So let's leave it as is for know.

However, I don't think build.rs scripts should be treated the same as compiler plugins/proc-macros.

Could you elaborate why do you think so? At least to me, they seem very similar indeed! And my hypothecical build profile would apply both to proc macros, build scripts and tasks (from aaturon's blogpost). It seems they all are things which a build only on host, and don't actually care about --release flag.

If you have a dependency that is shared as a regular dependency and a build dependency, and you run cargo test or cargo bench, the dependency will be built twice, but with the same settings. Internally it thinks one is "dev without panic" (for tests) and the other is regular "dev" (for build.rs).

I think it's fine to leave this duplication as is, but, if we are to fix it, then perhaps " Internally it thinks one is "dev without panic" (for tests) and the other is regular "dev" (for build.rs)." is the bit that needs adjustments?

@ehuss
Copy link
Contributor Author

ehuss commented Apr 22, 2018

Yep, I believe we can safely deprecate this profile.

Would you like that now, or in a separate PR (since this one is getting a little long)?

[profiles.dev.for.image] perhaps?

Hm, that is a little clearer. Not sure if anyone else wants to weigh in?

I think it is important to remove this duplication. Could we get rid of the Bench compile mode?

It would be nice, because once the Unit is constructed, it is not needed. However, there would need to be a way for generate_targets and generate_default_targets to know that we are cargo bench to select the correct targets and profiles. I could pass a flag in through CompileOptions, what do you think? An alternate solution is to just swap the mode in generate_targets when the Unit is made which would be a one-line change to fix this.

Hm, so currently build_scripts are compiled only with dev or release profiles. I suggest we keep this behavior, for three reasons:

This is how it works (before and after this change) for compiling the build.rs script. I was talking about executing it. In the new code, it is run twice because the OPT_LEVEL is different for different targets' dependencies. This goes back to, should cargo build --bench xxx build the dependencies of a benchmark in dev mode? I guess I left it sort this in a halfway state. I see two options. One is to restore the old behavior (benches get dev for custom-built dependencies), and I can fix that by just checking the global release flag for running build.rs, which I think is an easy change. The other is to actually fix that problem, which might be more difficult (involves removing the use of the global release flag). I'm fine with restoring the old behavior, this is an edge case (building benchmarks with cargo build without --release flag). I'm also fine with trying to fix it properly. Or other options (like not even trying to build benches with cargo build if you don't set --release?).

I think we need a separate use-visible profile for build scripts and their dependencies

Isn't that what build-override is essentially? I think I mentioned it earlier, that this pseudo-profile already exists, but you have to define it twice (profile.dev.build-override and profile.release.build-override), and it has a weird relationship with other overrides.

This brings up a issue that was nagging at me. I didn't fully understand the reasoning for the override precedence in the RFC. Since other rules take precedence over build-override, it seems like that can yank away the ability to independently control how a build script is compiled vs how the dependency itself is compiled. There is a comment in the RFC thread that you and Manishearth had separately about this, but it didn't make sense to me. The build-dependencies are always kept independent of the final binary, even if we removed the precedence. Changing the precedence could give the user more control. (Or even adding something like [profile.scripts.overrides.somedep] which would be very easy to do.) I guess my rambling question is, why not give independent control?

Could you elaborate why do you think so?

The only reason this exception is made is to ensure the panic flag is cleared (because plugins are linked with things that can't have it set). build.rs scripts don't have that limitation since they aren't linked with the compiler. I'm fine with clearing it for build.rs. I'm uncertain how likely it is to be a problem for someone. Considering it has always been this way, and things built with panic=abort is very rare to start with, it might not bother anyone. However, if panic is set, it could cause shared dependencies to be needlessly built twice.

I think it's fine to leave this duplication as is, but, if we are to fix it, then perhaps " Internally it thinks one is "dev without panic" (for tests) and the other is regular "dev" (for build.rs)." is the bit that needs adjustments?

Correct, it's a small, trivial change to say "panic = None" for build scripts in the profile builder.

@matklad
Copy link
Member

matklad commented Apr 22, 2018

Would you like that now, or in a separate PR (since this one is getting a little long)?

A separate one. I'd prefer to merge this sooner, and not to fix all profile bugs at once :)

Hm, that is a little clearer. Not sure if anyone else wants to weigh in?

I personally don't care about syntax at this stage. There's already override in there, so let's not change it for now.

I could pass a flag in through CompileOptions, what do you think? An alternate solution is to just swap the mode in generate_targets when the Unit is made which would be a one-line change to fix this.

Another option is to split CompileMode into two enums: one to be used with CompileOptions, which corresponds to which cli options were passed, and one to be used with Units. But "swapping the mode" solution also sounds fine with me!

I was talking about executing it

Oh, right! I've completely misunderstood your original comment then! The new behavior indeed seems more correct to me as well!

Isn't that what build-override is essentially? I think I mentioned it earlier, that this pseudo-profile already exists, but you have to define it twice (profile.dev.build-override and profile.release.build-override), and it has a weird relationship with other overrides.

Yep, it's almost like build-override, with two important differences: you don't need to override it twice, and, by default, build profile is the same for cargo build and cargo build --release.

The build-dependencies are always kept independent of the final binary, even if we removed the precedence.

Hm, I think we though that dependencies are shared between the library and the build script. Because they are independent, priority for build_overrides indeed does not make sense.

@ehuss
Copy link
Contributor Author

ehuss commented Apr 22, 2018

Hm, I think we though that dependencies are shared between the library and the build script. Because they are independent, priority for build_overrides indeed does not make sense.

OK, let me know if you want to iterate on this now or later. It's pretty easy to change.

Let me know if there's anything else. I think I've gone through the issues I can think of.

@@ -38,27 +50,33 @@ fn deps_of<'a, 'b, 'cfg>(
unit: &Unit<'a>,
cx: &Context<'a, 'cfg>,
deps: &'b mut HashMap<Unit<'a>, Vec<Unit<'a>>>,
profile_for: ProfileFor,
Copy link
Member

Choose a reason for hiding this comment

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

I feel uneasy about this profile_for argument. This function is memoized, so the sequence of calls deps_of(u, cx, deps, ProfileFor::Any); deps_of(u, cx, deps, ProfileFor::TestDependency) will return the same result each time (and the result might be different if we swap the order of calls).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I understand what you mean. In practice, that's never a problem for TestDependency since it only exists for clearing panic, and you'll never ask for a unit with panic set as a TestDependency.

I'm trying to think of a scenario where it would matter for CustomBuild, but I can't think of any. It might matter if we later add a "build-script"-specific profile, and you have an override deep in a dependency chain.

Adding it to the map would require a separate de-duplication phase, which I think would add ~15 lines of code.

Let me know if you think it's important. I'd lean towards not changing it unless there's a testable case. Or to make it easier to add a build-script profile later if there's a strong chance of that.

Copy link
Member

Choose a reason for hiding this comment

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

I think I'm fine with it: I would love to refactor code in such a way that this problem just not arises, or at least to put some sort of run-time assertion, but I have no ideas of how to actually do this. Storing pairs in the HashMap would be worse than the status quo.

Let's perhaps just add a small comment here though?

@@ -273,28 +283,65 @@ fn maybe_lib<'a, 'cfg>(unit: &Unit<'a>, cx: &Context<'a, 'cfg>) -> Option<Unit<'
/// script itself doesn't have any dependencies, so even in that case a unit
/// of work is still returned. `None` is only returned if the package has no
/// build script.
fn dep_build_script<'a, 'cfg>(unit: &Unit<'a>, cx: &Context<'a, 'cfg>) -> Option<Unit<'a>> {
fn dep_build_script<'a>(cx: &Context, unit: &Unit<'a>) -> Option<(Unit<'a>, ProfileFor)> {
Copy link
Member

Choose a reason for hiding this comment

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

Some function in this module now use cx, unit arg order, and some use (unit, cx). Let's unify them to one order? I don't have any preference for a specific order, as long as it is an order :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea, I derped. I had an earlier version that I had removed Context, but when I added it back I should have kept it the same.

foo custom build PROFILE=release DEBUG=false OPT_LEVEL=3
[RUNNING] `rustc --crate-name foo src[/]lib.rs --crate-type lib --emit=dep-info,link -C opt-level=3 -C panic=abort -C codegen-units=2 [..]
[RUNNING] `rustc --crate-name foo src[/]main.rs --crate-type bin --emit=dep-info,link -C opt-level=3 -C panic=abort -C codegen-units=2 [..]
[FINISHED] release [optimized] [..]
Copy link
Member

Choose a reason for hiding this comment

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

I guess I don't understand something about panic handling... How this even works? We link bar, which is compiled with panic=unwind into foo, which is compiled with panic=abort and rustc does not complain. Is it the expected behavior? Testing locally, I see that unwind -> abort direction works, and abort -> unwind fails. Does this mean that it is always safe to compile dependencies with panic = unwind?

This probably has nothing to do with the PR though, because the behavior has not changed here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm, yea, I don't know much how panic handling is implemented. I did a test where I attempt to panic in a dependency that was compiled with unwind, and it still aborts. It looks like it just affects whether libpanic_abort or libpanic_unwind is linked. I'm guessing since the API is the same, the dependencies don't know the difference.

Copy link
Member

Choose a reason for hiding this comment

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

It looks like it just affects whether libpanic_abort or libpanic_unwind is linked. I

I would guess that it also affects generation of landing pads? So we might be getting a bunch of dead code here?

@alexcrichton could you explain how mixing panic modes is supposed to work?

Copy link
Member

Choose a reason for hiding this comment

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

Ah yes the observed behavior here is expected, we , for example, ship panic=unwind libstd to everyone but still allow compiling with panic=abort

@matklad
Copy link
Member

matklad commented Apr 24, 2018

I've reviewed everything except the tests (which are super awesome!), and I don't have any more comments here! I would probably want to inrdocude a separate feature flag to build override, and maybe switch to a separate build profile, but this don't need to happen in this PR.

I'd like to hear what @alexcrichton thinks as well! :)

Copy link
Member

@alexcrichton alexcrichton left a comment

Choose a reason for hiding this comment

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

Everything is looking amazing here. I'm blown away at the thoroughness and cleanliness of the patch, thanks so much for taking this on @ehuss!

I've got basically a few tiny nits below, but overall the strategy here sounds great. I don't think I've fully grok'd everything yet, especially the ProfileFor threw me particularly for a spin around the unit_dependencies.rs files. Nothing here looks wrong though, tests are all passing, and the caveats you've mentioned along the way all sound great to me.

tl;dr; I'd be happy to land this at any time!

}

fn validate_packages(&self, shell: &mut Shell, packages: &HashSet<&str>) -> CargoResult<()> {
if let Some(ref toml) = self.toml {
Copy link
Member

Choose a reason for hiding this comment

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

Stylistically I'm always personally a fan of reducing indent here:

let toml = match self.toml {
    Some(ref toml) => toml,
    None => return Ok(()),
};
// ...

if let Some(ref toml) = self.toml {
if let Some(ref overrides) = toml.overrides {
for key in overrides.keys().filter(|k| k.as_str() != "*") {
if !packages.contains(key.as_str()) {
Copy link
Member

Choose a reason for hiding this comment

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

I think this is something we'll want to note on the tracking issue, I'd expect the packages set here to be more like a HashSet<PackageId> (or a Resolve) and the keys of overrides here would be a PackageIdSpec in theory

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure I follow. Are you saying that overrides could support full spec strings? That looks like it'd be pretty easy to support.

Copy link
Member

Choose a reason for hiding this comment

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

Basically yeah. We've got the idea of a "package id specification" which is intended to describe a particular crate in a crate graph unbambiguously, and this would allow you to, for example, customize the compilation of foo from crates.io but not customize foo from github.com/example/foo (or something like that)

@matklad
Copy link
Member

matklad commented Apr 27, 2018

@bors r+

Thank you so much for this, @ehuss! It was an amazing experience reviewing all this awesome work!

@bors
Copy link
Collaborator

bors commented Apr 27, 2018

📋 Looks like this PR is still in progress, ignoring approval

@matklad matklad changed the title [WIP] Profile Overrides (RFC #2282 Part 1) Profile Overrides (RFC #2282 Part 1) Apr 27, 2018
@matklad
Copy link
Member

matklad commented Apr 27, 2018

@bors r+

My undestanding is that this is not really a WIP, but, anyway, we could land additional fixes in separate PR's just fine!

@bors
Copy link
Collaborator

bors commented Apr 27, 2018

📌 Commit e82e9c1 has been approved by matklad

@bors
Copy link
Collaborator

bors commented Apr 27, 2018

⌛ Testing commit e82e9c1 with merge c24a097...

bors added a commit that referenced this pull request Apr 27, 2018
Profile Overrides (RFC #2282 Part 1)

Profile Overrides (RFC #2282 Part 1)

WIP: Putting this up before I dig into writing tests, but should be mostly complete.  I also have a variety of questions below.

This implements the ability to override profiles for dependencies and build scripts.  This includes a general rework of how profiles work internally. Closes #5298.

Profile overrides are available with `profile-overrides` set in `cargo-features` in the manifest.

Part 2 is to implement profiles in config files (to be in a separate PR).

General overview of changes:

- `Profiles` moved to `core/profiles.rs`. All profile selection is centralized there.
- Removed Profile flags `test`, `doc`, `run_custom_build`, and `check`.
- Removed `Profile` from `Unit` and replaced it with two enums: `CompileMode` and `ProfileFor`.  This is the minimum information needed to compute profiles at a later stage.
- Also removed `rustc_args`/`rustdoc_args` from `Profile` and place them in `Context`.  This is currently not very elegant because it is a special case, but it works. An alternate solution I considered was to leave them in the `Profile` and add a special uber-override layer.  Let me know if you think it should change.
- Did some general cleanup in `generate_targets`.

## Misc Fixes
- `cargo check` now honors the `--release` flag.  Fixes #5218.
- `cargo build --test` will set `panic` correctly for dependences. Fixes #5369.
- `cargo check --tests` will no longer include bins twice (once as a normal check, once as a `--test` check).  It only does `--test` check now.
    - Similarly, `cargo check --test name` no longer implicitly checks bins.
- Examples are no longer considered a "test".  (See #5397). Consequences:
    - `cargo test` will continue to build examples as a regular build (no change).
    - `cargo test --tests` will no longer build examples at all.
    - `cargo test --all-targets` will no longer build examples as tests, but instead build them as a regular build (now matches `cargo test` behavior).
    - `cargo check --all-targets` will no longer check examples twice (once as
      normal, once as `--test`).  It now only checks it once as a normal
      target.

## Questions
- Thumbs up/down on the general approach?
- The method to detect if a package is a member of a workspace should probably be redone.  I'm uncertain of the best approach.  Maybe `Workspace.members` could be a set?
- `Hash` and `PartialEq` are implemented manually for `Profile` only to avoid matching on the `name` field.  The `name` field is only there for debug purposes. Is it worth it to keep `name`?  Maybe useful for future use (like #4140)?
- I'm unhappy with the `Finished` line summary that displays `[unoptimized + debuginfo]`.  It doesn't actually show what was compiled.  Currently it just picks the base "dev" or "release" profile.  I'm not sure what a good solution is (to be accurate it would need to potentially display a list of different options).  Is it ok?  (See also #4140 for the wrong profile name being printed.)
- Build-dependencies use different profiles based on whether or not `--release` flag is given.  This means that if you want build-dependencies to always use a specific set of settings, you have to specify both `[profile.dev.build_override]` and `[profile.release.build_override]`.  Is that reasonable (for now)?  I've noticed some issues (like #1774, #2234, #2424) discussing having more control over how build-dependencies are handled.
- `build --bench xxx` or `--benches` builds dependencies with dev profile, which may be surprising.  `--release` does the correct thing.  Perhaps print a warning when using `cargo build` that builds benchmark deps in dev mode?
- Should it warn/error if you have an override for a package that does not exist?
- Should it warn/error if you attempt to set `panic` on the `test` or `bench` profile?

## TODO
- I have a long list of tests to add.
- Address a few "TODO" comments left behind.
@bors
Copy link
Collaborator

bors commented Apr 27, 2018

☀️ Test successful - status-appveyor, status-travis
Approved by: matklad
Pushing c24a097 to master...

@bors bors merged commit e82e9c1 into rust-lang:master Apr 27, 2018
bors added a commit that referenced this pull request Dec 2, 2019
Stabilize profile-overrides.

This stabilizes the profile-overrides feature. This was proposed in [RFC 2282](rust-lang/rfcs#2282) and implemented in #5384. Tracking issue is rust-lang/rust#48683.

This is intended to land in 1.41 which will reach the stable channel on Jan 30th.

This includes a new documentation chapter on profiles. See the ["Overrides" section](https://github.com/rust-lang/cargo/blob/9c993a92ce33f219aaaed963bef51fc0f6a7677a/src/doc/src/reference/profiles.md#overrides) in `profiles.md` file for details on what is being stabilized.

Note: The `config-profile` and `named-profiles` features are still unstable.

Closes #6214

**Concerns**
- There is some risk that `build-override` may be confusing with the [proposed future dedicated `build` profile](#6577). There is some more discussion about the differences at rust-lang/rust#48683 (comment). I don't expect it to be a significant drawback. If we proceed later with a dedicated `build` profile, I think we can handle explaining the differences in the documentation. (The linked PR is designed to work with profile-overrides.)
- I don't anticipate any unexpected interactions with `config-profiles` or `named-profiles`.
- Some of the syntax like `"*"` or `build-override` may not be immediately obvious what it means without reading the documentation. Nobody suggested any alternatives, though.
- Configuring overrides for multiple packages is currently a pain, as you have to repeat the settings separately for each package. I propose that we can extend the syntax in the future to allow a comma-separated list of package names to alleviate this concern if it is deemed worthwhile.
- The user may not know which packages to override, particularly for some dependencies that are split across multiple crates. I think, since this is an advanced feature, the user will likely be comfortable with using things like `cargo tree` to understand what needs to be overridden. There is [some discussion](rust-lang/rust#48683 (comment)) in the tracking issue about automatically including a package's dependencies, but this is somewhat complex.
- There is some possibly confusing interaction with the test/bench profile. Because dependencies are always built with the dev/release profiles, overridding test/bench usually does not have an effect (unless specifying a workspace member that is being tested/benched). Overriding test/bench was previously prohibited, but was relaxed when named profiles were added.
- We may want to allow overriding the `panic`, `lto`, and `rpath` settings in the future. I can imagine a case where someone has a large workspace, and wants to override those settings for just one package in the workspace. They are currently not allowed because it doesn't make sense to change those settings for rlibs, and `panic` usually needs to be in sync for the whole profile.
- There are some strange interactions with `dylib` crates detailed in rust-lang/rust#64319. A fix was attempted, but later reverted. Since `dylib` crates are rare (this mostly applied to `libstd`), and a workaround was implemented for `build-std` (it no longer builds a dylib), I'm not too worried about this.
- The interaction with `share-generics` can be quite confusing (see rust-lang/rust#63484). I added a section in the docs that tries to address this concern. It's also possible future versions of `rustc` may handle this better.
- The new documentation duplicates some of the information in the rustc book.  I think it's fine, as there are subtle differences, and it avoids needing to flip back and forth between the two books to learn what the settings do.
@ehuss ehuss added this to the 1.27.0 milestone Feb 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants