From a06f6d516ac8d107930cfd3a35c00cb8d8d55313 Mon Sep 17 00:00:00 2001 From: Christopher Osborn Date: Wed, 21 Aug 2019 12:53:14 -0400 Subject: [PATCH] Use Bespoke Tool Path for `dotnet-lambda` Commands There are situations in which the user's installation of "Amazon.Lambda.Tools" can be as good as read-only. This installs the builder's copy of the tool to a known location which is separate from the user's default global installation. Resolves: #126 --- .../workflows/dotnet_clipackage/actions.py | 11 ++++-- .../workflows/dotnet_clipackage/dotnetcli.py | 23 +++++++++++- .../workflows/dotnet_clipackage/workflow.py | 4 +- .../dotnet_clipackage/test_actions.py | 37 +++++++++++-------- 4 files changed, 54 insertions(+), 21 deletions(-) diff --git a/aws_lambda_builders/workflows/dotnet_clipackage/actions.py b/aws_lambda_builders/workflows/dotnet_clipackage/actions.py index 0d67eaed1..ec191e6a3 100644 --- a/aws_lambda_builders/workflows/dotnet_clipackage/actions.py +++ b/aws_lambda_builders/workflows/dotnet_clipackage/actions.py @@ -22,21 +22,22 @@ class GlobalToolInstallAction(BaseAction): DESCRIPTION = "Install or update the Amazon.Lambda.Tools .NET Core Global Tool." PURPOSE = Purpose.COMPILE_SOURCE - def __init__(self, subprocess_dotnet): + def __init__(self, subprocess_dotnet, tool_dir): super(GlobalToolInstallAction, self).__init__() self.subprocess_dotnet = subprocess_dotnet + self.tool_dir = tool_dir def execute(self): try: LOG.debug("Installing Amazon.Lambda.Tools Global Tool") self.subprocess_dotnet.run( - ['tool', 'install', '-g', 'Amazon.Lambda.Tools'], + ['tool', 'install', '--tool-path', self.tool_dir, 'Amazon.Lambda.Tools'], ) except DotnetCLIExecutionError as ex: LOG.debug("Error installing probably due to already installed. Attempt to update to latest version.") try: self.subprocess_dotnet.run( - ['tool', 'update', '-g', 'Amazon.Lambda.Tools'], + ['tool', 'update', '--tool-path', self.tool_dir, 'Amazon.Lambda.Tools'], ) except DotnetCLIExecutionError as ex: raise ActionFailedError("Error configuring the Amazon.Lambda.Tools .NET Core Global Tool: " + str(ex)) @@ -50,11 +51,12 @@ class RunPackageAction(BaseAction): DESCRIPTION = "Execute the `dotnet lambda package` command." PURPOSE = Purpose.COMPILE_SOURCE - def __init__(self, source_dir, subprocess_dotnet, artifacts_dir, options, mode, os_utils=None): + def __init__(self, source_dir, subprocess_dotnet, artifacts_dir, tool_dir, options, mode, os_utils=None): super(RunPackageAction, self).__init__() self.source_dir = source_dir self.subprocess_dotnet = subprocess_dotnet self.artifacts_dir = artifacts_dir + self.tool_dir = tool_dir self.options = options self.mode = mode self.os_utils = os_utils if os_utils else OSUtils() @@ -80,6 +82,7 @@ def execute(self): self.subprocess_dotnet.run( arguments, + tool_dir=self.tool_dir, cwd=self.source_dir ) diff --git a/aws_lambda_builders/workflows/dotnet_clipackage/dotnetcli.py b/aws_lambda_builders/workflows/dotnet_clipackage/dotnetcli.py index 41e2c2d3b..b2a130537 100644 --- a/aws_lambda_builders/workflows/dotnet_clipackage/dotnetcli.py +++ b/aws_lambda_builders/workflows/dotnet_clipackage/dotnetcli.py @@ -4,6 +4,7 @@ import sys import logging +import os from .utils import OSUtils @@ -36,7 +37,7 @@ def __init__(self, dotnet_exe=None, os_utils=None): self.dotnet_exe = dotnet_exe - def run(self, args, cwd=None): + def run(self, args, tool_dir=None, cwd=None): if not isinstance(args, list): raise ValueError('args must be a list') @@ -47,9 +48,17 @@ def run(self, args, cwd=None): LOG.debug("executing dotnet: %s", invoke_dotnet) + if tool_dir is None: + env = None + else: + env = SubprocessDotnetCLI._merge( + os.environ, + PATH=os.pathsep.join([tool_dir, os.environ['PATH']])) + p = self.os_utils.popen(invoke_dotnet, stdout=self.os_utils.pipe, stderr=self.os_utils.pipe, + env=env, cwd=cwd) out, err = p.communicate() @@ -61,3 +70,15 @@ def run(self, args, cwd=None): if p.returncode != 0: raise DotnetCLIExecutionError(message=err.decode('utf8').strip()) + + # The {**x, **y} syntax will not work in Python 2. + @staticmethod + def _merge(left, **right): + """ + Shallowly merges the elements of `right` onto a copy of `left` + and returns that copy. + """ + + output = left.copy() + output.update(right) + return output diff --git a/aws_lambda_builders/workflows/dotnet_clipackage/workflow.py b/aws_lambda_builders/workflows/dotnet_clipackage/workflow.py index 0c92462af..095d16989 100644 --- a/aws_lambda_builders/workflows/dotnet_clipackage/workflow.py +++ b/aws_lambda_builders/workflows/dotnet_clipackage/workflow.py @@ -40,11 +40,13 @@ def __init__(self, options = kwargs["options"] if "options" in kwargs else {} subprocess_dotnetcli = SubprocessDotnetCLI(os_utils=OSUtils()) - dotnetcli_install = GlobalToolInstallAction(subprocess_dotnet=subprocess_dotnetcli) + dotnetcli_install = GlobalToolInstallAction(subprocess_dotnet=subprocess_dotnetcli, + tool_dir=scratch_dir) dotnetcli_deployment = RunPackageAction(source_dir, subprocess_dotnet=subprocess_dotnetcli, artifacts_dir=artifacts_dir, + tool_dir=scratch_dir, options=options, mode=mode) self.actions = [ diff --git a/tests/unit/workflows/dotnet_clipackage/test_actions.py b/tests/unit/workflows/dotnet_clipackage/test_actions.py index 17d4edc59..4738eabb1 100644 --- a/tests/unit/workflows/dotnet_clipackage/test_actions.py +++ b/tests/unit/workflows/dotnet_clipackage/test_actions.py @@ -1,7 +1,7 @@ from unittest import TestCase -from mock import patch import os import platform +from mock import patch from aws_lambda_builders.actions import ActionFailedError from aws_lambda_builders.workflows.dotnet_clipackage.dotnetcli import DotnetCLIExecutionError @@ -13,26 +13,30 @@ class TestGlobalToolInstallAction(TestCase): @patch("aws_lambda_builders.workflows.dotnet_clipackage.dotnetcli.SubprocessDotnetCLI") def setUp(self, MockSubprocessDotnetCLI): self.subprocess_dotnet = MockSubprocessDotnetCLI.return_value + self.scratch_dir = os.path.join('/scratch_dir') def tearDown(self): self.subprocess_dotnet.reset_mock() def test_global_tool_install(self): - action = GlobalToolInstallAction(self.subprocess_dotnet) + action = GlobalToolInstallAction(self.subprocess_dotnet, self.scratch_dir) action.execute() - self.subprocess_dotnet.run.assert_called_once_with(['tool', 'install', '-g', 'Amazon.Lambda.Tools']) + self.subprocess_dotnet.run.assert_called_once_with( + ['tool', 'install', '--tool-path', self.scratch_dir, 'Amazon.Lambda.Tools']) def test_global_tool_update(self): self.subprocess_dotnet.run.side_effect = [DotnetCLIExecutionError(message="Already Installed"), None] - action = GlobalToolInstallAction(self.subprocess_dotnet) + action = GlobalToolInstallAction(self.subprocess_dotnet, self.scratch_dir) action.execute() - self.subprocess_dotnet.run.assert_any_call(['tool', 'install', '-g', 'Amazon.Lambda.Tools']) - self.subprocess_dotnet.run.assert_any_call(['tool', 'update', '-g', 'Amazon.Lambda.Tools']) + self.subprocess_dotnet.run.assert_any_call( + ['tool', 'install', '--tool-path', self.scratch_dir, 'Amazon.Lambda.Tools']) + self.subprocess_dotnet.run.assert_any_call( + ['tool', 'update', '--tool-path', self.scratch_dir, 'Amazon.Lambda.Tools']) def test_global_tool_update_failed(self): self.subprocess_dotnet.run.side_effect = [DotnetCLIExecutionError(message="Already Installed"), DotnetCLIExecutionError(message="Updated Failed")] - action = GlobalToolInstallAction(self.subprocess_dotnet) + action = GlobalToolInstallAction(self.subprocess_dotnet, self.scratch_dir) self.assertRaises(ActionFailedError, action.execute) @@ -54,21 +58,22 @@ def test_build_package(self): mode = "Release" options = {} - action = RunPackageAction(self.source_dir, self.subprocess_dotnet, self.artifacts_dir, options, mode, - self.os_utils) + action = RunPackageAction(self.source_dir, self.subprocess_dotnet, self.artifacts_dir, self.scratch_dir, + options, mode, self.os_utils) action.execute() zipFilePath = os.path.join('/', 'artifacts_dir', 'source_dir.zip') self.subprocess_dotnet.run.assert_called_once_with(['lambda', 'package', '--output-package', zipFilePath], + tool_dir='/scratch_dir', cwd='/source_dir') def test_build_package_arguments(self): mode = "Release" options = {"--framework": "netcoreapp2.1"} - action = RunPackageAction(self.source_dir, self.subprocess_dotnet, self.artifacts_dir, options, mode, - self.os_utils) + action = RunPackageAction(self.source_dir, self.subprocess_dotnet, self.artifacts_dir, self.scratch_dir, + options, mode, self.os_utils) action.execute() @@ -79,6 +84,7 @@ def test_build_package_arguments(self): self.subprocess_dotnet.run.assert_called_once_with(['lambda', 'package', '--output-package', zipFilePath, '--framework', 'netcoreapp2.1'], + tool_dir='/scratch_dir', cwd='/source_dir') def test_build_error(self): @@ -86,16 +92,16 @@ def test_build_error(self): self.subprocess_dotnet.run.side_effect = DotnetCLIExecutionError(message="Failed Package") options = {} - action = RunPackageAction(self.source_dir, self.subprocess_dotnet, self.artifacts_dir, options, mode, - self.os_utils) + action = RunPackageAction(self.source_dir, self.subprocess_dotnet, self.artifacts_dir, self.scratch_dir, + options, mode, self.os_utils) self.assertRaises(ActionFailedError, action.execute) def test_debug_configuration_set(self): mode = "Debug" options = None - action = RunPackageAction(self.source_dir, self.subprocess_dotnet, self.artifacts_dir, options, mode, - self.os_utils) + action = RunPackageAction(self.source_dir, self.subprocess_dotnet, self.artifacts_dir, self.scratch_dir, + options, mode, self.os_utils) zipFilePath = os.path.join('/', 'artifacts_dir', 'source_dir.zip') @@ -103,4 +109,5 @@ def test_debug_configuration_set(self): self.subprocess_dotnet.run.assert_called_once_with( ['lambda', 'package', '--output-package', zipFilePath, '--configuration', 'Debug'], + tool_dir='/scratch_dir', cwd='/source_dir')