diff --git a/setup.py b/setup.py index 736fda63..b464ed74 100644 --- a/setup.py +++ b/setup.py @@ -6,12 +6,12 @@ # importlib-metadata dependency can be removed when RHEL8 and other 3.6 based systems are not in support cycles install_requires = [ + 'ConfigArgParse', 'empy', 'importlib-metadata; python_version < "3.8"', 'pexpect', 'packaging', 'urllib3', - 'pyyaml', ] # docker API used to be in a package called `docker-py` before the 2.0 release diff --git a/src/rocker/cli.py b/src/rocker/cli.py index 2dc0e255..b8b90047 100644 --- a/src/rocker/cli.py +++ b/src/rocker/cli.py @@ -12,10 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import argparse +import configargparse import os import sys -import yaml from .core import DockerImageGenerator from .core import get_rocker_version @@ -28,15 +27,17 @@ def main(): - parser = argparse.ArgumentParser( + parser = configargparse.ArgumentParser( description='A tool for running docker with extra options', - formatter_class=argparse.ArgumentDefaultsHelpFormatter) + formatter_class=configargparse.ArgumentDefaultsHelpFormatter, + ignore_unknown_config_file_keys=False) parser.add_argument('image') parser.add_argument('command', nargs='*', default='') - parser.add_argument('--config', help='''Optional yaml file to handle command line arguments - (except positional args) as a config file. This config will override any other command line - arguments of the same name as the yaml keys (e.g. "--user-override-name" would have the key - "user_override_name" in the config file)''') + parser.add_argument('-c', '--config', is_config_file=True, + help='''Optional congig file to handle command line arguments (except positional args) + as a config file. Any setting in the config file will be overriden by the + command line arguments of the same name (e.g. '--user-override-name' would + override the key 'user_override_name' in the config file)''') parser.add_argument('--noexecute', action='store_true', help='Deprecated') parser.add_argument('--nocache', action='store_true') parser.add_argument('--nocleanup', action='store_true', help='do not remove the docker container when stopped') @@ -54,12 +55,8 @@ def main(): args = parser.parse_args() args_dict = vars(args) - - # Load config file if provided - if args.config: - with open(args.config, 'r') as f: - config = yaml.safe_load(f) - args_dict.update(config) + print(args) + print(args_dict) if args.noexecute: from .core import OPERATIONS_DRY_RUN @@ -86,7 +83,7 @@ def main(): def detect_image_os(): - parser = argparse.ArgumentParser(description='Detect the os in an image') + parser = configargparse.ArgumentParser(description='Detect the os in an image') parser.add_argument('image') parser.add_argument('--verbose', action='store_true', help='Display verbose output of the process') diff --git a/src/rocker/ros_ws.py b/src/rocker/ros_ws.py index b8f469c1..709ebf20 100644 --- a/src/rocker/ros_ws.py +++ b/src/rocker/ros_ws.py @@ -1,68 +1,81 @@ -import em +"""Extension for handling ROS workspaces in Rocker containers.""" + import copy -import pkgutil -import getpass import os +import tempfile +import xml.etree.ElementTree as ET +import pkgutil + +import em +from vcstools import VcsClient + from rocker.core import get_user_name from rocker.extensions import RockerExtension from rocker.volume_extension import Volume -import tempfile -from vcstools import VcsClient -import xml.etree.ElementTree as ET -import yaml class RosWs(RockerExtension): + """ + Extension for handling ROS workspaces in Rocker containers. + + This extension enables mounting and building ROS workspaces inside Docker + containers, with support for dependency management and workspace + configuration. + """ name = "ros_ws" @classmethod def get_name(cls): + """Return the extension's name.""" return cls.name def __init__(self): + """Initialize the extension.""" self._env_subs = None self.name = RosWs.get_name() - + @staticmethod def is_workspace_volume(workspace): + """Check if the workspace is a valid volume.""" if os.path.isdir(os.path.expanduser(workspace)): return True else: return False - def get_docker_args(self, cli_args): - """ - @param cli_args: {'volume': [[%arg%]]} - - 'volume' is fixed. - - %arg% can be: - - %path_host%: a path on the host. Same path will be populated in - the container. - - %path_host%:%path_cont% - - %path_host%:%path_cont%:%option% - """ - workspace = cli_args[self.name] + def get_docker_args(self, cliargs): + """Get Docker arguments for mounting the workspace volume.""" + workspace = cliargs[self.name] if RosWs.is_workspace_volume(workspace): - args = Volume.get_volume_args([[os.path.expanduser(workspace) + ":" + os.path.join(RosWs.get_home_dir(cli_args), self.name, 'src')]]) + args = Volume.get_volume_args( + [[os.path.expanduser(workspace) + ":" + + os.path.join(RosWs.get_home_dir(cliargs), + self.name, 'src')]]) return ' '.join(args) else: return '' - def precondition_environment(self, cli_args): + def precondition_environment(self, cliargs): + """Prepare the environment before running the container.""" pass - def validate_environment(self, cli_args): + def validate_environment(self, cliargs): + """Validate the environment before running the container.""" pass - def get_preamble(self, cli_args): + def get_preamble(self, cliargs): + """Return the preamble for the Dockerfile.""" return "" - def get_files(self, cli_args): - def get_files_from_path(path, only_ros_pacakges=False, is_ros_package=False): + def get_files(self, cliargs): + """Get the files to be included in the Docker build context.""" + def get_files_from_path(path, + only_ros_pacakges=False, + is_ros_package=False): if os.path.isdir(path): - if ( - not os.path.basename(path) == ".git" - ): # ignoring the .git directory allows the docker build context to cache the build context if the directories haven't been modified + # ignoring the .git directory allows the docker build context + # to cache the build context if the directories weren't modified + if (not os.path.basename(path) == ".git"): if not is_ros_package: is_ros_package = os.path.exists(os.path.join(path, 'package.xml')) for basename in os.listdir(path): @@ -89,7 +102,7 @@ def generate_ws_files(dir, only_ros_pacakges=False): ws_files[filepath.replace(os.path.expanduser(dir), "ros_ws_src" + os.path.sep)] = f.read() return ws_files - workspace = cli_args[self.name] + workspace = cliargs[self.name] if self.is_workspace_volume(workspace): return generate_ws_files(workspace, only_ros_pacakges=True) else: @@ -100,7 +113,7 @@ def generate_ws_files(dir, only_ros_pacakges=False): raise ValueError("Workspace file not currently supported") with tempfile.TemporaryDirectory() as td: - workspace_file = cli_args[self.name] + workspace_file = cliargs[self.name] with open(workspace_file, "r") as f: repos = yaml.safe_load(f) for repo in repos: @@ -157,17 +170,17 @@ def get_rosdeps(workspace): return sorted(deps - src_packages) @staticmethod - def get_home_dir(cli_args): - if cli_args["user"]: + def get_home_dir(cliargs): + if cliargs["user"]: return os.path.join(os.path.sep, "home", get_user_name()) else: return os.path.join(os.path.sep, "root") - def get_snippet(self, cli_args): + def get_snippet(self, cliargs): args = {} - args["home_dir"] = RosWs.get_home_dir(cli_args) - args["rosdeps"] = RosWs.get_rosdeps(cli_args[self.name]) - args["install_deps"] = cli_args["ros_ws_install_deps"] + args["home_dir"] = RosWs.get_home_dir(cliargs) + args["rosdeps"] = RosWs.get_rosdeps(cliargs[self.name]) + args["install_deps"] = cliargs["ros_ws_install_deps"] snippet = pkgutil.get_data( "rocker", @@ -175,14 +188,14 @@ def get_snippet(self, cli_args): ).decode("utf-8") return em.expand(snippet, args) - def get_user_snippet(self, cli_args): + def get_user_snippet(self, cliargs): args = {} - args["home_dir"] = RosWs.get_home_dir(cli_args) - args["rosdeps"] = RosWs.get_rosdeps(cli_args[self.name]) - args["build_source"] = cli_args["ros_ws_build_source"] - args["install_deps"] = cli_args["ros_ws_install_deps"] - args["ros_master_uri"] = cli_args["ros_ws_ros_master_uri"] - args["build_tool_args"] = cli_args["ros_ws_build_tool_args"] + args["home_dir"] = RosWs.get_home_dir(cliargs) + args["rosdeps"] = RosWs.get_rosdeps(cliargs[self.name]) + args["build_source"] = cliargs["ros_ws_build_source"] + args["install_deps"] = cliargs["ros_ws_install_deps"] + args["ros_master_uri"] = cliargs["ros_ws_ros_master_uri"] + args["build_tool_args"] = cliargs["ros_ws_build_tool_args"] snippet = pkgutil.get_data( "rocker", diff --git a/src/rocker/volume_extension.py b/src/rocker/volume_extension.py index b578de0c..a91463de 100644 --- a/src/rocker/volume_extension.py +++ b/src/rocker/volume_extension.py @@ -45,7 +45,7 @@ def get_volume_args(cls, volume_args): for volume in volumes: elems = volume.split(':') - host_dir = os.path.abspath(elems[0]) + host_dir = os.path.abspath(os.path.expanduser(elems[0])) if len(elems) == 1: args.append('{0} {1}:{1}'.format(cls.ARG_DOCKER_VOLUME, host_dir)) elif len(elems) == 2: