diff --git a/modules/common.nix b/modules/common.nix
new file mode 100644
index 0000000000..a4f3bff6ce
--- /dev/null
+++ b/modules/common.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ ros1Cfg = config.services.ros;
+ ros2Cfg = config.services.ros2;
+in {
+ # Interface
+
+ # Implementation
+
+ config = mkIf (ros1Cfg.enable || ros2Cfg.enable) {
+ environment.etc."ros/rosdep/sources.list.d/20-default.list".source = pkgs.fetchurl {
+ url = "https://raw.githubusercontent.com/ros/rosdistro/225c14be89fdf7ecf028b4cf85fa82032f7728e1/rosdep/sources.list.d/20-default.list";
+ sha256 = "0kxknc42y01pci8fxzhg84ybhgqyxqimycck27vb4b282lqfkzj7";
+ };
+
+ users = {
+ users.ros = {
+ group = "ros";
+ isSystemUser = true;
+ };
+ groups.ros = { };
+ };
+ };
+}
diff --git a/modules/default.nix b/modules/default.nix
index 938104592e..9feb559ed4 100644
--- a/modules/default.nix
+++ b/modules/default.nix
@@ -1,7 +1,10 @@
{ ... }: {
imports = [
- ./core.nix
- ./ros.nix
- ./nodes.nix
+ ./common.nix
+ ./ros1/core.nix
+ ./ros1/ros.nix
+ ./ros1/nodes.nix
+ ./ros2/ros.nix
+ ./ros2/nodes.nix
];
}
diff --git a/modules/core.nix b/modules/ros1/core.nix
similarity index 100%
rename from modules/core.nix
rename to modules/ros1/core.nix
diff --git a/modules/nodes.nix b/modules/ros1/nodes.nix
similarity index 100%
rename from modules/nodes.nix
rename to modules/ros1/nodes.nix
diff --git a/modules/ros.nix b/modules/ros1/ros.nix
similarity index 84%
rename from modules/ros.nix
rename to modules/ros1/ros.nix
index 66d2e3e116..2d5971986f 100644
--- a/modules/ros.nix
+++ b/modules/ros1/ros.nix
@@ -80,7 +80,7 @@ in {
# FIXME: mkAfter is used to make sure the Python overlay is applied. That
# means all other user configured Python overlays are ignored. This needs a
# fix in nixpkgs: https://github.com/NixOS/nixpkgs/issues/44426
- nixpkgs.overlays = mkAfter (singleton (import ../overlay.nix));
+ nixpkgs.overlays = mkAfter (singleton (import ../../overlay.nix));
services.ros = {
pkgs = mkDefault (pkgs.rosPackages."${cfg.distro}".overrideScope cfg.overlays);
@@ -89,11 +89,6 @@ in {
masterUri = mkDefault "http://${cfg.hostname}:11311/";
};
- environment.etc."ros/rosdep/sources.list.d/20-default.list".source = pkgs.fetchurl {
- url = "https://raw.githubusercontent.com/ros/rosdistro/225c14be89fdf7ecf028b4cf85fa82032f7728e1/rosdep/sources.list.d/20-default.list";
- sha256 = "0kxknc42y01pci8fxzhg84ybhgqyxqimycck27vb4b282lqfkzj7";
- };
-
environment.variables = {
ROS_HOSTNAME = cfg.hostname;
ROS_MASTER_URI = cfg.masterUri;
@@ -106,13 +101,5 @@ in {
inherit paths;
extraOutputsToInstall = optional config.environment.enableDebugInfo "debug";
}) ];
-
- users = {
- users.ros = {
- group = "ros";
- isSystemUser = true;
- };
- groups.ros = { };
- };
};
}
diff --git a/modules/ros2/nodes.nix b/modules/ros2/nodes.nix
new file mode 100644
index 0000000000..6177ca3f8c
--- /dev/null
+++ b/modules/ros2/nodes.nix
@@ -0,0 +1,172 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.ros2;
+
+ commonServiceOptions = {
+ package = mkOption {
+ type = types.str;
+ description = ''
+ ROS package name containing this node or launch file. This is the ROS
+ name for the package, rather than the attribute name, meaning it uses
+ underscores rather than dashes.
+ '';
+ };
+
+ paths = mkOption {
+ type = types.listOf types.package;
+ description = ''
+ Additional paths to add to the environment of this node. The
+ option will be turned into an attribute
+ name and automatically added to this option if valid.
+ '';
+ };
+
+ env = mkOption {
+ type = types.package;
+ description = ''
+ Environment created with the ROS specific buildEnv function for
+ this node.
+ '';
+ };
+ };
+
+ commonServiceConfig = { name, config, ... }: {
+ # Try to convert package name to attribute and add it to the
+ # environment
+ paths = let
+ packageAttr = replaceStrings ["_"] ["-"] config.package;
+ in optional (hasAttr packageAttr cfg.pkgs) cfg.pkgs."${packageAttr}";
+
+ env = mkDefault (cfg.pkgs.buildEnv {
+ name = "ros-node-${name}-env";
+ inherit (config) paths;
+ });
+ };
+
+ serviceGenerator = execStartFn: services: mapAttrs' (name: config: nameValuePair name {
+ serviceConfig = {
+ Type = "exec";
+ StateDirectory = "ros";
+ User = "ros";
+ Group = "ros";
+ ExecStart = execStartFn config;
+ };
+ wantedBy = [ "multi-user.target" ];
+ environment = {
+ ROS_DOMAIN_ID = toString cfg.domainId;
+ ROS_HOME = "/var/lib/ros";
+ };
+ }) services;
+in {
+ # Interface
+
+ options.services.ros2 = {
+ nodes = mkOption {
+ type = types.attrsOf (types.submodule ({ name, config, ... }@args: {
+ options = commonServiceOptions // {
+ node = mkOption {
+ type = types.str;
+ description = ''
+ Name of the node to launch.
+ '';
+ };
+
+ args = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ description = ''
+ Arguments to pass to the node.
+ '';
+ };
+
+ rosArgs = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ description = ''
+ ROS specific arguments to pass to the node using --ros-args
+ '';
+ };
+
+ params = mkOption {
+ type = types.attrsOf types.str;
+ default = {};
+ description = ''
+ Key-value parameters to pass to the node.
+ '';
+ };
+ };
+
+ config = mkMerge [
+ (commonServiceConfig args)
+ {
+ paths = [ cfg.pkgs.ros2run ];
+ rosArgs = concatMap (k: v: [ "--param" "${k}:=${v}" ]) cfg.params;
+ }
+ ];
+ }));
+ default = {};
+ description = ''
+ ROS nodes to launch at boot as systemd services.
+ '';
+ };
+
+ launchFiles = mkOption {
+ type = types.attrsOf (types.submodule ({ name, config, ... }@args: {
+ options = commonServiceOptions // {
+ launchFile = mkOption {
+ type = types.str;
+ description = ''
+ Name of the launch file.
+ '';
+ };
+
+ args = mkOption {
+ type = types.attrsOf types.str;
+ default = {};
+ description = ''
+ Key-value arguments to pass to the launch file
+ '';
+ };
+
+ launchArgs = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ description = ''
+ Extra command line arguments to pass to ros2 launch.
+ '';
+ };
+ };
+
+ config = mkMerge [
+ (commonServiceConfig args)
+ { paths = with cfg.pkgs; [ cfg.pkgs.ros2launch ]; }
+ ];
+ }));
+ default = {};
+ description = ''
+ ROS launch files to start at boot as systemd services.
+ '';
+ };
+ };
+
+ # Implementation
+
+ config = mkIf cfg.enable {
+ systemd.services = mkMerge [
+ (serviceGenerator (config: escapeShellArgs (
+ [ "${config.env}/bin/ros2" "run" config.package config.node ] ++
+ config.args ++
+ [ "--ros-args" ] ++ config.rosArgs ++ [ "--" ]
+ )) cfg.nodes)
+ (serviceGenerator (config: escapeShellArgs (
+ [ "${config.env}/bin/ros2" "launch" ] ++
+ config.launchArgs ++
+ [ config.package config.launchFile ] ++
+ (mapAttrsToList (n: v: "${n}:=${v}") config.args)
+ )) cfg.launchFiles)
+ ];
+ };
+}
diff --git a/modules/ros2/ros.nix b/modules/ros2/ros.nix
new file mode 100644
index 0000000000..80e5782d64
--- /dev/null
+++ b/modules/ros2/ros.nix
@@ -0,0 +1,89 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.ros2;
+
+ pkgsType = mkOptionType {
+ name = "ros-packages";
+ description = "ROS package set";
+ check = p: isAttrs p && hasAttr "ament-cmake" p;
+ };
+ overlayType = mkOptionType {
+ name = "ros-overlay";
+ description = "ROS package set overlay";
+ check = isFunction;
+ };
+in {
+ # Interface
+
+ options.services.ros2 = {
+ enable = mkEnableOption "Robot Operating System, version 2";
+
+ distro = mkOption {
+ type = types.str;
+ default = "humble";
+ description = ''
+ ROS distro to use. Must be defined in distros/default.nix.
+ '';
+ };
+
+ pkgs = mkOption {
+ type = pkgsType;
+ description = ''
+ ROS package set for the selected distro.
+ '';
+ };
+
+ overlays = mkOption {
+ type = types.listOf overlayType;
+ default = [];
+ apply = composeManyExtensions;
+ description = ''
+ Set of package overlays to apply to ROS package set for the configured
+ distro.
+ '';
+ };
+
+ domainId = mkOption {
+ type = types.ints.between 0 232;
+ default = 0;
+ description = ''
+ DDS Domain ID that defines the ports used for communication between
+ processes.
+ '';
+ };
+
+ systemPackages = mkOption {
+ default = p: [];
+ example = literalExample "p: with p; [ ros2cli ros2run ]";
+ description = ''
+ Packages to add to a ROS environment that will be added to the system
+ PATH. The provided function will be passed the package set configured by
+ .
+ '';
+ };
+ };
+
+ # Implementation
+
+ config = mkIf cfg.enable {
+ # FIXME: mkAfter is used to make sure the Python overlay is applied. That
+ # means all other user configured Python overlays are ignored. This needs a
+ # fix in nixpkgs: https://github.com/NixOS/nixpkgs/issues/44426
+ nixpkgs.overlays = mkAfter (singleton (import ../../overlay.nix));
+
+ services.ros2.pkgs = mkDefault (pkgs.rosPackages."${cfg.distro}".overrideScope cfg.overlays);
+
+ environment.variables.ROS_DOMAIN_ID = toString cfg.domainId;
+
+ environment.systemPackages = let
+ paths = cfg.systemPackages cfg.pkgs;
+ in mkIf (length paths != 0) [ (cfg.pkgs.buildEnv {
+ name = "ros2-system-env";
+ inherit paths;
+ extraOutputsToInstall = optional config.environment.enableDebugInfo "debug";
+ }) ];
+ };
+}