diff --git a/setup.py b/setup.py index 8f94e343..edbcd5ef 100755 --- a/setup.py +++ b/setup.py @@ -12,6 +12,7 @@ version=__version__, packages=['catkin_pkg'], package_dir={'': 'src'}, + package_data={'catkin_pkg': ['templates/*.in']}, scripts=[], author='Dirk Thomas', author_email='dthomas@willowgarage.com', diff --git a/src/catkin_pkg/package_templates.py b/src/catkin_pkg/package_templates.py new file mode 100644 index 00000000..87b70674 --- /dev/null +++ b/src/catkin_pkg/package_templates.py @@ -0,0 +1,205 @@ +# Software License Agreement (BSD License) +# +# Copyright (c) 2008, Willow Garage, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# * Neither the name of Willow Garage, Inc. nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import print_function + +import sys +import os +from string import Template + +from catkin_pkg.package import Package + + +class PackageTemplate(Package): + + def __init__(self, components=None, **kwargs): + super(PackageTemplate, self).__init__(**kwargs) + self.components = components or [] + self.validate() + + +def create_files(newfiles, target_dir): + """ + writes file contents to target_dir/filepath for all entries of newfiles. + Aborts early if files exist in places for new files or directories + + :param newfiles: a dict {filepath: contents} + :param target_dir: a string + """ + # first check no filename conflict exists + for filename in newfiles: + target_file = os.path.join(target_dir, filename) + if os.path.exists(target_file): + raise ValueError('File exists: %s' % target_file) + dirname = os.path.dirname(target_file) + while(dirname != target_dir): + if os.path.isfile(dirname): + raise ValueError('Cannot create directory, file exists: %s' % + dirname) + dirname = os.path.dirname(dirname) + + for filename, content in newfiles.items(): + target_file = os.path.join(target_dir, filename) + dirname = os.path.dirname(target_file) + if not os.path.exists(dirname): + os.makedirs(dirname) + # print(target_file, content) + with open(target_file, 'ab') as fhand: + fhand.write(content) + + +def create_package_files(target_path, package_template, newfiles): + """ + creates given newfiles and a package.xml from package template. + + :param target_path: parent folder where to create the package + :param package_template: contains the required information + :param newfiles: dict {filepath: file_contents_str} + """ + newfiles[os.path.join(target_path, 'package.xml')] = create_package_xml(package_template) + create_files(newfiles, target_path) + + +class CatkinTemplate(Template): + """subclass to use @ instead of $ as markers""" + delimiter = '@' + escape = '@' + + +def _create_depend_tag(dep_type, + name, + version_eq=None, + version_lt=None, + version_lte=None, + version_gt=None, + version_gte=None): + """ + Helper to create xml snippet for package.xml + """ + + version_string = [] + for key, var in {'version_eq': version_eq, + 'version_lt': version_lt, + 'version_lte': version_lte, + 'version_gt': version_gt, + 'version_gte': version_gte}.items(): + if var is not None: + version_string.append(' %s="%s"' % (key, var)) + result = ' <%s%s>%s\n' % (dep_type, + ''.join(version_string), + name, + dep_type) + return result + + +def create_package_xml(package_template): + """ + :param package_template: contains the required information + :returns: file contents as string + """ + with open(os.path.join(os.path.dirname(__file__), 'templates', 'package.xml.in'), 'r') as fhand: + package_xml_template = fhand.read() + ctemp = CatkinTemplate(package_xml_template) + temp_dict = {} + for key in package_template.__slots__: + temp_dict[key] = getattr(package_template, key) + + if package_template.version_abi: + temp_dict['version_abi'] = ' abi="%s"' % package_template.version_abi + else: + temp_dict['version_abi'] = '' + + if not package_template.description: + temp_dict['description'] = 'The %s package ...' % package_template.name + + licenses = [] + for plicense in package_template.licenses: + licenses.append(' %s\n' % plicense) + temp_dict['licenses'] = ''.join(licenses) + + def get_person_tag(tagname, person): + email_string = ("" + if person.email is None + else 'email="%s"' % person.email) + return ' <%s %s>%s\n' % (tagname, email_string, person.name, tagname) + + maintainers = [] + for maintainer in package_template.maintainers: + maintainers.append(get_person_tag('maintainer', maintainer)) + temp_dict['maintainers'] = ''.join(maintainers) + + urls = [] + for url in package_template.urls: + type_string = ("" if url.type is None + else 'type="%s"' % url.type) + urls.append(' %s\n' % (type_string, url.url)) + temp_dict['urls'] = ''.join(urls) + + authors = [] + for author in package_template.authors: + authors.append(get_person_tag('author', author)) + temp_dict['authors'] = ''.join(authors) + + dependencies = [] + for dep_type, dep_list in {'build_depend': package_template.build_depends, + 'buildtool_depend': package_template.buildtool_depends, + 'run_depend': package_template.run_depends, + 'test_depend': package_template.test_depends, + 'conflict': package_template.conflicts, + 'replace': package_template.replaces}.items(): + for dep in dep_list: + if 'depend' in dep_type: + dependencies.append(_create_depend_tag(dep_type, + dep.name, + dep.version_eq, + dep.version_lt, + dep.version_lte, + dep.version_gt, + dep.version_gte)) + else: + dependencies.append(_create_depend_tag(dep_type, + dep.name)) + temp_dict['dependencies'] = ''.join(dependencies) + + exports = [] + if package_template.exports is not None: + for export in package_template.exports: + if export.content is not None: + sys.stderr.write('WARNING: Create package does not know how to serialize exports with content: %s, %s, %s' % (export.tagname, export.attributes, export.content)) + else: + attribs = ['%s="%s"' % (k, v) for (k, v) in export.attributes] + exports.append(' <%s%s/>\n' % (export.tagname, ''.join(attribs))) + temp_dict['exports'] = ''.join(exports) + + temp_dict['components'] = package_template.components + + return ctemp.substitute(temp_dict) diff --git a/src/catkin_pkg/templates/package.xml.in b/src/catkin_pkg/templates/package.xml.in new file mode 100644 index 00000000..2de48701 --- /dev/null +++ b/src/catkin_pkg/templates/package.xml.in @@ -0,0 +1,31 @@ + + + @name + @version + @description + + + +@maintainers + +@licenses + + +@urls + + +@authors + + + + + +@dependencies + + + + +@exports + + \ No newline at end of file diff --git a/test/test_templates.py b/test/test_templates.py new file mode 100644 index 00000000..9e184a77 --- /dev/null +++ b/test/test_templates.py @@ -0,0 +1,195 @@ +import os +import unittest +import tempfile +import shutil + +from mock import MagicMock, Mock + +from catkin_pkg.package_templates import create_files, create_package_files, \ + create_package_xml, PackageTemplate +from catkin_pkg.package import parse_package_for_distutils, parse_package, \ + Dependency, Export, Url + + +class TemplateTest(unittest.TestCase): + + def get_maintainer(self): + maint = Mock() + maint.email = 'foo@bar.com' + maint.name = 'John Foo' + return maint + + def test_create_files(self): + file1 = os.path.join('foo', 'bar') + file2 = os.path.join('foo', 'baz') + newfiles = {file1: 'foobar', file2: 'barfoo'} + try: + rootdir = tempfile.mkdtemp() + create_files(newfiles, rootdir) + self.assertTrue(os.path.isfile(os.path.join(rootdir, file1))) + self.assertTrue(os.path.isfile(os.path.join(rootdir, file2))) + self.assertRaises(ValueError, create_files, newfiles, rootdir) + finally: + shutil.rmtree(rootdir) + + + def test_create_package_xml(self): + maint = self.get_maintainer() + pack = PackageTemplate(name='foo', + description='foo', + version='0.0.0', + maintainers=[maint], + licenses=['BSD']) + + result = create_package_xml(pack) + self.assertTrue('foo' in result, result) + + def test_create_package(self): + maint = self.get_maintainer() + pack = PackageTemplate(name='bar', + description='bar', + package_format='1', + version='0.0.0', + version_abi='pabi', + maintainers=[maint], + licenses=['BSD']) + try: + rootdir = tempfile.mkdtemp() + file1 = os.path.join(rootdir, 'CMakeLists.txt') + file2 = os.path.join(rootdir, 'package.xml') + create_package_files(rootdir, pack, {file1: ''}) + self.assertTrue(os.path.isfile(file1)) + self.assertTrue(os.path.isfile(file2)) + finally: + shutil.rmtree(rootdir) + + def test_parse_generated(self): + maint = self.get_maintainer() + pack = PackageTemplate(name='bar', + package_format=1, + version='0.0.0', + version_abi='pabi', + urls=[Url('foo')], + description='pdesc', + maintainers=[maint], + licenses=['BSD']) + try: + rootdir = tempfile.mkdtemp() + file2 = os.path.join(rootdir, 'package.xml') + create_package_files(rootdir, pack, {}) + self.assertTrue(os.path.isfile(file2)) + + pack_result = parse_package(file2) + self.assertEqual(pack.name, pack_result.name) + self.assertEqual(pack.package_format, pack_result.package_format) + self.assertEqual(pack.version, pack_result.version) + self.assertEqual(pack.version_abi, pack_result.version_abi) + self.assertEqual(pack.description, pack_result.description) + self.assertEqual(pack.maintainers[0].name, pack_result.maintainers[0].name) + self.assertEqual(pack.maintainers[0].email, pack_result.maintainers[0].email) + self.assertEqual(pack.authors, pack_result.authors) + self.assertEqual(pack.urls[0].url, pack_result.urls[0].url) + self.assertEqual('website', pack_result.urls[0].type) + self.assertEqual(pack.licenses, pack_result.licenses) + self.assertEqual(pack.build_depends, pack_result.build_depends) + self.assertEqual(pack.buildtool_depends, pack_result.buildtool_depends) + self.assertEqual(pack.run_depends, pack_result.run_depends) + self.assertEqual(pack.test_depends, pack_result.test_depends) + self.assertEqual(pack.conflicts, pack_result.conflicts) + self.assertEqual(pack.replaces, pack_result.replaces) + self.assertEqual(pack.exports, pack_result.exports) + + rdict = parse_package_for_distutils(file2) + self.assertEqual({'name': 'bar', + 'maintainer': u'John Foo', + 'maintainer_email': 'foo@bar.com', + 'description': 'pdesc', + 'license': 'BSD', + 'version': '0.0.0', + 'author': '', + 'url': 'foo', + 'keywords': ['ROS']}, rdict) + finally: + shutil.rmtree(rootdir) + + def test_parse_generated_multi(self): + # test with multiple attributes filled + maint = self.get_maintainer() + pack = PackageTemplate(name='bar', + package_format=1, + version='0.0.0', + version_abi='pabi', + description='pdesc', + maintainers=[maint, maint], + authors=[maint, maint], + licenses=['BSD', 'MIT'], + urls=[Url('foo', 'bugtracker'), Url('bar')], + build_depends=[Dependency('dep1')], + buildtool_depends=[Dependency('dep2'), + Dependency('dep3')], + run_depends=[Dependency('dep4', version_lt='4')], + test_depends=[Dependency('dep5', + version_gt='4', + version_lt='4')], + conflicts=[Dependency('dep6')], + replaces=[Dependency('dep7'), + Dependency('dep8')], + exports=[Export('architecture_independent'), + Export('meta_package')]) + + def assertEqualDependencies(deplist1, deplist2): + if len(deplist1) != len(deplist1): + return False + for depx, depy in zip(deplist1, deplist2): + for attr in ['name', 'version_lt', 'version_lte', + 'version_eq', 'version_gte', 'version_gt']: + if getattr(depx, attr) != getattr(depy, attr): + return False + return True + + try: + rootdir = tempfile.mkdtemp() + file2 = os.path.join(rootdir, 'package.xml') + create_package_files(rootdir, pack, {}) + self.assertTrue(os.path.isfile(file2)) + + pack_result = parse_package(file2) + self.assertEqual(pack.name, pack_result.name) + self.assertEqual(pack.package_format, pack_result.package_format) + self.assertEqual(pack.version, pack_result.version) + self.assertEqual(pack.version_abi, pack_result.version_abi) + self.assertEqual(pack.description, pack_result.description) + self.assertEqual(len(pack.maintainers), len(pack_result.maintainers)) + self.assertEqual(len(pack.authors), len(pack_result.authors)) + self.assertEqual(len(pack.urls), len(pack_result.urls)) + self.assertEqual(pack.urls[0].url, pack_result.urls[0].url) + self.assertEqual(pack.urls[0].type, pack_result.urls[0].type) + self.assertEqual(pack.licenses, pack_result.licenses) + self.assertTrue(assertEqualDependencies(pack.build_depends, + pack_result.build_depends)) + self.assertTrue(assertEqualDependencies(pack.build_depends, + pack_result.build_depends)) + self.assertTrue(assertEqualDependencies(pack.buildtool_depends, + pack_result.buildtool_depends)) + self.assertTrue(assertEqualDependencies(pack.run_depends, + pack_result.run_depends)) + self.assertTrue(assertEqualDependencies(pack.test_depends, + pack_result.test_depends)) + self.assertTrue(assertEqualDependencies(pack.conflicts, + pack_result.conflicts)) + self.assertTrue(assertEqualDependencies(pack.replaces, + pack_result.replaces)) + self.assertEqual(pack.exports[0].tagname, pack_result.exports[0].tagname) + self.assertEqual(pack.exports[1].tagname, pack_result.exports[1].tagname) + + rdict = parse_package_for_distutils(file2) + self.assertEqual({'name': 'bar', + 'maintainer': u'John Foo , John Foo ', + 'description': 'pdesc', + 'license': 'BSD, MIT', + 'version': '0.0.0', + 'author': u'John Foo , John Foo ', + 'url': 'bar', + 'keywords': ['ROS']}, rdict) + finally: + shutil.rmtree(rootdir)