Skip to content

Commit

Permalink
add roslaunch_depends.py from ros/ros_comm#998 to support if, ros/ros…
Browse files Browse the repository at this point in the history
…_comm#953 could not load launch file with args directory
  • Loading branch information
k-okada committed Jul 12, 2017
1 parent a4aa24c commit 1a41de8
Show file tree
Hide file tree
Showing 4 changed files with 380 additions and 2 deletions.
5 changes: 4 additions & 1 deletion jsk_fetch_robot/jsk_fetch_startup/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ install(DIRECTORY config launch scripts data
if(CATKIN_ENABLE_TESTING)
find_package(catkin REQUIRED COMPONENTS rostest roslaunch)
# https://github.com/ros/ros_comm/pull/730
# https://github.com/ros/ros_comm/pull/998
set(roslaunch_check_script ${PROJECT_SOURCE_DIR}/scripts/roslaunch-check)
# roslaunch_add_file_check(launch/fetch_bringup.launch)
# https://github.com/ros/ros_comm/issues/953 could not load launch file with args directory
#roslaunch_add_file_check(launch/fetch_bringup.launch launch_teleop:=false)
roslaunch_add_file_check(test/roslaunch-check-fetch_bringup.xml)
roslaunch_add_file_check(launch/rviz.launch)
set(roslaunch_check_script ${roslaunch_DIR}/../scripts/roslaunch-check)
endif()
Expand Down
5 changes: 4 additions & 1 deletion jsk_fetch_robot/jsk_fetch_startup/scripts/roslaunch-check
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ def check_roslaunch(f):
errors = []
# check for missing deps
try:
base_pkg, file_deps, missing = roslaunch.depends.roslaunch_deps([f])
# https://github.com/ros/ros_comm/pull/998
from roslaunch_depends import roslaunch_deps
ase_pkg, file_deps, missing = roslaunch_deps([f])
#base_pkg, file_deps, missing = roslaunch.depends.roslaunch_deps([f])
except rospkg.common.ResourceNotFound as r:
errors.append("Could not find package [%s] included from [%s]"%(str(r), f))
missing = {}
Expand Down
365 changes: 365 additions & 0 deletions jsk_fetch_robot/jsk_fetch_startup/scripts/roslaunch_depends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
# 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.

"""
Utility module of roslaunch that extracts dependency information from
roslaunch files, including calculating missing package dependencies.
"""

from __future__ import print_function

import os
import sys

from xml.dom.minidom import parse
from xml.dom import Node as DomNode

import rospkg

from roslaunch.loader import convert_value
from roslaunch.substitution_args import resolve_args

NAME="roslaunch-deps"

class RoslaunchDepsException(Exception):
"""
Base exception of roslaunch.depends errors.
"""
pass

class RoslaunchDeps(object):
"""
Represents dependencies of a roslaunch file.
"""
def __init__(self, nodes=None, includes=None, pkgs=None):
if nodes == None:
nodes = []
if includes == None:
includes = []
if pkgs == None:
pkgs = []
self.nodes = nodes
self.includes = includes
self.pkgs = pkgs

def __eq__(self, other):
if not isinstance(other, RoslaunchDeps):
return False
return set(self.nodes) == set(other.nodes) and \
set(self.includes) == set(other.includes) and \
set(self.pkgs) == set(other.pkgs)

def __repr__(self):
return "nodes: %s\nincludes: %s\npkgs: %s"%(str(self.nodes), str(self.includes), str(self.pkgs))

def __str__(self):
return "nodes: %s\nincludes: %s\npkgs: %s"%(str(self.nodes), str(self.includes), str(self.pkgs))

def _get_arg_value(tag, context):
name = tag.attributes['name'].value
if tag.attributes.has_key('value'):
return resolve_args(tag.attributes['value'].value, context)
elif name in context['arg']:
return context['arg'][name]
elif tag.attributes.has_key('default'):
return resolve_args(tag.attributes['default'].value, context)
else:
raise RoslaunchDepsException("No value for arg [%s]"%(name))

def _check_ifunless(tag, context):
if tag.attributes.has_key('if'):
val = resolve_args(tag.attributes['if'].value, context)
if not convert_value(val, 'bool'):
return False
elif tag.attributes.has_key('unless'):
val = resolve_args(tag.attributes['unless'].value, context)
if convert_value(val, 'bool'):
return False
return True

def _parse_subcontext(tags, context):
subcontext = {'arg': {}}

if tags == None:
return subcontext

for tag in [t for t in tags if t.nodeType == DomNode.ELEMENT_NODE]:
if tag.tagName == 'arg' and _check_ifunless(tag, context):
subcontext['arg'][tag.attributes['name'].value] = _get_arg_value(tag, context)
return subcontext

def _parse_launch(tags, launch_file, file_deps, verbose, context):
dir_path = os.path.dirname(os.path.abspath(launch_file))
launch_file_pkg = rospkg.get_package_name(dir_path)

# process group, include, node, and test tags from launch file
for tag in [t for t in tags if t.nodeType == DomNode.ELEMENT_NODE]:
if not _check_ifunless(tag, context):
continue

if tag.tagName == 'group':

#descend group tags as they can contain node tags
_parse_launch(tag.childNodes, launch_file, file_deps, verbose, context)

elif tag.tagName == 'arg':
context['arg'][tag.attributes['name'].value] = _get_arg_value(tag, context)

elif tag.tagName == 'include':
try:
sub_launch_file = resolve_args(tag.attributes['file'].value, context)
except KeyError as e:
raise RoslaunchDepsException("Cannot load roslaunch <%s> tag: missing required attribute %s.\nXML is %s"%(tag.tagName, str(e), tag.toxml()))

# Check if an empty file is included, and skip if so.
# This will allow a default-empty <include> inside a conditional to pass
if sub_launch_file == '':
if verbose:
print("Empty <include> in %s. Skipping <include> of %s" %
(launch_file, tag.attributes['file'].value))
continue

if verbose:
print("processing included launch %s"%sub_launch_file)

# determine package dependency for included file
sub_pkg = rospkg.get_package_name(os.path.dirname(os.path.abspath(sub_launch_file)))
if sub_pkg is None:
print("ERROR: cannot determine package for [%s]"%sub_launch_file, file=sys.stderr)

if sub_launch_file not in file_deps[launch_file].includes:
file_deps[launch_file].includes.append(sub_launch_file)
if launch_file_pkg != sub_pkg:
file_deps[launch_file].pkgs.append(sub_pkg)

# recurse
file_deps[sub_launch_file] = RoslaunchDeps()
try:
dom = parse(sub_launch_file).getElementsByTagName('launch')
if not len(dom):
print("ERROR: %s is not a valid roslaunch file"%sub_launch_file, file=sys.stderr)
else:
launch_tag = dom[0]
sub_context = _parse_subcontext(tag.childNodes, context)
_parse_launch(launch_tag.childNodes, sub_launch_file, file_deps, verbose, sub_context)
except IOError as e:
raise RoslaunchDepsException("Cannot load roslaunch include '%s' in '%s'"%(sub_launch_file, launch_file))

elif tag.tagName in ['node', 'test']:
try:
pkg, type = [resolve_args(tag.attributes[a].value, context) for a in ['pkg', 'type']]
except KeyError as e:
raise RoslaunchDepsException("Cannot load roslaunch <%s> tag: missing required attribute %s.\nXML is %s"%(tag.tagName, str(e), tag.toxml()))
if (pkg, type) not in file_deps[launch_file].nodes:
file_deps[launch_file].nodes.append((pkg, type))
# we actually want to include the package itself if that's referenced
#if launch_file_pkg != pkg:
if pkg not in file_deps[launch_file].pkgs:
file_deps[launch_file].pkgs.append(pkg)

def parse_launch(launch_file, file_deps, verbose):
if verbose:
print("processing launch %s"%launch_file)

try:
dom = parse(launch_file).getElementsByTagName('launch')
except:
raise RoslaunchDepsException("invalid XML in %s"%(launch_file))
if not len(dom):
raise RoslaunchDepsException("invalid XML in %s"%(launch_file))

file_deps[launch_file] = RoslaunchDeps()
launch_tag = dom[0]
context = { 'arg': {}}
_parse_launch(launch_tag.childNodes, launch_file, file_deps, verbose, context)

def rl_file_deps(file_deps, launch_file, verbose=False):
"""
Generate file_deps file dependency info for the specified
roslaunch file and its dependencies.
@param file_deps: dictionary mapping roslaunch filenames to
roslaunch dependency information. This dictionary will be
updated with dependency information.
@type file_deps: { str : RoslaunchDeps }
@param verbose: if True, print verbose output
@type verbose: bool
@param launch_file: name of roslaunch file
@type launch_file: str
"""
parse_launch(launch_file, file_deps, verbose)

def fullusage():
name = NAME
print("""Usage:
\t%(name)s [options] <file-or-package>
"""%locals())

def print_deps(base_pkg, file_deps, verbose):
pkgs = []

# for verbose output we print extra source information
if verbose:
for f, deps in file_deps.items():
for p, t in deps.nodes:
print("%s [%s/%s]"%(p, p, t))

pkg = rospkg.get_package_name(os.path.dirname(os.path.abspath(f)))
if pkg is None: #cannot determine package
print("ERROR: cannot determine package for [%s]"%pkg, file=sys.stderr)
else:
print("%s [%s]"%(pkg, f))
print('-'*80)

# print out list of package dependencies
pkgs = []
for deps in file_deps.values():
pkgs.extend(deps.pkgs)
# print space-separated to be friendly to rosmake
print(' '.join([p for p in set(pkgs)]))

def calculate_missing(base_pkg, missing, file_deps, use_test_depends=False):
"""
Calculate missing package dependencies in the manifest. This is
mainly used as a subroutine of roslaunch_deps().
@param base_pkg: name of package where initial walk begins (unused).
@type base_pkg: str
@param missing: dictionary mapping package names to set of missing package dependencies.
@type missing: { str: set(str) }
@param file_deps: dictionary mapping launch file names to RoslaunchDeps of each file
@type file_deps: { str: RoslaunchDeps}
@param use_test_depends [bool]: use test_depends as installed package
@type use_test_depends: [bool]
@return: missing (see parameter)
@rtype: { str: set(str) }
"""
rospack = rospkg.RosPack()
for launch_file in file_deps.keys():
pkg = rospkg.get_package_name(os.path.dirname(os.path.abspath(launch_file)))

if pkg is None: #cannot determine package
print("ERROR: cannot determine package for [%s]"%pkg, file=sys.stderr)
continue
m = rospack.get_manifest(pkg)
d_pkgs = set([d.name for d in m.depends])
if m.is_catkin:
# for catkin packages consider the run dependencies instead
# else not released packages will not appear in the dependency list
# since rospkg does uses rosdep to decide which dependencies to return
from catkin_pkg.package import parse_package
p = parse_package(os.path.dirname(m.filename))
d_pkgs = set([d.name for d in p.run_depends])
if use_test_depends:
for d in p.test_depends:
d_pkgs.add(d.name)
# make sure we don't count ourselves as a dep
d_pkgs.add(pkg)

diff = list(set(file_deps[launch_file].pkgs) - d_pkgs)
if not pkg in missing:
missing[pkg] = set()
missing[pkg].update(diff)
return missing


def roslaunch_deps(files, verbose=False, use_test_depends=False):
"""
@param packages: list of packages to check
@type packages: [str]
@param files [str]: list of roslaunch files to check. Must be in
same package.
@type files: [str]
@param use_test_depends [bool]: use test_depends as installed package
@type use_test_depends: [bool]
@return: base_pkg, file_deps, missing.
base_pkg is the package of all files
file_deps is a { filename : RoslaunchDeps } dictionary of
roslaunch dependency information to update, indexed by roslaunch
file name.
missing is a { package : [packages] } dictionary of missing
manifest dependencies, indexed by package.
@rtype: str, dict, dict
"""
file_deps = {}
missing = {}
base_pkg = None

for launch_file in files:
if not os.path.exists(launch_file):
raise RoslaunchDepsException("roslaunch file [%s] does not exist"%launch_file)

pkg = rospkg.get_package_name(os.path.dirname(os.path.abspath(launch_file)))
if base_pkg and pkg != base_pkg:
raise RoslaunchDepsException("roslaunch files must be in the same package: %s vs. %s"%(base_pkg, pkg))
base_pkg = pkg
rl_file_deps(file_deps, launch_file, verbose)

calculate_missing(base_pkg, missing, file_deps, use_test_depends=use_test_depends)
return base_pkg, file_deps, missing

def roslaunch_deps_main(argv=sys.argv):
from optparse import OptionParser
parser = OptionParser(usage="usage: %prog [options] <file(s)...>", prog=NAME)
parser.add_option("--verbose", "-v", action="store_true",
dest="verbose", default=False,
help="Verbose output")
parser.add_option("--warn", "-w", action="store_true",
dest="warn", default=False,
help="Warn about missing manifest dependencies")

(options, args) = parser.parse_args(argv[1:])
if not args:
parser.error('please specify a launch file')

files = [arg for arg in args if os.path.exists(arg)]
packages = [arg for arg in args if not os.path.exists(arg)]
if packages:
parser.error("cannot locate %s"%','.join(packages))
try:
base_pkg, file_deps, missing = roslaunch_deps(files, verbose=options.verbose)
except RoslaunchDepsException as e:
print(sys.stderr, str(e))
sys.exit(1)

if options.warn:
print("Dependencies:")

print_deps(base_pkg, file_deps, options.verbose)

if options.warn:
print('\nMissing declarations:')
for pkg, miss in missing.items():
if miss:
print("%s/manifest.xml:"%pkg)
for m in miss:
print(' <depend package="%s" />'%m)

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<launch>
<group if="false" >
<include if="false" file="$(find jsk_fetch_startup)/launch/fetch_bringup.launch">
<arg name="launch_teleop" value="false" />
</include>
</group>
</launch>

0 comments on commit 1a41de8

Please sign in to comment.