Pre-RFC: Allow packages to specify a set of supported targets

The word target is extensively used in this document. The glossary defines its many meanings. Here, target refers to the "Target Architeture" for which a package is built. Otherwise, the terms "cargo-target" and "target-triple" are used in accordance with their definitions in the glossary.

Summary

The addition of supported-targets to Cargo.toml. This field is an array of target-triple/cfg specifications that restricts the set of targets which a package supports. Packages must meet the supported-targets of their dependencies, and they can only be built for targets that satisfy their supported-targets.

Motivation

Some packages do not support every possible rustc target. Currently, there is no way to formally specify which targets a package does, or does not support.

Developer Experience

Trying to depend on a crate that does not support one's target often produces cryptic build errors, or worse, fails at runtime. Being able to specify which targets are supported ensures that unsupported targets cannot build the crate, and also makes build errors specific.

This feature also enhances developer experience when working in workspaces containing packages designed for many different targets. Commands run on a workspace ignore packages that don't support the selected target.

Cross Compilation

Once it is known that a package will only ever build for a subset of targets, it opens the door for more advanced control over dependencies. For example, transient dependencies declared under a [target.**.dependencies] table are excluded from Cargo.lock if the dependent's supported-targets is mutually exclusive with the target preconditions under which the dependencies are included. This is especially relevant to areas such as WebAssembly and embedded programming, where one usually supports only a few specific targets. Currently, auditing and vendoring is tedious because dependencies under [target.**.dependencies] tables always make their way in the dependency tree, even though they may not actually be used.

Guide-level explanation

The supported-targets field can be added to Cargo.toml under the [package] table.

This field consists of an array of strings, where each string is an explicit target-triple or a cfg specification (as for the [target.'cfg(**)'] table). The supported cfg syntax is the same as the one for platform-specific dependencies (i.e., cfg(test), cfg(debug_assertions), and cfg(proc_macro) are not supported). If a selected target satisfies any entry of the supported-targets list, then the package can be built for that target.

For example:

