File: rosws_cli.py

package info (click to toggle)
ros-rosinstall 0.7.8-4
  • links: PTS, VCS
  • area: main
  • in suites: bullseye, sid
  • size: 648 kB
  • sloc: python: 3,399; makefile: 131; xml: 12
file content (479 lines) | stat: -rw-r--r-- 20,263 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
# Software License Agreement (BSD License)
#
# Copyright (c) 2010, 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.

"""%(prog)s is a command to manipulate ROS workspaces. %(prog)s replaces its predecessor rosinstall.

Official usage:
  %(prog)s CMD [ARGS] [OPTIONS]

%(prog)s will try to infer install path from context

Type '%(prog)s help' for usage.
"""

from __future__ import print_function
import os
import sys
import yaml

from optparse import OptionParser

from wstool.cli_common import get_info_list, get_info_table, \
    get_info_table_raw_csv, get_workspace, ONLY_OPTION_VALID_ATTRS
import rosinstall.rosinstall_cmd as rosinstall_cmd
from wstool.multiproject_cmd import get_config, cmd_install_or_update, \
    cmd_snapshot, cmd_version, cmd_info, cmd_find_unmanaged_repos
import rosinstall.__version__

from wstool.common import MultiProjectException, select_elements
from wstool.helpers import ROSINSTALL_FILENAME
from rosinstall.helpers import get_ros_package_path, get_ros_stack_path
from wstool.multiproject_cli import MultiprojectCLI, __MULTIPRO_CMD_DICT__, \
    __MULTIPRO_CMD_HELP_LIST__, __MULTIPRO_CMD_ALIASES__, \
    IndentedHelpFormatterWithNL, list_usage

## This file adds or extends commands from multiproject_cli where ROS
## specific output has to be generated.

# extend the commands of multiproject
__ROSWS_CMD_DICT__ = {}
__ROSWS_CMD_DICT__.update(__MULTIPRO_CMD_DICT__)
__ROSWS_CMD_DICT__["regenerate"] = "create ROS workspace specific setup files"

__ROSWS_CMD_HELP_LIST__ = __MULTIPRO_CMD_HELP_LIST__[:]
__ROSWS_CMD_HELP_LIST__.extend([None, 'regenerate'])

_PROGNAME = 'rosws'
_VARNAME = 'ROS_WORKSPACE'


