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 13809 2011-05-26 17:50:09Z 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', 'notes', 'depend', 'rosdep', 'export', 'review', 'versioncontrol', 'platform'] 
 53  VALID = REQUIRED + OPTIONAL 
 54   
55 -class ManifestException(roslib.exceptions.ROSLibException): pass
56
57 -def get_nodes_by_name(n, name):
58 return [t for t in n.childNodes if t.nodeType == t.ELEMENT_NODE and t.tagName == name]
59 60 # TODO: this is all needlessly complicated in and indirect. Now that 61 # we are more commited to our manifest spec, this can be more direct 62 # (and unit tested) 63
64 -def check_optional(name, allowXHTML=False):
65 """ 66 Validator for optional elements. 67 @raise ManifestException: if validation fails 68 """ 69 def check(n, filename): 70 n = n.getElementsByTagName(name) 71 if len(n) > 1: 72 raise ManifestException("Invalid manifest file: must have a single '%s' element"%name) 73 if n: 74 if allowXHTML: 75 return ''.join([x.toxml() for x in n[0].childNodes]) 76 return _get_text(n[0].childNodes).strip()
77 return check 78
79 -def check_required(name, allowXHTML=False):
80 """ 81 Validator for required elements. 82 @raise ManifestException: if validation fails 83 """ 84 def check(n, filename): 85 n = n.getElementsByTagName(name) 86 if not n: 87 print >> sys.stderr, "Invalid manifest file[%s]: missing required '%s' element"%(filename, name) 88 return '' 89 if len(n) != 1: 90 raise ManifestException("Invalid manifest file: must have only one '%s' element"%name) 91 if allowXHTML: 92 return ''.join([x.toxml() for x in n[0].childNodes]) 93 return _get_text(n[0].childNodes).strip()
94 return check 95
96 -def check_platform(name):
97 """ 98 Validator for manifest platform. 99 @raise ManifestException: if validation fails 100 """ 101 def check(n, filename): 102 platforms = [e for e in n.getElementsByTagName(name)] 103 try: 104 vals = [(p.attributes['os'].value, p.attributes['version'].value, p.getAttribute('notes')) for p in platforms] 105 except KeyError, e: 106 raise ManifestException("<platform> tag is missing required '%s' attribute"%str(e)) 107 return [Platform(*v) for v in vals]
108 return check 109
110 -def check_depends(name):
111 """ 112 Validator for manifest depends. 113 @raise ManifestException: if validation fails 114 """ 115 def check(n, filename): 116 nodes = get_nodes_by_name(n, name) 117 depends = [e.attributes for e in nodes if 'thirdparty' not in e.attributes.keys()] 118 packages = [d['package'].value for d in depends] 119 return [Depend(p) for p in packages]
120 return check 121
122 -def check_stack_depends(name):
123 """ 124 Validator for stack depends. 125 @raise ManifestException: if validation fails 126 """ 127 def check(n, filename): 128 nodes = get_nodes_by_name(n, name) 129 depends = [e.attributes for e in nodes if 'thirdparty' not in e.attributes.keys()] 130 packages = [d['stack'].value for d in depends] 131 return [StackDepend(p) for p in packages]
132 return check 133
134 -def check_rosdeps(name):
135 """ 136 Validator for stack rosdeps. 137 @raise ManifestException: if validation fails 138 """ 139 def check(n, filename): 140 nodes = get_nodes_by_name(n, name) 141 rosdeps = [e.attributes for e in nodes] 142 names = [d['name'].value for d in rosdeps] 143 return [ROSDep(n) for n in names]
144 return check 145
146 -def _attrs(node):
147 attrs = {} 148 for k in node.attributes.keys(): 149 attrs[k] = node.attributes.get(k).value 150 return attrs
151
152 -def check_exports(name):
153 def check(n, filename): 154 ret_val = [] 155 for e in n.getElementsByTagName(name): 156 elements = [c for c in e.childNodes if c.nodeType == c.ELEMENT_NODE] 157 ret_val.extend([Export(t.tagName, _attrs(t), _get_text(t.childNodes)) for t in elements]) 158 return ret_val
159 return check 160
161 -def check_versioncontrol(name):
162 def check(n, filename): 163 e = n.getElementsByTagName(name) 164 if not e: 165 return None 166 # note: 'url' isn't actually required, but as we only support type=svn it implicitly is for now 167 return VersionControl(e[0].attributes['type'].value, e[0].attributes['url'].value)
168 return check 169
170 -def check(name):
171 if name == 'depend': 172 return check_depends('depend') 173 elif name == 'export': 174 return check_exports('export') 175 elif name == 'versioncontrol': 176 return check_versioncontrol('versioncontrol') 177 elif name == 'rosdep': 178 return check_rosdeps('rosdep') 179 elif name == 'platform': 180 return check_platform('platform') 181 elif name in REQUIRED: 182 if name in ALLOWXHTML: 183 return check_required(name, True) 184 return check_required(name) 185 elif name in OPTIONAL: 186 if name in ALLOWXHTML: 187 return check_optional(name, True) 188 return check_optional(name)
189
190 -class Export(object):
191 """ 192 Manifest 'export' tag 193 """ 194
195 - def __init__(self, tag, attrs, str):
196 """ 197 Create new export instance. 198 @param tag: name of the XML tag 199 @type tag: str 200 @param attrs: dictionary of XML attributes for this export tag 201 @type attrs: dict 202 @param str: string value contained by tag, if any 203 @type str: str 204 """ 205 self.tag = tag 206 self.attrs = attrs 207 self.str = str
208
209 - def get(self, attr):
210 """ 211 @return: value of attribute or None if attribute not set 212 @rtype: str 213 """ 214 return self.attrs.get(attr, None)
215 - def xml(self):
216 """ 217 @return: export instance represented as manifest XML 218 @rtype: str 219 """ 220 attrs = ' '.join([' %s="%s"'%(k,v) for k,v in self.attrs.iteritems()]) 221 if self.str: 222 return '<%s%s>%s</%s>'%(self.tag, attrs, self.str, self.tag) 223 else: 224 return '<%s%s />'%(self.tag, attrs)
225
226 -class Platform(object):
227 """ 228 Manifest 'platform' tag 229 """ 230 __slots__ = ['os', 'version', 'notes'] 231
232 - def __init__(self, os, version, notes=None):
233 """ 234 Create new depend instance. 235 @param os: OS name. must be non-empty 236 @type os: str 237 @param version: OS version. must be non-empty 238 @type version: str 239 @param notes: (optional) notes about platform support 240 @type notes: str 241 """ 242 if not os or not isinstance(os, basestring): 243 raise ValueError("bad 'os' attribute") 244 if not version or not isinstance(version, basestring): 245 raise ValueError("bad 'version' attribute") 246 if notes and not isinstance(notes, basestring): 247 raise ValueError("bad 'notes' attribute") 248 self.os = os 249 self.version = version 250 self.notes = notes
251
252 - def __str__(self):
253 return "%s %s"%(self.os, self.version)
254 - def __repr__(self):
255 return "%s %s"%(self.os, self.version)
256 - def __eq__(self, obj):
257 """ 258 Override equality test. notes *are* considered in the equality test. 259 """ 260 if not isinstance(obj, Platform): 261 return False 262 return self.os == obj.os and self.version == obj.version and self.notes == obj.notes
263 - def xml(self):
264 """ 265 @return: instance represented as manifest XML 266 @rtype: str 267 """ 268 if self.notes is not None: 269 return '<platform os="%s" version="%s" notes="%s"/>'%(self.os, self.version, self.notes) 270 else: 271 return '<platform os="%s" version="%s"/>'%(self.os, self.version)
272
273 -class Depend(object):
274 """ 275 Manifest 'depend' tag 276 """ 277 __slots__ = ['package'] 278
279 - def __init__(self, package):
280 """ 281 Create new depend instance. 282 @param package: package name. must be non-empty 283 @type package: str 284 """ 285 if not package or not isinstance(package, basestring): 286 raise ValueError("bad 'package' attribute") 287 self.package = package
288 - def __str__(self):
289 return self.package
290 - def __repr__(self):
291 return self.package
292 - def __eq__(self, obj):
293 if not isinstance(obj, Depend): 294 return False 295 return self.package == obj.package
296 - def xml(self):
297 """ 298 @return: depend instance represented as manifest XML 299 @rtype: str 300 """ 301 return '<depend package="%s" />'%self.package
302
303 -class StackDepend(object):
304 """ 305 Stack Manifest 'depend' tag 306 """ 307 __slots__ = ['stack', 'annotation'] 308
309 - def __init__(self, stack):
310 """ 311 @param stack: stack name. must be non-empty 312 @type stack: str 313 """ 314 if not stack or not isinstance(stack, basestring): 315 raise ValueError("bad 'stack' attribute") 316 self.stack = stack 317 self.annotation = None
318
319 - def __str__(self):
320 return self.stack
321 - def __repr__(self):
322 return self.stack
323 - def __eq__(self, obj):
324 if not isinstance(obj, StackDepend): 325 return False 326 return self.stack == obj.stack
327 - def xml(self):
328 """ 329 @return: stack depend instance represented as stack manifest XML 330 @rtype: str 331 """ 332 if self.annotation: 333 return '<depend stack="%s" /> <!-- %s -->'%(self.stack, self.annotation) 334 else: 335 return '<depend stack="%s" />'%self.stack
336
337 -class ROSDep(object):
338 """ 339 Manifest 'rosdep' tag 340 """ 341 __slots__ = ['name',] 342
343 - def __init__(self, name):
344 """ 345 Create new rosdep instance. 346 @param name: dependency name. Must be non-empty. 347 @type name: str 348 """ 349 if not name or not isinstance(name, basestring): 350 raise ValueError("bad 'name' attribute") 351 self.name = name
352 - def xml(self):
353 """ 354 @return: rosdep instance represented as manifest XML 355 @rtype: str 356 """ 357 return '<rosdep name="%s" />'%self.name
358
359 -class VersionControl(object):
360 """ 361 Manifest 'versioncontrol' tag 362 """ 363 __slots__ = ['type', 'url'] 364
365 - def __init__(self, type_, url):
366 """ 367 @param type_: version control type (e.g. 'svn'). must be non empty 368 @type type_: str 369 @param url: URL associated with version control. must be non empty 370 @type url: str 371 """ 372 if not type_ or not isinstance(type_, basestring): 373 raise ValueError("bad 'type' attribute") 374 if not url is None and not isinstance(url, basestring): 375 raise ValueError("bad 'url' attribute") 376 self.type = type_ 377 self.url = url
378 - def xml(self):
379 """ 380 @return: versioncontrol instance represented as manifest XML 381 @rtype: str 382 """ 383 if self.url: 384 return '<versioncontrol type="%s" url="%s" />'%(self.type, self.url) 385 else: 386 return '<versioncontrol type="%s" />'%self.type
387
388 -class _Manifest(object):
389 """ 390 Object representation of a ROS manifest file 391 """ 392 __slots__ = ['description', 'brief', \ 393 'author', 'license', 'license_url', 'url', \ 394 'depends', 'rosdeps','platforms',\ 395 'logo', 'exports',\ 396 'versioncontrol', 'status', 'notes',\ 397 'unknown_tags',\ 398 '_type']
399 - def __init__(self, _type='package'):
400 self.description = self.brief = self.author = \ 401 self.license = self.license_url = \ 402 self.url = self.logo = self.status = self.notes = '' 403 self.depends = [] 404 self.rosdeps = [] 405 self.exports = [] 406 self.platforms = [] 407 self._type = _type 408 409 # store unrecognized tags during parsing 410 self.unknown_tags = []
411
412 - def __str__(self):
413 return self.xml()
414 - def get_export(self, tag, attr):
415 """ 416 @return: exports that match the specified tag and attribute, e.g. 'python', 'path' 417 @rtype: [L{Export}] 418 """ 419 return [e.get(attr) for e in self.exports if e.tag == tag if e.get(attr) is not None]
420 - def xml(self):
421 """ 422 @return: Manifest instance as ROS XML manifest 423 @rtype: str 424 """ 425 if not self.brief: 426 desc = " <description>%s</description>"%self.description 427 else: 428 desc = ' <description brief="%s">%s</description>'%(self.brief, self.description) 429 author = " <author>%s</author>"%self.author 430 if self.license_url: 431 license = ' <license url="%s">%s</license>'%(self.license_url, self.license) 432 else: 433 license = " <license>%s</license>"%self.license 434 versioncontrol = url = logo = exports = "" 435 if self.url: 436 url = " <url>%s</url>"%self.url 437 if self.logo: 438 logo = " <logo>%s</logo>"%self.logo 439 depends = '\n'.join([" %s"%d.xml() for d in self.depends]) 440 rosdeps = '\n'.join([" %s"%rd.xml() for rd in self.rosdeps]) 441 platforms = '\n'.join([" %s"%p.xml() for p in self.platforms]) 442 if self.exports: 443 exports = ' <export>\n' + '\n'.join([" %s"%e.xml() for e in self.exports]) + ' </export>' 444 if self.versioncontrol: 445 versioncontrol = " %s"%self.versioncontrol.xml() 446 if self.status or self.notes: 447 review = ' <review status="%s" notes="%s" />'%(self.status, self.notes) 448 449 450 fields = filter(lambda x: x, [desc, author, license, review, url, logo, depends, rosdeps, platforms, exports, versioncontrol]) 451 return "<%s>\n"%self._type + "\n".join(fields) + "\n</%s>"%self._type
452
453 -def _get_text(nodes):
454 """ 455 DOM utility routine for getting contents of text nodes 456 """ 457 return "".join([n.data for n in nodes if n.nodeType == n.TEXT_NODE])
458
459 -def parse_file(m, file):
460 """ 461 Parse manifest file (package, stack) 462 @param m: field to populate 463 @type m: L{_Manifest} 464 @param file: manifest.xml file path 465 @type file: str 466 @return: return m, populated with parsed fields 467 @rtype: L{_Manifest} 468 """ 469 if not file: 470 raise ValueError("Missing manifest file argument") 471 if not os.path.isfile(file): 472 raise ValueError("Invalid/non-existent manifest file: %s"%file) 473 f = open(file, 'r') 474 try: 475 text = f.read() 476 finally: 477 f.close() 478 try: 479 return parse(m, text, file) 480 except ManifestException, e: 481 raise ManifestException("Invalid manifest file [%s]: %s"%(os.path.abspath(file), e))
482
483 -def parse(m, string, filename='string'):
484 """ 485 Parse manifest.xml string contents 486 @param string: manifest.xml contents 487 @type string: str 488 @param m: field to populate 489 @type m: L{_Manifest} 490 @return: return m, populated with parsed fields 491 @rtype: L{_Manifest} 492 """ 493 try: 494 d = dom.parseString(string) 495 except Exception, e: 496 raise ManifestException("invalid XML: %s"%e) 497 p = d.getElementsByTagName(m._type) 498 if len(p) != 1: 499 raise ManifestException("manifest must have a single '%s' element"%m._type) 500 p = p[0] 501 m.description = check('description')(p, filename) 502 m.brief = '' 503 try: 504 tag = p.getElementsByTagName('description')[0] 505 m.brief = tag.getAttribute('brief') or '' 506 except: 507 # means that 'description' tag is missing 508 pass 509 #TODO: figure out how to multiplex 510 if m._type == 'package': 511 m.depends = check_depends('depend')(p, filename) 512 elif m._type == 'stack': 513 m.depends = check_stack_depends('depend')(p, filename) 514 elif m._type == 'app': 515 # not implemented yet 516 pass 517 m.rosdeps = check('rosdep')(p, filename) 518 m.platforms = check('platform')(p, filename) 519 m.exports = check('export')(p, filename) 520 m.versioncontrol = check('versioncontrol')(p,filename) 521 m.license = check('license')(p, filename) 522 m.license_url = '' 523 try: 524 tag = p.getElementsByTagName('license')[0] 525 m.license_url = tag.getAttribute('url') or '' 526 except: 527 pass #manifest is missing required 'license' tag 528 529 m.status='unreviewed' 530 try: 531 tag = p.getElementsByTagName('review')[0] 532 m.status=tag.getAttribute('status') or '' 533 except: 534 pass #manifest is missing optional 'review status' tag 535 536 m.notes='' 537 try: 538 tag = p.getElementsByTagName('review')[0] 539 m.notes=tag.getAttribute('notes') or '' 540 except: 541 pass #manifest is missing optional 'review notes' tag 542 543 m.author = check('author')(p, filename) 544 m.url = check('url')(p, filename) 545 m.logo = check('logo')(p, filename) 546 547 # do some validation on what we just parsed 548 if m._type == 'stack': 549 if m.exports: 550 raise ManifestException("stack manifests are not allowed to have exports") 551 if m.rosdeps: 552 raise ManifestException("stack manifests are not allowed to have rosdeps") 553 554 # store unrecognized tags 555 m.unknown_tags = [e for e in p.childNodes if e.nodeType == e.ELEMENT_NODE and e.tagName not in VALID] 556 return m
557