From d47b718cb0a56f3370af5db8137d659064e1eeb1 Mon Sep 17 00:00:00 2001 From: Christophe Bedard Date: Sun, 9 Jul 2023 14:53:34 -0700 Subject: [PATCH] Prefer system packages over pip packages for Linux Signed-off-by: Christophe Bedard --- dist/index.js | 261 +++++++++++++++++++++++++------------ src/package_manager/apt.ts | 51 +++++--- src/package_manager/pip.ts | 13 +- src/setup-ros-linux.ts | 113 ++++++++++------ 4 files changed, 294 insertions(+), 144 deletions(-) diff --git a/dist/index.js b/dist/index.js index cf468e1bb..633ceeb90 100644 --- a/dist/index.js +++ b/dist/index.js @@ -4567,8 +4567,11 @@ var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || // Max safe segment length for coercion. var MAX_SAFE_COMPONENT_LENGTH = 16 +var MAX_SAFE_BUILD_LENGTH = MAX_LENGTH - 6 + // The actual regexps go on exports.re var re = exports.re = [] +var safeRe = exports.safeRe = [] var src = exports.src = [] var t = exports.tokens = {} var R = 0 @@ -4577,6 +4580,31 @@ function tok (n) { t[n] = R++ } +var LETTERDASHNUMBER = '[a-zA-Z0-9-]' + +// Replace some greedy regex tokens to prevent regex dos issues. These regex are +// used internally via the safeRe object since all inputs in this library get +// normalized first to trim and collapse all extra whitespace. The original +// regexes are exported for userland consumption and lower level usage. A +// future breaking change could export the safer regex only with a note that +// all input should have extra whitespace removed. +var safeRegexReplacements = [ + ['\\s', 1], + ['\\d', MAX_LENGTH], + [LETTERDASHNUMBER, MAX_SAFE_BUILD_LENGTH], +] + +function makeSafeRe (value) { + for (var i = 0; i < safeRegexReplacements.length; i++) { + var token = safeRegexReplacements[i][0] + var max = safeRegexReplacements[i][1] + value = value + .split(token + '*').join(token + '{0,' + max + '}') + .split(token + '+').join(token + '{1,' + max + '}') + } + return value +} + // The following Regular Expressions can be used for tokenizing, // validating, and parsing SemVer version strings. @@ -4586,14 +4614,14 @@ function tok (n) { tok('NUMERICIDENTIFIER') src[t.NUMERICIDENTIFIER] = '0|[1-9]\\d*' tok('NUMERICIDENTIFIERLOOSE') -src[t.NUMERICIDENTIFIERLOOSE] = '[0-9]+' +src[t.NUMERICIDENTIFIERLOOSE] = '\\d+' // ## Non-numeric Identifier // Zero or more digits, followed by a letter or hyphen, and then zero or // more letters, digits, or hyphens. tok('NONNUMERICIDENTIFIER') -src[t.NONNUMERICIDENTIFIER] = '\\d*[a-zA-Z-][a-zA-Z0-9-]*' +src[t.NONNUMERICIDENTIFIER] = '\\d*[a-zA-Z-]' + LETTERDASHNUMBER + '*' // ## Main Version // Three dot-separated numeric identifiers. @@ -4635,7 +4663,7 @@ src[t.PRERELEASELOOSE] = '(?:-?(' + src[t.PRERELEASEIDENTIFIERLOOSE] + // Any combination of digits, letters, or hyphens. tok('BUILDIDENTIFIER') -src[t.BUILDIDENTIFIER] = '[0-9A-Za-z-]+' +src[t.BUILDIDENTIFIER] = LETTERDASHNUMBER + '+' // ## Build Metadata // Plus sign, followed by one or more period-separated build metadata @@ -4715,6 +4743,7 @@ src[t.COERCE] = '(^|[^\\d])' + '(?:$|[^\\d])' tok('COERCERTL') re[t.COERCERTL] = new RegExp(src[t.COERCE], 'g') +safeRe[t.COERCERTL] = new RegExp(makeSafeRe(src[t.COERCE]), 'g') // Tilde ranges. // Meaning is "reasonably at or greater than" @@ -4724,6 +4753,7 @@ src[t.LONETILDE] = '(?:~>?)' tok('TILDETRIM') src[t.TILDETRIM] = '(\\s*)' + src[t.LONETILDE] + '\\s+' re[t.TILDETRIM] = new RegExp(src[t.TILDETRIM], 'g') +safeRe[t.TILDETRIM] = new RegExp(makeSafeRe(src[t.TILDETRIM]), 'g') var tildeTrimReplace = '$1~' tok('TILDE') @@ -4739,6 +4769,7 @@ src[t.LONECARET] = '(?:\\^)' tok('CARETTRIM') src[t.CARETTRIM] = '(\\s*)' + src[t.LONECARET] + '\\s+' re[t.CARETTRIM] = new RegExp(src[t.CARETTRIM], 'g') +safeRe[t.CARETTRIM] = new RegExp(makeSafeRe(src[t.CARETTRIM]), 'g') var caretTrimReplace = '$1^' tok('CARET') @@ -4760,6 +4791,7 @@ src[t.COMPARATORTRIM] = '(\\s*)' + src[t.GTLT] + // this one has to use the /g flag re[t.COMPARATORTRIM] = new RegExp(src[t.COMPARATORTRIM], 'g') +safeRe[t.COMPARATORTRIM] = new RegExp(makeSafeRe(src[t.COMPARATORTRIM]), 'g') var comparatorTrimReplace = '$1$2$3' // Something like `1.2.3 - 1.2.4` @@ -4788,6 +4820,14 @@ for (var i = 0; i < R; i++) { debug(i, src[i]) if (!re[i]) { re[i] = new RegExp(src[i]) + + // Replace all greedy whitespace to prevent regex dos issues. These regex are + // used internally via the safeRe object since all inputs in this library get + // normalized first to trim and collapse all extra whitespace. The original + // regexes are exported for userland consumption and lower level usage. A + // future breaking change could export the safer regex only with a note that + // all input should have extra whitespace removed. + safeRe[i] = new RegExp(makeSafeRe(src[i])) } } @@ -4812,7 +4852,7 @@ function parse (version, options) { return null } - var r = options.loose ? re[t.LOOSE] : re[t.FULL] + var r = options.loose ? safeRe[t.LOOSE] : safeRe[t.FULL] if (!r.test(version)) { return null } @@ -4867,7 +4907,7 @@ function SemVer (version, options) { this.options = options this.loose = !!options.loose - var m = version.trim().match(options.loose ? re[t.LOOSE] : re[t.FULL]) + var m = version.trim().match(options.loose ? safeRe[t.LOOSE] : safeRe[t.FULL]) if (!m) { throw new TypeError('Invalid Version: ' + version) @@ -5312,6 +5352,7 @@ function Comparator (comp, options) { return new Comparator(comp, options) } + comp = comp.trim().split(/\s+/).join(' ') debug('comparator', comp, options) this.options = options this.loose = !!options.loose @@ -5328,7 +5369,7 @@ function Comparator (comp, options) { var ANY = {} Comparator.prototype.parse = function (comp) { - var r = this.options.loose ? re[t.COMPARATORLOOSE] : re[t.COMPARATOR] + var r = this.options.loose ? safeRe[t.COMPARATORLOOSE] : safeRe[t.COMPARATOR] var m = comp.match(r) if (!m) { @@ -5452,9 +5493,16 @@ function Range (range, options) { this.loose = !!options.loose this.includePrerelease = !!options.includePrerelease - // First, split based on boolean or || + // First reduce all whitespace as much as possible so we do not have to rely + // on potentially slow regexes like \s*. This is then stored and used for + // future error messages as well. this.raw = range - this.set = range.split(/\s*\|\|\s*/).map(function (range) { + .trim() + .split(/\s+/) + .join(' ') + + // First, split based on boolean or || + this.set = this.raw.split('||').map(function (range) { return this.parseRange(range.trim()) }, this).filter(function (c) { // throw out any that are not relevant for whatever reason @@ -5462,7 +5510,7 @@ function Range (range, options) { }) if (!this.set.length) { - throw new TypeError('Invalid SemVer Range: ' + range) + throw new TypeError('Invalid SemVer Range: ' + this.raw) } this.format() @@ -5481,20 +5529,19 @@ Range.prototype.toString = function () { Range.prototype.parseRange = function (range) { var loose = this.options.loose - range = range.trim() // `1.2.3 - 1.2.4` => `>=1.2.3 <=1.2.4` - var hr = loose ? re[t.HYPHENRANGELOOSE] : re[t.HYPHENRANGE] + var hr = loose ? safeRe[t.HYPHENRANGELOOSE] : safeRe[t.HYPHENRANGE] range = range.replace(hr, hyphenReplace) debug('hyphen replace', range) // `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5` - range = range.replace(re[t.COMPARATORTRIM], comparatorTrimReplace) - debug('comparator trim', range, re[t.COMPARATORTRIM]) + range = range.replace(safeRe[t.COMPARATORTRIM], comparatorTrimReplace) + debug('comparator trim', range, safeRe[t.COMPARATORTRIM]) // `~ 1.2.3` => `~1.2.3` - range = range.replace(re[t.TILDETRIM], tildeTrimReplace) + range = range.replace(safeRe[t.TILDETRIM], tildeTrimReplace) // `^ 1.2.3` => `^1.2.3` - range = range.replace(re[t.CARETTRIM], caretTrimReplace) + range = range.replace(safeRe[t.CARETTRIM], caretTrimReplace) // normalize spaces range = range.split(/\s+/).join(' ') @@ -5502,7 +5549,7 @@ Range.prototype.parseRange = function (range) { // At this point, the range is completely trimmed and // ready to be split into comparators. - var compRe = loose ? re[t.COMPARATORLOOSE] : re[t.COMPARATOR] + var compRe = loose ? safeRe[t.COMPARATORLOOSE] : safeRe[t.COMPARATOR] var set = range.split(' ').map(function (comp) { return parseComparator(comp, this.options) }, this).join(' ').split(/\s+/) @@ -5602,7 +5649,7 @@ function replaceTildes (comp, options) { } function replaceTilde (comp, options) { - var r = options.loose ? re[t.TILDELOOSE] : re[t.TILDE] + var r = options.loose ? safeRe[t.TILDELOOSE] : safeRe[t.TILDE] return comp.replace(r, function (_, M, m, p, pr) { debug('tilde', comp, _, M, m, p, pr) var ret @@ -5643,7 +5690,7 @@ function replaceCarets (comp, options) { function replaceCaret (comp, options) { debug('caret', comp, options) - var r = options.loose ? re[t.CARETLOOSE] : re[t.CARET] + var r = options.loose ? safeRe[t.CARETLOOSE] : safeRe[t.CARET] return comp.replace(r, function (_, M, m, p, pr) { debug('caret', comp, _, M, m, p, pr) var ret @@ -5702,7 +5749,7 @@ function replaceXRanges (comp, options) { function replaceXRange (comp, options) { comp = comp.trim() - var r = options.loose ? re[t.XRANGELOOSE] : re[t.XRANGE] + var r = options.loose ? safeRe[t.XRANGELOOSE] : safeRe[t.XRANGE] return comp.replace(r, function (ret, gtlt, M, m, p, pr) { debug('xRange', comp, ret, gtlt, M, m, p, pr) var xM = isX(M) @@ -5777,7 +5824,7 @@ function replaceXRange (comp, options) { function replaceStars (comp, options) { debug('replaceStars', comp, options) // Looseness is ignored here. star is always as loose as it gets! - return comp.trim().replace(re[t.STAR], '') + return comp.trim().replace(safeRe[t.STAR], '') } // This function is passed to string.replace(re[t.HYPHENRANGE]) @@ -6103,7 +6150,7 @@ function coerce (version, options) { var match = null if (!options.rtl) { - match = version.match(re[t.COERCE]) + match = version.match(safeRe[t.COERCE]) } else { // Find the right-most coercible string that does not share // a terminus with a more left-ward coercible string. @@ -6114,17 +6161,17 @@ function coerce (version, options) { // Stop when we get a match that ends at the string end, since no // coercible string can be more right-ward without the same terminus. var next - while ((next = re[t.COERCERTL].exec(version)) && + while ((next = safeRe[t.COERCERTL].exec(version)) && (!match || match.index + match[0].length !== version.length) ) { if (!match || next.index + next[0].length !== match.index + match[0].length) { match = next } - re[t.COERCERTL].lastIndex = next.index + next[1].length + next[2].length + safeRe[t.COERCERTL].lastIndex = next.index + next[1].length + next[2].length } // leave it in a clean state - re[t.COERCERTL].lastIndex = -1 + safeRe[t.COERCERTL].lastIndex = -1 } if (match === null) { @@ -6555,24 +6602,21 @@ const aptCommandLine = [ const aptDependencies = [ "libssl-dev", "python3-dev", - "build-essential", "clang", - "cmake", - "git", "lcov", - "python3-catkin-pkg-modules", - "python3-pip", "python3-rosinstall-generator", - "python3-vcstool", - "wget", - // FastRTPS dependencies - "libasio-dev", - "libtinyxml2-dev", ]; const distributionSpecificAptDependencies = { focal: [ + // Basic development packages + "build-essential", + "cmake", + "git", + "python3-pip", + "python3-catkin-pkg-modules", + "python3-vcstool", + "wget", // python-rosdep does not exist on Focal, so python3-rosdep is used. - // The issue with ros-melodic-desktop-full is also non-applicable. "python3-rosdep", // python required for sourcing setup.sh "python", @@ -6580,14 +6624,34 @@ const distributionSpecificAptDependencies = { "libc++abi-dev", ], jammy: [ - // python-rosdep does not exist on Jammy, so python3-rosdep is used. - // The issue with ros-melodic-desktop-full is also non-applicable. - "python3-rosdep", - // libc++-dev and libc++abi-dev installs intentionally removed because https://github.com/ros-tooling/setup-ros/issues/506 + // Basic development packages (from ROS 2 source/development setup instructions) + // ros-dev-tools includes many packages that we needed to include manually in Focal & older + "python3-flake8-docstrings", + "python3-pip", + "python3-pytest-cov", + "python3-flake8-blind-except", + "python3-flake8-builtins", + "python3-flake8-class-newline", + "python3-flake8-comprehensions", + "python3-flake8-deprecated", + "python3-flake8-import-order", + "python3-flake8-quotes", + "python3-pytest-repeat", + "python3-pytest-rerunfailures", + "ros-dev-tools", + // Additional colcon packages (not included in ros-dev-tools) + "python3-colcon-coveragepy-result", + "python3-colcon-lcov-result", + "python3-colcon-meson", + "python3-colcon-mixin", + // FastRTPS dependencies + "libasio-dev", + "libtinyxml2-dev", + // libc++-dev and libc++abi-dev installs intentionally removed because: + // https://github.com/ros-tooling/setup-ros/issues/506 ], }; const aptRtiConnextDds = { - focal: "rti-connext-dds-5.3.1", jammy: "rti-connext-dds-6.0.1", }; /** @@ -6957,15 +7021,14 @@ const pip3CommandLine = ["pip3", "install", "--upgrade"]; * @param run_with_sudo whether to prefix the command with sudo * @returns Promise exit code */ -function runPython3PipInstall(packages, run_with_sudo) { +function runPython3PipInstall(packages, run_with_sudo = true) { return __awaiter(this, void 0, void 0, function* () { - const sudo_enabled = run_with_sudo === undefined ? true : run_with_sudo; const args = pip3CommandLine.concat(packages); // Set CWD to root to avoid running 'pip install' in directory with setup.cfg file const options = { cwd: path.sep, }; - if (sudo_enabled) { + if (run_with_sudo) { return utils.exec("sudo", pip3CommandLine.concat(packages), options); } else { @@ -6980,9 +7043,12 @@ exports.runPython3PipInstall = runPython3PipInstall; * @param run_with_sudo whether to prefix the command with sudo * @returns Promise exit code */ -function installPython3Dependencies(run_with_sudo) { +function installPython3Dependencies(run_with_sudo = true, packages = pip3Packages) { return __awaiter(this, void 0, void 0, function* () { - return runPython3PipInstall(pip3Packages, run_with_sudo); + if (packages.length === 0) { + return 0; + } + return runPython3PipInstall(packages, run_with_sudo); }); } exports.installPython3Dependencies = installPython3Dependencies; @@ -7080,12 +7146,10 @@ WE+F5FaIKwb72PL4rLi4 =i0tj -----END PGP PUBLIC KEY BLOCK----- `; -// List of linux distributions that need http://packages.ros.org/ros/ubuntu APT repo -const distrosRequiringRosUbuntu = ["focal"]; /** - * Install ROS 2 on a Linux worker. + * Configure basic OS stuff. */ -function runLinux() { +function configOs() { return __awaiter(this, void 0, void 0, function* () { // When this action runs in a Docker image, sudo may be missing. // This installs sudo to avoid having to handle both cases (action runs as @@ -7103,9 +7167,6 @@ function runLinux() { "sudo", ]); } - // Get user input & validate - const use_ros2_testing = core.getInput("use-ros2-testing") === "true"; - const installConnext = core.getInput("install-connext") === "true"; 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. @@ -7122,52 +7183,90 @@ function runLinux() { "/etc/localtime", ]); yield apt.runAptGetInstall(["tzdata"]); - // OSRF APT repository is necessary, even when building - // from source to install colcon, vcs, etc. + }); +} +/** + * Add OSRF APT repository key. + * + * This is necessary even when building from source to install colcon, vcs, etc. + */ +function addAptRepoKey() { + return __awaiter(this, void 0, void 0, function* () { const workspace = process.env.GITHUB_WORKSPACE; const keyFilePath = path.join(workspace, "ros.key"); fs_1.default.writeFileSync(keyFilePath, openRoboticsAptPublicGpgKey); yield utils.exec("sudo", ["apt-key", "add", keyFilePath]); - const distribCodename = yield utils.determineDistribCodename(); - if (distrosRequiringRosUbuntu.includes(distribCodename)) { + }); +} +// Ubuntu distribution for ROS 1 +const ros1UbuntuVersion = "focal"; +/** + * Add OSRF APT repository. + * + * @param ubuntuCodename the Ubuntu version codename + */ +function addAptRepo(ubuntuCodename, use_ros2_testing) { + return __awaiter(this, void 0, void 0, function* () { + // There is now no Ubuntu version overlap between ROS 1 and ROS 2 + if (ros1UbuntuVersion === ubuntuCodename) { + 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 { yield utils.exec("sudo", [ "bash", "-c", - `echo "deb http://packages.ros.org/ros/ubuntu ${distribCodename} main" > /etc/apt/sources.list.d/ros-latest.list`, + `echo "deb http://packages.ros.org/ros2${use_ros2_testing ? "-testing" : ""}/ubuntu ${ubuntuCodename} main" > /etc/apt/sources.list.d/ros2-latest.list`, ]); } + yield utils.exec("sudo", ["apt-get", "update"]); + }); +} +/** + * Initialize rosdep. + */ +function rosdepInit() { + return __awaiter(this, void 0, void 0, function* () { + /** + * Try to remove the default file first in case this environment has already done a rosdep + * init before. + */ yield utils.exec("sudo", [ "bash", "-c", - `echo "deb http://packages.ros.org/ros2${use_ros2_testing ? "-testing" : ""}/ubuntu ${distribCodename} main" > /etc/apt/sources.list.d/ros2-latest.list`, + "rm /etc/ros/rosdep/sources.list.d/20-default.list || true", ]); - yield utils.exec("sudo", ["apt-get", "update"]); + yield utils.exec("sudo", ["rosdep", "init"]); + }); +} +/** + * Install ROS 1 or 2 (development packages and/or ROS binaries) on a Linux worker. + */ +function runLinux() { + return __awaiter(this, void 0, void 0, function* () { + // Get user input & validate + const use_ros2_testing = core.getInput("use-ros2-testing") === "true"; + const installConnext = core.getInput("install-connext") === "true"; + yield configOs(); + yield addAptRepoKey(); + const ubuntuCodename = yield utils.determineDistribCodename(); + yield addAptRepo(ubuntuCodename, use_ros2_testing); // Temporary fix to avoid error mount: /var/lib/grub/esp: special device (...) does not exist. yield utils.exec("sudo", ["apt-mark", "hold", "grub-efi-amd64-signed"]); yield utils.exec("sudo", ["apt-get", "upgrade", "-y"]); - // Install rosdep and vcs, as well as FastRTPS dependencies, OpenSplice, and - // optionally RTI Connext. - // vcs dependencies (e.g. git), as well as base building packages are not pulled by rosdep, so - // they are also installed during this stage. + // Install development-related packages and some common dependencies yield apt.installAptDependencies(installConnext); - /* Get the latest version of pip before installing dependencies, - the version from apt can be very out of date (v9.0 on bionic) - The latest version of pip doesn't support Python3.5 as of v21, - but pip 9 doesn't understand the metadata that states this, so we must first - make an intermediate upgrade to pip 20, which does understand that information */ - yield pip.runPython3PipInstall(["pip==20.*"]); - yield pip.runPython3PipInstall(["pip"]); - /* pip3 dependencies need to be installed after the APT ones, as pip3 - modules such as cryptography requires python-dev to be installed, - because they rely on Python C headers. */ - yield pip.installPython3Dependencies(); - // Initializes rosdep, trying to remove the default file first in case this environment has already done a rosdep init before - yield utils.exec("sudo", [ - "bash", - "-c", - "rm /etc/ros/rosdep/sources.list.d/20-default.list || true", - ]); - yield utils.exec("sudo", ["rosdep", "init"]); + // We don't use pip here to install dependencies for ROS 2 + if (ubuntuCodename === ros1UbuntuVersion) { + /* pip3 dependencies need to be installed after the APT ones, as pip3 + modules such as cryptography requires python-dev to be installed, + because they rely on Python C headers. */ + yield pip.installPython3Dependencies(); + } + yield rosdepInit(); for (const rosDistro of utils.getRequiredRosDistributions()) { yield apt.runAptGetInstall([`ros-${rosDistro}-desktop`]); } diff --git a/src/package_manager/apt.ts b/src/package_manager/apt.ts index 837b943d1..8ccc13fd2 100644 --- a/src/package_manager/apt.ts +++ b/src/package_manager/apt.ts @@ -13,25 +13,22 @@ const aptCommandLine: string[] = [ const aptDependencies: string[] = [ "libssl-dev", // required for pip3 cryptography module "python3-dev", // required for pip3 cryptography module - "build-essential", "clang", - "cmake", - "git", "lcov", - "python3-catkin-pkg-modules", - "python3-pip", "python3-rosinstall-generator", - "python3-vcstool", - "wget", - // FastRTPS dependencies - "libasio-dev", - "libtinyxml2-dev", ]; const distributionSpecificAptDependencies = { focal: [ + // Basic development packages + "build-essential", + "cmake", + "git", + "python3-pip", + "python3-catkin-pkg-modules", + "python3-vcstool", + "wget", // python-rosdep does not exist on Focal, so python3-rosdep is used. - // The issue with ros-melodic-desktop-full is also non-applicable. "python3-rosdep", // python required for sourcing setup.sh "python", @@ -39,15 +36,35 @@ const distributionSpecificAptDependencies = { "libc++abi-dev", ], jammy: [ - // python-rosdep does not exist on Jammy, so python3-rosdep is used. - // The issue with ros-melodic-desktop-full is also non-applicable. - "python3-rosdep", - // libc++-dev and libc++abi-dev installs intentionally removed because https://github.com/ros-tooling/setup-ros/issues/506 + // Basic development packages (from ROS 2 source/development setup instructions) + // ros-dev-tools includes many packages that we needed to include manually in Focal & older + "python3-flake8-docstrings", + "python3-pip", + "python3-pytest-cov", + "python3-flake8-blind-except", + "python3-flake8-builtins", + "python3-flake8-class-newline", + "python3-flake8-comprehensions", + "python3-flake8-deprecated", + "python3-flake8-import-order", + "python3-flake8-quotes", + "python3-pytest-repeat", + "python3-pytest-rerunfailures", + "ros-dev-tools", + // Additional colcon packages (not included in ros-dev-tools) + "python3-colcon-coveragepy-result", + "python3-colcon-lcov-result", + "python3-colcon-meson", + "python3-colcon-mixin", + // FastRTPS dependencies + "libasio-dev", + "libtinyxml2-dev", + // libc++-dev and libc++abi-dev installs intentionally removed because: + // https://github.com/ros-tooling/setup-ros/issues/506 ], }; const aptRtiConnextDds = { - focal: "rti-connext-dds-5.3.1", jammy: "rti-connext-dds-6.0.1", }; @@ -74,7 +91,7 @@ export async function runAptGetInstall(packages: string[]): Promise { * @returns Promise exit code */ export async function installAptDependencies( - installConnext = false + installConnext: boolean = false ): Promise { const distribCodename = await utils.determineDistribCodename(); let aptPackages: string[] = installConnext diff --git a/src/package_manager/pip.ts b/src/package_manager/pip.ts index 6cef9103d..a1192b08a 100644 --- a/src/package_manager/pip.ts +++ b/src/package_manager/pip.ts @@ -72,15 +72,14 @@ const pip3CommandLine: string[] = ["pip3", "install", "--upgrade"]; */ export async function runPython3PipInstall( packages: string[], - run_with_sudo?: boolean + run_with_sudo: boolean = true ): Promise { - const sudo_enabled = run_with_sudo === undefined ? true : run_with_sudo; const args = pip3CommandLine.concat(packages); // Set CWD to root to avoid running 'pip install' in directory with setup.cfg file const options: im.ExecOptions = { cwd: path.sep, }; - if (sudo_enabled) { + if (run_with_sudo) { return utils.exec("sudo", pip3CommandLine.concat(packages), options); } else { return utils.exec(args[0], args.splice(1), options); @@ -94,7 +93,11 @@ export async function runPython3PipInstall( * @returns Promise exit code */ export async function installPython3Dependencies( - run_with_sudo?: boolean + run_with_sudo: boolean = true, + packages: string[] = pip3Packages ): Promise { - return runPython3PipInstall(pip3Packages, run_with_sudo); + if (packages.length === 0) { + return 0; + } + return runPython3PipInstall(packages, run_with_sudo); } diff --git a/src/setup-ros-linux.ts b/src/setup-ros-linux.ts index 431bb6a97..8981ea718 100644 --- a/src/setup-ros-linux.ts +++ b/src/setup-ros-linux.ts @@ -50,13 +50,10 @@ WE+F5FaIKwb72PL4rLi4 -----END PGP PUBLIC KEY BLOCK----- `; -// List of linux distributions that need http://packages.ros.org/ros/ubuntu APT repo -const distrosRequiringRosUbuntu = ["focal"]; - /** - * Install ROS 2 on a Linux worker. + * Configure basic OS stuff. */ -export async function runLinux() { +async function configOs(): Promise { // When this action runs in a Docker image, sudo may be missing. // This installs sudo to avoid having to handle both cases (action runs as // root, action does not run as root) everywhere in the action. @@ -73,10 +70,6 @@ export async function runLinux() { ]); } - // Get user input & validate - const use_ros2_testing = core.getInput("use-ros2-testing") === "true"; - const installConnext = core.getInput("install-connext") === "true"; - await utils.exec("sudo", ["bash", "-c", "echo 'Etc/UTC' > /etc/timezone"]); await utils.exec("sudo", ["apt-get", "update"]); @@ -96,61 +89,99 @@ export async function runLinux() { "/etc/localtime", ]); await apt.runAptGetInstall(["tzdata"]); +} - // OSRF APT repository is necessary, even when building - // from source to install colcon, vcs, etc. +/** + * Add OSRF APT repository key. + * + * This is necessary even when building from source to install colcon, vcs, etc. + */ +async function addAptRepoKey(): Promise { const workspace = process.env.GITHUB_WORKSPACE as string; const keyFilePath = path.join(workspace, "ros.key"); fs.writeFileSync(keyFilePath, openRoboticsAptPublicGpgKey); await utils.exec("sudo", ["apt-key", "add", keyFilePath]); +} - const distribCodename = await utils.determineDistribCodename(); - if (distrosRequiringRosUbuntu.includes(distribCodename)) { +// Ubuntu distribution for ROS 1 +const ros1UbuntuVersion = "focal"; + +/** + * Add OSRF APT repository. + * + * @param ubuntuCodename the Ubuntu version codename + */ +async function addAptRepo( + ubuntuCodename: string, + use_ros2_testing: boolean +): Promise { + // There is now no Ubuntu version overlap between ROS 1 and ROS 2 + if (ros1UbuntuVersion === ubuntuCodename) { + 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 { await utils.exec("sudo", [ "bash", "-c", - `echo "deb http://packages.ros.org/ros/ubuntu ${distribCodename} main" > /etc/apt/sources.list.d/ros-latest.list`, + `echo "deb http://packages.ros.org/ros2${ + use_ros2_testing ? "-testing" : "" + }/ubuntu ${ubuntuCodename} main" > /etc/apt/sources.list.d/ros2-latest.list`, ]); } + + await utils.exec("sudo", ["apt-get", "update"]); +} + +/** + * Initialize rosdep. + */ +async function rosdepInit(): Promise { + /** + * Try to remove the default file first in case this environment has already done a rosdep + * init before. + */ await utils.exec("sudo", [ "bash", "-c", - `echo "deb http://packages.ros.org/ros2${ - use_ros2_testing ? "-testing" : "" - }/ubuntu ${distribCodename} main" > /etc/apt/sources.list.d/ros2-latest.list`, + "rm /etc/ros/rosdep/sources.list.d/20-default.list || true", ]); + await utils.exec("sudo", ["rosdep", "init"]); +} + +/** + * Install ROS 1 or 2 (development packages and/or ROS binaries) on a Linux worker. + */ +export async function runLinux(): Promise { + // Get user input & validate + const use_ros2_testing = core.getInput("use-ros2-testing") === "true"; + const installConnext = core.getInput("install-connext") === "true"; + + await configOs(); + + await addAptRepoKey(); + + const ubuntuCodename = await utils.determineDistribCodename(); + await addAptRepo(ubuntuCodename, use_ros2_testing); - await utils.exec("sudo", ["apt-get", "update"]); // Temporary fix to avoid error mount: /var/lib/grub/esp: special device (...) does not exist. await utils.exec("sudo", ["apt-mark", "hold", "grub-efi-amd64-signed"]); await utils.exec("sudo", ["apt-get", "upgrade", "-y"]); - // Install rosdep and vcs, as well as FastRTPS dependencies, OpenSplice, and - // optionally RTI Connext. - // vcs dependencies (e.g. git), as well as base building packages are not pulled by rosdep, so - // they are also installed during this stage. + // Install development-related packages and some common dependencies await apt.installAptDependencies(installConnext); - /* Get the latest version of pip before installing dependencies, - the version from apt can be very out of date (v9.0 on bionic) - The latest version of pip doesn't support Python3.5 as of v21, - but pip 9 doesn't understand the metadata that states this, so we must first - make an intermediate upgrade to pip 20, which does understand that information */ - await pip.runPython3PipInstall(["pip==20.*"]); - await pip.runPython3PipInstall(["pip"]); - - /* pip3 dependencies need to be installed after the APT ones, as pip3 - modules such as cryptography requires python-dev to be installed, - because they rely on Python C headers. */ - await pip.installPython3Dependencies(); + // We don't use pip here to install dependencies for ROS 2 + if (ubuntuCodename === ros1UbuntuVersion) { + /* pip3 dependencies need to be installed after the APT ones, as pip3 + modules such as cryptography requires python-dev to be installed, + because they rely on Python C headers. */ + await pip.installPython3Dependencies(); + } - // Initializes rosdep, trying to remove the default file first in case this environment has already done a rosdep init before - await utils.exec("sudo", [ - "bash", - "-c", - "rm /etc/ros/rosdep/sources.list.d/20-default.list || true", - ]); - await utils.exec("sudo", ["rosdep", "init"]); + await rosdepInit(); for (const rosDistro of utils.getRequiredRosDistributions()) { await apt.runAptGetInstall([`ros-${rosDistro}-desktop`]);