Commit ac95d997 authored by Léo-Paul Géneau's avatar Léo-Paul Géneau 👾

adds deployment mode

parent 9d09fb7f
......@@ -44,6 +44,10 @@ version
ruby-executable
A path to a Ruby executable. Gems will be installed using this executable.
deployment
If set to ``true``, the version of each gem dependency must be provided in
``gems`` option. Default value is ``false``.
gem-options
Extra options, that will be passed to gem executable. Example::
......
......@@ -13,6 +13,7 @@ import zc.buildout
import six.moves.urllib as urllib
from distutils.util import strtobool
from slapos.recipe.downloadunpacked import Recipe as Download
strip = lambda x:x.strip() # noqa
......@@ -32,16 +33,22 @@ class Recipe(object):
self.name,
)
if 'gems' not in options:
self.log.error("Missing 'gems' option.")
raise zc.buildout.UserError('Configuration error')
self.gems = options.get('gems')
if self.gems:
self.gems = self.gems.split()
else:
raise zc.buildout.UserError(
"Configuration error, 'gems' option is missing")
self.gems = options['gems'].split()
self.version = options.get('version')
self.url = options.get('url')
# Allow to define specific ruby executable. If not, take just 'ruby'
self.ruby_executable = options.get('ruby-executable', 'ruby')
deployment = options.get('deployment', 'false')
self.deployment = bool(strtobool(deployment))
self.gem_regex = re.compile(r"\s+([\w\-_.]+) \((<|~>|>=) (\d+\.)*\d+(, >= (\d+\.)*\d+)?\)")
def run(self, cmd, environ=None):
"""Run the given ``cmd`` in a child process."""
env = os.environ.copy()
......@@ -49,22 +56,19 @@ class Recipe(object):
env.update(environ)
try:
subprocess.check_output(cmd, env=env)
cmd_result = subprocess.check_output(cmd, env=env).decode()
except OSError as e:
self.log.error('Command failed: %s: %s' % (e, cmd))
self.log.error('Command failed: %s: %s', e, cmd)
raise zc.buildout.UserError('System error')
except subprocess.CalledProcessError as e:
self.log.error(e.output)
if e.returncode < 0:
self.log.error('Command received signal %s: %s' % (
-e.returncode, e.cmd
))
self.log.error('Command received signal %s: %s', -e.returncode, e.cmd)
raise zc.buildout.UserError('System error')
elif e.returncode > 0:
self.log.error('Command failed with exit code %s: %s' % (
e.returncode, e.cmd
))
self.log.error('Command failed with exit code %s: %s', e.returncode, e.cmd)
raise zc.buildout.UserError('System error')
return cmd_result
def update(self):
pass
......@@ -107,6 +111,13 @@ class Recipe(object):
env.update({k: (v % env) for k, v in env_override})
return env
def _get_gem_name_and_version(self, gem_str):
if not '==' in gem_str:
raise zc.buildout.UserError(
"Configuration error, version for %s gem is missing" % gem_str)
else:
return map(strip, gem_str.split('==', 1))
def _get_latest_rubygems(self):
if self.url:
version = self.version
......@@ -153,9 +164,8 @@ class Recipe(object):
if e.errno == errno.EEXIST:
pass
else:
self.log.error((
"IO error while creating '%s' directory."
) % self.options['location'])
self.log.error("IO error while creating '%s' directory.",
self.options['location'])
raise zc.buildout.UserError('Configuration error')
os.chdir(srcdir)
......@@ -167,7 +177,7 @@ class Recipe(object):
self.ruby_executable,
'setup.rb',
'all',
'--prefix=%s' % self.options['location'],
'--prefix=' + self.options['location'],
'--no-rdoc',
'--no-ri',
]
......@@ -204,14 +214,17 @@ class Recipe(object):
gem_executable,
'install',
'--no-document',
'--bindir=%s' % bindir,
'--bindir=' + bindir,
]
if '==' in gemname:
gemname, version = map(strip, gemname.split('==', 1))
if self.deployment:
cmd.append('--ignore-dependencies')
try:
gemname, version = self._get_gem_name_and_version(gemname)
cmd.append(gemname)
cmd.append('--version=%s' % version)
else:
cmd.append('--version=' + version)
except zc.buildout.UserError: # No version provided
cmd.append(gemname)
extra = self.options.get('gem-options', '')
......@@ -222,6 +235,23 @@ class Recipe(object):
self.run(cmd, self._get_env())
def check_pinned_dependencies(self, gem_str, gem_executable,
pinned_gem_version_dict, checked_dependency_list):
gemname, version = self._get_gem_name_and_version(gem_str)
if gemname not in checked_dependency_list:
pinned_gem_list = pinned_gem_version_dict.keys()
for dependency in self.get_dependency_list(gemname, version, gem_executable):
if not dependency in pinned_gem_list:
raise zc.buildout.UserError(
"Configuration error, version for dependency %s is missing" % dependency)
dependency_str = dependency + '==' + pinned_gem_version_dict[dependency]
checked_dependency_list = self.check_pinned_dependencies(
dependency_str, gem_executable,
pinned_gem_version_dict,
checked_dependency_list)
checked_dependency_list.append(gemname)
return checked_dependency_list
def get_gem_executable(self, bindir):
gem_executable = os.path.join(bindir, 'gem')
gem_executable = glob.glob(gem_executable + '*')
......@@ -229,6 +259,22 @@ class Recipe(object):
if gem_executable:
return gem_executable[0]
def get_dependency_list(self, gemname, version, gem_executable):
cmd = [
self.ruby_executable,
gem_executable,
'dependency',
'-rv',
version,
'/^' + gemname + '$/',
]
cmd_result = self.run(cmd, self._get_env())
dependency_list = []
for match in self.gem_regex.findall(cmd_result):
dependency_list.append(match[0])
return dependency_list
def install(self):
parts = [self.options['location']]
......@@ -239,8 +285,19 @@ class Recipe(object):
self._install_rubygems()
gem_executable = self.get_gem_executable(bindir)
if self.deployment:
pinned_gem_version_dict = dict(map(self._get_gem_name_and_version,
self.gems))
checked_dependency_list = []
for gemname in self.gems:
self.log.info('installing ruby gem "%s"' % gemname)
if self.deployment:
checked_dependency_list = self.check_pinned_dependencies(
gemname, gem_executable,
pinned_gem_version_dict,
checked_dependency_list)
self.log.info('installing ruby gem "%s"', gemname)
self._install_gem(gemname, gem_executable, bindir)
for executable in os.listdir(bindir):
......
......@@ -66,8 +66,8 @@ class fixture(object):
self.makedirs((
'bin',
'ruby-%s' % version,
'rubygems-%s' % version,
'ruby-' + version,
'rubygems-' + version,
'rubygems/bin',
))
......@@ -87,11 +87,19 @@ class fixture(object):
class RubyGemsTests(unittest.TestCase):
@fixture({'recipe': {'gems': 'sass'}})
def test_success(self, path, patches, buildout, name, options):
recipe = rubygems.Recipe(buildout, name, options)
recipe.install()
def check_output_test(self, check_output_mock, expected_arg_list_list):
self.assertEqual(check_output_mock.call_count,
len(expected_arg_list_list))
for command_nb, expected_arg_list in enumerate(expected_arg_list_list):
# half of the mock calls come from the use of 'decode' method
self.assertEqual(check_output_mock.mock_calls[2*command_nb][1][0],
expected_arg_list)
self.assertEqual(str(check_output_mock.mock_calls[2*command_nb+1]),
'call().decode()')
def install_with_default_rubygems_test(self, path, patches,
expected_install_arg_list_list):
# One urlopen call to get latest version
self.assertEqual(patches['urlopen'].call_count, 1)
......@@ -107,21 +115,27 @@ class RubyGemsTests(unittest.TestCase):
'destination': str(path / 'rubygems-2.0.0'),
})
# Two check_output calls to install rubygems and specified gem
self.assertEqual(patches['check_output'].call_count, 2)
args = patches['check_output'].mock_calls[0][1]
self.assertEqual(args[0], [
expected_install_arg_list_list.insert(0, [
'ruby', 'setup.rb', 'all', '--prefix=%s/rubygems' % path,
'--no-rdoc', '--no-ri',
])
self.check_output_test(patches['check_output'],
expected_install_arg_list_list)
args = patches['check_output'].mock_calls[1][1]
self.assertEqual(args[0], [
'ruby', None, 'install', '--no-document',
'--bindir=%s/rubygems/bin' % path,
'sass', '--'
])
@fixture({'recipe': {'gems': 'sass'}})
def test_success(self, path, patches, buildout, name, options):
recipe = rubygems.Recipe(buildout, name, options)
recipe.install()
expected_install_arg_list_list = [
[
'ruby', None, 'install', '--no-document',
'--bindir=%s/rubygems/bin' % path,
'sass', '--',
],
]
self.install_with_default_rubygems_test(path, patches,
expected_install_arg_list_list)
@fixture({'recipe': {}})
def test_missing_gems(self, path, patches, buildout, name, options):
......@@ -204,3 +218,40 @@ class RubyGemsTests(unittest.TestCase):
if matched:
self.assertEqual(matched.group(), '--version=1.0')
break
@fixture({'recipe': {'gems': 'sass', 'deployment': 'true'}})
def test_deployment_not_pinned_version_error(
self, path, patches, buildout, name, options):
recipe = rubygems.Recipe(buildout, name, options)
self.assertRaises(zc.buildout.UserError, recipe.install)
@fixture({'recipe': {'gems': 'rash==0.1.0', 'deployment': 'true'}})
@mock.patch('rubygems.Recipe.get_dependency_list')
def test_deployment_not_pinned_dependency_error(
self, path, patches, buildout, name, options, get_dependency_list):
get_dependency_list.side_effect = [['hashie'], []]
recipe = rubygems.Recipe(buildout, name, options)
self.assertRaises(zc.buildout.UserError, recipe.install)
@fixture({'recipe': {'gems': 'rash==0.1.0 hashie==0.3.1', 'deployment': 'true'}})
@mock.patch('rubygems.Recipe.get_dependency_list')
def test_deployment_pinned_dependency(
self, path, patches, buildout, name, options, get_dependency_list):
get_dependency_list.side_effect = [['hashie'], []]
recipe = rubygems.Recipe(buildout, name, options)
recipe.install()
expected_install_arg_list_list = [
[
'ruby', None, 'install', '--no-document',
'--bindir=%s/rubygems/bin' % path,
'--ignore-dependencies', 'rash', '--version=0.1.0', '--',
],
[
'ruby', None, 'install', '--no-document',
'--bindir=%s/rubygems/bin' % path,
'--ignore-dependencies', 'hashie', '--version=0.3.1', '--',
],
]
self.install_with_default_rubygems_test(path, patches,
expected_install_arg_list_list)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment