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

RFC: cfg_os_version_min #3750

Open
wants to merge 20 commits into
base: master
Choose a base branch
from

Conversation

ChrisDenton
Copy link
Member

@ChrisDenton ChrisDenton commented Dec 27, 2024

This RFC is largely the work of @rylev and @chriswailes. As suggested by @tmandry I have stripped it down to an MVP that just adds os_version_min.

Rendered

@ehuss ehuss added the T-lang Relevant to the language team, which will review and decide on the RFC. label Dec 31, 2024
A set of comparison functions can be provided by `rustc` for common formats such as 2- and 3-part semantic versioning.
When a platform detects a key it doesn’t support it will return `false` and emit a warning.

Each target platform will set the minimum API versions it supports.
Copy link
Member

Choose a reason for hiding this comment

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

So there is no way for the user to configure which minimum API version they want to use? That would make it impossible for eg the libc crate to gate api's behind #[cfg(os_version_min)] corresponding to the libc version that introduced the API in question.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes. One of the issues with the original proposals was there was too much going on. This RFC aims to add the minimally useful feature. Adding compiler flags and Cargo configs to control the minimum version can be a future extension.

With this RFC it is still possible for libc to gate APIs like that. However, you'd need a new target in order to set a different minimal libc version.

@Aloso
Copy link

Aloso commented Jan 3, 2025

The idea is great, but I think the syntax can be improved. It makes more sense to have this feature under the existing target_os config, so here's an idea:

cfg(target_os("windows", min_version = "..."))

Another idea:

cfg(target_os = "windows >= ...")

@ChrisDenton
Copy link
Member Author

That may work for windows but not, for example, Linux where you may want to cfg on the kernel version or the libc version (or even both).

libc isn't really a target_os.

@Aloso
Copy link

Aloso commented Jan 3, 2025

Right, so this could be done with a target_libc attribute, like

cfg(target_libc("glibc", min_version = "..."))

But of course this would add more complexity.

Copy link
Contributor

@madsmtm madsmtm left a comment

Choose a reason for hiding this comment

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

Thanks for reviving this @ChrisDenton! I'd be willing to implement at least the Apple parts of this, and probably also other parts.

text/3750-cfg-os-version-min.md Outdated Show resolved Hide resolved
text/3750-cfg-os-version-min.md Outdated Show resolved Hide resolved
text/3750-cfg-os-version-min.md Show resolved Hide resolved
text/3750-cfg-os-version-min.md Outdated Show resolved Hide resolved
text/3750-cfg-os-version-min.md Show resolved Hide resolved
text/3750-cfg-os-version-min.md Outdated Show resolved Hide resolved
text/3750-cfg-os-version-min.md Outdated Show resolved Hide resolved
text/3750-cfg-os-version-min.md Outdated Show resolved Hide resolved
# Future possibilities
[future-possibilities]: #future-possibilities

The compiler could allow setting a higher minimum OS version than the target's default.
Copy link
Contributor

Choose a reason for hiding this comment

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

Remark: I'll add that rustc already has a mechanism for doing exactly this on Apple platforms, namely the *_DEPLOYMENT_TARGET family of environment variables.


**Note:** Here it would be important to link to documentation showing the `cfg` predicates and the different version strings that are supported.

# Reference-level explanation
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need to discuss here what happens when you link libraries compiled with different (even though we're not specifying how the user, we will need a mechanism for it at some point)

Specifically, it'd be nice to talk about the pre-compiled standard library, and how it effectively becomes a requirement to use -Zbuild-std if the user wants to enable this kind of stuff for the standard library.


**Note:** Here it would be important to link to documentation showing the `cfg` predicates and the different version strings that are supported.

# Reference-level explanation
Copy link
Contributor

Choose a reason for hiding this comment

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

Possibly also interesting is soundness concerns (what happens if I use a standard library compiled with a newer version of glibc/Windows APIs/macOS APIs, while I link with a binary compiled for older APIs?).

I don't think there are any soundness concerns, at least not on Apple platforms (the dynamic linker will simply fail to work if using an API for a newer system), but it's important that we're certain of this (and that function pointers for example aren't simply replaced by NULL if loaded on an older OS).

