Skip to content

Commit c17b01a

Browse files
Merge branch 'develop'
2 parents 404ee7b + 8bc62be commit c17b01a

File tree

13 files changed

+157
-42
lines changed

13 files changed

+157
-42
lines changed

.github/workflows/pr-labeler.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
pull-requests: write
1818
runs-on: ubuntu-latest
1919
steps:
20-
- uses: actions/github-script@v6
20+
- uses: actions/github-script@v7
2121
with:
2222
github-token: ${{secrets.GITHUB_TOKEN}}
2323
script: |

aws_lambda_builders/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
# Changing version will trigger a new release!
66
# Please make the version change as the last step of your development.
77

8-
__version__ = "1.42.0"
8+
__version__ = "1.43.0"
99
RPC_PROTOCOL_VERSION = "0.3"

aws_lambda_builders/actions.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class _ActionMetaClass(type):
5757
def __new__(mcs, name, bases, class_dict):
5858
cls = type.__new__(mcs, name, bases, class_dict)
5959

60-
if cls.__name__ == "BaseAction":
60+
if cls.__name__ in ["BaseAction", "NodejsNpmInstallOrUpdateBaseAction"]:
6161
return cls
6262

6363
# Validate class variables
@@ -156,7 +156,12 @@ def __init__(self, source: Union[str, os.PathLike], dest: Union[str, os.PathLike
156156
self._dest = dest
157157

158158
def execute(self):
159+
source_path = Path(self._source)
159160
destination_path = Path(self._dest)
161+
if not source_path.exists():
162+
# Source path doesn't exist, nothing to symlink
163+
LOG.debug("Source path %s does not exist, skipping generating symlink", source_path)
164+
return
160165
if not destination_path.exists():
161166
os.makedirs(destination_path.parent, exist_ok=True)
162167
utils.create_symlink_or_copy(str(self._source), str(destination_path))

aws_lambda_builders/utils.py

+4
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,10 @@ def create_symlink_or_copy(source: str, destination: str) -> None:
198198
"""Tries to create symlink, if it fails it will copy source into destination"""
199199
LOG.debug("Creating symlink; source: %s, destination: %s", source, destination)
200200
try:
201+
if Path(destination).exists() and Path(destination).is_symlink():
202+
# The symlink is already in place, don't try re-creating it
203+
LOG.debug("Symlink between %s and %s already exists, skipping generating symlink", source, destination)
204+
return
201205
os.symlink(Path(source).absolute(), Path(destination).absolute())
202206
except OSError as ex:
203207
LOG.warning(

aws_lambda_builders/workflows/nodejs_npm/actions.py

+44-12
Original file line numberDiff line numberDiff line change
@@ -72,32 +72,35 @@ def execute(self):
7272
raise ActionFailedError(str(ex))
7373

7474

75-
class NodejsNpmInstallAction(BaseAction):
76-
75+
class NodejsNpmInstallOrUpdateBaseAction(BaseAction):
7776
"""
78-
A Lambda Builder Action that installs NPM project dependencies
77+
A base Lambda Builder Action that is used for installs or updating NPM project dependencies
7978
"""
8079

81-
NAME = "NpmInstall"
82-
DESCRIPTION = "Installing dependencies from NPM"
8380
PURPOSE = Purpose.RESOLVE_DEPENDENCIES
8481

85-
def __init__(self, install_dir: str, subprocess_npm: SubprocessNpm, install_links: Optional[bool] = False):
82+
def __init__(self, install_dir: str, subprocess_npm: SubprocessNpm):
8683
"""
8784
Parameters
8885
----------
8986
install_dir : str
9087
Dependencies will be installed in this directory.
9188
subprocess_npm : SubprocessNpm
9289
An instance of the NPM process wrapper
93-
install_links : Optional[bool]
94-
Uses the --install-links npm option if True, by default False
9590
"""
9691

97-
super(NodejsNpmInstallAction, self).__init__()
92+
super().__init__()
9893
self.install_dir = install_dir
9994
self.subprocess_npm = subprocess_npm
100-
self.install_links = install_links
95+
96+
97+
class NodejsNpmInstallAction(NodejsNpmInstallOrUpdateBaseAction):
98+
"""
99+
A Lambda Builder Action that installs NPM project dependencies
100+
"""
101+
102+
NAME = "NpmInstall"
103+
DESCRIPTION = "Installing dependencies from NPM"
101104

102105
def execute(self):
103106
"""
@@ -109,9 +112,38 @@ def execute(self):
109112
LOG.debug("NODEJS installing in: %s", self.install_dir)
110113

111114
command = ["install", "-q", "--no-audit", "--no-save", "--unsafe-perm", "--production"]
112-
if self.install_links:
113-
command.append("--install-links")
115+
self.subprocess_npm.run(command, cwd=self.install_dir)
116+
117+
except NpmExecutionError as ex:
118+
raise ActionFailedError(str(ex))
119+
120+
121+
class NodejsNpmUpdateAction(NodejsNpmInstallOrUpdateBaseAction):
122+
"""
123+
A Lambda Builder Action that installs NPM project dependencies
124+
"""
114125

126+
NAME = "NpmUpdate"
127+
DESCRIPTION = "Updating dependencies from NPM"
128+
129+
def execute(self):
130+
"""
131+
Runs the action.
132+
133+
:raises lambda_builders.actions.ActionFailedError: when NPM execution fails
134+
"""
135+
try:
136+
LOG.debug("NODEJS updating in: %s", self.install_dir)
137+
138+
command = [
139+
"update",
140+
"--no-audit",
141+
"--no-save",
142+
"--unsafe-perm",
143+
"--production",
144+
"--no-package-lock",
145+
"--install-links",
146+
]
115147
self.subprocess_npm.run(command, cwd=self.install_dir)
116148

117149
except NpmExecutionError as ex:

aws_lambda_builders/workflows/nodejs_npm/workflow.py

+10-8
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
NodejsNpmPackAction,
2323
NodejsNpmrcAndLockfileCopyAction,
2424
NodejsNpmrcCleanUpAction,
25+
NodejsNpmUpdateAction,
2526
)
2627
from aws_lambda_builders.workflows.nodejs_npm.npm import SubprocessNpm
2728
from aws_lambda_builders.workflows.nodejs_npm.utils import OSUtils
@@ -119,7 +120,7 @@ def __init__(self, source_dir, artifacts_dir, scratch_dir, manifest_path, runtim
119120
subprocess_npm=subprocess_npm,
120121
osutils=osutils,
121122
build_options=self.options,
122-
install_links=is_building_in_source,
123+
is_building_in_source=is_building_in_source,
123124
)
124125
)
125126

@@ -211,7 +212,7 @@ def get_install_action(
211212
subprocess_npm: SubprocessNpm,
212213
osutils: OSUtils,
213214
build_options: Optional[dict],
214-
install_links: Optional[bool] = False,
215+
is_building_in_source: Optional[bool] = False,
215216
):
216217
"""
217218
Get the install action used to install dependencies.
@@ -228,8 +229,8 @@ def get_install_action(
228229
An instance of OS Utilities for file manipulation
229230
build_options : Optional[dict]
230231
Object containing build options configurations
231-
install_links : Optional[bool]
232-
Uses the --install-links npm option if True, by default False
232+
is_building_in_source : Optional[bool]
233+
States whether --build-in-source flag is set or not
233234
234235
Returns
235236
-------
@@ -245,12 +246,13 @@ def get_install_action(
245246

246247
if (osutils.file_exists(lockfile_path) or osutils.file_exists(shrinkwrap_path)) and npm_ci_option:
247248
return NodejsNpmCIAction(
248-
install_dir=install_dir, subprocess_npm=subprocess_npm, install_links=install_links
249+
install_dir=install_dir, subprocess_npm=subprocess_npm, install_links=is_building_in_source
249250
)
250251

251-
return NodejsNpmInstallAction(
252-
install_dir=install_dir, subprocess_npm=subprocess_npm, install_links=install_links
253-
)
252+
if is_building_in_source:
253+
return NodejsNpmUpdateAction(install_dir=install_dir, subprocess_npm=subprocess_npm)
254+
255+
return NodejsNpmInstallAction(install_dir=install_dir, subprocess_npm=subprocess_npm)
254256

255257
@staticmethod
256258
def can_use_install_links(npm_process: SubprocessNpm) -> bool:

aws_lambda_builders/workflows/nodejs_npm_esbuild/workflow.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def __init__(self, source_dir, artifacts_dir, scratch_dir, manifest_path, runtim
118118
subprocess_npm=self.subprocess_npm,
119119
osutils=self.osutils,
120120
build_options=self.options,
121-
install_links=is_building_in_source,
121+
is_building_in_source=is_building_in_source,
122122
)
123123
)
124124

requirements/dev.txt

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ pyelftools~=0.30 # Used to verify the generated Go binary architecture in integr
1313

1414
# formatter
1515
black==22.6.0; python_version < "3.8"
16-
black==23.10.1; python_version >= "3.8"
17-
ruff==0.1.4
16+
black==23.11.0; python_version >= "3.8"
17+
ruff==0.1.5

tests/integration/workflows/nodejs_npm/test_nodejs_npm.py

+40
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,46 @@ def test_build_in_source_with_download_dependencies(self, runtime):
315315
output_files = set(os.listdir(self.artifacts_dir))
316316
self.assertEqual(expected_files, output_files)
317317

318+
@parameterized.expand([("nodejs12.x",), ("nodejs14.x",), ("nodejs16.x",), ("nodejs18.x",), ("nodejs20.x",)])
319+
def test_build_in_source_with_removed_dependencies(self, runtime):
320+
# run a build with default requirements and confirm dependencies are downloaded
321+
source_dir = os.path.join(self.temp_testdata_dir, "npm-deps")
322+
323+
self.builder.build(
324+
source_dir,
325+
self.artifacts_dir,
326+
self.scratch_dir,
327+
os.path.join(source_dir, "package.json"),
328+
runtime=runtime,
329+
build_in_source=True,
330+
)
331+
332+
# dependencies installed in source folder
333+
source_node_modules = os.path.join(source_dir, "node_modules")
334+
self.assertTrue(os.path.isdir(source_node_modules))
335+
expected_node_modules_contents = {"minimal-request-promise", ".package-lock.json"}
336+
self.assertEqual(set(os.listdir(source_node_modules)), expected_node_modules_contents)
337+
338+
# update package.json with empty one and re-run the build then confirm node_modules are cleared up
339+
shutil.copy2(
340+
os.path.join(self.temp_testdata_dir, "no-deps", "package.json"),
341+
os.path.join(self.temp_testdata_dir, "npm-deps", "package.json"),
342+
)
343+
344+
self.builder.build(
345+
source_dir,
346+
self.artifacts_dir,
347+
self.scratch_dir,
348+
os.path.join(source_dir, "package.json"),
349+
runtime=runtime,
350+
build_in_source=True,
351+
)
352+
# dependencies installed in source folder
353+
source_node_modules = os.path.join(source_dir, "node_modules")
354+
self.assertTrue(os.path.isdir(source_node_modules))
355+
self.assertIn(".package-lock.json", set(os.listdir(source_node_modules)))
356+
self.assertNotIn("minimal-request-promise", set(os.listdir(source_node_modules)))
357+
318358
@parameterized.expand([("nodejs12.x",), ("nodejs14.x",), ("nodejs16.x",), ("nodejs18.x",), ("nodejs20.x",)])
319359
def test_build_in_source_with_download_dependencies_local_dependency(self, runtime):
320360
source_dir = os.path.join(self.temp_testdata_dir, "with-local-dependency")

tests/unit/test_actions.py

+13
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
MoveDependenciesAction,
1313
CleanUpAction,
1414
DependencyManager,
15+
LinkSinglePathAction,
1516
)
1617

1718

@@ -262,3 +263,15 @@ def test_excludes_dependencies_from_source(
262263
@staticmethod
263264
def _convert_strings_to_paths(source_dest_list):
264265
return map(lambda item: (Path(item[0]), Path(item[1])), source_dest_list)
266+
267+
268+
class TestLinkSinglePathAction(TestCase):
269+
@patch("aws_lambda_builders.actions.os.makedirs")
270+
@patch("aws_lambda_builders.utils.create_symlink_or_copy")
271+
def test_skips_non_existent_source(self, mock_create_symlink_or_copy, mock_makedirs):
272+
src = "src/path"
273+
dest = "dest/path"
274+
275+
LinkSinglePathAction(source=src, dest=dest).execute()
276+
mock_create_symlink_or_copy.assert_not_called()
277+
mock_makedirs.assert_not_called()

tests/unit/test_utils.py

+21-7
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
import platform
2+
from pathlib import Path
23

34
from unittest import TestCase
4-
from unittest.mock import patch
5+
from unittest.mock import patch, Mock, MagicMock
56

67
from aws_lambda_builders import utils
78
from aws_lambda_builders.utils import decode
89

910

1011
class Test_create_symlink_or_copy(TestCase):
11-
@patch("aws_lambda_builders.utils.Path")
1212
@patch("aws_lambda_builders.utils.os")
1313
@patch("aws_lambda_builders.utils.copytree")
14-
def test_must_create_symlink_with_absolute_path(self, patched_copy_tree, pathced_os, patched_path):
14+
def test_must_create_symlink_with_absolute_path(self, patched_copy_tree, patched_os):
1515
source_path = "source/path"
1616
destination_path = "destination/path"
17-
utils.create_symlink_or_copy(source_path, destination_path)
1817

19-
pathced_os.symlink.assert_called_with(
20-
patched_path(source_path).absolute(), patched_path(destination_path).absolute()
21-
)
18+
p = MagicMock()
19+
p.return_value = False
20+
21+
with patch("aws_lambda_builders.utils.Path.is_symlink", p):
22+
utils.create_symlink_or_copy(source_path, destination_path)
23+
24+
patched_os.symlink.assert_called_with(Path(source_path).absolute(), Path(destination_path).absolute())
2225
patched_copy_tree.assert_not_called()
2326

2427
@patch("aws_lambda_builders.utils.Path")
@@ -34,6 +37,17 @@ def test_must_copy_if_symlink_fails(self, patched_copy_tree, pathced_os, patched
3437
pathced_os.symlink.assert_called_once()
3538
patched_copy_tree.assert_called_with(source_path, destination_path)
3639

40+
@patch("aws_lambda_builders.utils.Path")
41+
@patch("aws_lambda_builders.utils.os")
42+
@patch("aws_lambda_builders.utils.copytree")
43+
def test_must_copy_if_symlink_fails(self, patched_copy_tree, pathced_os, patched_path):
44+
source_path = "source/path"
45+
destination_path = "destination/path"
46+
utils.create_symlink_or_copy(source_path, destination_path)
47+
48+
pathced_os.symlink.assert_not_called()
49+
patched_copy_tree.assert_not_called()
50+
3751

3852
class TestDecode(TestCase):
3953
def test_does_not_crash_non_utf8_encoding(self):

tests/unit/workflows/nodejs_npm/test_workflow.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
NodejsNpmrcCleanUpAction,
2121
NodejsNpmLockFileCleanUpAction,
2222
NodejsNpmCIAction,
23+
NodejsNpmUpdateAction,
2324
)
2425

2526

@@ -107,7 +108,7 @@ def test_workflow_sets_up_npm_actions_with_download_dependencies_without_depende
107108
self.assertIsInstance(workflow.actions[3], CopySourceAction)
108109
self.assertEqual(workflow.actions[3].source_dir, "source")
109110
self.assertEqual(workflow.actions[3].dest_dir, "artifacts")
110-
self.assertIsInstance(workflow.actions[4], NodejsNpmInstallAction)
111+
self.assertIsInstance(workflow.actions[4], NodejsNpmUpdateAction)
111112
self.assertEqual(workflow.actions[4].install_dir, "not_source")
112113
self.assertIsInstance(workflow.actions[5], LinkSinglePathAction)
113114
self.assertEqual(workflow.actions[5]._source, os.path.join("not_source", "node_modules"))
@@ -331,7 +332,7 @@ def test_build_in_source_with_download_dependencies(self, can_use_links_mock):
331332
self.assertIsInstance(workflow.actions[0], NodejsNpmPackAction)
332333
self.assertIsInstance(workflow.actions[1], NodejsNpmrcAndLockfileCopyAction)
333334
self.assertIsInstance(workflow.actions[2], CopySourceAction)
334-
self.assertIsInstance(workflow.actions[3], NodejsNpmInstallAction)
335+
self.assertIsInstance(workflow.actions[3], NodejsNpmUpdateAction)
335336
self.assertEqual(workflow.actions[3].install_dir, source_dir)
336337
self.assertIsInstance(workflow.actions[4], LinkSinglePathAction)
337338
self.assertEqual(workflow.actions[4]._source, os.path.join(source_dir, "node_modules"))
@@ -358,7 +359,7 @@ def test_build_in_source_with_download_dependencies_and_dependencies_dir(self, c
358359
self.assertIsInstance(workflow.actions[0], NodejsNpmPackAction)
359360
self.assertIsInstance(workflow.actions[1], NodejsNpmrcAndLockfileCopyAction)
360361
self.assertIsInstance(workflow.actions[2], CopySourceAction)
361-
self.assertIsInstance(workflow.actions[3], NodejsNpmInstallAction)
362+
self.assertIsInstance(workflow.actions[3], NodejsNpmUpdateAction)
362363
self.assertEqual(workflow.actions[3].install_dir, source_dir)
363364
self.assertIsInstance(workflow.actions[4], LinkSinglePathAction)
364365
self.assertEqual(workflow.actions[4]._source, os.path.join(source_dir, "node_modules"))
@@ -442,5 +443,5 @@ def test_workflow_revert_build_in_source(self, install_action_mock, install_link
442443
subprocess_npm=ANY,
443444
osutils=ANY,
444445
build_options=ANY,
445-
install_links=False,
446+
is_building_in_source=False,
446447
)

0 commit comments

Comments
 (0)