class RoswsCLI(MultiprojectCLI):

    def __init__(self, config_filename=ROSINSTALL_FILENAME, progname=_PROGNAME):
        MultiprojectCLI.__init__(self,
                                 progname=progname,
                                 config_filename=config_filename,
                                 allow_other_element=True,
                                 config_generator=rosinstall_cmd.cmd_persist_config)

    def cmd_init(self, argv):
        if self.config_filename is None:
            print('Error: Bug: config filename required for init')
            return 1
        parser = OptionParser(
            usage="""usage: %s init [TARGET_PATH [SOURCE_PATH]]?""" % self.progname,
            formatter=IndentedHelpFormatterWithNL(),
            description=__MULTIPRO_CMD_DICT__["init"] + """

%(prog)s init does the following:
  1. Reads folder/file/web-uri SOURCE_PATH looking for a rosinstall yaml
  2. Creates new %(cfg_file)s file at TARGET-PATH
  3. Generates ROS setup files

SOURCE_PATH can e.g. be a folder like /opt/ros/electric
If PATH is not given, uses current dir.

Examples:
$ %(prog)s init ~/fuerte /opt/ros/fuerte
""" % {'cfg_file': self.config_filename, 'prog': self.progname},
            epilog="See: http://www.ros.org/wiki/rosinstall for details\n")
        parser.add_option("-c", "--catkin", dest="catkin", default=False,
                          help="Declare this is a catkin build.",
                          action="store_true")
        parser.add_option("--cmake-prefix-path", dest="catkinpp", default=None,
                          help="Where to set the CMAKE_PREFIX_PATH",
                          action="store")
        parser.add_option("--continue-on-error", dest="robust", default=False,
                          help="Continue despite checkout errors",
                          action="store_true")
        parser.add_option("-j", "--parallel", dest="jobs", default=1,
                          help="How many parallel threads to use for installing",
                          action="store")
        (options, args) = parser.parse_args(argv)
        if len(args) < 1:
            target_path = '.'
        else:
            target_path = args[0]

        if not os.path.isdir(target_path):
            if not os.path.exists(target_path):
                os.mkdir(target_path)
            else:
                print('Error: Cannot create in target path %s ' % target_path)

        if os.path.exists(os.path.join(target_path, self.config_filename)):
            print('Error: There already is a workspace config file %s at "%s". Use %s install/modify.' %
                  (self.config_filename, target_path, self.progname))
            return 1
        if len(args) > 2:
            parser.error('Too many arguments')
        config_uris = []
        if len(args) == 2:
            config_uris.append(args[1])
        if len(config_uris) > 0:
            print('Using ROS_ROOT: %s' % config_uris[0])

        config = get_config(basepath=target_path,
                            additional_uris=config_uris,
                            config_filename=self.config_filename)

        # includes ROS specific files

        print("Writing %s" % os.path.join(config.get_base_path(),
              self.config_filename))
        rosinstall_cmd.cmd_persist_config(config)

        ## install or update each element
        install_success = cmd_install_or_update(
            config,
            robust=False,
            num_threads=int(options.jobs))

        rosinstall_cmd.cmd_generate_ros_files(config,
                                              target_path,
                                              nobuild=True,
                                              rosdep_yes=False,
                                              catkin=options.catkin,
                                              catkinpp=options.catkinpp,
                                              no_ros_allowed=True)

        if not install_success:
            print("Warning: installation encountered errors, but --continue-on-error was requested.  Look above for warnings.")
        print("\nrosinstall update complete.")
        if (options.catkin is False
            and options.catkinpp is None):
            print("\nType 'source %s/setup.bash' to change into this environment. Add that source command to the bottom of your ~/.bashrc to set it up every time you log in.\n\nIf you are not using bash please see http://www.ros.org/wiki/rosinstall/NonBashShells " % os.path.abspath(target_path))
        return 0

    def cmd_regenerate(self, target_path, argv, config=None):
        parser = OptionParser(usage="usage: %s regenerate" % self.progname,
                              formatter=IndentedHelpFormatterWithNL(),
                              description=__MULTIPRO_CMD_DICT__["remove"] + """

this command without options generates files setup.sh, setup.bash and
setup.zsh. Note that doing this is unnecessary in general, as these
files do not change anymore, unless you change from one ROS distro to
another (which you should never do like this, create a separate new
workspace instead), or you deleted or modified any of those files
accidentally.
""",
                              epilog="See: http://www.ros.org/wiki/rosinstall for details\n")
        parser.add_option("-c", "--catkin", dest="catkin", default=False,
                          help="Declare this is a catkin build.",
                          action="store_true")
        parser.add_option("--cmake-prefix-path", dest="catkinpp", default=None,
                          help="Where to set the CMAKE_PREFIX_PATH",
                          action="store")
        # -t option required here for help but used one layer above, see cli_common
        parser.add_option("-t", "--target-workspace", dest="workspace", default=None,
                          help="which workspace to use",
                          action="store")
        (options, args) = parser.parse_args(argv)
        if len(args) > 0:
            print("Error: Too many arguments.")
            print(parser.usage)
            return -1

        if config is None:
            config = get_config(
                target_path,
                additional_uris=[],
                config_filename=self.config_filename)
        elif config.get_base_path() != target_path:
            raise MultiProjectException(
                "Config path does not match %s %s " % (config.get_base_path(),
                                                       target_path))
        rosinstall_cmd.cmd_generate_ros_files(config,
                                              target_path,
                                              nobuild=True,
                                              rosdep_yes=False,
                                              catkin=options.catkin,
                                              catkinpp=options.catkinpp,
                                              no_ros_allowed=True)
        return 0

    def cmd_info(self, target_path, argv, reverse=True, config=None):
        # similar to multiproject_cli except shows ros-pkg-path
        # options
        parser = OptionParser(
            usage="usage: %s info [localname]* [OPTIONS]" % self.progname,
            formatter=IndentedHelpFormatterWithNL(),
            description=__MULTIPRO_CMD_DICT__["info"] + """

The Status (S) column shows
 x  for missing
 L  for uncommited (local) changes
 V  for difference in version and/or remote URI
 C  for difference in local and remote versions

The 'Version-Spec' column shows what tag, branch or revision was given
in the .rosinstall file. The 'UID' column shows the unique ID of the
current (and specified) version. The 'URI' column shows the configured
URL of the repo.

If status is V, the difference between what was specified and what is
real is shown in the respective column. For SVN entries, the url is
split up according to standard layout (trunk/tags/branches).  The
ROS_PACKAGE_PATH follows the order of the table, earlier entries
overlay later entries.

When given one localname, just show the data of one element in list form.
This also has the generic properties element which is usually empty.

The --only option accepts keywords: %(opts)s

Examples:
$ %(prog)s info -t ~/ros/fuerte
$ %(prog)s info robot_model
$ %(prog)s info --yaml
$ %(prog)s info --only=path,cur_uri,cur_revision robot_model geometry
""" % {'prog': self.progname, 'opts': ONLY_OPTION_VALID_ATTRS},
            epilog="See: http://www.ros.org/wiki/rosinstall for details\n")
        parser.add_option(
            "--root", dest="show_ws_root", default=False,
            help="Show workspace root path",
            action="store_true")
        parser.add_option(
            "--data-only", dest="data_only", default=False,
            help="Does not provide explanations",
            action="store_true")
        parser.add_option(
            "--no-pkg-path", dest="no_pkg_path", default=False,
            help="Suppress ROS_PACKAGE_PATH.",
            action="store_true")
        parser.add_option(
            "--pkg-path-only", dest="pkg_path_only", default=False,
            help="Shows only ROS_PACKAGE_PATH separated by ':'. Supercedes all other options.",
            action="store_true")
        parser.add_option(
            "--only", dest="only", default=False,
            help="Shows comma-separated lists of only given comma-separated attribute(s).",
            action="store")
        parser.add_option(
            "--yaml", dest="yaml", default=False,
            help="Shows only version of single entry. Intended for scripting.",
            action="store_true")
        parser.add_option(
            "--fetch", dest="fetch", default=False,
            help="When used, retrieves version information from remote (takes longer).",
            action="store_true")
        parser.add_option(
            "-u", "--untracked", dest="untracked",
            default=False,
            help="Also show untracked files as modifications",
            action="store_true")
        # -t option required here for help but used one layer above, see cli_common
        parser.add_option(
            "-t", "--target-workspace", dest="workspace", default=None,
            help="which workspace to use",
            action="store")
        parser.add_option(
            "-m", "--managed-only", dest="unmanaged", default=True,
            help="only show managed elements",
            action="store_false")
        (options, args) = parser.parse_args(argv)

        if config is None:
            config = get_config(
                target_path,
                additional_uris=[],
                config_filename=self.config_filename)
        elif config.get_base_path() != target_path:
            raise MultiProjectException("Config path does not match %s %s " %
                                        (config.get_base_path(), target_path))

        if options.show_ws_root:
            print(config.get_base_path())
            return 0

        if args == []:
            args = None

        if options.pkg_path_only:
            print(":".join(get_ros_package_path(config)))
            return 0
        if options.no_pkg_path:
            header = 'workspace: %s\nROS_ROOT: %s' % (target_path,
                                                      get_ros_stack_path(config))
            print(header)
            return 0
        elif options.only:
            only_options = options.only.split(",")
            if only_options == '':
                parser.error('No valid options given')
            lines = get_info_table_raw_csv(config,
                                           parser,
                                           properties=only_options,
                                           localnames=args)
            print('\n'.join(lines))
            return 0
        elif options.yaml:
            source_aggregate = cmd_snapshot(config, localnames=args)
            print(yaml.safe_dump(source_aggregate), end='')
            return 0

        # this call takes long, as it invokes scms.
        outputs = cmd_info(config, localnames=args,
                           untracked=options.untracked,
                           fetch=options.fetch)
        if args and len(args) == 1:
            # if only one element selected, print just one line
            print(get_info_list(config.get_base_path(),
                                outputs[0],
                                options.data_only))
            return 0

        header = 'workspace: %s\nROS_ROOT: %s' % (target_path,
                                                  get_ros_stack_path(config))
        print(header)
        table = get_info_table(config.get_base_path(),
                               outputs,
                               options.data_only,
                               reverse=reverse)
        if table is not None and table != '':
            print("\n%s" % table)

        if options.unmanaged:
            outputs2 = cmd_find_unmanaged_repos(config)
            table2 = get_info_table(config.get_base_path(),
                                   outputs2,
                                   options.data_only,
                                   reverse=reverse,
                                   unmanaged=True)
            if table2 is not None and table2 != '':
                print("\nAlso detected these repositories in the workspace, add using '%s set':\n\n%s" % (self.progname, table2))

        return 0


