Skip to content

Commit 9d37cc8

Browse files
mndeveciprower-examsoftCoshUS
authored
feat: java build changes (#318)
* feat: java build changes related to maven scope and layers - use "runtime" scope for maven builds if experimental flag is provided - copy created jar files for layer code instead of classes * Update the maven DESIGN.md to explain changes. * add maven integration tests * add gradle integration tests * fix formatting & unit tests * fix issues after merge * correct dependency with maven tests * change type from function to Callable Co-authored-by: Wilton_ <[email protected]> * disable too-many-locals for workflow.py only Co-authored-by: Phillip Rower <[email protected]> Co-authored-by: Wilton_ <[email protected]>
1 parent 79af5bf commit 9d37cc8

File tree

31 files changed

+617
-33
lines changed

31 files changed

+617
-33
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ init:
44
test:
55
# Run unit tests
66
# Fail if coverage falls below 94%
7-
LAMBDA_BUILDERS_DEV=1 pytest --cov aws_lambda_builders --cov-report term-missing --cov-fail-under 94 tests/unit tests/functional
7+
LAMBDA_BUILDERS_DEV=1 pytest -vv --cov aws_lambda_builders --cov-report term-missing --cov-fail-under 94 tests/unit tests/functional
88

99
func-test:
1010
LAMBDA_BUILDERS_DEV=1 pytest tests/functional

aws_lambda_builders/builder.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def build(
6969
dependencies_dir=None,
7070
combine_dependencies=True,
7171
architecture=X86_64,
72+
is_building_layer=False,
7273
experimental_flags=None,
7374
):
7475
# pylint: disable-msg=too-many-locals
@@ -130,6 +131,10 @@ def build(
130131
:param architecture:
131132
Type of architecture x86_64 and arm64 for Lambda Function
132133
134+
:type is_building_layer: bool
135+
:param is_building_layer:
136+
Boolean flag which will be set True if current build operation is being executed for layers
137+
133138
:type experimental_flags: list
134139
:param experimental_flags:
135140
List of strings, which will indicate enabled experimental flags for the current build session
@@ -152,6 +157,7 @@ def build(
152157
dependencies_dir=dependencies_dir,
153158
combine_dependencies=combine_dependencies,
154159
architecture=architecture,
160+
is_building_layer=is_building_layer,
155161
experimental_flags=experimental_flags,
156162
)
157163

aws_lambda_builders/utils.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
LOG = logging.getLogger(__name__)
1313

1414

15-
def copytree(source, destination, ignore=None):
15+
def copytree(source, destination, ignore=None, include=None):
1616
"""
1717
Similar to shutil.copytree except that it removes the limitation that the destination directory should
1818
be present.
@@ -29,17 +29,25 @@ def copytree(source, destination, ignore=None):
2929
:param ignore:
3030
A function that returns a set of file names to ignore, given a list of available file names. Similar to the
3131
``ignore`` property of ``shutils.copytree`` method
32+
33+
:type include: Callable[[str], bool]
34+
:param include:
35+
A function that will decide whether a file should be copied or skipped it. It accepts file name as parameter
36+
and return True or False. Returning True will continue copy operation, returning False will skip copy operation
37+
for that file
3238
"""
3339

3440
if not os.path.exists(source):
3541
LOG.warning("Skipping copy operation since source %s does not exist", source)
3642
return
3743

3844
if not os.path.exists(destination):
45+
LOG.debug("Creating target folders at %s", destination)
3946
os.makedirs(destination)
4047

4148
try:
4249
# Let's try to copy the directory metadata from source to destination
50+
LOG.debug("Copying directory metadata from source (%s) to destination (%s)", source, destination)
4351
shutil.copystat(source, destination)
4452
except OSError as ex:
4553
# Can't copy file access times in Windows
@@ -54,14 +62,20 @@ def copytree(source, destination, ignore=None):
5462
for name in names:
5563
# Skip ignored names
5664
if name in ignored_names:
65+
LOG.debug("File (%s) is in ignored set, skipping it", name)
5766
continue
5867

5968
new_source = os.path.join(source, name)
6069
new_destination = os.path.join(destination, name)
6170

71+
if include and not os.path.isdir(new_source) and not include(name):
72+
LOG.debug("File (%s) doesn't satisfy the include rule, skipping it", name)
73+
continue
74+
6275
if os.path.isdir(new_source):
63-
copytree(new_source, new_destination, ignore=ignore)
76+
copytree(new_source, new_destination, ignore=ignore, include=include)
6477
else:
78+
LOG.debug("Copying source file (%s) to destination (%s)", new_source, new_destination)
6579
shutil.copy2(new_source, new_destination)
6680

6781

aws_lambda_builders/workflow.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,10 @@ def __init__(
164164
dependencies_dir=None,
165165
combine_dependencies=True,
166166
architecture=X86_64,
167+
is_building_layer=False,
167168
experimental_flags=None,
168169
):
170+
# pylint: disable-msg=too-many-locals
169171
"""
170172
Initialize the builder with given arguments. These arguments together form the "public API" that each
171173
build action must support at the minimum.
@@ -201,6 +203,10 @@ def __init__(
201203
from dependency_folder into build folder
202204
architecture : str, optional
203205
Architecture type either arm64 or x86_64 for which the build will be based on in AWS lambda, by default X86_64
206+
207+
is_building_layer: bool, optional
208+
Boolean flag which will be set True if current build operation is being executed for layers
209+
204210
experimental_flags: list, optional
205211
List of strings, which will indicate enabled experimental flags for the current build session
206212
"""
@@ -218,6 +224,7 @@ def __init__(
218224
self.dependencies_dir = dependencies_dir
219225
self.combine_dependencies = combine_dependencies
220226
self.architecture = architecture
227+
self.is_building_layer = is_building_layer
221228
self.experimental_flags = experimental_flags if experimental_flags else []
222229

223230
# Actions are registered by the subclasses as they seem fit

aws_lambda_builders/workflows/java/utils.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
import platform
77
import shutil
88
import subprocess
9-
from aws_lambda_builders.utils import which
9+
from aws_lambda_builders.utils import which, copytree
10+
11+
12+
EXPERIMENTAL_MAVEN_SCOPE_AND_LAYER_FLAG = "experimentalMavenScopeAndLayer"
1013

1114

1215
class OSUtils(object):
@@ -37,17 +40,8 @@ def exists(self, p):
3740
def which(self, executable, executable_search_paths=None):
3841
return which(executable, executable_search_paths=executable_search_paths)
3942

40-
def copytree(self, source, destination):
41-
if not os.path.exists(destination):
42-
self.makedirs(destination)
43-
names = self.listdir(source)
44-
for name in names:
45-
new_source = os.path.join(source, name)
46-
new_destination = os.path.join(destination, name)
47-
if os.path.isdir(new_source):
48-
self.copytree(new_source, new_destination)
49-
else:
50-
self.copy(new_source, new_destination)
43+
def copytree(self, source, destination, ignore=None, include=None):
44+
copytree(source, destination, ignore=ignore, include=include)
5145

5246
def makedirs(self, d):
5347
return os.makedirs(d)
@@ -58,3 +52,21 @@ def rmtree(self, d):
5852
@property
5953
def pipe(self):
6054
return subprocess.PIPE
55+
56+
57+
def jar_file_filter(file_name):
58+
"""
59+
A function that will filter .jar files for copy operation
60+
61+
:type file_name: str
62+
:param file_name:
63+
Name of the file that will be checked against if it ends with .jar or not
64+
"""
65+
return bool(file_name) and isinstance(file_name, str) and file_name.endswith(".jar")
66+
67+
68+
def is_experimental_maven_scope_and_layers_active(experimental_flags):
69+
"""
70+
A function which will determine if experimental maven scope and layer changes are active
71+
"""
72+
return bool(experimental_flags) and EXPERIMENTAL_MAVEN_SCOPE_AND_LAYER_FLAG in experimental_flags

aws_lambda_builders/workflows/java_gradle/actions.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
from aws_lambda_builders.actions import ActionFailedError, BaseAction, Purpose
77
from .gradle import GradleExecutionError
8+
from ..java.utils import jar_file_filter
89

910

1011
class JavaGradleBuildAction(BaseAction):
@@ -56,7 +57,7 @@ def _build_project(self, init_script_file):
5657

5758

5859
class JavaGradleCopyArtifactsAction(BaseAction):
59-
NAME = "CopyArtifacts"
60+
NAME = "JavaGradleCopyArtifacts"
6061
DESCRIPTION = "Copying the built artifacts"
6162
PURPOSE = Purpose.COPY_SOURCE
6263

@@ -77,3 +78,26 @@ def _copy_artifacts(self):
7778
self.os_utils.copytree(lambda_build_output, self.artifacts_dir)
7879
except Exception as ex:
7980
raise ActionFailedError(str(ex))
81+
82+
83+
class JavaGradleCopyLayerArtifactsAction(JavaGradleCopyArtifactsAction):
84+
"""
85+
Java layers does not support using .class files in it.
86+
This action (different from the parent one) copies contents of the layer as jar files and place it
87+
into the artifact folder
88+
"""
89+
90+
NAME = "JavaGradleCopyLayerArtifacts"
91+
92+
def _copy_artifacts(self):
93+
lambda_build_output = os.path.join(self.build_dir, "build", "libs")
94+
layer_dependencies = os.path.join(self.build_dir, "build", "distributions", "lambda-build", "lib")
95+
try:
96+
if not self.os_utils.exists(self.artifacts_dir):
97+
self.os_utils.makedirs(self.artifacts_dir)
98+
self.os_utils.copytree(
99+
lambda_build_output, os.path.join(self.artifacts_dir, "lib"), include=jar_file_filter
100+
)
101+
self.os_utils.copytree(layer_dependencies, os.path.join(self.artifacts_dir, "lib"), include=jar_file_filter)
102+
except Exception as ex:
103+
raise ActionFailedError(str(ex))

aws_lambda_builders/workflows/java_gradle/workflow.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
from aws_lambda_builders.actions import CleanUpAction
77
from aws_lambda_builders.workflow import BaseWorkflow, Capability
88
from aws_lambda_builders.workflows.java.actions import JavaCopyDependenciesAction, JavaMoveDependenciesAction
9-
from aws_lambda_builders.workflows.java.utils import OSUtils
9+
from aws_lambda_builders.workflows.java.utils import OSUtils, is_experimental_maven_scope_and_layers_active
1010

11-
from .actions import JavaGradleBuildAction, JavaGradleCopyArtifactsAction
11+
from .actions import JavaGradleBuildAction, JavaGradleCopyArtifactsAction, JavaGradleCopyLayerArtifactsAction
1212
from .gradle import SubprocessGradle
1313
from .gradle_resolver import GradleResolver
1414
from .gradle_validator import GradleValidator
@@ -33,9 +33,16 @@ def __init__(self, source_dir, artifacts_dir, scratch_dir, manifest_path, **kwar
3333

3434
subprocess_gradle = SubprocessGradle(gradle_binary=self.binaries["gradle"], os_utils=self.os_utils)
3535

36+
copy_artifacts_action = JavaGradleCopyArtifactsAction(
37+
source_dir, artifacts_dir, self.build_output_dir, self.os_utils
38+
)
39+
if self.is_building_layer and is_experimental_maven_scope_and_layers_active(self.experimental_flags):
40+
copy_artifacts_action = JavaGradleCopyLayerArtifactsAction(
41+
source_dir, artifacts_dir, self.build_output_dir, self.os_utils
42+
)
3643
self.actions = [
3744
JavaGradleBuildAction(source_dir, manifest_path, subprocess_gradle, scratch_dir, self.os_utils),
38-
JavaGradleCopyArtifactsAction(source_dir, artifacts_dir, self.build_output_dir, self.os_utils),
45+
copy_artifacts_action,
3946
]
4047

4148
if self.dependencies_dir:

aws_lambda_builders/workflows/java_maven/DESIGN.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,28 @@ source directory.
7979

8080
```bash
8181
mvn clean install
82-
mvn dependency:copy-dependencies -DincludeScope=compile
82+
mvn dependency:copy-dependencies -DincludeScope=runtime
8383
```
8484

85+
Building artifact for an `AWS::Serverless::LayerVersion` requires different packaging than a
86+
`AWS::Serverless::Function`. [Layers](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html)
87+
use only artifacts under `java/lib/` which differs from Functions in that they in addition allow classes at
88+
the root level similar to normal jar packaging. `JavaMavenLayersWorkflow` handles packaging for Layers and
89+
`JavaMavenWorkflow` handles packaging for Functions.
90+
8591
#### Step 4: Copy to artifact directory
8692

87-
Built Java classes and dependencies are copied from `scratch_dir/target/classes` and `scratch_dir/target/dependency`
88-
to `artifact_dir` and `artifact_dir/lib` respectively.
93+
Built Java classes and dependencies for Functions are copied from `scratch_dir/target/classes` and `scratch_dir/target/dependency`
94+
to `artifact_dir` and `artifact_dir/lib` respectively. Built Java classes and dependencies for Layers are copied from
95+
`scratch_dir/target/*.jar` and `scratch_dir/target/dependency` to `artifact_dir/lib`. Copy all the artifacts
96+
required for runtime execution.
97+
98+
### Notes on changes of original implementation
99+
100+
The original implementation was not handling Layers well. Maven has provided a scope called `provided` which is
101+
used to declare that a particular dependency is required for compilation but should not be packaged with the
102+
declaring project artifact. Naturally this is the scope a maven java project would use for artifacts
103+
provided by Layers. Original implementation would package those `provided` scoped entities with the Function,
104+
and thus if a project was using Layers it would have the artifact both in the Layer and in the Function.
89105

90106
[Gradle Lambda Builder]:https://github.com/awslabs/aws-lambda-builders/blob/develop/aws_lambda_builders/workflows/java_gradle/DESIGN.md

aws_lambda_builders/workflows/java_maven/actions.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
import os
66
import logging
7+
import shutil
78

89
from aws_lambda_builders.actions import ActionFailedError, BaseAction, Purpose
910
from .maven import MavenExecutionError
11+
from ..java.utils import jar_file_filter
1012

1113
LOG = logging.getLogger(__name__)
1214

@@ -81,3 +83,33 @@ def _copy_artifacts(self):
8183
self.os_utils.copytree(dependency_output, os.path.join(self.artifacts_dir, "lib"))
8284
except Exception as ex:
8385
raise ActionFailedError(str(ex))
86+
87+
88+
class JavaMavenCopyLayerArtifactsAction(JavaMavenCopyArtifactsAction):
89+
"""
90+
Java layers does not support using .class files in it.
91+
This action (different from the parent one) copies contents of the layer as jar files and place it
92+
into the artifact folder
93+
"""
94+
95+
NAME = "MavenCopyLayerArtifacts"
96+
IGNORED_FOLDERS = ["classes", "dependency", "generated-sources", "maven-archiver", "maven-status"]
97+
98+
def _copy_artifacts(self):
99+
lambda_build_output = os.path.join(self.scratch_dir, "target")
100+
dependency_output = os.path.join(self.scratch_dir, "target", "dependency")
101+
102+
if not self.os_utils.exists(lambda_build_output):
103+
raise ActionFailedError("Required target/classes directory was not produced from 'mvn package'")
104+
105+
try:
106+
self.os_utils.copytree(
107+
lambda_build_output,
108+
os.path.join(self.artifacts_dir, "lib"),
109+
ignore=shutil.ignore_patterns(*self.IGNORED_FOLDERS),
110+
include=jar_file_filter,
111+
)
112+
if self.os_utils.exists(dependency_output):
113+
self.os_utils.copytree(dependency_output, os.path.join(self.artifacts_dir, "lib"))
114+
except Exception as ex:
115+
raise ActionFailedError(str(ex))

aws_lambda_builders/workflows/java_maven/maven.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@ def __init__(self, **kwargs):
1616

1717

1818
class SubprocessMaven(object):
19-
def __init__(self, maven_binary, os_utils=None):
19+
def __init__(self, maven_binary, os_utils=None, is_experimental_maven_scope_enabled=False):
2020
if maven_binary is None:
2121
raise ValueError("Must provide Maven BinaryPath")
2222
self.maven_binary = maven_binary
2323
if os_utils is None:
2424
raise ValueError("Must provide OSUtils")
2525
self.os_utils = os_utils
26+
self.is_experimental_maven_scope_enabled = is_experimental_maven_scope_enabled
2627

2728
def build(self, scratch_dir):
2829
args = ["clean", "install"]
@@ -34,7 +35,9 @@ def build(self, scratch_dir):
3435
raise MavenExecutionError(message=stdout.decode("utf8").strip())
3536

3637
def copy_dependency(self, scratch_dir):
37-
args = ["dependency:copy-dependencies", "-DincludeScope=compile", "-Dmdep.prependGroupId=true"]
38+
include_scope = "runtime" if self.is_experimental_maven_scope_enabled else "compile"
39+
LOG.debug("Running copy_dependency with scope: %s", include_scope)
40+
args = ["dependency:copy-dependencies", f"-DincludeScope={include_scope}", "-Dmdep.prependGroupId=true"]
3841
ret_code, stdout, _ = self._run(args, scratch_dir)
3942

4043
if ret_code != 0:

0 commit comments

Comments
 (0)