Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 20 additions & 20 deletions nixos/modules/misc/nixpkgs.nix
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ let
defaultPkgs =
if opt.hostPlatform.isDefined then
let
isCross = cfg.buildPlatform != cfg.hostPlatform;
isCross =
!(lib.systems.equals (lib.systems.elaborate cfg.buildPlatform) (
lib.systems.elaborate cfg.hostPlatform
));
systemArgs =
if isCross then
{
Expand Down Expand Up @@ -195,13 +198,10 @@ in
};

hostPlatform = lib.mkOption {
type = lib.types.either lib.types.str lib.types.attrs; # TODO utilize lib.systems.parsedPlatform
type = lib.types.either lib.types.str lib.types.attrs;
example = {
system = "aarch64-linux";
};
# Make sure that the final value has all fields for sake of other modules
# referring to this. TODO make `lib.systems` itself use the module system.
apply = lib.systems.elaborate;
Comment on lines -202 to -204
Copy link
Member

Choose a reason for hiding this comment

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

Please add this back (or something like it), so that the type of config.nixpkgs.hostPlatform is always a platform attrset.

Copy link
Contributor Author

@wolfgangwalther wolfgangwalther Feb 2, 2025

Choose a reason for hiding this comment

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

So, if we do that, then we can't do any of this PR, essentially.

Citing from the commit message:

The description for options.nixpkgs.system already hints at this:

Neither ${opt.system} nor any other option in nixpkgs.* is meant
to be read by modules and configurations.
Use pkgs.stdenv.hostPlatform instead.

We can support this goal by not elaborating the systems anymore, forcing
users to go via pkgs.stdenv.

It seems the intent of the apply = elaborate was, that consumers could look at config.nixpkgs.hostPlatform and use it for downstream logic... but the comment cited above already says otherwise.

Also, almost every consumer already depended on stdenv.hostPlatform instead - except for two tests. I think this makes more sense.

What's your reasoning to not do it like this / what am I missing?

defaultText = lib.literalExpression ''(import "''${nixos}/../lib").lib.systems.examples.aarch64-multiplatform'';
description = ''
Specifies the platform where the NixOS configuration will run.
Expand All @@ -213,22 +213,13 @@ in
};

buildPlatform = lib.mkOption {
type = lib.types.either lib.types.str lib.types.attrs; # TODO utilize lib.systems.parsedPlatform
type = lib.types.either lib.types.str lib.types.attrs;
default = cfg.hostPlatform;
example = {
system = "x86_64-linux";
};
# Make sure that the final value has all fields for sake of other modules
# referring to this.
apply =
inputBuildPlatform:
let
elaborated = lib.systems.elaborate inputBuildPlatform;
in
if lib.systems.equals elaborated cfg.hostPlatform then
cfg.hostPlatform # make identical, so that `==` equality works; see https://github.com/NixOS/nixpkgs/issues/278001
else
elaborated;
Copy link
Member

Choose a reason for hiding this comment

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

This one as well.

defaultText = lib.literalExpression ''config.nixpkgs.hostPlatform'';
description = ''
Specifies the platform on which NixOS should be built.
Expand All @@ -245,14 +236,11 @@ in
};

