想在机器人领域使用一下龙芯,但目前ROS2官方并未发预构建二进制包
调研时发现了loongros2 ,证明源码直接或小改后在龙芯上运行没有太大问题,遂决定开坑移植
2025.11.7更新:得益于AOSC的构建系统,也幸好ros2源码中没有太多架构特定的代码,ROS Jazzy desktop除Mimick相关的所有包在amd64/arm64/loongarch64/riscv64/ppc64el/mips均构建通过(但还未测试,估计也不会有人在ppc和mips这些平台上用)
准备工作
colcon是一个用于构建软件包集的命令行工具,大家都用它来编译ROS2项目。因此虽然它不属于ROS的一部分,还是先把它打包出来
colcon是完全由Python写的,不涉及到编译,也没有架构问题。虽然包的数量不少(Ubuntu的python3-colcon加上推荐依赖是二十多个),但构建也不麻烦,直接手动写一下再复制复制粘贴几次
相关PR:https://github.com/AOSC-Dev/aosc-os-abbs/pull/12771
flake8是一个代码检查工具,虽然没有直接用到,但ROS里很多Python包都依赖它,因此也要打包一下
相关PR:https://github.com/AOSC-Dev/aosc-os-abbs/pull/12817
其他包
主要涉及到mypy、lttng等一大堆东西
相关PR:https://github.com/AOSC-Dev/aosc-os-abbs/pull/12823
2025.11.7更新:由于社区反馈建议分开,就拆成了多个PR:
ROS构建目标
在所有的ROS版本中 ,现在还在生命周期中的正式版有三个。其中Kilted不是LTS版本,而Humble比较老,依赖版本比较低,因此选择最新的LTS版本Jazzy。查看文档中对依赖库版本的需求 均能满足,决定将目标distribution定为Jazzt Jalisco
REP 2001 中规定了每个版本的不同变种,Jazzy有六个,其中最低的是ROS Core,其次是ROS Base
我暂时将目标Variant定为base,如进展顺利再尝试打包更多包以达到desktop
2025.11.7更新:打包base时没有遇到太多问题,然后再花了一些时间打出了dekstop
编写脚本生成打包脚本
因为ROS的包数量大、相似度高,手工编写打包脚本远不如用脚本批量生成
个人比较熟悉Python,就用它来写了
编写软件包的类
参照AOSC的指南 ,编写一个Python类
1 2 3 4 5 6 7 8 class Package : version: str src: str name: str dependencies: list [str ] build_dependencies: list [str ] description: str autobuild_type: str
获取包列表
ros官方提供了一个工具rosinstall-generator,可以生成包源码的结构化数据,我们可通过提供的文档 来了解它的用法
使用pip安装后,尝试获取一份包含依赖项的core级别所有包的源码
1 rosinstall_generator ros_core --rosdistro jazzy --deps
使用python解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import subprocessimport sysfrom typing import Literal import yamlraw_package_type = dict [ Literal ["git" ], dict [Literal ["local-name" , "uri" , "version" ], str ] ] try : rosinstall_generator_output = subprocess.run( ["rosinstall_generator" , "ros_base" , "--rosdistro" , "jazzy" , "--deps" ], capture_output=True , check=True , ) raw_packages_str = rosinstall_generator_output.stdout raw_packages_data: raw_package_type = yaml.safe_load(raw_packages_str) except subprocess.CalledProcessError as e: print (f"命令执行失败: {e.returncode} " , file=sys.stderr) print (e.stderr) except FileNotFoundError: print (f"找不到命令,rosinstall-generator 未安装" , file=sys.stderr) except yaml.YAMLError as e: print (f"返回值解析错误: {e} " , file=sys.stderr)
注意rosinstall 和rosinstall-generator 是两个不同的包
解析包信息
版本
读取到信息中的版本号均为x.y.z-revision的格式
按照AOSC的规定 :
情形:版本号带有短横线(-)
应当采取的措施:将短横线替换为加号(+)
举例:ImageMagick 6.9.10-23 -> VER=6.9.10+23
将其进行字符串替换,改为x.y.z+revision
源码
rosinstall_generator输出的均为git仓库,因此只需要做简单拼接"git::commit=tags/{version}::{uri}"
但由于已经单独提出了VER变量,在SRC中再硬编码版本号就显得不太合适,所以稍作修改
1 2 3 4 semver = version.split("/" )[-1 ] self.version = semver.replace("-" , "+" ) version_prefix = "/" .join(version.split("/" )[:-1 ]) self.src = f"git::commit=tags/{version_prefix} /{'${VER/+/-}' } ::{uri} "
这样处理后,例如ament_package这个包的生成内容就是
1 2 VER=0.16.4+1 SRCS="git::commit=tags/release/jazzy/ament_package/${VER/+/-} ::https://github.com/ros2-gbp/ament_package-release.git"
更新检查
样式指南要求尽量使用anitya,但要把如此数量庞大的包都加到anitya过于麻烦,就选了使用GitHub格式
1 2 3 repo = uri[19 :-4 ] pattern = "/" .join(version.split("/" )[:-1 ]) + "/.*" check_update = f"github::repo={repo} ;pattern={pattern} "
包名
观察发现,rosinstall_generator输出的local-name都是包所属“大包”名/包名的格式,且包名一段是由下划线分割的。参考官方打包的deb包名,可推测出 最终包名的格式
1 2 original_name = local_name.split("/" )[-1 ] package_name = "ros-jazzy-" + original_name.replace("_" , "-" )
由于包名是推测的,没有找到相关文档,还需要进行验证
验证过程
利用ROS官方的apt仓库,通过检查目录是否存在来判断
1 2 3 4 5 6 7 8 9 10 11 12 import requestsfrom tqdm import tqdmfor package in tqdm(raw_packages_data): local_name = package["git" ]["local-name" ] original_name = local_name.split("/" )[-1 ] package_name = "ros-jazzy-" + original_name.replace("_" , "-" ) resp = requests.get( "http://packages.ros.org/ros2/ubuntu/pool/main/r/" + package_name ) if resp.status_code != 200 : print (local_name)
结果为全部通过,证实了我的猜测
依赖
这是非常重要又很难处理的一部分,每个包都有一个package.xml文件,里面记录了包的依赖情况
依赖分为两种,编译时依赖和构建时依赖,用<build_depend>(编译依赖库)/<buildtool_depend>(编译时需要的编译工具)和<depend>(构建和运行都用)/<exec_depend>(只有运行用)表示
这些依赖有都有两个来源,其他的ros包和非ros的第三方库。ros包的命名方式很简单,通过前述生成包名的方式可以很容易找到;官方获取第三方库在包管理中的包名的实现方式是维护一个大的映射表 ,我们使用其中的Arch作为基准,在此基础上手动进行修改:
AOSC中的包通常不会以-dev或者-devel结尾,因此遇到该后缀就去掉
AOSC打包的Python库不以python3-开头,因此遇到该前缀就去掉
上面两条中有个特例是Ubuntu中的包python3-dev,匹配上两条后啥都不剩了,所以要放在最前面做特殊处理
AOSC打包的包名中若原来不含lib前缀的,不会加上该前缀,但本来就有的则会保留;若对比同一包在多个系统中的包名,有些带lib前缀而有些没有,就可以大胆猜测它不是原项目名的一部分,将它去除
这里用一个简单粗暴(但不一定准确)的方法来区分:含有下划线_的认为是ros包,带有横杠-的是第三方库。 两种来源的包可以通过映射表中是否存在来判断
基于以上分析内容,写出python代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import requestsimport yamlrosdep_map: dict [str , dict ] = yaml.safe_load( requests.get( "https://github.com/ros/rosdistro/raw/refs/heads/master/rosdep/base.yaml" ).text ) def process_rosdep_package_list ( rosdep_package_list: list [str ] | dict [str , list [str ]], ) -> str : system_package_name = "" if isinstance (rosdep_package_list, list ): system_package_name = rosdep_package_list[0 ] else : for system in rosdep_package_list.keys(): if rosdep_package_list[system]: system_package_name = rosdep_package_list[system][0 ] assert system_package_name return system_package_name def rosdep_key_to_package_name (rosdep_key: str ) -> str : package_name = "" if rosdep_key in rosdep_map.keys(): current_package = rosdep_map[rosdep_key] if "arch" in current_package: package_name = process_rosdep_package_list(current_package["arch" ]) elif "ubuntu" in current_package: package_name = process_rosdep_package_list(current_package["ubuntu" ]) else : package_name = process_rosdep_package_list( current_package[list (current_package.keys())[0 ]] ) else : original_name = rosdep_key.split("/" )[-1 ] package_name = f"ros-{rosdistro} -" + original_name.replace("_" , "-" ) if package_name == "python3-dev" or package_name == "python3" : package_name = "python-3" if package_name.startswith(f"ros-{rosdistro} -python3-" ): package_name = package_name[13 + len (rosdistro) :] if package_name.startswith("python3-" ): package_name = package_name[8 :] if package_name.endswith("-dev" ): package_name = package_name[:-4 ] return package_name
这里还要注意有个特殊的包ros-jazzy-ros-workspace,负责提供/opt/ros/jazzy/setup.{sh,bash,zsh}等脚本,它是除了本身及其依赖外所有包的依赖项
描述
package.xml中有<description>字段包含了对包的描述;但它可能有多行,因此只取第一行
有个别包中的描述中含有反斜杠,需要注意转义
构建方式
绝大多数包都是使用CMake编译或不需要编译的Python包,我选择根据是否存在CMakeLists.txt和setup.py判断;如遇到特殊情况手动处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 repo = uri[19 :-4 ] file_list = requests.get(f"https://api.github.com/repos/{repo} /contents?ref={version} " ).json() has_pyproject = False has_setup_py = False has_cmake_lists = False for file in file_list: if file["name" ] == "pyproject.toml" : has_pyproject = True if file["name" ] == "setup.py" : has_setup_py = True if file["name" ] == "CMakeLists.txt" : has_cmake_lists = True if has_cmake_lists: self.build_type = "cmakeninja" elif has_pyproject: self.build_type = "pep517" elif has_setup_py: self.build_type = "python" else : self.build_type = "unknown"
若包为Python写的,还要加上NOPYTHON2和noarch声明
生成ACBS文件
通过以上步骤,整合代码,写出完整程序
展开程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 import osimport subprocessimport sysimport xml.etree.ElementTree as ETfrom concurrent.futures import ThreadPoolExecutorfrom typing import Literal import requestsimport yamlfrom tqdm import tqdmrosdistro = "jazzy" print ("Fetching rosdep map..." )rosdep_map: dict [str , dict ] = yaml.safe_load( requests.get( "https://ghfast.top/https://github.com/ros/rosdistro/raw/refs/heads/master/rosdep/base.yaml" ).text ) def process_rosdep_package_list ( rosdep_package_list: list [str ] | dict [str , list [str ]], ) -> str : system_package_name = "" if isinstance (rosdep_package_list, list ): system_package_name = rosdep_package_list[0 ] else : for system in rosdep_package_list.keys(): if rosdep_package_list[system]: system_package_name = rosdep_package_list[system][0 ] assert system_package_name return system_package_name def rosdep_key_to_package_name (rosdep_key: str ) -> str : package_name = "" if rosdep_key in rosdep_map.keys(): current_package = rosdep_map[rosdep_key] if "arch" in current_package: package_name = process_rosdep_package_list(current_package["arch" ]) elif "ubuntu" in current_package: package_name = process_rosdep_package_list(current_package["ubuntu" ]) else : package_name = process_rosdep_package_list( current_package[list (current_package.keys())[0 ]] ) else : original_name = rosdep_key.split("/" )[-1 ] package_name = f"ros-{rosdistro} -" + original_name.replace("_" , "-" ) if package_name == "python3-dev" or package_name == "python3" : package_name = "python-3" if package_name.startswith(f"ros-{rosdistro} -python3-" ): package_name = package_name[13 + len (rosdistro) :] if package_name.startswith("python3-" ): package_name = package_name[8 :] if package_name.endswith("-dev" ): package_name = package_name[:-4 ] return package_name class Package : version: str src: str check_update: str name: str dependencies: list [str ] build_dependencies: list [str ] description: str autobuild_type: str file_list_url: str package_xml_url: str package_info: ET.Element build_type: str def __init__ (self, local_name: str , uri: str , version: str ): semver = version.split("/" )[-1 ] self.version = semver.replace("-" , "+" ) version_prefix = "/" .join(version.split("/" )[:-1 ]) self.src = f"git::commit=tags/{version_prefix} /{'${VER/+/-}' } ::{uri} " repo = uri[19 :-4 ] pattern = "/" .join(version.split("/" )[:-1 ]) + "/.*" self.check_update = f"github::repo={repo} ;pattern={pattern} " self.name = rosdep_key_to_package_name(local_name) self.file_list_url = ( f"https://api.github.com/repos/{repo} /contents?ref={version} " ) self.package_xml_url = ( f"https://ghfast.top/{uri[:-4 ]} /raw/refs/tags/{version} /package.xml" ) self.dependencies = [f"ros-{rosdistro} -ros-workspace" ] self.build_dependencies = [] def add_builddep (self, rosdep_key ): package_name = rosdep_key_to_package_name(rosdep_key) if ( package_name not in self.dependencies and package_name not in self.build_dependencies ): self.build_dependencies.append(package_name) def get_metadata (self ): assert self.package_xml_url package_xml_string = requests.get(self.package_xml_url).text packge_info = ET.fromstring(package_xml_string) for elment in packge_info.findall("depend" ): self.dependencies.append(rosdep_key_to_package_name(elment.text)) for elment in packge_info.findall("exec_depend" ): self.dependencies.append(rosdep_key_to_package_name(elment.text)) for elment in packge_info.findall("build_export_depend" ): self.dependencies.append(rosdep_key_to_package_name(elment.text)) for elment in packge_info.findall("buildtool_export_depend" ): self.dependencies.append(elment.text) for elment in packge_info.findall("buildtool_depend" ): self.add_builddep(elment.text) for elment in packge_info.findall("build_depend" ): self.add_builddep(elment.text) for elment in packge_info.findall("test_depend" ): self.add_builddep(elment.text) raw_description = packge_info.findall("description" )[0 ].text.splitlines() self.description = self.name for line in raw_description: clean_line = line.strip() if clean_line: self.description = clean_line break file_list = requests.get(self.file_list_url).json() has_pyproject = False has_setup_py = False has_cmake_lists = False for file in file_list: if file["name" ] == "pyproject.toml" : has_pyproject = True if file["name" ] == "setup.py" : has_setup_py = True if file["name" ] == "CMakeLists.txt" : has_cmake_lists = True if has_cmake_lists: self.build_type = "cmakeninja" elif has_pyproject: self.build_type = "pep517" elif has_setup_py: self.build_type = "python" else : self.build_type = "unknown" def generate_spec (self ): return f"""VER={self.version} SRCS="{self.src} " CHKSUMS="SKIP" CHKUPDATE="{self.check_update} " """ def generate_defines (self ): defines_content = f"""PKGNAME={self.name} PKGSEC=ros PKGDEP="{" " .join(self.dependencies)} " """ if self.build_dependencies: defines_content += f'BUILDDEP="{" " .join(self.build_dependencies)} "' defines_content += f""" PKGDES="{self.description} " ABTYPE={self.build_type} PREFIX="/opt/ros/{rosdistro} " ABSPLITDBG=0 """ if self.build_type == "pep517" or self.build_type == "python" : defines_content += """NOPYTHON2=1 ABHOST=noarch """ if self.build_type == "cmakeninja" : defines_content += f"""CMAKE_AFTER=( "-DCMAKE_PREFIX_PATH=/opt/ros/jazzy" "-DBUILD_TESTING=OFF" ) """ defines_content += "NOSTATIC=0\n" return defines_content def write_file (new_package: Package ): dirname = f"runtime-ros/{new_package.name} " if not os.path.exists(dirname): try : new_package.get_metadata() except TypeError: print ("API rate limit exceeded" ) return except requests.exceptions.SSLError: print ("API rate limit exceeded" ) return except requests.exceptions.ConnectionError: print ("API rate limit exceeded" ) return spec = new_package.generate_spec() defines = new_package.generate_defines() prepare = f"""export PYTHONPATH=/opt/ros/{rosdistro} /lib/python3.10/site-packages/ export PKG_CONFIG_PATH=/opt/ros/jazzy/lib/pkgconfig . /opt/ros/jazzy/setup.sh """ os.mkdir(dirname) os.mkdir(dirname + "/autobuild" ) open (dirname + "/spec" , "w" , encoding="utf-8" ).write(spec) open (dirname + "/autobuild/defines" , "w" , encoding="utf-8" ).write(defines) open (dirname + "/autobuild/prepare" , "w" , encoding="utf-8" ).write(prepare) print ("Scanning packages ..." )raw_package_type = dict [ Literal ["git" ], dict [Literal ["local-name" , "uri" , "version" ], str ] ] try : rosinstall_generator_output = subprocess.run( ["rosinstall_generator" , "desktop" , "--rosdistro" , rosdistro, "--deps" ], capture_output=True , check=True , ) raw_packages_str = rosinstall_generator_output.stdout raw_packages_data: raw_package_type = yaml.safe_load(raw_packages_str) except subprocess.CalledProcessError as e: print (f"命令执行失败: {e.returncode} " , file=sys.stderr) print (e.stderr) except FileNotFoundError: print (f"找不到命令,rosinstall-generator 未安装" , file=sys.stderr) except yaml.YAMLError as e: print (f"返回值解析错误: {e} " , file=sys.stderr) print ("Generating files..." )futures = [] if not os.path.exists("runtime-ros" ): os.mkdir("runtime-ros" ) with ThreadPoolExecutor(max_workers=10 ) as executor: for package in tqdm(raw_packages_data): current_data = package["git" ] current_package = Package( current_data["local-name" ], current_data["uri" ], current_data["version" ] ) future = executor.submit(write_file, current_package) futures.append(future) for future in tqdm(futures): future.result()
手动处理依赖
虽然大多数依赖都使用脚本自动化完成了,还有一些依赖包名需要手动处理,例如eigen3-dev和eigen-3等包名不一致的,还有importlib-*等属于另一个包的一部分的情况,以及ros-jazzy-ros-workspace会产生循环依赖等等
这部分工作量不大且无法自动化,需要人工手动完成
处理架构不兼容的包
打包过程中遇到了一个特殊的包Mimick,它用于C的测试
它使用了一些汇编代码 ,而且只有x86和arm,甚至添加RV支持的PR 几年了也没有被上游合并。即使不考虑上游,学会写这么多架构的汇编也不是一件简单的事。所幸它只是一个用于测试的mock库,可以直接在不支持的架构上不依赖它并设置-DBUILD_TESTING=OFF,顺利解决