Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Windows] add Chocolatey installer for Windows. #662

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/rosdep2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ def create_default_installer_context(verbose=False):
from .platforms import freebsd
from .platforms import slackware
from .platforms import source
from .platforms import windows

platform_mods = [arch, cygwin, debian, gentoo, opensuse, osx, redhat, slackware, freebsd]
platform_mods = [arch, cygwin, debian, gentoo, opensuse, osx, redhat, slackware, freebsd, windows]
installer_mods = [source, pip, gem] + platform_mods

context = InstallerContext()
Expand Down
118 changes: 118 additions & 0 deletions src/rosdep2/platforms/windows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Copyright 2019 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 subprocess
import json
import sys
import traceback

from rospkg.os_detect import OS_WINDOWS, OsDetect

from ..core import InstallFailed, RosdepInternalError, InvalidData
from .pip import PIP_INSTALLER
from .source import SOURCE_INSTALLER
from ..installers import PackageManagerInstaller
from ..shell_utils import read_stdout

CHOCO_INSTALLER = 'chocolatey'


def register_installers(context):
context.set_installer(CHOCO_INSTALLER, ChocolateyInstaller())


def register_platforms(context):
context.add_os_installer_key(OS_WINDOWS, CHOCO_INSTALLER)
context.add_os_installer_key(OS_WINDOWS, PIP_INSTALLER)
context.add_os_installer_key(OS_WINDOWS, SOURCE_INSTALLER)
context.set_default_os_installer_key(OS_WINDOWS, lambda self: CHOCO_INSTALLER)
context.set_os_version_type(OS_WINDOWS, OsDetect.get_codename)


def is_choco_installed():
if "not-found" not in get_choco_version():
return True
else:
return False


def get_choco_version():
try:
p = subprocess.Popen(
['powershell', 'choco'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
# If choco is not installed, we get a PS general failure
if "not recognized" not in stdout:
version = stdout.replace('Chocolatey v', '').strip()
return version
else:
return 'Chocolatey not-found'
except OSError:
return 'Chocolatey not-found'


def choco_detect(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)
"""
ret_list = []
if not is_choco_installed():
return ret_list
if exec_fn is None:
exec_fn = read_stdout
pkg_list = exec_fn(['powershell', 'choco', 'list', '--localonly']).split('\r\n')
pkg_list.pop()
pkg_list.pop()

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


class ChocolateyInstaller(PackageManagerInstaller):

def __init__(self):
super(ChocolateyInstaller, self).__init__(choco_detect, supports_depends=True)

def get_version_strings(self):
version_strings = [
get_choco_version()
]
return version_strings

def get_install_command(self, resolved, interactive=True, reinstall=False, quiet=False):
if not is_choco_installed():
raise InstallFailed((CHOCO_INSTALLER, "Chocolatey is not installed"))
# convenience function that calls outs to our detect function
packages = self.get_packages_to_install(resolved, reinstall=reinstall)
if not packages:
return []

# interactive switch doesn't matter
if reinstall:
commands = []
for p in packages:
# --force uninstalls all versions of that package
commands.append(['powershell', 'choco', 'uninstall', '--force', '-y', p])
commands.append(['powershell', 'choco', 'install', '-y', p])
return commands
else:
return [['powershell', 'choco', 'upgrade', '-y', p] for p in packages]
114 changes: 114 additions & 0 deletions test/test_rosdep_windows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
# Copyright 2019 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 traceback

from mock import call
from mock import Mock
from mock import patch


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


def test_choco_installed():
# don't know the correct answer, but make sure this does not throw
from rosdep2.platforms.windows import is_choco_installed
assert is_choco_installed() in [True, False]


def choco_command(command):
if command[1] == 'list':
with open(os.path.join(get_test_dir(), 'choco-list-output'), 'r') as f:
return f.read()
elif command[1] == 'info':
pkg = command[2]
with open(os.path.join(get_test_dir(), 'choco-info-output'), 'r') as f:
output = f.readlines()
for line in output:
res = line.split(':', 1)
if res[0] == pkg:
return res[1]
return ''


def test_choco_detect():
from rosdep2.platforms.windows import choco_detect

m = Mock()
m.return_value = ''
val = choco_detect([], exec_fn=m)
assert val == [], val

m = Mock()
m.return_value = ''
val = choco_detect(['tinyxml'], exec_fn=m)
assert val == [], val
# make sure our test harness is based on the same implementation
m.assert_called_with(['choco', 'list'])
assert m.call_args_list == [call(['choco', 'list'])], m.call_args_list

m = Mock()
m.side_effect = choco_command
val = choco_detect(['apt', 'subversion', 'python', 'bazaar'], exec_fn=m)
# make sure it preserves order
expected = ['subversion', 'bazaar']
assert set(val) == set(expected), val
assert val == expected, val
assert len(val) == len(set(val)), val


def test_ChocolateyInstaller():
from rosdep2.platforms.windows import ChocolateyInstaller

@patch('rosdep2.platforms.windows.is_choco_installed')
@patch.object(ChocolateyInstaller, 'get_packages_to_install')
def test(mock_get_packages_to_install, mock_choco_installed):
mock_choco_installed.return_value = True

installer = ChocolateyInstaller()
mock_get_packages_to_install.return_value = []
assert [] == installer.get_install_command(['fake'])

mock_get_packages_to_install.return_value = ['subversion', 'bazaar']
expected = [['powershell', 'choco', 'install', 'subversion'],
['powershell', 'choco', 'install', 'bazaar']]
# Chocolatey can be interactive
for interactive in [True, False]:
val = installer.get_install_command(['whatever'], interactive=interactive)
assert val == expected, val

expected = [['powershell', 'choco', 'uninstall', '--force', 'subversion'],
['powershell', 'choco', 'install', 'subversion'],
['powershell', 'choco', 'uninstall', '--force', 'bazaar'],
['powershell', 'choco', 'install', 'bazaar']]
val = installer.get_install_command(['whatever'], reinstall=True)
assert val == expected, val

try:
expected = eval("[['powershell', 'choco', 'install', 'subversion', u'f´´ßß', u'öäö'], ['brew', 'install', 'bazaar', u'tüü']]")
except SyntaxError:
# Python 3.2, u'...' is not allowed, but string literals are unicode
expected = [['powershell', 'choco', 'install', 'subversion', 'f´´ßß', 'öäö'],
['powershell', 'choco', 'install', 'bazaar', 'tüü']]
val = installer.get_install_command(['whatever'])
assert val == expected, val
try:
test()
except AssertionError:
traceback.print_exc()
raise