Skip to content

Commit 00b4a46

Browse files
authored
Merge pull request #332 from aws/develop
chore: Merge develop into master
2 parents 6f94390 + 9786073 commit 00b4a46

File tree

21 files changed

+809
-13
lines changed

21 files changed

+809
-13
lines changed

aws_lambda_builders/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
22
AWS Lambda Builder Library
33
"""
4-
__version__ = "1.11.0"
4+
__version__ = "1.12.0"
55
RPC_PROTOCOL_VERSION = "0.3"

aws_lambda_builders/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ def main(): # pylint: disable=too-many-statements
129129
dependencies_dir=params.get("dependencies_dir", None),
130130
combine_dependencies=params.get("combine_dependencies", True),
131131
architecture=params.get("architecture", X86_64),
132+
is_building_layer=params.get("is_building_layer", False),
133+
experimental_flags=params.get("experimental_flags", []),
132134
)
133135

134136
# Return a success response

aws_lambda_builders/actions.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def execute(self):
136136
if os.path.isdir(dependencies_source):
137137
copytree(dependencies_source, new_destination)
138138
else:
139-
os.makedirs(os.path.dirname(dependencies_source), exist_ok=True)
139+
os.makedirs(os.path.dirname(new_destination), exist_ok=True)
140140
shutil.copy2(dependencies_source, new_destination)
141141

142142

@@ -162,6 +162,10 @@ def execute(self):
162162
dependencies_source = os.path.join(self.artifact_dir, name)
163163
new_destination = os.path.join(self.dest_dir, name)
164164

165+
# shutil.move can't create subfolders if this is the first file in that folder
166+
if os.path.isfile(dependencies_source):
167+
os.makedirs(os.path.dirname(new_destination), exist_ok=True)
168+
165169
shutil.move(dependencies_source, new_destination)
166170

167171

aws_lambda_builders/workflows/nodejs_npm_esbuild/actions.py

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Actions specific to the esbuild bundler
33
"""
44
import logging
5+
from tempfile import NamedTemporaryFile
6+
57
from pathlib import Path
68

79
from aws_lambda_builders.actions import BaseAction, Purpose, ActionFailedError
@@ -23,7 +25,16 @@ class EsbuildBundleAction(BaseAction):
2325

2426
ENTRY_POINTS = "entry_points"
2527

26-
def __init__(self, scratch_dir, artifacts_dir, bundler_config, osutils, subprocess_esbuild):
28+
def __init__(
29+
self,
30+
scratch_dir,
31+
artifacts_dir,
32+
bundler_config,
33+
osutils,
34+
subprocess_esbuild,
35+
subprocess_nodejs=None,
36+
skip_deps=False,
37+
):
2738
"""
2839
:type scratch_dir: str
2940
:param scratch_dir: an existing (writable) directory for temporary files
@@ -35,15 +46,23 @@ def __init__(self, scratch_dir, artifacts_dir, bundler_config, osutils, subproce
3546
:type osutils: aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils
3647
:param osutils: An instance of OS Utilities for file manipulation
3748
38-
:type subprocess_esbuild: aws_lambda_builders.workflows.nodejs_npm.npm.SubprocessEsbuild
49+
:type subprocess_esbuild: aws_lambda_builders.workflows.nodejs_npm_esbuild.esbuild.SubprocessEsbuild
3950
:param subprocess_esbuild: An instance of the Esbuild process wrapper
51+
52+
:type subprocess_nodejs: aws_lambda_builders.workflows.nodejs_npm_esbuild.node.SubprocessNodejs
53+
:param subprocess_nodejs: An instance of the nodejs process wrapper
54+
55+
:type skip_deps: bool
56+
:param skip_deps: if dependencies should be omitted from bundling
4057
"""
4158
super(EsbuildBundleAction, self).__init__()
4259
self.scratch_dir = scratch_dir
4360
self.artifacts_dir = artifacts_dir
4461
self.bundler_config = bundler_config
4562
self.osutils = osutils
4663
self.subprocess_esbuild = subprocess_esbuild
64+
self.skip_deps = skip_deps
65+
self.subprocess_nodejs = subprocess_nodejs
4766

4867
def execute(self):
4968
"""
@@ -81,11 +100,73 @@ def execute(self):
81100
args.append("--sourcemap")
82101
args.append("--target={}".format(target))
83102
args.append("--outdir={}".format(self.artifacts_dir))
103+
104+
if self.skip_deps:
105+
LOG.info("Running custom esbuild using Node.js")
106+
script = EsbuildBundleAction._get_node_esbuild_template(
107+
explicit_entry_points, target, self.artifacts_dir, minify, sourcemap
108+
)
109+
self._run_external_esbuild_in_nodejs(script)
110+
return
111+
84112
try:
85113
self.subprocess_esbuild.run(args, cwd=self.scratch_dir)
86114
except EsbuildExecutionError as ex:
87115
raise ActionFailedError(str(ex))
88116

117+
def _run_external_esbuild_in_nodejs(self, script):
118+
"""
119+
Run esbuild in a separate process through Node.js
120+
Workaround for https://github.com/evanw/esbuild/issues/1958
121+
122+
:type script: str
123+
:param script: Node.js script to execute
124+
125+
:raises lambda_builders.actions.ActionFailedError: when esbuild packaging fails
126+
"""
127+
with NamedTemporaryFile(dir=self.scratch_dir, mode="w") as tmp:
128+
tmp.write(script)
129+
tmp.flush()
130+
try:
131+
self.subprocess_nodejs.run([tmp.name], cwd=self.scratch_dir)
132+
except EsbuildExecutionError as ex:
133+
raise ActionFailedError(str(ex))
134+
135+
@staticmethod
136+
def _get_node_esbuild_template(entry_points, target, out_dir, minify, sourcemap):
137+
"""
138+
Get the esbuild nodejs plugin template
139+
140+
:type entry_points: List[str]
141+
:param entry_points: list of entry points
142+
143+
:type target: str
144+
:param target: target version
145+
146+
:type out_dir: str
147+
:param out_dir: output directory to bundle into
148+
149+
:type minify: bool
150+
:param minify: if bundled code should be minified
151+
152+
:type sourcemap: bool
153+
:param sourcemap: if esbuild should produce a sourcemap
154+
155+
:rtype: str
156+
:return: formatted template
157+
"""
158+
curr_dir = Path(__file__).resolve().parent
159+
with open(str(Path(curr_dir, "esbuild-plugin.js.template")), "r") as f:
160+
input_str = f.read()
161+
result = input_str.format(
162+
target=target,
163+
minify="true" if minify else "false",
164+
sourcemap="true" if sourcemap else "false",
165+
out_dir=repr(out_dir),
166+
entry_points=entry_points,
167+
)
168+
return result
169+
89170
def _get_explicit_file_type(self, entry_point, entry_path):
90171
"""
91172
Get an entry point with an explicit .ts or .js suffix.
@@ -112,3 +193,67 @@ def _get_explicit_file_type(self, entry_point, entry_path):
112193
return entry_point + ext
113194

114195
raise ActionFailedError("entry point {} does not exist".format(entry_path))
196+
197+
198+
class EsbuildCheckVersionAction(BaseAction):
199+
"""
200+
A Lambda Builder Action that verifies that esbuild is a version supported by sam accelerate
201+
"""
202+
203+
NAME = "EsbuildCheckVersion"
204+
DESCRIPTION = "Checking esbuild version"
205+
PURPOSE = Purpose.COMPILE_SOURCE
206+
207+
MIN_VERSION = "0.14.13"
208+
209+
def __init__(self, scratch_dir, subprocess_esbuild):
210+
"""
211+
:type scratch_dir: str
212+
:param scratch_dir: temporary directory where esbuild is executed
213+
214+
:type subprocess_esbuild: aws_lambda_builders.workflows.nodejs_npm_esbuild.esbuild.SubprocessEsbuild
215+
:param subprocess_esbuild: An instance of the Esbuild process wrapper
216+
"""
217+
super().__init__()
218+
self.scratch_dir = scratch_dir
219+
self.subprocess_esbuild = subprocess_esbuild
220+
221+
def execute(self):
222+
"""
223+
Runs the action.
224+
225+
:raises lambda_builders.actions.ActionFailedError: when esbuild version checking fails
226+
"""
227+
args = ["--version"]
228+
229+
try:
230+
version = self.subprocess_esbuild.run(args, cwd=self.scratch_dir)
231+
except EsbuildExecutionError as ex:
232+
raise ActionFailedError(str(ex))
233+
234+
LOG.debug("Found esbuild with version: %s", version)
235+
236+
try:
237+
check_version = EsbuildCheckVersionAction._get_version_tuple(self.MIN_VERSION)
238+
esbuild_version = EsbuildCheckVersionAction._get_version_tuple(version)
239+
240+
if esbuild_version < check_version:
241+
raise ActionFailedError(
242+
f"Unsupported esbuild version. To use a dependency layer, the esbuild version must be at "
243+
f"least {self.MIN_VERSION}. Version found: {version}"
244+
)
245+
except (TypeError, ValueError) as ex:
246+
raise ActionFailedError(f"Unable to parse esbuild version: {str(ex)}")
247+
248+
@staticmethod
249+
def _get_version_tuple(version_string):
250+
"""
251+
Get an integer tuple representation of the version for comparison
252+
253+
:type version_string: str
254+
:param version_string: string containing the esbuild version
255+
256+
:rtype: tuple
257+
:return: version tuple used for comparison
258+
"""
259+
return tuple(map(int, version_string.split(".")))
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
let skipBundleNodeModules = {{
2+
name: 'make-all-packages-external',
3+
setup(build) {{
4+
let filter = /^[^.\/]|^\.[^.\/]|^\.\.[^\/]/ // Must not start with "/" or "./" or "../"
5+
build.onResolve({{ filter }}, args => ({{ path: args.path, external: true }}))
6+
}},
7+
}}
8+
9+
require('esbuild').build({{
10+
entryPoints: {entry_points},
11+
bundle: true,
12+
platform: 'node',
13+
format: 'cjs',
14+
target: '{target}',
15+
sourcemap: {sourcemap},
16+
outdir: {out_dir},
17+
minify: {minify},
18+
plugins: [skipBundleNodeModules],
19+
}}).catch(() => process.exit(1))
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""
2+
Wrapper around calling nodejs through a subprocess.
3+
"""
4+
5+
import logging
6+
7+
from aws_lambda_builders.exceptions import LambdaBuilderError
8+
9+
LOG = logging.getLogger(__name__)
10+
11+
12+
class NodejsExecutionError(LambdaBuilderError):
13+
14+
"""
15+
Exception raised in case nodejs execution fails.
16+
It will pass on the standard error output from the Node.js console.
17+
"""
18+
19+
MESSAGE = "Nodejs Failed: {message}"
20+
21+
22+
class SubprocessNodejs(object):
23+
24+
"""
25+
Wrapper around the nodejs command line utility, making it
26+
easy to consume execution results.
27+
"""
28+
29+
def __init__(self, osutils, executable_search_paths, which):
30+
"""
31+
:type osutils: aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils
32+
:param osutils: An instance of OS Utilities for file manipulation
33+
34+
:type executable_search_paths: list
35+
:param executable_search_paths: List of paths to the node package binary utilities. This will
36+
be used to find embedded Nodejs at runtime if present in the package
37+
38+
:type which: aws_lambda_builders.utils.which
39+
:param which: Function to get paths which conform to the given mode on the PATH
40+
with the prepended additional search paths
41+
"""
42+
self.osutils = osutils
43+
self.executable_search_paths = executable_search_paths
44+
self.which = which
45+
46+
def nodejs_binary(self):
47+
"""
48+
Finds the Nodejs binary at runtime.
49+
50+
The utility may be present as a package dependency of the Lambda project,
51+
or in the global path. If there is one in the Lambda project, it should
52+
be preferred over a global utility. The check has to be executed
53+
at runtime, since nodejs dependencies will be installed by the workflow
54+
using one of the previous actions.
55+
"""
56+
57+
LOG.debug("checking for nodejs in: %s", self.executable_search_paths)
58+
binaries = self.which("node", executable_search_paths=self.executable_search_paths)
59+
LOG.debug("potential nodejs binaries: %s", binaries)
60+
61+
if binaries:
62+
return binaries[0]
63+
else:
64+
raise NodejsExecutionError(message="cannot find nodejs")
65+
66+
def run(self, args, cwd=None):
67+
68+
"""
69+
Runs the action.
70+
71+
:type args: list
72+
:param args: Command line arguments to pass to Nodejs
73+
74+
:type cwd: str
75+
:param cwd: Directory where to execute the command (defaults to current dir)
76+
77+
:rtype: str
78+
:return: text of the standard output from the command
79+
80+
:raises aws_lambda_builders.workflows.nodejs_npm.npm.NodejsExecutionError:
81+
when the command executes with a non-zero return code. The exception will
82+
contain the text of the standard error output from the command.
83+
84+
:raises ValueError: if arguments are not provided, or not a list
85+
"""
86+
87+
if not isinstance(args, list):
88+
raise ValueError("args must be a list")
89+
90+
if not args:
91+
raise ValueError("requires at least one arg")
92+
93+
invoke_nodejs = [self.nodejs_binary()] + args
94+
95+
LOG.debug("executing Nodejs: %s", invoke_nodejs)
96+
97+
p = self.osutils.popen(invoke_nodejs, stdout=self.osutils.pipe, stderr=self.osutils.pipe, cwd=cwd)
98+
99+
out, err = p.communicate()
100+
101+
if p.returncode != 0:
102+
raise NodejsExecutionError(message=err.decode("utf8").strip())
103+
104+
return out.decode("utf8").strip()

0 commit comments

Comments
 (0)