Skip to content

nixos/nixpkgs: use type-checking to enforce buildPlatform & hostPlatform restrictions & restore hostPlatform == buildPlatform support#379411

Closed
MattSturgeon wants to merge 2 commits intoNixOS:masterfrom
MattSturgeon:nixpkgs_module
Closed

nixos/nixpkgs: use type-checking to enforce buildPlatform & hostPlatform restrictions & restore hostPlatform == buildPlatform support#379411
MattSturgeon wants to merge 2 commits intoNixOS:masterfrom
MattSturgeon:nixpkgs_module

Conversation

@MattSturgeon
Copy link
Contributor

@MattSturgeon MattSturgeon commented Feb 4, 2025

The assertion added in #376988 was ineffective, because pkgs/top-level has its own assertions that are checked earlier.

Validating the option definition feels like a good place to use the module system's type-checking, so that is done by this PR.

Added a custom platformType that accepts string or attr definitions, but performs some additional checks on the attr definitions to ensure they do not contain unexpected attrs.

Currently, it checks a blacklist consisting of [ "parsed" ], but this could be expanded if there are other attrs that should not be defined as platform input args. Currently, only platform ? parsed is checked.

coercedTo is used to ensure *Platform options are always attrs, even when defined as strings. This partially restores previous behaviour that was implemented via an apply function calling lib.systems.elaborate.

Additionally, this PR restores the ability to do cfg.hostPlatform == cfg.buildPlatform.

As an alternative, if we really want these options to be write-only, we could warn/throw whenever the *Platform options are read (i.e. throw on apply). Internal reading could be done via lib.modules.mergeDefinitions, see #376988 (comment).

See discussion in #376988, in particular #376988 (comment)

cc @wolfgangwalther @roberth

Things done

  • Built on platform(s)
    • x86_64-linux
    • aarch64-linux
    • x86_64-darwin
    • aarch64-darwin
  • Tested, as applicable:
    • nix-build ./nixos/tests/misc.nix
    • nix-build ./lib/tests/release.nix
  • 25.05 Release Notes (or backporting 24.11 and 25.05 Release notes)
    • (Package updates) Added a release notes entry if the change is major or breaking
    • (Module updates) Added a release notes entry if the change is significant
    • (Module addition) Added a release notes entry if adding a new NixOS module
  • Fits CONTRIBUTING.md.

Add a 👍 reaction to pull requests you find important.

@github-actions github-actions bot added 6.topic: nixos Issues or PRs affecting NixOS modules, or package usability issues specific to NixOS 8.has: module (update) This PR changes an existing module in `nixos/` 10.rebuild-darwin: 0 This PR does not cause any packages to rebuild on Darwin. 10.rebuild-linux: 1-10 This PR causes between 1 and 10 packages to rebuild on Linux. labels Feb 4, 2025
Copy link
Contributor

@wolfgangwalther wolfgangwalther left a comment

Choose a reason for hiding this comment

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

Thank you so much for improving this, I didn't find the time myself, yet!

❤️

Comment on lines 60 to 63
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't like the denylist approach, because it implies that "parsed" per se is wrong, and other attributes might be fine. One could assume that "by removing parsed from an elaborated system, we're good to go". This is not the case.

"parsed" is just a symptom of an elaborated system. Anything that passes non-user-defined stuff forward is problematic.

I'd rather hardcode the parsed check below and not have this list.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

"parsed" is just a symptom of an elaborated system. Anything that passes non-user-defined stuff forward is problematic.

Sure, but how do we define what is user-defined?

You're more familiar with this than me, so I'm probably missing some context.

My intention was that platformAttrsBlacklist should contain all attr names that should not be user-defined.

If there's a better approach, I'm certainly open to it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could we solve this by adding a _type to elaborated platforms?

That way we can check:

check =
  platform:
  if platform._type or null == "elaborated-platform" then
    # TODO: trace, warn, or throw
    false
  else
    true;

Copy link
Contributor

@wolfgangwalther wolfgangwalther Feb 4, 2025

Choose a reason for hiding this comment

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

This is the odd thing about lib.systems.elaborate. Essentially all derived values should not be set by the user. But some of the attrs are sometimes derived and sometimes not.

Valid Example:

{
  config = "riscv64-none-elf";
  libc = "newlib";
}

Invalid Example:

{
  config = "powerpc64le-unknown-linux-musl";
  libc = "newlib";
}

This will be very hard to encode properly - essentially you'd be fixing systems.elaborate that way.

Sure, but how do we define what is user-defined?

There is some place where the user explicitly specifies their configuration for the system. They might run native and just have a single { system = "..."; } or so. They might try to run a system fully on musl, so maybe { config = "x86_64-unknown-linux-musl"; }. Or built via LLVM, or they might be cross-compiling something etc... - in all those cases they explicitly specify some values.

Then, two things happens to those values:

  • they are passed on, e.g. from nixos to nixpkgs
  • they might be extended by package set modifiers like pkgsStatic etc.

Everything that is either defined by the user directly or added via a package set like pkgsStatic is allowed. Everything that is only derived via systems.elaborate is not.

You can't hardcode this.


Let me phrase it otherwise: We can't guard against invalid values, which are passed to the nixos/nixpkgs module. We can only guess from the input that the user used a wrong code pattern (i.e. they passed an elaborated system), so we try to give them a hint, otherwise everything else breaks down.

Copy link
Contributor

Choose a reason for hiding this comment

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

Could we solve this by adding a _type to elaborated platforms?

Yes, this could work, I think.

Not sure whether that might have been what was meant by a previous comment saying this:

TODO make lib.systems itself use the module system.

@roberth did you add that comment?

Copy link
Contributor Author

@MattSturgeon MattSturgeon Feb 4, 2025

Choose a reason for hiding this comment

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

TODO make lib.systems itself use the module system.

I believe that TODO was suggesting that the lib.systems functions could themselves use modules in order to produce the elaborated system. Similar to how nixpkgs's config uses the module system, for example.

That's unrelated to the suggestion of adding an _type.

Comment on lines 77 to 79
Copy link
Contributor

Choose a reason for hiding this comment

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

See reasoning above, I don't think we can list "unsupported" attributes.

For example libc might sometimes be correct and sometimes be incorrect to pass - depending on the circumstances.

Comment on lines 74 to 76
Copy link
Contributor

Choose a reason for hiding this comment

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

We should probably extend this to say something about using nixpkgs.pkgs.stdenv.xxxPlatform instead for reading.

Comment on lines +251 to +257
Copy link
Contributor

Choose a reason for hiding this comment

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

While technically this does not hurt, it does invite reading from config.nixpkgs.xxxPlatform, which I think should be discouraged... or maybe not? I don't really know

It just needs to be clear that only user input can be read here, nothing else.

Copy link
Contributor Author

@MattSturgeon MattSturgeon Feb 4, 2025

Choose a reason for hiding this comment

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

IMO, so long as these options remain readable, this apply should remain.

If we prefer to make the options write-only, then we would have something like:

apply = _: throw "`nixpkgs.buildPlatform` should not be read. Use `pkgs.stdenv.buildPlatform` instead.";

Or we could silently apply pkgs.stdenv.*Platform here:

apply = _: pkgs.stdenv.buildPlatform;

Or we could do that, but lib.warn about it:

apply =
  _:
  lib.warn
    "`nixpkgs.buildPlatform` should not be read. Use `pkgs.stdenv.buildPlatform` instead."
    pkgs.stdenv.buildPlatform;

With any of these approaches, we would need to manually merge opt.buildPlatform's definitions for our internal usage, as per #376988 (comment).

Copy link
Contributor

@wolfgangwalther wolfgangwalther Feb 4, 2025

Choose a reason for hiding this comment

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

Or we could silently apply pkgs.stdenv.*Platform here:
[...]
With any of these approaches, we would need to manually merge opt.buildPlatform's definitions for our internal usage

Isn't that essentially what I tried in eec2100 ?

This didn't work out.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Isn't that essentially what I tried in eec2100 ?

Very similar yes. The apply would be different, but we'd still need something like rawValue or manually merging the option defs to get the un-applied value.

This didn't work out.

What was the issue with that approach? I'm trying to work out if the same issues would apply to my suggestion here.

Copy link
Contributor

Choose a reason for hiding this comment

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

For future reference, if anyone ever comes back to this PR.

Isn't that essentially what I tried in eec2100 ?

What was the issue with that approach?

It's not so much that the approach was wrong.. it just wasn't enough - and lead to my later change naturally.

