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