Skip to content

Commit

Permalink
Add a way to pin a NixOS version within the module system.
Browse files Browse the repository at this point in the history
This modification add a way to re-evaluate the module system with a
different version of NixOS, or with a different set of arguments.
  • Loading branch information
nbp committed Nov 19, 2015
1 parent bd33a41 commit a5992ad
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 4 deletions.
16 changes: 12 additions & 4 deletions nixos/default.nix
@@ -1,27 +1,35 @@
{ configuration ? import ./lib/from-env.nix "NIXOS_CONFIG" <nixos-config>
, system ? builtins.currentSystem
, extraModules ? []
# This attribute is used to specify a different nixos version, a different
# system or additional modules which might be set conditionally.
, reEnter ? false
}:

let
reEnterModule = {
config.nixos.path = with (import ../lib); mkIf reEnter (mkForce null);
config.nixos.configuration = configuration;
};

eval = import ./lib/eval-config.nix {
inherit system;
modules = [ configuration ];
modules = [ configuration reEnterModule ] ++ extraModules;
};

inherit (eval) pkgs;

# This is for `nixos-rebuild build-vm'.
vmConfig = (import ./lib/eval-config.nix {
inherit system;
modules = [ configuration ./modules/virtualisation/qemu-vm.nix ];
modules = [ configuration reEnterModule ./modules/virtualisation/qemu-vm.nix ] ++ extraModules;
}).config;