Copy link
Member Author

@ChrisDenton ChrisDenton Jan 27, 2025

Choose a reason for hiding this comment

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

There aren't unsoundness issues for Windows. Either a dll or symbol is available or it isn't and there's an error. EDIT: I'm being told that attempting to use incompatible glibcs will also cause an error.

I don't know if that's true of all OSes but Rust libs do carry around metadata with them so if they declare incompatible versions then the compiler could simply error. And linking together native static libraries compiled for different API versions would seem to be squarely in the realm of "you must know what you're doing" (and that's true more broadly when linking native libs). However, considering the current narrow scope of this RFC, it would not be a situation that arises any more often than it currently does. So it may only worth a mention in future possibilities.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd actually like to update my previous statement, I suspect that it may actually be unsound to use the combination dependency (compiled for newer OS) + user_crate (compiled for older OS) + link (for older OS), since e.g. LLVM may do codegen optimizations that are only valid on newer architectures (which one can do because Apple restricts OS upgrades after a certain point, so we know that newer OS versions only run on newer hardware, and can be required to have for example a certain level of SIMD features).

Related here is #3716.

But I agree that this is only tangentially related to the RFC.

text/3750-cfg-os-version-min.md Show resolved Hide resolved
@ChrisDenton
Copy link
Member Author

Ok I've rewritten this a bit and further simplified it based on feedback.


Operating systems and their libraries are continually advancing, adding and sometimes removing APIs or otherwise changing behaviour.
Versioning of APIs is common so that developers can target the set of APIs they support.
Crates including the standard library must account for various API version requirements for the crate to be able to run.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Reads more easily IMO.

Suggested change
Crates including the standard library must account for various API version requirements for the crate to be able to run.
Crates, including the standard library, must account for various API version requirements for the crate to be able to run.


* Relying on dynamic detection of API support has a runtime cost.
The standard library often performs [dynamic API detection](https://github.com/rust-lang/rust/blob/f283d3f02cf3ed261a519afe05cde9e23d1d9278/library/std/src/sys/windows/compat.rs) falling back to older (and less ideal) APIs or forgoing entire features when a certain API is not available.
For example, the [current `Mutex` impl](https://github.com/rust-lang/rust/blob/234099d1d12bef9d6e81a296222fbc272dc51d89/library/std/src/sys/windows/mutex.rs#L1-L20) has a Windows 7 fallback. Users who only ever intend to run their code on newer versions of Windows will still pay a runtime cost for this dynamic API detection.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Linking a 5 year old commit cannot really be called "current" anymore ;). It's a fine example though, just wish it was more honest. Maybe:

Suggested change
For example, the [current `Mutex` impl](https://github.com/rust-lang/rust/blob/234099d1d12bef9d6e81a296222fbc272dc51d89/library/std/src/sys/windows/mutex.rs#L1-L20) has a Windows 7 fallback. Users who only ever intend to run their code on newer versions of Windows will still pay a runtime cost for this dynamic API detection.
For example, when the standard library still supported Windows 7 by default, [the `Mutex` impl](https://github.com/rust-lang/rust/blob/234099d1d12bef9d6e81a296222fbc272dc51d89/library/std/src/sys/windows/mutex.rs#L1-L20) had a Windows 7 fallback. Users who only ever intended to run their code on newer versions of Windows still paid a runtime cost for this dynamic API detection.

For example, the [current `Mutex` impl](https://github.com/rust-lang/rust/blob/234099d1d12bef9d6e81a296222fbc272dc51d89/library/std/src/sys/windows/mutex.rs#L1-L20) has a Windows 7 fallback. Users who only ever intend to run their code on newer versions of Windows will still pay a runtime cost for this dynamic API detection.
Providing a mechanism for specifying which minimum API version the user cares about, allows for statically specifying which APIs a binary can use.
* Certain features cannot be dynamically detected and thus limit possible implementations.
The libc crate must use [a raw syscalls on Android for `accept4`](https://github.com/rust-lang/libc/pull/1968), because this was only exposed in libc in version 21 of the Android API.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Grammar.

Suggested change
The libc crate must use [a raw syscalls on Android for `accept4`](https://github.com/rust-lang/libc/pull/1968), because this was only exposed in libc in version 21 of the Android API.
The libc crate must use [a raw syscall on Android for `accept4`](https://github.com/rust-lang/libc/pull/1968), because this was only exposed in libc in version 21 of the Android API.


**Note:** Here it would be important to link to documentation showing the `cfg` predicates and the different version strings that are supported.

# Reference-level explanation
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd actually like to update my previous statement, I suspect that it may actually be unsound to use the combination dependency (compiled for newer OS) + user_crate (compiled for older OS) + link (for older OS), since e.g. LLVM may do codegen optimizations that are only valid on newer architectures (which one can do because Apple restricts OS upgrades after a certain point, so we know that newer OS versions only run on newer hardware, and can be required to have for example a certain level of SIMD features).

Related here is #3716.

But I agree that this is only tangentially related to the RFC.

Comment on lines +77 to +83
#[any(
os_version_min("macos", "11.0"),
os_version_min("ios", "14.0"),
os_version_min("tvos", "14.0"),
os_version_min("watchos", "7.0"),
os_version_min("visionos", "1.0")
)]
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: missing cfg.

Suggested change
#[any(
os_version_min("macos", "11.0"),
os_version_min("ios", "14.0"),
os_version_min("tvos", "14.0"),
os_version_min("watchos", "7.0"),
os_version_min("visionos", "1.0")
)]
#[cfg(any(
os_version_min("macos", "11.0"),
os_version_min("ios", "14.0"),
os_version_min("tvos", "14.0"),
os_version_min("watchos", "7.0"),
os_version_min("visionos", "1.0")
))]

Comment on lines +131 to +133
Version strings can take on nearly any form and while there are some standard formats, such as semantic versioning or release dates, projects can change schemas or provide aliases for some or all of their releases.
Because of this diversity in version strings each platform will be responsible for defining an `is_gte` comparison function for each key they support.
Alternatively they can use one of the pre-defined functions.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: This sounds very implementation-specific / a leftover from previous RFCs. Maybe instead:

Suggested change
Version strings can take on nearly any form and while there are some standard formats, such as semantic versioning or release dates, projects can change schemas or provide aliases for some or all of their releases.
Because of this diversity in version strings each platform will be responsible for defining an `is_gte` comparison function for each key they support.
Alternatively they can use one of the pre-defined functions.
Version strings can take on nearly any form and while there are some standard formats, such as semantic versioning or release dates, projects/platforms can change schemas or provide aliases for some or all of their releases.
Because of this diversity in version strings, each target will be responsible for validating the version, and defining comparisons on it.

It requires a key and a version string.
The key can be either a `target_os` string or else one of a set of target-defined strings.
Version strings are always target defined (see [Versioning Schema][versioning-schema]) and will be compared against the target's supported version.
For example, `#[cfg("macos", "11.0")]` has the key `macos` and the minimum version `11.0`, which will match any macos version greater than or equal to `11.0`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Write full cfg, and mention macOS name.

Suggested change
For example, `#[cfg("macos", "11.0")]` has the key `macos` and the minimum version `11.0`, which will match any macos version greater than or equal to `11.0`.
For example, `#[cfg(os_version_min("macos", "11.0"))]` has the key `macos` and the minimum version `11.0`, which will match any macOS version greater than or equal to macOS 11 Big Sur.

Comment on lines +138 to +140
By default `os_version_min` will be linted by `check_cfg` in a similar way to `target_os`.
That is, all valid values for `target_os` will be accepted as valid keys for `os_version_min` on all platforms.
The list of additional keys supported by the target will be consulted, which will then be allowed on a per-target basis.
Copy link
Contributor

Choose a reason for hiding this comment

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

Remark: I want to note here that I want version validation, and I want it everywhere ;).

E.g. writing just #[cfg(os_version_min("macos", "invalid"))] should give an error stating that invalid is not a valid version string, and ideally regardless of what target I am currently compiling for.

Custom targets usually specify their configurations in JSON files.
It is unclear how the target maintainers would add version comparison information to these files.

What exactly should the syntax be?
Copy link
Contributor

Choose a reason for hiding this comment

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

To further the bikeshedding: os_version_min is definitely wrong, it should at the very least be version_min (since it works for libc too, which is not an OS).

But how about the name available?

exftern "C" {
    #[cfg(any(
        available("libc", "x.y.z"),
        available("macos", "10.12"),
    ))]
    fn foo();
}

