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

Allow configuring unstable flags via config file #8393

Merged
merged 17 commits into from
Jul 10, 2020
Merged

Allow configuring unstable flags via config file #8393

merged 17 commits into from
Jul 10, 2020

Conversation

bearcage
Copy link
Contributor

Summary

This fixes #8127 by mapping the unstable key in .cargo/config to Z flags.

It should have no impact on stable/beta cargo, and on nightlies it gives folks the ability to configure Z flags for an entire project or workspace. This is meant to make it easier to try things like the new features resolver behavior, mtime-on-use, build-std, or timing reports across a whole project.

I've also included a small (but entirely independent) ergonomics change -- treating dashes and underscores identically in Z flags. That's along for the ride in this PR as the last commit, and is included here because it makes for more idiomatic toml file keys (print_im_a_teapot = yes vs print-im-a-teapot = yes). Please let me know if y'all would prefer that be in a separate PR, or not happen at all.

Test Plan

Apologies if I've missed anything -- this is my first cargo contrib and I've tried to hew to the contributing guide. If I've slipped up, please let me know how and I'll fix it.

NB. My linux machine doesn't have multilib set up, so I disabled cross tests.

  • cargo test passes for each commit in the stack
  • I formatted each commit in the stack with rustfmt
  • New tests are included alongside the relevant change for each change
  • I've validated each test by locally undoing the code change they support and confirming failure.
  • The CLI wins, for both enable and disabling Z flags, as you'd expect.

Keys in unstable which do not correspond with a Z flag will trigger an error indicating the invalid flag came from a config file read:

Invalid [unstable] entry in Cargo config

Caused by:
  unknown `-Z` flag specified: an-invalid-flag

If you'd like to see a test case which isn't represented here, I'm happy to add it. Just let me know.

Documentation

I've included commits in this stack updating the only docs page that seemed relevant to me, skimming the book -- unstable.md.

@rust-highfive
Copy link

Thanks for the pull request, and welcome! The Rust team is excited to review your changes, and you should hear from @alexcrichton (or someone else) soon.

If any changes to this PR are deemed necessary, please add them as extra commits. This ensures that the reviewer can see what has changed since they last reviewed the code. Due to the way GitHub handles out-of-date commits, this should also make it reasonably obvious what issues have or haven't been addressed. Large or tricky changes may require several passes of review and changes.

Please see the contribution instructions for more information.

@rust-highfive rust-highfive added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Jun 21, 2020
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.

Seems like a reasonable feature to me, I find the description of #8127 pretty compelling! I'm curious though if we could rely on serde for deserialization here?

Additionally can you add a test where a flag is specified but we're on the stable channel? (an error should be returned). Ideally the stable/beta channel would error if anything exists in the unstable table as well, not just the known keys.

let v = parts.next();
/// Read unstable options from a hashmap.
/// Intended for consuming unstable settings from config files
pub fn from_table(&mut self, flags: &HashMap<String, String>) -> CargoResult<()> {
Copy link
Member

Choose a reason for hiding this comment

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

Instead of from_table, perhaps CliUnstable could implement Deserialize? That way we could do config.get::<CliUnstable>("unstable")?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That'd be my preference too -- I didn't add it initially for fear of making too large an unsolicited diff, but I'll see what it looks like to swap over the little bespoke deserializer table.

Copy link
Contributor Author

@bearcage bearcage Jun 24, 2020

Choose a reason for hiding this comment

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

Hm, on closer inspection I think kicking this over to serde is orthogonal -- both the parse and from_table functions are updating-in-place, rather than deserializing to construct a new instance. I think that makes a difference here because we rely on being able to differentiate between "false because that's the default" and "explicitly set to false" to give the CLI the ability to override a config file in turning off a Zflag.

@bearcage
Copy link
Contributor Author

I gather it's probably fair game since we're strictly talking about unstable flags / features here, but a hard error on Zflags in config would be a break with how mtime-on-use is configured today in that stable cargo simply ignores the setting.

I can see a reasonable argument either way --

Pro: A hard error matches the behavior of setting these same flags at the command line, and reduces the chances of a nasty surprise change-in-behavior if you switch channels and recompile.

Con: A hard error means you can't leave unstable options in config and toggle back and forth between stable and nightly, or configure CI runs on nightlies to assert that a new feature does not break a build that works on stable.

Let me know how you'd like to see it work, and I'll get it changed.

@alexcrichton
Copy link
Member

Oh right nevermind, I forgot that we ignore it on stable/beta! That seems fine for now!

@bearcage
Copy link
Contributor Author

bearcage commented Jun 26, 2020

@alexcrichton would you still like me to include a Deserialize impl in this PR, given it won't slot into the update path it's modifying (I think, anyway)?

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.

I personally think that when using .cargo/config.toml we should go through serde as much as possible. That gives us env vars, good error mesages, etc. Avoiding it will likely result in us quickly having a list of issues to fix and I think it's best to head that off.

// NB. It sucks to parse these twice, but doing it again here
// allows the CLI to override config files for both enabling
// and disabling.
self.unstable_flags.parse(unstable_flags)?;
Copy link
Member

Choose a reason for hiding this comment

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

Could this perhaps move from the beginning of the function to the end to avoid doing this twice?

Copy link
Contributor Author

@bearcage bearcage Jun 30, 2020

Choose a reason for hiding this comment

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

It can, but it'll introduce a couple funky edge cases around bits of the config deserializer that are themselves gated on Zflags. In particular I'm thinking of advanced_env (used twice in that file).

EDIT: I haven't gone through the whole call graph from higher up in the initialization function to see where else we might wind up leaning on the cli parsing happening first, but I will, once I've got your other comments addressed.

Copy link
Member

Choose a reason for hiding this comment

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

Ah either way is fine, if it needs to go near the beginning it's gotta go near the beginning, alas!

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 gave this another go and I think (unfortunately) parsing the cli flags twice is the simplest answer for allowing explicit disables at the command line for flags that are otherwise enabled in config.

src/cargo/util/config/mod.rs Outdated Show resolved Hide resolved
src/cargo/core/features.rs Outdated Show resolved Hide resolved
src/cargo/core/features.rs Outdated Show resolved Hide resolved
@bearcage
Copy link
Contributor Author

bearcage commented Jul 3, 2020

EDIT: also I rebased on master, out of force of habit.

Just a quick update — I'm still working on this, I just hit an oddity in the Deserializer that's treating missing fields as an error even with #[serde(default)] on the struct. Curiously, forcing every field to use the Option deserializer first and explicitly defaulting them works,
(a trick I shamelessly borrowed from dtolnay's answer to a totally unrelated question about serde usage elsewhere on the internet). That looks something like:

#[serde(deserialize_with="null_default")]
and

fn null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
    D: serde::Deserializer<'de>,
    T: Deserialize<'de> + Default,
{
    let option = Option::deserialize(deserializer)?;
    Ok(option.unwrap_or_default())
}

I'm digging through the Deserializer now to see what's going on there.

@bearcage
Copy link
Contributor Author

bearcage commented Jul 3, 2020

Thinking out loud a bit here:

After talking it over with a buddy (thx @packysauce!) I think there's a bit of a layering problem in the de module that's preventing serde from doing defaults or deny_unknown here — when we're deserializing a struct, ConfigMapAccess is providing an iterator over the union of two sets: the fields we're interested in for this struct (the fields member slice), and k/v pairs available in the config table we're currently considering. As I understand them, serde MapAccess implementers are expected to serve up kv pairs of the underlying map without any particular care for what set of keys the caller is interested in. With that in mind, handling env reading inline complicates the Deserializer.

As I understand it, that means there are two problems:

  1. Serde's handling for defaulting and denying-unknown will always be a noop, because we never hand back a k/v pair that the Deserialize impl didn't explicitly ask for as a field, and if we're missing a field we error out early ourselves instead of letting the derived Deserialize impl do it. This is caused by the union done in ConfigMapAccess, but fixing it would complicate point 2.
  2. Environment variables don't fit super neatly in that model — if we modified ConfigMapAccess to return all k/v pairs from the config object under consideration plus all env vars which match the prefix under consideration, env vars which match member structs would show up as unexpected field keys (e.g. CARGO_UNSTABLE_SOMETABLE_SUBKEY would show up as an unknown key in CARGO_UNSTABLE even if CARGO_INSTABLE_SOMETABLE would have been valid).

While I could work around this for CliUnstable by annotating every non-Option field with a deserialize_with, I think the "Right" solution here is probably a larger refactor to split the env reading out of the Deserializer impl such that Config::get would try reading each source in decreasing priority order independently, with its own purpose-specific deserializer, rather than using a combined deserializer.

Alternatively, I can make the change suggested in 1 above — tweak the map accessor to report all fields in the joined set of env/config — but I suspect it'll lead to a bunch of funky corner case bugs around required/unknown fields and env vars.

EDIT: added alternative.

@alexcrichton
Copy link
Member

I'd definitely believe that the config bits with serde aren't exactly the best they could be, I've never really fully grok'd how to write a serde deserializer. That being said I'm happy with whatever works here, I think switching all the fields to Option would also be fine.

bearcage and others added 10 commits July 7, 2020 23:38
This makes it easier to populate unstable
options from configuration files. Might also
help make unstable option tests easier to write.
Obviously this only works with nightlies and all
that, but if you're e.g. testing resolver v2, want
to always have Ztiming on in your workspace, or
something like that, this makes it easier to do.
This makes it a little easier to match whatever
the natural formatting is for where you're setting
unstable options -- CLI or config file.
Co-authored-by: Eric Huss <eric@huss.org>
These masquerade like construction functions, but they're
in-place mutators since they're meant to be used one after
another in a precedence chain.
Per comments on the PR, a couple changes need to be backed out.
Tests are currently failing, looks like there's something in the
Deserializer impl that's forcing field-missing errors even when
the serde `default` annotation is applied.
This is a workaround in case y'all would like to land this change
before I'm able to finish up a Deserializer refactor.
The current behavior, with the default-false workaround in place,
is not able to identify extra members of the `unstable` table, so
it can't error on unexpected members.

I'll add this test back in with a Deserializer refactor to clean up
the extra logic added to CliUnstable.
@bearcage
Copy link
Contributor Author

bearcage commented Jul 8, 2020

I've never totally understood the serde model myself, either — I'm learning my way through it on this PR. Definitely tell me if I've got it wrong!

Changes in this round:

  • Rebased on master
  • Added a default-to-false deserialize_with workaround to CliUnstable
  • Dropped a (probably helpful) test that's broken by that change.

I'm gonna keep hacking on top of this to flip the Deserializer impl around to allow serde's default/deny_unknown_fields to do the heavy lifting, but I didn't want y'all to have to wait on me finishing that up to land this and close the linked issue, if you'd like to.

In the meantime please don't hesitate to ask if you'd like me to make some more changes to this first.

@alexcrichton
Copy link
Member

Looks good to me! I'd be fine landing this as-is. FWIW I don't think we want to add deny_unknown_fields because that'll make it painful to use this from stable Cargo when nightly adds a new feature

This patch changes how ConfigMapAccess iterates k/v pairs when
deserializing structs.

Previously we produced keys for exactly the set of fields needed
for a struct, and errored out in the deserializer if we can't find
anything for that field.

This patch makes us produces keys from the union of two sets:

1. All fields that are both needed for the struct and can be found
   in the environment.
2. All fields in the config table.

This change allows serde's codegen to handle both missing and
unknown fields via the usual derive annotations (default or
deny_unknown_fields respectively)
Serde's errors for missing fields are a lil' different than
the ones from our Deserializer.
@bearcage
Copy link
Contributor Author

bearcage commented Jul 9, 2020

Just kidding, this was way simpler than I was making it out to be — the new_map ctor for ConfigMapAccess does the right thing already, it's just the struct behavior is trying to be a little too clever in how it's prefiltering fields.

I patched new_struct and dropped the workaround. If this still looks good I think it's ready to rock! Otherwise I'm happy to revert this Deserializer change and ship the deserialize_with version.

@bearcage
Copy link
Contributor Author

bearcage commented Jul 9, 2020

Happy to help - thanks for the reviews!

@bors
Copy link
Contributor

bors commented Jul 9, 2020

💔 Test failed - checks-azure

@bors bors added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. labels Jul 9, 2020
@bearcage
Copy link
Contributor Author

bearcage commented Jul 9, 2020

Test failure, bummer.

I couldn't find a way to pull the proptest codegen'd case off the builder host, so I've left my workstation running a copy of the failing proptest (prop_limited_independence_of_irrelevant_alternatives in resolver) per core until I get a reproducer. Fingers crossed it doesn't take toooooo long.

EDIT: 128000 iterations down, no repro yet. Kicking off 5120000 more.

@ehuss
Copy link
Contributor

ehuss commented Jul 9, 2020

@bors retry
#6258 (comment)

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jul 9, 2020
@bors
Copy link
Contributor

bors commented Jul 9, 2020

⌛ Testing commit d035daa with merge f35ebaf...

@bearcage
Copy link
Contributor Author

bearcage commented Jul 9, 2020

thx ehuss!

I just got a repro case locally, but skimming the linked thread it looks like y'all already have a handle on where the edge case proptest is turning up is so I'll let it lie.

@bors
Copy link
Contributor

bors commented Jul 10, 2020

☀️ Test successful - checks-azure
Approved by: alexcrichton
Pushing f35ebaf to master...

@bors bors merged commit f35ebaf into rust-lang:master Jul 10, 2020
@Eh2406
Copy link
Contributor

Eh2406 commented Jul 10, 2020

Technically there have to be an infinite supply of edge cases. The problem is NP-Complete so there have to be exponencial cases. So far (at least recently) the proptest are finding particularly un-realistic examples and my investigations have not found ways to make them fast that are worth the additional code complexity. But each one is a new adventure, that can be completely unrelated.

@bearcage bearcage deleted the unstable_flags_in_config branch July 12, 2020 22:55
Manishearth added a commit to Manishearth/rust that referenced this pull request Jul 14, 2020
Update cargo

4 commits in 4f74d9b2a771c58b7ef4906b2668afd075bc8081..43cf77395cad5b79887b20b7cf19d418bbd703a9
2020-07-08 17:13:00 +0000 to 2020-07-13 17:35:42 +0000
- fix: add space to comments (rust-lang/cargo#8476)
- Allow configuring unstable flags via config file (rust-lang/cargo#8393)
- Add support for rustc's `-Z terminal-width`. (rust-lang/cargo#8427)
- Avoid colliding with older Cargo fingerprint changes (rust-lang/cargo#8473)
@phil-opp
Copy link
Contributor

I just tried this feature with the latest nightly and it seems to work great overall. Thanks a lot @bearcage for implementing this!

One minor thing I noticed was that the -Zbuild=core command line argument behaves slightly different than setting unstable.build-std = ["core"] in a .cargo/config file. The former seems to automatically derive that compiler_builtins is also needed when building core, while the latter seems not. With the config option, a "can't find crate for compiler_builtins" error occurs unless I add compiler_builtins to the unstable.build-std list. This isn't a problem for me, but I wanted to report it in case this is unintended.

alexcrichton added a commit to alexcrichton/cargo that referenced this pull request Jul 16, 2020
This fixes an issue where the deserializer for `-Zbuild-std` was a bit
fancier than the `unstable.build-std` directive.

cc rust-lang#8393
@alexcrichton
Copy link
Member

@phil-opp looks like that's a bug

@phil-opp
Copy link
Contributor

Thanks a lot for the quick investigation and fix!

bors added a commit that referenced this pull request Jul 16, 2020
…h2406

Ensure `unstable.build-std` works like `-Zbuild-std`

This fixes an issue where the deserializer for `-Zbuild-std` was a bit
fancier than the `unstable.build-std` directive.

cc #8393
@simnalamburt
Copy link

Thanks to this PR, now we can use the rust-analyzer with the unstable cargo option! Yay

simnalamburt added a commit to simnalamburt/flashbench that referenced this pull request Jul 28, 2020
@ehuss ehuss added this to the 1.47.0 milestone Feb 6, 2022
ronnychevalier added a commit to ronnychevalier/cargo-multivers that referenced this pull request Dec 21, 2023
…er build

If a user adds `-Zbuild-std` to the command line to rebuild the std it is only propagated to the build of each version, but not the runner.

These flags, however, can also be configured with [environment variables](rust-lang/cargo#8393).
So if a user uses the `CARGO_UNSTABLE_BUILD_STD` environment variable to rebuild the std of the crate, it is propagated to the build of the runner. Since the runner adds `panic=abort`, if the user does not add `CARGO_UNSTABLE_BUILD_STD=std,panic_abort`, there are duplicate lang item errors.
These errors might be confusing since the build of the runner is not necessarily known, and its profile even less.

See #7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add a build.cargoflags configuration key
8 participants