1. Packaging the asyn EPICS support module

Sometimes you might want to depend on an EPICS support module that isn’t packaged in EPNix.

In this tutorial, we’ll package the asyn support module version 4-45 step by step. The asyn support module is already packaged in EPNix, but for this tutorial, we’ll pretend it doesn’t exist.

Combined with Nix, asyn-4-45 has some packaging quirks common to some EPICS support modules, which is why we’re using it as an example.

Packaging an EPICS support module is similar to writing the ioc.nix file for an EPNix top. After all, both asyn and EPNix tops are EPICS tops.

1.1. Prerequisites

Before contributing to EPNix you need to follow the contributing Prerequisites, which covers forking EPNix and creating branches.

1.2. Create the package definition

Create a new directory under pkgs/support/by-name/, called my-asyn for this tutorial.

In this directory, create a package.nix file with the following template content:

pkgs/support/by-name/my-asyn/package.nix — EPNix support module template
{
  epnixLib,
  mkEpicsPackage,
  local_config_site ? { },
  local_release ? { },
}:
mkEpicsPackage {
  pname = "TODO";
  version = "TODO";
  varname = "TODO";

  src = TODO;

  inherit local_config_site local_release;

  # For EPICS, native libraries need to be in both
  # nativeBuildInputs and buildInputs
  nativeBuildInputs = [ ];
  buildInputs = [ ];

  # EPICS support modules can only be in propagatedBuildInputs
  propagatedBuildInputs = [ ];

  meta = {
    description = "TODO";
    homepage = "TODO";
    license = TODO;
    maintainers = with epnixLib.maintainers; [ ];
  };
}

Remember to git add this file.

1.3. Filling out the template

Next, fill out those TODOs. Here are the first fields to complete:

pname

The name of the package. Here, it’s "my-asyn".

version

The version of the package’s source code. Here, it’s "4-45".

varname

The variable name EPNix puts in the configure/RELEASE.local file of dependent packages. It must be unique across all EPNix packages.

Usually, it’s the package name in SCREAMING_SNAKE_CASE. In this tutorial, it’s "MY_ASYN".

meta.description

A description of the package. We recommend using the official description or tagline of the project.

meta.homepage

A URL to the project’s homepage. Here, it’s "https://epics-modules.github.io/master/asyn/".

meta.license

The license of the project.

For projects under the “EPICS” license, set epnixLib.licenses.epics. For other, more common licenses, it’s lib.licenses.license.

A list of available license names can be found in Nixpkgs’ lib/licenses.nix file.

In this tutorial, use epnixLib.licenses.epics.

meta.maintainers

A list of people maintaining this package. In this tutorial, it can stay empty, but in a real package, you must fill it in.

After completing these fields, your package file should look like this:

pkgs/support/by-name/my-asyn/package.nix — Filling out the metadata
{
  epnixLib,
  mkEpicsPackage,
  local_config_site ? { },
  local_release ? { },
}:
mkEpicsPackage {
  pname = "my-asyn";
  version = "4-45";
  varname = "MY_ASYN";

  src = TODO;

  inherit local_config_site local_release;

  # For EPICS, native libraries need to be in both
  # nativeBuildInputs and buildInputs
  nativeBuildInputs = [ ];
  buildInputs = [ ];

  # EPICS support modules can only be in propagatedBuildInputs
  propagatedBuildInputs = [ ];

  meta = {
    description = "Here is a fancy description of my asyn package";
    homepage = "https://epics-modules.github.io/master/asyn/";
    license = epnixLib.licenses.epics;
    maintainers = with epnixLib.maintainers; [ johndoe ];
  };
}

1.4. Fetching the source code

The asyn source code is hosted on GitHub at https://github.com/epics-modules/asyn.

asyn also tags its releases in the RX-Y format. For this tutorial, we’re interested in the tag R4-45.

For GitHub sources, Nixpkgs provides a fetchFromGitHub function, which you can use like this:

pkgs/support/by-name/my-asyn/package.nix — Fetching the source code
{
  epnixLib,
  mkEpicsPackage,
  fetchFromGitHub,
  local_config_site ? { },
  local_release ? { },
}:
mkEpicsPackage {
  pname = "my-asyn";
  version = "4-45";
  varname = "MY_ASYN";

  src = fetchFromGitHub {
    owner = "epics-modules";
    repo = "asyn";
    tag = "R4-45";
    hash = "";
  };

  inherit local_config_site local_release;

  # For EPICS, native libraries need to be in both
  # nativeBuildInputs and buildInputs
  nativeBuildInputs = [ ];
  buildInputs = [ ];

  # EPICS support modules can only be in propagatedBuildInputs
  propagatedBuildInputs = [ ];

  meta = {
    description = "Here is a fancy description of my asyn package";
    homepage = "https://epics-modules.github.io/master/asyn/";
    license = epnixLib.licenses.epics;
    maintainers = with epnixLib.maintainers; [ johndoe ];
  };
}

Note

You might notice that the hash field is empty.

This is a common development pattern in Nix: when you don’t know the hash ahead of time, set it to the empty string. When you build this package, Nix will tell you what the hash should be.

Be sure to git add this newly added file, and build your package with:

Building your my-asyn EPICS support package
nix build -L ".#support/my-asyn"

You should see Nix fail to build the package, with an error like this:

warning: Git tree '/path/to/EPNix' is dirty
warning: found empty hash, assuming 'sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
source>
source> trying https://github.com/epics-modules/asyn/archive/refs/tags/R4-45.tar.gz
source>   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
source>                                  Dload  Upload   Total   Spent    Left  Speed
source>   0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
source> 100 1453k    0 1453k    0     0  1563k      0 --:--:-- --:--:-- --:--:-- 15.0M
source> unpacking source archive /build/download.tar.gz
error: hash mismatch in fixed-output derivation '/nix/store/crhdrjqciq1n84s3azi7gjz49w16mxkz-source.drv':
         specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
            got:    sha256-VOHgDuRSj3dUmCWX+nyCf/i+VNGpC0ZsyIP0qBUG0vw=
error: Cannot build '/nix/store/rqa8lfsx8qrxd53g7sa1w0rxvhgba9g3-my-asyn-4-45.drv'.
       Reason: 1 dependency failed.
       Output paths:
         /nix/store/jmrig6qsh0ygy6q45jg17y9jjyq8jsx6-my-asyn-4-45

Here, Nix downloads the asyn source code tarball and fails to validate the hash. Thankfully, Nix tells you the correct hash, so you can update your package:

pkgs/support/by-name/my-asyn/package.nix — Updating the source hash
  # ...

  src = fetchFromGitHub {
    owner = "epics-modules";
    repo = "asyn";
    tag = "R4-45";
    hash = "sha256-VOHgDuRSj3dUmCWX+nyCf/i+VNGpC0ZsyIP0qBUG0vw=";
  };

  # ...

1.5. Adding dependencies

To determine what a package depends on, it’s best to consult its documentation if available. If the documentation doesn’t provide this information, you can inspect the package’s source code and read build errors to identify dependencies.

For asyn-4-45, not all dependencies are documented, so we’ll need to investigate.

1.5.1. Dependency types

In the current version of your package, you can notice several types of dependencies, such as nativeBuildInputs and propagatedBuildInputs.

These dependency types relate to their roles during cross-compilation and determine whether dependencies should be propagated to dependent packages.

For a summary on how to add dependencies to an EPICS top, read the Dependencies guide. For a detailed explanation of these dependency types, read Nix dependency types.

1.5.2. Adding EPICS dependencies

To find EPICS dependencies if unspecified in the documentation, you can look at asyn’s configure/RELEASE file.

Important

Make sure to inspect the source code from the R4-45 version.

From this source file, we see that asyn depends on epics-base which mkEpicsPackage includes by default.

It also optionally depends on ipac, sncseq from the seq package, calc, and transitively on sscan. You don’t need to specify transitive dependencies, as they’re automatically propagated.

In EPNix we try to include optional dependencies by default, so we’ll add them to our package. Since EPICS dependencies go into propagatedBuildInputs, edit your package file as follows:

pkgs/support/by-name/my-asyn/package.nix — Adding EPICS dependencies
{
  epnixLib,
  mkEpicsPackage,
  fetchFromGitHub,
  calc,
  ipac,
  seq,
  local_config_site ? { },
  local_release ? { },
}:
mkEpicsPackage {
  # ...

  propagatedBuildInputs = [
    calc
    ipac
    seq
  ];

  # ...
}

If you try to build it you’ll see at the beginning of the my-asyn build logs:

EPNix adding EPICS dependencies during the build
my-asyn> Running phase: configurePhase
my-asyn> ==============================
my-asyn> CONFIG_SITE.local
my-asyn> ------------------------------
my-asyn> GENVERSIONDEFAULT="EPNix"
my-asyn> GNU_DIR="/var/empty"
my-asyn>
my-asyn> ==============================
my-asyn> RELEASE.local
my-asyn> ------------------------------
my-asyn> undefine SUPPORT
my-asyn>
my-asyn> EPICS_BASE=/nix/store/<...>-epics-base-7.0.9
my-asyn> CALC=/nix/store/<...>-calc-3-7-5
my-asyn> SSCAN=/nix/store/<...>-sscan-2-11-6
my-asyn> IPAC=/nix/store/<...>-ipac-2.16
my-asyn> SNCSEQ=/nix/store/<...>-seq-2.2.9
my-asyn> ------------------------------

These are the lines added to CONFIG_SITE.local and RELEASE.local. We see that EPNix added the path to the EPICS dependencies we specified. The package still doesn’t build, but it’s progress.

1.5.3. Adding build tools

The current version of our my-asyn package doesn’t build, due to this error:

my-asyn> /nix/store/<...>-bash-5.2p37/bin/bash: line 1: rpcgen: command not found

The asyn build system tries to run the rpcgen command, but couldn’t find it.

To determine which package provides the rpcgen command, you can search for it online or use a package manager.

Using Nix, you can use the nix-index tool to find it:

# Install the 'nix-index' tool for your current user
nix-env -iA nixpkgs.nix-index
# Update the index database
nix-index
# Search for the given file
nix-locate --top-level /bin/rpcgen

The nix-locate command returns only one result: rpcsvc-proto. As explained in Dependency types, for EPICS packages, build tools go into nativeBuildInputs. Edit your package as follows:

pkgs/support/by-name/my-asyn/package.nix — Adding the rpcsvc-proto build tool
{
  epnixLib,
  mkEpicsPackage,
  fetchFromGitHub,
  rpcsvc-proto,
  calc,
  ipac,
  seq,
  local_config_site ? { },
  local_release ? { },
}:
mkEpicsPackage {
  # ...

  nativeBuildInputs = [ rpcsvc-proto ];

  # ...
}

If you try to build the package, you should see the build progress further.

1.5.4. Adding native dependencies

The current version of our my-asyn package still doesn’t build due to this error:

my-asyn> In file included from vxi11core_xdr.c:6:
my-asyn> vxi11core.h:9:10: fatal error: rpc/rpc.h: No such file or directory
my-asyn>     9 | #include <rpc/rpc.h>
my-asyn>       |          ^~~~~~~~~~~
my-asyn> compilation terminated.

This tells us that there’s a C library that asyn can’t find, specifically, a library that provides the rpc/rpc.h include file, but the build system didn’t have this file in its search path.

The first step to fixing this build failure is adding this library to the package’s dependencies.

To find which library provides this file comes from, you can:

  • Search for the filename online

  • Use the package manager to search for the file

  • Inspect the asyn source code

1.5.4.1. Searching by using the package manager

You can again use the nix-index tool to search for files contained in Nixpkgs packages:

nix-locate --top-level /rpc/rpc.h

The nix-locate command returns several packages. Example output:

python313Packages.torchWithVulkan.dev    164 r /nix/store/<...>-python3.13-torch-2.7.0-dev/include/torch/csrc/distributed/rpc/rpc.h
python313Packages.torch.dev              164 r /nix/store/<...>-python3.13-torch-2.7.0-dev/include/torch/csrc/distributed/rpc/rpc.h
python313Packages.torch-no-triton.dev    164 r /nix/store/<...>-python3.13-torch-2.7.0-dev/include/torch/csrc/distributed/rpc/rpc.h
<...>
python312Packages.autopxd2.out            55 r /nix/store/<...>-python3.12-python-autopxd2-2.5.0/lib/python3.12/site-packages/autopxd/stubs/darwin-include/rpc/rpc.h
ntirpc.dev                             3,613 r /nix/store/<...>-ntirpc-6.3-dev/include/ntirpc/rpc/rpc.h
livekit-libwebrtc.dev                  7,096 r /nix/store/<...>-livekit-libwebrtc-125-unstable-2025-03-24-dev/include/third_party/perfetto/src/trace_processor/rpc/rpc.h
libtirpc.dev                           4,130 r /nix/store/<...>-libtirpc-1.3.6-dev/include/tirpc/rpc/rpc.h
libtorch-bin.dev                         164 r /nix/store/<...>-libtorch-2.5.0-dev/include/torch/csrc/distributed/rpc/rpc.h

We can ignore the Python packages, since we’re looking for a C library. Searching these packages by using Nixpkgs’ package search, we can also eliminate other packages:

  • libtorch is for machine learning

  • livekit is for WebRTC

That leaves ntirpc and libtirpc as candidates.

Reading the README of ntirpc, we see references to libtirpc, which suggests that ntirpc is a fork of libtirpc. Therefore libtirpc is probably the correct library.

1.5.4.2. Searching the source code

Another way to find this library is by inspecting the asyn source code.

First, try searching for “RPC” in asyn’s Makefiles. Clone the asyn repository and switch to the R4-45 version:

Cloning the asyn source code.
git clone https://github.com/epics-modules/asyn
git switch -d R4-45

From the top directory of the asyn source code, run:

Search for RPC in every Makefile
grep rpc -i --include=Makefile -R .

From the results, you will see these lines:

All highlighted lines reference tirpc or libtirpc. This confirms the libtirpc is the package we want.

Another approach would be searching for the string INCLUDES, since variables for modifying the include path follow the pattern xxx_INCLUDES, according to EPICS’ Build facility specifications.

1.5.4.3. Editing the package

Now that we’ve found the dependency, edit your package. As explained in Dependency types, for EPICS packages native libraries go into both nativeBuildInputs and buildInputs.

pkgs/support/by-name/my-asyn/package.nix — Adding the libtirpc library
{
  epnixLib,
  mkEpicsPackage,
  fetchFromGitHub,
  rpcsvc-proto,
  libtirpc,
  calc,
  ipac,
  seq,
  local_config_site ? { },
  local_release ? { },
}:
mkEpicsPackage {
  # ...

  nativeBuildInputs = [ rpcsvc-proto libtirpc ];
  buildInputs = [ libtirpc ];

  # ...
}

If you try to build the package, you’ll see that although we added libtirpc as a dependency, the error wasn’t fixed.

1.6. Changing the CONFIG_SITE

One reason the package still doesn’t compile can be found in asyn’s configure/CONFIG_SITE file, specifically these lines:

configure/CONFIG_SITEasyn’s warning about libtirpc
# Some linux systems moved RPC related symbols to libtirpc
# To enable linking against this library, uncomment the following line
# TIRPC=YES

Since we had to add libtirpc to the dependencies, the comment applies.

Instead of uncommenting the file, we’ll write TIRPC=YES to configure/CONFIG_SITE.local using the special local_config_site argument of mkEpicsPackage:

pkgs/support/by-name/my-asyn/package.nix — Setting TIRPC=YES in asyn’s configure/CONFIG_SITE.local
# ...
mkEpicsPackage {
  # ...

  inherit local_release;
  local_config_site = local_config_site // {
    TIRPC = "YES";
  };

  # ...
}

You can see EPNix adding the variable at the beginning of the my-asyn build logs:

EPNix adding TIRPC=YES during the build
my-asyn> ==============================
my-asyn> CONFIG_SITE.local
my-asyn> ------------------------------
my-asyn> GENVERSIONDEFAULT="EPNix"
my-asyn> GNU_DIR="/var/empty"
my-asyn> TIRPC=YES
my-asyn>
my-asyn> ==============================

1.7. Patching the source code

The error rpc/rpc.h: No such file or directory is still present when building our package.

This error comes from how libtirpc is added to the include search path.

From our RPC search results in asyn’s source code, we see that the path /usr/include/tirpc is added as-is. This suggests that libtirpc must be installed globally, at the root directory of your system. However, this won’t work for Nix packages:

  • Nix builds packages in a sandbox, preventing builds from accessing global system files. This ensures you don’t forget specifying dependencies.

  • Nix installs packages in directories such as /nix/store/<...>-libtirpc-1.3.6-dev, not /usr.

We need to change asyn’s source code so its build system can find libtirpc.

1.7.1. Making changes

To locate libtirpc, we’ll use pkg-config, which is the de facto standard in the C/C++ world for finding libraries.

If you haven’t already, clone the asyn repository and switch to the R4-45 version:

Cloning the asyn source code.
git clone https://github.com/epics-modules/asyn
git switch -d R4-45

In the asyn source code, replace every instance of -I/usr/include/tirpc with a call to pkg-config --cflags:

Replacing hardcoded includes with calls to pkg-config
# Replace these kinds of lines:
USR_INCLUDES_Linux += -I/usr/include/tirpc
# With:
USR_INCLUDES_Linux += `pkg-config --cflags libtirpc`

1.7.2. Creating the patch

After you have made these changes in every relevant Makefile, generate a patch file:

Generating a patch file with your asyn changes
git diff --patch > use-pkg-config.patch

Copy this file into EPNix, in the same directory as your package, at pkgs/support/by-name/my-asyn/use-pkg-config.patch.

Remember to git add this file.

1.7.3. Using the patch

Next, we need to use that patch in the package and add pkg-config to the package’s dependencies.

