-
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
Add min_$TARGET_API_version cfg predicates #3036
Conversation
This RFC could potentially help with preventing accidentally introducing platform API dependencies that break the |
I am currently looking for feedback from folks with perspectives on other OS versioning schemes. This seems like a good solution for Windows and Linux (most likely macOS), but what about others like freeBSD? |
This is the first use of Am I reading the Versioning schema section correctly that the ordering used to resolved these comparison to boolean is not What’s the default behavior for targets that don’t specify an ordering? What’s the stability story for this ordering evolving over time? For example if Rust 1.73.0 introduces a support for a Should target-specific parsing be fallible, with unknown/invalid values used in |
* [Discussion as it relates to dropping XP Support](https://rust-lang.zulipchat.com/#narrow/stream/233931-t-compiler.2Fmajor-changes/topic/Drop.20official.20support.20for.20Windows.20XP.20compiler-team.23378) | ||
* [More XP Support Discussion](https://rust-lang.zulipchat.com/#narrow/stream/122651-general/topic/On.20WinXP.20support) | ||
|
||
# Unresolved questions |
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.
Another question, what does versioning on linux based OSs mean? Should we switch on the kernel version, libc version, distro version or something else.
Eg on android, the relevent version is the API version, not the kernel version. Linux distros are unique (afaik) for being the only os where the kernel and other parts of the API are versioned seperatly.
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.
Yeah it almost sounds like there should be different version keys or so that a target can have.
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 must admit I was only thinking about the libc version. What are some examples of changes to the kernel that would not bubble through to changes in libc which the user would like to conditionally compile code on?
What is meant by target_api_version
is the platform's API that it exposes for end applications.
I was summoned by @skade to provide The BSD Perspective ™️ . As far as I can tell in the BSD world this would largely come down to the API changes that happen at a @yoshuawuyts' suggested iteration feels like it would be more ergonomic for my use, but after I think about it more, I think it would be just as simple to do:
To @aDotInTheVoid's feedback, I'm interpreting the version to imply For the express purposes of this API, I don't think the OS version means anything useful, but I'm sure if you gave that to Rust developers, we'd find some use for it 😆 From a programmatic standpoint, Personally, I think the |
Yes I believe it is
Yes you are. The schema should be controlled through means other than naive string comparison.
The RFC states: "Many targets don't support a
Are you asking if a user has previously used the value "Windows11" which did not exist but now does. This would have previously evaluated to false (presumably with a warning for an unknown value) but now would evaluate to true. This could cause breakage. I wonder if erroring on unknown version strings is the right way to go.
I think this definition is a bit restrictive as on Windows you would also care about things outside of the CRT like which win32 APIs are available. This does make me wonder if we're not going to find a stable cross platform definition for what |
Cross Platform ChallengesAt the core of this question is the fact that Rust binaries typically rely on external symbols to function (unless built with This RFC was motivated by a Windows use case, which has a relatively clear line for when API symbols are introduced. Windows is a monolithic thing with a fairly strict backwards compatibility policy. This means it is relatively easy to know whether an external symbol is guaranteed to be defined by the Windows version number (i.e., the build number). Other systems might not be as clear for several reasons:
In other words: is it possible to define the set of external, platform-provided symbols a Rust binary relies on with a single number? |
This point specifically sounds like it could be addressed by something akin to a |
What if, instead of a single #[cfg(all(linux, min_kernel_version = "4.0", any(
all(target_env = "gnu", min_glibc_version = "2.28"),
all(target_env = "musl", min_musl_version = "1.1")
)))] Using different names making it an error to use an undefined value would avoid accidentally comparing "1.1" (which is intended as a musl version number) to a glibc version number. |
While this ultimately may be the only way to go, the reason I am trying to avoid this is the proliferation of target specific |
It would be very unfortunate for this RFC to not solve this for macOS and iOS, which have a number of very useful APIs that only exist in certain versions. I know @nvzqz has hit this when working on https://github.com/nvzqz/fruity |
I've pushed a new version of the RFC that more closely tracks to what @SimonSapin was suggesting. This gets around all challenges of coming up with a cross platform meaning for The main challenge with this proposal is the large amount of |
From my understanding, it's fairly common to invoke syscalls directly in Linux and bypass libc. So it's not only a concern of symbol availability, but also whether the author of a program/library can assume that a syscall will work as expected. A great example of a similar split is being able to cfg against the Darwin kernel version vs the macOS/iOS/*OS version. There's certain APIs in libsystem that are cleaner to conditionally use if checking kernel version rather than OS version. |
@nvzqz Thanks! That's really great context for this. I think everything you've said would be addressed by my latest proposal (assuming we add another |
Yeah, seconding this. Several things like futex, perf_event, ... don't have a libc interface, and require you use the syscall directly. Personally, I think there's more of a use case for checking kernel version than libc version (you could always work around a limited libc if you know the kernel version). Also, what if you wanted to write a hypothetical rust-libc — I could imagine cfg-ing based on a target kernel version would be useful here.
Yeah, so, I agree that it's cleaner to check this way — sometimes you know was added in Darwin 19 and it's tedious to look up which macOS/iOS/tvOS/watchOS/etc version it applies to — especially since Big Sur being 11.x complicates the "just add 4" pattern you could use on macOS) but I don't think this is actually a good idea. While you can query it at runtime (with the kern.version sysctl), there's (AFAIK) no direct runtime API for it, it's not exposed at compile time to C/C++/ObjC/Swift, and stuff like the That is to say: there's a well-defined notion of what "this binary (or code) requires macOS 10.12", but no direct equivalent for Darwin kernel version, and so the compiler would end up needing to map between these, which sounds like a problem (very likely impossible to do without hardcoding the mapping, which would have lots of problems) Additionally, the fact that the way the version checks work in OS-provided header files on these platforms bothers to list out every version for each platform and not going based a single darwin version feels like a suggestion that we should do the same, if not an indication that either there's a reason for it. |
|
||
If an end user sets their `min_windows_build_version` to an incompatible version then the user receives an error. For instance, in the example above where the user is setting their `min_windows_build_version` to Windows Vista, they will receive an error when linking with the standard library which imposes Windows 7 as its `min_windows_build_version` by default for the `x86_64-pc-windows-msvc` target. | ||
|
||
If a library does not explicitly set its `min_windows_build_version`, it will automatically be set to the largest `min_windows_build_version` of all of its transitive dependencies. |
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.
How would a library set its minimum version? I don't see anything to that effect in this section nor in the reference-level explanation.
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.
This is actually an unanswered question - thanks for bringing it up. The RFC mentions storing this information in the crate metadata but gives no mechanism for doing so. I'm not aware of any existing mechanism that we could use for such a thing. The closest that comes to mind is #![no_std]
, but having a #![min_windows_build_version]
seems like overkill.
If we switch to a dedicated syntax like #[min_version(...)]
we could conceivably just store the smallest of those used. Perhaps this is another point in favor of the dedicated syntax.
Originally the proposal was to not have libraries store any information about their minimum supported target. They would simply conditionally compile based on what was passed as the cfg
flag through rustc. This was changed at some point, but I'm not sure that was a good idea.
|
||
If a library does not explicitly set its `min_windows_build_version`, it will automatically be set to the largest `min_windows_build_version` of all of its transitive dependencies. | ||
|
||
When user's code checks against a `min_windows_build_version`, a `>=` check is performed. This means that if the `min_windows_build_version` is "10.0.10240" and the user specifies `#[cfg(min_windows_build_version >= "6.2.9200"]` then the `cfg` predicate will be true and the code it marks will be compiled, since "10.0.10240" is a greater version than "6.2.9200". |
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.
The min_
prefix in the proposed cfg labels and the >=
operator seem duplicate each other in the meaning. Since >=
is a new syntax (as is acknowledged later) and would most likely need its own rfc, I think we want to avoid >=
in this rfc.
(besides, if we push for >=
, what would then the behaviour of #[cfg(min_windows_build_version="6.2.9200")]
be?)
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.
The semantics of the min_*_version
is that marked code should compile when the set minimum supported build version is at least equal to that version. i.e., if code is marked with #[cfg(min_windows_build_version >= "windows7")]
, it should compile if the min_windows_build_version
is set to "windows10". It has been argued that a plain "=" implies that the code would only compile when the min_windows_build_version
matches the version provided exactly.
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.
The Zulip thread linked up top goes back and forth on >=
a couple of times. It might be worth revisiting some of the arguments there to help move the discussion forward and save circling around the same points.
|
||
When user's code checks against a `min_windows_build_version`, a `>=` check is performed. This means that if the `min_windows_build_version` is "10.0.10240" and the user specifies `#[cfg(min_windows_build_version >= "6.2.9200"]` then the `cfg` predicate will be true and the code it marks will be compiled, since "10.0.10240" is a greater version than "6.2.9200". | ||
|
||
For targets where `min_window_build_version` does not make sense (i.e., non-Windows targets), the `cfg` predicate will return false and emit a warning saying that the particular `cfg` predicate is not supported on that target. Therefore, it's important to pair `min_windows_build_version` with a `cfg(windows)` using the existing mechanisms for combining `cfg` predicates. |
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.
What would be the expected behaviour in case of invocation such as
rustc --target=x86_64-unknown-linux-gnu --cfg 'min_windows_build_version="6.0.6000"'
(both when no cfg
s with min_windows_build_version
and when there are some present in the crate code)
|
||
[drawbacks]: #drawbacks | ||
|
||
There are no known large drawbacks to this proposal. Some small drawbacks include: |
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.
One drawback that comes to mind is that it is possible for users to specify arbitrary foo="x"
cfgs today, and so this change (and any future addition for other platforms) as proposed would not be backwards compatible in a strict interpretation of compatibility.
|
||
As previously stated, a mechanism which tries to bridge cross-platform differences under one `min_target_api_version` predicate [was suggested](https://github.com/rust-lang/rfcs/blob/b0f94000a3ddbd159013e100e48cd887ba2a0b54/text/0000-min-target-api-version.md) but was rejected due to different platforms having divergent needs. | ||
|
||
To combat the issue of ever increasing number of platform API `cfg` predicates, a new syntax could be introduce like `#[cfg(min_version("windows_build", "6.0.6000")]` |
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 would strongly prefer this. Firstly this avoids the can of worms related to backwards compatibility.
As a secondhand benefit something like this could also pave the road to different matching predicates within the version string, much like what we have in cargo. So for instance, crates use semver and so a variety of operators that make sense for semver can be used (rather than just >=
):
library = "^1.0"
other_library = ">= 1.0 && <3.34"
If, out there, there's a target that utilizes semantic versioning for itself or its components (I think freebsd
is a possible close match here), then utilizing the familiar semver matching algorithms would also make sense for it.
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.
Specifying a generic mechanism for targets to provide their own versioning schemes, rather than concrete cfg
s also sounds like a more productive direction to me. Then the fate of this RFC wouldn't need to linger on us being comfortable with all sorts of versioning schemes proposed for the various targets. Once an RFC with a generic mechanism is merged, we could spend our time on figuring out out the high value versioning cfgs (windows, macos) sooner via other channels (PR FCPs, MCPs etc) even before we know what to do about linux, android or bsds.
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.
Amen. Better to have a single syntax for all operating systems, rather than defining a separate variable name for each.
|
||
If a library does not explicitly set its `min_windows_build_version`, it will automatically be set to the largest `min_windows_build_version` of all of its transitive dependencies. | ||
|
||
When user's code checks against a `min_windows_build_version`, a `>=` check is performed. This means that if the `min_windows_build_version` is "10.0.10240" and the user specifies `#[cfg(min_windows_build_version >= "6.2.9200"]` then the `cfg` predicate will be true and the code it marks will be compiled, since "10.0.10240" is a greater version than "6.2.9200". |
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.
Nit: #[cfg(min_windows_build_version >= "6.2.9200"]
is missing a closing paren.
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 like this. We definitely need some feature like this. If I understand correctly, it would work like this:
- Applications set
min_freebsd_build_version
in theirbuild.rs
files, either by runtime detection or by fiat. - Libraries never set this variable, except for their tests. The compiler forwards it to them based on whatever the application set, and the libraries conditionally compile accordingly.
- At the bottom of the stack, libc works just like any other library. It conditionally compiles accordingly.
|
||
### Future compatibility | ||
|
||
A given version of the Rust compiler will only know about certain versions of a given API version string. If a compiler does not recognize the given version string, it will issue a warning and the `cfg` predicate will return false. This means as new versions of platforms are introduced, new support must be added to the compiler. As code is written to take advantage of these new version strings, older compilers will not be able to conditionally compile based on them. |
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.
Why does the compiler need to know about each value of the API version string? It seems like the compiler should just forward the version string to each crate as it builds them.
|
||
As previously stated, a mechanism which tries to bridge cross-platform differences under one `min_target_api_version` predicate [was suggested](https://github.com/rust-lang/rfcs/blob/b0f94000a3ddbd159013e100e48cd887ba2a0b54/text/0000-min-target-api-version.md) but was rejected due to different platforms having divergent needs. | ||
|
||
To combat the issue of ever increasing number of platform API `cfg` predicates, a new syntax could be introduce like `#[cfg(min_version("windows_build", "6.0.6000")]` |
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.
Amen. Better to have a single syntax for all operating systems, rather than defining a separate variable name for each.
This wouldn't work, since the build.rs for the application will run after the build.rs for libc under most circumstances, since the application depends on libc. At the very least, if thats to change, this would need to be spelled out in the RFC (which I don't think it is? But maybe it's just been too long since I've looked) |
Oh, I guess I didn't understand correctly. The RFC mentions setting the min_freebsd_build_version in ~/.cargo/config. But IMHO that's not good enough. I think it needs to be controllable by the application being build. Perhaps by a feature flag in Cargo.toml? |
@rylev Could we extend this RFC to be more explicit, and differentiate OS version from libc version? In many BSD derived OSes, this doesn't matter (as the libc is versioned with the OS) but on other platforms, they are not in lock-step. Further, could we also use this RFC to version any C-like library dependency (ie so)? I've had lots of problems over the years with breaking changes between c libraries wrapped and bound to Rust binaries. |
Closing this as I don't currently have time to work on this. |
I was tempted to reach for something like this when doing rust-lang/cargo#10322, but ultimately there feature testing seemed like the better approach, since it wasn't exactly clear in which version of Windows 11 the behavior changed. |
This RFC adds various
min_$TARGET_API_version
cfg predicates which allow users to conditionally compile code based on the minimum supported API version for a particular target platform API such as libc, the windows build version, macOS version, etc.. For example, this allows for conditionally compiling code for Windows 10+ that will not be compiled when targeting older versions of Windows.Zulip discussion thread
Pre-RFC discourse thread
cc @joshtriplett
Rendered