if cfg!(available("windows", "10.0.10240")) {
    // ...
}

A nice thing about available is that it reads a tiny bit more like English:

#[cfg(version_min("macos", "10.12"))] // configured where version minimum macOS 10.12
#[cfg(available("macos", "10.12"))]   // configured where available macOS 10.12.

While still avoiding the "does cfg!(xyz("macos", "10.12")) mean >10.12 or >=10.12" issue.

This could also tie in nicely with a macro available! that first does the static check, and then falls back to a runtime version check against e.g. gnu_get_libc_version() (not proposing this macro here, and probably not implement-able everywhere either, but just to get the idea across):

if available!("libc", "x.y.z") || available!("macos", "10.12") {
    // ... Dynamically use some new feature or API, maybe with `dlsym` or libloading.
}

(Note: I'm heavily biased by Swift's @available).

Copy link
Member

Choose a reason for hiding this comment

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

cfg(available) is confusable with cfg(accessible).

Copy link
Member

Choose a reason for hiding this comment

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

If we wanted parity with cfg(version), we could do something like cfg(target_version("macos", "10.12")). They both follow the >= rule, so it would be good to align their naming scheme.

This RFC is a continuation of [RFC #3379](https://github.com/rust-lang/rfcs/pull/3379) more narrowly scoped to just `os_version_min`.
That RFC was in turn an updated version of [this RFC draft](https://github.com/rust-lang/rfcs/pull/3036), with the changes reflecting conversations from the draft review process and [further Zulip discussion](https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/CFG.20OS.20Redux.20.28migrated.29/near/294738760).

# Unresolved questions
Copy link
Contributor

Choose a reason for hiding this comment

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

Another unresolved question to add: How should this work in Cargo [target.'cfg(os_version_min(...))'.dependencies] sections?

madsmtm added a commit to madsmtm/rust that referenced this pull request Feb 11, 2025
Based on in-progress RFC: rust-lang/rfcs#3750.

Only implemented for Apple platforms for now, but written in a way that
should be easily expandable to include other platforms.
madsmtm added a commit to madsmtm/rust that referenced this pull request Feb 11, 2025
Based on in-progress RFC: rust-lang/rfcs#3750.

Only implemented for Apple platforms for now, but written in a way that
should be easily expandable to include other platforms.
madsmtm added a commit to madsmtm/rust that referenced this pull request Feb 11, 2025
Based on in-progress RFC: rust-lang/rfcs#3750.

Only implemented for Apple platforms for now, but written in a way that
should be easily expandable to include other platforms.
Copy link
Contributor

@madsmtm madsmtm left a comment

Choose a reason for hiding this comment

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

I've created a tracking issue for this feature in rust-lang/rust#136866, and filed a PR implementing it (unstably) for Apple platforms in rust-lang/rust#136867. I guess that at least shows feasibility of the RFC.

- Feature Name: `cfg_os_version_min`
- Start Date: 2024-12-27
- RFC PR: [rust-lang/rfcs#3750](https://github.com/rust-lang/rfcs/pull/3750)
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)
- Rust Issue: [rust-lang/rust#136866](https://github.com/rust-lang/rust/issues/136866)

madsmtm added a commit to madsmtm/rust that referenced this pull request Feb 11, 2025
Based on in-progress RFC: rust-lang/rfcs#3750.

Only implemented for Apple platforms for now, but written in a way that
should be easily expandable to include other platforms.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants