python distutils

distutils可以用来在Python环境中构建和安装额外的模块。新的模块可以是纯python的,也可以是用C/C++写的扩展模块,或者可以是Python包,包中包含了由C和Python编写的模块。
对于模块开发者以及需要安装模块的使用者来说,distutils的使用都很简单,作为一个开发者,除了编写源码之外,还需要:

  • 编写setup脚本(一般是setup.py);
  • 编写一个setup配置文件(可选);
  • 创建一个源码发布;
  • 创建一个或多个构建(二进制)发布(可选);

一个setup.py的简单例子,

from distutils.core import setup

setup(name='Distutils',
      version='1.0',
      description='Python Distribution Utilities',
      author='Greg Ward',
      author_email='gward@python.net',
      url='https://www.python.org/sigs/distutils-sig/',
      packages=['distutils', 'distutils.command'],
     )

关于distutils的具体用法,可以参考https://docs.python.org/2/distutils/setupscript.html

roslaunch 包结构分析

下面是roslaunch的setup.py,

from distutils.core import setup
from catkin_pkg.python_setup import generate_distutils_setup

#参数收集,返回到d,dict
d = generate_distutils_setup(
    packages=['roslaunch'],
    package_dir={'': 'src'},
    scripts=['scripts/roscore',
             'scripts/roslaunch',
             'scripts/roslaunch-complete',
             'scripts/roslaunch-deps',
             'scripts/roslaunch-logs'],
    requires=['genmsg', 'genpy', 'roslib', 'rospkg']
)
#将字典反转为参数值
setup(**d)

而其中的catkin_pkg,其git地址为https://github.com/ros-infrastructure/catkin_pkg.git
功能介绍如下,

catkin_pkg

Standalone Python library for the Catkin package system.

下面是generate_distutils_setup()函数的实现,这里用到了**在函数定义时的参数收集功能(dict),
其核心功能就是将package.xml文件中的内容解析放到一个字典中,然后返回。(而且还要加上输入参数kwargs,输入参数kwargs中收集的key如果在package.xml中有,则值必须一样,如果没有,则添加到返回值中)

#catkin_pkg\src\catkin_pkg\python_setup.py

from .package import InvalidPackage, parse_package

def generate_distutils_setup(package_xml_path=os.path.curdir, **kwargs):
    """
    Extract the information relevant for distutils from the package
    manifest. The following keys will be set:

    The "name" and "version" are taken from the eponymous tags.

    A single maintainer will set the keys "maintainer" and
    "maintainer_email" while multiple maintainers are merged into the
    "maintainer" fields (including their emails). Authors are handled
    likewise.

    The first URL of type "website" (or without a type) is used for
    the "url" field.

    The "description" is taken from the eponymous tag if it does not
    exceed 200 characters. If it does "description" contains the
    truncated text while "description_long" contains the complete.

    All licenses are merged into the "license" field.

    :param kwargs: All keyword arguments are passed through. The above
        mentioned keys are verified to be identical if passed as a
        keyword argument

    :returns: return dict populated with parsed fields and passed
        keyword arguments
    :raises: :exc:`InvalidPackage`
    :raises: :exc:`IOError`
    """
    package = parse_package(package_xml_path)

    data = {}
    data['name'] = package.name
    data['version'] = package.version

    # either set one author with one email or join all in a single field
    if len(package.authors) == 1 and package.authors[0].email is not None:
        data['author'] = package.authors[0].name
        data['author_email'] = package.authors[0].email
    else:
        data['author'] = ', '.join([('%s <%s>' % (a.name, a.email) if a.email is not None else a.name) for a in package.authors])

    # either set one maintainer with one email or join all in a single field
    if len(package.maintainers) == 1:
        data['maintainer'] = package.maintainers[0].name
        data['maintainer_email'] = package.maintainers[0].email
    else:
        data['maintainer'] = ', '.join(['%s <%s>' % (m.name, m.email) for m in package.maintainers])

    # either set the first URL with the type 'website' or the first URL of any type
    websites = [url.url for url in package.urls if url.type == 'website']
    if websites:
        data['url'] = websites[0]
    elif package.urls:
        data['url'] = package.urls[0].url

    if len(package.description) <= 200:
        data['description'] = package.description
    else:
        data['description'] = package.description[:197] + '...'
        data['long_description'] = package.description

    data['license'] = ', '.join(package.licenses)

    #输入参数kwargs中收集的key如果在package.xml中有,则值必须一样;
    #如果没有,则添加到返回值中。
    # pass keyword arguments and verify equality if generated and passed in
    for k, v in kwargs.items():
        if k in data:
            if v != data[k]:
                raise InvalidPackage('The keyword argument "%s" does not match the information from package.xml: "%s" != "%s"' % (k, v, data[k]))
        else:
            data[k] = v

    return data

而,package.xml中都是一些distutils中setup()函数执行时需要的一些参数,用xml进行可配置化。

<package>
  <name>roslaunch</name>
  <version>1.13.0</version>
  <description>
    roslaunch is a tool for easily launching multiple ROS <a
    href="http://ros.org/wiki/Nodes">nodes</a> locally and remotely
    via SSH, as well as setting parameters on the <a
    href="http://ros.org/wiki/Parameter Server">Parameter
    Server</a>. It includes options to automatically respawn processes
    that have already died. roslaunch takes in one or more XML
    configuration files (with the <tt>.launch</tt> extension) that
    specify the parameters to set and nodes to launch, as well as the
    machines that they should be run on.
  </description>
  <maintainer email="dthomas@osrfoundation.org">Dirk Thomas</maintainer>
  <license>BSD</license>

  <url>http://ros.org/wiki/roslaunch</url>
  <author>Ken Conley</author>

  <buildtool_depend version_gte="0.5.78">catkin</buildtool_depend>

  <run_depend>python-paramiko</run_depend>
  <run_depend version_gte="1.0.37">python-rospkg</run_depend>
  <run_depend>python-yaml</run_depend>
  <run_depend>rosclean</run_depend>
  <run_depend>rosgraph_msgs</run_depend>
  <run_depend>roslib</run_depend>
  <run_depend version_gte="1.11.16">rosmaster</run_depend>
  <run_depend>rosout</run_depend>
  <run_depend>rosparam</run_depend>
  <run_depend version_gte="1.13.3">rosunit</run_depend>

  <test_depend>rosbuild</test_depend>

  <export>
    <rosdoc config="rosdoc.yaml"/>
    <architecture_independent/>
  </export>
</package>

setup()函数的输入参数中,scripts的解释如下,

So far we have been dealing with pure and non-pure Python modules, which are usually not run by themselves but imported by scripts.
Scripts are files containing Python source code, intended to be started from the command line. Scripts don’t require Distutils to do anything very complicated.

所以,python 模块主要是用来被其他模块去import,而script是为了直接在命令行执行,类似于应用程序。
而roscore就是这样一个需要在命令行执行的脚本(程序),

scripts=['scripts/roscore',
             'scripts/roslaunch',
             'scripts/roslaunch-complete',
             'scripts/roslaunch-deps',
             'scripts/roslaunch-logs']

而roscore最终会去import roslaunch package,去调用其中的main函数。

#ros_comm\tools\roslaunch\scripts\roscore
import roslaunch
roslaunch.main(['roscore', '--core'] + sys.argv[1:])

后续将对roslaunch package源代码进行分析,但是由于其又依赖于ros_comm的其他tools,例如rosmaster,rosgraph,所以后续将逐步对ros_comm中的tools从简到难进行分析。