# This is for `nixos-rebuild build-vm-with-bootloader'.
vmWithBootLoaderConfig = (import ./lib/eval-config.nix {
inherit system;
modules =
[ configuration
[ configuration reEnterModule
./modules/virtualisation/qemu-vm.nix
{ virtualisation.useBootLoader = true; }
];
Expand All @@ -30,7 +38,7 @@ let
in

{
inherit (eval) config options;
inherit (eval.config.nixos.reflect) config options;

system = eval.config.system.build.toplevel;

Expand Down
1 change: 1 addition & 0 deletions nixos/doc/manual/configuration/configuration.xml
Expand Up @@ -26,6 +26,7 @@ effect after you run <command>nixos-rebuild</command>.</para>

<!-- FIXME: auto-include NixOS module docs -->
<xi:include href="postgresql.xml" />
<xi:include href="nixos.xml" />

<!-- Apache; libvirtd virtualisation -->

Expand Down
1 change: 1 addition & 0 deletions nixos/doc/manual/default.nix
Expand Up @@ -55,6 +55,7 @@ let
cp -prd $sources/* . # */
chmod -R u+w .
cp ${../../modules/services/databases/postgresql.xml} configuration/postgresql.xml
cp ${../../modules/misc/nixos.xml} configuration/nixos.xml
ln -s ${optionsDocBook} options-db.xml
echo "${version}" > version
'';
Expand Down
20 changes: 20 additions & 0 deletions nixos/doc/manual/release-notes/rl-unstable.xml
Expand Up @@ -6,6 +6,26 @@

<title>Unstable</title>

<para>In addition to numerous new and upgraded packages, this release
has the following highlights:</para>

<itemizedlist>

<listitem>
<para>You can now pin a specific version of NixOS in your <filename>configuration.nix</filename>
by setting:

<programlisting>
nixos.path = ./nixpkgs-unstable-2015-12-06/nixos;
</programlisting>

This will make NixOS re-evaluate your configuration with the modules of
the specified NixOS version at the given path. For more details, see
<xref linkend="module-misc-nixos" /></para>
</listitem>

</itemizedlist>

<para>When upgrading from a previous release, please be aware of the
following incompatible changes:</para>

Expand Down
82 changes: 82 additions & 0 deletions nixos/modules/misc/nixos.nix
@@ -0,0 +1,82 @@
{ config, options, lib, ... }:

# This modules is used to inject a different NixOS version as well as its
# argument such that one can pin a specific version with the versionning
# system of the configuration.
let
nixosReentry = import config.nixos.path {
inherit (config.nixos) configuration extraModules;
inherit (config.nixpkgs) system;
reEnter = true;
};
in

with lib;

{
options = {
nixos.path = mkOption {

This comment has been minimized.

Copy link
@edolstra

edolstra Dec 15, 2015

Member

Having a nixos option prefix seems very strange to me, given that all NixOS options are about NixOS.

This comment has been minimized.

Copy link
@nbp

nbp Mar 11, 2016

Author Member

The main reason behind this choice was to avoif making path a top-level option, and this sounded nicer when compared to nixpkgs.

default = null;
example = literalExample "./nixpkgs-15.09/nixos";
type = types.nullOr types.path;
description = ''
This option give the ability to evaluate the current set of modules
with a different version of NixOS. This option can be used version
the version of NixOS with the configuration without relying on the
<literal>NIX_PATH</literal> environment variable.
'';
};

nixos.system = mkOption {

This comment has been minimized.

Copy link
@edolstra

edolstra Dec 15, 2015

Member

How does this interact with nixpkgs.system?

example = "i686-linux";
type = types.uniq types.str;
description = ''
Name of the system used to compile NixOS.
'';
};

nixos.extraModules = mkOption {
default = [];
example = literalExample "mkIf config.services.openssh.enable [ ./sshd-config.nix ]";
type = types.listOf types.unspecified;
description = ''
Define additional modules which would be loaded to evaluate the
configuration.
'';
};

nixos.configuration = mkOption {
type = types.unspecified;
internal = true;
description = ''
Option used by <filename>nixos/default.nix</filename> to re-inject
the same configuration module as the one used for the current
execution.
'';
};

nixos.reflect = mkOption {
default = { inherit config options; };
type = types.unspecified;
internal = true;
description = ''
Provides <literal>config</literal> and <literal>options</literal>
computed by the module system and given as argument to all
modules. These are used for introspection of options and
configuration by tools such as <literal>nixos-option</literal>.
'';
};
};

config = mkMerge [
(mkIf (config.nixos.path != null) (mkForce {
system.build.toplevel = nixosReentry.system;
system.build.vm = nixosReentry.vm;
nixos.reflect = { inherit (nixosReentry) config options; };
}))

{ meta.maintainers = singleton lib.maintainers.pierron;
meta.doc = ./nixos.xml;
}
];
}
84 changes: 84 additions & 0 deletions nixos/modules/misc/nixos.xml
@@ -0,0 +1,84 @@
<chapter xmlns="http://docbook.org/ns/docbook"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xi="http://www.w3.org/2001/XInclude"
version="5.0"
xml:id="module-misc-nixos">

<title>NixOS Reentry</title>

<!-- FIXME: render nicely -->

<!-- FIXME: source can be added automatically -->
<para><emphasis>Source:</emphasis> <filename>modules/misc/nixos.nix</filename></para>

<!-- FIXME: more stuff, like maintainer? -->

<para>NixOS reentry can be used for both pinning the evaluation to a

This comment has been minimized.

Copy link
@edolstra

edolstra Dec 15, 2015

Member

What does reentry mean? This term is not explained anywhere.

specific version of NixOS, and to dynamically add additional modules into
the Module evaluation.</para>

<section><title>NixOS Version Pinning</title>

<para>To pin a specific version of NixOS, you need a version that you can
either clone localy, or that you can fetch remotely.</para>

<para>If you already have a cloned version of NixOS in the directory
<filename>/etc/nixos/nixpkgs-16-03</filename>, then you can specify the
<option>nixos.path</option> with either the path or the relative path of
your NixOS clone. For example, you can add the following to your
<filename>/etc/nixos/configuration.nix</filename> file:

<programlisting>
nixos.path = ./nixpkgs-16-03/nixos;
</programlisting>
</para>

<para>Another option is to fetch a specific version of NixOS, with either
the <literal>fetchTarball</literal> builtin, or the
<literal>pkgs.fetchFromGithub</literal> function and use the result as an
input.

<programlisting>
nixos.path = "${builtins.fetchTarball https://github.com/NixOS/nixpkgs/archive/1f27976e03c15183191d1b4aa1a40d1f14666cd2.tar.gz}/nixos";
</programlisting>
</para>

</section>


<section><title>Adding Module Dynamically</title>

<para>To add additional module, the recommended way is to use statically
known modules in the list of imported arguments as described in <xref
linkend="sec-modularity" />. Unfortunately, this recommended method has
limitation, such that the list of imported files cannot be selected based on
the content of the configuration.

This comment has been minimized.

Copy link
@edolstra

edolstra Dec 15, 2015

Member

=> '</para>'.

Fortunately, NixOS reentry system can be used as an alternative to register
new imported modules based on the content of the configuration. To do so,
one should define both <option>nixos.path</option> and
<option>nixos.extraModules</option> options.

<programlisting>
nixos.path = &lt;nixos&gt;;
nixos.extraModules =
if config.networking.hostName == "server" then
[ ./server.nix ] else [ ./client.nix ];
</programlisting>

Also note, that the above can be reimplemented in a different way which is
not as expensive, by using <literal>mkIf</literal> at the top each
configuration if both modules are present on the file system (see <xref
linkend="sec-option-definitions" />) and by always inmporting both
modules.</para>

</section>

<section><title>Options</title>

<para>FIXME: auto-generated list of module options.</para>

</section>


</chapter>
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Expand Up @@ -52,6 +52,7 @@
./misc/lib.nix
./misc/locate.nix
./misc/meta.nix
./misc/nixos.nix
./misc/nixpkgs.nix
./misc/passthru.nix
./misc/version.nix
Expand Down
1 change: 1 addition & 0 deletions nixos/release.nix
Expand Up @@ -276,6 +276,7 @@ in rec {
tests.networkingProxy = callTest tests/networking-proxy.nix {};
tests.nfs3 = callTest tests/nfs.nix { version = 3; };
tests.nfs4 = callTest tests/nfs.nix { version = 4; };
tests.nixosPinVersion = callTest tests/nixos-pin-version.nix {};
tests.nsd = callTest tests/nsd.nix {};
tests.openssh = callTest tests/openssh.nix {};
tests.panamax = hydraJob (import tests/panamax.nix { system = "x86_64-linux"; });
Expand Down
57 changes: 57 additions & 0 deletions nixos/tests/nixos-pin-version.nix
@@ -0,0 +1,57 @@
{ system ? builtins.currentSystem }:

with import ../lib/testing.nix { inherit system; };
let
in

pkgs.stdenv.mkDerivation rec {
name = "nixos-pin-version";
src = ../..;
buildInputs = with pkgs; [ nix gnugrep ];

withoutPath = pkgs.writeText "configuration.nix" ''
{
nixos.extraModules = [ ({lib, ...}: { system.nixosRevision = lib.mkForce "ABCDEF"; }) ];
}
'';

withPath = pkgs.writeText "configuration.nix" ''
{
nixos.path = ${src}/nixos ;
nixos.extraModules = [ ({lib, ...}: { system.nixosRevision = lib.mkForce "ABCDEF"; }) ];
}
'';

phases = "buildPhase";
buildPhase = ''
datadir="${pkgs.nix}/share"
export TEST_ROOT=$(pwd)/test-tmp
export NIX_STORE_DIR=$TEST_ROOT/store
export NIX_LOCALSTATE_DIR=$TEST_ROOT/var
export NIX_LOG_DIR=$TEST_ROOT/var/log/nix
export NIX_STATE_DIR=$TEST_ROOT/var/nix
export NIX_DB_DIR=$TEST_ROOT/db
export NIX_CONF_DIR=$TEST_ROOT/etc
export NIX_MANIFESTS_DIR=$TEST_ROOT/var/nix/manifests
export NIX_BUILD_HOOK=
export PAGER=cat
cacheDir=$TEST_ROOT/binary-cache
nix-store --init
export NIX_PATH="nixpkgs=$src:nixos=$src/nixos:nixos-config=${withoutPath}" ;
if test $(nix-instantiate $src/nixos -A config.system.nixosRevision --eval-only) != '"ABCDEF"' ; then :;
else
echo "Unexpected re-entry without the nixos.path option defined.";
exit 1;
fi;
export NIX_PATH="nixpkgs=$src:nixos=$src/nixos:nixos-config=${withPath}" ;
if test $(nix-instantiate $src/nixos -A config.system.nixosRevision --eval-only) = '"ABCDEF"' ; then :;
else
echo "Expected a re-entry when the nixos.path option is defined.";
exit 1;
fi;
touch $out;
'';
}

5 comments on commit a5992ad

@edolstra
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this is a good idea:

  • It's expensive: you need to evaluate the config once just to get nixos.path, and then again to evaluate the system.
  • There is no guarantee that the current Nixpkgs can evaluate the config. E.g. if <nixpkgs> is 14.12 and nixos.path is 16.12, and the config uses 16.12 features.
  • It only allows overriding <nixpkgs>, but no other $NIX_PATH entries.
  • The "re-entry" terminology is unclear and exposes an implementation detail.
  • The re-entry evaluates system.build.toplevel and system.build.vm but not other top-level attributes that might be used (e.g. system.build.amazonImage).

I think it would be better to have a more direct way to specify a $NIX_PATH to nixos-rebuild (e.g. it could read the desired path from /etc/nixos/path or something like that).

@edolstra
Copy link
Member

Choose a reason for hiding this comment

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

Or nixos-rebuild could evaluate nix.nixPath and add it $NIX_PATH for the final system evaluation, like like it already evaluates nix.package to get the Nix package. This does suffer from problem 2 though.

Another solution that avoids all these problems is to have nixos-rebuild build a file named /etc/nixos/system.nix (or something like that) if it exists, which would be responsible for returning the same result as <nixpkgs/nixos> (i.e. the evaluated NixOS system). E.g.

import "${fetchTarball https://github.com/NixOS/nixpkgs/<hash>.tar.gz}/nixos"
  { configuration = 
      { config, pkgs, ... }:
      { ... };
  }

It could also use scopedImport to set a Nix search path declaratively.

@domenkozar
Copy link
Member

Choose a reason for hiding this comment

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

+100 for nix.nixPath. That would pretty much remove the channel state needed for different nixops deployments and would make everything together declarative.

@edolstra
Copy link
Member

Choose a reason for hiding this comment

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

Note that in NixOps you can already avoid a dependency on the channel state by doing nixops create/modify -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/<commit>.tar.gz.

@nbp
Copy link
Member Author

@nbp nbp commented on a5992ad Mar 11, 2016

Choose a reason for hiding this comment

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

It's expensive: you need to evaluate the config once just to get nixos.path, and then again to evaluate the system.

Yes, we would parse all the files twice, but we only evaluate the nixos options to resolve the new resolution when needed. Thus, this should not evaluate the configuration twice, as we only request the configuration from the pinned version.

There is no guarantee that the current Nixpkgs can evaluate the config. E.g. if is 14.12 and nixos.path is 16.12, and the config uses 16.12 features.

This feature is made to pin a specific version of nixos, knowing that all other options are ignored, the configuration should be the configuration of the pinned version.

Which is why we should use the pkgs, or relative path within nixpkgs and not the bracket notation.
As pkgs is defined relative to nixos.path.

It only allows overriding , but no other $NIX_PATH entries.

There is no need, because you can add extra modules to the sub-evaluation. Extra modules can carry all the information without the need for NIX_PATH.

Note, adding nix.nixPath using scopedImport to this re-entry mechanism should be easy.

Please sign in to comment.