DRAFT lib/recipetool: Drop
Recipes are now generated by superflore.
This commit is contained in:
parent
62a4712929
commit
5b6aa54d82
|
@ -1,51 +0,0 @@
|
||||||
Extension to recipetool to enable automatic creation of
|
|
||||||
BitBake recipe files for ROS packages.
|
|
||||||
|
|
||||||
## REQUIREMENTS ##
|
|
||||||
|
|
||||||
Two changes to the recipetool are required to use this plugin:
|
|
||||||
|
|
||||||
Requires either:
|
|
||||||
From OE-Core rev: 1df60b09f7a60427795ec828c9c7180e4e52f98c
|
|
||||||
From OE-Core rev: 3bb979c13463705c4db6c59034661c4cd8100756
|
|
||||||
or
|
|
||||||
From poky rev: b1f237ebd0d4180c5d23a0ecd9aaf7193c63a48a
|
|
||||||
From poky rev: a7baa47c876c7895499909731aaa451c6009610a
|
|
||||||
|
|
||||||
These changes are currently in the master branch as of 2017-Aug-24.
|
|
||||||
|
|
||||||
|
|
||||||
## USAGE ##
|
|
||||||
|
|
||||||
Initialize the build environment:
|
|
||||||
|
|
||||||
source oe-init-build-env
|
|
||||||
|
|
||||||
Currently the plugin only allow for processing a single
|
|
||||||
ROS package in a repository. If a repository contains more than
|
|
||||||
one package, or the package is not in the root of the repository,
|
|
||||||
then use the `--src-subdir=<dir>` option.
|
|
||||||
|
|
||||||
ROS repositories generally do not use `master` as their default
|
|
||||||
branch, so be sure to include the correct branch for the desired
|
|
||||||
distribution as part of the URI: `<URI>;branch=indigo-devel`
|
|
||||||
|
|
||||||
## EXAMPLES ##
|
|
||||||
Build the Vector Nav package
|
|
||||||
```
|
|
||||||
devtool add https://github.com/dawonn/vectornav
|
|
||||||
```
|
|
||||||
|
|
||||||
Use the XBox Kinect Camera
|
|
||||||
```
|
|
||||||
devtool add --src-subdir=freenect_stack "https://github.com/ros-drivers/freenect_stack.git"
|
|
||||||
devtool add --src-subdir=freenect_launch "https://github.com/ros-drivers/freenect_stack.git"
|
|
||||||
devtool add --src-subdir=freenect_camera "https://github.com/ros-drivers/freenect_stack.git"
|
|
||||||
```
|
|
||||||
|
|
||||||
## TO DO ##
|
|
||||||
|
|
||||||
* Wrapper to generate recipes for each package in a repository.
|
|
||||||
* Add support for ament for ROS2 packages.
|
|
||||||
* Wrapper for using rosdistro data to generate recipes for various ROS distributions.
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
# Enable other layers to have modules in the same named directory
|
|
||||||
from pkgutil import extend_path
|
|
||||||
__path__ = extend_path(__path__, __name__)
|
|
|
@ -1,429 +0,0 @@
|
||||||
#!/usr/bin/python3
|
|
||||||
"""Recipe creation tool - catkin support plugin."""
|
|
||||||
|
|
||||||
# Copyright (C) 2017 Intel Corporation
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License version 2 as
|
|
||||||
# published by the Free Software Foundation.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along
|
|
||||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
|
||||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
||||||
|
|
||||||
import re
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from html.parser import HTMLParser
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from recipetool.create import RecipeHandler
|
|
||||||
from recipetool.create_buildsys import CmakeExtensionHandler
|
|
||||||
|
|
||||||
LOGGER = logging.getLogger('recipetool')
|
|
||||||
|
|
||||||
|
|
||||||
class RosHTMLParser(HTMLParser):
|
|
||||||
"""ROS HTML Parser class.
|
|
||||||
|
|
||||||
Primarily for removing any XHTML from the <description> tag.
|
|
||||||
See: http://www.ros.org/reps/rep-0127.html#description (Format 1)
|
|
||||||
See: http://www.ros.org/reps/rep-0140.html#description (Format 2)
|
|
||||||
"""
|
|
||||||
|
|
||||||
basic_text = ""
|
|
||||||
|
|
||||||
def handle_data(self, data):
|
|
||||||
"""Override HTMLParser handle_data method."""
|
|
||||||
if len(self.basic_text) > 0:
|
|
||||||
self.basic_text = self.basic_text + " "
|
|
||||||
self.basic_text = self.basic_text + data.strip()
|
|
||||||
|
|
||||||
|
|
||||||
class RosXmlParser:
|
|
||||||
"""ROS package.xml Parser class.
|
|
||||||
|
|
||||||
Uses the etree class from lxml to parse the ROS package.xml file.
|
|
||||||
This file is main source for information for constructing the BitBake
|
|
||||||
recipe for the ROS package.
|
|
||||||
|
|
||||||
See: http://www.ros.org/reps/rep-0127.html (Format 1)
|
|
||||||
See: http://www.ros.org/reps/rep-0140.html (Format 2)
|
|
||||||
"""
|
|
||||||
|
|
||||||
package_format = 0
|
|
||||||
|
|
||||||
def __init__(self, xml_path):
|
|
||||||
"""Initialize the class by finding the package format version."""
|
|
||||||
# Default to ROS package format 1
|
|
||||||
# http://wiki.ros.org/catkin/package.xml#Format_1_.28Legacy.29
|
|
||||||
self.package_format = 1
|
|
||||||
|
|
||||||
self.xml_path = xml_path
|
|
||||||
self.tree = etree.parse(self.xml_path)
|
|
||||||
# Check the ROS package format
|
|
||||||
# http://wiki.ros.org/catkin/package.xml#Format_1_.28Legacy.29
|
|
||||||
# or
|
|
||||||
# http://wiki.ros.org/catkin/package.xml#Format_2_.28Recommended.29
|
|
||||||
package_format_list = self.tree.xpath("/package[@format]")
|
|
||||||
for pkg_format in package_format_list:
|
|
||||||
self.package_format = int(pkg_format.get('format',
|
|
||||||
self.package_format))
|
|
||||||
if self.package_format > 2:
|
|
||||||
self.package_format = 2
|
|
||||||
LOGGER.warning("FORCING ROS Package Format to version " +
|
|
||||||
str(self.package_format))
|
|
||||||
elif self.package_format < 1:
|
|
||||||
self.package_format = 1
|
|
||||||
LOGGER.warning("FORCING ROS Package Format to version " +
|
|
||||||
str(self.package_format))
|
|
||||||
|
|
||||||
LOGGER.debug("ROS Package Format version " + str(self.package_format))
|
|
||||||
|
|
||||||
def get_format(self):
|
|
||||||
"""Return the package.xml format version."""
|
|
||||||
return str(self.package_format)
|
|
||||||
|
|
||||||
def clean_string(self, raw_string):
|
|
||||||
"""Remove white space and sanitize the string.
|
|
||||||
|
|
||||||
Replace double quotes with single quotes as bitbake
|
|
||||||
recipes variables will be set with double quotes.
|
|
||||||
"""
|
|
||||||
return re.sub(r'\s+', ' ', raw_string.strip().replace('"', "'"))
|
|
||||||
|
|
||||||
def get_single(self, xpath, required=True):
|
|
||||||
"""Return a single string value for the given xpath."""
|
|
||||||
xpath_list = self.tree.xpath(xpath)
|
|
||||||
if len(xpath_list) < 1:
|
|
||||||
if required:
|
|
||||||
LOGGER.error("ROS package.xml missing element " + str(xpath))
|
|
||||||
return None
|
|
||||||
elif len(xpath_list) > 1:
|
|
||||||
LOGGER.warning("ROS package.xml has more than 1 match for " +
|
|
||||||
str(xpath))
|
|
||||||
|
|
||||||
return self.clean_string(xpath_list[0].text)
|
|
||||||
|
|
||||||
def get_multiple_with_type(self, xpath, required=False):
|
|
||||||
"""Return dict of type attributes and the matching urls from xpath."""
|
|
||||||
items = {}
|
|
||||||
xpath_list = self.tree.xpath(xpath)
|
|
||||||
if len(xpath_list) < 1:
|
|
||||||
if required:
|
|
||||||
LOGGER.error("ROS package.xml missing element " + str(xpath))
|
|
||||||
for item in xpath_list:
|
|
||||||
url_string = self.clean_string(item.text)
|
|
||||||
url_type = self.clean_string(item.get('type', '')).lower()
|
|
||||||
items[url_type] = url_string
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
def get_multiple_with_email(self, xpath, required=True):
|
|
||||||
"""Return list of string values and email attrib for given xpath."""
|
|
||||||
items = []
|
|
||||||
xpath_list = self.tree.xpath(xpath)
|
|
||||||
if len(xpath_list) < 1:
|
|
||||||
if required:
|
|
||||||
LOGGER.error("ROS package.xml missing element " + str(xpath))
|
|
||||||
for item in xpath_list:
|
|
||||||
fullstring = self.clean_string(item.text)
|
|
||||||
email = self.clean_string(item.get('email', ''))
|
|
||||||
if len(email) > 0:
|
|
||||||
fullstring = fullstring + " <" + email + ">"
|
|
||||||
items.append(fullstring)
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
def get_multiple_with_version(self, xpath, required=False):
|
|
||||||
"""Return list of dependencies and version attribs for given xpath."""
|
|
||||||
def catkin_to_bitbake(version_type):
|
|
||||||
"""Map the Catkin version modifier to BitBake."""
|
|
||||||
mapper = {
|
|
||||||
"version_lt": "<",
|
|
||||||
"version_lte": "<=",
|
|
||||||
"version_eq": "=",
|
|
||||||
"version_gte": ">=",
|
|
||||||
"version_gt": ">",
|
|
||||||
}
|
|
||||||
return mapper.get(version_type, "UNDEFINED")
|
|
||||||
|
|
||||||
items = []
|
|
||||||
xpath_list = self.tree.xpath(xpath)
|
|
||||||
if len(xpath_list) < 1:
|
|
||||||
if required:
|
|
||||||
LOGGER.error("ROS package.xml missing element " + str(xpath))
|
|
||||||
for item in xpath_list:
|
|
||||||
fullstring = self.clean_string(item.text)
|
|
||||||
if len(item.attrib) > 1:
|
|
||||||
LOGGER.error("ROS package.xml element " + str(xpath) +
|
|
||||||
" has too many attributes!")
|
|
||||||
for version_type in item.attrib:
|
|
||||||
c_version_type = catkin_to_bitbake(version_type)
|
|
||||||
c_value = self.clean_string(item.attrib[version_type])
|
|
||||||
if len(c_value) > 1:
|
|
||||||
c_version_type = c_version_type + " " + c_value
|
|
||||||
if len(c_version_type) > 1:
|
|
||||||
fullstring = fullstring + " (" + c_version_type + ")"
|
|
||||||
items.append(fullstring)
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
def get_multiple_with_linenumber(self, xpath, required=False):
|
|
||||||
"""Return dict of string and their line numbers for given xpath."""
|
|
||||||
items = {}
|
|
||||||
xpath_list = self.tree.xpath(xpath)
|
|
||||||
if len(xpath_list) < 1:
|
|
||||||
if required:
|
|
||||||
LOGGER.error("ROS package.xml missing element " + str(xpath))
|
|
||||||
for item in xpath_list:
|
|
||||||
c_string = self.clean_string(item.text)
|
|
||||||
items[c_string] = item.sourceline
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
def get_name(self):
|
|
||||||
"""Return the Name of the ROS package."""
|
|
||||||
return self.get_single("/package/name")
|
|
||||||
|
|
||||||
def get_version(self):
|
|
||||||
"""Return the Version of the ROS package."""
|
|
||||||
return self.get_single("/package/version")
|
|
||||||
|
|
||||||
def get_description(self):
|
|
||||||
"""Return the Description of the ROS package.
|
|
||||||
|
|
||||||
Remove the XHTML information, if present, and only return
|
|
||||||
a simple text string description for the package.
|
|
||||||
"""
|
|
||||||
parser = RosHTMLParser()
|
|
||||||
parser.feed(self.get_single("/package/description"))
|
|
||||||
return self.clean_string(parser.basic_text)
|
|
||||||
|
|
||||||
def get_authors(self):
|
|
||||||
"""Return list of Authors of the ROS package."""
|
|
||||||
return self.get_multiple_with_email("/package/author", required=False)
|
|
||||||
|
|
||||||
def get_maintainers(self):
|
|
||||||
"""Return list of Maintainers of the ROS package."""
|
|
||||||
return self.get_multiple_with_email("/package/maintainer")
|
|
||||||
|
|
||||||
def get_urls(self):
|
|
||||||
"""Return list of Website URLs for the ROS package."""
|
|
||||||
return self.get_multiple_with_type("/package/url", required=False)
|
|
||||||
|
|
||||||
def get_licenses(self):
|
|
||||||
"""Return list of Licenses of the ROS package."""
|
|
||||||
return self.get_multiple_with_linenumber("/package/license")
|
|
||||||
|
|
||||||
def get_build_dependencies(self):
|
|
||||||
"""Return list of package Build Dependencies of the ROS package."""
|
|
||||||
dependencies = []
|
|
||||||
|
|
||||||
# build_depend is both format 1 & 2
|
|
||||||
for dependency in self.get_multiple_with_version(
|
|
||||||
"/package/build_depend"):
|
|
||||||
dependencies.append(dependency.replace("_", "-"))
|
|
||||||
if self.package_format > 1:
|
|
||||||
for dependency in self.get_multiple_with_version(
|
|
||||||
"/package/depend"):
|
|
||||||
dependencies.append(dependency.replace("_", "-"))
|
|
||||||
|
|
||||||
# remove any duplicates
|
|
||||||
dependencies = list(set(dependencies))
|
|
||||||
|
|
||||||
return dependencies
|
|
||||||
|
|
||||||
def get_runtime_dependencies(self):
|
|
||||||
"""Return list of package Run Dependencies of the ROS package."""
|
|
||||||
dependencies = []
|
|
||||||
|
|
||||||
# run_depend is format 1 only
|
|
||||||
if self.package_format == 1:
|
|
||||||
for dependency in self.get_multiple_with_version(
|
|
||||||
"/package/run_depend"):
|
|
||||||
dependencies.append(dependency.replace("_", "-"))
|
|
||||||
if self.package_format == 2:
|
|
||||||
for dependency in self.get_multiple_with_version(
|
|
||||||
"/package/exec_depend"):
|
|
||||||
dependencies.append(dependency.replace("_", "-"))
|
|
||||||
for dependency in self.get_multiple_with_version(
|
|
||||||
"/package/depend"):
|
|
||||||
dependencies.append(dependency.replace("_", "-"))
|
|
||||||
|
|
||||||
# remove any duplicates
|
|
||||||
dependencies = list(set(dependencies))
|
|
||||||
|
|
||||||
return dependencies
|
|
||||||
|
|
||||||
|
|
||||||
class CatkinRecipeHandler(RecipeHandler):
|
|
||||||
"""Catkin handler extension for recipetool."""
|
|
||||||
|
|
||||||
def process_license(self,
|
|
||||||
srctree, classes, lines_before,
|
|
||||||
lines_after, handled, extravalues,
|
|
||||||
licenses, license_file):
|
|
||||||
"""Generate the Catkin license data.
|
|
||||||
|
|
||||||
licenses: a dictionary of license:line_number
|
|
||||||
license_file: the name of the license file
|
|
||||||
"""
|
|
||||||
all_lines = []
|
|
||||||
license_keys = list(licenses.keys())
|
|
||||||
|
|
||||||
def get_license_checksum(license):
|
|
||||||
"""Output the license details."""
|
|
||||||
line_number = licenses[license]
|
|
||||||
|
|
||||||
m = hashlib.md5()
|
|
||||||
try:
|
|
||||||
lic_line = all_lines[line_number - 1]
|
|
||||||
m.update(lic_line.encode('utf-8'))
|
|
||||||
md5val = m.hexdigest()
|
|
||||||
except UnicodeEncodeError:
|
|
||||||
md5val = None
|
|
||||||
|
|
||||||
LOGGER.debug("License: '" + license + "' on line " +
|
|
||||||
str(line_number) + " with md5 " + md5val)
|
|
||||||
|
|
||||||
checksum = "file://" + os.path.basename(license_file) + \
|
|
||||||
";" + "beginline=" + str(line_number) + ";endline=" + \
|
|
||||||
str(line_number) + ";md5=" + md5val
|
|
||||||
return checksum
|
|
||||||
|
|
||||||
try:
|
|
||||||
lic_file = open(license_file)
|
|
||||||
all_lines = lic_file.readlines()
|
|
||||||
lic_file.close()
|
|
||||||
except:
|
|
||||||
LOGGER.error("License file '" + license_file + "'not readable!")
|
|
||||||
return False
|
|
||||||
|
|
||||||
checksum_files = []
|
|
||||||
if len(license_keys) > 0:
|
|
||||||
checksum_files.append(get_license_checksum(license_keys[0]))
|
|
||||||
del license_keys[0]
|
|
||||||
|
|
||||||
for license in license_keys:
|
|
||||||
checksum_files.append(get_license_checksum(license))
|
|
||||||
|
|
||||||
# Commas are not valid in BitBake License name
|
|
||||||
clean_keys = [re.sub(r',', '', lic) for lic in licenses.keys()]
|
|
||||||
|
|
||||||
extravalues['LICENSE'] = " & ".join(clean_keys)
|
|
||||||
extravalues['LIC_FILES_CHKSUM'] = " ".join(checksum_files)
|
|
||||||
|
|
||||||
handled.append('license')
|
|
||||||
|
|
||||||
def process(self, srctree, classes, lines_before, lines_after, handled,
|
|
||||||
extravalues):
|
|
||||||
"""Process the Catkin recipe.
|
|
||||||
|
|
||||||
Read the key tags from the package.xml ROS file and generate
|
|
||||||
the corresponding recipe variables for the recipe file.
|
|
||||||
"""
|
|
||||||
package_list = RecipeHandler.checkfiles(srctree, ['package.xml'],
|
|
||||||
recursive=False)
|
|
||||||
if len(package_list) > 0:
|
|
||||||
handled.append('buildsystem')
|
|
||||||
|
|
||||||
for package_file in package_list:
|
|
||||||
LOGGER.info("Found package_file: " + package_file)
|
|
||||||
xml = RosXmlParser(package_file)
|
|
||||||
|
|
||||||
classes.append('catkin')
|
|
||||||
|
|
||||||
extravalues['PN'] = xml.get_name() # Ignored if set
|
|
||||||
extravalues['PV'] = xml.get_version()
|
|
||||||
|
|
||||||
licenses = xml.get_licenses()
|
|
||||||
if len(licenses) < 1:
|
|
||||||
LOGGER.error("package.xml missing required LICENSE field!")
|
|
||||||
else:
|
|
||||||
self.process_license(srctree, classes, lines_before,
|
|
||||||
lines_after, handled, extravalues,
|
|
||||||
licenses, package_file)
|
|
||||||
|
|
||||||
lines_after.append('# This is a Catkin (ROS) based recipe')
|
|
||||||
lines_after.append('# ROS package.xml format version ' +
|
|
||||||
xml.get_format())
|
|
||||||
lines_after.append('')
|
|
||||||
|
|
||||||
lines_after.append("SUMMARY = \"" +
|
|
||||||
"ROS package " + xml.get_name() + "\"")
|
|
||||||
lines_after.append("DESCRIPTION = \"" +
|
|
||||||
xml.get_description() + "\"")
|
|
||||||
|
|
||||||
# Map the Catkin URLs to BitBake
|
|
||||||
urls = xml.get_urls()
|
|
||||||
if 'website' in urls:
|
|
||||||
lines_after.append("HOMEPAGE = \"" +
|
|
||||||
urls['website'] + "\"")
|
|
||||||
else:
|
|
||||||
if '' in urls:
|
|
||||||
lines_after.append("HOMEPAGE = \"" +
|
|
||||||
urls[''] + "\"")
|
|
||||||
if 'bugtracker' in urls:
|
|
||||||
lines_after.append("# ROS_BUGTRACKER = \"" +
|
|
||||||
urls['bugtracker'] + "\"")
|
|
||||||
|
|
||||||
if 'repository' in urls:
|
|
||||||
lines_after.append("# SRC_URI = \"" +
|
|
||||||
urls['repository'] + "\"")
|
|
||||||
|
|
||||||
authors = xml.get_authors()
|
|
||||||
if len(authors) > 0:
|
|
||||||
lines_after.append("# ROS_AUTHOR = \"" +
|
|
||||||
authors[0] + "\"")
|
|
||||||
del authors[0]
|
|
||||||
for author in authors:
|
|
||||||
lines_after.append("# ROS_AUTHOR += \"" +
|
|
||||||
author + "\"")
|
|
||||||
|
|
||||||
maintainers = xml.get_maintainers()
|
|
||||||
if len(maintainers) > 0:
|
|
||||||
lines_after.append("# ROS_MAINTAINER = \"" +
|
|
||||||
maintainers[0] + "\"")
|
|
||||||
del maintainers[0]
|
|
||||||
for maintainer in maintainers:
|
|
||||||
lines_after.append("# ROS_MAINTAINER += \"" +
|
|
||||||
maintainer + "\"")
|
|
||||||
|
|
||||||
lines_after.append("SECTION = \"devel\"")
|
|
||||||
|
|
||||||
dependencies = xml.get_build_dependencies()
|
|
||||||
if len(dependencies) > 0:
|
|
||||||
lines_after.append("DEPENDS = \"" +
|
|
||||||
dependencies[0] + "\"")
|
|
||||||
del dependencies[0]
|
|
||||||
for dependency in dependencies:
|
|
||||||
lines_after.append("DEPENDS += \"" +
|
|
||||||
dependency + "\"")
|
|
||||||
|
|
||||||
dependencies = xml.get_runtime_dependencies()
|
|
||||||
if len(dependencies) > 0:
|
|
||||||
lines_after.append("RDEPENDS_${PN}-dev = \"" +
|
|
||||||
dependencies[0] + "\"")
|
|
||||||
del dependencies[0]
|
|
||||||
for dependency in dependencies:
|
|
||||||
lines_after.append("RDEPENDS_${PN}-dev += \"" +
|
|
||||||
dependency + "\"")
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def register_recipe_handlers(handlers):
|
|
||||||
"""Register our recipe handler in front of default cmake handler.
|
|
||||||
|
|
||||||
Catkin needs to be a higher priority than CMake (50).
|
|
||||||
"""
|
|
||||||
handlers.append((CatkinRecipeHandler(), 90))
|
|
Loading…
Reference in New Issue