[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"
supported-targets = [
    "wasm32-unknown-unknown",
    'cfg(target_os = "linux")',
    'cfg(target_os = "macos")'
]

Here, only targets satisfying: the wasm32-unknown-unknown target, or the linux OS, or the macos OS, are allowed to build the package. If no supported-targets was specified, then any target would be allowed.

User experience is enhanced by raising an error that fails compilation when the supported targets of a package are not satisfied by the selected target. A package's supported-targets must be a subset of its dependencies' supported-targets, otherwise the build also fails.

When supported-targets is not specified, any target is accepted, so all dependencies must support all targets.

This feature should be used when a package clearly does not support all targets. For example: io-uring requires cfg(target_os = "linux"), gloo requires cfg(target_family = "wasm"), and riscv requires cfg(target_arch = "riscv32") or cfg(target_arch = "riscv64").

This feature should also be used to increase cargo's knowledge of a package. For example, when working in a workspace where some packages compile with #[no_std] and target_os = "none", and some others are tools that require a desktop OS, using supported-targets makes cargo <command> ignore packages which have supported-targets that are not satisfied by the selected target.

Reference-level explanation

When cargo is run on a package, it checks that the selected target satisfies the supported-targets of the package. If it does not, an error is raised and the build fails.

Compatibility of [dependencies]

The set of supported-targets of a package must be a subset of the supported-targets of its [dependencies]. If the crate itself has no supported-targets specified, then all dependencies must support all targets.

If a dependency does not respect this requirement (if it is not compatible), an error is raised and the build fails.

If this was not enforced, a package could support targets that are not supported by its dependencies, which does not make sense.

Compatibility of [dev-dependencies]

[dev-dependencies] are checked the using the same method as regular [dependencies]. That is, the package's supported-targets must be a subset of every [dev-dependencies]'s supported-targets. The rationale is that an example, test, or benchmark has access to the package's library and binaries, and so it must respect the supported-targets of the package.

Compatibility of [build-dependencies]

What makes [build-dependencies] unique is that they are built for the host computer, and not the selected target. As such, they are not restrained by the supported-targets of the package. Hence, all dependencies are allowed in the [build-dependencies] table. However, a build error is raised if one of the build dependencies does not support the host's target-triple at build time.

In the future, having all build dependencies support all targets could be enforced to ensure that a crate can be built on any host. This is left as a future possibility.

Platform-specific dependencies

Platform-specific dependencies are dependencies under the [target.**] table. This includes normal dependencies, build-dependencies, and dev-dependencies.

When platform-specific dependencies are declared, the conditions under which they are declared must be a subset of each dependency's supported-targets. For example, a dependency declared under [target.'cfg(target_os = "linux")'.dependencies] must at least support the linux OS.

For regular dependencies and dev-dependencies, it suffices for a platform-specific dependency to support the intersection of the package's supported-targets, and the target conditions it is declared under. For example:

[package]
# ...
supported-targets = ['cfg(target_os = "linux")']

[target.'cfg(target_pointer_width = "64")'.dependencies]
foo = "0.1.0"

Here, it suffices for foo to support cfg(all(target_os = "linux", target_pointer_width = "64")).

Artifact dependencies

If an artifact dependency has a target field, then the dependency is not checked against the package's supported-targets. However, the selected target for the dependency must be compatible with the dependency's supported-targets. If it does not have a target field, then it is checked against the package's supported-targets, like any other dependency.

Comparing supported-targets

When comparing two supported-targets lists, it is necessary to know if one is a subset of the other, or if both are mutually exclusive. To proceed, both lists are flattened to the same representation, and they are then compared.

Flattening not, any, and all in cfg specifications

Since cfg specifications can contain not, any, and all operators, these must be handled. This is done by flattening the cfg specification to a specific form.

The not operator is "passed through" any and all operators using De Morgan's laws, until it reaches a single cfg specification. For example, cfg(not(all(target_os = "linux", target_arch = "x86_64"))) is equivalent to cfg(any(not(target_os = "linux"), not(target_arch = "x86_64"))).

Top level any operators are separated into multiple cfg specifications. For example,

supported-targets = ['cfg(any(target_os = "linux", target_os = "macos"))']

is transformed into

supported-targets = ['cfg(target_os = "linux")', 'cfg(target_os = "macos")']

Top level all operators are kept as is, as long as they do not contain nested anys or alls. If there is an any inside an all, the statement is split into multiple all statements. For example,

supported-targets = ['cfg(all(target_os = "linux", any(target_arch = "x86_64", target_arch = "arm"))']

is transformed into

supported-targets = [
    'cfg(all(target_os = "linux", target_arch = "x86_64"))',
    'cfg(all(target_os = "linux", target_arch = "arm"))'
]

If an all contains an all, the inner all is flattened into the outer all.

The result of these transformations on a cfg specification is a list of cfg specifications that either contains a single specification, or an all operator with no nested operators.

This procedure is run on all cfg elements of a supported-targets list. The resulting list can then be used to evaluate relations. To be clear, the flattened list can only contain explicit target-triples, cfg(A) containing a single A, or cfg(all(A, B, ...)), all A, B, ... being single elements.

The subset relation

To determine if a supported-targets list "A" is a subset of another such list "B", the standard mathematical definition of subset is used. That is, "A" is a subset of "B" if and only if each element of "A" is contained in "B".

So each element of "A" is compared against each element of "B" using the following rules:

  • A target-triple is a subset of another target-triple if they are the same.
  • A target-triple is a subset of a cfg(..) if the cfg(..) is satisfied by the target-triple.
  • A cfg(..) is not a subset of a target-triple.
  • A cfg(all(A, B, ...)) is a subset of a cfg(all(C, D ...)), if the list C, D, ... is a subset of the list A, B, ....

Note: All possible cases have been covered, since cfg(A) == cfg(all(A)).

More rules could be defined, but these are left as a future possibility.

Mutual exclusivity

For a supported-targets list "A" to be mutually exclusive with another such list "B", each element of "A" must be mutually exclusive with all elements of "B" (The inverse is also true).

So each element of "A" is compared against each element of "B" using the following rules:

  • A target-triple is mutually exclusive with another target-triple if they are different.
  • A target-triple is mutually exclusive with a cfg(..) if the cfg(..) is not satisfied by the target-triple.
  • A cfg(all(A, B, ...)) is mutually exclusive with a cfg(all(C, D, ...)) if any element of the list A, B, ... is mutually exclusive with any element of the list C, D, ....

Note: All possible cases have been covered, since cfg(A) == cfg(all(A)).

Two cfg singletons are mutually exclusive under the following rules:

  • cfg(A) is mutually exclusive with cfg(not(A)).
  • cfg(<option> = "A") is mutually exclusive with cfg(<option> = "B") if A and B are different, and <option> has mutually exclusive elements.

Some cfg options have mutually exclusive elements, while some do not. What is meant here is, for example, target_arch = "x86_64" and target_arch = "arm" are mutually exclusive (a target-triple cannot have both), while target_feature = "avx" and target_feature = "rdrand" are not.

cfg options that have mutually exclusive elements:

  • target_arch
  • target_os
  • target_env
  • target_abi
  • target_endian
  • target_pointer_width
  • target_vendor

Those that do not:

  • target_feature
  • target_has_atomic
  • target_family

target_family = "windows and target_family = "unix" could also be defined as mutually exclusive to enhance usability, but this is left as a future possibility.

Behavior with unknown entries (and custom targets)

Just as [target.my-custom-target.dependencies] is allowed by cargo, supported-targets can contain unknown entries. This is important because users may have different rustc versions, and the set of official rustc targets is unstable; Targets can change name or be removed. Also, developers may want to support their custom target.

To determine if an entry in supported-targets is a target name or a cfg specification, the same mechanism as for [target.'cfg(..)'] is used (using cargo-platform).

Ignoring builds for unsupported targets

When the target used is not supported by the package being built, the package will either be skipped or an error will be raised, depending on how cargo was invoked. If cargo is invoked in a workspace or virtual workspace without specifying a specific package, then cargo skips the package. If a specific package is specified using --package, or if cargo is invoked on a single package, then an error is raised.

Eliminating unused dependencies from Cargo.lock

A package's dependencies may themselves have [target.'cfg(..)'.dependencies] tables, which may never be used because of the supported-targets restrictions of the package. These can safely be eliminated from the dependency tree of the package.

Consider the following example:

[package]
name = "foo"
# ...
supported-targets = ['cfg(target_os = "linux")']

[dependencies]
bar = "0.1.0"
[package]
name = "bar"

[target.'cfg(target_os = "macos")'.dependencies]
baz = "0.1.0"

Currently, baz is included in the dependency tree of foo, even though foo is never built for macos. With the addition of supported-targets, baz can be purged from the dependency tree of foo, since target_os = "macos" is mutually exclusive with target_os = "linux".

Formally, dependencies (and transitive dependencies) under [target.**.dependencies] tables are eliminated from the dependency tree of a package if the supported-targets of the package is mutually exclusive with the target preconditions of the dependency.

Drawbacks

  • The cfg syntax is very expressive, but also very complex. As outlined above, detecting subset and mutual exclusivity relations is not trivial.
  • Comparing supported-targets lists is an O(n * m) operation, with n and m being the number of elements in the lists. (I doubt this will be a problem, as n and m are expected to be small).
  • Performance: this is a lot of extra calls to rustc. Hopefully these are all compatible with the rustc cache, so they won't make things too bad.
  • This feature must be learned by those wanting to use dependencies with supported-targets specified.
  • This is the first step towards a target aware cargo, which may increase cargo's complexity, and bring more feature requests along these lines.

Rationale and alternatives

Format

The list of strings format was chosen because of its simplicity and expressiveness. Other formats can also be considered:

  • Using the [target] table, for example:
    [target.'cfg(target_os = "linux")']
    supported = true
    
    If the list of supported targets is long (should it ever be?), then the Cargo.toml file becomes very verbose as well.
  • A [suppported] table, with arch = ["<arch>", ...], os = ["<os>", ...], target = ["<target>", ...], etc. This is more verbose, complex to implement, learn, and remember. It is also not obvious how not and all could be represented in this format. For example:
    [supported]
    os = ["linux", "macos"]
    arch = ["x86_64"]
    

Naming

Some other names for this field can be considered:

  • required-targets. Pro: it matches with the naming of required-features. Con: required-features is a list of features that must all be enabled (conjunction), whereas supported-targets is a list of targets where any is allowed (disjunction).
  • targets. As in "this package targets ...". Pro: Concise. Con: Ambiguous, and could be confused with the target table.

Package scope vs. cargo-target scope

The supported-targets field is placed at the package level, and not at the cargo-target level (i.e., under, [lib], [[bin]], etc.). A rationale is given for why the cargo-target level is not used.

Dependencies are resolved at the package level, so even if one would have cargo-targets with different supported-targets, the dependencies would still be available to all cargo-targets. So, either cargo-targets would have access to dependencies that they cannot use, or all dependencies would need to support the union of all supported-targets of all cargo-targets.

Examples, tests and benchmarks also have access to the package's library and binaries, so they must have the same set of supported-targets.

It is possible to allow cargo-targets to further restrict the supported-targets of the package, but this is left as a future possibility.

See also: using a package vs. using a workspace.

Not using cfg

Using cfg complicates the implementation (and thus maintenance), and may require a substantial amount of calls to rustc to check target-cfg compatibility. Some alternatives are discussed here along with their drawbacks.

Using wildcards

Instead of using cfg specifications, one could use wildcards (e.g., x86_64-*-linux-*). This is much simpler to implement, target-triples are syntactically checked for a match instead of solving set relations for cfg. However, this is not as expressive as cfg, and does not correctly represent the semantics of target triples. For example, supporting target_family = "unix" would require an annoyingly long list of wildcard patterns. Things like target_pointer_width = "32" are even harder to represent, and things like target_feature = "avx" are basically not representable. Also, this is new syntax not currently used by cargo.

Allowing only target triples

This is an even stricter version of the above. Being even simpler to implement, this alternative may not be expressive enough for the common use case. Packages rarely support specific target triples, rather they support/require specific target attributes. What would likely happen is that packages would copy and paste the target triple list matching their requirements from somewhere or someone else. Every time a new target with the same attribute is added, the whole ecosystem would have to be updated.

Prior art

Has this feature been seen anywhere else before?

Unresolved questions

  • What if one wants to exclude a single target-triple? Groups can be excluded with cfg(not(..)), but there is currently no way of excluding specific targets (Would anyone ever require this?).
  • Some crates will inevitably have target requirements that are too strict, how do we make users bypass the error (probably with some --ignore-** flag)? Do we want to allow this?
  • Should we solve for this during dependency version resolution? (the current rationale is that we do not want targets to affect package version resolution).

Future possibilities

Restrict build dependencies

Currently, a build script can have any dependency. A problem can arise if a crate's build script depends on a package that does not support target_os = "windows" for example. With this RFC, it would be possible to only allow dependencies supporting all targets in build scripts.

Lint against useless target-specific tables

If a package has:

[package]
name = "example"
# ...
supported-targets = ['cfg(target_os = "linux")']

[target.'cfg(target_os = "windows")'.dependencies]
# ...

A lint could be added to highlight the fact that the [target] table is useless.

More cfg relations

Consider the following scenario:

[package]
name = "bar"
supported-targets = ['cfg(target_family = "unix")']
# ...
[package]
name = "foo"
supported-targets = ['cfg(target_os = "macos")']

[dependencies]
bar = "0.1.0"

With the RFC implemented, a build error would be raised when compiling foo, because cargo does not understand that target_os = "macos" is a subset of target_family = "unix".

To go around this issue, foo would need to use:

[package]
name = "foo"
supported-targets = ['cfg(all(target_family = "unix", target_os = "macos"))']

[dependencies]
bar = "0.1.0"

To improve usability, two extra relations can be defined:

  • cfg(target_os = "windows")cfg(target_family = "windows").
  • cfg(target_os = <unix-os>)cfg(target_family = "unix"), where <unix-os> is any of ["freebsd", "linux", "netbsd", "redox", "illumos", "fuchsia", "emscripten", "android", "ios", "macos", "solaris"]. This list needs to be updated if a new unix OS is supported by rustc's official target list. This would make the first example compile.

Note: The contrapositive of these relations is also true.

Also, target_family is currently defined as not having mutually exclusive elements. This is because target_family = "wasm" is not mutually exclusive with other target families. But, target_family = "unix" could be defined as mutually exclusive with target_family = "windows" to increase usability.

Misc

  • Have cargo add check the supported-targets before adding a dependency.
  • Make this process part of the resolver.
  • Show which targets are supported on docs.rs.
  • Have search filters on crates.io for crates with support for specific targets.
  • Also add this field at the cargo-target level.
7 Likes

Targets can and have been renamed (let alone removed). How do you propose this is handled?

1 Like

It would help if this

  • gave a motivation and justified the design around the motivation
  • contrasted itself with the designs I gave in those issues for why different decisions were made

Thanks for working on this!

Should the package be able to set a default for all of its targets, to avoid unnecessary repetition?

A little bikeshedding: I don't love the target-requirements name, which feels a little indirect. Maybe supported-targets?

Is there any interaction with the resolver? IMO it would be nice if Cargo.lock does not end up containing dependencies for unsupported targets.

Should there be a CLI interface for ignoring the set of currently supported targets, to enable the use case of someone trying to add support for another (set of) target(s)?

1 Like

The way this is framed, target-requirements is effectively a top-level any. This isn't entirely obvious, though, and it'd be easy for someone to think it was a top-level all instead.

This is a good one which I had not thought about.

So if I understand correctly this is only an issue when targets are explicitly named, not when cfg(..) is used.

I ran some quick tests to see how [target.non-existent-target.dependencies] tables are handled, and it seems like they are ignored without a warning.

My first idea was that we could have a warning against this sort of thing when the "unknown" target comes from the packages' supported target, and we silently ignore when the unknown specification lives in a dependency.

However, I could see why we may want to ignore unknown targets without warning. In a sense it does not pose a problem because we are doing an "or" on all entries, so if the target "never matches," then it is as if it was never there (in set theory A ∪ {} = A). Also, say if someone writes a package for a specific board, and for a specific custom target, then they would likely want to be able to specify only this "unknown" custom target.

I will add this to the more fleshed out draft I am working on.

Should the package be able to set a default for all of its targets, to avoid unnecessary repetition?

I was trying to see if cargo does this for other fields, and I was not able to find any instance, which I why I was reluctant to add this in. I do not have a strong opinion on this idea, and I will add this to my second draft to have opinions of others.

A little bikeshedding: I don't love the target-requirements name, which feels a little indirect. Maybe supported-targets?

Soon after I posted this draft I realized how much I prefered supported-targets, it also emphasizes that the list is an or/union over its elements, not an and/intersection. This will be the identifier used in my next draft.

Is there any interaction with the resolver? IMO it would be nice if Cargo.lock does not end up containing dependencies for unsupported targets.

As this comment says, I think we should focus on having the feature well defined at first, and then we can consider Cargo.lock later as an extension to this RFC. But this is definitely a feature people seem to want.

Should there be a CLI interface for ignoring the set of currently supported targets, to enable the use case of someone trying to add support for another (set of) target(s)?

I'm not sure I fully understand the extent of your question, but I think it is important for users to be able to bypass the supported-targets (hence why it is only a lint) because people may get these wrong.

Do you think supported-targets would be a better choice to indicate that the list is a "union"?

1 Like

Yeah, making "targets" plural seems like it'd more obviously convey that each one is independent.

I skimmed those issues and it was not clear to me what these "different decisions" you're talking about even were. Perhaps @carloskiki has a better sense of them but I think it would be helpful if you would lay out what you perceive as the differences between this proposal and any conclusions reached in the issues cited.

Having said that, I did notice that allowing cfg expressions in target-requirements appears to be new in this proposal, and so I want to speak up specifically in favor of that. It is very common to have programs that are specific to one operating system but don't care about any other target property. For example, right now I'm working on a couple of crates whose entire purpose is to make use of Linux-kernel-specific functions, but they have no CPU-specific requirements. For such crates, target-requirements = ['cfg(target_os = "linux")'] would be easier to write and easier to verify than enumerating every *-*-linux* target tuple. Maybe even more important, it doesn't need to be updated when Linux and/or Rust add support for another CPU architecture. It only needs to change if the program itself changes, e.g. by gaining support for another operating system.

Programs specific to one CPU architecture (or a handful of them), but not to the combination of a CPU and OS, are less common, but not rare, and a similar argument would apply.

3 Likes

What should happen if a crate declares itself Linux specific (due to a libc API wrapping a Linux specific syscall for example) but then another OS implements support for that same syscall and libc API? Ideally the crate should be updated to add thst other OS to the allowlist. But what happens if the crate is no longer actively maintained (as happens often)?

Mechanisms like supported-targets are not just about what works, they're also about what's supported. If a non-Linux OS adds support for a syscall, that doesn't mean the crate author is prepared to support that OS going forward, either because they don't want to test on that OS, or because they may expect to make future changes that adopt new Linux-specific mechanisms and don't intend to limit themselves to what that other OS supports.

That said, for the benefit of old unsupported crates, I expect we'd probably have some kind of --force-something option, the same way we have such an option to override a crate's specified MSRV.

2 Likes

Giving the information to Cargo is still beneficial for earlier, clearer, and more consistent error messaging, but if this doesn't interact with package resolution at all, then it's worth noting that it's roughly equivalent to today writing

#[cfg(not(any(…)))]
compile_error!("unsupported target cfg");

which also has the added benefit of allowing the crate to say why the cfg is unsupported instead of just that the cfg is unsupported. (Only if they put in that effort, of course.)

4 Likes

Yeah, the #1 reason why I'd personally want this over a compile_error! for those Linux-specific crates I was talking about, is if it would insulate me from problems like "rustix depends transitively on multiple versions of windows-sys · Issue #1233 · bytecodealliance/rustix · GitHub and this causes clippy's multiple-crate-versions lint to fire on my crate (which uses rustix) even though I know that my crate will never be used on Windows."

It seems to me that there are "ecosystem" reasons to want this feature, though. For instance, structured, declarative metadata about which crates can be used on which targets could be valuable for searching and tagging on crates.io and lib.rs.

2 Likes

If we ever get targets which we do not expect crates to support by default since they are sufficiently non-standard (e.g., CHERI), this may also be a good way to explicitly opt-in to support such targets.

2 Likes

Somewhat relevant proposal (see the "Integration with Cargo.toml" section):

See my posts

Items include

  • Naming: Though this acknowledges my proposed name of required-targets, it does say why it went with a name that is inconsistent with the existing manifest format
  • Semantics: I specified them to be like required-features, skip a build-target if its an unsupported platform-target, instead of being a deny-by-default lint
    • At least for myself, I would be interested in auto-enabling a build-target's required-features if it was explicitly requested (for some definition of explicit) and would see us doing similar here if --target isn't specified
  • cfg: I excluded it for now for an MVP because supporting it in package.required-targets adds a lot of complexity to handle it for the Cargo.lock file. Likely, we'll have to decide between cfg and having smaller lockfiles / vendored results
    • No where was globbing a consideration. so what little comparison is made to my proposal is not even for my proposal
    • Why an array and why any rather than all? because cfg` wasn't going to be supported in the MVP and this allowed for specifying multiple platform tuples
  • Validating a build-target or package is a subset of dependencies: This also hinges on not supporting cfg. We can check required-targets against requires-targets easily if they are platform tuples. If our required-targets is a platform tuple, we can enumerate all of them with rustc to check if target.cfg() matches. However, if we're doing set logic with cfg against cfg, I have questions. rustc only tells us what cfg values exist for a given platform-target. We do not know the relationship to be able to do things like target_os = "windows" ⊆ target_family = "windows". We could say that the same set conditions must exist but then we'll have to do the set logic to handle any, all, and not which I worry will be a lot of complexity.
2 Likes

I have updated (75% rewritten) the Pre-RFC. I edited the top post with the new version, while the first draft is available here.

I believe most of your bullet points are now covered in this new draft.

Make sure to correct me if I am missing anything!

Would be good to link to the glossary and either use the terms from there (cargo-target, target triple) or my own spin which is build-target and platform-target.

This never says what happens. Is it an error? A warning? A lint?

Since this gets into terminology, there are "crates" which are build units and there are .crate files which are published.

This can matter for more than just published packages, like git dependencies or even path dependencies in a monorepo.

Also, publish-time is too late to notify people of a problem as that is the very tail end of a release process. Feedback needs to happen during development. Also, irreversible actions may have already been taken as part of the release process.

This seems like the place to specify this behavior or at least provide guidance on when each is chosen. Leaving it this vague prevents meaningful discussion to work out the design.

Isn't this the same check as "Compatibility with dependencies" and one says it should lint as incompatible while this says it is unused?

I don't think that is enough of a difference to justify a different name.

This leaves off a big problem with cfg: if we support this in package and want to rely on this for trimming the Cargo.lock file, things may get messy.

imo this is my problem with calling it a lint: Cargo only offers static lint control and I don't see it making sense to set this and override it with static lint control.

It can just be an error instead, even if there is a flag for overriding it. The question is, do we need to override it?

A big problem with dependency resolution is you'd have to set this for all cargo-targets.

Something this leaves out from the Alternatives is why not also support this in package which would apply to everything and could be easier to use in most cases and be to apply to dependency resolution.