Since pkg-config is a tool run during the build process, it goes into nativeBuildInputs.

pkgs/support/by-name/my-asyn/package.nix — Importing the patch
{
  epnixLib,
  mkEpicsPackage,
  fetchFromGitHub,
  pkg-config,
  rpcsvc-proto,
  libtirpc,
  calc,
  ipac,
  seq,
  local_config_site ? { },
  local_release ? { },
}:
mkEpicsPackage {
  # ...

  src = ...;

  patches = [ ./use-pkg-config.patch ];

  # ...

  nativeBuildInputs = [ pkg-config rpcsvc-proto libtirpc ];

  # ...
}

With these changes, the package should now build.

Tip

If you want to try the pkg-config command yourself, you can enter the my-asyn development shell just as you would for any EPNix IOC:

Manually trying the pkg-config command
nix develop '.#support/my-asyn'
pkg-config --cflags libtirpc

1.8. Complete example

pkgs/support/by-name/my-asyn/package.nix — Complete example
{
  epnixLib,
  mkEpicsPackage,
  fetchFromGitHub,
  pkg-config,
  rpcsvc-proto,
  libtirpc,
  calc,
  ipac,
  seq,
  sscan,
  local_config_site ? { },
  local_release ? { },
}:
mkEpicsPackage {
  pname = "my-asyn";
  version = "4-45";
  varname = "MY_ASYN";

  src = fetchFromGitHub {
    owner = "epics-modules";
    repo = "asyn";
    tag = "R4-45";
    hash = "sha256-VOHgDuRSj3dUmCWX+nyCf/i+VNGpC0ZsyIP0qBUG0vw=";
  };

  patches = [ ./use-pkg-config.patch ];

  inherit local_release;
  local_config_site = local_config_site // {
    TIRPC = "YES";
  };

  nativeBuildInputs = [
    pkg-config
    rpcsvc-proto
    libtirpc
  ];
  buildInputs = [ libtirpc ];
  propagatedBuildInputs = [
    calc
    ipac
    seq
    sscan
  ];

  meta = {
    description = "Here is a fancy description of my asyn package";
    homepage = "https://epics-modules.github.io/master/asyn/";
    license = epnixLib.licenses.epics;
    maintainers = with epnixLib.maintainers; [ ];
  };
}
pkgs/support/by-name/my-asyn/use-pkg-config.patch — Complete example
diff --git i/asyn/Makefile w/asyn/Makefile
index 3f803f48..20974179 100644
--- i/asyn/Makefile
+++ w/asyn/Makefile
@@ -15,7 +15,7 @@ ASYN = $(TOP)/asyn
 USR_CFLAGS += -DUSE_TYPED_RSET -DUSE_TYPED_DSET -DUSE_TYPED_DRVET
 USR_CPPFLAGS += -DUSE_TYPED_RSET -DUSE_TYPED_DSET -DUSE_TYPED_DRVET
 
-USR_INCLUDES_cygwin32 += -I/usr/include/tirpc
+USR_INCLUDES_cygwin32 += `pkg-config --cflags libtirpc`
 
 # The following gets rid of the -fno-implicit-templates flag on vxWorks, 
 # so we get automatic template instantiation.
@@ -33,7 +33,7 @@ asyn_SYS_LIBS_cygwin32 = $(CYGWIN_RPC_LIB)
 # Some linux systems moved RPC related symbols to libtirpc
 # Define TIRPC in configure/CONFIG_SITE in this case
 ifeq ($(TIRPC),YES)
-  USR_INCLUDES_Linux += -I/usr/include/tirpc
+  USR_INCLUDES_Linux += `pkg-config --cflags libtirpc`
   asyn_SYS_LIBS_Linux += tirpc
 endif
 
diff --git i/testGpibApp/src/Makefile w/testGpibApp/src/Makefile
index 20957003..e619dc28 100644
--- i/testGpibApp/src/Makefile
+++ w/testGpibApp/src/Makefile
@@ -43,7 +43,7 @@ testGpibVx_SRCS_vxWorks += testGpibVx_registerRecordDeviceDriver.cpp
 testGpib_LIBS += devTestGpib
 testGpib_LIBS += testSupport asyn
 ifeq ($(TIRPC),YES)
-  USR_INCLUDES += -I/usr/include/tirpc
+  USR_INCLUDES += `pkg-config --cflags libtirpc`
   testGpib_SYS_LIBS += tirpc
 endif
 SYS_PROD_LIBS_cygwin32 += $(CYGWIN_RPC_LIB)

1.9. External resources

Here are some resources to learn more about Nix packaging: