From f6b0f71b946038390ee307c57c5b9520121e5ece Mon Sep 17 00:00:00 2001 From: Iori Yanokura Date: Tue, 28 Oct 2025 00:59:30 +0900 Subject: [PATCH 1/2] Add support for ROS-O (ROS One) distribution ROS-O is a community-supported ROS 1 distribution that works on Ubuntu 20.04 and newer versions. This change adds support for installing ROS-O alongside ROS 2 distributions. Changes: - Add ROS-O repository configuration and GPG key handling - Support mixed ROS 1 and ROS 2 repository installation - Add ROS-O test cases to CI workflow - Fix SSL certificate handling for ROS-O repository - Ensure correct repository selection based on Ubuntu version - Pin meson version to avoid colcon-meson compatibility issues The implementation allows installing multiple ROS distributions simultaneously, such as 'noetic rolling' on Ubuntu 20.04 or 'one humble' on Ubuntu 22.04. Technical notes: - Meson is constrained to >=0.60.0,<0.64.0 for colcon-meson 0.4.2 compatibility, avoiding IntrospectionInterpreter API breakage Signed-off-by: Iori Yanokura --- .github/workflows/test.yml | 10 ++++ __test__/main.test.ts | 1 + dist/index.js | 100 +++++++++++++++++++++++++++++++--- src/package_manager/pip.ts | 1 + src/setup-ros-ubuntu.ts | 109 +++++++++++++++++++++++++++++++++++-- src/utils.ts | 1 + 6 files changed, 209 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b4aee6040..fa8481ed4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,6 +8,7 @@ on: schedule: # Run the CI automatically twice per day to look for flakyness. - cron: "0 */12 * * *" + workflow_dispatch: defaults: run: @@ -115,6 +116,15 @@ jobs: ros_distribution: noetic ros_version: 1 + # ROS-O (ROS One) - ROS 1 for Ubuntu 20.04+ + - docker_image: ubuntu:jammy + ros_distribution: one + ros_version: 1 + + - docker_image: ubuntu:noble + ros_distribution: one + ros_version: 1 + # Humble Hawksbill (May 2022 - May 2027) - docker_image: ubuntu:jammy ros_distribution: humble diff --git a/__test__/main.test.ts b/__test__/main.test.ts index ed3780545..51d7c0674 100644 --- a/__test__/main.test.ts +++ b/__test__/main.test.ts @@ -64,6 +64,7 @@ describe("required-ros-distributions/noetic workflow tests", () => { describe("validate distribution test", () => { it("test valid", async () => { await expect(utils.validateDistro(["noetic"])).toBe(true); + await expect(utils.validateDistro(["one"])).toBe(true); await expect(utils.validateDistro(["humble"])).toBe(true); await expect(utils.validateDistro(["iron"])).toBe(true); await expect(utils.validateDistro(["jazzy"])).toBe(true); diff --git a/dist/index.js b/dist/index.js index 3c329bb0e..9ace45235 100644 --- a/dist/index.js +++ b/dist/index.js @@ -6682,6 +6682,7 @@ const pip3Packages = [ "colcon-recursive-crawl==0.2.1", "colcon-ros==0.3.23", "colcon-test-result==0.3.8", + "meson>=0.60.0,<0.64.0", "coverage", "cryptography", "empy<4", @@ -7144,7 +7145,13 @@ function configOs() { yield utils.exec("sudo", ["bash", "-c", "echo 'Etc/UTC' > /etc/timezone"]); yield utils.exec("sudo", ["apt-get", "update"]); // Install tools required to configure the worker system. - yield apt.runAptGetInstall(["curl", "gnupg2", "locales", "lsb-release"]); + yield apt.runAptGetInstall([ + "ca-certificates", + "curl", + "gnupg2", + "locales", + "lsb-release", + ]); // Select a locale supporting Unicode. yield utils.exec("sudo", ["locale-gen", "en_US", "en_US.UTF-8"]); core.exportVariable("LANG", "en_US.UTF-8"); @@ -7172,24 +7179,44 @@ function addAptRepoKey() { yield utils.exec("sudo", ["apt-key", "add", keyFilePath]); }); } +/** + * Add ROS-O (ROS One) APT repository key. + * + * Downloads and installs the GPG key for the ROS-O repository. + */ +function addRosOneAptRepoKey() { + return __awaiter(this, void 0, void 0, function* () { + // Ensure the keyrings directory exists and ca-certificates is up to date + yield utils.exec("sudo", ["mkdir", "-p", "/etc/apt/keyrings"]); + yield utils.exec("sudo", ["update-ca-certificates"]); + yield utils.exec("sudo", [ + "bash", + "-c", + "curl -sSL https://ros.packages.techfak.net/gpg.key -o /etc/apt/keyrings/ros-one-keyring.gpg", + ]); + }); +} // Ubuntu distribution for ROS 1 const ros1UbuntuVersion = "focal"; /** * Add OSRF APT repository. * * @param ubuntuCodename the Ubuntu version codename + * @param needsRos1 whether ROS 1 packages are needed + * @param needsRos2 whether ROS 2 packages are needed */ -function addAptRepo(ubuntuCodename, use_ros2_testing) { +function addAptRepo(ubuntuCodename, use_ros2_testing, needsRos1, needsRos2) { return __awaiter(this, void 0, void 0, function* () { - // There is now no Ubuntu version overlap between ROS 1 and ROS 2 - if (ros1UbuntuVersion === ubuntuCodename) { + // Add ROS 1 repository if needed + if (needsRos1) { yield utils.exec("sudo", [ "bash", "-c", `echo "deb http://packages.ros.org/ros/ubuntu ${ubuntuCodename} main" > /etc/apt/sources.list.d/ros-latest.list`, ]); } - else { + // Add ROS 2 repository if needed + if (needsRos2) { yield utils.exec("sudo", [ "bash", "-c", @@ -7199,6 +7226,24 @@ function addAptRepo(ubuntuCodename, use_ros2_testing) { yield utils.exec("sudo", ["apt-get", "update"]); }); } +/** + * Add ROS-O (ROS One) APT repository. + * + * @param ubuntuCodename the Ubuntu version codename + * @param use_testing whether to use the testing repository + */ +function addRosOneAptRepo(ubuntuCodename, use_testing) { + return __awaiter(this, void 0, void 0, function* () { + const arch = yield utils.getArch(); + const repo = use_testing ? `${ubuntuCodename}-testing` : ubuntuCodename; + yield utils.exec("sudo", [ + "bash", + "-c", + `echo "deb [arch=${arch} signed-by=/etc/apt/keyrings/ros-one-keyring.gpg] https://ros.packages.techfak.net ${repo} main" > /etc/apt/sources.list.d/ros-one.list`, + ]); + yield utils.exec("sudo", ["apt-get", "update"]); + }); +} /** * Initialize rosdep. */ @@ -7216,6 +7261,20 @@ function rosdepInit() { yield utils.exec("sudo", ["rosdep", "init"]); }); } +/** + * Configure rosdep for ROS-O (ROS One). + * + * Adds custom rosdep source for ROS-O packages. + */ +function configureRosOneRosdep() { + return __awaiter(this, void 0, void 0, function* () { + yield utils.exec("sudo", [ + "bash", + "-c", + 'echo "yaml https://ros.packages.techfak.net/ros-one.yaml one" > /etc/ros/rosdep/sources.list.d/1-ros-one.list', + ]); + }); +} /** * Install ROS 1 or 2 (development packages and/or ROS binaries) on a Linux worker. */ @@ -7224,10 +7283,32 @@ function runLinux() { // Get user input & validate const use_ros2_testing = core.getInput("use-ros2-testing") === "true"; const installConnext = core.getInput("install-connext") === "true"; + const requiredDistros = utils.getRequiredRosDistributions(); + const needsRosOne = requiredDistros.includes("one"); + // Determine which ROS versions are needed + // ROS 1 distributions: noetic (from packages.ros.org/ros) + // ROS 2 distributions: rolling, humble, jazzy, iron, kilted, etc. (from packages.ros.org/ros2) + // ROS-O "one": separate repository (ros.packages.techfak.net) + const ros1Distros = ["noetic"]; + const needsRos1 = requiredDistros.some((distro) => ros1Distros.includes(distro)); + const needsRos2 = requiredDistros.some((distro) => !ros1Distros.includes(distro) && distro !== "one"); yield configOs(); yield addAptRepoKey(); const ubuntuCodename = yield utils.determineDistribCodename(); - yield addAptRepo(ubuntuCodename, use_ros2_testing); + // For backward compatibility when no ROS distributions are specified: + // - Focal (Ubuntu 20.04): add ROS 1 repository (for focal-specific dependencies) + // - Other versions: add ROS 2 repository (for jammy/noble-specific dependencies) + // For ROS-O (one): also add ROS 2 repository as it depends on ROS 2 packages + const addRos1Repo = needsRos1 || ubuntuCodename === ros1UbuntuVersion; + const addRos2Repo = needsRos2 || + needsRosOne || + (requiredDistros.length === 0 && ubuntuCodename !== ros1UbuntuVersion); + yield addAptRepo(ubuntuCodename, use_ros2_testing, addRos1Repo, addRos2Repo); + // Add ROS-O repository if needed + if (needsRosOne) { + yield addRosOneAptRepoKey(); + yield addRosOneAptRepo(ubuntuCodename, use_ros2_testing); + } if ("noble" !== ubuntuCodename) { // Temporary fix to avoid error mount: /var/lib/grub/esp: special device (...) does not exist. const arch = yield utils.getArch(); @@ -7244,7 +7325,11 @@ function runLinux() { yield pip.installPython3Dependencies(); } yield rosdepInit(); - for (const rosDistro of utils.getRequiredRosDistributions()) { + // Configure rosdep for ROS-O if needed + if (needsRosOne) { + yield configureRosOneRosdep(); + } + for (const rosDistro of requiredDistros) { yield apt.runAptGetInstall([`ros-${rosDistro}-desktop`]); } }); @@ -7570,6 +7655,7 @@ function getRequiredRosDistributions() { //list of valid linux distributions const validDistro = [ "noetic", + "one", "humble", "iron", "jazzy", diff --git a/src/package_manager/pip.ts b/src/package_manager/pip.ts index a19afd48c..42e483b27 100644 --- a/src/package_manager/pip.ts +++ b/src/package_manager/pip.ts @@ -27,6 +27,7 @@ const pip3Packages: string[] = [ "colcon-recursive-crawl==0.2.1", "colcon-ros==0.3.23", "colcon-test-result==0.3.8", + "meson>=0.60.0,<0.64.0", "coverage", "cryptography", "empy<4", diff --git a/src/setup-ros-ubuntu.ts b/src/setup-ros-ubuntu.ts index 23566f164..1f328d599 100644 --- a/src/setup-ros-ubuntu.ts +++ b/src/setup-ros-ubuntu.ts @@ -83,7 +83,13 @@ async function configOs(): Promise { await utils.exec("sudo", ["apt-get", "update"]); // Install tools required to configure the worker system. - await apt.runAptGetInstall(["curl", "gnupg2", "locales", "lsb-release"]); + await apt.runAptGetInstall([ + "ca-certificates", + "curl", + "gnupg2", + "locales", + "lsb-release", + ]); // Select a locale supporting Unicode. await utils.exec("sudo", ["locale-gen", "en_US", "en_US.UTF-8"]); @@ -112,6 +118,22 @@ async function addAptRepoKey(): Promise { await utils.exec("sudo", ["apt-key", "add", keyFilePath]); } +/** + * Add ROS-O (ROS One) APT repository key. + * + * Downloads and installs the GPG key for the ROS-O repository. + */ +async function addRosOneAptRepoKey(): Promise { + // Ensure the keyrings directory exists and ca-certificates is up to date + await utils.exec("sudo", ["mkdir", "-p", "/etc/apt/keyrings"]); + await utils.exec("sudo", ["update-ca-certificates"]); + await utils.exec("sudo", [ + "bash", + "-c", + "curl -sSL https://ros.packages.techfak.net/gpg.key -o /etc/apt/keyrings/ros-one-keyring.gpg", + ]); +} + // Ubuntu distribution for ROS 1 const ros1UbuntuVersion = "focal"; @@ -119,19 +141,26 @@ const ros1UbuntuVersion = "focal"; * Add OSRF APT repository. * * @param ubuntuCodename the Ubuntu version codename + * @param needsRos1 whether ROS 1 packages are needed + * @param needsRos2 whether ROS 2 packages are needed */ async function addAptRepo( ubuntuCodename: string, use_ros2_testing: boolean, + needsRos1: boolean, + needsRos2: boolean, ): Promise { - // There is now no Ubuntu version overlap between ROS 1 and ROS 2 - if (ros1UbuntuVersion === ubuntuCodename) { + // Add ROS 1 repository if needed + if (needsRos1) { await utils.exec("sudo", [ "bash", "-c", `echo "deb http://packages.ros.org/ros/ubuntu ${ubuntuCodename} main" > /etc/apt/sources.list.d/ros-latest.list`, ]); - } else { + } + + // Add ROS 2 repository if needed + if (needsRos2) { await utils.exec("sudo", [ "bash", "-c", @@ -144,6 +173,26 @@ async function addAptRepo( await utils.exec("sudo", ["apt-get", "update"]); } +/** + * Add ROS-O (ROS One) APT repository. + * + * @param ubuntuCodename the Ubuntu version codename + * @param use_testing whether to use the testing repository + */ +async function addRosOneAptRepo( + ubuntuCodename: string, + use_testing: boolean, +): Promise { + const arch = await utils.getArch(); + const repo = use_testing ? `${ubuntuCodename}-testing` : ubuntuCodename; + await utils.exec("sudo", [ + "bash", + "-c", + `echo "deb [arch=${arch} signed-by=/etc/apt/keyrings/ros-one-keyring.gpg] https://ros.packages.techfak.net ${repo} main" > /etc/apt/sources.list.d/ros-one.list`, + ]); + await utils.exec("sudo", ["apt-get", "update"]); +} + /** * Initialize rosdep. */ @@ -160,6 +209,19 @@ async function rosdepInit(): Promise { await utils.exec("sudo", ["rosdep", "init"]); } +/** + * Configure rosdep for ROS-O (ROS One). + * + * Adds custom rosdep source for ROS-O packages. + */ +async function configureRosOneRosdep(): Promise { + await utils.exec("sudo", [ + "bash", + "-c", + 'echo "yaml https://ros.packages.techfak.net/ros-one.yaml one" > /etc/ros/rosdep/sources.list.d/1-ros-one.list', + ]); +} + /** * Install ROS 1 or 2 (development packages and/or ROS binaries) on a Linux worker. */ @@ -168,12 +230,42 @@ export async function runLinux(): Promise { const use_ros2_testing = core.getInput("use-ros2-testing") === "true"; const installConnext = core.getInput("install-connext") === "true"; + const requiredDistros = utils.getRequiredRosDistributions(); + const needsRosOne = requiredDistros.includes("one"); + + // Determine which ROS versions are needed + // ROS 1 distributions: noetic (from packages.ros.org/ros) + // ROS 2 distributions: rolling, humble, jazzy, iron, kilted, etc. (from packages.ros.org/ros2) + // ROS-O "one": separate repository (ros.packages.techfak.net) + const ros1Distros = ["noetic"]; + const needsRos1 = requiredDistros.some((distro) => + ros1Distros.includes(distro), + ); + const needsRos2 = requiredDistros.some( + (distro) => !ros1Distros.includes(distro) && distro !== "one", + ); + await configOs(); await addAptRepoKey(); const ubuntuCodename = await utils.determineDistribCodename(); - await addAptRepo(ubuntuCodename, use_ros2_testing); + // For backward compatibility when no ROS distributions are specified: + // - Focal (Ubuntu 20.04): add ROS 1 repository (for focal-specific dependencies) + // - Other versions: add ROS 2 repository (for jammy/noble-specific dependencies) + // For ROS-O (one): also add ROS 2 repository as it depends on ROS 2 packages + const addRos1Repo = needsRos1 || ubuntuCodename === ros1UbuntuVersion; + const addRos2Repo = + needsRos2 || + needsRosOne || + (requiredDistros.length === 0 && ubuntuCodename !== ros1UbuntuVersion); + await addAptRepo(ubuntuCodename, use_ros2_testing, addRos1Repo, addRos2Repo); + + // Add ROS-O repository if needed + if (needsRosOne) { + await addRosOneAptRepoKey(); + await addRosOneAptRepo(ubuntuCodename, use_ros2_testing); + } if ("noble" !== ubuntuCodename) { // Temporary fix to avoid error mount: /var/lib/grub/esp: special device (...) does not exist. @@ -195,7 +287,12 @@ export async function runLinux(): Promise { await rosdepInit(); - for (const rosDistro of utils.getRequiredRosDistributions()) { + // Configure rosdep for ROS-O if needed + if (needsRosOne) { + await configureRosOneRosdep(); + } + + for (const rosDistro of requiredDistros) { await apt.runAptGetInstall([`ros-${rosDistro}-desktop`]); } } diff --git a/src/utils.ts b/src/utils.ts index 27bd0d8cd..5442e4a0f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -43,6 +43,7 @@ export function getRequiredRosDistributions(): string[] { //list of valid linux distributions const validDistro: string[] = [ "noetic", + "one", "humble", "iron", "jazzy", From 97d7cc2aab375295b453ebb339048aa5584fd30f Mon Sep 17 00:00:00 2001 From: Iori Yanokura Date: Sun, 28 Jun 2026 19:01:15 +0900 Subject: [PATCH 2/2] Document separate APT repo for ROS-O Signed-off-by: Iori Yanokura --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 23b599baf..667b0b943 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,9 @@ It also performs the following actions: - Setting the locale to `en_US.UTF-8` and, the timezone to UTC - GCC and clang default APT packages - Registering the Open Robotics APT repository + - For ROS-O (`one`), packages are installed from a separate APT repository + (`ros.packages.techfak.net`) maintained by the ROS-O community at CITEC + Bielefeld, not from the Open Robotics APT repository. - Installing ROS and ROS 2 system dependencies using APT - On macOS: - Installing ROS and ROS 2 system dependencies using [Homebrew] and [pip]