diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9282590 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +defaults: + run: + shell: bash + +env: + DEBIAN_FRONTEND: noninteractive + +jobs: + build: + name: Ubuntu ${{ matrix.ubuntu }} (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + container: ubuntu:${{ matrix.ubuntu }} + strategy: + fail-fast: false + matrix: + ubuntu: ["24.04", "26.04"] + arch: [amd64, arm64] + include: + - arch: amd64 + runner: ubuntu-24.04 + - arch: arm64 + runner: ubuntu-24.04-arm + steps: + # Without git, actions/checkout downloads an archive, and init.sh can't check out the submodules + - name: Install git + run: apt-get update && apt-get install -y --no-install-recommends ca-certificates git + - uses: actions/checkout@v5 + - name: Install dependencies + run: | + git config --global --add safe.directory '*' + ./init.sh + - name: Build + run: ./build.sh + - name: Test + run: ctest --test-dir build --output-on-failure --no-tests=error diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..82f6e36 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,7 @@ +[submodule "external/openocean-messages"] + path = external/openocean-messages + url = https://github.com/openocean-software/openocean-messages.git +[submodule "external/moos-ivp"] + path = external/moos-ivp + url = https://github.com/GobySoft/moos-ivp.git + shallow = true diff --git a/CMakeLists.txt b/CMakeLists.txt index b847828..78324dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,4 +9,9 @@ endif() set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/") +# MOOS-IvP's helm libraries (IVP_BUILD_HELM_LIBS_ONLY) +find_package(IVP CONFIG REQUIRED) + +include(CTest) + add_subdirectory(src) diff --git a/README.md b/README.md index 2f1ac14..9421a49 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,26 @@ One way to think of this project is as a generator for the equivalent of the MOO - bin: Binary source - goby: Goby middleware app - ros: ROS middleware node +- external: Submodules + - openocean-messages: [openocean-messages](https://github.com/openocean-software/openocean-messages), for the `Navigation` and `ControlSetpoint` messages + - moos-ivp: [MOOS-IvP](https://github.com/GobySoft/moos-ivp), for the IvP solver and behaviors ## Building *This section was written by Claude.* -The interface messages use `Navigation` and `ControlSetpoint` from [openocean-messages](https://github.com/openocean-software/openocean-messages), found with `find_package(openocean_messages)`: install it, or point CMake at its build directory: +`init.sh` checks out the submodules and installs the Ubuntu packages needed to build them and IvPico. `build.sh` then builds, all in `build/`: + +- openocean-messages, into `build/external/openocean-messages`; +- MOOS-IvP's helm libraries (`IVP_BUILD_HELM_LIBS_ONLY`, as shared libraries, without MOOS or its apps), into `build/external/moos-ivp`, installed into `build/external/install` for `find_package(IVP)`; +- IvPico, against both. ``` -cmake -S . -B build -Dopenocean_messages_DIR=/path/to/openocean-messages/build -cmake --build build +./init.sh +./build.sh +ctest --test-dir build ``` + +`build.sh` passes any arguments on to IvPico's CMake, and `BUILD_DIR` overrides the build directory. + +To update a submodule, check out the commit you want in it and commit the new pointer, e.g. `git -C external/openocean-messages pull origin main && git add external/openocean-messages`. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..bfbe048 --- /dev/null +++ b/build.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Builds the dependencies from their submodules (openocean-messages, and MOOS-IvP's helm libraries), +# then IvPico against them, all in BUILD_DIR (default build). Arguments are passed on to IvPico's +# cmake. +set -euo pipefail + +cd "$(dirname "$0")" +for submodule in openocean-messages moos-ivp; do + if [ -z "$(ls -A "external/${submodule}")" ]; then + echo "external/${submodule} is empty: run ./init.sh first" >&2 + exit 1 + fi +done +BUILD_DIR=$(realpath -m "${BUILD_DIR:-build}") +OPENOCEAN_BUILD_DIR=${BUILD_DIR}/external/openocean-messages +IVP_BUILD_DIR=${BUILD_DIR}/external/moos-ivp +# find_package(IVP) only works from an installation +IVP_INSTALL_DIR=${BUILD_DIR}/external/install + +BUILD_DIR=${OPENOCEAN_BUILD_DIR} external/openocean-messages/build.sh -DBUILD_TESTING=OFF + +cmake -S external/moos-ivp/ivp/src -B "${IVP_BUILD_DIR}" -G Ninja \ + -DIVP_BUILD_HELM_LIBS_ONLY=ON -DBUILD_SHARED_LIBS=ON \ + -DIVP_LIB_DIRECTORY="${IVP_BUILD_DIR}/lib" \ + -DIVP_INC_DIRECTORY="${IVP_BUILD_DIR}/include" \ + -DIVP_BIN_DIRECTORY="${IVP_BUILD_DIR}/bin" +ninja -C "${IVP_BUILD_DIR}" +cmake --install "${IVP_BUILD_DIR}" --prefix "${IVP_INSTALL_DIR}" > /dev/null + +cmake -S . -B "${BUILD_DIR}" -G Ninja \ + -Dopenocean_messages_DIR="${OPENOCEAN_BUILD_DIR}" \ + -DIVP_DIR="${IVP_INSTALL_DIR}/lib/cmake/IVP" \ + "$@" +ninja -C "${BUILD_DIR}" diff --git a/external/moos-ivp b/external/moos-ivp new file mode 160000 index 0000000..be40e2c --- /dev/null +++ b/external/moos-ivp @@ -0,0 +1 @@ +Subproject commit be40e2c7cef168be29f57a756df6a16a1c7882f1 diff --git a/external/openocean-messages b/external/openocean-messages new file mode 160000 index 0000000..11a1328 --- /dev/null +++ b/external/openocean-messages @@ -0,0 +1 @@ +Subproject commit 11a132839af11befb3d858f4a4b3570d9dc013e5 diff --git a/init.sh b/init.sh new file mode 100755 index 0000000..1821fc7 --- /dev/null +++ b/init.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Checks out the submodules and installs the Ubuntu packages needed to build them and IvPico. +# +# Usage: init.sh +set -euo pipefail + +cd "$(dirname "$0")" + +case "${1:-}" in + "") ;; + -h | --help) + sed -n '4p' "$0" | sed 's/^# //' + exit 0 + ;; + *) + sed -n '4p' "$0" | sed 's/^# //' >&2 + exit 1 + ;; +esac + +git submodule update --init --recommend-shallow + +SUDO="" +if [ "$(id -u)" -ne 0 ]; then + SUDO=sudo +fi + +# moos-ivp's helm libraries (IVP_BUILD_HELM_LIBS_ONLY) need only CMake and a compiler +packages=(cmake ninja-build g++ git ca-certificates protobuf-compiler libprotobuf-dev) +$SUDO apt-get update +$SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${packages[@]}" + +external/openocean-messages/init.sh --cxx diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1d235e1..9489610 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,2 +1,6 @@ add_subdirectory(lib) add_subdirectory(bin) + +if(BUILD_TESTING) + add_subdirectory(test) +endif() diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt new file mode 100644 index 0000000..6631915 --- /dev/null +++ b/src/test/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(check_helm_libs check_helm_libs.cpp) +target_link_libraries(check_helm_libs PRIVATE IVP::helmivp IVP::ivpbuild IVP::ivpsolve IVP::ivpcore) +add_test(NAME helm_libs COMMAND check_helm_libs) diff --git a/src/test/check_helm_libs.cpp b/src/test/check_helm_libs.cpp new file mode 100644 index 0000000..0168326 --- /dev/null +++ b/src/test/check_helm_libs.cpp @@ -0,0 +1,59 @@ +// Checks that IvPico can use MOOS-IvP's helm libraries (external/moos-ivp, built by build.sh): +// +// 1. The static behavior factory creates behaviors from lib_behaviors-marine +// and lib_behaviors-colregs, which loads every helm library. +// 2. The IvP solver finds the peak of a one-function problem over course. +// +// Prints one line per problem and exits non-zero if there were any. + +#include +#include +#include + +#include "BFactoryStatic.h" +#include "IvPBehavior.h" +#include "IvPDomain.h" +#include "IvPProblem.h" +#include "ZAIC_PEAK.h" + +int main() +{ + int errors = 0; + + IvPDomain domain; + domain.addDomain("course", 0, 359, 360); + domain.addDomain("speed", 0, 4, 41); + + BFactoryStatic factory; + factory.setDomain(domain); + for (const std::string type : {"BHV_Waypoint", "BHV_AvdColregsV22"}) + { + IvPBehavior* behavior = factory.newBehavior(type); + if (!behavior) + { + std::cerr << "failed: no " << type << " from BFactoryStatic\n"; + ++errors; + } + delete behavior; + } + + IvPDomain course; + course.addDomain("course", 0, 359, 360); + ZAIC_PEAK zaic(course, "course"); + zaic.setSummit(90); + zaic.setBaseWidth(90); + zaic.setValueWrap(true); + + IvPProblem problem; + problem.setDomain(course); + problem.addOF(zaic.extractIvPFunction()); + problem.solve(); + const double result = problem.getResult("course"); + if (std::fabs(result - 90) > 1) + { + std::cerr << "failed: solved course " << result << ", not 90\n"; + ++errors; + } + + return errors == 0 ? 0 : 1; +}