-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Target Version #1147
Target Version #1147
Conversation
|
||
Currently every rustc version implements only its own version, having multiple versions is possible using something like multirust, though this does not work within a build. Also currently rustc versions do not guarantee interoperability. This RFC aims to change this situation. | ||
|
||
First, crates should state their target version using a `#![version = "1.0.0"]` attribute. Cargo should insert the current rust version by default on `cargo new` and *warn* if no version is defined on all other commands. It may optionally *note* that the specified target version is outdated on `cargo package`. [crates.io](https://crates.io) may deny packages that do not declare a version to give the target version requirement more weight to library authors. Cargo should also be able to hold back a new library version if its declared target version is newer than the rust version installed on the system. In those cases, cargo should emit a warning urging the user to upgrade their rust installation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cargo should insert the current Rust version
How should cargo do this? How does it know how to access different Rust versions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have extended the paragraph to clarify. Basically either std defines a symbol with the current version, or rustc -V
could be used after some post-processing.
You might want to word-wrap this, it's hard to comment on the exact part you want to discuss when each paragraph is a line :) |
Thanks for the input, @steveklabnik ! |
Just adding the thoughts I had from the thread on discuss: I haven't read all of the comments so far, but I'm curious if using a different mechanism for avoiding warnings would be useful. The idea would be to have a targeted rust version specified for a program and have each deprecation list a rust version that it applies to and the warning would only be shown when the targeted rust version is less than or equal to the version that the deprecation applies to. That would also allow the reverse, which is warning when using features that are newer than the desired version, though I understand that's an entirely different consideration. |
@ahmedcharles this is already done using However, retroactively removing the deprecation altogether is probably the saner alternative here. It also wouldn't require new syntax. Thus, unless I get some votes in favor, I'm not gonna add it. |
Perhaps there should be a difference between deprecation and (planned) feature removal. You want to warn new users of using deprecated functionality (because of some reason), but supporting the old function is not worth the cost of breaking compatibility. But at some point some function might have a bigger issue than just being "wrong", "unclear" or "inconvenient". It might have security, performance or be in the way for necessary API changes. In that case a deprecation should also feature some kind of "removal" version. Usage of a deprecated function in a crate that has a target version before that deprecation and no removal clause: No warning, all is fine. |
Deprecation actually is planned future{{*}} feature removal (for some version) to allow evolving the API. Note that this has nothing to do with removing features which are deemed inherently insecure – the RFC explicitly states that these should be removed completely (even retroactively), if it is deemed that the insecurity caused by a feature is worse than the breakage resulting from removing it. Otherwise normal deprecation (perhaps with no forward warning) will be the chosen path.
I also don't think that performance problems are not a valid reason for removing an API as long as deprecating it is a viable choice. Being "in the way" should be carefully evaluated – in most cases, deprecation should be sufficient to evolve the API. Note that this RFC avoids talking about language changes, which need to be handled within rustc anyway.
We have to assume that the user has no control over their build's dependencies. We can however assume that the dependencies' authors mostly follow the rules and specify their target version. Thus any error that will result from deprecation can easily be traced back to the dependency – whose author have by definition seen the same error. As {{*}} for some value of future – it may well be never if we never get around to it. |
No it's an indication that an API should not be used any longer, like you say:
As long as it's not actually going to get removed, it's no problem that something is using it, apart from notifying (direct) users of it's "unwanted" usage. As long as an item is just "bad practice" and supporting it is no problem (or removal would be prohibitively problematic for the foreseeable future) usage of it would be acceptable. Usage these deprecated items by dependencies should hardly concern the developer. Like you say:
A developer shouldn't have to care if one of it's dependencies uses deprecated functionality that will be supported for the forseeable future. But if removal has been scheduled, there he should at least know that one of his depencies will be broken in the (near) future and that they should probably request upstream attention or prepare to move away from a possibly unmaintained dependency. The alternative would be to not give any warning at all and all of a sudden, after a rust upgrade, his dependencies would stop building. At that point he can't upgrade dependency A because it needs a newer rust version and he can't upgrade to a new rust version for that would break dependency B.
This was more intended for compiler intrinsics where the compile time optimizations would be impossible as long as a certain feature was still being used. Not performance optimizations because of an old inefficient API in some crate, though developers of said crate might want something for such a use-case too. It's about giving an option to explicitly sunset a feature in a responsible way (not making assumptions that all (indirect) users of a feature have CI enviroments that track beta and nightly rust releases for all their stuff). |
That's why I wrote the RFC – we should not (retroactively) remove features unless absolutely necessary, or supporting it becomes too costly. Note that this even rules out future performance benefits, if those break backwards compatibility irreparably (however, it may well be that the performance benefit can be introduced on a per-crate basis for all crates that target a newer version). Of course, sunsetting a feature once it is determined that it has no viable use anymore is still an option – though not one explored within this RFC.
No. The alternative is that newer rust versions will continue to compile code that targets older rust versions. Though this RFC has steered clear of handling the language itself, the scheme it proposes could also be used to evolve the language itself in the face of backwards-breaking changes, as long as the old target versions can continue to compile using the old semantics and post-change code can interoperate with pre-change code. Also note the section on cargo being able to hold back dependency version, so library writers can update their target versions to introduce new features or get new performance benefits. |
Is there any interest in extending this RFC to language/compiler changes? |
Oi, there seems to be a lot of overlap among RFCs and internals discussions when it comes to this issue. For the record, in this comment on RFC PR 1105, I discussed how the idea of using a version number attribute to provide full backwards compatibility could interact with language features such as trait selection, glob imports, and some trickier cases. I personally support a similar policy for language changes, which is also covered by RFC PR 1122. |
I feel that this RFC may still be a little premature from the discussion on the internals thread as I'm not quite sure that there's been a consensus reached. I think the discussion has gotten somewhat sidetrack from the initial intention of talking about our existing thoughts on a deprecation strategy and how it might play out, unfortunately. There have been a number times that a language version marker has been discussed (either via an attribute, compiler flag, Cargo.toml field, etc), but there's always been one blocker or another, and I'm not sure that we've quite reached the point where we want to definitively say that it needs to be added. Specifically on the topic of this RFC, though, I would personally like to see the motivation section spelled out a little more. The standard library currently has a deprecation strategy for APIs (via In terms of the "drawbacks" section, I feel it doesn't quite do a "require attribute in all Rust code" topic justice. I feel like it's pretty weighty decision to have Cargo warn on all invocations if a version attribute isn't specified. This seems somewhat hostile to newcomers and isn't always a problem that needs to be dealt with. Another drawback I believe needs to be mentioned is that the cross-platform story of deprecation warnings and feature detection is not that great. The fundamental drawback is that the compiler and/or Cargo can only know about one profile the code is being built with, and that profile may be slightly different (e.g. different sets of code) on different platforms). This can lead code to inadvertently believe it works on Rust 1.0 when in fact on Windows it requires Rust 1.1, for example. |
@alexcrichton Thank you for the detailed input and clarification. I'm certainly open to extend the RFC, though I stand by my stance on the internals discussion that we need some versioning (akin to PR #1122) for the standard APIs, too, in order to reduce the API surface and improve discoverability while keeping things backwards compatible. I can understand that you may feel this RFC be premature, however, considering that I have determined that a per-crate attribute (I have to flesh this out a bit more, but I also have to re-check, but the current version of the RFC would keep warnings at a manageable level, unless a developer updates their target versions, in which case warnings regarding deprecation is to be expected – perhaps such warnings could be grouped by error, in order to reduce compiler output). I have not yet included thoughts about different operating system profiles; I agree that this is a shortcoming of the RFC in its current form and I'm willing to address this. |
Creating a Cargo.toml file already involves specifying several metadata fields, and most .rs files at least contain a long list of boilerplate use statements, potentially in addition to metadata attributes and the like; I don't think adding one more attribute in one location or the other, required only for full-fledged Cargo crates (as opposed to throwaway standalone .rs files) is a big deal. And it is basically the only way for the compiler to provide full backwards compatibility, as opposed to having at least some probability of breakage, which is much more burdensome than the one attribute. If the compiler warns about items a crate imports from the "future" (compared to its declared Rust version), then I don't think there is any new OS hazard; it just becomes a special case of a crate that doesn't compile on X OS. (This also provides an easier way for a crate to ensure compatibility with older versions than just keeping around an old compiler.) |
This is one of the fundamental aspects here to consider, though, I believe. First, there's the tradeoff between named features and version numbers. Taking the route of version numbers can lead to problems. For example if I declare I'm targeting 1.1, but I'm compiling with 1.2, then I still want to get deprecation warnings for functions which have security issues, for example (in favor of a crates.io crate probably). I didn't actually want to turn off all future deprecations, just those that I'm actually legitimately using. If we instead take the route of named features in some form then you have the problem of a growing set of features over time, plus sometimes the header of a crate can be a little like spaghetti. The benefit here, however, is more granular control while in theory being compatible with other implementations (perhaps). The RFC doesn't make much mention of this alternative, and there's definitely quite a design space here to explore I believe.
I think this is one of the points where I think a clarified motivation will help a bit. For example if I want to keep warnings manageable it's unclear why we should go with an entire versioning scheme for the libraries when |
Thanks again! I'm fleshing out the motivation, but will take a few days to make it good. |
I have tried to split the Motivations section in two parts, where the first just lists the constraints in short form and the second expands on that list to add explanation. I still don't think I have fully captured all of @alexcrichton 's concerns, but it's a start. |
|
||
We want to: | ||
|
||
1. evolve the `std` API, including making items unavailable with new |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I definitely agree that we want to evolve the APIs that libstd provides, but I'm not quite in agreement that we want to remove deprecated APIs. I think that the overhead of keeping around unstable APIs is unlikely to become too burdensome, but this is of course tough to predict. I also think that it's possible to leave these APIs around without confusing new users through various mitigation tactics like compiler error messages, documentation, etc.
|
||
1. Add a `--target=`*<version string> command line argument to rustc. | ||
This will be used for deprecation checking and for selecting code paths | ||
in the compiler. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rustc already has a --target
flag, used for a completely separate purpose (selecting target triples).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ouch. @bstrie: Care for some bikeshedding?
--std
(from C, but people could confuse it w/ nostd)--semver
(too abbreviated)--source
(from Java)--lang-version
--target-version
What do you prefer?
I have an open discussion on internals for bikeshedding the names for the cargo attribute and the rustc option. Apart from that, it feels pretty much done. |
@llogiq I feel like I'm still somewhat confused by the motivation of this RFC. The three sections you have under motivation talk about Language/std evolution, user experience, and security considerations. When talking about evolution, I don't quite follow how this motivates The motivation spelled out in the user experience section also talks about wanting to outright remove an API, but this isn't something I think we need to start worrying about at this time. There are many strategies for dealing with deprecated surface area of the standard library, and I don't think that we feel any need to start removing any of it. Finally, in terms of "insecure APIs", this is quite a bit of machinery to be added just to get a better error message on using an insecure API, so I'm not sure it's entirely motivated to do so. Overall my main hesitation on this RFC is that I think I still don't quite understand the motivation for all the machinery being added. My main points are:
|
Why do you say it's impossible, and what is the issue with trait implementations? I'd say it should be easy, given some initial work, in most cases, where the compiler would just have to hide public items and trait implementations from "the future"; harder to keep compatibility in the face of minor breaking changes, but doable, and those should be avoided whenever possible anyway. |
Right now the compiler has no ability to look back in the linting phase to determine whether a trait implementation was used or not. Pruning them out earlier from the AST may be a bit of an invasive and perhaps surprising change. |
Doing it in the linting phase is definitely too late: for compatibility, the hidden names should not have the potential to cause conflicts in method resolution. Invasive... well, you know better than I, of course. I shouldn't have said "easy" - what I meant is that after the initial work is done to allow hiding future names and maybe deal with some edge cases, supporting old compatibility versions doesn't seem likely to require an unreasonable amount of continuing effort. But I think I misunderstood the nature of your "reliability" concern anyway. |
@alexcrichton: Like @comex, I think the availability logic needs to be baked in the name resolution. I agree that this would be pretty invasive, but it certainly won't be surprising, given that the crate requested exactly this behavior.
I'd like to explore the reasoning for this. Could we at least offer a best-effort approximation of version X? How far from version X would it be? Apart from that re-evaluating the approach it seems something is missing: Because we don't currently have a
Ok then. I'm actually fine with removing this part of the RFC. |
Yeah we could definitely get close to a different version, but unless we get all the way I don't think it's worth having a flag making it look like we're doing so. Otherwise there will be a never-ending stream of bug reports about how the 1.4 compiler doesn't behave exactly like the 1.2 compiler. And example of how I think this is hard to do would be this PR which is just a minor bugfix to resolve. In general little bug fixes here and there will make it impossible for later compilers to behave perfectly like a previous compiler. |
Regarding your PR example, I believe this is strictly an extension of current behavior, i.e. if we backported it, the only things that would break are tests actively checking for the current (wrong or insufficient?) behavior. Your "never-ending stream of bug reports" would be empty in this particular case. Anyhow, I have added this conundrum to open questions (will push later today).
I think there is a finer distinction to make – there are changes, e.g. bugfixes that are strictly backwards compatible. Perhaps we could even define LTS versions that get those fixes in a later release (e.g. 1.0.2 etc.). Frankly I'm not sure how (or even if) to proceed. On one hand I believe that backwards compatibility will be vital in avoiding fragmentation in the Rust ecosystem and furthermore I believe that using a target version scheme as defined in this PR is the only way to reliably do this without imposing a huge cost on library implementers. On the other hand I understand that implementing it correctly will have a big cost on each implemented breaking change. We will also need to define a policy to distinguish a) actual bugs that no one should rely on and b) accepted behavior that should be preserved for the given target version. |
I totally agree with this, and I certainly want crates to be able to work on any number of Rust versions they want to work on!
I would also consider "test against the desired Rust versions" to be an acceptable threshold for reliably maintaining backwards compatibility. For example Travis makes it super easy to test against multiple Rust versions, and most crates do.
The key idea here for me is that maintaining a target scheme where a compiler can masquerade as a previous version doesn't just affect breaking changes. Any change to the compiler needs to be "backported" and/or only happy in a mode where the compiler knows it's not masquerading as an old version. Unfortunately I feel like the burden in doing this is too high to pay off. |
The way I see this proposal, the proposed arguments and attributes currently only can be implemented to make sure rustc is exactly the same version as specified by the attribute/argument; otherwise compilation unconditionally fails (which might be a nice convenience in build scripts?). If you want to use Rust, as it was implemented by an exact rustc version, why not use that exact version of rustc to compile your programs instead? To make something closer to clang’s/gcc’s --std=c* happen, we need at least:
We have neither and both are pretty big undertakings. |
You can do that - indeed, large companies that vendor everything will do that no matter what - but that prevents you from using any features or bugfixes from newer versions of Rust, or libraries that depend on them, in your entire program. It just defers the problem.
It would definitely be nice if rustc could (mostly) ensure that code it accepts with target version X is accepted by rustc version X, but it's not necessary to reap much of the benefit here - for mitigating upgrade pain, the reverse is sufficient, so backwards compatible changes need not pay attention to the target version. |
I have one problem with this: The original maintainer of the library may no longer be around. But perhaps @nagisa is right and the best way to implement it (for those who may need it) is to use a MultiRust that can compile different crates, then link them together. This would make the whole rustc machinery moot (because we'd simply have different rustcs), and remove much of the cost arising from the change. However, it would very probably fail at link time, unless we require that implementations of any std items stay the same between versions, which we expressly do not want. |
The problem I'd expect with this is that if 1.4 cannot perfectly emulate 1.2 then we're still in a situation where there may be upgrade pain because large codebases are likely to exploit the corner cases the compiler isn't emulating. Unless we get to 100% of being able to emulate a previous version I'm not sure that it's worth it to do the masquerade because it's unlikely to solve the problem for large codebases in the extreme.
Yeah this is a good point, but unfortunately if the compiler doesn't have perfect emulation then if a tweak is needed to make a library compile it may fall in the category of "what the compiler doesn't emulate" so the flag proposed by this RFC wouldn't help in that case. Overall, I still feel like the best strategy forward is not breaking things :) |
Alas, we have already strayed from that path. |
Since this PR isn't going anywhere before 2.0 I'm closing this. I will however start a new pre-RFC to allow deprecate annotations outside of lang crates; this would be really helpful for library writers. |
The rendered link is broken. Here's a working one: Rendered |
This is basically a writeup of the discussion on internals. I hope I have faithfully captured the spirit of our discussion.
Edit: The above statement is no longer true. I have rewritten the RFC extensively following input from various sources.
Rendered