To be able to follow through on my first try, you'd need to make sure that nobody passes elaborated systems into those options - otherwise it doesn't matter whether you apply or not, pass the rawValue on or not. If you don't add an assertion to guard against that, you will end up with really hard to debug, obscure errors in entirely different places, though.

Once you add asserts against passing elaborated systems.. you suddenly can't pass config.nixpkgs.hostPlatform etc. onwards. But this is not only done in the nixos/nixpkgs module itself - it's done by random consumers, too. So you can't really use an undocumented, internal interface (the one I added with rawValue) to do that. You need access to the original values - and they are there, if we don't apply. Which we don't really need to, for any reason.

Copy link
Contributor Author

@MattSturgeon MattSturgeon Feb 5, 2025

Choose a reason for hiding this comment

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

If it's helpful, I could implement this as a standalone PR without your wider changes?

That could either be merged before you re-apply your changes or it could be cherry-picked into your changes, as appropriate.

Otherwise, I can just review whatever you come up with.

Copy link
Contributor

Choose a reason for hiding this comment

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

If you'd like to follow up on this, that would be great.

I won't push this myself for now, because as outlined in #376988 (comment)... I never really intended to work on the NixOS modules :D. I was dragged into this by accident.

The pkgs/top-level is on hold for now, because:

  • we need a deprecation phase for the nixos module (especially about passing elaborated systems)
  • we can't do the package set composability without having finished the deprecation phase

I would love to come back to this once the NixOS modules are ready for it - and if you can push this side, I'm more than happy to help you whereever I can.

Copy link
Contributor

@wolfgangwalther wolfgangwalther Feb 5, 2025

Choose a reason for hiding this comment

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

  1. Use option-type's check to reject elaborated platform definitions

  2. Use option's apply to enforce write-only and/or return the actual pkgs.stdenv.hostPlatform

  3. Use rawValue and/or manual definition merging to bypass the apply internally

I think it would also be great to have some kind of readonly config.nixpkgs.<something>, which bundles all the system, localSystem, crossSystem, hostPlatform and buildPlatform inputs in one place. This can then be easily passed on instead of accessing rawValue for everything manually. This would make the linux-builder case discussed in #376988 (comment) much easier to handle (and there are quite a few more cases like that).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you'd like to follow up on this, that would be great.

[...]

I would love to come back to this once the NixOS modules are ready for it - and if you can push this side, I'm more than happy to help you whereever I can.

Awesome 😎

I think it would also be great to have some kind of readonly config.nixpkgs.<something>, which bundles all the system, localSystem, crossSystem, hostPlatform and buildPlatform inputs in one place. This can then be easily passed on instead of accessing rawValue for everything manually.

As outlined in #376988 (comment), I think this is better handled in pkgs/top-level rather than the nixpkgs module. The reason being when we want to "pass on" platform input, we usually want to do so for whichever pkgs is actually in use; not necessarily the one being configured by the nixpkgs module.

Perhaps internal un-elaborated options could be useful for avoiding the apply without hacks though, but still I'm uneasy about them being used externally to "pass on" the input values, for the reasons outlined above.

Copy link
Contributor

Choose a reason for hiding this comment

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

when we want to "pass on" platform input, we usually want to do so for whichever pkgs is actually in use; not necessarily the one being configured by the nixpkgs module.

Yes, I start to see this, after you made multiple comments in various places all along the same line... ;)

I think this is better handled in pkgs/top-level rather than the nixpkgs module.

This is an interesting thought, which I will have to think about a bit more!

…orm restrictions

The assertion added in 0a19371 was
ineffective, as `pkgs/top-level` has its own assertions that are checked
earlier.

Validating the option definition feels like a good place to use the
module system's type-checking.

Added a custom `platformType` that accepts string or attr definitions,
but performs some additional checks on the attr definitions to ensure
they do not contain unexpected attrs.
0a19371 removed the ability to compare
`hostPlatform` and `buildPlatform`.
@MattSturgeon
Copy link
Contributor Author

I'm closing this because #376988 was reverted in #379615.

Of course this PR can still be used/referenced in any future implementation if needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

6.topic: nixos Issues or PRs affecting NixOS modules, or package usability issues specific to NixOS 8.has: module (update) This PR changes an existing module in `nixos/` 10.rebuild-darwin: 0 This PR does not cause any packages to rebuild on Darwin. 10.rebuild-linux: 1-10 This PR causes between 1 and 10 packages to rebuild on Linux.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants