Skip to content

Commit

Permalink
add npm support (#692)
Browse files Browse the repository at this point in the history
  • Loading branch information
k-okada authored Nov 12, 2020
1 parent ae03ba3 commit 53025ab
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 1 deletion.
3 changes: 2 additions & 1 deletion src/rosdep2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,15 @@ def create_default_installer_context(verbose=False):
from .platforms import opensuse
from .platforms import osx
from .platforms import pip
from .platforms import npm
from .platforms import gem
from .platforms import redhat
from .platforms import freebsd
from .platforms import slackware
from .platforms import source

platform_mods = [alpine, arch, cygwin, debian, gentoo, nix, openembedded, opensuse, osx, redhat, slackware, freebsd]
installer_mods = [source, pip, gem] + platform_mods
installer_mods = [source, pip, gem, npm] + platform_mods

context = InstallerContext()
context.set_verbose(verbose)
Expand Down
3 changes: 3 additions & 0 deletions src/rosdep2/platforms/debian.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
)
from .pip import PIP_INSTALLER
from .gem import GEM_INSTALLER
from .npm import NPM_INSTALLER
from .source import SOURCE_INSTALLER
from ..installers import PackageManagerInstaller
from ..shell_utils import read_stdout
Expand Down Expand Up @@ -72,6 +73,7 @@ def register_debian(context):
context.add_os_installer_key(OS_DEBIAN, APT_INSTALLER)
context.add_os_installer_key(OS_DEBIAN, PIP_INSTALLER)
context.add_os_installer_key(OS_DEBIAN, GEM_INSTALLER)
context.add_os_installer_key(OS_DEBIAN, NPM_INSTALLER)
context.add_os_installer_key(OS_DEBIAN, SOURCE_INSTALLER)
context.set_default_os_installer_key(OS_DEBIAN, lambda self: APT_INSTALLER)
context.set_os_version_type(OS_DEBIAN, OsDetect.get_codename)
Expand Down Expand Up @@ -133,6 +135,7 @@ def register_ubuntu(context):
context.add_os_installer_key(OS_UBUNTU, APT_INSTALLER)
context.add_os_installer_key(OS_UBUNTU, PIP_INSTALLER)
context.add_os_installer_key(OS_UBUNTU, GEM_INSTALLER)
context.add_os_installer_key(OS_UBUNTU, NPM_INSTALLER)
context.add_os_installer_key(OS_UBUNTU, SOURCE_INSTALLER)
context.set_default_os_installer_key(OS_UBUNTU, lambda self: APT_INSTALLER)
context.set_os_version_type(OS_UBUNTU, OsDetect.get_codename)
Expand Down
100 changes: 100 additions & 0 deletions src/rosdep2/platforms/npm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Copyright (c) 2009, Willow Garage, Inc.
# Copyright (c) 2019, Kei Okada
# 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 the 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.

from __future__ import print_function

import os
import subprocess

from ..core import InstallFailed
from ..installers import PackageManagerInstaller
from ..shell_utils import read_stdout

# npm package manager key
NPM_INSTALLER = 'npm'


def register_installers(context):
context.set_installer(NPM_INSTALLER, NpmInstaller())


def is_npm_installed():
try:
subprocess.Popen(['npm'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
return True
except OSError:
return False


class NpmInstaller(PackageManagerInstaller):
"""
:class:`Installer` support for npm.
"""

def __init__(self):
super(NpmInstaller, self).__init__(self.npm_detect, supports_depends=True)

def npm_detect(self, pkgs, exec_fn=None):
"""
Given a list of package, return the list of installed packages.
:param exec_fn: function to execute Popen and read stdout (for testing)
"""
if exec_fn is None:
exec_fn = read_stdout

# npm list -parseable returns [dir, dir/node_modules/path, dir/node_modules/path, ...]
if self.as_root:
cmd = ['npm', 'list', '-g']
else:
cmd = ['npm', 'list']
pkg_list = exec_fn(cmd + ['-parseable']).split('\n')

ret_list = []
for pkg in pkg_list[1:]:
pkg_row = pkg.split('/')
if pkg_row[-1] in pkgs:
ret_list.append(pkg_row[-1])
return ret_list

def get_version_strings(self):
npm_version = subprocess.check_output(['npm', '--version']).strip()
return ['npm {}'.format(npm_version)]

def get_install_command(self, resolved, interactive=True, reinstall=False, quiet=False):
if not is_npm_installed():
raise InstallFailed((NPM_INSTALLER, 'npm is not installed'))
packages = self.get_packages_to_install(resolved, reinstall=reinstall)
if not packages:
return []
if self.as_root:
cmd = ['npm', 'install', '-g']
else:
cmd = ['npm', 'install']

return [self.elevate_priv(cmd + [p]) for p in packages]
38 changes: 38 additions & 0 deletions test/npm/list_output
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/home/ws/rosdep/test
/home/ws/rosdep/test/node_modules/rosnodejs
/home/ws/rosdep/test/node_modules/argparse
/home/ws/rosdep/test/node_modules/sprintf-js
/home/ws/rosdep/test/node_modules/async
/home/ws/rosdep/test/node_modules/lodash
/home/ws/rosdep/test/node_modules/bn.js
/home/ws/rosdep/test/node_modules/bunyan
/home/ws/rosdep/test/node_modules/dtrace-provider
/home/ws/rosdep/test/node_modules/nan
/home/ws/rosdep/test/node_modules/mv
/home/ws/rosdep/test/node_modules/mkdirp
/home/ws/rosdep/test/node_modules/minimist
/home/ws/rosdep/test/node_modules/ncp
/home/ws/rosdep/test/node_modules/rimraf
/home/ws/rosdep/test/node_modules/glob
/home/ws/rosdep/test/node_modules/inflight
/home/ws/rosdep/test/node_modules/wrappy
/home/ws/rosdep/test/node_modules/inherits
/home/ws/rosdep/test/node_modules/minimatch
/home/ws/rosdep/test/node_modules/brace-expansion
/home/ws/rosdep/test/node_modules/balanced-match
/home/ws/rosdep/test/node_modules/concat-map
/home/ws/rosdep/test/node_modules/once
/home/ws/rosdep/test/node_modules/path-is-absolute
/home/ws/rosdep/test/node_modules/safe-json-stringify
/home/ws/rosdep/test/node_modules/md5
/home/ws/rosdep/test/node_modules/charenc
/home/ws/rosdep/test/node_modules/crypt
/home/ws/rosdep/test/node_modules/is-buffer
/home/ws/rosdep/test/node_modules/moment
/home/ws/rosdep/test/node_modules/ultron
/home/ws/rosdep/test/node_modules/walker
/home/ws/rosdep/test/node_modules/makeerror
/home/ws/rosdep/test/node_modules/tmpl
/home/ws/rosdep/test/node_modules/xmlrpc
/home/ws/rosdep/test/node_modules/sax
/home/ws/rosdep/test/node_modules/xmlbuilder
122 changes: 122 additions & 0 deletions test/test_rosdep_npm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Copyright (c) 2011, Willow Garage, Inc.
# Copyright (c) 2019, Kei Okada
# 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 the 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.

# Author Ken Conley/[email protected]
# Author Kei Okada/[email protected]

import os
import traceback
from mock import Mock, patch


def get_test_dir():
return os.path.abspath(os.path.join(os.path.dirname(__file__), 'npm'))


def test_npm_detect():
from rosdep2.platforms.npm import NpmInstaller
installer = NpmInstaller()
npm_detect = installer.npm_detect

m = Mock()

# test behavior with empty freeze
m.return_value = ''
val = npm_detect([], exec_fn=m)
assert val == [], val

val = npm_detect(['rosnodejs'], exec_fn=m)
assert val == [], val

# read list output into mock exec_fn
with open(os.path.join(get_test_dir(), 'list_output'), 'r') as f:
m.return_value = f.read()
val = npm_detect(['rosnodejs'], exec_fn=m)
assert val == ['rosnodejs'], val

val = npm_detect(['argparse', 'fakito', 'rosnodejs'], exec_fn=m)
assert val == ['rosnodejs', 'argparse'], val


def test_NpmInstaller_get_depends():
# make sure NpmInstaller supports depends
from rosdep2.platforms.npm import NpmInstaller
installer = NpmInstaller()
assert ['foo'] == installer.get_depends(dict(depends=['foo']))


def test_NpmInstaller():
from rosdep2 import InstallFailed
from rosdep2.platforms.npm import NpmInstaller

@patch('rosdep2.platforms.npm.is_npm_installed')
def test_no_npm(mock_method):
mock_method.return_value = False
try:
installer = NpmInstaller()
installer.get_install_command(['whatever'])
assert False, 'should have raised'
except InstallFailed:
pass

test_no_npm()

@patch('rosdep2.platforms.npm.is_npm_installed')
@patch.object(NpmInstaller, 'get_packages_to_install')
def test(mock_method, mock_is_npm_installed):
mock_is_npm_installed.return_value = True
installer = NpmInstaller()
mock_method.return_value = []
assert [] == installer.get_install_command(['fake'])

# no interactive option with NPM
mock_method.return_value = ['a', 'b']
expected = [['sudo', '-H', 'npm', 'install', '-g', 'a'],
['sudo', '-H', 'npm', 'install', '-g', 'b']]
val = installer.get_install_command(['whatever'], interactive=False)
assert val == expected, val
expected = [['sudo', '-H', 'npm', 'install', '-g', 'a'],
['sudo', '-H', 'npm', 'install', '-g', 'b']]
val = installer.get_install_command(['whatever'], interactive=True)
assert val == expected, val

# unset as_root option with NPM
installer.as_root = False
expected = [['npm', 'install', 'a'],
['npm', 'install', 'b']]
val = installer.get_install_command(['whatever'], interactive=False)
assert val == expected, val
expected = [['npm', 'install', 'a'],
['npm', 'install', 'b']]
val = installer.get_install_command(['whatever'], interactive=True)
assert val == expected, val
try:
test()
except AssertionError:
traceback.print_exc()
raise

0 comments on commit 53025ab

Please sign in to comment.