せしまるです。
本日はros2 runコマンドをちょっとだけいじった話を書いていきたいと思います。
いじった経緯
仕事内で、あるルールが決められました。
それは、実行ファイルをinstall/pkgname/に移動させるというもの
簡単に書くと、以下のように変更されます。
// 今までのインストール先.
install/pkgname/lib/pkgname/実行ファイル
// install/pkgname下はこんな感じ.
$ ls install/pkgname
lib share
// 変更後のインストール先.
install/pkgname/実行ファイル
// install/pkgname下はこんな感じ.
$ ls install/pkgname
実行ファイル share
変更する点
まず、CMakeLists.txtをいじりました。
具体的に言うとinstall部分の変更です。
// before.
install(TARGETS
xxxx
DESTINATION lib/xxxx
)
↓に変更
// after.
install(TARGETS
xxxx
DESTINATION .
)
これをビルドすると、install/pkgname/に実行ファイルが置かれます。
とりあえず実行してみるかと思い、local_setup.bashを読み込んでからros2 runコマンドを叩きます。
$ source install/local_setup.bash
$ ros2 run xxxx xxxx
No executable found
あれ、実行ファイルが見つからないだと・・・?
ROS2 runの仕様
なんでだろうと思い、とりあえず調べましたが納得のいくものは見つからず。
仕方ないのでros2 runの処理部分を見てみる。
(公式マニュアルの通りにROS2がインストールされていると仮定します)
# Copyright 2017 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from argparse import REMAINDER
import shlex
from ros2cli.command import CommandExtension
from ros2pkg.api import package_name_completer
from ros2pkg.api import PackageNotFound
from ros2run.api import ExecutableNameCompleter
from ros2run.api import get_executable_path
from ros2run.api import MultipleExecutables
from ros2run.api import run_executable
class RunCommand(CommandExtension):
"""Run a package specific executable."""
def add_arguments(self, parser, cli_name):
arg = parser.add_argument(
'--prefix',
help='Prefix command, which should go before the executable. '
'Command must be wrapped in quotes if it contains spaces '
"(e.g. --prefix 'gdb -ex run --args').")
try:
from argcomplete.completers import SuppressCompleter
except ImportError:
pass
else:
arg.completer = SuppressCompleter()
arg = parser.add_argument(
'package_name',
help='Name of the ROS package')
arg.completer = package_name_completer
arg = parser.add_argument(
'executable_name',
help='Name of the executable')
arg.completer = ExecutableNameCompleter(
package_name_key='package_name')
parser.add_argument(
'argv', nargs=REMAINDER,
help='Pass arbitrary arguments to the executable')
def main(self, *, parser, args):
try:
path = get_executable_path(
package_name=args.package_name,
executable_name=args.executable_name)
except PackageNotFound:
raise RuntimeError(
"Package '{args.package_name}' not found"
.format_map(locals()))
except MultipleExecutables as e:
msg = 'Multiple executables found:'
for p in e.paths:
msg += '\n- {p}'.format_map(locals())
raise RuntimeError(msg)
if path is None:
return 'No executable found'
prefix = shlex.split(args.prefix) if args.prefix is not None else None
return run_executable(path=path, argv=args.argv, prefix=prefix)
お、mainの最初のほうでget_executable_pathとかいうの呼んでるじゃん。
これはros2runのapiの方に定義されているみたいなのでそちらも見てみる。
# Copyright 2017 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import subprocess
import sys
from ros2pkg.api import get_executable_paths
from ros2pkg.api import PackageNotFound
class MultipleExecutables(Exception):
def __init__(self, paths):
self.paths = paths
def get_executable_path(*, package_name, executable_name):
paths = get_executable_paths(package_name=package_name)
paths2base = {}
for p in paths:
basename = os.path.basename(p)
if basename == executable_name:
# pick exact match
paths2base[p] = basename
elif sys.platform == 'win32':
# check extensions listed in PATHEXT for match without extension
pathext = os.environ.get('PATHEXT', '').lower().split(os.pathsep)
ext = os.path.splitext(basename)[1].lower()
if ext in pathext and basename[:-len(ext)] == executable_name:
# pick match because of known extension
paths2base[p] = basename
if not paths2base:
return None
if len(paths2base) > 1:
raise MultipleExecutables(paths2base.keys())
return list(paths2base.keys())[0]
def run_executable(*, path, argv, prefix=None):
cmd = [path] + argv
# on Windows Python scripts are invokable through the interpreter
if os.name == 'nt' and path.endswith('.py'):
cmd.insert(0, sys.executable)
if prefix is not None:
cmd = prefix + cmd
process = subprocess.Popen(cmd)
while process.returncode is None:
try:
process.communicate()
except KeyboardInterrupt:
# the subprocess will also receive the signal and should shut down
# therefore we continue here until the process has finished
pass
return process.returncode
class ExecutableNameCompleter:
"""Callable returning a list of executable names within a package."""
def __init__(self, *, package_name_key=None):
self.package_name_key = package_name_key
def __call__(self, prefix, parsed_args, **kwargs):
package_name = getattr(parsed_args, self.package_name_key)
try:
paths = get_executable_paths(package_name=package_name)
except PackageNotFound:
return []
return [os.path.basename(p) for p in paths]
get_executable_pathは見つけたけど、その中でさらにget_executable_pathsを呼んでいる。
get_executable_pathsはros2pkgの方にあるらしい。
# Copyright 2017 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from ament_index_python import get_package_prefix
from ament_index_python import get_packages_with_prefixes
from ament_index_python import PackageNotFoundError
class PackageNotFound(Exception):
def __init__(self, package_name):
self.package_name = package_name
def get_package_names():
return get_packages_with_prefixes().keys()
def get_prefix_path(package_name):
try:
prefix_path = get_package_prefix(package_name)
except PackageNotFoundError:
return None
return prefix_path
def get_executable_paths(*, package_name):
prefix_path = get_prefix_path(package_name)
if prefix_path is None:
raise PackageNotFound(package_name)
base_path = os.path.join(prefix_path, 'lib', package_name)
executable_paths = []
for dirpath, dirnames, filenames in os.walk(base_path):
# ignore folder starting with .
dirnames[:] = [d for d in dirnames if d[0] not in ['.']]
dirnames.sort()
# select executable files
for filename in sorted(filenames):
path = os.path.join(dirpath, filename)
if os.access(path, os.X_OK):
executable_paths.append(path)
return executable_paths
def package_name_completer(**kwargs):
"""Callable returning a list of packages names."""
return get_package_names()
見てみると、44行目にそれらしきものが(os.path.joinのところ)。
というかべた書きでlibって書いてあるしw
ちなみにos.path.joinの引数になっている変数の中身はこんな感じ。
・prefix_path:install/pkgnameまでのパス
・'lib':libという名前のディレクトリ(べた)
・package_name:パッケージ名。これはlib下のパッケージ名を指す
なるほどなるほど。
今の実装だとlib/pkgnameはいらないから、こうすればいいかな。
編集には管理者権限がいるのでsudoしましょう。
# 44行目
base_path = prefix_path
# 一応コメントアウト
#base_path = os.path.join(prefix_path, 'lib', package_name)
変更を保存したら、とりあえず再起動(怖いので)
実行してみる
再起動後、作業ディレクトリに移動し、local_setup.bashを読み込む。
そしてros2 runコマンドを実行・・・の前にちゃんとros2が動いてるか確認してみる。
$ ros2 daemon status
The daemon is not running
動いてはいるな…。
よし、実行だ!
$ ros2 run xxxx xxxx
xxxx start
うごいたー!やったぜ。
余談
怖くなったのでその他のros2関連コマンドを確認しましたが、どれも問題なく動いている感じはします。
ただ、この方法はルールを無視しようという考えのものやってみただけなので、実際にやる場合は自己責任でお願いします(遠い目)
というわけで今回はここまでです。
ありがとうございました。