From ed4095a34ce04c7a1cdd794e32ec0457e94b970e Mon Sep 17 00:00:00 2001 From: Felix Exner Date: Wed, 15 May 2024 10:02:33 +0200 Subject: [PATCH] First release --- .coveragerc | 4 + .github/workflows/coverage.yaml | 41 ++ .github/workflows/docs.yaml | 37 ++ .github/workflows/package.yaml | 32 ++ .github/workflows/pre-commit.yaml | 21 + .gitignore | 8 + .pre-commit-config.yaml | 18 + Docker/Dockerfile | 23 + Docker/README.md | 24 + Docker/entrypoint.sh | 5 + Docker/source_global.sh | 1 + LICENSE.txt | 17 + README.rst | 71 +++ bin/rob_folders-complete.sh | 53 +++ bin/rob_folders_get_source_command.py | 53 +++ bin/rob_folders_source.sh | 199 ++++++++ bin/source_environment.sh | 266 +++++++++++ docs/_static/robot_folders.svg | 441 ++++++++++++++++++ docs/aliases.rst | 24 + docs/conf.py | 29 ++ docs/configuration.rst | 62 +++ docs/faq.rst | 7 + docs/index.rst | 21 + docs/installation.rst | 68 +++ docs/misc_workspace.rst | 73 +++ docs/usage.rst | 253 ++++++++++ setup.py | 42 ++ src/robot_folders/__init__.py | 21 + src/robot_folders/commands/__init__.py | 21 + .../commands/active_environment.py | 46 ++ .../commands/adapt_environment.py | 415 ++++++++++++++++ src/robot_folders/commands/add_environment.py | 416 +++++++++++++++++ src/robot_folders/commands/cd.py | 93 ++++ .../commands/change_environment.py | 98 ++++ src/robot_folders/commands/clean.py | 81 ++++ .../commands/delete_environment.py | 165 +++++++ .../commands/get_checkout_base_dir.py | 30 ++ src/robot_folders/commands/make.py | 86 ++++ .../commands/manage_underlays.py | 50 ++ src/robot_folders/commands/run.py | 91 ++++ .../commands/scrape_environment.py | 160 +++++++ src/robot_folders/helpers/ConfigParser.py | 74 +++ src/robot_folders/helpers/__init__.py | 21 + src/robot_folders/helpers/build_helpers.py | 313 +++++++++++++ src/robot_folders/helpers/clean_helpers.py | 116 +++++ .../helpers/compilation_db_helpers.py | 44 ++ src/robot_folders/helpers/config_helpers.py | 144 ++++++ .../helpers/directory_helpers.py | 247 ++++++++++ .../helpers/environment_helpers.py | 362 ++++++++++++++ src/robot_folders/helpers/exceptions.py | 35 ++ .../helpers/repository_helpers.py | 95 ++++ .../helpers/resources/__init__.py | 21 + .../resources/userconfig_distribute.yaml | 37 ++ .../helpers/ros_version_helpers.py | 66 +++ src/robot_folders/helpers/underlays.py | 71 +++ src/robot_folders/helpers/which.py | 45 ++ .../helpers/workspace_chooser.py | 65 +++ src/robot_folders/main.py | 100 ++++ tests/test_add_delete.py | 101 ++++ tests/test_functionality.py | 35 ++ tests/test_get_checkout_base_dir.py | 35 ++ tests/test_which.py | 42 ++ 62 files changed, 5735 insertions(+) create mode 100644 .coveragerc create mode 100644 .github/workflows/coverage.yaml create mode 100644 .github/workflows/docs.yaml create mode 100644 .github/workflows/package.yaml create mode 100644 .github/workflows/pre-commit.yaml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 Docker/Dockerfile create mode 100644 Docker/README.md create mode 100755 Docker/entrypoint.sh create mode 100644 Docker/source_global.sh create mode 100644 LICENSE.txt create mode 100644 README.rst create mode 100644 bin/rob_folders-complete.sh create mode 100755 bin/rob_folders_get_source_command.py create mode 100755 bin/rob_folders_source.sh create mode 100644 bin/source_environment.sh create mode 100644 docs/_static/robot_folders.svg create mode 100644 docs/aliases.rst create mode 100644 docs/conf.py create mode 100644 docs/configuration.rst create mode 100644 docs/faq.rst create mode 100644 docs/index.rst create mode 100644 docs/installation.rst create mode 100644 docs/misc_workspace.rst create mode 100644 docs/usage.rst create mode 100644 setup.py create mode 100644 src/robot_folders/__init__.py create mode 100644 src/robot_folders/commands/__init__.py create mode 100644 src/robot_folders/commands/active_environment.py create mode 100644 src/robot_folders/commands/adapt_environment.py create mode 100644 src/robot_folders/commands/add_environment.py create mode 100644 src/robot_folders/commands/cd.py create mode 100644 src/robot_folders/commands/change_environment.py create mode 100644 src/robot_folders/commands/clean.py create mode 100644 src/robot_folders/commands/delete_environment.py create mode 100644 src/robot_folders/commands/get_checkout_base_dir.py create mode 100644 src/robot_folders/commands/make.py create mode 100644 src/robot_folders/commands/manage_underlays.py create mode 100644 src/robot_folders/commands/run.py create mode 100644 src/robot_folders/commands/scrape_environment.py create mode 100644 src/robot_folders/helpers/ConfigParser.py create mode 100644 src/robot_folders/helpers/__init__.py create mode 100644 src/robot_folders/helpers/build_helpers.py create mode 100644 src/robot_folders/helpers/clean_helpers.py create mode 100644 src/robot_folders/helpers/compilation_db_helpers.py create mode 100644 src/robot_folders/helpers/config_helpers.py create mode 100644 src/robot_folders/helpers/directory_helpers.py create mode 100644 src/robot_folders/helpers/environment_helpers.py create mode 100644 src/robot_folders/helpers/exceptions.py create mode 100644 src/robot_folders/helpers/repository_helpers.py create mode 100644 src/robot_folders/helpers/resources/__init__.py create mode 100644 src/robot_folders/helpers/resources/userconfig_distribute.yaml create mode 100644 src/robot_folders/helpers/ros_version_helpers.py create mode 100644 src/robot_folders/helpers/underlays.py create mode 100644 src/robot_folders/helpers/which.py create mode 100644 src/robot_folders/helpers/workspace_chooser.py create mode 100644 src/robot_folders/main.py create mode 100644 tests/test_add_delete.py create mode 100644 tests/test_functionality.py create mode 100644 tests/test_get_checkout_base_dir.py create mode 100644 tests/test_which.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..02285b9 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,4 @@ +[report] +include = src/* +omit = test/* +show_missing = true diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml new file mode 100644 index 0000000..33a6c60 --- /dev/null +++ b/.github/workflows/coverage.yaml @@ -0,0 +1,41 @@ +name: Coverage +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +jobs: + test: + name: run tests + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: ubuntu-22.04 + ros_distribution: humble + + steps: + - uses: actions/checkout@v3 + - name: Setup ROS + uses: ros-tooling/setup-ros@v0.7 + with: + required-ros-distributions: ${{ matrix.ros_distribution }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest-cov codecov + python setup.py develop + - name: Test with pytest + run: | + source bin/rob_folders_source.sh + python3 -m pytest . -rA --cov=robot_folders --cov-report term --cov-report xml:coverage.xml + #- name: Upload coverage to Codecov + #uses: codecov/codecov-action@v4 + #with: + #file: ./coverage.xml + #flags: unittests + #name: codecov-umbrella + #fail_ci_if_error: true + #token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 0000000..fcf08d4 --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,37 @@ +## +## Copyright (c) 2024 FZI Forschungszentrum Informatik +## +## Permission is hereby granted, free of charge, to any person obtaining a copy +## of this software and associated documentation files (the "Software"), to deal +## in the Software without restriction, including without limitation the rights +## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +## copies of the Software, and to permit persons to whom the Software is +## furnished to do so, subject to the following conditions: +## +## The above copyright notice and this permission notice shall be included in +## all copies or substantial portions of the Software. +## +## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +## THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +## THE SOFTWARE. +## +name: "Build documentation" +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ammaraskar/sphinx-action@master + with: + docs-folder: "docs/" diff --git a/.github/workflows/package.yaml b/.github/workflows/package.yaml new file mode 100644 index 0000000..a102033 --- /dev/null +++ b/.github/workflows/package.yaml @@ -0,0 +1,32 @@ +name: Package +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +jobs: + package: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - '3.10' + - '3.11' + name: Python ${{ matrix.python-version }} + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + - name: Build package + run: | + python -m pip install build twine check-wheel-contents + python -m build --sdist --wheel . + ls -l dist + check-wheel-contents dist/*.whl + - name: "Check long_description" + run: "python -m twine check dist/*" diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml new file mode 100644 index 0000000..63ee9e9 --- /dev/null +++ b/.github/workflows/pre-commit.yaml @@ -0,0 +1,21 @@ +--- +name: pre-commit +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +jobs: + pre-commit: + name: pre-commit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + - uses: pre-commit/action@v3.0.1 + with: + extra_args: --all-files --hook-stage manual diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df9a63b --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*~ +config/userconfig.yaml +checkout +*.pyc +robot_folders.egg-info +src/robot_folders/build/ +build/ +dist/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..edd5552 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict + - id: check-symlinks + - id: check-xml + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace + - id: fix-byte-order-marker # Forbid UTF-8 byte-order markers + - repo: https://github.com/psf/black + rev: 24.3.0 + hooks: + - id: black diff --git a/Docker/Dockerfile b/Docker/Dockerfile new file mode 100644 index 0000000..4957743 --- /dev/null +++ b/Docker/Dockerfile @@ -0,0 +1,23 @@ +FROM ros:iron + +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3-pip \ + zsh \ + stow \ + git \ + golang-go \ + curl \ + wget \ + tmux \ + python3-argcomplete \ + ros-humble-ros-base \ + && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /root + +COPY ./entrypoint.sh / +COPY ./source_global.sh /root + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/Docker/README.md b/Docker/README.md new file mode 100644 index 0000000..ac4e2e8 --- /dev/null +++ b/Docker/README.md @@ -0,0 +1,24 @@ +# robot_folders development container + +This dockerfile is mainly used for testing on clean systems. It is not by any means cleanly +maintained or wants to be documented for end users. + +We usually use it for development as such (all run from this project's root directory) + +1. Build the container + +```bash +docker build -t robot_folders Docker +``` + +2. Run the container +```bash +docker run -it -v .:/robot_folders --rm robot_folders /usr/bin/zsh # replace with /bin/bash to test +# You can also mount a checkout folder to be persistent with that between runs +docker run -it -v .:/robot_folders ~/checkout_playground:/root/checkout --rm robot_folders /usr/bin/zsh +``` + +3. Inside the container source robot_folders +```bash +source source_global.sh +``` diff --git a/Docker/entrypoint.sh b/Docker/entrypoint.sh new file mode 100755 index 0000000..39d1bbd --- /dev/null +++ b/Docker/entrypoint.sh @@ -0,0 +1,5 @@ +#!/usr/bin/zsh + +pip3 install -e /robot_folders + +exec "$@" diff --git a/Docker/source_global.sh b/Docker/source_global.sh new file mode 100644 index 0000000..5e9e2a8 --- /dev/null +++ b/Docker/source_global.sh @@ -0,0 +1 @@ +source /usr/local/bin/rob_folders_source.sh diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..30e8e2e --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,17 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..cba0775 --- /dev/null +++ b/README.rst @@ -0,0 +1,71 @@ +Robot Folders +============= + +Welcome to ``robot_folders``! ``robot_folders`` is a collection of utility scripts designed to +streamline and facilitate the management of workspaces used for (ROS) development. It is designed +to enhance efficiency in handling different environments, consisting of multiple workspaces like a +``catkin_workspace`` and ``colcon_workspace`` or a plain CMake workspace. + +It focuses on optimizing the management of various subworkspaces, contributing +to a seamless development experience. + + +Quick start +------------ + +It is recommended to install robot_folders using ``pipx`` (You can install ``pipx`` using ``sudo apt +install pipx``). Please note: With pipx versions < 1.0 you'll have to provide the `--spec` flag + +.. code:: bash + + # pipx >= 1.0 (from ubuntu 22.04 on) + pipx install git+https://github.com/fzi-forschungszentrum-informatik/robot_folders.git + # pipx < 1.0 (ubuntu 20.04) + pipx install --spec git+https://github.com/fzi-forschungszentrum-informatik/robot_folders.git robot-folders + +Upgrade +------- + +To upgrade robot_folders using ``pipx`` do + +.. code:: bash + + # pipx >= 1.0 (from ubuntu 22.04 on) + pipx upgrade robot-folders + # pipx < 1.0 (ubuntu 20.04) + pipx upgrade --spec git+https://github.com/fzi-forschungszentrum-informatik/robot_folders.git robot-folders + +Shell setup +----------- + +In order to use ``robot_folders`` you'll have to call its source file. How to do this depends on +the way you installed ``robot_folders`` and on the version of the installation tool. + +In case you have installed +``robot_folders`` using ``pipx`` as described above (and given you use the bash shell), do: + +.. code:: bash + + # pipx < 1.3.0 or if ${HOME}/.local/pipx already existed + echo "source ${HOME}/.local/pipx/venvs/robot-folders/bin/rob_folders_source.sh" >> ~/.bashrc + # pipx >= 1.3.0 + echo "source ${HOME}/.local/share/pipx/venvs/robot-folders/bin/rob_folders_source.sh" >> ~/.bashrc + +In case you manually installed ``robot_folders`` using a python virtualenv the path is similarly + +.. code:: bash + + echo "source /bin/rob_folders_source.sh" >> ~/.bashrc + + +Basic usage +----------- + +After installation open up a new terminal to use robot_folders. The main +command for using robot_folders is ``fzirob``. Type + +.. code:: bash + + fzirob --help + +to get an overview over all available commands. diff --git a/bin/rob_folders-complete.sh b/bin/rob_folders-complete.sh new file mode 100644 index 0000000..35c6369 --- /dev/null +++ b/bin/rob_folders-complete.sh @@ -0,0 +1,53 @@ +## +## Copyright (c) 2024 FZI Forschungszentrum Informatik +## +## Permission is hereby granted, free of charge, to any person obtaining a copy +## of this software and associated documentation files (the "Software"), to deal +## in the Software without restriction, including without limitation the rights +## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +## copies of the Software, and to permit persons to whom the Software is +## furnished to do so, subject to the following conditions: +## +## The above copyright notice and this permission notice shall be included in +## all copies or substantial portions of the Software. +## +## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +## THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +## THE SOFTWARE. +## + +if [ -n "${ZSH_VERSION+1}" ]; +then + compdef _rob_folders_completion fzirob +fi +if [ -n "${BASH_VERSION+1}" ]; +then + _fzirob_completion() { + # replace 'fzirob' with 'rob_folders' for completion + _rob_folders_completion rob_folders ${@:2} + return 0 + } + complete -F _fzirob_completion -o default fzirob +fi +#complete -F _rob_folders_completion -o default rob_folders; + +# ce completion in zsh works through the alias definition. For bash we have to declare it. +if [ -n "${BASH_VERSION+1}" ]; +then + _ce_completion() { + checkout_dir=$(rob_folders get_checkout_base_dir) + #_envs=$(ls ${checkout_dir}) + _envs=$(ce --help | sed -e '1,/Commands:/d' ) + local cur prev + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + COMPREPLY=( $(compgen -W "${_envs}" -- ${cur}) ) + return 0 + } + + complete -F _ce_completion -o default ce; +fi diff --git a/bin/rob_folders_get_source_command.py b/bin/rob_folders_get_source_command.py new file mode 100755 index 0000000..ea40a1b --- /dev/null +++ b/bin/rob_folders_get_source_command.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +"""Small hacky script to get the correct source_zsh command per click version""" + +import argparse +import click + + +def get_click_version(): + click_version = click.__version__ + click_major_version = click_version.split(".")[0] + return int(click_major_version) + + +def print_source_command(shell: str): + click_version = get_click_version() + if click_version >= 8: + print(f"{shell}_source") + else: + print(f"source_{shell}") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "-s", "--shell", type=str, choices=["bash", "zsh"], default="bash" + ) + args = parser.parse_args() + print_source_command(args.shell) + + +if __name__ == "__main__": + main() diff --git a/bin/rob_folders_source.sh b/bin/rob_folders_source.sh new file mode 100755 index 0000000..54363f1 --- /dev/null +++ b/bin/rob_folders_source.sh @@ -0,0 +1,199 @@ +## +## Copyright (c) 2024 FZI Forschungszentrum Informatik +## +## Permission is hereby granted, free of charge, to any person obtaining a copy +## of this software and associated documentation files (the "Software"), to deal +## in the Software without restriction, including without limitation the rights +## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +## copies of the Software, and to permit persons to whom the Software is +## furnished to do so, subject to the following conditions: +## +## The above copyright notice and this permission notice shall be included in +## all copies or substantial portions of the Software. +## +## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +## THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +## THE SOFTWARE. +## + +# zsh +if [ -d "$HOME/.local/bin" ] ; then + PATH="$HOME/.local/bin:$PATH" +fi +my_source="source" +if [ -n "${ZSH_VERSION+1}" ]; +then + # Get the base directory where the install script is located + export ROB_FOLDERS_BASE_DIR="$( cd "$( dirname "${(%):-%N}" )/.." && pwd )" + my_source=$(rob_folders_get_source_command.py -s zsh) +fi + +# bash +if [ -n "${BASH_VERSION+1}" ]; +then + # Get the base directory where the install script is located + export ROB_FOLDERS_BASE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" + my_source=$(rob_folders_get_source_command.py -s bash) +fi +eval "$(_ROB_FOLDERS_COMPLETE=${my_source} rob_folders)" + +# if there is alreay an active environment, refrain from scraping the empty paths +# (they must already there) +if [ -z "$ROB_FOLDERS_ACTIVE_ENV" ]; then + export ROB_FOLDERS_EMPTY_CMAKE_PATH=${CMAKE_PREFIX_PATH} + export ROB_FOLDERS_EMPTY_PATH=${PATH} + export ROB_FOLDERS_EMPTY_LD_LIBRARY_PATH=${LD_LIBRARY_PATH} + export ROB_FOLDERS_EMPTY_QML_IMPORT_PATH=${QML_IMPORT_PATH} + export ROB_FOLDERS_EMPTY_PYTHONPATH=${PYTHONPATH} + + if [ ! -z "${ROB_FOLDERS_EMPTY_CMAKE_PATH}" ] && [ -z $ROB_FOLDERS_IGNORE_CMAKE_PREFIX_PATH ] + then + echo "WARNING! WARNING! WARNING!" + echo "Your CMAKE_PREFIX_PATH is not empty. This probably means that you explicitly set your +CMAKE_PREFIX_PATH to include some custom directory. If you want to keep this that way you can +export the environment variable ROB_FOLDERS_IGNORE_CMAKE_PREFIX_PATH to some arbitrary value to +suppress this warning. e.g. + + export ROB_FOLDERS_IGNORE_CMAKE_PREFIX_PATH=\":-)\" + source /home/mauch/robot_folders/bin/fzirob_source.sh +" + echo "By, the way your CMAKE_PREFIX_PATH is: \"${CMAKE_PREFIX_PATH}\"" + echo "WARNING! WARNING! WARNING! END." + fi +else + echo "Robot folder environment active: ($ROB_FOLDERS_ACTIVE_ENV)" +fi + + + +# define some legacy aliases from old robot_folders +alias ce="fzirob change_environment" +alias cdr="fzirob cd" +alias cdros="fzirob cd ros" +alias cdcol="fzirob cd colcon" +alias cdhome="fzirob cd" +alias makeros="fzirob make ros" +alias makecol="fzirob make colcon" + +add_fzi_project() +{ + echo "DEPRECATED! DEPRECATED! DEPRECATED! DEPRECATED!" + echo "The use of this call is deprecated!!!" + echo "DEPRECATED! DEPRECATED! DEPRECATED! DEPRECATED!" + echo "Please use the command 'fzirob add_environment' in future" + echo "DEPRECATED! DEPRECATED! DEPRECATED! DEPRECATED!" + echo "Executing 'fzirob add_environment' now" + fzirob add_environment $@ +} + +# These aliases are created after an environment is changed into. +# They are environment-specific but are created for all environments. +# Note: This is not done in the source_environment.sh script as the source +# script might be overwritten by the user and then suddenly these aliases +# won't be there anymore. We decided to keep all alias definitions inside +# this file. +env_aliases() +{ + alias kdevsession="kdevelop -s ${ROB_FOLDERS_ACTIVE_ENV}" +} + + +check_env() +{ + # for relevant env_vars + # for each entry + # if ROB_FOLDERS_CHECKOUT_DIR in entry and ROB_FOLDERS_ACTIVE_END not in entry + # cry + VAR=$(echo "${CMAKE_PREFIX_PATH}" | sed 's/:/ /g') + checkout_dir=$(rob_folders get_checkout_base_dir) + for entry in $(echo "${VAR}"); do + if [[ "${entry}" =~ ${checkout_dir} ]]; then + if [[ ! "${entry}" =~ ${checkout_dir}/${ROB_FOLDERS_ACTIVE_ENV} ]]; then + echo "\033[0;33mWARNING: CMAKE_PREFIX_PATH contains a path outside the current env: ${entry}\033[0m" + echo "You probably built your catkin_ws the first time, when another workspace was sourced. To solve this, go to your catkin_ws, delete build and devel, open a new shell, source your environment, build your catkin_ws." + fi + fi + done +} + +reset_environment() +{ + export CMAKE_PREFIX_PATH=${ROB_FOLDERS_EMPTY_CMAKE_PATH} + export PATH=${ROB_FOLDERS_EMPTY_PATH} + export LD_LIBRARY_PATH=${ROB_FOLDERS_EMPTY_LD_LIBRARY_PATH} + export QML_IMPORT_PATH=${ROB_FOLDERS_EMPTY_QML_IMPORT_PATH} + export PYTHONPATH=${ROB_FOLDERS_EMPTY_PYTHONPATH} +} + + +# Create the fzirob function +# +# Since rob_folders is a python program, it cannot execute commands +# directly on the shell such as 'cd' or setting environment variables. +# For this reason we created this wrapper function that performs +# the shell actions. +fzirob() +{ + if [ $# -ge 1 ]; then + # if we want to cd to a directory, we need to capture the output + if [ $1 = "cd" ]; then + output=$(rob_folders $@) + echo "$output" + cd_target=$(echo "$output" | grep "^cd" | tail -n 1 | sed s/cd\ //) + if [ ! -z "${cd_target// }" ]; then + cd ${cd_target} + fi + else + if [ $1 = "add_environment" ] && [ "$2" != "--help" ]; then + echo "Resetting environment before adding new one." + reset_environment + fi + + echo "rob_folders $@" + + rob_folders $@ + + if [ $? -eq 0 ]; then + if [ $1 = "change_environment" ] && [ "$2" != "--help" ]; then + checkout_dir=$(rob_folders get_checkout_base_dir) + if [ -f ${checkout_dir}/.cur_env ]; then + export ROB_FOLDERS_ACTIVE_ENV=$(cat ${checkout_dir}/.cur_env) + environment_dir="${checkout_dir}/${ROB_FOLDERS_ACTIVE_ENV}" + if [ -f ${environment_dir}/setup.sh ]; then + source ${environment_dir}/setup.sh + elif [ -f ${environment_dir}/setup.zsh ]; then + source ${environment_dir}/setup.zsh + elif [ -f ${environment_dir}/setup.bash ]; then + source ${environment_dir}/setup.bash + else + source ${ROB_FOLDERS_BASE_DIR}/bin/source_environment.sh + fi + # declare environment-specific aliases + env_aliases + #check_env + else + echo "Could not change environment" + fi + elif [ $1 = "add_environment" ] && [ "$2" != "--help" ]; then + if [ -z "$ROB_FOLDERS_ACTIVE_ENV" ]; then + fzirob change_environment + echo "Sourced new environment" + else + echo "Currently, there is a sourced environment ($ROB_FOLDERS_ACTIVE_ENV). Not sourcing new one." + fi + fi + else + return $? + fi + fi + else + rob_folders --help + fi +} + +# sourcing alias +source ${ROB_FOLDERS_BASE_DIR}/bin/rob_folders-complete.sh diff --git a/bin/source_environment.sh b/bin/source_environment.sh new file mode 100644 index 0000000..b6817b6 --- /dev/null +++ b/bin/source_environment.sh @@ -0,0 +1,266 @@ +## +## Copyright (c) 2024 FZI Forschungszentrum Informatik +## +## Permission is hereby granted, free of charge, to any person obtaining a copy +## of this software and associated documentation files (the "Software"), to deal +## in the Software without restriction, including without limitation the rights +## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +## copies of the Software, and to permit persons to whom the Software is +## furnished to do so, subject to the following conditions: +## +## The above copyright notice and this permission notice shall be included in +## all copies or substantial portions of the Software. +## +## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +## THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +## THE SOFTWARE. +## + +################################################################# +#------- This script is part of the robot folders system -------# +# # +# This script handles sourcing of an environment. Simply # +# source this file using # +# source source_environment.sh # +# This script will search for a workspace and call their # +# respective source files. # +# Afterwards the setup_local.sh file will be sourced if present.# +# # +# Please do not make modifications to this file, if you don't # +# know exactly what you're doing. Use the setup_local.sh file # +# inside the environment_dir which will be sourced afterwards. # +# # +# Usually a symlink to this file will be stored in each # +# environment. So when copying an environment to a location # +# where no robot_folders are available, remember to copy this # +# file to the symlink's location. # +# # +# You can either source this symlink directly with the above # +# command or use the robot_folders change_environment command. # +################################################################# + +# At first, we determine which shell type we're in. +# Note that the $environment_dir will be set from outside, if we called this script using +# robot_folders. Otherwise we assume, that we're manually sourcing the source file inside an +# environment directory. + +# zsh +if [ -n "${ZSH_VERSION+1}" ]; +then + if [ -z $environment_dir ] + then + # Get the base directory where the install script is located + environment_dir="$( cd "$( dirname "${(%):-%N}" )" && pwd )" + fi + + export shell_type="zsh" +fi + +# bash +if [ -n "${BASH_VERSION+1}" ]; +then + if [ -z $environment_dir ] + then + # Get the base directory where the install script is located + environment_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + fi + export shell_type="bash" +fi + +if [ -z ${rob_folders_overlay+x} ]; then + echo "Sourcing environment '$environment_dir'" + ROB_FOLDERS_ROOT_ENV=$environment_dir +fi + +# This is the environment's name, which we will print out later +env_name=$(basename $environment_dir) + +# Remove the packages cache from cmake as this creates problems with multiple envs +rm -rf $HOME/.cmake/packages/ + +function source_underlay() { + # cache current recursion step + local this_env_dir="$environment_dir" + local this_env_name="$env_name" + export ROB_FOLDERS_IS_UNDERLAY=true + + export rob_folders_overlay="$this_env_dir" + echo "Sourcing underlay $1 overlaying $rob_folders_overlay" + export environment_dir="$1" + if [ -f ${environment_dir}/setup.sh ]; then + source ${environment_dir}/setup.sh + elif [ -f ${environment_dir}/setup.zsh ]; then + source ${environment_dir}/setup.zsh + elif [ -f ${environment_dir}/setup.bash ]; then + source ${environment_dir}/setup.bash + else + source ${ROB_FOLDERS_BASE_DIR}/bin/source_environment.sh + fi + # reset things + export environment_dir=$this_env_dir + export env_name=$this_env_name + +} + +function source_underlays() { + local overlay=$1 + local underlay_file="$overlay/underlays.txt" + + if [ -e $underlay_file ]; then + while read underlay; do + if [ ! -z "$underlay" ]; then + source_underlay $underlay + fi + done < "$underlay_file" + fi + +} + +# This is basically only relevant when calling the script with an externally defined environment_dir +if [ -d $environment_dir ]; then + if [ -n ${ROB_FOLDERS_EMPTY_PATH} ]; then + if [ -z "$rob_folders_overlay" ]; then + # Set the prefix path to the one stored away when starting a new session + export CMAKE_PREFIX_PATH=${ROB_FOLDERS_EMPTY_CMAKE_PATH} + export PATH=${ROB_FOLDERS_EMPTY_PATH} + export LD_LIBRARY_PATH=${ROB_FOLDERS_EMPTY_LD_LIBRARY_PATH} + export QML_IMPORT_PATH=${ROB_FOLDERS_EMPTY_QML_IMPORT_PATH} + export PYTHONPATH=${ROB_FOLDERS_EMPTY_PYTHONPATH} + fi + fi + + source_underlays $environment_dir + + + # It is important to source the catkin_ws first, as it will remove non-existing paths from the + # Run ROS initialization if available + # We run the setup.sh in the catkin_ws folder. Afterwards we can run rosrun, roslaunch etc. with the files in it. + catkin_dir_long=$environment_dir/catkin_workspace + catkin_dir_short=$environment_dir/catkin_ws + + # check if catkin_ws is used + if [ -d $catkin_dir_short ] + then + catkin_dir=$catkin_dir_short + else + catkin_dir=$catkin_dir_long + fi + + if [ -d $catkin_dir ] + then + if [ -d $catkin_dir/.catkin_tools ] + then + SETUP_FILE=$(catkin locate --workspace $catkin_dir -d)/setup.$shell_type + echo "Sourcing $SETUP_FILE" + source $SETUP_FILE + elif [ -f $catkin_dir/devel_isolated/setup.$shell_type ] + then + source $catkin_dir/devel_isolated/setup.$shell_type + echo "Sourced catkin_workspace" + elif [ -f $catkin_dir/devel/setup.$shell_type ] + then + source $catkin_dir/devel/setup.$shell_type + + echo "Sourced catkin_workspace" + + elif [ -f $catkin_dir/install/setup.$shell_type ] + then + echo "Only found installed workspace. Sourcing $catkin_dir/install/setup.$shell_type" + source $catkin_dir/install/setup.$shell_type + else + echo "no setup.$shell_type for the catkin workspace found. Sourcing global ROS" + num_ros_distros=$(find /opt/ros -maxdepth 1 -mindepth 1 -type d | wc -l) + if [[ $num_ros_distros -gt 1 ]]; then + echo "Found more than one ros_distribution:" + echo $(ls /opt/ros/) + echo "Please insert the distro that should be used:" + read ros_distro + else + ros_distro=$(ls /opt/ros/) + fi + setup_file=/opt/ros/$ros_distro/setup.$shell_type + source $setup_file + echo "sourced $setup_file" + fi + fi + + colcon_dir_long=$environment_dir/colcon_workspace + colcon_dir_short=$environment_dir/colcon_ws + colcon_dir_dev_ws=$environment_dir/dev_ws + + # check if colcon_ws is used + if [ -d $colcon_dir_short ] + then + colcon_dir=$colcon_dir_short + elif [ -d $colcon_dir_long ] + then + colcon_dir=$colcon_dir_long + else + colcon_dir=$colcon_dir_dev_ws + fi + + if [ -d $colcon_dir ] + then + if [ -f $colcon_dir/install/local_setup.$shell_type ] + then + if [ "$environment_dir" = "$ROB_FOLDERS_ROOT_ENV" ]; then + ros2_version=$(grep "COLCON_CURRENT_PREFIX=\"/opt/ros" $colcon_dir/install/setup.sh | cut -d '"' -f2) + echo "Sourcing ${ros2_version}/setup.${shell_type}" + source "${ros2_version}/setup.${shell_type}" + fi + source $colcon_dir/install/local_setup.$shell_type + echo "Sourced colcon workspace $colcon_dir" + else + echo "no setup.$shell_type for the colcon workspace found. Sourcing global ROS2" + num_ros_distros=$(find /opt/ros -maxdepth 1 -mindepth 1 -type d | wc -l) + if [[ $num_ros_distros -gt 1 ]]; then + echo "Found more than one ros_distribution:" + echo $(ls /opt/ros/) + echo "Please insert the distro that should be used:" + read ros_distro + else + ros_distro=$(ls /opt/ros/) + fi + setup_file=/opt/ros/$ros_distro/setup.$shell_type + source $setup_file + echo "sourced $setup_file" + fi + fi + + # source misc_ws environment if existing. + misc_ws_dir=$environment_dir/misc_ws + if [ -d $misc_ws_dir ] + then + if [[ ! "$LD_LIBRARY_PATH" =~ "$misc_ws_dir/export/lib" ]]; then + export LD_LIBRARY_PATH=$misc_ws_dir/export/lib/:$LD_LIBRARY_PATH + fi + if [[ ! "$PATH" =~ "$misc_ws_dir/export/bin" ]]; then + export PATH=$misc_ws_dir/export/bin:${PATH} + fi + if [[ ! "$CMAKE_PREFIX_PATH" =~ "$misc_ws_dir/export/" ]]; then + export CMAKE_PREFIX_PATH=$misc_ws_dir/export:$CMAKE_PREFIX_PATH + fi + echo "Sourced misc_ws workspace from $misc_ws_dir" + fi + + # source local source file + if [ -f $environment_dir/setup_local.sh ]; + then + source $environment_dir/setup_local.sh + elif [ -f $environment_dir/source_local.sh ]; # for backward compatibility + then + source $environment_dir/source_local.sh + fi + + if [ "$environment_dir" = "$ROB_FOLDERS_ROOT_ENV" ]; then + echo "Environment setup for '${env_name}' done. You now have a sourced environment." + #else + #echo "Sourced underlay '${env_name}'" + fi +else + echo "No environment with the given name found!" +fi diff --git a/docs/_static/robot_folders.svg b/docs/_static/robot_folders.svg new file mode 100644 index 0000000..979b756 --- /dev/null +++ b/docs/_static/robot_folders.svg @@ -0,0 +1,441 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/aliases.rst b/docs/aliases.rst new file mode 100644 index 0000000..18aa3ab --- /dev/null +++ b/docs/aliases.rst @@ -0,0 +1,24 @@ +Robot folders aliases +===================== + +``robot_folders`` defines a number of bash aliases that help you typing even less. They aren't +necessarily as easy and logical to remember as for example ``fzirob change_environment``, but once +you get to know them, you'll probably enjoy simply typing ``ce`` instead. + ++---------+---------------------------+ +| Alias | fzirob command | ++=========+===========================+ +| ce | fzirob change_environment | ++---------+---------------------------+ +| cdr | fzirob cd | ++---------+---------------------------+ +| cdros | fzirob cd ros | ++---------+---------------------------+ +| cdcol | fzirob cd colcon | ++---------+---------------------------+ +| cdhome | fzirob cd | ++---------+---------------------------+ +| makeros | fzirob make ros | ++---------+---------------------------+ +| makecol | fzirob make colcon | ++---------+---------------------------+ diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..d838db2 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,29 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "robot_folders" +copyright = "2024, FZI Forschungszentrum Informatik" +author = "Felix Exner" +release = "0.3.5" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ["sphinx.ext.autosectionlabel", "sphinx_tabs.tabs"] +autosectionlabel_prefix_document = True + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "alabaster" +html_static_path = ["_static"] +html_logo = "_static/robot_folders.svg" diff --git a/docs/configuration.rst b/docs/configuration.rst new file mode 100644 index 0000000..d137092 --- /dev/null +++ b/docs/configuration.rst @@ -0,0 +1,62 @@ +Configuration +============= + +The default configuration is stored inside the file ``userconfig_distribute.yaml``, +which will be copied to ``~/.config/robot_folders.yaml`` automatically on first invocation. + +.. literalinclude:: ../src/robot_folders/helpers/resources/userconfig_distribute.yaml + :language: yaml + :lines: 22- + +The configuration is split into different sections which will be +explained in the following. + + +Build options +------------- + +``generator`` + Currently make and ninja can be used. If ninja is configured, but not + installed, building will throw an error. + +``cmake_flags`` + These flags will be passed to the cmake command. + +``make_threads`` + Number of threads that should be used with make. Only relevant when + generator is set to make. + +``install_catkin`` + If set to true, the build command will also install the catkin_workspace + (into the catkin_ws/install folder by default). + +``catkin_make_cmd`` + Set to catkin_make by default but can be changed to catkin build. + +``colcon_build_options`` + Options passed to each ``colcon build`` invocation that is piped through ``fzirob make``. + + +Directory options +----------------- + +``checkout_dir`` + By default, environments are stored inside + ~/checkout. If environments should be stored + somewhere else, specify this path here. This **must** be an absolute path, but ``${HOME}/`` or + ``~/`` can be used, as well. + +``catkin_names`` + All first level subdirectories in an environment that match one of these + names will be treated as catkin workspaces. If you name yor catkin + workspaces differently, please specify this name here. + +``colcon_names`` + All first level subdirectories in an environment that match one of these + names will be treated as colcon workspaces. If you name yor colcon + workspaces differently, please specify this name here. + +``no_backup_dir`` + Location where build and install steps should be performed if they should be outside of the + checkout tree. If that folder exists, users will be prompted whether to build inside the + ``no_backup_dir`` when creating a new environment. diff --git a/docs/faq.rst b/docs/faq.rst new file mode 100644 index 0000000..92b87ac --- /dev/null +++ b/docs/faq.rst @@ -0,0 +1,7 @@ +Frequendly Asked Questions +========================== + +Where are my environments stored and how do I change that? +---------------------------------------------------------- + +By default, environments are saved within ``~/checkout``. If you'd like to change that, see the ``checkout_dir`` parameter in :ref:`configuration:Directory options`. diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..c7d7bf7 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,21 @@ +.. robot_folders documentation master file, created by + sphinx-quickstart on Wed Aug 30 14:47:39 2023. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to robot_folders's documentation! +========================================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + installation + configuration + usage + misc_workspace + aliases + faq + + +.. include:: ../README.rst diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..7eafda5 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,68 @@ +Installation +============ + +It is recommended to install robot_folders using ``pipx`` (You can install ``pipx`` on Ubuntu using ``sudo apt +install pipx``). Please note: With pipx versions < 1.0 you'll have to provide the `--spec` flag. + +.. tabs:: + + .. group-tab:: pipx >= 1.0 + + .. code:: bash + + pipx install git+https://github.com/fzi-forschungszentrum-informatik/robot_folders.git + .. group-tab:: pipx < 1.0 + + .. code:: bash + + pipx install --spec git+https://github.com/fzi-forschungszentrum-informatik/robot_folders.git robot-folders + +Upgrade +------- + +To upgrade robot_folders using ``pipx`` do + +.. tabs:: + + .. group-tab:: pipx >= 1.0 + + .. code:: bash + + pipx upgrade robot-folders + .. group-tab:: pipx < 1.0 + + .. code:: bash + + pipx upgrade --spec git+https://github.com/fzi-forschungszentrum-informatik/robot_folders.git robot-folders + +Shell setup +----------- + +In order to use ``robot_folders`` you'll have to source its source file. The path of the source file +depends on the installation method: + +.. tabs:: + + .. tab:: pipx < 1.3.0 + + .. code:: bash + + # pipx < 1.3.0 or if ${HOME}/.local/pipx already existed + echo "source ${HOME}/.local/pipx/venvs/robot-folders/bin/rob_folders_source.sh" >> ~/.bashrc + + .. tab:: pipx >= 1.3.0 + + Note: When coming from older pipx versions the path ``${HOME}/.local/pipx/venvs`` might + already exist, in which case newer pipx versions will use that one. In that case you'll have + to use the source instructions from pre-1.3.0. + + .. code:: bash + + echo "source ${HOME}/.local/share/pipx/venvs/robot-folders/bin/rob_folders_source.sh" >> ~/.bashrc + .. tab:: pip venv + + In case you manually installed ``robot_folders`` using a python virtualenv the path is similarly + + .. code:: bash + + echo "source /bin/rob_folders_source.sh" >> ~/.bashrc diff --git a/docs/misc_workspace.rst b/docs/misc_workspace.rst new file mode 100644 index 0000000..9ec4c9c --- /dev/null +++ b/docs/misc_workspace.rst @@ -0,0 +1,73 @@ +Misc Workspace +============== + +**Note:** the misc workspace should be used with caution as it is an +unconvenient way to build your software. + +The misc workspace can be used to build plain cmake, fla or other types +of git repositories, but the build procedure has to be managed manually +by the user. The misc workspace has the following structure: + +.. code:: bash + + |-- misc_ws + |-- export + |-- repo-A + |-- repo-B + |-- ... + +The misc workspace is included when the command + +.. code:: bash + + fzirob scrape_environment + +is used and also applied when + +.. code:: bash + + fzirob adapt_environment + +or + +.. code:: bash + + # If your environment contains a misc_ws you probably want to built its contents first + # (see next section) before building any workspace depending on that. That's why the + # '--no_build' flag is activated in this example + fzirob add_environment --config_file --no_build + +is used to share or save a workspace with others. + +When sourcing an environment, the misc_ws export folder will be sourced +ontop of the catkin_workspace / colcon workspace. This way, it will be +available to other workspaces automatically. + +Exported (and sourced) paths +---------------------------- + +Currently, the following paths are added to the respective environment variables when sourcing an environment: + +- ``misc_ws/export/lib`` gets added to ``$LD_LIBRARY_PATH`` +- ``misc_ws/export/bin`` gets added to ``$PATH`` +- ``misc_ws/export`` gets added to ``$CMAKE_PREFIX_PATH`` + +Misc workspace example +---------------------- + +Assume that repository “repo-A” has build dependencies on repository +“repo-B”: repo-B depends on repo-A. Then you can build the workspace +manually by calling: + +.. code:: bash + + cd repo-B + mkdir build && cd build + cmake .. -DCMAKE_INSTALL_PREFIX=../../export -DBUILD_SHARED_LIBS=1 + make + make install + cd ../../repo-A + mkdir build && cd build + cmake .. -DCMAKE_INSTALL_PREFIX=../../export -DBUILD_SHARED_LIBS=1 + make + make install diff --git a/docs/usage.rst b/docs/usage.rst new file mode 100644 index 0000000..bce995c --- /dev/null +++ b/docs/usage.rst @@ -0,0 +1,253 @@ +Usage +===== + +This page should give you an overview of different tasks that can be performed using +``robot_folders``. It is kind of an inverse documentation to the command documentation that comes +with the command line interface. + +What is an environment? +----------------------- + +An "environment" inside ``robot_folders`` consists of a set of workspaces. At +the moment the following types of workspaces ares supported: + +1. **catkin_ws (ROS Workspace):** + Modeled after the standard ROS (Robot Operating System) workspace, + ``catkin_ws`` adheres to ROS tutorials and conventions, serving as a + standardized environment for ROS-related development. + +1. **colcon_ws (ROS 2 Workspace):** + Modeled after the standard ROS 2 (Robot Operating System) workspace, + ``colcon_ws`` adheres to ROS tutorials and conventions, serving as a + standardized environment for ROS 2 related development. + +1. **misc_ws:** + The ``misc_ws`` adds the ability to add custom packages to your workspace. + It simply adds its export paths to your environment. You'll have to take + care to make sure the targets are built and exported there, yourself. See + :ref:`misc_workspace:Misc Workspace` for details. + + +After installation, open up a new terminal to use ``robot_folders``. The main +command for using ``robot_folders`` is ``fzirob``. To get an overview of all available commands type + +.. code:: bash + + fzirob --help + +To utilize any command, adhere to the general syntax: + +.. code:: bash + + fzirob command 'argument' + + +Creating an Environment +----------------------- +To create a new environment called ``ENV_NAME``, simply run + +.. code:: bash + + fzirob add_environment ENV_NAME + +This will ask which types of workspaces should be generated for the new +environment. + +One of the questions will be which underlay you'd like to use. If you don't know what underlays are +and what to use them for, you can just leave the selection empty and press enter. If you want to +know more, see :ref:`usage:Using underlay environments`. + +Depending on your specific setup further questions may be asked: + +- When creating a catkin or colcon workspace with multiple installed ROS + versions, you will be prompted for the ROS version being used for that + workspace. +- If you use an automatic backup strategy for your machine you probably do not want to backup the + build and install artifacts from your workspaces, but only the sources. For that purpose + ``robot_folders`` allows building and installing outside of your checkout tree and creating + symlinks at the respecitve places instead. Whenever the configured backup location exists on the + system (By default that's ``$HOME/no_backup``, that can be changed in the + :ref:`configuration:Configuration`.) you will be prompted whether + you want to build inside the checkout tree or in the no-backup folder. + +Upon completion, you will receive the confirmation: *"Environment setup for +'ENV_NAME` is complete."* + + +Sourcing an environment +----------------------- + +To activate or source an environment, use the ``fzirob change_environment +ENV_NAME`` command. This command sources the appropriate setup scripts of the +environment depending on its contents. + +You can use tab completion on the environments so typing ``fzirob +change_environment`` and then pressing :kbd:`Tab` should list all environments +present. + +Executing ``fzirob change_environment`` without any environment specified will +source the most recently sourced environment. + +Using underlay environments +--------------------------- + +``robot_folders`` supports using underlay environments that will be sourced during sourcing the +current environment. With this it is possible to keep parts of your setup in another workspace. +Imaging for example, you have two packages: "Application" and "Library". "Application" is depending +on "Library", but there are also other packages in other environments depending on "Library". You +can reuse your local "Library" installation by putting it into its own environment and use that as +an underlay for your environment containing "Application" (and for all the other environments with +packages depending on "Library"). + +This way, you'll only have to keep "Library" up to date in one place. + +Another use case is if you want to test your "Application" against different versions of "Library". +You could keep compiled versions of "Library" in separate environments and select the correct one +as an underlay for your application's environment. You'll only have to recompile your application +while with keeping "Library" inside your application environment you would have to go the to +library package, switch branches and rebuild the library and application package. + +When creating new environment you will be prompted for the underlays to be used. + +Underlays will be stored in a ``underlays.txt`` file inside your environment's folder. You can +either edit that file manually or use the ``fzirob manage_underlays`` command to change the +environment's underlays. + +.. note:: + The underlay file will not get deleted by ``fzirob clean``. Although you will be asked for the + ROS distribution to use (if more than one is installed), underlay configuration persists. + +.. note:: + You can stack underlays. That means you can create dependency chains e.g. env1 -> env2 -> env3 + +.. note:: + Currently, initial build isn't supported when using underlay workspaces. When specifying an + environment config when creating a new environment the user will have to manually trigger the + build process after initially creating the environment. Usually, a simple `fzirob make` should + do the trick. + +.. warning:: + Currently there is no check for cyclic environment dependency. Please make sure you do not run + into this problem. + +.. warning:: + The order in which environments are sourced is depending on the order in which they are written + into the ``underlays.txt`` file. That order might be alterd by the ``fzirob manage_underlays`` + command. If you depend on an order, you may instead consider stacking underlays depending on + each other. + +Compiling an environment +------------------------ + +To build an environment please use the ``fzirob make`` command. It has to be +invoked on an already sourced environment but can be called from anywhwere. + +Without any further options this will build all the workspace present which it +knows how to build. So, if there is a catkin workspace present, it will build +that, if there's a colcon workspace this will be built. + +You can also manually specify which workspace to build by using ``fzirob make +ros`` or ``fzirob make colcon``. When using ``fzirob make`` you don't have to +worry about the particular build command at all. + +Default options for building environments such as the builder for catkin +workspaces or cmake arguments for a colcon workspace can be set in the +:ref:`configuration:Configuration`. + +Cleaning an environment +----------------------- + +Sometimes you want to completely rebuild your environment. ``fziron clean`` +provides a command that will delete all build and installation artifacts from +your environment's workspaces. Before actual deletion it will show a list of +all the folders to be deleted with a safety prompt so you don't accidentally +delete things. + +Navigating inside an environment +-------------------------------- + +You can use the ``fzirob cd`` command to navigate around in an environment. The +following examples show a couple of possible locations you can navigate to. For +the examples we have sourced the environment ``env_name`` and environments are +stored inside ``~/checkout``: + + .. code:: bash + + fzirob cd # Env root folder e.g. ~/checkout/env_name + fzirob cd ros # Env catkin folder e.g. ~/checkout/env_name/catkin_ws + fzirob cd colcon # Env colcon folder e.g. ~/checkout/env_name/colcon_ws + +Again, tab completion will present the possible options for the currently sourced environment. + +Start / Demo scripts +-------------------- + +For easy interaction with unknown environments ``robot_folders`` provides the +``fzirob run`` command. With that any executable file inside the environment's +``demos`` folder can be executed. So, if you've got an unknown environment and +just want to startup a predefined demo, simply source the environment, type +``fzirob run `` which should list all the possible demo scripts. + +Sharing environments +-------------------- + +Often you'd like to share an environment with colleagues working on the same +project. While for colcon workspaces there exist external tools such as +vcstool2_, ``robot_folders`` provides extended functionality to that: + +* Contents of multiple workspaces can be exported and imported at once (e.g. + colcon workspace and misc workspace). +* Startup scripts are stored inside the exchange format for easy interaction. + +Exporting an environment +~~~~~~~~~~~~~~~~~~~~~~~~ + +To export an environment, use the ``fzirob scrape_environment`` command. It scrapes an environment configuration into a config file, facilitating sharing with others. You'll have to provide the environment name and the target file as arguments e.g. + +.. code:: bash + + fzirob scrape_environment env_name /tmp/env_name.yaml + +In case you've got multiple remotes configured for repos inside your +environments, you will be queried which remote should be used for each +repository. + +When providing ``--use_commit_id true``, the exact commit IDs get scraped +instead of branch names. which is rather useful if you want to save a "working +state" of your whole environment. + +Creating a new environment with a configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you've got provided an environment configuration from somebody you can +create a new environment with that using + +.. code:: bash + + fzirob add_environment --config-file /tmp/env_name.yaml other_env + +which will create an environment called ``other_env`` with the configuration +from the previously exported ``env_name`` environment. + +Adapting an environment with a configuration file +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you already have an environment but you want to update it to match a config +file e.g. because some repositories have been added to the environment or some +branch names have been changed you can do this using the ``fzirob +adapt_environment`` command. + +If there are repositories in your local environment that are not in the config +file you will be prompted whether you want to keep those repositories. If +branches or remotes in the config file differ from those present locally, you +will also be asked. You can override that to a default behavior using the +``--local_delete_policy`` and ``--local_override_policy`` options. + +Deleting an environment +----------------------- + +If you want to delete an environment alltogether, you can use ``fzirob +delete_environment =8.0", "gitpython", "inquirer", "PyYaml", "vcstool"], + entry_points=""" + [console_scripts] + rob_folders=robot_folders.main:cli + """, + scripts=[ + "bin/rob_folders_get_source_command.py", + "bin/rob_folders-complete.sh", + "bin/rob_folders_source.sh", + "bin/source_environment.sh", + ], + include_package_data=True, + package_data={"": ["*.yaml"]}, + license_files=("LICENSE.txt"), +) diff --git a/src/robot_folders/__init__.py b/src/robot_folders/__init__.py new file mode 100644 index 0000000..3130f78 --- /dev/null +++ b/src/robot_folders/__init__.py @@ -0,0 +1,21 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# diff --git a/src/robot_folders/commands/__init__.py b/src/robot_folders/commands/__init__.py new file mode 100644 index 0000000..3130f78 --- /dev/null +++ b/src/robot_folders/commands/__init__.py @@ -0,0 +1,21 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# diff --git a/src/robot_folders/commands/active_environment.py b/src/robot_folders/commands/active_environment.py new file mode 100644 index 0000000..f8f79f4 --- /dev/null +++ b/src/robot_folders/commands/active_environment.py @@ -0,0 +1,46 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +"""Prints the currently sourced environment""" + +import click +from robot_folders.helpers.directory_helpers import ( + get_active_env, + get_last_activated_env, +) + + +@click.command("active_environment") +def cli(): + """Prints out the current environment. If none + is sourced right now, it tells which was the last active + environment, which will be sourced by simply calling the + source command.""" + + active_env = get_active_env() + if active_env is None: + click.echo( + "No active environment. Last activated environment: {}".format( + get_last_activated_env() + ) + ) + else: + click.echo("Active environment: {}".format(active_env)) diff --git a/src/robot_folders/commands/adapt_environment.py b/src/robot_folders/commands/adapt_environment.py new file mode 100644 index 0000000..03d8aa6 --- /dev/null +++ b/src/robot_folders/commands/adapt_environment.py @@ -0,0 +1,415 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +""" +This implements a command to adapt an environment with a config file. +""" +from __future__ import print_function +import os +import stat +import subprocess +import click + +import robot_folders.helpers.directory_helpers as dir_helpers +from robot_folders.helpers.repository_helpers import create_rosinstall_entry +from robot_folders.helpers.ConfigParser import ConfigFileParser +import robot_folders.helpers.environment_helpers as environment_helpers + + +class EnvironmentAdapter(click.Command): + """ + Implements a click command interface to adapt an environment. + """ + + def __init__(self, name=None, **attrs): + click.Command.__init__(self, name, **attrs) + + self.local_delete_policy = "ask" + self.local_override_policy = "ask" + self.ignore_catkin = False + self.ignore_colcon = False + self.ignore_misc = False + self.no_submodules = False + self.rosinstall = dict() + + def invoke(self, ctx): + """ + This invokes the actual command. + """ + env_dir = os.path.join(dir_helpers.get_checkout_dir(), self.name) + catkin_dir = dir_helpers.get_catkin_dir(env_dir) + colcon_dir = dir_helpers.get_colcon_dir(env_dir) + catkin_src_dir = os.path.join(catkin_dir, "src") + colcon_src_dir = os.path.join(colcon_dir, "src") + misc_ws_dir = os.path.join(env_dir, "misc_ws") + demos_dir = os.path.join(env_dir, "demos") + + self.local_delete_policy = ctx.parent.params["local_delete_policy"] + self.local_override_policy = ctx.parent.params["local_override_policy"] + self.ignore_catkin = ctx.parent.params["ignore_catkin"] + self.ignore_colcon = ctx.parent.params["ignore_colcon"] + self.ignore_misc = ctx.parent.params["ignore_misc"] + self.no_submodules = ctx.parent.params["no_submodules"] + + config_file_parser = ConfigFileParser(ctx.params["in_file"]) + has_catkin, ros_rosinstall = config_file_parser.parse_ros_config() + has_colcon, ros2_rosinstall = config_file_parser.parse_ros2_config() + has_misc_ws, misc_ws_rosinstall = config_file_parser.parse_misc_ws_config() + os.environ["ROB_FOLDERS_ACTIVE_ENV"] = self.name + + if has_misc_ws and (not self.ignore_misc): + if os.path.isdir(misc_ws_dir): + click.echo("Adapting misc workspace") + self.rosinstall = dict() + self.parse_folder(misc_ws_dir) + if misc_ws_rosinstall: + self.adapt_rosinstall(misc_ws_rosinstall, misc_ws_dir) + else: + click.echo("Creating misc workspace") + has_nobackup = dir_helpers.check_nobackup() + build_base_dir = dir_helpers.get_build_base_dir(has_nobackup) + misc_ws_build_root = os.path.join(build_base_dir, self.name, "misc_ws") + + environment_helpers.MiscCreator( + misc_ws_dir, + rosinstall=misc_ws_rosinstall, + build_root=misc_ws_build_root, + no_submodules=self.no_submodules, + ) + + if has_catkin and (not self.ignore_catkin): + if os.path.isdir(catkin_src_dir): + click.echo("Adapting catkin workspace") + self.rosinstall = dict() + self.parse_folder(catkin_src_dir) + if ros_rosinstall: + self.adapt_rosinstall(ros_rosinstall, catkin_src_dir) + else: + click.echo("Creating catkin workspace") + has_nobackup = dir_helpers.check_nobackup() + catkin_dir = os.path.join(env_dir, "catkin_ws") + build_base_dir = dir_helpers.get_build_base_dir(has_nobackup) + catkin_build_dir = os.path.join( + build_base_dir, self.name, "catkin_ws", "build" + ) + + catkin_creator = environment_helpers.CatkinCreator( + catkin_directory=catkin_dir, + build_directory=catkin_build_dir, + rosinstall=ros_rosinstall, + no_submodules=self.no_submodules, + ) + catkin_creator.create() + + if has_colcon and (not self.ignore_colcon): + if os.path.isdir(colcon_src_dir): + click.echo("Adapting colcon workspace") + self.rosinstall = dict() + self.parse_folder(colcon_src_dir) + if ros2_rosinstall: + self.adapt_rosinstall(ros2_rosinstall, colcon_src_dir) + else: + click.echo("Creating colcon workspace") + has_nobackup = dir_helpers.check_nobackup() + colcon_dir = os.path.join(env_dir, "colcon_ws") + build_base_dir = dir_helpers.get_build_base_dir(has_nobackup) + colcon_build_dir = os.path.join( + build_base_dir, self.name, "colcon_ws", "build" + ) + + colcon_creator = environment_helpers.ColconCreator( + colcon_directory=colcon_dir, + build_directory=colcon_build_dir, + rosinstall=ros2_rosinstall, + no_submodules=self.no_submodules, + ) + colcon_creator.create() + + click.echo("Looking for demo scripts") + dir_helpers.mkdir_p(demos_dir) + scripts = config_file_parser.parse_demo_scripts() + for script in scripts: + click.echo( + "Found {} in config. Will be overwritten if file exists".format(script) + ) + script_path = os.path.join(demos_dir, script) + with open(script_path, mode="w") as demo_script: + demo_script.write(scripts[script]) + demo_script.close() + os.chmod( + script_path, + stat.S_IRWXU + | stat.S_IRGRP + | stat.S_IXGRP + | stat.S_IROTH + | stat.S_IXOTH, + ) + + def adapt_rosinstall(self, config_rosinstall, packages_dir, workspace_dir=""): + """ + Parses the given config rosinstall and compares it to the locally installed packages + """ + for repo in config_rosinstall: + local_version_exists = False + version_update_required = True + uri_update_required = True + local_name = "" + uri = "" + version = "" + + if "local-name" in repo["git"]: + local_name = repo["git"]["local-name"] + else: + click.echo( + "No local-name given for package '{}'. " + "Skipping package".format(repo["git"]) + ) + continue + package_dir = os.path.join(packages_dir, local_name) + + if "version" in repo["git"]: + version = repo["git"]["version"] + else: + click.echo( + "WARNING: No version tag given for package '{}'. " + "The local version will be kept or the master " + "version will be checked out for new package".format(local_name) + ) + + if "uri" in repo["git"]: + uri = repo["git"]["uri"] + else: + click.echo( + "WARNING: No uri given for package '{}'. " + "Skipping package".format(local_name) + ) + continue + + # compare the repos' versions and uris + if local_name in self.rosinstall.keys(): + local_version_exists = True + local_repo = self.rosinstall[local_name] + if version == "" or version == local_repo["git"]["version"]: + version_update_required = False + elif self.local_override_policy == "keep_local": + version_update_required = False + elif self.local_override_policy == "override": + version_update_required = True + else: + click.echo( + "Package '{}' version differs from local version. ".format( + local_name + ) + ) + click.echo( + "1) local version: {}".format(local_repo["git"]["version"]) + ) + click.echo("2) config_file version: {}".format(version)) + version_to_keep = click.prompt( + "Which version should be used?", + type=click.Choice(["1", "2"]), + default="1", + ) + version_update_required = version_to_keep == "2" + + if uri == local_repo["git"]["uri"]: + uri_update_required = False + elif self.local_override_policy == "keep_local": + uri_update_required = False + elif self.local_override_policy == "override": + uri_update_required = True + else: + click.echo( + "Package '{}' uri differs from local version. ".format( + local_name + ) + ) + click.echo("local version: {}".format(local_repo["git"]["uri"])) + click.echo("config_file version: {}".format(uri)) + version_to_keep = click.prompt( + "Which uri should be used?", + type=click.Choice(["1", "2"]), + default="2", + ) + uri_update_required = version_to_keep == "2" + + else: + click.echo( + "Package '{}' does not exist in local structure. " + "Going to download.".format(local_name) + ) + + # Create repo if it does not exist yet. + if not local_version_exists: + if self.no_submodules: + subprocess.check_call(["git", "clone", uri, package_dir]) + else: + subprocess.check_call( + ["git", "clone", uri, package_dir, "--recurse-submodules"] + ) + + # Change the origin to the uri specified + if uri_update_required: + process = subprocess.check_call( + ["git", "remote", "set-url", "origin", uri], cwd=package_dir + ) + + # Checkout the version specified + if version_update_required: + process = subprocess.check_call(["git", "fetch"], cwd=package_dir) + process = subprocess.check_call( + ["git", "checkout", version], cwd=package_dir + ) + + config_name_list = [d["git"]["local-name"] for d in config_rosinstall] + + for repo in self.rosinstall: + if repo not in config_name_list: + click.echo( + "Package '{}' found locally, but not in config.".format(repo) + ) + local_name = self.rosinstall[repo]["git"]["local-name"] + package_dir = os.path.join(packages_dir, local_name) + if self.local_delete_policy == "delete_all": + dir_helpers.recursive_rmdir(package_dir) + click.echo("Deleted '{}'".format(repo)) + elif self.local_delete_policy == "ask": + if click.confirm("Do you want to delete it?"): + dir_helpers.recursive_rmdir(package_dir) + click.echo("Deleted '{}'".format(repo)) + elif self.local_delete_policy == "keep_all": + click.echo("Keeping repository as all should be kept") + + def parse_folder(self, folder, prefix=""): + """ + Function to recursively find git repositories + """ + subfolders = os.listdir(folder) + for subfolder in subfolders: + subfolder_abs = os.path.join(folder, subfolder) + if os.path.isdir(subfolder_abs): + git_dir = os.path.join(subfolder_abs, ".git") + local_name = os.path.join(prefix, subfolder) + if os.path.isdir(git_dir): + click.echo( + "Found '{}' in local folder structure.".format(local_name) + ) + entry = create_rosinstall_entry(subfolder_abs, local_name) + self.rosinstall[local_name] = entry + self.parse_folder(subfolder_abs, local_name) + + +class EnvironmentChooser(click.MultiCommand): + """Class to select an environment""" + + def list_commands(self, ctx): + return dir_helpers.list_environments() + + def get_command(self, ctx, name): + # return empty command with the correct name + if name in dir_helpers.list_environments(): + cmd = EnvironmentAdapter( + name=name, params=[click.Argument(param_decls=["in_file"])] + ) + return cmd + else: + click.echo("No environment with name < %s > found." % name) + return None + + +@click.command( + "adapt_environment", + cls=EnvironmentChooser, + short_help="Adapt an environment to a config file", + invoke_without_command=True, +) +@click.option( + "--local_delete_policy", + type=click.Choice(["delete_all", "keep_all", "ask"]), + default="ask", + help=( + "Defines whether repositories existing local, but NOT " + "in the config file should be kept or deleted. " + "Asking the user is the default behavior." + ), +) +@click.option( + "--local_override_policy", + type=click.Choice(["keep_local", "override", "ask"]), + default="ask", + help=( + "Defines whether repositories existing locally AND " + "in different version or with different URI in the config file," + "should be kept in local version or be overridden" + "Asking the user is the default behavior." + ), +) +@click.option( + "--ignore_catkin", + default=False, + is_flag=True, + help="Prevent catkin workspace from getting adapted", +) +@click.option( + "--ignore_colcon", + default=False, + is_flag=True, + help="Prevent colcon workspace from getting adapted", +) +@click.option( + "--ignore_misc", + default=False, + is_flag=True, + help="Prevent misc workspace from getting adapted", +) +@click.option( + "--no_submodules", + default=False, + is_flag=True, + help="Prevent git submodules from being cloned", +) +@click.pass_context +def cli( + ctx, + local_delete_policy, + ignore_catkin, + ignore_colcon, + ignore_misc, + no_submodules, + local_override_policy, +): + """Adapts an environment to given config file. + New repositories will be added, versions/branches will be changed and + deleted repositories will/may be removed. + Provide path to config file as [ARGS] after the environment name. + """ + + print( + "The policy for deleting repos only existing locally is: '{}'".format( + local_delete_policy + ) + ) + + if ctx.invoked_subcommand is None: + click.echo( + "No environment specified. Please choose one " + "of the available environments!" + ) diff --git a/src/robot_folders/commands/add_environment.py b/src/robot_folders/commands/add_environment.py new file mode 100644 index 0000000..7fb6148 --- /dev/null +++ b/src/robot_folders/commands/add_environment.py @@ -0,0 +1,416 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +"""implements the add functionality""" +import os +import stat +import subprocess +import sys + +import click +import inquirer + +import robot_folders.helpers.directory_helpers as dir_helpers +import robot_folders.helpers.build_helpers as build +import robot_folders.helpers.environment_helpers as environment_helpers +from robot_folders.helpers.ConfigParser import ConfigFileParser +from robot_folders.helpers.exceptions import ModuleException +from robot_folders.helpers.ros_version_helpers import * +from robot_folders.helpers.underlays import UnderlayManager + + +class EnvCreator(object): + """Worker class that actually handles the environment creation""" + + def __init__(self, name, no_submodules=False): + self.env_name = name + self.no_submodules = no_submodules + self.build_base_dir = dir_helpers.get_checkout_dir() + self.demos_dir = os.path.join( + dir_helpers.get_checkout_dir(), self.env_name, "demos" + ) + + self.misc_ws_directory = os.path.join( + dir_helpers.get_checkout_dir(), self.env_name, "misc_ws" + ) + self.misc_ws_build_directory = "to_be_set" + self.misc_ws_rosinstall = None + + self.catkin_directory = os.path.join( + dir_helpers.get_checkout_dir(), self.env_name, "catkin_ws" + ) + self.catkin_build_directory = "to_be_set" + self.cama_flags = "" + self.catkin_rosinstall = "" + + self.colcon_directory = os.path.join( + dir_helpers.get_checkout_dir(), self.env_name, "colcon_ws" + ) + self.colcon_build_directory = "to_be_set" + self.colcon_rosinstall = "" + + self.script_list = list() + self.build = True + + self.create_catkin = False + self.create_colcon = False + self.create_misc_ws = False + + self.underlays = UnderlayManager(self.env_name) + + def create_new_environment( + self, + config_file, + no_build, + create_misc_ws, + create_catkin, + create_colcon, + copy_cmake_lists, + local_build, + ros_distro, + ros2_distro, + underlays, + ): + """Worker method that does the actual job""" + if os.path.exists(os.path.join(dir_helpers.get_checkout_dir(), self.env_name)): + # click.echo("An environment with the name \"{}\" already exists. Exiting now." + # .format(self.env_name)) + raise ModuleException( + 'Environment "{}" already exists'.format(self.env_name), "add" + ) + + has_nobackup = dir_helpers.check_nobackup(local_build) + self.build_base_dir = dir_helpers.get_build_base_dir(has_nobackup) + + self.misc_ws_build_directory = os.path.join( + self.build_base_dir, self.env_name, "misc_ws" + ) + self.catkin_build_directory = os.path.join( + self.build_base_dir, self.env_name, "catkin_ws", "build" + ) + self.colcon_build_directory = os.path.join( + self.build_base_dir, self.env_name, "colcon_ws", "build" + ) + + # If config file is give, parse it + if config_file: + self.parse_config(config_file) + # Otherwise ask the user or check given flags + else: + if create_misc_ws == "ask": + self.create_misc_ws = click.confirm( + "Would you like to create a misc workspace?", default=True + ) + else: + self.create_misc_ws = dir_helpers.yes_no_to_bool(create_misc_ws) + + if create_catkin == "ask": + self.create_catkin = click.confirm( + "Would you like to create a catkin_ws?", default=True + ) + else: + self.create_catkin = dir_helpers.yes_no_to_bool(create_catkin) + + if create_colcon == "ask": + self.create_colcon = click.confirm( + "Would you like to create a colcon_ws?", default=True + ) + else: + self.create_colcon = dir_helpers.yes_no_to_bool(create_colcon) + + click.echo('Creating environment with name "{}"'.format(self.env_name)) + + catkin_creator = None + if self.create_catkin: + catkin_creator = environment_helpers.CatkinCreator( + catkin_directory=self.catkin_directory, + build_directory=self.catkin_build_directory, + rosinstall=self.catkin_rosinstall, + copy_cmake_lists=copy_cmake_lists, + ros_distro=ros_distro, + no_submodules=self.no_submodules, + ) + colcon_creator = None + if self.create_colcon: + colcon_creator = environment_helpers.ColconCreator( + colcon_directory=self.colcon_directory, + build_directory=self.colcon_build_directory, + rosinstall=self.colcon_rosinstall, + ros2_distro=ros2_distro, + no_submodules=self.no_submodules, + ) + + if underlays == "ask": + self.underlays.query_underlays() + elif underlays == "skip": + pass + else: + raise NotImplementedError( + "Manually passing underlays isn't implemented yet." + ) + + if self.underlays.underlays: + if not no_build and (self.catkin_rosinstall or self.colcon_rosinstall): + click.secho( + "Underlays selected without the 'no-build' option with a specified workspace. " + "Initial build will be deactivated. " + "Please manually build your environment by calling `fzirob make`.", + fg="yellow", + ) + no_build = True + + # Let's get down to business + self.create_directories() + self.create_demo_docs() + self.create_demo_scripts() + + if self.create_misc_ws: + click.echo("Creating misc workspace") + environment_helpers.MiscCreator( + misc_ws_directory=self.misc_ws_directory, + rosinstall=self.misc_ws_rosinstall, + build_root=self.misc_ws_build_directory, + no_submodules=self.no_submodules, + ) + else: + click.echo("Requested to not create a misc workspace") + + # Check if we should create a catkin workspace and create one if desired + if catkin_creator: + click.echo("Creating catkin_ws") + catkin_creator.create() + else: + click.echo("Requested to not create a catkin_ws") + + if colcon_creator: + click.echo("Creating colcon_ws") + colcon_creator.create() + else: + click.echo("Requested to not create a colcon_ws") + + if not no_build: + if self.create_catkin and self.catkin_rosinstall != "": + ros_builder = build.CatkinBuilder( + name=catkin_creator.ros_distro, add_help_option=False + ) + ros_builder.invoke(None) + if self.create_colcon and self.colcon_rosinstall != "": + ros2_builder = build.ColconBuilder( + name=colcon_creator.ros2_distro, add_help_option=False + ) + ros2_builder.invoke(None) + + def create_directories(self): + """Creates the directory skeleton with build_directories and symlinks""" + os.mkdir(os.path.join(dir_helpers.get_checkout_dir(), self.env_name)) + os.mkdir(self.demos_dir) + + # Add a custom source file to the environment. Custom source commands go in here. + env_source_file = open( + os.path.join( + dir_helpers.get_checkout_dir(), self.env_name, "setup_local.sh" + ), + "w", + ) + env_source_file.write( + "#This file is for custom source commands in this environment.\n" + ) + env_source_file.write( + '# zsh\nif [ -n "${ZSH_VERSION+1}" ]; then\n # Get the base directory where the install script is located\n' + ' setup_local_dir="$( cd "$( dirname "${(%):-%N}" )" && pwd )"\nfi\n\n' + '# bash\nif [ -n "${BASH_VERSION+1}" ]; then\n' + " # Get the base directory where the install script is located\n" + ' setup_local_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"\nfi\n' + ) + env_source_file.close() + + self.underlays.write_underlay_file() + + os.symlink( + os.path.join(dir_helpers.get_base_dir(), "bin", "source_environment.sh"), + os.path.join(dir_helpers.get_checkout_dir(), self.env_name, "setup.sh"), + ) + + def parse_config(self, config_file): + """Parses a config file to get an environment information""" + parser = ConfigFileParser(config_file) + + (self.create_misc_ws, self.misc_ws_rosinstall) = parser.parse_misc_ws_config() + + # Parse catkin_workspace packages + self.create_catkin, self.catkin_rosinstall = parser.parse_ros_config() + + # Parse colcon_workspace packages + self.create_colcon, self.colcon_rosinstall = parser.parse_ros2_config() + + # Parse demo scripts and copy them to individual files + self.script_list = parser.parse_demo_scripts() + + def create_demo_scripts(self): + """If there are demo scripts given to the environment, create them.""" + click.echo("Found the following demo scripts:") + for demo in self.script_list: + click.echo(demo) + filename = os.path.join(self.demos_dir, demo) + with open(filename, mode="w") as script_file_content: + script_file_content.write(self.script_list[demo]) + script_file_content.close() + os.chmod( + filename, + stat.S_IRWXU + | stat.S_IRGRP + | stat.S_IXGRP + | stat.S_IROTH + | stat.S_IXOTH, + ) + + def create_demo_docs(self): + """Creates a readme.txt in the demos folder to explain it's functionality""" + doc_filename = os.path.join(self.demos_dir, "readme.txt") + with open(doc_filename, "w") as out_file: + docstring = """This folder can contain any executable files. These files can be run + with the fzirob run command. When scraping an environment to a config file + all demo scripts will be copied into the environment config, as well.""" + out_file.write(docstring) + + +@click.command("add_environment", short_help="Add a new environment") +@click.option("--config_file", help="Create an environment from a given config file.") +@click.option( + "--no_build", is_flag=True, default=False, help="Do not perform an initial build." +) +@click.option( + "--create_misc_ws", + type=click.Choice(["yes", "no", "ask"]), + default="ask", + help="If set to 'yes', a folder for miscellaneous projects like \"fla\"-libraries or plain cmake projects is created and its export path is added to the environment.", +) +@click.option( + "--create_catkin", + type=click.Choice(["yes", "no", "ask"]), + default="ask", + help="If set to 'yes', a catkin-workspace is created without asking for it again.", +) +@click.option( + "--create_colcon", + type=click.Choice(["yes", "no", "ask"]), + default="ask", + help="If set to 'yes', a colcon-workspace is created without asking for it again.", +) +@click.option( + "--copy_cmake_lists", + type=click.Choice(["yes", "no", "ask"]), + default="ask", + help=( + "If set to 'yes', the toplevel CMakeLists files of the catkin workspace is " + "copied to the src folder of the catkin ws without asking for it again." + ), +) +@click.option( + "--local_build", + type=click.Choice(["yes", "no", "ask"]), + default="ask", + help=( + "If set to 'yes', the environment folder will be used for building directly. " + " If set to 'no', builds will be done inside the `no_backup` directory" + ), +) +@click.option( + "--ros_distro", + default="ask", + help=( + "If set, use this ROS1 distro instead of asking when multiple ROS1 distros are present on the system." + ), +) +@click.option( + "--ros2_distro", + default="ask", + help=( + "If set, use this ROS2 distro instead of asking when multiple ROS2 distros are present on the system." + ), +) +@click.option( + "--no_submodules", + default=False, + is_flag=True, + help="Prevent git submodules from being cloned", +) +@click.option( + "--underlays", + type=click.Choice(["ask", "skip"]), + default="ask", + help=( + 'When set to "ask" the user will be prompted for a list of underlays ' + 'to be used. When set to "skip", no underlays will be configured.' + ), +) +@click.argument("env_name", nargs=1) +def cli( + env_name, + config_file, + no_build, + create_misc_ws, + create_catkin, + create_colcon, + copy_cmake_lists, + local_build, + ros_distro, + ros2_distro, + no_submodules, + underlays, +): + """Adds a new environment and creates the basic needed folders, + e.g. a colcon_workspace and a catkin_ws.""" + environment_creator = EnvCreator(env_name, no_submodules=no_submodules) + environment_creator.build = not no_build + + is_env_active = False + if os.environ.get("ROB_FOLDERS_ACTIVE_ENV"): + is_env_active = True + os.environ["ROB_FOLDERS_ACTIVE_ENV"] = env_name + + try: + environment_creator.create_new_environment( + config_file, + no_build, + create_misc_ws, + create_catkin, + create_colcon, + copy_cmake_lists, + local_build, + ros_distro, + ros2_distro, + underlays, + ) + except subprocess.CalledProcessError as err: + raise (ModuleException(str(err), "add")) + except Exception as err: + click.echo(err) + click.echo("Something went wrong while creating the environment!") + raise (ModuleException(str(err), "add")) + click.echo("Initial workspace setup completed") + + if not is_env_active: + click.echo("Writing env %s into .cur_env" % env_name) + with open( + os.path.join(dir_helpers.get_checkout_dir(), ".cur_env"), "w" + ) as cur_env_file: + cur_env_file.write("{}".format(env_name)) diff --git a/src/robot_folders/commands/cd.py b/src/robot_folders/commands/cd.py new file mode 100644 index 0000000..da79f49 --- /dev/null +++ b/src/robot_folders/commands/cd.py @@ -0,0 +1,93 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +"""This command populates the 'cd' functionality""" +import os +import click + +from robot_folders.helpers.workspace_chooser import WorkspaceChooser +import robot_folders.helpers.directory_helpers as dir_helpers + + +class CdCommand(click.Command): + """Command to output a cd command""" + + def __init__(self, name=None, target_dir=None, **attrs): + click.Command.__init__(self, name, **attrs) + + self.short_help = target_dir + self.target_dir = target_dir + + def invoke(self, ctx): + """Prints a cd command to the output""" + click.echo("cd {}".format(self.target_dir)) + + +class CdChooser(WorkspaceChooser): + """Class implementing the cd command""" + + def get_command(self, ctx, name): + env = dir_helpers.get_active_env() + if env is None: + click.echo( + "No active environment found. Using most recently activated \ +environment '{}'".format( + dir_helpers.get_last_activated_env() + ) + ) + env = dir_helpers.get_last_activated_env() + + target_dir = dir_helpers.get_active_env_path() + if name in self.list_commands(ctx): + if name == "ros": + target_dir = dir_helpers.get_catkin_dir() + elif name == "colcon": + target_dir = dir_helpers.get_colcon_dir() + else: + click.echo( + "Did not find a workspace with the key < {} > inside " + "current environment < {} >.".format(name, env) + ) + return self + + return CdCommand(name=name, target_dir=target_dir) + + +@click.command( + "cd", + cls=CdChooser, + invoke_without_command=True, + short_help="CDs to a workspace inside the active environment", +) +@click.pass_context +def cli(ctx): + """CDs to a workspace inside the active environment""" + + if ctx.invoked_subcommand is None and ctx.parent.invoked_subcommand == "cd": + if dir_helpers.get_active_env() is None: + click.echo( + "No active environment found. Using most recently " + "activated environment" + ) + active_env_path = dir_helpers.get_active_env_path() + if active_env_path is not None: + click.echo("cd {}".format(active_env_path)) + return diff --git a/src/robot_folders/commands/change_environment.py b/src/robot_folders/commands/change_environment.py new file mode 100644 index 0000000..26d454f --- /dev/null +++ b/src/robot_folders/commands/change_environment.py @@ -0,0 +1,98 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +"""This command implements the change_environment functionality""" +import os +import click +import subprocess + +import robot_folders.helpers.directory_helpers as dir_helpers +from robot_folders.helpers.exceptions import ModuleException + + +def set_active_env(env_name): + with open( + os.path.join(dir_helpers.get_checkout_dir(), ".cur_env"), "w" + ) as cur_env_file: + cur_env_file.write("{}".format(env_name)) + + +class EnvironmentChoice(click.Command): + """ + Writes the chosen environment into the cur_env temp file. + NOTE: The actual sourcing takes place in a a shell script that reads that file's content + """ + + def invoke(self, ctx): + set_active_env(self.name) + + +class EnvironmentChooser(click.MultiCommand): + """Class that helps finding and choosing an environment.""" + + def list_commands(self, ctx): + return dir_helpers.list_environments() + + def get_command(self, ctx, name): + # return empty command with the correct name + if name in dir_helpers.list_environments(): + cmd = EnvironmentChoice(name=name, add_help_option=False) + return cmd + else: + click.echo("No environment with name < %s > found." % name) + raise ModuleException("unknown environment", "EnvironmentChoice", 1) + + def invoke(self, ctx): + try: + super(EnvironmentChooser, self).invoke(ctx) + except subprocess.CalledProcessError as err: + raise (ModuleException(str(err), "change")) + + +@click.command( + "change_environment", + cls=EnvironmentChooser, + short_help="Source an existing environment", + invoke_without_command=True, +) +@click.pass_context +def cli(ctx): + """ + Changes the global environment to the specified one. All environment-specific commands + are then executed relative to that environment. + Only one environment can be active at a time. + """ + if ctx.invoked_subcommand is None: + env_name = dir_helpers.get_last_activated_env() + if env_name is not None: + active_env_name = os.environ.get("ROB_FOLDERS_ACTIVE_ENV") + if active_env_name: + click.echo(f"Re-sourcing {active_env_name}") + set_active_env(active_env_name) + else: + click.echo( + "No environment specified. Sourcing the most recent active environment: {}".format( + env_name + ) + ) + else: + pass + # print(env_name) diff --git a/src/robot_folders/commands/clean.py b/src/robot_folders/commands/clean.py new file mode 100644 index 0000000..543f63a --- /dev/null +++ b/src/robot_folders/commands/clean.py @@ -0,0 +1,81 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +"""Command to perform environment builds""" +import click + +from robot_folders.helpers.workspace_chooser import WorkspaceChooser +import robot_folders.helpers.clean_helpers as clean +from robot_folders.helpers.directory_helpers import get_active_env + + +class CleanChooser(WorkspaceChooser): + """Checks which workspaces are inside an env and returns these as subcommands""" + + def get_command(self, ctx, name): + if get_active_env() is None: + # click.echo("Currently, there is no sourced environment. " + # "Please source one before calling the make function.") + return self + + if name in self.list_commands(ctx): + if name == "ros": + return clean.CatkinCleaner(name=name, add_help_option=False) + elif name == "colcon": + return clean.ColconCleaner(name=name, add_help_option=False) + else: + click.echo("Did not find a workspace with the key < {} >.".format(name)) + return None + + return self + + +@click.command( + "clean", + cls=CleanChooser, + invoke_without_command=True, + short_help="Cleans an environment", +) +@click.pass_context +def cli(ctx): + """ Cleans the currently active environment (deletes build and install files). + You can choose to only clean one of \ +the workspaces by adding the respective arg. Use tab completion to see which \ +workspaces are present. + """ + if get_active_env() is None: + click.echo( + "Currently, there is no sourced environment. Please source one \ +before calling the clean function." + ) + return + + if ctx.invoked_subcommand is None and ctx.parent.invoked_subcommand == "clean": + click.echo("clean called without argument. Cleaning everything") + + # Check which workspaces are present + cmd = CleanChooser(ctx) + + # Build all present workspaces individually + for workspace in cmd.list_commands(ctx): + clean_cmd = cmd.get_command(ctx, workspace) + clean_cmd.invoke(ctx) + return diff --git a/src/robot_folders/commands/delete_environment.py b/src/robot_folders/commands/delete_environment.py new file mode 100644 index 0000000..496a773 --- /dev/null +++ b/src/robot_folders/commands/delete_environment.py @@ -0,0 +1,165 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +""" +Implements the delete command +""" +import os +import shutil +import getpass +import click + +import robot_folders.helpers.directory_helpers as directory_helpers + + +def append_to_list_if_symlink(path, delete_list): + """Checks whether the given path exists and is a symlink and appends it to the + given list if both are true""" + if os.path.exists(os.path.realpath(path)): + if os.path.islink(path): + delete_list.append(os.path.realpath(path)) + return True + return False + + +def append_to_list_if_folder(path, delete_list): + """Appends the given path to the given list, if it exists, is a folder + and is not in the list yet. + Returns true, if folder exists, false otherwise.""" + if os.path.exists(os.path.realpath(path)): + if os.path.isdir(path) and os.path.realpath(path) not in delete_list: + delete_list.append(os.path.realpath(path)) + return True + return False + + +def delete_folder(path): + """Deletes the given path, if it exists and is a folder. + Returns true, if folder exists and was successfully delete.""" + if os.path.exists(path): + if os.path.isdir(path): + shutil.rmtree(path) + return True + return False + + +class EnvironmentDeleter(click.Command): + """Command that deletes an environment""" + + def __init__(self, name=None, **attrs): + click.Command.__init__(self, name, **attrs) + + self.force = False + + def invoke(self, ctx): + env_dir = os.path.join(directory_helpers.get_checkout_dir(), self.name) + catkin_dir = directory_helpers.get_catkin_dir(env_dir) + + delete_list = list() + + self.force = ctx.parent.params["force"] + + # Catkin workspace + if catkin_dir is not None: + catkin_build = os.path.join(catkin_dir, "build") + catkin_devel = os.path.join(catkin_dir, "devel") + catkin_install = os.path.join(catkin_dir, "install") + append_to_list_if_symlink(catkin_build, delete_list) + append_to_list_if_symlink(catkin_devel, delete_list) + append_to_list_if_symlink(catkin_install, delete_list) + append_to_list_if_folder(catkin_dir, delete_list) + else: + click.echo("No catkin workspace found") + + delete_list.append(env_dir) + + # no_backup build base + build_base_dir = directory_helpers.get_build_base_dir(use_no_backup=True) + append_to_list_if_folder(os.path.join(build_base_dir, self.name), delete_list) + + click.echo( + "Going to delete the following paths:\n{}".format("\n".join(delete_list)) + ) + confirmed = False + if self.force: + confirmed = True + else: + confirm = click.prompt( + "Please confirm by typing in the environment name '{}' once again.\nWARNING: " + "After this all environment files will be deleted and cannot be recovered! " + "If you wish to abort your delete request, type 'abort'".format( + self.name + ), + type=click.Choice([self.name, "abort"]), + default="abort", + ) + if confirm == self.name: + confirmed = True + + if confirmed: + click.echo("performing deletion!") + for folder in delete_list: + click.echo("Deleting {}".format(folder)) + delete_folder(folder) + click.echo("Successfully deleted environment '{}'".format(self.name)) + else: + click.echo("Delete request aborted. Nothing happened.") + + +class EnvironmentChooser(click.MultiCommand): + """Choose an environment""" + + def list_commands(self, ctx): + return directory_helpers.list_environments() + + def get_command(self, ctx, name): + # return empty command with the correct name + if name in directory_helpers.list_environments(): + cmd = EnvironmentDeleter(name=name) + return cmd + else: + click.echo("No environment with name < %s > found." % name) + return None + + +@click.command( + "delete_environment", + cls=EnvironmentChooser, + short_help="Deletes an environment from the checkout folder.", + invoke_without_command=True, +) +@click.option( + "--force", + default=False, + is_flag=True, + help="Skip confirmation and delete directly. This is meant for automated runs only.", +) +@click.pass_context +def cli(ctx, force): + """Removes an existing environment. This means that all files from this environment + will be deleted from the checkout folder. If build or install directories are symlinked + to another location (e.g. because it was built on no_backup), those will be deleted as well. + """ + if ctx.invoked_subcommand is None: + click.echo( + "No environment specified. Please choose one " + "of the available environments!" + ) diff --git a/src/robot_folders/commands/get_checkout_base_dir.py b/src/robot_folders/commands/get_checkout_base_dir.py new file mode 100644 index 0000000..71df42f --- /dev/null +++ b/src/robot_folders/commands/get_checkout_base_dir.py @@ -0,0 +1,30 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +"""Command to reveive the real checkout base directory""" +import click +from robot_folders.helpers.directory_helpers import get_checkout_dir + + +@click.command("get_checkout_base_dir") +def cli(): + """Command implementation to get the checkout directory""" + click.echo(get_checkout_dir()) diff --git a/src/robot_folders/commands/make.py b/src/robot_folders/commands/make.py new file mode 100644 index 0000000..6dfb36c --- /dev/null +++ b/src/robot_folders/commands/make.py @@ -0,0 +1,86 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +"""Command to perform environment builds""" +import click +import subprocess + +from robot_folders.helpers.workspace_chooser import WorkspaceChooser +import robot_folders.helpers.build_helpers as build +from robot_folders.helpers.directory_helpers import get_active_env +from robot_folders.helpers.exceptions import ModuleException + + +class BuildChooser(WorkspaceChooser): + """Checks which workspaces are inside an env and returns these as subcommands""" + + def get_command(self, ctx, name): + if get_active_env() is None: + # click.echo("Currently, there is no sourced environment. " + # "Please source one before calling the make function.") + return self + + if name in self.list_commands(ctx): + if name == "ros": + return build.CatkinBuilder(name=name, add_help_option=False) + elif name == "colcon": + return build.ColconBuilder(name=name, add_help_option=False) + else: + click.echo("Did not find a workspace with the key < {} >.".format(name)) + return None + + return self + + def invoke(self, ctx): + ### may raise an error. + super(BuildChooser, self).invoke(ctx) + + +@click.command( + "make", + cls=BuildChooser, + invoke_without_command=True, + short_help="Builds an environment", +) +@click.pass_context +def cli(ctx): + """ Builds the currently active environment. You can choose to only build one of \ +the workspaces by adding the respective arg. Use tab completion to see which \ +workspaces are present. + """ + if get_active_env() is None: + click.echo( + "Currently, there is no sourced environment. Please source one \ +before calling the make function." + ) + return + + if ctx.invoked_subcommand is None and ctx.parent.invoked_subcommand == "make": + click.echo("make called without argument. Building everything") + + # Check which workspaces are present + cmd = BuildChooser(ctx) + + # Build all present workspaces individually + for workspace in cmd.list_commands(ctx): + build_cmd = cmd.get_command(ctx, workspace) + build_cmd.invoke(ctx) + return diff --git a/src/robot_folders/commands/manage_underlays.py b/src/robot_folders/commands/manage_underlays.py new file mode 100644 index 0000000..b30c78e --- /dev/null +++ b/src/robot_folders/commands/manage_underlays.py @@ -0,0 +1,50 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +"""This command populates the 'cd' functionality""" +import os +import click + +from robot_folders.helpers.workspace_chooser import WorkspaceChooser +import robot_folders.helpers.directory_helpers as dir_helpers +from robot_folders.helpers.underlays import UnderlayManager + + +@click.command( + "manage_underlays", + short_help="Manage underlay workspaces used for the current workspace.", +) +def cli(): + """Opens a selection menu to manage the underlay environments used for the workspace.""" + + current_env = dir_helpers.get_active_env() + + if current_env is None: + click.echo( + "Currently, there is no sourced environment. Please source one \ +before calling the manage_underlays function." + ) + return + underlay_manager = UnderlayManager(current_env) + underlays = underlay_manager.read_underlay_file() + print(underlays) + underlay_manager.query_underlays(active_list=underlays) + underlay_manager.write_underlay_file() diff --git a/src/robot_folders/commands/run.py b/src/robot_folders/commands/run.py new file mode 100644 index 0000000..c830148 --- /dev/null +++ b/src/robot_folders/commands/run.py @@ -0,0 +1,91 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +"""Command to run scripts from the demos folder""" +import os +import subprocess +import click + +from robot_folders.helpers.directory_helpers import get_active_env_path, get_active_env + + +def get_demo_binaries(): + """List all executable scripts in the demos folder""" + demo_dir = os.path.join(get_active_env_path(), "demos") + if not os.path.exists(demo_dir): + return list() + + script_list = [ + script_file + for script_file in os.listdir(demo_dir) + if os.path.isfile(os.path.join(demo_dir, script_file)) + and os.access(os.path.join(demo_dir, script_file), os.X_OK) + ] + return script_list + + +class ScriptExecutor(click.Command): + """Command implementation for script running""" + + def invoke(self, ctx): + demo_dir = os.path.join(get_active_env_path(), "demos") + process = subprocess.Popen(["bash", "-c", os.path.join(demo_dir, self.name)]) + process.wait() + + +class ScriptSelector(click.MultiCommand): + """Class that helps finding the right demo script""" + + def list_commands(self, ctx): + return get_demo_binaries() + + def get_command(self, ctx, name): + if name in get_demo_binaries(): + cmd = ScriptExecutor(name=name) + return cmd + else: + click.echo("No such executable: {}".format(name)) + return None + + +@click.command( + "run", + cls=ScriptSelector, + short_help="Run a demo script", + invoke_without_command=True, +) +@click.pass_context +def cli(ctx): + """Runs an executable script inside the environment's 'demos' directory.""" + if get_active_env() is None: + click.echo( + "Currently, there is no sourced environment. " + "Please source one before calling the make function." + ) + return + + if ctx.invoked_subcommand is None: + cmd = ScriptSelector(ctx) + click.echo( + "No demo script specified. Please specify one of {}".format( + cmd.list_commands(ctx) + ) + ) diff --git a/src/robot_folders/commands/scrape_environment.py b/src/robot_folders/commands/scrape_environment.py new file mode 100644 index 0000000..ea1741f --- /dev/null +++ b/src/robot_folders/commands/scrape_environment.py @@ -0,0 +1,160 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +"""Command that scrapes an environment's configuration into a config file""" +import os +import click + +from yaml import safe_dump as yaml_safe_dump + +from robot_folders.helpers.directory_helpers import ( + get_checkout_dir, + get_catkin_dir, + get_colcon_dir, + list_environments, +) +from robot_folders.helpers.repository_helpers import create_rosinstall_entry + + +class EnvironmentScraper(click.Command): + """Class that implements the command""" + + def __init__(self, name=None, **attrs): + click.Command.__init__(self, name, **attrs) + + self.use_commit_id = False + + def invoke(self, ctx): + env_dir = os.path.join(get_checkout_dir(), self.name) + misc_ws_pkg_dir = os.path.join(env_dir, "misc_ws") + catkin_dir = get_catkin_dir(env_dir) + catkin_src_dir = os.path.join(catkin_dir, "src") + colcon_dir = get_colcon_dir(env_dir) + colcon_src_dir = os.path.join(colcon_dir, "src") + demos_dir = os.path.join(env_dir, "demos") + + self.use_commit_id = ctx.parent.params["use_commit_id"] + + yaml_data = dict() + + if os.path.isdir(misc_ws_pkg_dir): + click.echo("Scraping misc_ws_dir") + yaml_data["misc_ws"] = dict() + yaml_data["misc_ws"]["rosinstall"] = self.parse_folder(misc_ws_pkg_dir) + + if os.path.isdir(catkin_src_dir): + click.echo("Scraping catkin workspace") + yaml_data["catkin_workspace"] = dict() + yaml_data["catkin_workspace"]["rosinstall"] = self.parse_folder( + catkin_src_dir + ) + + if os.path.isdir(colcon_src_dir): + click.echo("Scraping colcon workspace") + yaml_data["colcon_workspace"] = dict() + yaml_data["colcon_workspace"]["rosinstall"] = self.parse_folder( + colcon_src_dir + ) + + if os.path.isdir(demos_dir): + yaml_data["demos"] = dict() + script_list = [ + script_file + for script_file in os.listdir(demos_dir) + if os.path.isfile(os.path.join(demos_dir, script_file)) + and os.access(os.path.join(demos_dir, script_file), os.X_OK) + ] + for script in script_list: + script_path = os.path.join(demos_dir, script) + with open(script_path, "r") as filecontent: + # content = f.read() + yaml_data["demos"][script] = filecontent.read() + filecontent.close() + + yaml_stream = open(ctx.params["out_file"], "w") + yaml_safe_dump( + yaml_data, stream=yaml_stream, encoding="utf-8", allow_unicode=True + ) + + def parse_folder(self, folder): + """Recursively parse subfolders""" + repos = list() + files = os.walk(folder) + for elem in files: + try: + subfolder = elem[0] + except UnicodeDecodeError: + click.echo("Unicode parsing error within folder {}".format(folder)) + continue + subfolder_abs = os.path.join(folder, subfolder) + git_dir = os.path.join(subfolder_abs, ".git") + local_name = os.path.relpath(subfolder, folder) + if os.path.isdir(git_dir): + click.echo(local_name) + entry = create_rosinstall_entry( + subfolder_abs, local_name, self.use_commit_id + ) + repos.append(entry) + return sorted(repos, key=lambda k: k["git"]["local-name"]) + + +class EnvironmentChooser(click.MultiCommand): + """Select the requested environment""" + + def list_commands(self, ctx): + return list_environments() + + def get_command(self, ctx, name): + # return empty command with the correct name + if name in list_environments(): + cmd = EnvironmentScraper( + name=name, params=[click.Argument(param_decls=["out_file"])] + ) + return cmd + else: + click.echo("No environment with name < %s > found." % name) + return None + + +@click.command( + "scrape_environment", + cls=EnvironmentChooser, + short_help="Scrape an environment config to a config file", + invoke_without_command=True, +) +@click.option( + "--use_commit_id", + is_flag=True, + default=False, + help="If checked, the exact commit IDs get scraped instead of branch names.", +) +@click.pass_context +def cli(ctx, use_commit_id): + """Scrapes an environment configuration into a config file, + so that it can be given to somebody else. \ + This config file can then be used to initialize the environment + in another robot_folders configuration. + """ + if ctx.invoked_subcommand is None: + click.echo( + "No environment specified. Please choose one " + "of the available environments!" + ) diff --git a/src/robot_folders/helpers/ConfigParser.py b/src/robot_folders/helpers/ConfigParser.py new file mode 100644 index 0000000..34bf31e --- /dev/null +++ b/src/robot_folders/helpers/ConfigParser.py @@ -0,0 +1,74 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +"""This module help parsing environment config files""" +import click + +import yaml + + +class ConfigFileParser(object): + """Parser for robot_folders environment configs""" + + def __init__(self, config_file_name): + with open(config_file_name, "r") as file_content: + self.data = yaml.load(file_content, Loader=yaml.SafeLoader) + click.echo("The following config file is passed:\n{}".format(self.data)) + + def parse_misc_ws_config(self): + """Parses the misc_ws part of the data""" + has_misc_ws = False + misc_ws_rosinstall = None + if "misc_ws" in self.data: + has_misc_ws = True + if "rosinstall" in self.data["misc_ws"]: + misc_ws_rosinstall = self.data["misc_ws"]["rosinstall"] + return has_misc_ws, misc_ws_rosinstall + + def parse_ros_config(self): + """Parses the catkin_workspace part of the data""" + ros_rosinstall = None + has_catkin = False + if "catkin_workspace" in self.data: + has_catkin = True + if "rosinstall" in self.data["catkin_workspace"]: + ros_rosinstall = self.data["catkin_workspace"]["rosinstall"] + + return has_catkin, ros_rosinstall + + def parse_ros2_config(self): + """Parses the colcon_workspace part of the data""" + ros2_rosinstall = None + has_colcon = False + if "colcon_workspace" in self.data: + has_colcon = True + if "rosinstall" in self.data["colcon_workspace"]: + ros2_rosinstall = self.data["colcon_workspace"]["rosinstall"] + + return has_colcon, ros2_rosinstall + + def parse_demo_scripts(self): + """Parses the demos part of the data""" + script_list = dict() + if "demos" in self.data: + for script in self.data["demos"]: + script_list[script] = self.data["demos"][script] + return script_list diff --git a/src/robot_folders/helpers/__init__.py b/src/robot_folders/helpers/__init__.py new file mode 100644 index 0000000..3130f78 --- /dev/null +++ b/src/robot_folders/helpers/__init__.py @@ -0,0 +1,21 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# diff --git a/src/robot_folders/helpers/build_helpers.py b/src/robot_folders/helpers/build_helpers.py new file mode 100644 index 0000000..976eec7 --- /dev/null +++ b/src/robot_folders/helpers/build_helpers.py @@ -0,0 +1,313 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +"""Module that helps building workspaces""" +import os +import re +import subprocess +import click + +from robot_folders.helpers.directory_helpers import ( + get_active_env_path, + mkdir_p, + get_catkin_dir, + get_colcon_dir, +) +from robot_folders.helpers.which import which +from robot_folders.helpers import compilation_db_helpers +from robot_folders.helpers import config_helpers +from robot_folders.helpers.exceptions import ModuleException + + +def get_cmake_flags(): + """Reads the configuration for default cmake flags""" + generator = config_helpers.get_value_safe_default( + section="build", value="generator", default="make" + ) + + cmake_flags = config_helpers.get_value_safe_default( + section="build", value="cmake_flags", default="" + ) + + if generator == "ninja": + cmake_flags = " ".join([cmake_flags, "-GNinja"]) + + if which(generator) is None: + click.echo( + "WARNING: Generator '{}' was requested. However, " + "that generator seems not to be installed. " + "Will fallback to make instead.".format(generator) + ) + cmake_flags = "" + return cmake_flags + + +def get_cmake_flags(): + cmake_flags = config_helpers.get_value_safe_default( + section="build", value="cmake_flags", default="" + ) + return cmake_flags + + +class Builder(click.Command): + """General builder class""" + + build_dir = "build" + + def get_build_command(self): + """Determine whether to use make or ninja. If it cannot be guessed + from the build directory, use the config value""" + + build_cmd = "make" + + cmake_cache_file = os.path.join(self.build_dir, "CMakeCache.txt") + search_str = "CMAKE_MAKE_PROGRAM:FILEPATH=" + if os.path.isfile(cmake_cache_file): + for line in open(cmake_cache_file): + start = line.find(search_str) + if start > -1: + # remove any trailing chars like newlines + build_cmd = os.path.basename( + os.path.normpath(line[start + len(search_str) :].rstrip()) + ) + else: + build_cmd = config_helpers.get_value_safe_default( + section="build", value="generator", default="make" + ) + + if which(build_cmd) is None: + click.echo( + "WARNING: Generator '{}' was requested. However, " + "that generator seems not to be installed. " + "Will fallback to make instead.".format(build_cmd) + ) + build_cmd = "make" + + if "make" in build_cmd: + if "-j" not in build_cmd: + num_threads = config_helpers.get_value_safe_default( + section="build", value="make_threads", default=2 + ) + build_cmd = " ".join([build_cmd, "-j", str(num_threads)]) + + if self.should_install(): + build_cmd = " ".join([build_cmd, "install"]) + + return build_cmd + + def check_previous_build(self, base_directory): + """Checks whether the build directory exists and creates it if needed. + Also performs an initial cmake command, if no CMakeCache.txt exists.""" + mkdir_p(self.build_dir) + cmake_cache_file = os.path.join(self.build_dir, "CMakeCache.txt") + if not os.path.isfile(cmake_cache_file): + cmake_cmd = " ".join(["cmake", base_directory, get_cmake_flags()]) + click.echo("Starting initial build with command\n\t{}".format(cmake_cmd)) + try: + process = subprocess.check_call( + ["bash", "-c", cmake_cmd], cwd=self.build_dir + ) + except subprocess.CalledProcessError as err: + raise ( + ModuleException(err.message, "build_check_previous", err.returncode) + ) + + def should_install(self): + """Checks if the build command should be run with the install option.""" + foobar = config_helpers.get_value_safe_default( + section="build", + value=self.get_install_key(), + default=str(self.get_install_default()), + ) + return foobar + + @classmethod + def get_install_key(cls): + """Returns the userconfig key for the install command of this builder.""" + return "" + + @classmethod + def get_install_default(cls): + """Returns the default install behavior for this builder, + if none is provided by the user config""" + return False + + +class ColconBuilder(Builder): + """Builder class for colcon workspace""" + + def get_build_command(self, ros_distro): + build_cmd = "colcon build" + + colcon_options = config_helpers.get_value_safe_default( + section="build", value="colcon_build_options", default="" + ) + + generator_flag = "" + generator = config_helpers.get_value_safe_default( + section="build", value="generator", default="make" + ) + if generator == "ninja": + generator_flag = "-GNinja" + + ros_global_dir = "/opt/ros/{}".format(ros_distro) + + if os.path.isdir(ros_global_dir): + build_cmd_with_source = "source {}/setup.bash && {}".format( + ros_global_dir, build_cmd + ) + build_cmd = build_cmd_with_source + + final_cmd = " ".join( + [ + build_cmd, + colcon_options, + "--cmake-args ", + generator_flag, + get_cmake_flags(), + ] + ) + + click.echo("Building with command " + final_cmd) + return final_cmd + + def invoke(self, ctx): + colcon_dir = get_colcon_dir() + click.echo("Building colcon_ws in {}".format(colcon_dir)) + + # Colcon needs to build in an env that does not have the current workspace sourced + # See https://docs.ros.org/en/galactic/Tutorials/Workspace/Creating-A-Workspace.html#source-the-overlay + my_env = os.environ.copy() + keys_with_colcon_dir = [key for key, val in my_env.items() if colcon_dir in val] + for key in keys_with_colcon_dir: + my_env[key] = re.sub(colcon_dir + r"[^:]*", "", my_env[key]) + + # We abuse the name to code the ros distribution if we're building for the first time. + try: + process = subprocess.check_call( + ["bash", "-c", self.get_build_command(self.name)], + cwd=colcon_dir, + env=my_env, + ) + except subprocess.CalledProcessError as err: + raise (ModuleException(err.output, "build_colcon", err.returncode)) + + +class CatkinBuilder(Builder): + """Builder class for catkin workspace""" + + def get_build_command(self, catkin_dir, ros_distro): + # default: make + build_cmd = config_helpers.get_value_safe_default( + section="build", value="catkin_make_cmd", default="catkin_make" + ) + mkdir_p(self.build_dir) + + if build_cmd != "catkin_make_isolated": + if os.path.isfile( + os.path.join( + get_catkin_dir(), "build_isolated", "catkin_make_isolated.cache" + ) + ): + click.echo( + "WARNING: There seems to be an existing isolated build environment in " + "{}, however catkin_make_isolated is not configured as build command.\n" + "The workspace will be built using catkin_make_isolated. If this is not " + "the desired behavior, please remove the isolated build folder." + ) + build_cmd = "catkin_make_isolated" + + generator_cmd = "" + cmake_cache_file = os.path.join(catkin_dir, "build", "CMakeCache.txt") + search_str = "CMAKE_MAKE_PROGRAM:FILEPATH=" + if os.path.isfile(cmake_cache_file): + for line in open(cmake_cache_file): + start = line.find(search_str) + if start > -1: + # remove any trailing chars like newlines + if "ninja" in line: + if build_cmd == "catkin build": + raise ( + ModuleException( + "Catkin build does not support an option for using ninja. " + "Please set the generator to make if you want to use catkin build", + "build_ros", + 1, + ) + ) + else: + generator_cmd = "--use-ninja" + else: + generator = config_helpers.get_value_safe_default( + section="build", value="generator", default="make" + ) + if generator == "ninja": + if build_cmd == "catkin build": + raise ( + ModuleException( + "Catkin build does not support an option for using ninja. Please set the" + "generator to make if you want to use catkin build", + "build_ros", + 1, + ) + ) + else: + generator_cmd = "--use-ninja" + + install_cmd = "" + if self.should_install(): + if build_cmd == "catkin_make": + install_cmd = "install" + elif build_cmd == "catkin_make_isolated": + install_cmd = "--install" + + ros_global_dir = "/opt/ros/{}".format(ros_distro) + + if os.path.isdir(ros_global_dir): + build_cmd_with_source = "source {}/setup.bash && {}".format( + ros_global_dir, build_cmd + ) + build_cmd = build_cmd_with_source + + final_cmd = " ".join([build_cmd, generator_cmd, install_cmd, get_cmake_flags()]) + click.echo("Building with command " + final_cmd) + return final_cmd + + def invoke(self, ctx): + catkin_dir = get_catkin_dir() + self.build_dir = os.path.join(catkin_dir, "build") + click.echo("Building catkin_workspace in {}".format(catkin_dir)) + + # We abuse the name to code the ros distribution if we're building for the first time. + try: + process = subprocess.check_call( + ["bash", "-c", self.get_build_command(catkin_dir, self.name)], + cwd=catkin_dir, + ) + except subprocess.CalledProcessError as err: + raise (ModuleException(err.output, "build_ros", err.returncode)) + + compilation_db_helpers.merge_compile_commands( + self.build_dir, os.path.join(catkin_dir, "compile_commands.json") + ) + + def get_install_key(self): + return "install_catkin" diff --git a/src/robot_folders/helpers/clean_helpers.py b/src/robot_folders/helpers/clean_helpers.py new file mode 100644 index 0000000..4c8205f --- /dev/null +++ b/src/robot_folders/helpers/clean_helpers.py @@ -0,0 +1,116 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +"""Module that helps cleaning workspaces""" +import os +import shutil +import click + +from robot_folders.helpers.directory_helpers import ( + get_active_env_path, + mkdir_p, + get_catkin_dir, + get_colcon_dir, +) +from robot_folders.helpers.which import which +from robot_folders.helpers import config_helpers + + +def clean_folder(folder): + """Deletes everything inside a given folder. The folder itself is not deleted.""" + click.echo("Cleaning everything in {}".format(folder)) + if os.path.isdir(folder): + for the_file in os.listdir(folder): + file_path = os.path.join(folder, the_file) + if os.path.islink(file_path): + click.echo("Deleting symlink {}".format(file_path)) + os.unlink(file_path) + elif os.path.isfile(file_path): + click.echo("Deleting file {}".format(file_path)) + os.unlink(file_path) + elif os.path.isdir(file_path): + click.echo("Deleting folder {}".format(file_path)) + shutil.rmtree(file_path) + else: + click.echo('Skipping non-existing folder "{}"'.format(folder)) + + +def confirm_deletion(delete_list): + """Requests a confirmation from the user that the mentioned paths should be deleted""" + click.echo( + "Going to delete all files inside the following paths:\n{}".format( + "\n".join(delete_list) + ) + ) + + confirm = click.prompt( + "Please confirm by typing 'clean' (case sensitive).\nWARNING: " + "After this all above mentioned paths will be cleaned and cannot be recovered! " + "If you wish to abort your delete request, type 'abort'", + type=click.Choice(["clean", "abort"]), + default="abort", + ) + return confirm == "clean" + + +class Cleaner(click.Command): + """General cleaner class""" + + # Dummy variable to satisfy the linter + clean_list = list() + + def clean(self): + """General clean function""" + if confirm_deletion(self.clean_list): + for folder in self.clean_list: + clean_folder(folder) + else: + click.echo("Cleaning not confirmed. Aborting now") + click.echo("") + + +class CatkinCleaner(Cleaner): + """Cleaner class for catkin workspace""" + + def invoke(self, ctx): + click.echo("========== Cleaning catkin workspace ==========") + catkin_dir = get_catkin_dir() + self.clean_list.append(os.path.join(catkin_dir, "build")) + self.clean_list.append(os.path.join(catkin_dir, "build_isolated")) + self.clean_list.append(os.path.join(catkin_dir, "devel")) + self.clean_list.append(os.path.join(catkin_dir, "devel_isolated")) + self.clean_list.append(os.path.join(catkin_dir, "install")) + self.clean_list.append(os.path.join(catkin_dir, "install_isolated")) + click.echo("Cleaning catkin_workspace in {}".format(catkin_dir)) + self.clean() + + +class ColconCleaner(Cleaner): + """Cleaner class for colcon workspace""" + + def invoke(self, ctx): + click.echo("========== Cleaning colcon workspace ==========") + colcon_dir = get_colcon_dir() + self.clean_list.append(os.path.join(colcon_dir, "build")) + self.clean_list.append(os.path.join(colcon_dir, "log")) + self.clean_list.append(os.path.join(colcon_dir, "install")) + click.echo("Cleaning colcon_workspace in {}".format(colcon_dir)) + self.clean() diff --git a/src/robot_folders/helpers/compilation_db_helpers.py b/src/robot_folders/helpers/compilation_db_helpers.py new file mode 100644 index 0000000..2a4bf57 --- /dev/null +++ b/src/robot_folders/helpers/compilation_db_helpers.py @@ -0,0 +1,44 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +"""Small module that helps combining compilation database""" + +import pathlib +import json + + +def find_compilation_db_files(root): + """Finds all `compilation_database.json` files under `root`""" + return pathlib.Path(root).rglob("compile_commands.json") + + +def merge_compile_commands(root, target_file): + """ + Merges all 'compile_commands.json' files under `root` and saves the result in `target_file` + """ + commands = [] + for filename in find_compilation_db_files(root): + with open(filename, "r") as content: + commands.extend(json.load(content)) + + if commands: + with open(target_file, "w") as out_file: + json.dump(commands, out_file, indent=4, sort_keys=True) diff --git a/src/robot_folders/helpers/config_helpers.py b/src/robot_folders/helpers/config_helpers.py new file mode 100644 index 0000000..7bc0602 --- /dev/null +++ b/src/robot_folders/helpers/config_helpers.py @@ -0,0 +1,144 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +"""Module for reading and handling the config.""" +from __future__ import print_function +import os +import shutil +import yaml + +from importlib import resources + +XDG_CONFIG_HOME = os.getenv( + "XDG_CONFIG_HOME", os.path.expandvars(os.path.join("$HOME", ".config")) +) +FILENAME_USERCONFIG = os.path.join(XDG_CONFIG_HOME, "robot_folders.yaml") + + +class Userconfig(object): + """Class for managing a userconfig""" + + config = None + config_fallback = None + initialized = False + + @classmethod + def init_class(cls): + """Load the distribution config file""" + with resources.path( + ".".join([__package__, "resources"]), "userconfig_distribute.yaml" + ) as p: + filename_distribute = p.as_posix() + file_content = p.read_text() + try: + Userconfig.config_fallback = yaml.safe_load(file_content) + except yaml.YAMLError as exc: + print("Error in configuration file:", exc) + except IOError as exc: + print("ERROR: There was a problem loading the distribution file:", exc) + + # Load the user-modified config file + try: + with open(FILENAME_USERCONFIG, "r") as file_content: + try: + Userconfig.config = yaml.safe_load(file_content) + except yaml.YAMLError as exc: + print("Error in configuration file:", exc) + except (IOError, FileNotFoundError) as exc: + print("Did not find userconfig file. Copying the distribution file.") + Userconfig.config = Userconfig.config_fallback + if not os.path.exists(XDG_CONFIG_HOME): + os.makedirs(XDG_CONFIG_HOME) + shutil.copy(filename_distribute, FILENAME_USERCONFIG) + Userconfig.initialized = True + + +# This is the internal yaml query. It can be used for the modified +# or the distribution config +def _get_value_safe(dictionary, section, value, debug=True): + result = None + try: + result = dictionary[section][value] + except KeyError: + if debug: + print("Did not find key {}.{}!!!".format(section, value)) + except TypeError: + print( + "There is an illegal config. " + "Probably something went wrong during initialization. " + "Check your config file." + ) + return result + + +# Interface funtion to the outside. You might want to consider the +def get_value_safe(section, value, debug=True): + """Retrieve an entry from the config backend. + + Will try to find the given entry in the config. If it is not + found in the user-modified config, the default value from the + distributed config will be used. + If it is not found at all, a None object will be returned. + + - **parameters**, **types**, **return** and **return types**:: + :param section: The top level key for the searched entry + :param value: The second level key for the searched entry + :param debug: Print debug output when searching for the entry (default True) + :type section: string + :type value: string + :type debug: bool + :returns: config entry + :rtype: mixed + """ + if not Userconfig.initialized: + Userconfig.init_class() + + result = _get_value_safe(Userconfig.config, section, value, debug=False) + if result is None: + result = _get_value_safe(Userconfig.config_fallback, section, value, debug) + return result + + +def get_value_safe_default(section, value, default, debug=True): + """Retrieve an entry from the config backend or use default value. + + Will try to find the given entry in the config. If it is not + found in the user-modified config, the default value from the + distributed config will be used. + If it is not found at all, the default value will be returned. + + - **parameters**, **types**, **return** and **return types**:: + :param section: The top level key for the searched entry + :param value: The second level key for the searched entry + :param default: The default entry if not found + :param debug: Print debug output when searching for the entry (default True) + :type section: string + :type value: string + :type default: mixed, the same as return type + :type debug: bool + :returns: config entry + :rtype: mixed + """ + result = get_value_safe(section, value, debug) + if result is None: + result = default + + return result diff --git a/src/robot_folders/helpers/directory_helpers.py b/src/robot_folders/helpers/directory_helpers.py new file mode 100644 index 0000000..ee3246e --- /dev/null +++ b/src/robot_folders/helpers/directory_helpers.py @@ -0,0 +1,247 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +"""Helpers around handling folders""" +from __future__ import print_function +import os +import errno +import getpass +import subprocess + +import click + +import robot_folders.helpers.config_helpers as config_helpers + + +def get_base_dir(): + """Returns the robot_folders base dir.""" + base_dir = os.environ["ROB_FOLDERS_BASE_DIR"] + return os.path.realpath(base_dir) + + +def get_last_activated_env(): + """Looks for the most recently sourced environment""" + env_file = os.path.join(get_checkout_dir(), ".cur_env") + + if os.path.isfile(env_file): + with open(env_file, "r") as file_content: + return file_content.read().rstrip() + else: + print( + "No recently activated environment found. Is this your first run?" + "Try to add an environment and then do a change_environment to this." + ) + return None + + +def get_active_env(): + """Returns the currently sourced environment. If none is sourced, this will return None""" + try: + active_env = os.environ["ROB_FOLDERS_ACTIVE_ENV"] + return active_env + except KeyError: + # print "Currently, there is no active environment!\n\ + # To get rid of this message, please source the most recent environment or \ + # change to another one." + return None + + +def get_active_env_path(): + """Returns the path of the currently sourced environment""" + active_env = get_active_env() + if active_env is None: + active_env_fallback = get_last_activated_env() + if active_env_fallback is None: + return None + active_env = active_env_fallback + return os.path.join(get_checkout_dir(), active_env) + + +def mkdir_p(path): + """Checks whether a directory exists, otherwise it will be created.""" + try: + os.makedirs(path) + except OSError as exc: + if exc.errno == errno.EEXIST and os.path.isdir(path): + pass + else: + raise + + +def recursive_rmdir(path): + """Recursively deletes a path""" + for i in os.listdir(path): + item = os.path.join(path, i) + if os.path.isdir(item): + recursive_rmdir(item) + else: + os.remove(item) + os.rmdir(path) + + +def get_checkout_dir(): + """Get the robot folders checkout directory from the userconfig""" + checkout_config = config_helpers.get_value_safe( + "directories", "checkout_dir", debug=False + ) + if checkout_config == "" or checkout_config is None: + checkout_config = "~/checkout" + expanded = os.path.expanduser(checkout_config) + if not os.path.exists(expanded): + mkdir_p(expanded) + return expanded + + +def get_catkin_dir(env_dir=""): + """Tries to find the right catkin workspace in the Currently \ + sourced environment.""" + + path = "" + cur_env_path = env_dir + if env_dir == "": + cur_env_path = get_active_env_path() + + valid_names = config_helpers.get_value_safe_default( + "directories", "catkin_names", ["catkin_workspace", "catkin_ws"], debug=False + ) + for path_name in valid_names: + path = os.path.join(cur_env_path, path_name) + if os.path.exists(path): + return path + return os.path.join(cur_env_path, "catkin_ws") + + +def get_colcon_dir(env_dir=""): + """Tries to find the right colcon workspace in the Currently \ + sourced environment.""" + + path = "" + cur_env_path = env_dir + if env_dir == "": + cur_env_path = get_active_env_path() + + valid_names = config_helpers.get_value_safe_default( + "directories", + "colcon_names", + ["colcon_workspace", "colcon_ws", "dev_ws"], + debug=False, + ) + for path_name in valid_names: + path = os.path.join(cur_env_path, path_name) + if os.path.exists(path): + return path + return os.path.join(cur_env_path, "colcon_ws") + + +def yes_no_to_bool(bool_str): + """ + Converts a yes/no string to a bool + """ + return bool_str == "yes" or bool_str == "Yes" + + +def check_nobackup(local_build="ask"): + """ + Checks whether there is a nobackup on this system. If there is, the local_build + parameter is used to determine whether a the no_backup folder should be used or + the local structure should be used. + + local_build == 'yes' - ignore nobackup even if it exists -> will return false + """ + # If the no_backup location exists, offer to build in no_backup + has_nobackup = False + no_backup_location = os.path.expanduser( + config_helpers.get_value_safe("directories", "no_backup_dir") + ) + try: + if os.path.isdir(no_backup_location): + has_nobackup = True + except subprocess.CalledProcessError: + pass + + if has_nobackup: + if local_build == "ask": + build_dir_choice = click.prompt( + "Which folder should I use as a base for creating the build tree?\n" + "Type 'local' for building inside the local checkout tree.\n" + "Type 'no_backup' (or simply press enter) for building in the no_backup " + "space.\n", + type=click.Choice(["no_backup", "local"]), + default="no_backup", + ) + build_dir_choice = build_dir_choice == "local" + else: + build_dir_choice = yes_no_to_bool(local_build) + + return not build_dir_choice + + +def get_build_base_dir(use_no_backup): + """ + Gets the base directory for building depending on whether no_backup should be used or not + """ + if use_no_backup: + no_backup_dir = config_helpers.get_value_safe("directories", "no_backup_dir") + build_base_dir = os.path.expanduser(f"{no_backup_dir}/robot_folders_build_base") + else: + build_base_dir = get_checkout_dir() + + return build_base_dir + + +def is_fzirob_environment(checkout_folder, env_dir): + """Checks whether a given directory actually contains an environment""" + is_environment = False + + environment_folders = config_helpers.get_value_safe_default( + section="directories", value="catkin_names", default=["catkin_workspace"] + ) + environment_folders = environment_folders + config_helpers.get_value_safe_default( + section="directories", value="colcon_names", default=["colcon_workspace"] + ) + environment_files = ["setup.bash", "setup.zsh", "setup.sh"] + + possible_env = os.path.join(checkout_folder, env_dir) + if os.path.isdir(possible_env): + # check folders for existence + for folder in environment_folders: + if os.path.isdir(os.path.join(possible_env, folder)): + is_environment = True + break + # check if folder was already found + if not is_environment: + # check files for existences + for filename in environment_files: + if os.path.exists(os.path.join(possible_env, filename)): + is_environment = True + break + + return is_environment + + +def list_environments(): + """List all environments""" + checkout_folder = get_checkout_dir() + return [ + env_dir + for env_dir in sorted(os.listdir(checkout_folder)) + if is_fzirob_environment(checkout_folder, env_dir) + ] diff --git a/src/robot_folders/helpers/environment_helpers.py b/src/robot_folders/helpers/environment_helpers.py new file mode 100644 index 0000000..d7cd40b --- /dev/null +++ b/src/robot_folders/helpers/environment_helpers.py @@ -0,0 +1,362 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +""" +Module with helper classes to create workspaces +""" +import os +import subprocess + +import click + +import inquirer + +import robot_folders.helpers.config_helpers as config_helpers +import robot_folders.helpers.build_helpers as build_helpers +import robot_folders.helpers.directory_helpers as dir_helpers +from robot_folders.helpers import config_helpers +from robot_folders.helpers.ros_version_helpers import * + +from yaml import dump as yaml_dump + + +class MiscCreator(object): + """ + Class to create a misc workspace + """ + + def __init__( + self, misc_ws_directory, build_root, rosinstall=None, no_submodules=False + ): + + self.misc_ws_directory = misc_ws_directory + self.build_root = build_root + self.no_submodules = no_submodules + + self.create_build_folders() + self.add_rosinstall(rosinstall) + + def add_rosinstall(self, rosinstall): + if rosinstall: + # Dump the rosinstall to a file and use vcstool for getting the packages + rosinstall_filename = "/tmp/rob_folders_rosinstall" + with open(rosinstall_filename, "w") as rosinstall_content: + yaml_dump(rosinstall, rosinstall_content) + + os.makedirs(self.misc_ws_directory, exist_ok=True) + if self.no_submodules: + process = subprocess.check_call( + ["vcs", "import", "--input", rosinstall_filename, "."], + cwd=self.misc_ws_directory, + ) + else: + process = subprocess.check_call( + [ + "vcs", + "import", + "--recursive", + "--input", + rosinstall_filename, + ".", + ], + cwd=self.misc_ws_directory, + ) + + os.remove(rosinstall_filename) + + def create_build_folders(self): + """ + Creates the necessary export directory of the misc workspace in the file system. If a remote build is used (e.g. + no_backup) then symlinks are created automatically. + """ + export_directory = os.path.join(self.build_root, "export") + local_export_dir_name = os.path.join(self.misc_ws_directory, "export") + + if local_export_dir_name != export_directory: + os.symlink(export_directory, local_export_dir_name) + + os.makedirs(export_directory) + + +class CatkinCreator(object): + """ + Creates a catkin workspace + """ + + def __init__( + self, + catkin_directory, + build_directory, + rosinstall, + copy_cmake_lists="ask", + ros_distro="ask", + no_submodules=False, + ): + self.catkin_directory = catkin_directory + self.build_directory = build_directory + self.copy_cmake_lists = copy_cmake_lists + self.ros_distro = ros_distro + self.rosinstall = rosinstall + self.no_submodules = no_submodules + + self.ask_questions() + self.ros_global_dir = "/opt/ros/{}".format(self.ros_distro) + + def create(self): + self.create_catkin_skeleton() + self.build() + self.clone_packages(self.rosinstall) + + if self.copy_cmake_lists: + if os.path.exists( + os.path.join(self.catkin_directory, "src", "CMakeLists.txt") + ): + subprocess.check_call( + ["rm", "{}/src/CMakeLists.txt".format(self.catkin_directory)] + ) + subprocess.check_call( + [ + "cp", + "{}/share/catkin/cmake/toplevel.cmake".format( + self.ros_global_dir + ), + "{}/src/CMakeLists.txt".format(self.catkin_directory), + ] + ) + + def ask_questions(self): + """ + When creating a catkin workspace some questions need to be answered such as which ros + version to use and whether to copy the CMakeLists.txt + """ + if self.ros_distro == "ask": + installed_ros_distros = sorted(installed_ros_1_versions()) + self.ros_distro = installed_ros_distros[-1] + if len(installed_ros_distros) > 1: + questions = [ + inquirer.List( + "ros_distro", + message="Which ROS distribution would you like to use for catkin?", + choices=installed_ros_distros, + ), + ] + self.ros_distro = inquirer.prompt(questions)["ros_distro"] + click.echo("Using ROS distribution '{}'".format(self.ros_distro)) + if self.copy_cmake_lists == "ask": + self.copy_cmake_lists = click.confirm( + "Would you like to copy the top-level " + "CMakeLists.txt to the catkin" + " src directory instead of using a symlink?\n" + "(This is incredibly useful when using the " + "QtCreator.)", + default=True, + ) + else: + self.copy_cmake_lists = dir_helpers.yes_no_to_bool(self.copy_cmake_lists) + + def build(self): + """ + Launch the build process + """ + # We abuse the name parameter to code the ros distribution + # if we're building for the first time. + ros_builder = build_helpers.CatkinBuilder( + name=self.ros_distro, add_help_option=False + ) + ros_builder.invoke(None) + + def create_catkin_skeleton(self): + """ + Creates the workspace skeleton and if necessary the relevant folders for remote build (e.g. + no_backup) + """ + # Create directories and symlinks, if necessary + os.mkdir(self.catkin_directory) + os.mkdir(os.path.join(self.catkin_directory, "src")) + os.makedirs(self.build_directory) + + local_build_dir_name = os.path.join(self.catkin_directory, "build") + (catkin_base_dir, _) = os.path.split(self.build_directory) + + catkin_devel_directory = os.path.join(catkin_base_dir, "devel") + local_devel_dir_name = os.path.join(self.catkin_directory, "devel") + click.echo("devel_dir: {}".format(catkin_devel_directory)) + + catkin_install_directory = os.path.join(catkin_base_dir, "install") + local_install_dir_name = os.path.join(self.catkin_directory, "install") + click.echo("install_dir: {}".format(catkin_install_directory)) + + if local_build_dir_name != self.build_directory: + os.symlink(self.build_directory, local_build_dir_name) + os.makedirs(catkin_devel_directory) + os.symlink(catkin_devel_directory, local_devel_dir_name) + os.makedirs(catkin_install_directory) + os.symlink(catkin_install_directory, local_install_dir_name) + + def clone_packages(self, rosinstall): + """ + Clone in packages froma rosinstall structure + """ + # copy packages + if rosinstall != "": + # Dump the rosinstall to a file and use vcstools for getting the packages + rosinstall_filename = "/tmp/rob_folders_rosinstall" + with open(rosinstall_filename, "w") as rosinstall_content: + yaml_dump(rosinstall, rosinstall_content) + + os.makedirs(os.path.join(self.catkin_directory, "src"), exist_ok=True) + if self.no_submodules: + process = subprocess.check_call( + ["vcs", "import", "--input", rosinstall_filename, "src"], + cwd=self.catkin_directory, + ) + else: + process = subprocess.check_call( + [ + "vcs", + "import", + "--recursive", + "--input", + rosinstall_filename, + "src", + ], + cwd=self.catkin_directory, + ) + + os.remove(rosinstall_filename) + + +class ColconCreator(object): + """ + Creates a colcon workspace + """ + + def __init__( + self, + colcon_directory, + build_directory, + rosinstall, + ros2_distro="ask", + no_submodules=False, + ): + self.colcon_directory = colcon_directory + self.build_directory = build_directory + self.ros2_distro = ros2_distro + self.rosinstall = rosinstall + self.no_submodules = no_submodules + + self.ask_questions() + + def create(self): + """Actually creates the workspace""" + self.create_colcon_skeleton() + self.build() + self.clone_packages(self.rosinstall) + + def ask_questions(self): + """ + When creating a colcon workspace some questions need to be answered such as which ros + version to use + """ + if self.ros2_distro == "ask": + installed_ros_distros = sorted(installed_ros_2_versions()) + self.ros_distro = installed_ros_distros[-1] + if len(installed_ros_distros) > 1: + questions = [ + inquirer.List( + "ros_distro", + message="Which ROS2 distribution would you like to use for colcon?", + choices=installed_ros_distros, + ), + ] + self.ros2_distro = inquirer.prompt(questions)["ros_distro"] + click.echo("Using ROS2 distribution '{}'".format(self.ros2_distro)) + + def build(self): + """ + Launch the build process + """ + # We abuse the name parameter to code the ros distribution + # if we're building for the first time. + ros2_builder = build_helpers.ColconBuilder( + name=self.ros2_distro, add_help_option=False + ) + ros2_builder.invoke(None) + + def create_colcon_skeleton(self): + """ + Creates the workspace skeleton and if necessary the relevant folders for remote build (e.g. + no_backup) + """ + # Create directories and symlinks, if necessary + os.mkdir(self.colcon_directory) + os.mkdir(os.path.join(self.colcon_directory, "src")) + os.makedirs(self.build_directory) + + local_build_dir_name = os.path.join(self.colcon_directory, "build") + (colcon_base_dir, _) = os.path.split(self.build_directory) + + colcon_log_directory = os.path.join(colcon_base_dir, "log") + local_log_dir_name = os.path.join(self.colcon_directory, "log") + click.echo("log_dir: {}".format(colcon_log_directory)) + + colcon_install_directory = os.path.join(colcon_base_dir, "install") + local_install_dir_name = os.path.join(self.colcon_directory, "install") + click.echo("install_dir: {}".format(colcon_install_directory)) + + if local_build_dir_name != self.build_directory: + os.symlink(self.build_directory, local_build_dir_name) + os.makedirs(colcon_log_directory) + os.symlink(colcon_log_directory, local_log_dir_name) + os.makedirs(colcon_install_directory) + os.symlink(colcon_install_directory, local_install_dir_name) + + def clone_packages(self, rosinstall): + """ + Clone packages from rosinstall structure + """ + # copy packages + if rosinstall != "": + # Dump the rosinstall to a file and use vcstool for getting the packages + rosinstall_filename = "/tmp/rob_folders_rosinstall" + with open(rosinstall_filename, "w") as rosinstall_content: + yaml_dump(rosinstall, rosinstall_content) + + os.makedirs(os.path.join(self.colcon_directory, "src"), exist_ok=True) + if self.no_submodules: + process = subprocess.check_call( + ["vcs", "import", "--input", rosinstall_filename, "src"], + cwd=self.colcon_directory, + ) + else: + process = subprocess.check_call( + [ + "vcs", + "import", + "--recursive", + "--input", + rosinstall_filename, + "src", + ], + cwd=self.colcon_directory, + ) + + os.remove(rosinstall_filename) diff --git a/src/robot_folders/helpers/exceptions.py b/src/robot_folders/helpers/exceptions.py new file mode 100644 index 0000000..9f96e20 --- /dev/null +++ b/src/robot_folders/helpers/exceptions.py @@ -0,0 +1,35 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +"""Defines exceptions used with robot_folders""" + + +class ModuleException(Exception): + """Generic exception in one command module""" + + def __init__(self, message, module_name, return_code=1): + super(ModuleException, self).__init__(message) + self.message = message + self.module_name = module_name + self.return_code = return_code + + def __str__(self): + return repr(self.message) diff --git a/src/robot_folders/helpers/repository_helpers.py b/src/robot_folders/helpers/repository_helpers.py new file mode 100644 index 0000000..95508fb --- /dev/null +++ b/src/robot_folders/helpers/repository_helpers.py @@ -0,0 +1,95 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +""" +This module contains helper functions around managing git repositories +""" +import git +import click +from robot_folders.helpers.exceptions import ModuleException + + +def parse_repository(repo_path, use_commit_id): + """ + Parses a repository path and returns the remote URL and the version (branch/commit) + """ + repo = git.Repo(repo_path) + remotes = repo.remotes + choice = 0 + + detached_head = repo.head.is_detached + + if len(remotes) > 1: + click.echo("Found multiple remotes for repo {}.".format(repo_path)) + upstream_remote = None + if not detached_head: + upstream_branch = repo.active_branch.tracking_branch() + if upstream_branch == None: + raise ModuleException( + 'Branch "{}" from repository "{}" does not have a tracking branch configured. Cannot scrape environment.'.format( + repo.active_branch, repo_path + ), + "repository_helpers", + 1, + ) + + upstream_remote = upstream_branch.name.split("/")[0] + default = None + for index, remote in enumerate(remotes): + click.echo("{}: {} ({})".format(index, remote.name, remote.url)) + if remote.name == upstream_remote: + default = index + valid_choice = -1 + while valid_choice < 0: + choice = click.prompt( + "Which one do you want to use?", + type=int, + default=default, + show_default=True, + ) + if choice >= 0 and choice < len(remotes): + valid_choice = choice + else: + click.echo("Invalid choice: '{}'".format(choice)) + click.echo( + "Selected remote {} ({})".format(remotes[choice].name, remotes[choice].url) + ) + url = remotes[choice].url + + if detached_head or use_commit_id: + version = repo.head.commit.hexsha + else: + version = repo.active_branch.name + return url, version + + +def create_rosinstall_entry(repo_path, local_name, use_commit_id=False): + """ + Creates a rosinstall dict entry for a given repo path and local folder name + """ + repo = dict() + repo["git"] = dict() + repo["git"]["local-name"] = local_name + + url, version = parse_repository(repo_path, use_commit_id) + repo["git"]["uri"] = url + repo["git"]["version"] = version + return repo diff --git a/src/robot_folders/helpers/resources/__init__.py b/src/robot_folders/helpers/resources/__init__.py new file mode 100644 index 0000000..3130f78 --- /dev/null +++ b/src/robot_folders/helpers/resources/__init__.py @@ -0,0 +1,21 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# diff --git a/src/robot_folders/helpers/resources/userconfig_distribute.yaml b/src/robot_folders/helpers/resources/userconfig_distribute.yaml new file mode 100644 index 0000000..6ea01de --- /dev/null +++ b/src/robot_folders/helpers/resources/userconfig_distribute.yaml @@ -0,0 +1,37 @@ +## +## Copyright (c) 2024 FZI Forschungszentrum Informatik +## +## Permission is hereby granted, free of charge, to any person obtaining a copy +## of this software and associated documentation files (the "Software"), to deal +## in the Software without restriction, including without limitation the rights +## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +## copies of the Software, and to permit persons to whom the Software is +## furnished to do so, subject to the following conditions: +## +## The above copyright notice and this permission notice shall be included in +## all copies or substantial portions of the Software. +## +## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +## THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +## THE SOFTWARE. +## +build: { + generator: make, + cmake_flags: "-DCMAKE_EXPORT_COMPILE_COMMANDS=1", + make_threads: 4, + install_catkin: False, + catkin_make_cmd: catkin_make, + colcon_build_options: "--symlink-install" +} + +directories: { + # if left blank, the default ~/checkout will be used + checkout_dir: , + catkin_names: ["catkin_workspace", "catkin_ws"], + colcon_names: ["colcon_workspace", "colcon_ws", "dev_ws"], + no_backup_dir: "~/no_backup" +} diff --git a/src/robot_folders/helpers/ros_version_helpers.py b/src/robot_folders/helpers/ros_version_helpers.py new file mode 100644 index 0000000..fde91a7 --- /dev/null +++ b/src/robot_folders/helpers/ros_version_helpers.py @@ -0,0 +1,66 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +"""Little helper around ROS versions""" + +import subprocess +import os + + +def installed_ros_distros(): + """Returns all installed ROS versions (ROS1 and ROS2)""" + return os.listdir("/opt/ros") + + +def installed_ros_1_versions(): + """Returns a list of all installed ROS1 versions""" + # Check the setup if it contains catkin the ROS Build system + temp_installed_ros_distros = os.listdir("/opt/ros") + installed_ros_distros = [] + for distro in temp_installed_ros_distros: + if "catkin" in open("/opt/ros/" + distro + "/setup.sh").read(): + installed_ros_distros.append(distro) + return installed_ros_distros + + +def installed_ros_2_versions(): + """Returns a list of all installed ROS2 versions""" + # Check the setup if it contains ament the ROS2 Build system + temp_installed_ros_distros = os.listdir("/opt/ros") + installed_ros_distros = [] + for distro in temp_installed_ros_distros: + source_script_path = os.path.join("/opt/ros", distro, "setup.sh") + if "AMENT_PREFIX_PATH" in shell_source_env(source_script_path): + installed_ros_distros.append(distro) + return installed_ros_distros + + +def shell_source_env(source_script_path): + """Emulates sourcing a shell file and returns the sourced environment as dictionary.""" + pipe = subprocess.Popen( + ". %s; env" % source_script_path, stdout=subprocess.PIPE, shell=True + ) + output = pipe.communicate()[0].decode("utf-8") + + key_values = (x.split("=", 1) for x in output.splitlines()) + filtered_pairs = filter(lambda x: len(x) == 2, key_values) + + return dict(filtered_pairs) diff --git a/src/robot_folders/helpers/underlays.py b/src/robot_folders/helpers/underlays.py new file mode 100644 index 0000000..6fc3d71 --- /dev/null +++ b/src/robot_folders/helpers/underlays.py @@ -0,0 +1,71 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +import os + +import inquirer + +import robot_folders.helpers.directory_helpers as dir_helpers + + +class UnderlayManager: + def __init__(self, env_name): + self.env_name = env_name + self.underlays = self.read_underlay_file() + + def query_underlays(self, active_list=None): + """CLI interface to update the underlay_list""" + envs = dir_helpers.list_environments() + if envs: + questions = [ + inquirer.Checkbox( + "underlays", + message="Which environments would you like to use as underlays (Can be left empty)?", + choices=envs, + default=active_list, + ), + ] + self.underlays = inquirer.prompt(questions)["underlays"] + + def _get_underlay_filename(self): + return os.path.join( + dir_helpers.get_checkout_dir(), self.env_name, "underlays.txt" + ) + + def read_underlay_file(self): + underlays = [] + if os.path.exists(self._get_underlay_filename()): + with open( + self._get_underlay_filename(), encoding="utf-8", mode="r" + ) as underlay_file: + lines = underlay_file.readlines() + for line in lines: + underlays.append(os.path.basename(line).strip()) + return underlays + + def write_underlay_file(self): + with open( + self._get_underlay_filename(), encoding="utf-8", mode="w" + ) as underlay_file: + for underlay in self.underlays: + underlay_file.write( + os.path.join(dir_helpers.get_checkout_dir(), underlay) + "\n" + ) diff --git a/src/robot_folders/helpers/which.py b/src/robot_folders/helpers/which.py new file mode 100644 index 0000000..13338c1 --- /dev/null +++ b/src/robot_folders/helpers/which.py @@ -0,0 +1,45 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +"""Module for implementing the 'which' command in python""" +import os + + +def is_exe(fpath): + """Checks whether a path points to an executable file""" + return os.path.isfile(fpath) and os.access(fpath, os.X_OK) + + +def which(program): + """Defines a 'which' function similar to the linux command""" + fpath, fname = os.path.split(program) + if fpath: + if is_exe(program): + return program + else: + for path in os.environ["PATH"].split(os.pathsep): + path = path.strip('"') + exe_file = os.path.join(path, program) + if is_exe(exe_file): + # We should never end up here probably. This is hard to unit-test + return exe_file + + return None diff --git a/src/robot_folders/helpers/workspace_chooser.py b/src/robot_folders/helpers/workspace_chooser.py new file mode 100644 index 0000000..5cc08d3 --- /dev/null +++ b/src/robot_folders/helpers/workspace_chooser.py @@ -0,0 +1,65 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +""" +The workspace chooser is a click Multicommand to generate a command out of existing +workspaces inside an environment. +""" + +import os +import click +from robot_folders.helpers.directory_helpers import ( + get_checkout_dir, + get_active_env_path, + get_catkin_dir, + get_colcon_dir, +) + + +class WorkspaceChooser(click.MultiCommand): + """ + The workspace chooser finds all existing environments. + """ + + def get_workspaces(self): + """Searches all environments inside the checkout folder""" + checkout_folder = get_checkout_dir() + # TODO possibly check whether the directory contains actual workspace + return [ + folder + for folder in os.listdir(checkout_folder) + if os.path.isdir(os.path.join(checkout_folder, folder)) + ] + + def list_commands(self, ctx): + if get_active_env_path() is None: + return list() + workspaces = [folder for folder in os.listdir(get_active_env_path())] + cmds = list() + if os.path.exists(get_colcon_dir()): + cmds.append("colcon") + if os.path.exists(get_catkin_dir()): + cmds.append("ros") + + return cmds + + def format_commands(self, ctx, formatter): + return "ic, ros" diff --git a/src/robot_folders/main.py b/src/robot_folders/main.py new file mode 100644 index 0000000..6c3ad24 --- /dev/null +++ b/src/robot_folders/main.py @@ -0,0 +1,100 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +""" Robot Folders + + This is the robot_folders main file. If you'd like to change or add + functionality, modify or add an according file in the 'commands' + subfolder. Every file in there is automatically added as command to + robot_folders. + Make sure that each command file consists at least of the following: + import click + @click.command() + def cli(): + \"\"\"Description of the command\"\"\" + + For further info either have a look at the other commands or at the + click documentation under http://click.pocoo.org +""" + +import click +import os +import traceback + +from click.exceptions import UsageError +from robot_folders.helpers.exceptions import ModuleException + +plugin_folder = os.path.join(os.path.dirname(__file__), "commands") + + +class RobotFolders(click.MultiCommand): + + def list_commands(self, ctx): + rv = [] + for filename in os.listdir(plugin_folder): + if filename.endswith(".py") and filename != "__init__.py": + rv.append(filename[:-3]) + rv.sort() + return rv + + def get_command(self, ctx, name): + ns = {} + fn = os.path.join(plugin_folder, name + ".py") + if os.path.isfile(fn): + with open(fn) as f: + code = compile(f.read(), fn, "exec") + eval(code, ns, ns) + return ns["cli"] + else: + return None + + def invoke(self, ctx): + try: + super(RobotFolders, self).invoke(ctx) + except ModuleException as err: + click.echo( + "Execution of module '{}' failed. Error message:\n{}".format( + err.module_name, err + ) + ) + os._exit(err.return_code) + except UsageError as err: + click.echo(err.show()) + except SystemExit as err: + # If a SystemExit comes from inside click, simply execute it. + pass + # click.echo("A system exit was triggered from inside robot_folders.") + except click.exceptions.Exit as err: + # If an Exit comes from inside click(v7), simply execute it. + pass + # click.echo("A exit was triggered from inside robot_folders.") + except: + click.echo("Execution of an unknown module failed. Exit with code 1.") + click.echo("Here's a traceback for debugging purposes:") + click.echo(traceback.format_exc()) + os._exit(1) + + +cli = RobotFolders( + help="This tool helps you managing different robot environments. " + "Use tab-completion for combining commands or type --help on each level " + "to get a help message." +) diff --git a/tests/test_add_delete.py b/tests/test_add_delete.py new file mode 100644 index 0000000..74899cc --- /dev/null +++ b/tests/test_add_delete.py @@ -0,0 +1,101 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +import os + +import pytest + +from click.testing import CliRunner + +import robot_folders.helpers.directory_helpers as directory_helpers +import robot_folders.helpers.ros_version_helpers as ros_versions +import robot_folders.commands.add_environment as add_environment +import robot_folders.commands.delete_environment as delete_environment + + +def test_add_catkin(): + + runner = CliRunner() + + installed_ros_distros = sorted(ros_versions.installed_ros_1_versions()) + if len(installed_ros_distros) == 0: + pytest.skip("Skipping this test since no ROS 1 distro is installed.") + ros_distro = installed_ros_distros[-1] + + result = runner.invoke( + add_environment.cli, + " ".join( + [ + "--create_catkin=yes", + "--create_misc_ws=no", + "--create_colcon=no", + "--copy_cmake_lists=no", + "--underlays=skip", + f"--ros2_distro={ros_distro}", + "testing_ws", + ] + ), + ) + assert result.exit_code == 0 + catkin_dir = directory_helpers.get_catkin_dir( + os.path.join(directory_helpers.get_checkout_dir(), "testing_ws") + ) + assert os.path.isdir(catkin_dir) + + result = runner.invoke(delete_environment.cli, "--force testing_ws") + assert result.exit_code == 0 + assert os.path.isdir(catkin_dir) is False + + +def test_add_colcon(): + + runner = CliRunner() + + installed_ros_distros = sorted(ros_versions.installed_ros_2_versions()) + if len(installed_ros_distros) == 0: + pytest.skip("Skipping this test since no ROS 2 distro is installed.") + ros_distro = installed_ros_distros[-1] + + result = runner.invoke( + add_environment.cli, + " ".join( + [ + "--create_catkin=no", + "--create_misc_ws=no", + "--create_colcon=yes", + "--copy_cmake_lists=no", + "--underlays=skip", + f"--ros2_distro={ros_distro}", + "testing_ws", + ] + ), + ) + print(result.output) + assert result.exit_code == 0 + + colcon_dir = directory_helpers.get_colcon_dir( + os.path.join(directory_helpers.get_checkout_dir(), "testing_ws") + ) + assert os.path.isdir(colcon_dir) + + result = runner.invoke(delete_environment.cli, "--force testing_ws") + assert result.exit_code == 0 + assert os.path.isdir(colcon_dir) is False diff --git a/tests/test_functionality.py b/tests/test_functionality.py new file mode 100644 index 0000000..b934ed8 --- /dev/null +++ b/tests/test_functionality.py @@ -0,0 +1,35 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +import pytest + +import os + +from click.testing import CliRunner + +import robot_folders.main + + +def test_rob_folders_runs(): + runner = CliRunner() + + result = runner.invoke(robot_folders.main.cli) + assert result.exit_code == 0 diff --git a/tests/test_get_checkout_base_dir.py b/tests/test_get_checkout_base_dir.py new file mode 100644 index 0000000..45d015f --- /dev/null +++ b/tests/test_get_checkout_base_dir.py @@ -0,0 +1,35 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import pytest + +from click.testing import CliRunner + +import robot_folders.commands.get_checkout_base_dir as get_checkout_base_dir +import robot_folders.helpers.directory_helpers as directory_helpers + + +def test_get_checkout_basedir(): + runner = CliRunner() + result = runner.invoke(get_checkout_base_dir.cli) + assert result.exit_code == 0 + assert result.output.strip() == directory_helpers.get_checkout_dir() diff --git a/tests/test_which.py b/tests/test_which.py new file mode 100644 index 0000000..32b951e --- /dev/null +++ b/tests/test_which.py @@ -0,0 +1,42 @@ +# +# Copyright (c) 2024 FZI Forschungszentrum Informatik +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +import sys +import subprocess + +from robot_folders.helpers import which + + +def test_is_exe(): + assert which.is_exe(sys.executable) + + +def test_which_python(): + system_call = subprocess.run(["which", "python3"], stdout=subprocess.PIPE) + assert which.which("python3") == system_call.stdout.strip().decode("utf-8") + + +def test_which_interpreter(): + assert which.which(sys.executable) == sys.executable + + +def test_which_none(): + assert which.which("I_dont exist") is None