Package roslib :: Module manifestlib
[frames] | no frames]

Source Code for Module roslib.manifestlib

  1  #! /usr/bin/env python 
  2  # Software License Agreement (BSD License) 
  3  # 
  4  # Copyright (c) 2008, Willow Garage, Inc. 
  5  # All rights reserved. 
  6  # 
  7  # Redistribution and use in source and binary forms, with or without 
  8  # modification, are permitted provided that the following conditions 
  9  # are met: 
 10  # 
 11  #  * Redistributions of source code must retain the above copyright 
 12  #    notice, this list of conditions and the following disclaimer. 
 13  #  * Redistributions in binary form must reproduce the above 
 14  #    copyright notice, this list of conditions and the following 
 15  #    disclaimer in the documentation and/or other materials provided 
 16  #    with the distribution. 
 17  #  * Neither the name of Willow Garage, Inc. nor the names of its 
 18  #    contributors may be used to endorse or promote products derived 
 19  #    from this software without specific prior written permission. 
 20  # 
 21  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 22  # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 23  # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
 24  # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
 25  # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 26  # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 27  # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 28  # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 29  # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
 30  # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 31  # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 32  # POSSIBILITY OF SUCH DAMAGE. 
 33  # 
 34  # Revision $Id: manifestlib.py 14595 2011-08-08 16:25:36Z kwc $ 
 35  # $Author: kwc $ 
 36   
 37  """ 
 38  Internal library for processing 'manifest' files, i.e. manifest.xml and stack.xml. 
 39  For external code apis, see L{roslib.manifest} and L{roslib.stack_manifest}. 
 40  """ 
 41   
 42  import sys 
 43  import os 
 44  import xml.dom 
 45  import xml.dom.minidom as dom 
 46   
 47  import roslib.exceptions 
 48   
 49  # stack.xml and manifest.xml have the same internal tags right now 
 50  REQUIRED = ['author', 'license'] 
 51  ALLOWXHTML = ['description'] 
 52  OPTIONAL = ['logo', 'url', 'brief', 'description', 'status', 
 53              'notes', 'depend', 'rosdep', 'export', 'review', 
 54              'versioncontrol', 'platform', 'version', 'rosbuild2'] 
 55  VALID = REQUIRED + OPTIONAL 
 56   
57 -class ManifestException(roslib.exceptions.ROSLibException): pass
58
59 -def get_nodes_by_name(n, name):
60 return [t for t in n.childNodes if t.nodeType == t.ELEMENT_NODE and t.tagName == name]
61
62 -def check_optional(name, allowXHTML=False):
63 """ 64 Validator for optional elements. 65 @raise ManifestException: if validation fails 66 """ 67 def check(n, filename): 68 n = get_nodes_by_name(n, name) 69 if len(n) > 1: 70 raise ManifestException("Invalid manifest file: must have a single '%s' element"%name) 71 if n: 72 if allowXHTML: 73 return ''.join([x.toxml() for x in n[0].childNodes]) 74 return _get_text(n[0].childNodes).strip()
75 return check 76
77 -def check_required(name, allowXHTML=False):
78 """ 79 Validator for required elements. 80 @raise ManifestException: if validation fails 81 """ 82 def check(n, filename): 83 n = get_nodes_by_name(n, name) 84 if not n: 85 #print >> sys.stderr, "Invalid manifest file[%s]: missing required '%s' element"%(filename, name) 86 return '' 87 if len(n) != 1: 88 raise ManifestException("Invalid manifest file: must have only one '%s' element"%name) 89 if allowXHTML: 90 return ''.join([x.toxml() for x in n[0].childNodes]) 91 return _get_text(n[0].childNodes).strip()
92 return check 93
94 -def check_platform(name):
95 """ 96 Validator for manifest platform. 97 @raise ManifestException: if validation fails 98 """ 99 def check(n, filename): 100 platforms = get_nodes_by_name(n, name) 101 try: 102 vals = [(p.attributes['os'].value, p.attributes['version'].value, p.getAttribute('notes')) for p in platforms] 103 except KeyError as e: 104 raise ManifestException("<platform> tag is missing required '%s' attribute"%str(e)) 105 return [Platform(*v) for v in vals]
106 return check 107
108 -def check_depends(name):
109 """ 110 Validator for manifest depends. 111 @raise ManifestException: if validation fails 112 """ 113 def check(n, filename): 114 nodes = get_nodes_by_name(n, name) 115 # TDS 20110419: this is a hack. 116 # rosbuild2 has a <depend thirdparty="depname"/> tag, 117 # which is confusing this subroutine with 118 # KeyError: 'package' 119 # for now, explicitly don't consider thirdparty depends 120 depends = [e.attributes for e in nodes if 'thirdparty' not in e.attributes.keys()] 121 try: 122 packages = [d['package'].value for d in depends] 123 except KeyError: 124 raise ManifestException("Invalid manifest file: depends is missing 'package' attribute") 125 126 return [Depend(p) for p in packages]
127 return check 128
129 -def check_stack_depends(name):
130 """ 131 Validator for stack depends. 132 @raise ManifestException: if validation fails 133 """ 134 def check(n, filename): 135 nodes = get_nodes_by_name(n, name) 136 depends = [e.attributes for e in nodes] 137 packages = [d['stack'].value for d in depends] 138 return [StackDepend(p) for p in packages]
139 return check 140
141 -def check_rosdeps(name):
142 """ 143 Validator for stack rosdeps. 144 @raise ManifestException: if validation fails 145 """ 146 def check(n, filename): 147 nodes = get_nodes_by_name(n, name) 148 rosdeps = [e.attributes for e in nodes] 149 names = [d['name'].value for d in rosdeps] 150 return [ROSDep(n) for n in names]
151 return check 152
153 -def _attrs(node):
154 attrs = {} 155 for k in node.attributes.keys(): 156 attrs[k] = node.attributes.get(k).value 157 return attrs
158
159 -def check_exports(name):
160 def check(n, filename): 161 ret_val = [] 162 for e in get_nodes_by_name(n, name): 163 elements = [c for c in e.childNodes if c.nodeType == c.ELEMENT_NODE] 164 ret_val.extend([Export(t.tagName, _attrs(t), _get_text(t.childNodes)) for t in elements]) 165 return ret_val
166 return check 167
168 -def check_versioncontrol(name):
169 def check(n, filename): 170 e = get_nodes_by_name(n, name) 171 if not e: 172 return None 173 # note: 'url' isn't actually required, but as we only support type=svn it implicitly is for now 174 return VersionControl(e[0].attributes['type'].value, e[0].attributes['url'].value)
175 return check 176
177 -def check(name):
178 if name == 'depend': 179 return check_depends('depend') 180 elif name == 'export': 181 return check_exports('export') 182 elif name == 'versioncontrol': 183 return check_versioncontrol('versioncontrol') 184 elif name == 'rosdep': 185 return check_rosdeps('rosdep') 186 elif name == 'platform': 187 return check_platform('platform') 188 elif name in REQUIRED: 189 if name in ALLOWXHTML: 190 return check_required(name, True) 191 return check_required(name) 192 elif name in OPTIONAL: 193 if name in ALLOWXHTML: 194 return check_optional(name, True) 195 return check_optional(name)
196
197 -class Export(object):
198 """ 199 Manifest 'export' tag 200 """ 201
202 - def __init__(self, tag, attrs, str):
203 """ 204 Create new export instance. 205 @param tag: name of the XML tag 206 @type tag: str 207 @param attrs: dictionary of XML attributes for this export tag 208 @type attrs: dict 209 @param str: string value contained by tag, if any 210 @type str: str 211 """ 212 self.tag = tag 213 self.attrs = attrs 214 self.str = str
215
216 - def get(self, attr):
217 """ 218 @return: value of attribute or None if attribute not set 219 @rtype: str 220 """ 221 return self.attrs.get(attr, None)
222 - def xml(self):
223 """ 224 @return: export instance represented as manifest XML 225 @rtype: str 226 """ 227 attrs = ' '.join([' %s="%s"'%(k,v) for k,v in self.attrs.items()]) #py3k 228 if self.str: 229 return '<%s%s>%s</%s>'%(self.tag, attrs, self.str, self.tag) 230 else: 231 return '<%s%s />'%(self.tag, attrs)
232
233 -class Platform(object):
234 """ 235 Manifest 'platform' tag 236 """ 237 __slots__ = ['os', 'version', 'notes'] 238
239 - def __init__(self, os, version, notes=None):
240 """ 241 Create new depend instance. 242 @param os: OS name. must be non-empty 243 @type os: str 244 @param version: OS version. must be non-empty 245 @type version: str 246 @param notes: (optional) notes about platform support 247 @type notes: str 248 """ 249 if not os: 250 raise ValueError("bad 'os' attribute") 251 if not version: 252 raise ValueError("bad 'version' attribute") 253 self.os = os 254 self.version = version 255 self.notes = notes
256
257 - def __str__(self):
258 return "%s %s"%(self.os, self.version)
259 - def __repr__(self):
260 return "%s %s"%(self.os, self.version)
261 - def __eq__(self, obj):
262 """ 263 Override equality test. notes *are* considered in the equality test. 264 """ 265 if not isinstance(obj, Platform): 266 return False 267 return self.os == obj.os and self.version == obj.version and self.notes == obj.notes
268 - def xml(self):
269 """ 270 @return: instance represented as manifest XML 271 @rtype: str 272 """ 273 if self.notes is not None: 274 return '<platform os="%s" version="%s" notes="%s"/>'%(self.os, self.version, self.notes) 275 else: 276 return '<platform os="%s" version="%s"/>'%(self.os, self.version)
277
278 -class Depend(object):
279 """ 280 Manifest 'depend' tag 281 """ 282 __slots__ = ['package'] 283
284 - def __init__(self, package):
285 """ 286 Create new depend instance. 287 @param package: package name. must be non-empty 288 @type package: str 289 """ 290 if not package: 291 raise ValueError("bad 'package' attribute") 292 self.package = package
293 - def __str__(self):
294 return self.package
295 - def __repr__(self):
296 return self.package
297 - def __eq__(self, obj):
298 if not isinstance(obj, Depend): 299 return False 300 return self.package == obj.package
301 - def xml(self):
302 """ 303 @return: depend instance represented as manifest XML 304 @rtype: str 305 """ 306 return '<depend package="%s" />'%self.package
307
308 -class StackDepend(object):
309 """ 310 Stack Manifest 'depend' tag 311 """ 312 __slots__ = ['stack', 'annotation'] 313
314 - def __init__(self, stack):
315 """ 316 @param stack: stack name. must be non-empty 317 @type stack: str 318 """ 319 if not stack: 320 raise ValueError("bad 'stack' attribute") 321 self.stack = stack 322 self.annotation = None
323
324 - def __str__(self):
325 return self.stack
326 - def __repr__(self):
327 return self.stack
328 - def __eq__(self, obj):
329 if not isinstance(obj, StackDepend): 330 return False 331 return self.stack == obj.stack
332 - def xml(self):
333 """ 334 @return: stack depend instance represented as stack manifest XML 335 @rtype: str 336 """ 337 if self.annotation: 338 return '<depend stack="%s" /> <!-- %s -->'%(self.stack, self.annotation) 339 else: 340 return '<depend stack="%s" />'%self.stack
341
342 -class ROSDep(object):
343 """ 344 Manifest 'rosdep' tag 345 """ 346 __slots__ = ['name',] 347
348 - def __init__(self, name):
349 """ 350 Create new rosdep instance. 351 @param name: dependency name. Must be non-empty. 352 @type name: str 353 """ 354 if not name: 355 raise ValueError("bad 'name' attribute") 356 self.name = name
357 - def xml(self):
358 """ 359 @return: rosdep instance represented as manifest XML 360 @rtype: str 361 """ 362 return '<rosdep name="%s" />'%self.name
363
364 -class VersionControl(object):
365 """ 366 Manifest 'versioncontrol' tag 367 """ 368 __slots__ = ['type', 'url'] 369
370 - def __init__(self, type_, url):
371 """ 372 @param type_: version control type (e.g. 'svn'). must be non empty 373 @type type_: str 374 @param url: URL associated with version control. must be non empty 375 @type url: str 376 """ 377 if not type_ or not isinstance(type_, basestring): 378 raise ValueError("bad 'type' attribute") 379 if not url is None and not isinstance(url, basestring): 380 raise ValueError("bad 'url' attribute") 381 self.type = type_ 382 self.url = url
383 - def xml(self):
384 """ 385 @return: versioncontrol instance represented as manifest XML 386 @rtype: str 387 """ 388 if self.url: 389 return '<versioncontrol type="%s" url="%s" />'%(self.type, self.url) 390 else: 391 return '<versioncontrol type="%s" />'%self.type
392
393 -class _Manifest(object):
394 """ 395 Object representation of a ROS manifest file 396 """ 397 __slots__ = ['description', 'brief', \ 398 'author', 'license', 'license_url', 'url', \ 399 'depends', 'rosdeps','platforms',\ 400 'logo', 'exports', 'version',\ 401 'versioncontrol', 'status', 'notes',\ 402 'unknown_tags',\ 403 '_type']
404 - def __init__(self, _type='package'):
405 self.description = self.brief = self.author = \ 406 self.license = self.license_url = \ 407 self.url = self.logo = self.status = \ 408 self.version = self.notes = '' 409 self.depends = [] 410 self.rosdeps = [] 411 self.exports = [] 412 self.platforms = [] 413 self._type = _type 414 415 # store unrecognized tags during parsing 416 self.unknown_tags = []
417
418 - def __str__(self):
419 return self.xml()
420 - def get_export(self, tag, attr):
421 """ 422 @return: exports that match the specified tag and attribute, e.g. 'python', 'path' 423 @rtype: [L{Export}] 424 """ 425 return [e.get(attr) for e in self.exports if e.tag == tag if e.get(attr) is not None]
426 - def xml(self):
427 """ 428 @return: Manifest instance as ROS XML manifest 429 @rtype: str 430 """ 431 if not self.brief: 432 desc = " <description>%s</description>"%self.description 433 else: 434 desc = ' <description brief="%s">%s</description>'%(self.brief, self.description) 435 author = " <author>%s</author>"%self.author 436 if self.license_url: 437 license = ' <license url="%s">%s</license>'%(self.license_url, self.license) 438 else: 439 license = " <license>%s</license>"%self.license 440 versioncontrol = url = logo = exports = version = "" 441 if self.url: 442 url = " <url>%s</url>"%self.url 443 if self.version: 444 version = " <version>%s</version>"%self.version 445 if self.logo: 446 logo = " <logo>%s</logo>"%self.logo 447 depends = '\n'.join([" %s"%d.xml() for d in self.depends]) 448 rosdeps = '\n'.join([" %s"%rd.xml() for rd in self.rosdeps]) 449 platforms = '\n'.join([" %s"%p.xml() for p in self.platforms]) 450 if self.exports: 451 exports = ' <export>\n' + '\n'.join([" %s"%e.xml() for e in self.exports]) + ' </export>' 452 if self.versioncontrol: 453 versioncontrol = " %s"%self.versioncontrol.xml() 454 if self.status or self.notes: 455 review = ' <review status="%s" notes="%s" />'%(self.status, self.notes) 456 457 458 fields = filter(lambda x: x, 459 [desc, author, license, review, url, logo, depends, 460 rosdeps, platforms, exports, versioncontrol, version]) 461 return "<%s>\n"%self._type + "\n".join(fields) + "\n</%s>"%self._type
462
463 -def _get_text(nodes):
464 """ 465 DOM utility routine for getting contents of text nodes 466 """ 467 return "".join([n.data for n in nodes if n.nodeType == n.TEXT_NODE])
468
469 -def parse_file(m, file):
470 """ 471 Parse manifest file (package, stack) 472 @param m: field to populate 473 @type m: L{_Manifest} 474 @param file: manifest.xml file path 475 @type file: str 476 @return: return m, populated with parsed fields 477 @rtype: L{_Manifest} 478 """ 479 if not file: 480 raise ValueError("Missing manifest file argument") 481 if not os.path.isfile(file): 482 raise ValueError("Invalid/non-existent manifest file: %s"%file) 483 with open(file, 'r') as f: 484 text = f.read() 485 try: 486 return parse(m, text, file) 487 except ManifestException as e: 488 raise ManifestException("Invalid manifest file [%s]: %s"%(os.path.abspath(file), e))
489
490 -def parse(m, string, filename='string'):
491 """ 492 Parse manifest.xml string contents 493 @param string: manifest.xml contents 494 @type string: str 495 @param m: field to populate 496 @type m: L{_Manifest} 497 @return: return m, populated with parsed fields 498 @rtype: L{_Manifest} 499 """ 500 try: 501 d = dom.parseString(string) 502 except Exception as e: 503 raise ManifestException("invalid XML: %s"%e) 504 505 p = get_nodes_by_name(d, m._type) 506 if len(p) != 1: 507 raise ManifestException("manifest must have a single '%s' element"%m._type) 508 p = p[0] 509 m.description = check('description')(p, filename) 510 m.brief = '' 511 try: 512 tag = get_nodes_by_name(p, 'description')[0] 513 m.brief = tag.getAttribute('brief') or '' 514 except: 515 # means that 'description' tag is missing 516 pass 517 #TODO: figure out how to multiplex 518 if m._type == 'package': 519 m.depends = check_depends('depend')(p, filename) 520 elif m._type == 'stack': 521 m.depends = check_stack_depends('depend')(p, filename) 522 elif m._type == 'app': 523 # not implemented yet 524 pass 525 m.rosdeps = check('rosdep')(p, filename) 526 m.platforms = check('platform')(p, filename) 527 m.exports = check('export')(p, filename) 528 m.versioncontrol = check('versioncontrol')(p,filename) 529 m.license = check('license')(p, filename) 530 m.license_url = '' 531 try: 532 tag = get_nodes_by_name(p, 'license')[0] 533 m.license_url = tag.getAttribute('url') or '' 534 except: 535 pass #manifest is missing required 'license' tag 536 537 m.status='unreviewed' 538 try: 539 tag = get_nodes_by_name(p, 'review')[0] 540 m.status=tag.getAttribute('status') or '' 541 except: 542 pass #manifest is missing optional 'review status' tag 543 544 m.notes='' 545 try: 546 tag = get_nodes_by_name(p, 'review')[0] 547 m.notes=tag.getAttribute('notes') or '' 548 except: 549 pass #manifest is missing optional 'review notes' tag 550 551 m.author = check('author')(p, filename) 552 m.url = check('url')(p, filename) 553 m.version = check('version')(p, filename) 554 m.logo = check('logo')(p, filename) 555 556 # do some validation on what we just parsed 557 if m._type == 'stack': 558 if m.exports: 559 raise ManifestException("stack manifests are not allowed to have exports") 560 if m.rosdeps: 561 raise ManifestException("stack manifests are not allowed to have rosdeps") 562 563 # store unrecognized tags 564 m.unknown_tags = [e for e in p.childNodes if e.nodeType == e.ELEMENT_NODE and e.tagName not in VALID] 565 return m
566