localSystem = lib.mkOption {
type = lib.types.attrs; # TODO utilize lib.systems.parsedPlatform
type = lib.types.attrs;
default = { inherit (cfg) system; };
example = {
system = "aarch64-linux";
};
# Make sure that the final value has all fields for sake of other modules
# referring to this. TODO make `lib.systems` itself use the module system.
apply = lib.systems.elaborate;
defaultText = lib.literalExpression ''(import "''${nixos}/../lib").lib.systems.examples.aarch64-multiplatform'';
description = ''
Systems with a recently generated `hardware-configuration.nix`
Expand Down Expand Up @@ -280,7 +268,7 @@ in
# is a relation between at least 2 systems in the context of a
# specific build step, not a single system.
crossSystem = lib.mkOption {
type = lib.types.nullOr lib.types.attrs; # TODO utilize lib.systems.parsedPlatform
type = lib.types.nullOr lib.types.attrs;
default = null;
example = {
system = "aarch64-linux";
Expand Down Expand Up @@ -416,6 +404,18 @@ in
${lib.concatMapStringsSep "\n" (file: " - ${file}") opt.config.files}
'';
}
{
assertion =
(opt.hostPlatform.isDefined -> builtins.isAttrs cfg.buildPlatform -> !(cfg.buildPlatform ? parsed))
Copy link
Member

Choose a reason for hiding this comment

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

cfg.buildPlatform must always be attrs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, nixos/modules/misc/nixpkgs/test.nix says otherwise - it sets nixpkgs.buildPlatform to different strings, too.

Copy link
Contributor

Choose a reason for hiding this comment

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

cfg.buildPlatform must always be attrs.

it sets nixpkgs.buildPlatform to different strings, too.

It could be defined as a string, but previously the apply function would always coerce the final value into attrs.

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess what we really need here is a lib.systems.partiallyElaborate, so that the apply function can be restored without violating the purpose of this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The important part is, that it must be possible to pass the inputs to the module (localSystem, crossSystem, buildPlatform, hostPlatform) onwards unchanged.

No matter what you do as apply, it will always break this premise, as soon as a second layer is involved. I tried adding rawValue in eec2100 to avoid touching apply, but that didn't work out.

But there is really no need to be able to access config.nixpkgs.buildPlatform etc. at all - you can get that information from pkgs.stdenv.buildPlatform etc, right?

Copy link
Contributor

@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.

But there is really no need to be able to access config.nixpkgs.buildPlatform etc. at all - you can get that information from pkgs.stdenv.buildPlatform etc, right?

It seems that up until now, it was intended for users to be able to read those options. But I agree, accessing pkgs.stdenv is better (and is what I do in my configs).

I'm trying to think about whether there's a sane way to enforce that though...

Obviously, the option descriptions should be updated with a note explaining they are write-only, and pkgs.stdenv should be read instead.

Perhaps an apply function could warn or throw when evaluated (similar to mkRemovedOptionModule), but internal usage could bypass the apply by merging the option definitions manually?

# Bypass `apply` function by merging `foo` manually:
(
  lib.modules.mergeDefinitions
    options.foo.loc
    options.foo.type
    options.foo.definitionsWithLocations
).mergedValue

&& (opt.hostPlatform.isDefined -> builtins.isAttrs cfg.hostPlatform -> !(cfg.hostPlatform ? parsed))
&& (builtins.isAttrs cfg.localSystem -> !(cfg.localSystem ? parsed))
&& (builtins.isAttrs cfg.crossSystem -> !(cfg.crossSystem ? parsed));
message = ''
Passing fully elaborated systems to `nixpkgs.localSystem`, `nixpkgs.crossSystem`, `nixpkgs.buildPlatform`
or `nixpkgs.hostPlatform` will break composability of package sets in nixpkgs. For example, pkgs.pkgsStatic
would not work in modules anymore.
'';
Comment on lines +413 to +417
Copy link
Contributor

Choose a reason for hiding this comment

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

In nixvim we had to bisect in order to find this PR; we had this error:

error: assertion '(((builtins).isAttrs localSystem) -> (! ((localSystem) ? parsed)))' failed
       at /nix/store/2cxciy0vg27dg357nm08gvgmr00j1f90-source/pkgs/top-level/default.nix:51:1

I guess this assertion is intended to solve this, but it seems that the top-level assertion is being checked before the assertions option is checked, because pkgs itself is needed before module-option assertions can be checked?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess this assertion is intended to solve this

Yes!

but it seems that the top-level assertion is being checked before the assertions option is checked, because pkgs itself is needed before module-option assertions can be checked?

Maybe... I could apply the assertion together with a no-op / identity function?

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe this would be better off being handled by option-type checking.

E.g. create a custom platformInputType with some checks for blacklisted attrs and some coercion from string to attrs.

That way the platform option definitions are checked by the module system and we can still normalise them to attrs to allow reading them if needed.

This could be implemented using a platformAttrsBlacklist. The blacklist could be used when checking option-types.

# might be simpler to use `lib.types.addCheck`
check =
  v:
  if builtins.isString v then
    true
  else if builtins.isAttrs v then
    # TODO: we could print the offending attr names here
    [ ] == builtins.filter (n: builtins.elem n platformAttrsBlacklist) (builtins.attrNames v)
  else
    false;

It could also be used to normalise the final value:

# could either be done in the option's `apply`, or the type's `merge` function
apply = platform: builtins.removeAttrs (lib.systems.elaborate platform) platformAttrsBlacklist;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Your suggestion implies that it would be correct to "take a fully elaborated system, remove forbidden attributes, and pass it to the nixos/nixpkgs module". This is not the case, unfortunately.

Some attributes are not incorrect per se, but can still be incorrectly passed on.

}
];
};

Expand Down
25 changes: 6 additions & 19 deletions nixos/modules/misc/nixpkgs/read-only.nix
Original file line number Diff line number Diff line change
Expand Up @@ -40,33 +40,20 @@ in
The Nixpkgs overlays that `pkgs` was initialized with.
'';
};
hostPlatform = mkOption {
internal = true;
readOnly = true;
description = ''
The platform of the machine that is running the NixOS configuration.
'';
};
buildPlatform = mkOption {
internal = true;
readOnly = true;
description = ''
The platform of the machine that built the NixOS configuration.
'';
};
# buildPlatform and hostPlatform left out on purpose:
# - They are not supposed to be changed with this read-only module.
# - They are not supposed to be read either, according to the description
# of "system" in the traditional nixpkgs module.
#
# NOTE: do not add the legacy options such as localSystem here. Let's keep
# this module simple and let module authors upgrade their code instead.
};
};
config = {
_module.args.pkgs =
# find mistaken definitions
builtins.seq cfg.config builtins.seq cfg.overlays builtins.seq cfg.hostPlatform builtins.seq
cfg.buildPlatform
cfg.pkgs;
builtins.seq cfg.config builtins.seq cfg.overlays cfg.pkgs;
nixpkgs.config = cfg.pkgs.config;
nixpkgs.overlays = cfg.pkgs.overlays;
nixpkgs.hostPlatform = cfg.pkgs.stdenv.hostPlatform;
nixpkgs.buildPlatform = cfg.pkgs.stdenv.buildPlatform;
};
}
8 changes: 6 additions & 2 deletions nixos/modules/virtualisation/nixos-containers.nix
Original file line number Diff line number Diff line change
Expand Up @@ -506,8 +506,12 @@ in
config = {
nixpkgs =
if options.nixpkgs?hostPlatform
then { inherit (host.pkgs.stdenv) hostPlatform; }
else { localSystem = host.pkgs.stdenv.hostPlatform; }
then {
hostPlatform =
if host.options.nixpkgs.hostPlatform.isDefined
then host.config.nixpkgs.hostPlatform
else lib.defaultTo host.config.nixpkgs.localSystem host.config.nixpkgs.crossSystem;
} else { localSystem = lib.defaultTo host.config.nixpkgs.localSystem host.config.nixpkgs.crossSystem; }
;
boot.isContainer = true;
networking.hostName = mkDefault name;
Expand Down
4 changes: 2 additions & 2 deletions nixos/tests/appliance-repart-image-verity-store.nix
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
verityStore = {
enable = true;
# by default the module works with systemd-boot, for simplicity this test directly boots the UKI
ukiPath = "/EFI/BOOT/BOOT${lib.toUpper config.nixpkgs.hostPlatform.efiArch}.EFI";
ukiPath = "/EFI/BOOT/BOOT${lib.toUpper pkgs.stdenv.hostPlatform.efiArch}.EFI";
};

name = "appliance-verity-store-image";
Expand All @@ -51,7 +51,7 @@
repartConfig = {
Type = "esp";
Format = "vfat";
SizeMinBytes = if config.nixpkgs.hostPlatform.isx86_64 then "64M" else "96M";
SizeMinBytes = if pkgs.stdenv.hostPlatform.isx86_64 then "64M" else "96M";
};
};
${partitionIds.store-verity}.repartConfig = {
Expand Down
4 changes: 2 additions & 2 deletions nixos/tests/appliance-repart-image.nix
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ in
"esp" = {
contents =
let
efiArch = config.nixpkgs.hostPlatform.efiArch;
efiArch = pkgs.stdenv.hostPlatform.efiArch;
in
{
"/EFI/BOOT/BOOT${lib.toUpper efiArch}.EFI".source =
Expand All @@ -70,7 +70,7 @@ in
# aarch64 kernel seems to generally be a little bigger than the
# x86_64 kernel. To stay on the safe side, leave some more slack
# for every platform other than x86_64.
SizeMinBytes = if config.nixpkgs.hostPlatform.isx86_64 then "64M" else "96M";
SizeMinBytes = if pkgs.stdenv.hostPlatform.isx86_64 then "64M" else "96M";
};
};
"root" = {
Expand Down
128 changes: 128 additions & 0 deletions pkgs/test/top-level/stage.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# run like this:
# nix-build pkgs/test/top-level/stage.nix
{
localSystem ? {
system = builtins.currentSystem;
},
}:

with import ../../top-level { inherit localSystem; };

let
# To silence platform specific evaluation errors
discardEvaluationErrors = e: (builtins.tryEval e).success -> e;

# Basic test for idempotency of the package set, i.e:
# Applying the same package set twice should work and
# not change anything.
isIdempotent = set: discardEvaluationErrors (pkgs.${set}.stdenv == pkgs.${set}.${set}.stdenv);

# Some package sets should be noops in certain circumstances.
# This is very similar to the idempotency test, but not going
# via the super' overlay.
isNoop =
parent: child:
discardEvaluationErrors (
(lib.getAttrFromPath parent pkgs).stdenv == (lib.getAttrFromPath parent pkgs).${child}.stdenv
);

allMuslExamples = builtins.attrNames (
lib.filterAttrs (_: system: lib.hasSuffix "-musl" system.config) lib.systems.examples
);

allLLVMExamples = builtins.attrNames (
lib.filterAttrs (_: system: system.useLLVM or false) lib.systems.examples
);

# A package set should only change specific configuration, but needs
# to keep all other configuration from previous layers in place.
# Each package set has one or more key characteristics for which we
# test here. Those should be kept, even when applying the "set" package
# set.
isComposable =
set:
(
# Can't compose two different libcs...
builtins.elem set [ "pkgsLLVMLibc" ]
|| discardEvaluationErrors (
pkgsCross.mingwW64.${set}.stdenv.hostPlatform.config == "x86_64-w64-mingw32"
)
)
&& (
# Can't compose two different libcs...
builtins.elem set [ "pkgsLLVMLibc" ]
|| discardEvaluationErrors (pkgsCross.mingwW64.${set}.stdenv.hostPlatform.libc == "msvcrt")
)
&& discardEvaluationErrors (pkgsCross.ppc64-musl.${set}.stdenv.hostPlatform.gcc.abi == "elfv2")
&& discardEvaluationErrors (
builtins.elem "trivialautovarinit" pkgs.pkgsExtraHardening.${set}.stdenv.cc.defaultHardeningFlags
)
&& discardEvaluationErrors (pkgs.pkgsLLVM.${set}.stdenv.hostPlatform.useLLVM)
&& (
# Can't compose two different libcs...
builtins.elem set [
"pkgsMusl"
"pkgsStatic"
]
|| discardEvaluationErrors (pkgs.pkgsLLVMLibc.${set}.stdenv.hostPlatform.isLLVMLibc)
)
&& discardEvaluationErrors (pkgs.pkgsArocc.${set}.stdenv.hostPlatform.useArocc)
&& discardEvaluationErrors (pkgs.pkgsZig.${set}.stdenv.hostPlatform.useZig)
&& discardEvaluationErrors (pkgs.pkgsLinux.${set}.stdenv.buildPlatform.isLinux)
&& (
# Can't compose two different libcs...
builtins.elem set [ "pkgsLLVMLibc" ]
|| discardEvaluationErrors (pkgs.pkgsMusl.${set}.stdenv.hostPlatform.isMusl)
)
&& discardEvaluationErrors (pkgs.pkgsStatic.${set}.stdenv.hostPlatform.isStatic)
&& discardEvaluationErrors (pkgs.pkgsi686Linux.${set}.stdenv.hostPlatform.isx86_32)
&& discardEvaluationErrors (pkgs.pkgsx86_64Darwin.${set}.stdenv.hostPlatform.isx86_64);
in

# Appends same defaultHardeningFlags again on each .pkgsExtraHardening - thus not idempotent.
# assert isIdempotent "pkgsExtraHardening";
# TODO: Remove the isDarwin condition, which currently results in infinite recursion.
# Also see https://github.com/NixOS/nixpkgs/pull/330567#discussion_r1894653309
assert (stdenv.hostPlatform.isDarwin || isIdempotent "pkgsLLVM");
# TODO: This currently results in infinite recursion, even on Linux
# assert isIdempotent "pkgsLLVMLibc";
assert isIdempotent "pkgsArocc";
assert isIdempotent "pkgsZig";
assert isIdempotent "pkgsLinux";
assert isIdempotent "pkgsMusl";
assert isIdempotent "pkgsStatic";
assert isIdempotent "pkgsi686Linux";
assert isIdempotent "pkgsx86_64Darwin";

assert isNoop [ "pkgsStatic" ] "pkgsMusl";
assert lib.all (sys: isNoop [ "pkgsCross" sys ] "pkgsMusl") allMuslExamples;
assert lib.all (sys: isNoop [ "pkgsCross" sys ] "pkgsLLVM") allLLVMExamples;

assert isComposable "pkgsExtraHardening";
assert isComposable "pkgsLLVM";
# TODO: Results in infinite recursion
# assert isComposable "pkgsLLVMLibc";
assert isComposable "pkgsArocc";
# TODO: unexpected argument 'bintools' - uncomment once https://github.com/NixOS/nixpkgs/pull/331011 is done
# assert isComposable "pkgsZig";
assert isComposable "pkgsMusl";
assert isComposable "pkgsStatic";
assert isComposable "pkgsi686Linux";

# Special cases regarding buildPlatform vs hostPlatform
assert discardEvaluationErrors (pkgsCross.gnu64.pkgsMusl.stdenv.hostPlatform.isMusl);
assert discardEvaluationErrors (pkgsCross.gnu64.pkgsi686Linux.stdenv.hostPlatform.isx86_32);
assert discardEvaluationErrors (pkgsCross.mingwW64.pkgsLinux.stdenv.hostPlatform.isLinux);
assert discardEvaluationErrors (
pkgsCross.aarch64-darwin.pkgsx86_64Darwin.stdenv.hostPlatform.isx86_64
);

# pkgsCross should keep upper cross settings
assert discardEvaluationErrors (
with pkgsStatic.pkgsCross.gnu64.stdenv.hostPlatform; isGnu && isStatic
);
assert discardEvaluationErrors (
with pkgsLLVM.pkgsCross.musl64.stdenv.hostPlatform; isMusl && useLLVM
);

emptyFile
6 changes: 3 additions & 3 deletions pkgs/top-level/all-packages.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1669,9 +1669,9 @@ with pkgs;
# pkgsCross.aarch64-multiplatform.freshBootstrapTools.build
freshBootstrapTools = if stdenv.hostPlatform.isDarwin then
callPackage ../stdenv/darwin/make-bootstrap-tools.nix {
localSystem = stdenv.buildPlatform;
localSystem = { config = lib.systems.parse.tripleFromSystem stdenv.buildPlatform; };
crossSystem =
if stdenv.buildPlatform == stdenv.hostPlatform then null else stdenv.hostPlatform;
if stdenv.buildPlatform == stdenv.hostPlatform then null else { config = lib.systems.parse.tripleFromSystem stdenv.hostPlatform; };
}
else if stdenv.hostPlatform.isLinux then
callPackage ../stdenv/linux/make-bootstrap-tools.nix {}
Expand Down Expand Up @@ -17732,7 +17732,7 @@ with pkgs;
[(
{ lib, ... }: {
config.nixpkgs.pkgs = lib.mkDefault pkgs;
config.nixpkgs.localSystem = lib.mkDefault stdenv.hostPlatform;
config.nixpkgs.localSystem = lib.mkDefault ({ config = lib.systems.parse.tripleFromSystem stdenv.hostPlatform; });
}
)] ++ (
if builtins.isList configuration
Expand Down
23 changes: 12 additions & 11 deletions pkgs/top-level/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
...
} @ args:

# Passing fully elaborated systems to localSystem or crossSystem will break composability
# of package sets.
assert builtins.isAttrs localSystem -> !(localSystem ? parsed);
assert builtins.isAttrs crossSystem -> !(crossSystem ? parsed);

let # Rename the function arguments
config0 = config;
crossSystem0 = crossSystem;
Expand Down Expand Up @@ -122,22 +127,18 @@ in let
config = lib.showWarnings configEval.config.warnings configEval.config;

# A few packages make a new package set to draw their dependencies from.
# (Currently to get a cross tool chain, or forced-i686 package.) Rather than
# give `all-packages.nix` all the arguments to this function, even ones that
# don't concern it, we give it this function to "re-call" nixpkgs, inheriting
# whatever arguments it doesn't explicitly provide. This way,
# `all-packages.nix` doesn't know more than it needs too.
# Rather than give `all-packages.nix` all the arguments to this function,
# even ones that don't concern it, we give it this function to "re-call"
# nixpkgs, inheriting whatever arguments it doesn't explicitly provide. This
# way, `all-packages.nix` doesn't know more than it needs to.
#
# It's OK that `args` doesn't include default arguments from this file:
# they'll be deterministically inferred. In fact we must *not* include them,
# because it's important that if some parameter which affects the default is
# substituted with a different argument, the default is re-inferred.
#
# To put this in concrete terms, this function is basically just used today to
# use package for a different platform for the current platform (namely cross
# compiling toolchains and 32-bit packages on x86_64). In both those cases we
# want the provided non-native `localSystem` argument to affect the stdenv
# chosen.
# To put this in concrete terms, we want the provided non-native `localSystem`
# and `crossSystem` arguments to affect the stdenv chosen.
#
# NB!!! This thing gets its `config` argument from `args`, i.e. it's actually
# `config0`. It is important to keep it to `config0` format (as opposed to the
Expand All @@ -146,7 +147,7 @@ in let
# via `evalModules` is not idempotent. In other words, if you add `config` to
# `newArgs`, expect strange very hard to debug errors! (Yes, I'm speaking from
# experience here.)
nixpkgsFun = newArgs: import ./. (args // newArgs);
nixpkgsFun = f0: import ./. (args // f0 args);

# Partially apply some arguments for building bootstraping stage pkgs
# sets. Only apply arguments which no stdenv would want to override.
Expand Down
Loading