1# Software License Agreement (BSD License) 2# 3# Copyright (c) 2009, Willow Garage, Inc. 4# All rights reserved. 5# 6# Redistribution and use in source and binary forms, with or without 7# modification, are permitted provided that the following conditions 8# are met: 9# 10# * Redistributions of source code must retain the above copyright 11# notice, this list of conditions and the following disclaimer. 12# * Redistributions in binary form must reproduce the above 13# copyright notice, this list of conditions and the following 14# disclaimer in the documentation and/or other materials provided 15# with the distribution. 16# * Neither the name of Willow Garage, Inc. nor the names of its 17# contributors may be used to endorse or promote products derived 18# from this software without specific prior written permission. 19# 20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31# POSSIBILITY OF SUCH DAMAGE. 32 33import os 34import rosinstall.__version__ 35 36from rosinstall.helpers import ROSInstallException, get_ros_stack_path 37 38# template for catkin fuerte, not valid for Groovy and beyond, to be 39# removed once fuerte goes out of support 40CATKIN_CMAKE_TOPLEVEL = """# 41# TOPLEVEL cmakelists 42# 43cmake_minimum_required(VERSION 2.8) 44cmake_policy(SET CMP0003 NEW) 45cmake_policy(SET CMP0011 NEW) 46 47set(CMAKE_CXX_FLAGS_INIT "-Wall") 48 49enable_testing() 50 51include(${CMAKE_SOURCE_DIR}/workspace-config.cmake OPTIONAL) 52 53list(APPEND CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR} ${CMAKE_BINARY_DIR}/cmake) 54 55file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 56file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 57 58if (IS_DIRECTORY ${CMAKE_SOURCE_DIR}/catkin) 59 message(STATUS "+++ catkin") 60 set(CATKIN_BUILD_PROJECTS "ALL" CACHE STRING 61 "List of projects to build, or ALL for all. Use to completely exclude certain projects from cmake traversal.") 62 add_subdirectory(catkin) 63else() 64 find_package(catkin) 65endif() 66 67catkin_workspace() 68""" 69 70SHELL_HEADER = """# THIS IS AN AUTO-GENERATED FILE 71# IT IS UNLIKELY YOU WANT TO EDIT THIS FILE BY HAND 72# IF YOU WANT TO CHANGE THE ROS ENVIRONMENT VARIABLES 73# USE THE rosinstall OR rosws TOOL INSTEAD. 74# Generator version: %s 75# see: http://www.ros.org/wiki/rosinstall 76""" % rosinstall.__version__.version 77 78 79def generate_catkin_cmake(path, catkinpp): 80 with open(os.path.join(path, "CMakeLists.txt"), 'w') as cmake_file: 81 cmake_file.write(CATKIN_CMAKE_TOPLEVEL) 82 83 if catkinpp: 84 with open(os.path.join(path, "workspace-config.cmake"), 'w') as config_file: 85 config_file.write("set (CMAKE_PREFIX_PATH %s)" % catkinpp) 86 87 88def generate_embedded_python(): 89 return """import sys 90import os 91import yaml 92 93workspace_path = os.environ.get('ROS_WORKSPACE', os.path.abspath('.')) 94filename = os.path.join(workspace_path, '.rosinstall') 95 96if not os.path.isfile(filename): 97 print('ERROR') 98 sys.exit("There is no file at %s" % filename) 99 100with open(filename, "r") as fhand: 101 try: 102 v = fhand.read(); 103 except Exception as e: 104 print('ERROR') 105 sys.exit("Failed to read file: %s %s " % (filename, str(e))) 106 107try: 108 y = yaml.load(v); 109except Exception as e: 110 print('ERROR') 111 sys.exit("Invalid yaml in %s: %s " % (filename, str(e))) 112 113if y is not None: 114 115 # put all non-setupfile entries into ROS_PACKAGE_PATH 116 paths = [] 117 for vdict in y: 118 for k, v in vdict.items(): 119 if v is not None and k != "setup-file": 120 path = os.path.join(workspace_path, v['local-name']) 121 if not os.path.isfile(path): 122 # add absolute path from workspace to relative paths 123 paths.append(os.path.normpath(path)) 124 else: 125 print('ERROR') 126 sys.exit("ERROR: referenced path is a file, not a folder: %s" % path) 127 output = '' 128 # add paths in reverse order 129 if len(paths) > 0: 130 output += ':'.join(reversed(paths)) 131 132 # We also want to return the location of any setupfile elements 133 output += 'ROSINSTALL_PATH_SETUPFILE_SEPARATOR' 134 setupfile_paths = [] 135 for vdict in y: 136 for k, v in vdict.items(): 137 if v is not None and k == "setup-file": 138 path = os.path.join(workspace_path, v['local-name']) 139 if not os.path.exists(path): 140 print('ERROR') 141 sys.exit("WARNING: referenced setupfile does not exist: %s" % path) 142 elif os.path.isfile(path): 143 setupfile_paths.append(path) 144 else: 145 print('ERROR') 146 sys.exit("ERROR: referenced setupfile is a folder: %s" % path) 147 output += ':'.join(setupfile_paths) 148 149 # printing will store the result in the variable 150 print(output)""" 151 152 153def generate_setup_sh_text(workspacepath): 154 ''' 155 generates the string that goes into setup.sh. 156 157 Sadly we cannot infer the workspacepath from within the sourced 158 file, previous hacks trying to determine it from the shell context 159 all failed in corner cases. 160 161 :param workspacepath: The path to the workspace 162 ''' 163 164 pycode = generate_embedded_python() 165 166 # overlay or standard 167 text = """#!/usr/bin/env sh 168%(header)s 169 170# This setup.sh file has to parse .rosinstall file, and source similar 171# setup.sh files recursively. In the course of recursion, shell 172# variables get overwritten. This means that when returning from 173# recursion, any variable may be in a different state 174 175# These variables accumulate data through recursion and must only be 176# reset and unset at the top level of recursion. 177 178if [ x"$_ROSINSTALL_IN_RECURSION" != x"recurse" ] ; then 179 # reset setupfile accumulator 180 _SETUPFILES_ROSINSTALL= 181 _ROS_PACKAGE_PATH_ROSINSTALL= 182 # reset RPP before sourcing other setup files 183 export ROS_PACKAGE_PATH= 184fi 185 186export ROS_WORKSPACE=%(wspath)s 187if [ ! "$ROS_MASTER_URI" ] ; then export ROS_MASTER_URI=http://localhost:11311 ; fi 188unset ROS_ROOT 189 190unset _SETUP_SH_ERROR 191 192# python script to read .rosinstall even when rosinstall is not installed 193# this files parses the .rosinstall and sets environment variables accordingly 194# The ROS_PACKAGE_PATH contains all elements in reversed order (for historic reasons) 195 196# We store into _PARSED_CONFIG the result of python code, 197# which is the ros_package_path and the list of setup_files to source 198# Using python here to benefit of the pyyaml library 199export _PARSED_CONFIG=`/usr/bin/env python << EOPYTHON 200 201%(pycode)s 202EOPYTHON` 203 204if [ x"$_PARSED_CONFIG" = x"ERROR" ]; then 205 echo 'Could not parse .rosinstall file' 1<&2 206 _SETUP_SH_ERROR=1 207fi 208 209# using sed to split up ros_package_path and setupfile results 210_ROS_PACKAGE_PATH_ROSINSTALL_NEW=`echo "$_PARSED_CONFIG" | sed 's,\(.*\)ROSINSTALL_PATH_SETUPFILE_SEPARATOR\(.*\),\\1,'` 211if [ ! -z "$_ROS_PACKAGE_PATH_ROSINSTALL_NEW" ]; then 212 if [ ! -z "$_ROS_PACKAGE_PATH_ROSINSTALL" ]; then 213 export _ROS_PACKAGE_PATH_ROSINSTALL=$_ROS_PACKAGE_PATH_ROSINSTALL:$_ROS_PACKAGE_PATH_ROSINSTALL_NEW 214 else 215 export _ROS_PACKAGE_PATH_ROSINSTALL=$_ROS_PACKAGE_PATH_ROSINSTALL_NEW 216 fi 217fi 218 219_SETUPFILES_ROSINSTALL_NEW=`echo "$_PARSED_CONFIG" | sed 's,\(.*\)'ROSINSTALL_PATH_SETUPFILE_SEPARATOR'\(.*\),\\2,'` 220if [ ! -z "$_SETUPFILES_ROSINSTALL_NEW" ]; then 221 if [ ! -z "$_SETUPFILES_ROSINSTALL" ]; then 222 _SETUPFILES_ROSINSTALL=$_SETUPFILES_ROSINSTALL_NEW:$_SETUPFILES_ROSINSTALL 223 else 224 _SETUPFILES_ROSINSTALL=$_SETUPFILES_ROSINSTALL_NEW 225 fi 226fi 227unset _PARSED_CONFIG 228 229# colon separates entries 230_LOOP_SETUP_FILE=`echo $_SETUPFILES_ROSINSTALL | sed 's,\([^:]*\)[:]\(.*\),\\1,'` 231# this loop does fake recursion, as the called setup.sh may work on 232# the remaining elements in the _SETUPFILES_ROSINSTALL stack 233while [ ! -z "$_LOOP_SETUP_FILE" ] 234do 235 # need to pop from stack before recursing, as chained setup.sh might rely on this 236 _SETUPFILES_ROSINSTALL=`echo $_SETUPFILES_ROSINSTALL | sed 's,\([^:]*[:]*\),,'` 237 if [ -f "$_LOOP_SETUP_FILE" ]; then 238 _ROSINSTALL_IN_RECURSION=recurse 239 . $_LOOP_SETUP_FILE 240 unset _ROSINSTALL_IN_RECURSION 241 else 242 echo warn: no such file : "$_LOOP_SETUP_FILE" 243 fi 244 _LOOP_SETUP_FILE=`echo $_SETUPFILES_ROSINSTALL | sed 's,\([^:]*\)[:]\(.*\),\\1,'` 245done 246 247unset _LOOP_SETUP_FILE 248unset _SETUPFILES_ROSINSTALL 249 250# prepend elements from .rosinstall file to ROS_PACKAGE_PATH 251# ignoring duplicates entries from value set by setup files 252export ROS_PACKAGE_PATH=`/usr/bin/env python << EOPYTHON 253import os 254ros_package_path = os.environ.get('ROS_PACKAGE_PATH', '') 255original_elements = ros_package_path.split(':') 256ros_package_path2 = os.environ.get('_ROS_PACKAGE_PATH_ROSINSTALL', '') 257new_elements = ros_package_path2.split(':') 258new_elements = [path for path in new_elements if path] 259 260for original_path in original_elements: 261 if original_path and original_path not in new_elements: 262 new_elements.append(original_path) 263print(':'.join(new_elements)) 264EOPYTHON` 265 266unset _ROS_PACKAGE_PATH_ROSINSTALL 267 268# restore ROS_WORKSPACE in case other setup.sh changed/unset it 269export ROS_WORKSPACE=%(wspath)s 270 271# if setup.sh did not set ROS_ROOT (pre-fuerte) 272if [ -z "${ROS_ROOT}" ]; then 273 # using ROS_ROOT now being in ROS_PACKAGE_PATH 274 export _ROS_ROOT_ROSINSTALL=`/usr/bin/env python << EOPYTHON 275import sys, os; 276if 'ROS_PACKAGE_PATH' in os.environ: 277 pkg_path = os.environ['ROS_PACKAGE_PATH'] 278 for path in pkg_path.split(':'): 279 if (os.path.basename(path) == 'ros' 280 and os.path.isfile(os.path.join(path, 'stack.xml'))): 281 print(path) 282 break 283EOPYTHON` 284 285 if [ ! -z "${_ROS_ROOT_ROSINSTALL}" ]; then 286 export ROS_ROOT=$_ROS_ROOT_ROSINSTALL 287 export PATH=$ROS_ROOT/bin:$PATH 288 export PYTHONPATH=$ROS_ROOT/core/roslib/src:$PYTHONPATH 289 fi 290unset _ROS_ROOT_ROSINSTALL 291fi 292 293if [ ! -z "$_SETUP_SH_ERROR" ]; then 294 # return failure code when sourcing file 295 false 296fi 297""" % {'header': SHELL_HEADER, 'wspath': workspacepath, 'pycode': pycode} 298 299 return text 300 301 302def generate_setup_bash_text(shell): 303 ''' 304 Generates the contents that go into a setup.bash or setup.zsh 305 file. The intent of such a file is to enable shell extensions, 306 such as special ros commands and tab completion. The generation 307 is complex because the setup of the system changed between ROS 308 electric and fuerte. In fuerte, the distro setup.sh also loads 309 distro rosbash based on CATKIN_SHELL. Before fuerte, it is up to 310 setup.bash to do so. 311 ''' 312 if shell == 'bash': 313 script_path = """ 314SCRIPT_PATH="${BASH_SOURCE[0]}"; 315if([ -h "${SCRIPT_PATH}" ]) then 316 while([ -h "${SCRIPT_PATH}" ]) do SCRIPT_PATH=`readlink "${SCRIPT_PATH}"`; done 317fi 318export OLDPWDBAK=$OLDPWD 319pushd . > /dev/null 320cd `dirname ${SCRIPT_PATH}` > /dev/null 321SCRIPT_PATH=`pwd`; 322popd > /dev/null 323export OLDPWD=$OLDPWDBAK 324""" 325 call_setup_sh = ". $SCRIPT_PATH/setup.sh" 326 elif shell == 'zsh': 327 script_path = 'SCRIPT_PATH="$(dirname $0)"' 328 call_setup_sh = """ 329emulate sh # emulate POSIX 330. $SCRIPT_PATH/setup.sh 331emulate zsh # back in zsh 332""" 333 else: 334 raise ROSInstallException("%s shell unsupported." % shell) 335 336 text = """#!/usr/bin/env %(shell)s 337%(header)s 338 339CATKIN_SHELL=%(shell)s 340 341%(script_path)s 342 343# Load the path of this particular setup.%(shell)s 344 345if [ ! -f "$SCRIPT_PATH/setup.sh" ]; then 346 echo "Bug: shell script unable to determine its own location: $SCRIPT_PATH" 347 return 22 348fi 349 350# unset _ros_decode_path (function of rosbash) to check later whether setup.sh has sourced ros%(shell)s 351unset -f _ros_decode_path 1> /dev/null 2>&1 352 353%(call_setup_sh)s 354 355# if we have a ROS_ROOT, then we might need to source rosbash (pre-fuerte) 356if [ ! -z "${ROS_ROOT}" ]; then 357 # check whether setup.sh also already sourced rosbash 358 # Cannot rely on $? due to set -o errexit in build scripts 359 RETURNCODE=`type _ros_decode_path 2> /dev/null | grep function 1>/dev/null 2>&1 || echo error` 360 361 # for ROS electric and before, source rosbash 362 if [ ! "$RETURNCODE" = "" ]; then 363 RETURNCODE=`rospack help 1> /dev/null 2>&1 || echo error` 364 if [ "$RETURNCODE" = "" ]; then 365 ROSSHELL_PATH=`rospack find rosbash`/ros%(shell)s 366 if [ -e "$ROSSHELL_PATH" ]; then 367 . $ROSSHELL_PATH 368 fi 369 else 370 echo "rospack could not be found, you cannot have ros%(shell)s features until you bootstrap ros" 371 fi 372 fi 373fi 374""" % {'shell': shell, 375 'script_path': script_path, 376 'call_setup_sh': call_setup_sh, 377 'header': SHELL_HEADER} 378 return text 379 380 381def generate_setup(config, no_ros_allowed=False): 382 ros_root = get_ros_stack_path(config) 383 if ros_root is None: 384 if not no_ros_allowed: 385 candidates = [] 386 for t in config.get_config_elements(): 387 if os.path.basename(t.get_local_name()) == 'ros': 388 candidates.append(t.get_path()) 389 raise ROSInstallException(""" 390No 'ros' stack detected in candidates %s. 391Please add the location of a ros distribution to this command. 392 393See http://ros.org/wiki/rosinstall.""" % (candidates)) 394 395 text = generate_setup_sh_text(workspacepath=config.get_base_path()) 396 setup_path = os.path.join(config.get_base_path(), 'setup.sh') 397 with open(setup_path, 'w') as fhand: 398 fhand.write(text) 399 400 for shell in ['bash', 'zsh']: 401 text = generate_setup_bash_text(shell) 402 setup_path = os.path.join(config.get_base_path(), 'setup.%s' % shell) 403 with open(setup_path, 'w') as fhand: 404 fhand.write(text) 405