Migrating from modules development

Deprecated since version 25.05: Developing IOC using NixOS-like modules

Explanation

NixOS-like modules were used to define your IOC, for example:

Deprecated IOC definition
myEpnixConfig = {pkgs, ...}: {
  epnix = {
    inherit inputs;

    meta.name = "my-top";

    support.modules = with pkgs.epnix.support; [StreamDevice];

    checks.imports = [./checks/simple.nix];

    nixos.services.ioc = {
      app = "example";
      ioc = "iocExample";
    };
  };
};

This style of development is deprecated since EPNix version nixos-25.05 and will be removed in EPNix version nixos-26.05.

This style of development was deprecated because it led to complex logic inside of EPNix, and provided no tangible benefit. Moreover, support top IOCs are packaged differently inside of EPNix, in a style much more similar to what you can find in nixpkgs.

The newer way of developing IOCs is more similar to the Nix code you can find in the wild, which makes public documentation more applicable to EPNix developments.

Copying the new template

From the top directory of your IOC, move your flake.nix file and checks out of the way, and initialize the new template over your project:

Applying the new template
mv flake.nix flake.nix.old
mv checks checks.old
nix flake init -t epnix

Edit the new template

Flake

  • For every flake input that you added in your flake.nix.old file, add them in your new flake.nix file.

  • For every overlay that’s in your flake.nix.old’s nixpkgs.overlays attribute, add them in your new flake.nix file, in pkgsoverlays.

  • Change the name of your IOC by replacing every instance of myIoc in flake.nix.

Warning

If your top is used as an EPICS support top, your package will be located in a different attribute path.

For example, if your package was under pkgs.epnix.support.supportTop before, after the migration it will be exported under pkgs.supportTop.

IOC package

Edit the ioc.nix file to match your IOC:

  • Change the pname, version, and varname variables

  • Add your EPICS support modules dependencies into propagatedBuildInputs

  • Add your system libraries dependencies into both nativeBuildInputs and buildInputs

If you had buildConfig.attrs.something = value; defined in flake.nix.old, add something = value; to your ioc.nix file.

If you used applications.apps, see External apps (IEE).

Checks

For each checks.old/check.nix file, take the new checks/simple.nix as a base and:

  • replace myIoc with your the name of your IOC

  • make sure the name of your systemd.services.myIoc in checks.old/check.nix corresponds to services.iocs.myIoc in your new check

  • set your iocBoot directory by setting services.iocs.<name>.workingDirectory

  • copy the testScript from your old check into the new one

  • if you made changes to nodes or nodes.machine in your old check, add them to the new one

External apps (IEE)

If you defined external apps in flake.nix.old such as this:

Deprecated usage of external apps
application.apps = [
  "inputs.exampleApp"
];

You need to copy them manually in ioc.nix.

To do this, make sure you’ve re-added inputs.exampleApp to your new flake.nix, and pass your inputs as argument to your IOC:

flake.nix
 overlays.default = final: _prev: {
-  myIoc = final.callPackage ./ioc.nix {};
+  myIoc = final.callPackage ./ioc.nix { inherit inputs; };
 };
ioc.nix
 {
   mkEpicsPackage,
   lib,
   epnix,
+  inputs,
 }:
 mkEpicsPackage {
   pname = "myIoc";

Copy your apps manually, during the preConfigure phase. For example, if you have two apps exampleApp and otherExampleApp:

ioc.nix
#local_release = {
#  PCRE_INCLUDE = "${lib.getDev pcre}/include";
#  PCRE_LIB = "${lib.getLib pcre}/lib";
#};

preConfigure = ''
  echo "Copying exampleApp"
  cp -rTvf --no-preserve=mode ${inputs.exampleApp} ./exampleApp
  echo "Copying otherExampleApp"
  cp -rTvf --no-preserve=mode ${inputs.otherExampleApp} ./otherExampleApp
'';

meta = {
  description = "A description of my IOC";
  homepage = "<homepage URL>";
  # ...
};

NixOS machines

If you have in a single project both a NixOS configuration and an IOC, you need to adapt your code to package your IOC outside of NixOS modules.

The simplest way to do that is by separating your IOC into a new project, and follow the migration guide from there.

Complete example

Here is a complete example of a successful migration.

Before

flake.nix — Before
{
  description = "EPICS IOC for migration demonstration purposes";

  inputs.flake-utils.url = "github:numtide/flake-utils";
  inputs.epnix.url = "github:epics-extensions/epnix/nixos-24.11";

  inputs.mySupportModule = {
    url = "git+ssh://git@my-server.org/me/exampleApp.git";
    inputs.epnix.follows = "epnix";
  };

  inputs.exampleApp = {
    url = "git+ssh://git@my-server.org/me/exampleApp.git";
    flake = false;
  };

  outputs = {
    self,
    flake-utils,
    epnix,
    ...
  } @ inputs: let
    myEpnixConfig = {pkgs, ...}: {
      nixpkgs.overlays = [inputs.mySupportModule.overlays.default];

      epnix = {
        inherit inputs;

        meta.name = "myExampleTop";

        support.modules = with pkgs.epnix.support; [StreamDevice mySupportModule];
        applications.apps = ["inputs.exampleApp"];

        buildConfig.attrs.buildInputs = [pkgs.openssl];
        buildConfig.attrs.nativeBuildInputs = [pkgs.openssl];

        checks.imports = [./checks/simple.nix];

        nixos.services.myExampleIoc = {
          app = "myExample";
          ioc = "iocMyExample";
        };
      };
    };
  in
    # Add your supported systems here.
    # ---
    # "x86_64-linux" should still be specified so that the development
    # environment can be built on your machine.
    flake-utils.lib.eachSystem ["x86_64-linux"] (system: let
      epnixDistribution = epnix.lib.evalEpnixModules {
        nixpkgsConfig = {
          # This specifies the build architecture
          inherit system;

          # This specifies the host architecture, uncomment for cross-compiling
          #
          # The complete of example architectures is here:
          # https://github.com/NixOS/nixpkgs/blob/nixos-22.11/lib/systems/examples.nix
          # ---
          #crossSystem = epnix.inputs.nixpkgs.lib.systems.examples.armv7l-hf-multiplatform;
        };
        epnixConfig = myEpnixConfig;
      };
    in {
      packages =
        epnixDistribution.outputs
        // {
          default = self.packages.${system}.build;
        };

      inherit epnixDistribution;

      devShells.default = self.packages.${system}.devShell;

      checks = epnixDistribution.config.epnix.checks.derivations;
    })
    // {
      overlays.default = final: prev:
        self.epnixDistribution.x86_64-linux.generatedOverlay final prev;
    };
}
checks/simple.nix — Before
{
  epnix,
  epnixConfig,
  pkgs,
  ...
}:
pkgs.nixosTest {
  name = "simple";

  nodes.machine = {config, ...}: {
    imports = [
      epnix.nixosModules.ioc
      epnixConfig
    ];
    environment.systemPackages = [pkgs.epnix.epics-base];

    systemd.services.ioc = config.epnix.nixos.services.ioc.config;
  };

  testScript = ''
    machine.wait_for_unit("default.target")
    machine.wait_for_unit("ioc.service")

    machine.wait_until_succeeds("caget stringin", timeout=10)
    machine.wait_until_succeeds("caget stringout", timeout=10)
    machine.fail("caget non-existing")

    with subtest("testing stringout"):
      def test_stringout(_) -> bool:
        machine.succeed("caput stringout 'hello'")
        status, _output = machine.execute("caget -t stringout | grep -qxF 'hello'")

        return status == 0

      retry(test_stringout)

      assert "hello" not in machine.succeed("caget -t stringin")
  '';
}

After

flake.nix — After
{
  description = "EPICS IOC for migration demonstration purposes";

  inputs.flake-utils.url = "github:numtide/flake-utils";
  inputs.epnix.url = "github:epics-extensions/epnix/nixos-24.11";

  inputs.mySupportModule = {
    url = "git+ssh://git@my-server.org/me/exampleApp.git";
    inputs.epnix.follows = "epnix";
  };

  inputs.exampleApp = {
    url = "git+ssh://git@my-server.org/me/exampleApp.git";
    flake = false;
  };

  outputs = {
    self,
    flake-utils,
    epnix,
    ...
  } @ inputs:
  # Add your supported systems here.
  # ---
  # "x86_64-linux" should still be specified so that the development
  # environment can be built on your machine.
    flake-utils.lib.eachSystem ["x86_64-linux"] (system: let
      pkgs = import epnix.inputs.nixpkgs {
        inherit system;
        overlays = [
          epnix.overlays.default
          self.overlays.default

          inputs.mySupportModule.overlays.default
        ];
      };
    in {
      packages.default = pkgs.myIoc;

      checks = {
        simple = pkgs.callPackage ./checks/simple.nix {};
      };
    })
    // {
      overlays.default = final: _prev: {
        myIoc = final.callPackage ./ioc.nix {inherit inputs;};
      };
    };
}
flake.nix — After
{
  mkEpicsPackage,
  epnix,
  openssl,
  inputs,
}:
mkEpicsPackage {
  pname = "myExampleTop";
  version = "0.0.1";
  varname = "MY_EXAMPLE_TOP";

  src = ./.;

  buildInputs = [openssl];
  nativeBuildInputs = [openssl];

  propagatedBuildInputs = [
    epnix.support.StreamDevice
    epnix.support.mySupportModule
  ];

  preConfigure = ''
    echo "Copying exampleApp"
    cp -rTvf --no-preserve=mode ${inputs.exampleApp} ./exampleApp
  '';

  meta = {
    description = "EPICS IOC for migration demonstration purposes";
    homepage = "<homepage URL>";
  };
}
checks/simple.nix — After
{
  nixosTest,
  epnix,
  epnixLib,
  myIoc,
  ...
}:
nixosTest {
  name = "simple";

  nodes.machine = {
    imports = [epnixLib.inputs.self.nixosModules.nixos];
    environment.systemPackages = [epnix.epics-base];

    services.iocs.myExampleIoc = {
      package = myIoc;
      workingDirectory = "iocBoot/iocMyExample";
    };
  };

  testScript = ''
    machine.wait_for_unit("default.target")
    machine.wait_for_unit("ioc.service")

    machine.wait_until_succeeds("caget stringin", timeout=10)
    machine.wait_until_succeeds("caget stringout", timeout=10)
    machine.fail("caget non-existing")

    with subtest("testing stringout"):
      def test_stringout(_) -> bool:
        machine.succeed("caput stringout 'hello'")
        status, _output = machine.execute("caget -t stringout | grep -qxF 'hello'")

        return status == 0

      retry(test_stringout)

      assert "hello" not in machine.succeed("caget -t stringin")
  '';
}