Skip to content

Commit 1f3c658

Browse files
authored
feat: update node_npm actions and workflow to support building in source directory (#462)
1 parent 2959925 commit 1f3c658

File tree

8 files changed

+443
-66
lines changed

8 files changed

+443
-66
lines changed

aws_lambda_builders/workflows/nodejs_npm/actions.py

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
"""
44

55
import logging
6+
from typing import Optional
67

78
from aws_lambda_builders.actions import ActionFailedError, BaseAction, Purpose
89
from aws_lambda_builders.utils import extract_tarfile
910

10-
from .npm import NpmExecutionError
11+
from .npm import NpmExecutionError, SubprocessNpm
1112

1213
LOG = logging.getLogger(__name__)
1314

@@ -82,21 +83,22 @@ class NodejsNpmInstallAction(BaseAction):
8283
DESCRIPTION = "Installing dependencies from NPM"
8384
PURPOSE = Purpose.RESOLVE_DEPENDENCIES
8485

85-
def __init__(self, install_dir, subprocess_npm):
86+
def __init__(self, install_dir: str, subprocess_npm: SubprocessNpm, install_links: Optional[bool] = False):
8687
"""
87-
:type install_dir: str
88-
:param install_dir: Dependencies will be installed in this directory.
89-
90-
:type subprocess_npm: aws_lambda_builders.workflows.nodejs_npm.npm.SubprocessNpm
91-
:param subprocess_npm: An instance of the NPM process wrapper
92-
93-
:type is_production: bool
94-
:param is_production: NPM installation mode is production (eg --production=false to force dev dependencies)
88+
Parameters
89+
----------
90+
install_dir : str
91+
Dependencies will be installed in this directory.
92+
subprocess_npm : SubprocessNpm
93+
An instance of the NPM process wrapper
94+
install_links : Optional[bool]
95+
Uses the --install-links npm option if True, by default False
9596
"""
9697

9798
super(NodejsNpmInstallAction, self).__init__()
9899
self.install_dir = install_dir
99100
self.subprocess_npm = subprocess_npm
101+
self.install_links = install_links
100102

101103
def execute(self):
102104
"""
@@ -107,9 +109,11 @@ def execute(self):
107109
try:
108110
LOG.debug("NODEJS installing in: %s", self.install_dir)
109111

110-
self.subprocess_npm.run(
111-
["install", "-q", "--no-audit", "--no-save", "--unsafe-perm", "--production"], cwd=self.install_dir
112-
)
112+
command = ["install", "-q", "--no-audit", "--no-save", "--unsafe-perm", "--production"]
113+
if self.install_links:
114+
command.append("--install-links")
115+
116+
self.subprocess_npm.run(command, cwd=self.install_dir)
113117

114118
except NpmExecutionError as ex:
115119
raise ActionFailedError(str(ex))
@@ -128,18 +132,22 @@ class NodejsNpmCIAction(BaseAction):
128132
DESCRIPTION = "Installing dependencies from NPM using the CI method"
129133
PURPOSE = Purpose.RESOLVE_DEPENDENCIES
130134

131-
def __init__(self, install_dir, subprocess_npm):
135+
def __init__(self, install_dir: str, subprocess_npm: SubprocessNpm, install_links: Optional[bool] = False):
132136
"""
133-
:type install_dir: str
134-
:param install_dir: Dependencies will be installed in this directory.
135-
136-
:type subprocess_npm: aws_lambda_builders.workflows.nodejs_npm.npm.SubprocessNpm
137-
:param subprocess_npm: An instance of the NPM process wrapper
137+
Parameters
138+
----------
139+
install_dir : str
140+
Dependencies will be installed in this directory.
141+
subprocess_npm : SubprocessNpm
142+
An instance of the NPM process wrapper
143+
install_links : Optional[bool]
144+
Uses the --install-links npm option if True, by default False
138145
"""
139146

140147
super(NodejsNpmCIAction, self).__init__()
141148
self.install_dir = install_dir
142149
self.subprocess_npm = subprocess_npm
150+
self.install_links = install_links
143151

144152
def execute(self):
145153
"""
@@ -151,7 +159,11 @@ def execute(self):
151159
try:
152160
LOG.debug("NODEJS installing ci in: %s", self.install_dir)
153161

154-
self.subprocess_npm.run(["ci"], cwd=self.install_dir)
162+
command = ["ci"]
163+
if self.install_links:
164+
command.append("--install-links")
165+
166+
self.subprocess_npm.run(command, cwd=self.install_dir)
155167

156168
except NpmExecutionError as ex:
157169
raise ActionFailedError(str(ex))

aws_lambda_builders/workflows/nodejs_npm/workflow.py

Lines changed: 104 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
"""
44

55
import logging
6+
import os
7+
from typing import Optional
68

79
from aws_lambda_builders.actions import (
810
CleanUpAction,
911
CopyDependenciesAction,
1012
CopySourceAction,
13+
LinkSinglePathAction,
1114
MoveDependenciesAction,
1215
)
1316
from aws_lambda_builders.path_resolver import PathResolver
@@ -43,7 +46,7 @@ class NodejsNpmWorkflow(BaseWorkflow):
4346
CONFIG_PROPERTY = "aws_sam"
4447

4548
DEFAULT_BUILD_DIR = BuildDirectory.ARTIFACTS
46-
BUILD_IN_SOURCE_SUPPORT = BuildInSourceSupport.NOT_SUPPORTED
49+
BUILD_IN_SOURCE_SUPPORT = BuildInSourceSupport.OPTIONALLY_SUPPORTED
4750

4851
def __init__(self, source_dir, artifacts_dir, scratch_dir, manifest_path, runtime=None, osutils=None, **kwargs):
4952
super(NodejsNpmWorkflow, self).__init__(
@@ -52,6 +55,7 @@ def __init__(self, source_dir, artifacts_dir, scratch_dir, manifest_path, runtim
5255

5356
if osutils is None:
5457
osutils = OSUtils()
58+
self.osutils = osutils
5559

5660
if not osutils.file_exists(manifest_path):
5761
LOG.warning("package.json file not found. Continuing the build without dependencies.")
@@ -62,6 +66,8 @@ def __init__(self, source_dir, artifacts_dir, scratch_dir, manifest_path, runtim
6266

6367
tar_dest_dir = osutils.joinpath(scratch_dir, "unpacked")
6468
tar_package_dir = osutils.joinpath(tar_dest_dir, "package")
69+
# TODO: we should probably unpack straight into artifacts dir, rather than unpacking into tar_dest_dir and
70+
# then copying into artifacts. Just make sure EXCLUDED_FILES are not included, or remove them.
6571
npm_pack = NodejsNpmPackAction(
6672
tar_dest_dir, scratch_dir, manifest_path, osutils=osutils, subprocess_npm=subprocess_npm
6773
)
@@ -75,39 +81,82 @@ def __init__(self, source_dir, artifacts_dir, scratch_dir, manifest_path, runtim
7581
]
7682

7783
if self.download_dependencies:
78-
# install the dependencies into artifact folder
7984
self.actions.append(
80-
NodejsNpmWorkflow.get_install_action(source_dir, artifacts_dir, subprocess_npm, osutils, self.options)
85+
NodejsNpmWorkflow.get_install_action(
86+
source_dir=source_dir,
87+
install_dir=self.build_dir,
88+
subprocess_npm=subprocess_npm,
89+
osutils=osutils,
90+
build_options=self.options,
91+
install_links=self.build_dir == self.source_dir,
92+
)
8193
)
8294

83-
artifacts_cleanup_actions = [
84-
NodejsNpmrcCleanUpAction(artifacts_dir, osutils=osutils),
85-
NodejsNpmLockFileCleanUpAction(artifacts_dir, osutils=osutils),
86-
]
95+
if self.download_dependencies and self.build_dir == self.source_dir:
96+
self.actions += self._actions_for_linking_source_dependencies_to_artifacts
8797

8898
# if no dependencies dir, just cleanup artifacts and we're done
8999
if not self.dependencies_dir:
90-
self.actions += artifacts_cleanup_actions
100+
self.actions += self._actions_for_cleanup
91101
return
92102

93103
# if we downloaded dependencies, update dependencies_dir
94104
if self.download_dependencies:
95-
# clean up the dependencies folder first
96-
self.actions.append(CleanUpAction(self.dependencies_dir))
97-
# if combine_dependencies is set, we should keep dependencies and source code in the artifact folder
98-
# while copying the dependencies. Otherwise we should separate the dependencies and source code
99-
dependencies_dir_update_action = (
100-
CopyDependenciesAction if self.combine_dependencies else MoveDependenciesAction
101-
)
102-
self.actions.append(dependencies_dir_update_action(source_dir, artifacts_dir, self.dependencies_dir))
105+
self.actions += self._actions_for_updating_dependencies_dir
103106
# otherwise if we want to use the dependencies from dependencies_dir and we want to combine them,
104107
# then copy them into the artifacts dir
105108
elif self.combine_dependencies:
106-
self.actions.append(CopySourceAction(self.dependencies_dir, artifacts_dir))
109+
self.actions.append(
110+
CopySourceAction(
111+
self.dependencies_dir, artifacts_dir, maintain_symlinks=self.build_dir == self.source_dir
112+
)
113+
)
107114

108-
# cleanup
109-
self.actions += artifacts_cleanup_actions
110-
self.actions.append(NodejsNpmLockFileCleanUpAction(self.dependencies_dir, osutils=osutils))
115+
self.actions += self._actions_for_cleanup
116+
117+
@property
118+
def _actions_for_cleanup(self):
119+
actions = [NodejsNpmrcCleanUpAction(self.artifacts_dir, osutils=self.osutils)]
120+
121+
# we don't want to cleanup the lockfile in the source code's symlinked node_modules
122+
if self.build_dir != self.source_dir:
123+
actions.append(NodejsNpmLockFileCleanUpAction(self.artifacts_dir, osutils=self.osutils))
124+
if self.build_dir != self.source_dir and self.dependencies_dir:
125+
actions.append(NodejsNpmLockFileCleanUpAction(self.dependencies_dir, osutils=self.osutils))
126+
127+
return actions
128+
129+
@property
130+
def _actions_for_linking_source_dependencies_to_artifacts(self):
131+
source_dependencies_path = os.path.join(self.source_dir, "node_modules")
132+
artifact_dependencies_path = os.path.join(self.artifacts_dir, "node_modules")
133+
return [LinkSinglePathAction(source=source_dependencies_path, dest=artifact_dependencies_path)]
134+
135+
@property
136+
def _actions_for_updating_dependencies_dir(self):
137+
# clean up the dependencies folder first
138+
actions = [CleanUpAction(self.dependencies_dir)]
139+
# if combine_dependencies is set, we should keep dependencies and source code in the artifact folder
140+
# while copying the dependencies. Otherwise we should separate the dependencies and source code
141+
if self.combine_dependencies:
142+
actions.append(
143+
CopyDependenciesAction(
144+
source_dir=self.source_dir,
145+
artifact_dir=self.artifacts_dir,
146+
destination_dir=self.dependencies_dir,
147+
maintain_symlinks=self.build_dir == self.source_dir,
148+
)
149+
)
150+
else:
151+
actions.append(
152+
MoveDependenciesAction(
153+
source_dir=self.source_dir,
154+
artifact_dir=self.artifacts_dir,
155+
destination_dir=self.dependencies_dir,
156+
)
157+
)
158+
159+
return actions
111160

112161
def get_resolvers(self):
113162
"""
@@ -116,30 +165,36 @@ def get_resolvers(self):
116165
return [PathResolver(runtime=self.runtime, binary="npm")]
117166

118167
@staticmethod
119-
def get_install_action(source_dir, install_dir, subprocess_npm, osutils, build_options):
168+
def get_install_action(
169+
source_dir: str,
170+
install_dir: str,
171+
subprocess_npm: SubprocessNpm,
172+
osutils: OSUtils,
173+
build_options: Optional[dict],
174+
install_links: Optional[bool] = False,
175+
):
120176
"""
121-
Get the install action used to install dependencies at artifacts_dir
122-
123-
:type source_dir: str
124-
:param source_dir: an existing (readable) directory containing source files
125-
126-
:type install_dir: str
127-
:param install_dir: Dependencies will be installed in this directory.
128-
129-
:type osutils: aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils
130-
:param osutils: An instance of OS Utilities for file manipulation
131-
132-
:type subprocess_npm: aws_lambda_builders.workflows.nodejs_npm.npm.SubprocessNpm
133-
:param subprocess_npm: An instance of the NPM process wrapper
134-
135-
:type build_options: Dict
136-
:param build_options: Object containing build options configurations
137-
138-
:type is_production: bool
139-
:param is_production: NPM installation mode is production (eg --production=false to force dev dependencies)
140-
141-
:rtype: BaseAction
142-
:return: Install action to use
177+
Get the install action used to install dependencies.
178+
179+
Parameters
180+
----------
181+
source_dir : str
182+
an existing (readable) directory containing source files
183+
install_dir : str
184+
Dependencies will be installed in this directory
185+
subprocess_npm : SubprocessNpm
186+
An instance of the NPM process wrapper
187+
osutils : OSUtils
188+
An instance of OS Utilities for file manipulation
189+
build_options : Optional[dict]
190+
Object containing build options configurations
191+
install_links : Optional[bool]
192+
Uses the --install-links npm option if True, by default False
193+
194+
Returns
195+
-------
196+
BaseAction
197+
Install action to use
143198
"""
144199
lockfile_path = osutils.joinpath(source_dir, "package-lock.json")
145200
shrinkwrap_path = osutils.joinpath(source_dir, "npm-shrinkwrap.json")
@@ -149,6 +204,10 @@ def get_install_action(source_dir, install_dir, subprocess_npm, osutils, build_o
149204
npm_ci_option = build_options.get("use_npm_ci", False)
150205

151206
if (osutils.file_exists(lockfile_path) or osutils.file_exists(shrinkwrap_path)) and npm_ci_option:
152-
return NodejsNpmCIAction(install_dir=install_dir, subprocess_npm=subprocess_npm)
207+
return NodejsNpmCIAction(
208+
install_dir=install_dir, subprocess_npm=subprocess_npm, install_links=install_links
209+
)
153210

154-
return NodejsNpmInstallAction(install_dir=install_dir, subprocess_npm=subprocess_npm)
211+
return NodejsNpmInstallAction(
212+
install_dir=install_dir, subprocess_npm=subprocess_npm, install_links=install_links
213+
)

0 commit comments

Comments
 (0)