From c2e5162a588e72c03afb88bbbd7eaf45e981495b Mon Sep 17 00:00:00 2001 From: Mateusz Szczygielski <112629916+msz-rai@users.noreply.github.com> Date: Tue, 11 Jun 2024 11:46:55 +0100 Subject: [PATCH] Optimize dockerfile (building and caching) (#294) * Introduce multistage dockerfile for build process (core only) * Update .dockerignore * Update README * Do not apt update at build time internet should not be required when compiling * Use apt-get instead of apt as apt does not have a stable CLI and thus unsuitable for scripting - https://unix.stackexchange.com/a/590703/213124 * Automate apt-get yes prompts for non-interactive scripting * Use placeholder stage for dependabot automation * Rename stages uniquely for locatability and mark for readability * Move ARG to appropriate scope to avoid needless busting build cache of unrelated stages * Disable internet for build time to flush out all non-determinism given offline caching is impractical otherwise * Edit apt config for caching and update once * Bootstrap and cache tools for install scripts * Add missing implicit dependency * Add pseudo code to install dependencies only in pepper stage * Add dependabot config For both docker and github actions. This should: - auto open a PR when new nvidia tags are available - as well as keep Github actions versions up to date * Simplify docker ignore config by inverting exclusion set to robustly ignore randomly named IDE files and temp folders * Exclude .git from .dockerignore Required by cmake_git_version_tracking * Separate dependencies script from compilation script * WIP - RGL extensions handling * Add ROS2 installation * Add build command argument * Add ros2 standalone libs and rgl executables to exporter * Fix pcl deps * Fix ros2 extension compilation * Fix CMakeLists for tests * Add ORIGIN rpath to test/tools executables * Do not change current working directory for extension install_deps scripts * Improve docs * Bring back support for taped test * Formatting * Adjust CI * Add dependencies installation info to README * Ignore Dockerfile in .dockerignore * Remove unused components from CI * Fix CI * Output libraries to lib folder * Output executables to bin folder * Simplify exporter stage by cache dancing * Formatting alpha sort * Output libraries to lib folder kind of hacky should probably simplify using better CMake * Simplify ROS install as timezone and LC_ALL setup is more for running and shouldn't be needed if only just building * Simplify ROS install remove unnecessary packages * Formatting * Use ROS_DISTRO ARG and set ENV if ever needed for later after docker build * Drop pinned package version * Formatting * Simplify ROS install by using minimal build dependency set * Alpha sort package sub lists * Simplify RUN directive via shell * Simplify RUN directive via shell * Revert to installing core as `radar_msgs` has a number of other msg dependencies though rosdep should eventually be used here * Formatting * Avoid hard coded ubuntu code name * Simplify RUN directives * Cache apt update * Format package use septate lines for version control * Update README.md Co-authored-by: Mateusz Szczygielski <112629916+msz-rai@users.noreply.github.com> * Revert "Remove Disable DNS" This partially reverts commit 74317d55fe54086e2b1ec09af6dcff4f0fdcaa3c. * Simplify CMake for INSTALL_DESTINATION_DIR * Revert "Revert "Remove Disable DNS"" This reverts commit 32016bb0b0683769fefd0962ab32d67ca968ae8a. * Add missing dep for ros2-standalone * Adjust CI * Fix CI: use dot instead of source * Fix CI: source sh file * Improve ROS2 check * Use new dev docker to test * Fix formatting (some packages weren't installed) * Use ROS_DISTRO instead of fixed version --------- Co-authored-by: ruffsl --- .dockerignore | 28 ++- .github/dependabot.yml | 14 ++ .github/workflows/build-and-test.yml | 166 +++++++------- .github/workflows/build-subworkflow.yml | 46 ---- .github/workflows/load-env-subworkflow.yml | 8 - CMakeLists.txt | 4 + Dockerfile | 205 ++++++++++-------- README.md | 40 +++- extensions/pcl/install_deps.py | 80 +++++++ extensions/ros2/install_deps.py | 83 +++++++ external/CMakeLists.txt | 34 +-- install_deps.py | 67 ++++++ ros2_standalone/CMakeLists.txt | 2 +- setup.py | 238 +++++++-------------- test/CMakeLists.txt | 9 +- test/taped_test/CMakeLists.txt | 9 +- tools/CMakeLists.txt | 5 + 17 files changed, 610 insertions(+), 428 deletions(-) create mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/build-subworkflow.yml create mode 100755 extensions/pcl/install_deps.py create mode 100755 extensions/ros2/install_deps.py create mode 100755 install_deps.py diff --git a/.dockerignore b/.dockerignore index 41c7f2d2e..9ce9de889 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,19 @@ -.vs -.vscode -Builds/* -Testing/* -.idea -cmake-build-debug -cmake-build-release -build -external/vcpkg +# Ignore everything by default +* + +# First-order allow exception for select directories +!/.clang-format +!/.githooks +!/.git +!/CMakeLists.txt +!/docs +!/extensions +!/extensions.repos +!external/CMakeLists.txt +!/include +!/ros2_standalone +!/setup.py +!/install_deps.py +!/src +!/test +!/tools diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..8724e8d8a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "daily" + commit-message: + prefix: "🐳 " + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + commit-message: + prefix: "🛠️ " diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 6d2dcc433..fc3cef5ee 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -24,12 +24,12 @@ jobs: uses: actions/checkout@v3 with: clean: false - - name: Clean repository excluding taped test data - run: git clean -ffdx -e external/taped_test_data + - name: Clean repository excluding RGL blobs repo + run: git clean -ffdx -e external/rgl_blobs - name: Import private extensions run: vcs import < extensions.repos - - name: Install taped test dependencies - run: ./setup.py --install-taped-test-deps + - name: Install taped test runtime dependencies + run: ./setup.py --fetch-rgl-blobs load-env: uses: ./.github/workflows/load-env-subworkflow.yml @@ -37,78 +37,81 @@ jobs: ###### BUILD USING RGL DOCKER ###### build-core: needs: [checkout-repository, load-env] - uses: ./.github/workflows/build-subworkflow.yml - with: - build-command: ./setup.py --build-dir build-core - self-hosted-user-id: ${{ needs.load-env.outputs.user-id }} - optix-install-dir: ${{ needs.load-env.outputs.optix-install-dir }} - docker-image: localhost:5000/rgl:latest + runs-on: self-hosted + steps: + - run: ' + docker build --build-context optix=${{ needs.load-env.outputs.optix-install-dir }} + --build-arg WITH_PCL=1 --build-arg WITH_ROS2=1 + --build-arg BUILD_CMD="./setup.py" + --target=exporter --output=bin-core .' build-pcl: needs: [checkout-repository, load-env] - uses: ./.github/workflows/build-subworkflow.yml - with: - build-command: './setup.py --with-pcl --build-taped-test --build-dir build-pcl' - self-hosted-user-id: ${{ needs.load-env.outputs.user-id }} - optix-install-dir: ${{ needs.load-env.outputs.optix-install-dir }} - docker-image: localhost:5000/rgl:latest + runs-on: self-hosted + steps: + - run: ' + docker build --build-context optix=${{ needs.load-env.outputs.optix-install-dir }} + --build-arg WITH_PCL=1 --build-arg WITH_ROS2=1 + --build-arg BUILD_CMD="./setup.py --with-pcl --build-taped-test" + --target=exporter --output=bin-pcl .' build-ros2: needs: [ checkout-repository, load-env ] - uses: ./.github/workflows/build-subworkflow.yml - with: - build-command: ' - source /opt/ros/humble/setup.bash && - ./setup.py --with-ros2-standalone --build-dir build-ros2' - self-hosted-user-id: ${{ needs.load-env.outputs.user-id }} - optix-install-dir: ${{ needs.load-env.outputs.optix-install-dir }} - docker-image: localhost:5000/rgl:latest + runs-on: self-hosted + steps: + - run: ' + docker build --build-context optix=${{ needs.load-env.outputs.optix-install-dir }} + --build-arg WITH_PCL=1 --build-arg WITH_ROS2=1 + --build-arg BUILD_CMD=". /opt/ros/\$ROS_DISTRO/setup.sh && ./setup.py --with-ros2-standalone" + --target=exporter --output=bin-ros2 .' build-udp: needs: [ checkout-repository, load-env ] - uses: ./.github/workflows/build-subworkflow.yml - with: - build-command: './setup.py --with-udp --build-dir build-udp' - self-hosted-user-id: ${{ needs.load-env.outputs.user-id }} - optix-install-dir: ${{ needs.load-env.outputs.optix-install-dir }} - docker-image: localhost:5000/rgl:latest + runs-on: self-hosted + steps: + - run: ' + docker build --build-context optix=${{ needs.load-env.outputs.optix-install-dir }} + --build-arg WITH_PCL=1 --build-arg WITH_ROS2=1 + --build-arg BUILD_CMD="./setup.py --with-udp" + --target=exporter --output=bin-udp .' build-snow: needs: [ checkout-repository, load-env ] - uses: ./.github/workflows/build-subworkflow.yml - with: - build-command: './setup.py --with-snow --build-dir build-snow' - self-hosted-user-id: ${{ needs.load-env.outputs.user-id }} - optix-install-dir: ${{ needs.load-env.outputs.optix-install-dir }} - docker-image: localhost:5000/rgl:latest + runs-on: self-hosted + steps: + - run: ' + docker build --build-context optix=${{ needs.load-env.outputs.optix-install-dir }} + --build-arg WITH_PCL=1 --build-arg WITH_ROS2=1 + --build-arg BUILD_CMD="./setup.py --with-snow" + --target=exporter --output=bin-snow .' build-all: needs: [ checkout-repository, load-env ] - uses: ./.github/workflows/build-subworkflow.yml - with: - build-command: ' - source /opt/ros/humble/setup.bash && - ./setup.py --with-pcl --with-ros2-standalone --with-udp --with-snow --build-taped-test --build-dir build-all' - self-hosted-user-id: ${{ needs.load-env.outputs.user-id }} - optix-install-dir: ${{ needs.load-env.outputs.optix-install-dir }} - docker-image: localhost:5000/rgl:latest - -###### TEST WITH RGL DOCKER IMAGE ###### + runs-on: self-hosted + steps: + - run: ' + docker build --build-context optix=${{ needs.load-env.outputs.optix-install-dir }} + --build-arg WITH_PCL=1 --build-arg WITH_ROS2=1 + --build-arg BUILD_CMD=". /opt/ros/\$ROS_DISTRO/setup.sh && ./setup.py --with-pcl --with-ros2-standalone --with-udp --with-snow --build-taped-test" + --target=exporter --output=bin-all .' + +####### TEST WITH RGL DOCKER IMAGE ###### test-core-dev: needs: [build-core] uses: ./.github/workflows/test-subworkflow.yml with: - test-command: 'cd build-core/test && ./RobotecGPULidar_test' - docker-image: localhost:5000/rgl:latest + test-command: ' + cd bin-core/bin/test && ./RobotecGPULidar_test' + docker-image: localhost:5000/robotecgpulidar-all:latest test-pcl-dev: needs: [ build-pcl ] uses: ./.github/workflows/test-subworkflow.yml with: test-command: ' - export RGL_TAPED_TEST_DATA_DIR=$(pwd)/external/taped_test_data && - cd build-pcl/test && ./RobotecGPULidar_test && ./taped_test/RobotecGPULidar_taped_test' - docker-image: localhost:5000/rgl:latest + export RGL_TAPED_TEST_DATA_DIR=$(pwd)/external/rgl_blobs && + cd bin-pcl/bin/test && ./RobotecGPULidar_test && ./RobotecGPULidar_taped_test' + docker-image: localhost:5000/robotecgpulidar-all:latest test-ros2-dev: needs: [ build-ros2 ] @@ -117,26 +120,28 @@ jobs: # Source ROS2 and radar_msgs, standalone build is tested in prod environment # Run tests twice, each for different RMW implementation test-command: ' - source /opt/ros/humble/setup.bash && - source /rgldep/radar_msgs/install/setup.bash && - cd build-ros2/test && + . /opt/ros/$ROS_DISTRO/setup.sh && + . /opt/rgl/external/radar_msgs/install/setup.sh && + cd bin-ros2/bin/test && export RMW_IMPLEMENTATION=rmw_fastrtps_cpp && ./RobotecGPULidar_test && export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp && ./RobotecGPULidar_test' - docker-image: localhost:5000/rgl:latest + docker-image: localhost:5000/robotecgpulidar-all:latest test-udp-dev: needs: [ build-udp ] uses: ./.github/workflows/test-subworkflow.yml with: - test-command: 'cd build-udp/test && ./RobotecGPULidar_test' - docker-image: localhost:5000/rgl:latest + test-command: ' + cd bin-udp/bin/test && ./RobotecGPULidar_test' + docker-image: localhost:5000/robotecgpulidar-all:latest test-snow-dev: needs: [ build-snow ] uses: ./.github/workflows/test-subworkflow.yml with: - test-command: 'cd build-snow/test && ./RobotecGPULidar_test' - docker-image: localhost:5000/rgl:latest + test-command: ' + cd bin-snow/bin/test && ./RobotecGPULidar_test' + docker-image: localhost:5000/robotecgpulidar-all:latest test-all-dev: needs: [ build-all ] @@ -146,21 +151,22 @@ jobs: # Set `RGL_TEST_VLP16_CALIB_FILE` for UDP-ROS2 integration test # Run tests twice, each for different RMW implementation test-command: ' - source /opt/ros/humble/setup.bash && - source /rgldep/radar_msgs/install/setup.bash && + . /opt/ros/$ROS_DISTRO/setup.sh && + . /opt/rgl/external/radar_msgs/install/setup.sh && export RGL_TEST_VLP16_CALIB_FILE=$(pwd)/extensions/udp/test/resources/Ros2Vlp16Calib.yaml && - export RGL_TAPED_TEST_DATA_DIR=$(pwd)/external/taped_test_data && - cd build-all/test && - export RMW_IMPLEMENTATION=rmw_fastrtps_cpp && ./RobotecGPULidar_test && ./taped_test/RobotecGPULidar_taped_test && - export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp && ./RobotecGPULidar_test && ./taped_test/RobotecGPULidar_taped_test' - docker-image: localhost:5000/rgl:latest + export RGL_TAPED_TEST_DATA_DIR=$(pwd)/external/rgl_blobs && + cd bin-all/bin/test && + export RMW_IMPLEMENTATION=rmw_fastrtps_cpp && ./RobotecGPULidar_test && ./RobotecGPULidar_taped_test && + export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp && ./RobotecGPULidar_test && ./RobotecGPULidar_taped_test' + docker-image: localhost:5000/robotecgpulidar-all:latest -###### TEST WITH CLEAN UBUNTU DOCKER IMAGE ###### +####### TEST WITH CLEAN UBUNTU DOCKER IMAGE ###### test-core-prod: needs: [build-core] uses: ./.github/workflows/test-subworkflow.yml with: - test-command: 'cd build-core/test && ./RobotecGPULidar_test' + test-command: ' + cd bin-core/bin/test && ./RobotecGPULidar_test' docker-image: nvidia/cuda:11.7.1-base-ubuntu22.04 test-pcl-prod: @@ -170,8 +176,8 @@ jobs: # Additionally, install PCL extension dependent libraries for runtime test-command: ' apt update && apt install -y libxcursor1 libgl1 && - export RGL_TAPED_TEST_DATA_DIR=$(pwd)/external/taped_test_data && - cd build-pcl/test && ./RobotecGPULidar_test && ./taped_test/RobotecGPULidar_taped_test' + export RGL_TAPED_TEST_DATA_DIR=$(pwd)/external/rgl_blobs && + cd bin-pcl/bin/test && ./RobotecGPULidar_test && ./RobotecGPULidar_taped_test' docker-image: nvidia/cuda:11.7.1-base-ubuntu22.04 test-ros2-prod: @@ -181,8 +187,8 @@ jobs: # Copy ROS2 libraries to be visible for libRobotecGPULidar.so # Run tests twice, each for different RMW implementation test-command: ' - cd build-ros2/test && - cp -r ../ros2_standalone/*.so* ../ && + cp -p bin-ros2/lib/ros2_standalone/* bin-ros2/lib && + cd bin-ros2/bin/test && export RMW_IMPLEMENTATION=rmw_fastrtps_cpp && ./RobotecGPULidar_test && export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp && ./RobotecGPULidar_test' docker-image: nvidia/cuda:11.7.1-base-ubuntu22.04 @@ -191,14 +197,16 @@ jobs: needs: [ build-udp ] uses: ./.github/workflows/test-subworkflow.yml with: - test-command: 'cd build-udp/test && ./RobotecGPULidar_test' + test-command: ' + cd bin-udp/bin/test && ./RobotecGPULidar_test' docker-image: nvidia/cuda:11.7.1-base-ubuntu22.04 test-snow-prod: needs: [ build-snow ] uses: ./.github/workflows/test-subworkflow.yml with: - test-command: 'cd build-snow/test && ./RobotecGPULidar_test' + test-command: ' + cd bin-snow/bin/test && ./RobotecGPULidar_test' docker-image: nvidia/cuda:11.7.1-base-ubuntu22.04 test-all-prod: @@ -210,9 +218,9 @@ jobs: # Run tests twice, each for different RMW implementation test-command: ' apt update && apt install -y libxcursor1 libgl1 && - export RGL_TAPED_TEST_DATA_DIR=$(pwd)/external/taped_test_data && - cd build-all/test && - cp -r ../ros2_standalone/*.so* ../ && - export RMW_IMPLEMENTATION=rmw_fastrtps_cpp && ./RobotecGPULidar_test && ./taped_test/RobotecGPULidar_taped_test && - export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp && ./RobotecGPULidar_test && ./taped_test/RobotecGPULidar_taped_test' + export RGL_TAPED_TEST_DATA_DIR=$(pwd)/external/rgl_blobs && + cp -p bin-all/lib/ros2_standalone/* bin-all/lib && + cd bin-all/bin/test && + export RMW_IMPLEMENTATION=rmw_fastrtps_cpp && ./RobotecGPULidar_test && ./RobotecGPULidar_taped_test && + export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp && ./RobotecGPULidar_test && ./RobotecGPULidar_taped_test' docker-image: nvidia/cuda:11.7.1-base-ubuntu22.04 diff --git a/.github/workflows/build-subworkflow.yml b/.github/workflows/build-subworkflow.yml deleted file mode 100644 index 722f2735c..000000000 --- a/.github/workflows/build-subworkflow.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: build-subworkflow - -on: - workflow_call: - inputs: - build-command: - required: true - type: string - docker-image: - required: true - type: string - # User id is used to run the docker container as the self-hosted user - # It allows to delete build files created in the docker while on the host (checkout-repository job) - # By default, those files are owned by the docker user, and root privileges are required to delete them - self-hosted-user-id: - required: true - type: string - optix-install-dir: - required: true - type: string - -permissions: - contents: read - -jobs: - build: - defaults: - run: - shell: bash - runs-on: self-hosted - container: - image: ${{ inputs.docker-image }} - env: - OptiX_INSTALL_DIR: /optix - NVIDIA_DRIVER_CAPABILITIES: all - volumes: - - ${{ inputs.optix-install-dir }}:/optix - options: - --rm - --gpus all - --user ${{ inputs.self-hosted-user-id }} - steps: - - name: fix git - run: git config --global --add safe.directory $PWD - - name: build - run: ${{ inputs.build-command }} diff --git a/.github/workflows/load-env-subworkflow.yml b/.github/workflows/load-env-subworkflow.yml index 95da9ab76..e48d459fe 100644 --- a/.github/workflows/load-env-subworkflow.yml +++ b/.github/workflows/load-env-subworkflow.yml @@ -5,15 +5,12 @@ on: outputs: optix-install-dir: value: ${{ jobs.load-env.outputs.optix-install-dir }} - user-id: - value: ${{ jobs.load-env.outputs.user-id }} jobs: load-env: runs-on: self-hosted outputs: optix-install-dir: ${{ steps.set-envs.outputs.optix-install-dir }} - user-id: ${{ steps.set-envs.outputs.user-id }} steps: - id: check-envs run: | @@ -21,11 +18,6 @@ jobs: echo "OptiX_INSTALL_DIR env is empty" exit 1 fi - if [[ -z "$(id -u $USER)" ]]; then - echo "Cannot deduce id of the USER" - exit 1 - fi - id: set-envs run: | echo "optix-install-dir=$OptiX_INSTALL_DIR" | tee -a $GITHUB_OUTPUT - echo "user-id=$(id -u $USER)" | tee -a $GITHUB_OUTPUT diff --git a/CMakeLists.txt b/CMakeLists.txt index 4bc660649..6c2ca5194 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -131,6 +131,7 @@ set(RGL_SOURCE_FILES src/graph/YieldPointsNode.cpp ) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) if (RGL_BUILD_STATIC) add_library(RobotecGPULidar STATIC ${RGL_SOURCE_FILES}) target_compile_definitions(RobotecGPULidar PUBLIC RGL_STATIC) @@ -212,6 +213,9 @@ target_compile_definitions(RobotecGPULidar PRIVATE RGL_BUILD # Used in headers to differentiate whether it is parsed as library or client's code, affects __declspec on Windows. ) +# Enable relative paths in the build RPATH for executables +set(CMAKE_BUILD_RPATH_USE_ORIGIN ON) + # Include tests if (RGL_BUILD_TESTS OR RGL_BUILD_TAPED_TESTS) enable_testing() diff --git a/Dockerfile b/Dockerfile index 02d1c98eb..fa561a871 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,87 +1,122 @@ -FROM nvidia/cuda:11.7.1-devel-ubuntu22.04 - +ARG BASE_IMAGE=base +ARG WITH_PCL=0 +ARG WITH_ROS2=0 +# Stage from full image tag name for dependabot detection +FROM nvidia/cuda:11.7.1-devel-ubuntu22.04 as base + +################################################################################ +# MARK: prepper - prep rgl dependencies +################################################################################ +### Core dependencies stage +FROM $BASE_IMAGE as prepper-core ARG DEBIAN_FRONTEND=noninteractive -ENV RGL_IS_BUILDING_DOCKER_IMAGE True - -RUN apt update -RUN apt install -y \ - cmake \ - git - -##### PCL extension ##### - -# Install vcpkg dependencies -RUN apt install -y \ - curl \ - zip \ - unzip \ - tar \ - pkg-config \ - freeglut3-dev \ - libglew-dev \ - libglfw3-dev \ - python3 - -# Install RGL PCL dependencies via vcpkg -COPY setup.py / -RUN /setup.py --install-pcl-deps -RUN rm /setup.py - -##### ROS2 extension ##### - -# Setup timezone -RUN echo 'Etc/UTC' > /etc/timezone && \ - ln -s /usr/share/zoneinfo/Etc/UTC /etc/localtime && \ - apt-get update && \ - apt-get install -q -y --no-install-recommends tzdata && \ - rm -rf /var/lib/apt/lists/* - -# Install packages -RUN apt-get update && apt-get install -q -y --no-install-recommends \ - dirmngr \ - gnupg2 \ - && rm -rf /var/lib/apt/lists/* - -# Setup sources.list -RUN echo "deb http://packages.ros.org/ros2/ubuntu jammy main" > /etc/apt/sources.list.d/ros2-latest.list - -# Setup keys -RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654 - -# Setup environment -ENV LANG C.UTF-8 -ENV LC_ALL C.UTF-8 -ENV ROS_DISTRO humble - -# Install ros2 packages -RUN apt-get update && apt-get install -y --no-install-recommends \ - ros-humble-ros-core=0.10.0-1* \ - && rm -rf /var/lib/apt/lists/* - -# Install packages for --install-ros2-deps -RUN apt-get update && apt-get install -y --no-install-recommends \ - python3-colcon-common-extensions - -# Install RGL ROS2 dependencies (e.g. radar_msgs) -COPY setup.py / -RUN /bin/bash -c "source /opt/ros/${ROS_DISTRO}/setup.bash; /setup.py --install-ros2-deps" # ROS2 must be sourced before installing deps -RUN rm /setup.py - -# Install required packages for RGL ROS2 standalone build (may be moved to setup.py in the future) -RUN apt-get update && apt-get install -y --no-install-recommends \ - ros-${ROS_DISTRO}-cyclonedds ros-${ROS_DISTRO}-rmw-cyclonedds-cpp \ - ros-${ROS_DISTRO}-fastrtps ros-${ROS_DISTRO}-rmw-fastrtps-cpp \ - patchelf - -# Install required packages for UDP-ROS2 integration test -RUN apt-get update && apt-get install -y --no-install-recommends \ - ros-${ROS_DISTRO}-velodyne-driver ros-${ROS_DISTRO}-velodyne-pointcloud \ - psmisc # for `killall` command - -##### Post-build configuration ##### - -WORKDIR /code -RUN git config --system --add safe.directory /code - -CMD [ "/bin/bash" ] +# Edit apt config for caching and update once +RUN mv /etc/apt/apt.conf.d/docker-clean /etc/apt/ && \ + echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' \ + > /etc/apt/apt.conf.d/keep-cache && \ + apt-get update + +# Install bootstrap tools for install scripts +RUN --mount=type=cache,sharing=locked,target=/var/cache/apt \ + apt-get install -y --no-install-recommends \ + cmake \ + git \ + python3 \ + sudo + +# Set working directory using standard opt path +WORKDIR /opt/rgl + +# Copy only dependencies definition files +COPY ./install_deps.py . + +# Install dependencies while caching apt downloads +RUN --mount=type=cache,sharing=locked,target=/var/cache/apt \ + ./install_deps.py + +### PCL extension dependencies stage (added on top of core depenencies based on `WITH_PCL` argument) +FROM prepper-core AS prepper-pcl-0 +# Do nothing, PCL extension is not enabled +FROM prepper-core AS prepper-pcl-1 +# Copy only dependencies definition files for PCL extension +COPY ./extensions/pcl/install_deps.py . + +# Install PCL extension dependencies while caching apt downloads +RUN --mount=type=cache,sharing=locked,target=/var/cache/apt \ + ./install_deps.py + +### ROS2 extension dependencies stage (added on top of PCL depenencies based on `WITH_ROS2` argument) +FROM prepper-pcl-${WITH_PCL} AS prepper-ros2-0 +# Do nothing, ROS2 extension is not enabled +FROM prepper-pcl-${WITH_PCL} AS prepper-ros2-1 + +# Install ROS2: Setup sources.list and keys +RUN . /etc/os-release && \ + echo "deb http://packages.ros.org/ros2/ubuntu $UBUNTU_CODENAME main" > /etc/apt/sources.list.d/ros2-latest.list && \ + apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654 && \ + apt-get update + +ARG ROS_DISTRO=humble +ENV ROS_DISTRO=$ROS_DISTRO +# Install ROS2: Install packages +RUN --mount=type=cache,sharing=locked,target=/var/cache/apt \ + apt-get install -y --no-install-recommends \ + # Packages for RGL ROS2 standalone build + patchelf \ + pkg-config \ + ros-$ROS_DISTRO-cyclonedds \ + ros-$ROS_DISTRO-fastrtps \ + ros-$ROS_DISTRO-rmw-cyclonedds-cpp \ + ros-$ROS_DISTRO-rmw-fastrtps-cpp \ + ros-$ROS_DISTRO-ros-core \ + # Packages for UDP-ROS2 integration test + # psmisc for`killall` command + psmisc \ + ros-$ROS_DISTRO-velodyne-driver \ + ros-$ROS_DISTRO-velodyne-pointcloud + +# Copy only dependencies definition files for ROS2 extension +COPY ./extensions/ros2/install_deps.py . + +# Install ROS2 extension dependencies while caching apt downloads +RUN --mount=type=cache,sharing=locked,target=/var/cache/apt \ + . /opt/ros/$ROS_DISTRO/setup.sh && \ + ./install_deps.py + +### Final prepper stage with selected extensions +FROM prepper-ros2-${WITH_ROS2} AS prepper + +################################################################################ +# MARK: builder - build rgl binaries +################################################################################ +FROM prepper AS builder +ARG OptiX_INSTALL_DIR=/optix + +# Copy rest of source tree +COPY . . + +ARG BUILD_CMD="./setup.py" +RUN --mount=type=bind,from=optix,target=${OptiX_INSTALL_DIR} \ + sh -c "$BUILD_CMD" + +################################################################################ +# MARK: dancer - multi-stage for cache dancing +################################################################################ +FROM builder AS dancer + +# Copy entire build directory +# RUN mkdir /dancer && \ +# cp -rT build /dancer + +# Copy only the lib and bin directories +RUN mkdir /dancer && \ + cp -r build/bin /dancer/ && \ + cp -r build/lib /dancer/ + +################################################################################ +# MARK: exporter - export rgl binaries and executables +################################################################################ +FROM scratch AS exporter + +COPY --from=dancer /dancer / diff --git a/README.md b/README.md index 5585c1b95..f016ffad6 100644 --- a/README.md +++ b/README.md @@ -67,14 +67,32 @@ An introduction to the RGL API along with an example can be found [here](docs/Us ## Building in Docker (Linux) -1. Set up [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#docker) -2. Download [NVidia OptiX](https://developer.nvidia.com/designworks/optix/downloads/legacy) **7.2** -3. `export OptiX_INSTALL_DIR=` -4. `docker build . --tag rgl:latest` -5. `docker run --net=host --gpus all -v $(pwd):/code -v ${OptiX_INSTALL_DIR}:/optix -e OptiX_INSTALL_DIR=/optix -e NVIDIA_DRIVER_CAPABILITIES=all -it rgl:latest /bin/bash` -6. `./setup.py --clean-build --with-pcl` - - For build with ROS2 extension, do source ROS2 first:\ - `source /opt/ros/humble/setup.bash && ./setup.py --clean-build --with-pcl --with-ros2` +1. Download [NVidia OptiX](https://developer.nvidia.com/designworks/optix/downloads/legacy) **7.2** +2. `export OptiX_INSTALL_DIR=` +3. `docker build --build-context optix=${OptiX_INSTALL_DIR} --target=exporter --output=build .` + - The binaries will be exported to the `build` directory +4. To build RGL with extensions, docker must install additional dependencies. + - It could be enabled by setting the following arguments: + - `--build-arg WITH_PCL=1` - adds stage to install dependencies for PCL extension + - `--build-arg WITH_ROS2=1` - adds stage to install dependencies for ROS2 extension + - By default, the build command compiles the core part of the library only. To include extensions it must be overwritten: + - `--build-arg BUILD_CMD="./setup.py --with-pcl"` - includes PCL extension + - `--build-arg BUILD_CMD='. /opt/ros/\$ROS_DISTRO/setup.sh && ./setup.py --with-ros2'` - includes ROS2 extension (ROS2 must be sourced first) + - The command for building RGL with PCL and ROS2 extensions would be: + +```shell +docker build \ + --build-arg WITH_ROS2=1 \ + --build-arg WITH_PCL=1 \ + --build-arg BUILD_CMD='\ + . /opt/ros/\$ROS_DISTRO/setup.sh && \ + ./setup.py \ + --with-ros2 \ + --with-pcl' \ + --build-context optix=$OptiX_INSTALL_DIR \ + --target=exporter \ + --output=build . +``` ## Building on Ubuntu 22 @@ -83,7 +101,8 @@ An introduction to the RGL API along with an example can be found [here](docs/Us 1. You may be asked to create a Nvidia account to download 3. Export environment variable: 1. `export OptiX_INSTALL_DIR=`. -4. Use `setup.py` script to build. +4. Install dependencies with command: `./setup.py --install-deps` +5. Use `setup.py` script to build. - It will use CMake to generate files for the build system (make) and the build. - You can pass optional CMake and make parameters, e.g. - `./setup.py --cmake="-DCMAKE_BUILD_TYPE=Debug" --make="-j 16"` @@ -99,7 +118,8 @@ An introduction to the RGL API along with an example can be found [here](docs/Us - install the framework and set the environment variable `OptiX_INSTALL_DIR` 4. Install [Python3](https://www.python.org/downloads/). 5. Run `x64 Native Tools Command Prompt for VS 20xx` and navigate to the RGL repository. -6. Run `python setup.py` command to build the project. +6. Run `python setup.py --install-deps` command to install dependencies. +7. Run `python setup.py` command to build the project. - It will use CMake to generate files for the build system (ninja) and build. - You can pass optional CMake and ninja parameters, e.g. - `python setup.py --cmake="-DCMAKE_BUILD_TYPE=Debug" --ninja="-j 16"` diff --git a/extensions/pcl/install_deps.py b/extensions/pcl/install_deps.py new file mode 100755 index 000000000..820cef85d --- /dev/null +++ b/extensions/pcl/install_deps.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +import os +import platform +import sys +import subprocess + + +class Config: + # Default values for Linux + VCPKG_TAG = "2023.08.09" + # Paths relative to project root + VCPKG_DIR = os.path.join("external", "vcpkg") + VCPKG_EXEC = "vcpkg" + VCPKG_BOOTSTRAP = "bootstrap-vcpkg.sh" + VCPKG_TRIPLET = "x64-linux" + + def __init__(self): + if on_windows(): + self.VCPKG_EXEC = "vcpkg.exe" + self.VCPKG_BOOTSTRAP = "bootstrap-vcpkg.bat" + self.VCPKG_TRIPLET = "x64-windows" + + +def install_deps(): + cfg = Config() + + # Clone vcpkg + if not os.path.isdir(cfg.VCPKG_DIR): + if on_linux(): + print("Installing dependencies for vcpkg...") + run_subprocess_command(""" + sudo apt-get install -y \ + curl \ + freeglut3-dev \ + git \ + libglew-dev \ + libglfw3-dev \ + pkg-config \ + tar \ + unzip \ + zip + """) + run_subprocess_command( + f"git clone -b {cfg.VCPKG_TAG} --single-branch --depth 1 https://github.com/microsoft/vcpkg {cfg.VCPKG_DIR}") + # Bootstrap vcpkg + if not os.path.isfile(os.path.join(cfg.VCPKG_DIR, cfg.VCPKG_EXEC)): + run_subprocess_command(f"{os.path.join(cfg.VCPKG_DIR, cfg.VCPKG_BOOTSTRAP)}") + + # Install dependencies via vcpkg + run_subprocess_command( + f"{os.path.join(cfg.VCPKG_DIR, cfg.VCPKG_EXEC)} install --clean-after-build pcl[core,visualization]:{cfg.VCPKG_TRIPLET}") + + print("PCL deps installed successfully") + + +def are_deps_installed() -> bool: + cfg = Config() + return os.path.isdir(cfg.VCPKG_DIR) + + +def on_windows(): + return platform.system() == "Windows" + + +def on_linux(): + return platform.system() == "Linux" + + +def run_subprocess_command(command: str, shell=True, stderr=sys.stderr, stdout=sys.stdout): + print(f"Executing command: '{command}'") + process = subprocess.Popen(command, shell=shell, stderr=stderr, stdout=stdout) + process.wait() + if process.returncode != 0: + raise RuntimeError(f"Failed to execute command: '{command}'") + + +if __name__ == "__main__": + print("Important: this script should be executed from the root of the project (e.g. `./extensions/pcl/install_deps.py`)") + + sys.exit(install_deps()) diff --git a/extensions/ros2/install_deps.py b/extensions/ros2/install_deps.py new file mode 100755 index 000000000..a4773790b --- /dev/null +++ b/extensions/ros2/install_deps.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +import os +import platform +import sys +import subprocess +from pathlib import Path + + +class Config: + SUPPORTED_ROS_DISTROS = ["humble"] + + # Paths relative to project root + RADAR_MSGS_DIR = os.path.join("external", "radar_msgs") + RADAR_MSGS_INSTALL_DIR = os.path.join("external", "radar_msgs", "install") + RADAR_MSGS_COMMIT = "47d2f26906ef38fa15ada352aea6b5aad547781d" + + +def install_deps(): + cfg = Config() + + # Install dependencies for ROS2 extension + check_ros2_version() + install_ros2_deps(cfg) + + print("ROS2 deps installed successfully") + + +def are_deps_installed() -> bool: + cfg = Config() + return os.path.isdir(cfg.RADAR_MSGS_INSTALL_DIR) + + +def install_ros2_deps(cfg): + # Install colcon if needed + if not is_command_available("colcon --help"): + if on_windows(): + run_subprocess_command("pip install colcon-common-extensions") + else: + run_subprocess_command("sudo apt-get install -y python3-colcon-common-extensions") + # Clone radar msgs + if not os.path.isdir(cfg.RADAR_MSGS_DIR): + run_subprocess_command("ls") + run_subprocess_command( + f"git clone --single-branch --depth 1 https://github.com/ros-perception/radar_msgs.git {cfg.RADAR_MSGS_DIR}") + run_subprocess_command(f"cd {cfg.RADAR_MSGS_DIR} && git checkout {cfg.RADAR_MSGS_COMMIT} && cd ..") + # Build radar msgs + if not os.path.isdir(cfg.RADAR_MSGS_INSTALL_DIR): + original_path = Path.cwd() + os.chdir(cfg.RADAR_MSGS_DIR) + run_subprocess_command(f"colcon build") + os.chdir(original_path) + # TODO: cyclonedds rmw may be installed here (instead of manually in readme) + + +def check_ros2_version(): + if "ROS_DISTRO" not in os.environ and "AMENT_PREFIX_PATH" not in os.environ: + raise RuntimeError("ROS2 environment not found! Make sure you have sourced ROS2 setup file") + if os.environ["ROS_DISTRO"] not in Config().SUPPORTED_ROS_DISTROS: + raise RuntimeError(f"ROS distro '{os.environ['ROS_DISTRO']}' not supported. Choose one of {Config().SUPPORTED_ROS_DISTROS}") + + +def on_windows(): + return platform.system() == "Windows" + + +def run_subprocess_command(command: str, shell=True, stderr=sys.stderr, stdout=sys.stdout): + print(f"Executing command: '{command}'") + process = subprocess.Popen(command, shell=shell, stderr=stderr, stdout=stdout) + process.wait() + if process.returncode != 0: + raise RuntimeError(f"Failed to execute command: '{command}'") + + +def is_command_available(command): + process = subprocess.Popen(f"{command}", shell=True, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) + process.wait() + return process.returncode == 0 + + +if __name__ == "__main__": + print("Important: this script should be executed from the root of the project (e.g. `./extensions/ros2/install_deps.py`)") + + sys.exit(install_deps()) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 551cb9391..c622a32eb 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -1,19 +1,7 @@ -include(FetchContent) -FetchContent_Declare( - spdlog - GIT_REPOSITORY https://github.com/gabime/spdlog.git - GIT_TAG v1.9.2 - GIT_SHALLOW TRUE -) -FetchContent_MakeAvailable(spdlog) - -FetchContent_Declare( - cmake_git_version_tracking - GIT_REPOSITORY https://github.com/andrew-hardin/cmake-git-version-tracking.git - GIT_TAG 904dbda1336ba4b9a1415a68d5f203f576b696bb -) -FetchContent_MakeAvailable(cmake_git_version_tracking) +add_subdirectory("${PROJECT_SOURCE_DIR}/external/spdlog") +add_subdirectory("${PROJECT_SOURCE_DIR}/external/yaml-cpp") +add_subdirectory("${PROJECT_SOURCE_DIR}/external/cmake_git_version_tracking") # An attempt to disable targets from yaml-cpp; had difficulties with some of them # More details here: https://github.com/jbeder/yaml-cpp/issues/1158 @@ -22,13 +10,6 @@ set(YAML_CPP_BUILD_CONTRIB OFF CACHE INTERNAL "Disable yaml-cpp artifacts") set(YAML_CPP_BUILD_TOOLS OFF CACHE INTERNAL "Disable yaml-cpp artifacts") set(YAML_CPP_INSTALL OFF CACHE INTERNAL "Disable yaml-cpp artifacts") set(YAML_CPP_FORMAT_SOURCE OFF CACHE INTERNAL "Disable yaml-cpp artifacts") -FetchContent_Declare( - yaml-cpp - GIT_REPOSITORY https://github.com/jbeder/yaml-cpp.git - GIT_TAG yaml-cpp-0.7.0 - GIT_SHALLOW TRUE -) -FetchContent_MakeAvailable(yaml-cpp) set_property(TARGET yaml-cpp PROPERTY POSITION_INDEPENDENT_CODE ON) # Disable compilation warnings for dependencies @@ -51,15 +32,10 @@ if (UNIX) endif() if (${RGL_BUILD_TESTS} OR ${RGL_BUILD_TAPED_TESTS}) + add_subdirectory("${PROJECT_SOURCE_DIR}/external/googletest") + set(INSTALL_GTEST OFF CACHE INTERNAL "Disable installation of googletest") - FetchContent_Declare( - googletest - GIT_REPOSITORY https://github.com/google/googletest - GIT_TAG release-1.11.0 - GIT_SHALLOW TRUE - ) # For Windows: Prevent overriding the parent project's compiler/linker settings set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) - FetchContent_MakeAvailable(googletest) endif() diff --git a/install_deps.py b/install_deps.py new file mode 100755 index 000000000..c3391cbc1 --- /dev/null +++ b/install_deps.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +import os +import sys +import subprocess + + +class Config: + # Paths relative to project root + SPDLOG_DIR = os.path.join("external", "spdlog") + SPDLOG_VERSION = "v1.9.2" + + CMAKE_GIT_VERSION_TRACKING_DIR = os.path.join("external", "cmake_git_version_tracking") + CMAKE_GIT_VERSION_TRACKING_VERSION = "904dbda1336ba4b9a1415a68d5f203f576b696bb" + + YAML_CPP_DIR = os.path.join("external", "yaml-cpp") + YAML_CPP_VERSION = "yaml-cpp-0.7.0" + + GOOGLETEST_DIR = os.path.join("external", "googletest") + GOOGLETEST_VERSION = "release-1.11.0" + + +def install_deps(): + cfg = Config() + + # Go to script directory + os.chdir(sys.path[0]) + + if not os.path.isdir(cfg.SPDLOG_DIR): + run_subprocess_command( + f"git clone -b {cfg.SPDLOG_VERSION} --single-branch --depth 1 https://github.com/gabime/spdlog.git {cfg.SPDLOG_DIR}") + + if not os.path.isdir(cfg.CMAKE_GIT_VERSION_TRACKING_DIR): + run_subprocess_command( + f"git clone --single-branch https://github.com/andrew-hardin/cmake-git-version-tracking.git {cfg.CMAKE_GIT_VERSION_TRACKING_DIR}") + run_subprocess_command( + f"cd {cfg.CMAKE_GIT_VERSION_TRACKING_DIR} && git reset --hard {cfg.CMAKE_GIT_VERSION_TRACKING_VERSION}") + os.chdir(sys.path[0]) # Back to script directory + + if not os.path.isdir(cfg.YAML_CPP_DIR): + run_subprocess_command( + f"git clone -b {cfg.YAML_CPP_VERSION} --single-branch --depth 1 https://github.com/jbeder/yaml-cpp.git {cfg.YAML_CPP_DIR}") + + if not os.path.isdir(cfg.GOOGLETEST_DIR): + run_subprocess_command( + f"git clone -b {cfg.GOOGLETEST_VERSION} --single-branch --depth 1 https://github.com/google/googletest {cfg.GOOGLETEST_DIR}") + + print("RGL deps installed successfully") + + +def are_deps_installed() -> bool: + cfg = Config() + return os.path.isdir(cfg.SPDLOG_DIR) \ + and os.path.isdir(cfg.CMAKE_GIT_VERSION_TRACKING_DIR) \ + and os.path.isdir(cfg.YAML_CPP_DIR) \ + and os.path.isdir(cfg.GOOGLETEST_DIR) + + +def run_subprocess_command(command: str, shell=True, stderr=sys.stderr, stdout=sys.stdout): + print(f"Executing command: '{command}'") + process = subprocess.Popen(command, shell=shell, stderr=stderr, stdout=stdout) + process.wait() + if process.returncode != 0: + raise RuntimeError(f"Failed to execute command: '{command}'") + + +if __name__ == "__main__": + sys.exit(install_deps()) diff --git a/ros2_standalone/CMakeLists.txt b/ros2_standalone/CMakeLists.txt index 8524cbdfb..d823c89c2 100644 --- a/ros2_standalone/CMakeLists.txt +++ b/ros2_standalone/CMakeLists.txt @@ -16,7 +16,7 @@ set(REQ_STANDALONE_LIBS "") set(REQ_THIRD_PARTY_STANDALONE_LIBS "") set(REQ_STANDALONE_DLLS "") -set(INSTALL_DESTINATION_DIR "ros2_standalone") +set(INSTALL_DESTINATION_DIR "lib/ros2_standalone") # Extend REQ_THIRD_PARTY_STANDALONE_LIBS with _library_name third party dependencies macro(get_standalone_third_party_dependencies _library_name) diff --git a/setup.py b/setup.py index 2570b7ac2..f29f55260 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +import sys +sys.dont_write_bytecode = True import os import platform import sys @@ -6,7 +8,10 @@ import subprocess import shutil import argparse -from pathlib import Path + +import install_deps as core_deps +from extensions.ros2 import install_deps as ros2_deps +from extensions.pcl import install_deps as pcl_deps class Config: @@ -15,32 +20,17 @@ class Config: CUDA_MIN_VER_MINOR = 7 CUDA_MIN_VER_PATCH = 0 CMAKE_GENERATOR = "'Unix Makefiles'" - RADAR_MSGS_DIR = os.path.join("external", "radar_msgs") - RADAR_MSGS_INSTALL_DIR = os.path.join("external", "radar_msgs", "install") - RADAR_MSGS_COMMIT = "47d2f26906ef38fa15ada352aea6b5aad547781d" - VCPKG_DIR = os.path.join("external", "vcpkg") - VCPKG_TAG = "2023.08.09" - VCPKG_EXEC = "vcpkg" - VCPKG_BOOTSTRAP = "bootstrap-vcpkg.sh" - VCPKG_TRIPLET = "x64-linux" - TAPED_TEST_DATA_DIR = os.path.join("external", "taped_test_data") - TAPED_TEST_DATA_REPO = "git@github.com:RobotecAI/RGL-blobs.git" - TAPED_TEST_DATA_BRANCH = "main" + + RGL_BLOBS_DIR = os.path.join("external", "rgl_blobs") + RGL_BLOBS_REPO = "git@github.com:RobotecAI/RGL-blobs.git" + RGL_BLOBS_BRANCH = "main" def __init__(self): # Platform-dependent configuration - if inside_docker(): - self.VCPKG_DIR = os.path.join("/rgldep", "vcpkg") - self.RADAR_MSGS_DIR = os.path.join("/rgldep", "radar_msgs") - self.RADAR_MSGS_INSTALL_DIR = os.path.join("/rgldep", "radar_msgs", "install") - if on_windows(): self.CUDA_MIN_VER_MINOR = 4 self.CUDA_MIN_VER_PATCH = 152 # patch for CUDA 11.4 Update 4 self.CMAKE_GENERATOR = "Ninja" - self.VCPKG_EXEC = "vcpkg.exe" - self.VCPKG_BOOTSTRAP = "bootstrap-vcpkg.bat" - self.VCPKG_TRIPLET = "x64-windows" def main(): @@ -49,10 +39,14 @@ def main(): parser = argparse.ArgumentParser(description="Helper script to build RGL.") parser.add_argument("--build-dir", type=str, default="build", help="Path to build directory. Default: 'build'") + parser.add_argument("--install-deps", action='store_true', + help="Install dependencies for RGL and exit") parser.add_argument("--install-pcl-deps", action='store_true', help="Install dependencies for PCL extension and exit") parser.add_argument("--install-ros2-deps", action='store_true', help="Install dependencies for ROS2 extension and exit") + parser.add_argument("--fetch-rgl-blobs", action='store_true', + help="Fetch RGL blobs and exit (repo used for storing closed-source testing data)") parser.add_argument("--clean-build", action='store_true', help="Remove build directory before cmake") parser.add_argument("--with-pcl", action='store_true', @@ -72,10 +66,8 @@ def main(): help="Pass arguments to make. Usage: --make=\"args...\". Defaults to \"-j \"") parser.add_argument("--lib-rpath", type=str, nargs='*', help="Add run-time search path(s) for RGL library. $ORIGIN (actual library path) is added by default.") - parser.add_argument("--install-taped-test-deps", action='store_true', - help="Install dependencies for taped test and exit (closed-source dependencies)") parser.add_argument("--build-taped-test", action='store_true', - help="Build taped test") + help="Build taped test (requires RGL blobs repo in runtime)") if on_windows(): parser.add_argument("--ninja", type=str, default=f"-j{os.cpu_count()}", dest="build_args", help="Pass arguments to ninja. Usage: --ninja=\"args...\". Defaults to \"-j \"") @@ -88,36 +80,26 @@ def main(): # Go to script directory os.chdir(sys.path[0]) - # Prepare build directory - if args.clean_build and os.path.isdir(args.build_dir): - shutil.rmtree(args.build_dir, ignore_errors=True) - if not os.path.isdir(args.build_dir): - os.makedirs(args.build_dir) + # Install RGL dependencies + if args.install_deps: + core_deps.install_deps() + return 0 # Install dependencies for PCL extension if args.install_pcl_deps: - install_pcl_deps(cfg) - print('Installed PCL deps, exiting...') + pcl_deps.install_deps() return 0 # Install dependencies for ROS2 extension if args.install_ros2_deps: - check_ros2_version() - install_ros2_deps(cfg) - print('Installed ROS2 deps, exiting...') + ros2_deps.install_deps() return 0 - # Install taped test dependencies - if on_linux() and args.install_taped_test_deps: - install_taped_test_deps(cfg) - print('Installed dependencies for taped test, exiting...') + # Install dependencies for ROS2 extension + if args.fetch_rgl_blobs: + fetch_rgl_blobs_repo(cfg) return 0 - # Check taped test requirements - if on_linux() and args.build_taped_test and not args.with_pcl: - raise RuntimeError( - "Taped test requires PCL extension to be built: run this script with --with-pcl flag") - # Check CUDA if not is_cuda_version_ok(cfg): raise RuntimeError( @@ -127,30 +109,41 @@ def main(): if os.environ["OptiX_INSTALL_DIR"] == "": raise RuntimeError("OptiX not found! Make sure you have exported environment variable OptiX_INSTALL_DIR") - # Check extension requirements - if args.with_pcl and not os.path.isdir(cfg.VCPKG_DIR): + # Check if dependencies are installed + if not core_deps.are_deps_installed(): + raise RuntimeError( + "RGL requires dependencies to be installed: run this script with --install-deps flag") + + if args.with_pcl and not pcl_deps.are_deps_installed(): raise RuntimeError( "PCL extension requires dependencies to be installed: run this script with --install-pcl-deps flag") - if args.with_ros2 and not os.path.isdir(cfg.RADAR_MSGS_INSTALL_DIR): + if args.with_ros2 and not ros2_deps.are_deps_installed(): raise RuntimeError( "ROS2 extension requires radar_msgs to be built: run this script with --install-ros2-deps flag") + # Prepare build directory + if args.clean_build and os.path.isdir(args.build_dir): + shutil.rmtree(args.build_dir, ignore_errors=True) + if not os.path.isdir(args.build_dir): + os.makedirs(args.build_dir) + # Extend Path with libRobotecGPULidar location to link tests properly during the build on Windows if on_windows(): os.environ["Path"] = os.environ["Path"] + ";" + os.path.join(os.getcwd(), args.build_dir) if args.with_ros2: + cfg_ros2 = ros2_deps.Config() # Source environment for additional packages # ROS2 itself must be sourced by the user, because its location is unknown to this script - check_ros2_version() + ros2_deps.check_ros2_version() setup = "setup.bat" if on_windows() else "setup.sh" - source_environment(os.path.join(os.getcwd(), cfg.RADAR_MSGS_INSTALL_DIR, setup)) + source_environment(os.path.join(os.getcwd(), cfg_ros2.RADAR_MSGS_INSTALL_DIR, setup)) # Build cmake_args = [ - f"-DCMAKE_TOOLCHAIN_FILE={os.path.join(cfg.VCPKG_DIR, 'scripts', 'buildsystems', 'vcpkg.cmake') if args.with_pcl else ''}", - f"-DVCPKG_TARGET_TRIPLET={cfg.VCPKG_TRIPLET if args.with_pcl else ''}", + f"-DCMAKE_TOOLCHAIN_FILE={os.path.join(pcl_deps.Config().VCPKG_DIR, 'scripts', 'buildsystems', 'vcpkg.cmake') if args.with_pcl else ''}", + f"-DVCPKG_TARGET_TRIPLET={pcl_deps.Config().VCPKG_TRIPLET if args.with_pcl else ''}", f"-DRGL_BUILD_PCL_EXTENSION={'ON' if args.with_pcl else 'OFF'}", f"-DRGL_BUILD_ROS2_EXTENSION={'ON' if args.with_ros2 else 'OFF'}", f"-DRGL_BUILD_UDP_EXTENSION={'ON' if args.with_udp else 'OFF'}", @@ -165,7 +158,7 @@ def main(): rpath = rpath.replace("$ORIGIN", "\\$ORIGIN") # cmake should not treat this as variable linker_rpath_flags.append(f"-Wl,-rpath={rpath}") cmake_args.append(f"-DCMAKE_SHARED_LINKER_FLAGS=\"{' '.join(linker_rpath_flags)}\"") - # Taped tests + # Taped test cmake_args.append(f"-DRGL_BUILD_TAPED_TESTS={'ON' if args.build_taped_test else 'OFF'}") # Append user args, possibly overwriting @@ -194,15 +187,6 @@ def on_windows(): return platform.system() == "Windows" -def inside_docker(): - path = "/proc/self/cgroup" - return ( - os.environ.get("RGL_IS_BUILDING_DOCKER_IMAGE", False) or - os.path.exists("/.dockerenv") or - (os.path.isfile(path) and any("docker" in line for line in open(path))) - ) - - def run_subprocess_command(command: str, shell=True, stderr=sys.stderr, stdout=sys.stdout): print(f"Executing command: '{command}'") process = subprocess.Popen(command, shell=shell, stderr=stderr, stdout=stdout) @@ -211,12 +195,6 @@ def run_subprocess_command(command: str, shell=True, stderr=sys.stderr, stdout=s raise RuntimeError(f"Failed to execute command: '{command}'") -def run_system_command(command: str): - print(f"Executing command: '{command}'") - if os.system(command) != 0: - raise RuntimeError(f"Failed to execute command: '{command}'") - - def is_cuda_version_ok(cfg): nvcc_process = subprocess.run("nvcc --version", shell=True, stdout=subprocess.PIPE) nvcc_ver_match = re.search("V[0-9]+.[0-9]+.[0-9]+", nvcc_process.stdout.decode("utf-8")) @@ -235,93 +213,6 @@ def is_cuda_version_ok(cfg): return True -def install_pcl_deps(cfg): - # Clone vcpkg - if not os.path.isdir(cfg.VCPKG_DIR): - if on_linux() and not inside_docker(): # Inside docker already installed - print("Installing dependencies for vcpkg...") - run_system_command("sudo apt update") - run_system_command("sudo apt install git curl zip unzip tar freeglut3-dev libglew-dev libglfw3-dev") - run_subprocess_command( - f"git clone -b {cfg.VCPKG_TAG} --single-branch --depth 1 https://github.com/microsoft/vcpkg {cfg.VCPKG_DIR}") - # Bootstrap vcpkg - if not os.path.isfile(os.path.join(cfg.VCPKG_DIR, cfg.VCPKG_EXEC)): - run_subprocess_command(f"{os.path.join(cfg.VCPKG_DIR, cfg.VCPKG_BOOTSTRAP)}") - - # Install dependencies via vcpkg - run_subprocess_command( - f"{os.path.join(cfg.VCPKG_DIR, cfg.VCPKG_EXEC)} install --clean-after-build pcl[core,visualization]:{cfg.VCPKG_TRIPLET}") - - -def is_command_available(command): - process = subprocess.Popen(f"{command}", shell=True, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) - process.wait() - return process.returncode == 0 - - -def install_ros2_deps(cfg): - # Install colcon if needed - if not is_command_available("colcon --help"): - if on_windows(): - run_system_command("pip install colcon-common-extensions") - elif not inside_docker(): # Linux; Inside docker already installed - run_system_command("sudo apt update") - run_system_command("sudo apt install python3-colcon-common-extensions") - # Clone radar msgs - if not os.path.isdir(cfg.RADAR_MSGS_DIR): - run_subprocess_command( - f"git clone --single-branch --depth 1 https://github.com/ros-perception/radar_msgs.git {cfg.RADAR_MSGS_DIR}") - run_subprocess_command(f"cd {cfg.RADAR_MSGS_DIR} && git checkout {cfg.RADAR_MSGS_COMMIT} && cd ..") - # Build radar msgs - if not os.path.isdir(cfg.RADAR_MSGS_INSTALL_DIR): - original_path = Path.cwd() - os.chdir(cfg.RADAR_MSGS_DIR) - run_subprocess_command(f"colcon build") - os.chdir(original_path) - # TODO: cyclonedds rmw may be installed here (instead of manually in readme) - - -def ensure_git_lfs_installed(): - if not is_command_available("git-lfs --help"): - print("Installing git-lfs...") - run_subprocess_command( - "curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | sudo bash") - run_subprocess_command("sudo apt install git-lfs") - - -def clone_taped_test_data_repo(cfg): - run_subprocess_command( - f"git clone -b {cfg.TAPED_TEST_DATA_BRANCH} --single-branch --depth 1 {cfg.TAPED_TEST_DATA_REPO} {cfg.TAPED_TEST_DATA_DIR}") - os.chdir(cfg.TAPED_TEST_DATA_DIR) - # Set up git-lfs for this repository - run_subprocess_command("git-lfs install && git-lfs pull") - - -def is_taped_data_up_to_date(cfg): - result = subprocess.Popen("git fetch --dry-run --verbose", shell=True, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - stdout, _ = result.communicate() - return f"[up to date] {cfg.TAPED_TEST_DATA_BRANCH}" in stdout.decode() - - -def update_taped_test_data_repo(cfg): - if not is_taped_data_up_to_date(cfg): - print("Updating taped test benchmark data repository...") - run_subprocess_command("git pull && git-lfs pull") - - -def install_taped_test_deps(cfg): - # Cloning and updating taped test benchmark data repo requires git-lfs to be installed - ensure_git_lfs_installed() - if not os.path.isdir(cfg.TAPED_TEST_DATA_DIR): - print("Cloning taped test benchmark data repository...") - clone_taped_test_data_repo(cfg) - else: - print("Checking for updates in taped test benchmark data repository...") - os.chdir(cfg.TAPED_TEST_DATA_DIR) - update_taped_test_data_repo(cfg) - - # Returns a dict with env variables visible for a command after running in a system shell # Used to capture effects of sourcing file such as ros2 setup def capture_environment(command="cd ."): @@ -352,11 +243,44 @@ def source_environment(filepath): os.environ[new_key] = new_env[new_key] -def check_ros2_version(): - if "ROS_DISTRO" not in os.environ: - raise RuntimeError("ROS2 environment not found! Make sure you have sourced ROS2 setup file") - if os.environ["ROS_DISTRO"] != "humble": - raise RuntimeError(f"RGL requires ROS2 humble, found {os.environ['ROS_DISTRO']}") +def is_command_available(command): + process = subprocess.Popen(f"{command}", shell=True, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) + process.wait() + return process.returncode == 0 + + +def fetch_rgl_blobs_repo(cfg): + def is_up_to_date(branch): + result = subprocess.Popen("git fetch --dry-run --verbose", shell=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + stdout, _ = result.communicate() + return f"[up to date] {branch}" in stdout.decode() + + def ensure_git_lfs_installed(): + + if not is_command_available("git-lfs --help"): + print("Installing git-lfs...") + run_subprocess_command( + "curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | sudo bash") + run_subprocess_command("sudo apt install git-lfs") + + ensure_git_lfs_installed() + if not os.path.isdir(cfg.RGL_BLOBS_DIR): + print("Cloning rgl blobs repository...") + run_subprocess_command( + f"git clone -b {cfg.RGL_BLOBS_BRANCH} --single-branch --depth 1 {cfg.RGL_BLOBS_REPO} {cfg.RGL_BLOBS_DIR}") + os.chdir(cfg.RGL_BLOBS_DIR) + # Set up git-lfs for this repository + run_subprocess_command("git-lfs install && git-lfs pull") + print("RGL blobs repo cloned successfully") + return + + print("Checking for updates in rgl blobs repository...") + os.chdir(cfg.RGL_BLOBS_DIR) + if not is_up_to_date(cfg.RGL_BLOBS_BRANCH): + print("Updating rgl blobs repository...") + run_subprocess_command("git pull && git-lfs pull") + print("RGL blobs repo fetched successfully") if __name__ == "__main__": diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6dbc7b331..3de78dde9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.16) +cmake_minimum_required(VERSION 3.18) # # 3.18 To have DISCOVERY_MODE option for gtest_discover_tests() set(RGL_TEST_FILES src/apiReadmeExample.cpp @@ -77,6 +77,7 @@ endif() file(COPY ${CMAKE_SOURCE_DIR}/test/data/ DESTINATION ${CMAKE_BINARY_DIR}/data/) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/test) add_executable(RobotecGPULidar_test ${RGL_TEST_FILES}) target_compile_definitions(RobotecGPULidar_test PRIVATE @@ -120,6 +121,10 @@ if (RGL_BUILD_ROS2_EXTENSION) ) endif() +# Set $ORIGIN rpath to search for dependencies in the executable location (Linux only) +set_target_properties(RobotecGPULidar_test PROPERTIES LINK_FLAGS "-Wl,-rpath,$ORIGIN") + include(GoogleTest) -gtest_discover_tests(RobotecGPULidar_test) \ No newline at end of file +gtest_discover_tests(RobotecGPULidar_test + DISCOVERY_MODE PRE_TEST) diff --git a/test/taped_test/CMakeLists.txt b/test/taped_test/CMakeLists.txt index b721c553c..c33a48eb3 100644 --- a/test/taped_test/CMakeLists.txt +++ b/test/taped_test/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.16) +cmake_minimum_required(VERSION 3.18) # # 3.18 To have DISCOVERY_MODE option for gtest_discover_tests() if(WIN32) message(FATAL_ERROR "Tape not supported on Windows") @@ -10,6 +10,7 @@ set(RGL_TAPED_TEST_FILES include(GoogleTest) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/test) add_executable(RobotecGPULidar_taped_test ${RGL_TAPED_TEST_FILES}) target_link_libraries(RobotecGPULidar_taped_test PRIVATE @@ -24,4 +25,8 @@ target_include_directories(RobotecGPULidar_taped_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../include ) -gtest_discover_tests(RobotecGPULidar_taped_test) \ No newline at end of file +# Set $ORIGIN rpath to search for dependencies in the executable location (Linux only) +set_target_properties(RobotecGPULidar_taped_test PROPERTIES LINK_FLAGS "-Wl,-rpath,$ORIGIN") + +gtest_discover_tests(RobotecGPULidar_taped_test + DISCOVERY_MODE PRE_TEST) diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 1e7f93d89..be2e354d0 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -1,3 +1,4 @@ +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/tools) add_executable(inspectLibRGL inspectLibRGL.cpp) # Linux only - tape related tools @@ -6,9 +7,13 @@ if ((NOT WIN32)) add_executable(tapeVisualizer tapeVisualizer.cpp) target_link_libraries(tapeVisualizer RobotecGPULidar spdlog yaml-cpp) target_include_directories(tapeVisualizer PRIVATE ${CMAKE_SOURCE_DIR}/src) + # Set $ORIGIN rpath to search for dependencies in the executable location (Linux only) + set_target_properties(tapeVisualizer PROPERTIES LINK_FLAGS "-Wl,-rpath,$ORIGIN") endif() add_executable(tapePlayer tapePlayer.cpp) target_link_libraries(tapePlayer RobotecGPULidar spdlog) target_include_directories(tapePlayer PRIVATE ${CMAKE_SOURCE_DIR}/src) + # Set $ORIGIN rpath to search for dependencies in the executable location (Linux only) + set_target_properties(tapePlayer PROPERTIES LINK_FLAGS "-Wl,-rpath,$ORIGIN") endif()