def rosws_main(argv=None, usage=None):
    """
    Calls the function corresponding to the first argument.

    :param argv: sys.argv by default
    :param usage: function printing usage string, multiproject_cli.list_usage by default
    """
    if argv is None:
        argv = sys.argv
    if (sys.argv[0] == '-c'):
        sys.argv = [_PROGNAME] + sys.argv[1:]
    if '--version' in argv:
        print("%s: \t%s\n%s" %
              (_PROGNAME, rosinstall.__version__.version, cmd_version()))
        sys.exit(0)

    if not usage:
        usage = lambda: print(list_usage(progname=_PROGNAME,
                                         description=__doc__,
                                         command_keys=__ROSWS_CMD_HELP_LIST__,
                                         command_helps=__ROSWS_CMD_DICT__,
                                         command_aliases=__MULTIPRO_CMD_ALIASES__))
    workspace = None
    if len(argv) < 2:
        try:
            workspace = get_workspace(argv,
                                      os.getcwd(),
                                      config_filename=ROSINSTALL_FILENAME,
                                      varname=_VARNAME)
            argv.append('info')
        except MultiProjectException as e:
            print(str(e))
            usage()
            return 0

    if argv[1] in ['--help', '-h']:
        usage()
        return 0

    try:
        command = argv[1]
        args = argv[2:]

        if command == 'help':
            if len(argv) < 3:
                usage()
                return 0

            else:
                command = argv[2]
                args = argv[3:]
                args.insert(0, "--help")
                # help help
                if command == 'help':
                    usage()
                    return 0
        cli = RoswsCLI()

        # commands for which we do not infer target workspace
        commands = {'init': cli.cmd_init}
        # commands which work on a workspace
        ws_commands = {
            'info': cli.cmd_info,
            'remove': cli.cmd_remove,
            'regenerate': cli.cmd_regenerate,
            'set': cli.cmd_set,
            'merge': cli.cmd_merge,
            'foreach': cli.cmd_foreach,
            'scrape': cli.cmd_scrape,
            'diff': cli.cmd_diff,
            'status': cli.cmd_status,
            'update': cli.cmd_update}
        for label in list(ws_commands.keys()):
            alias = __MULTIPRO_CMD_ALIASES__.get(label, None)
            if alias:
                ws_commands[alias] = ws_commands[label]

        if command not in commands and command not in ws_commands:
            if os.path.exists(command):
                args = ['-t', command] + args
                command = 'info'
            else:
                if command.startswith('-'):
                    print("First argument must be name of a command: %s" %
                          command)
                else:
                    print("Error: unknown command: %s" % command)
                usage()
                return 1

        if command in commands:
            return commands[command](args)
        else:
            if workspace is None and not '--help' in args and not '-h' in args:
                workspace = get_workspace(args,
                                          os.getcwd(),
                                          config_filename=ROSINSTALL_FILENAME,
                                          varname=_VARNAME)
            return ws_commands[command](workspace, args)

    except KeyboardInterrupt:
        return 1