Skip to content

Commit 30818b9

Browse files
authored
Merge pull request #170 from awslabs/develop
Merge to master - v0.9.0
2 parents f7b60c7 + 112a5f9 commit 30818b9

File tree

23 files changed

+703
-5
lines changed

23 files changed

+703
-5
lines changed

.appveyor.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ environment:
1212
matrix:
1313

1414
- PYTHON: "C:\\Python27-x64"
15-
PYTHON_VERSION: '2.7.17'
15+
PYTHON_VERSION: '2.7.18'
1616
PYTHON_ARCH: '64'
1717
LINE_COVERAGE: '91'
1818
NEW_FLAKE8: 0
@@ -24,7 +24,7 @@ environment:
2424
NEW_FLAKE8: 0
2525
JAVA_HOME: "C:\\Program Files\\Java\\jdk11"
2626
- PYTHON: "C:\\Python37-x64"
27-
PYTHON_VERSION: '3.7.4'
27+
PYTHON_VERSION: '3.7.7'
2828
PYTHON_ARCH: '64'
2929
LINE_COVERAGE: '91'
3030
NEW_FLAKE8: 0
@@ -70,6 +70,9 @@ for:
7070
# setup Gradle
7171
- "choco install gradle"
7272

73+
# setup make
74+
- "choco install make"
75+
7376
# Echo final Path
7477
- "echo %PATH%"
7578

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__ = "0.8.0"
4+
__version__ = "0.9.0"
55
RPC_PROTOCOL_VERSION = "0.3"

aws_lambda_builders/workflows/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
import aws_lambda_builders.workflows.java_gradle
1111
import aws_lambda_builders.workflows.java_maven
1212
import aws_lambda_builders.workflows.dotnet_clipackage
13+
import aws_lambda_builders.workflows.custom_make
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
## Provided Make Builder
2+
3+
### Scope
4+
5+
Provided runtimes need a flexible builder approach as the binaries required for a building for each language is very different. The introduced builder should have scope
6+
to be able build irrespective of the provided language, this is only possible if the logic of the build is not entirely contained within the builder itself, but that the builder
7+
executes commands that are user provided. The builder should showcase a contract to the user of how those commmands need to be provided.
8+
9+
10+
### Mechanism
11+
12+
The Mechanism proposed here is that of `Makefile` Builder that has a particular make target available based on the logical id of the function to be built.
13+
The `Makefile` is the manifest and is present in the same path as the original source directory.
14+
15+
16+
### Implementation
17+
18+
When the makefile is called through a lambda builder workflow, the appropriate target is triggered and artifacts should be copied to the exposed environmental variable `$ARTIFACTS_DIR`. This environment variable is seeded by the lambda builder with a value set to it. The targets are defined as
19+
20+
```
21+
build-{Function_Logical_Id}
22+
```
23+
24+
Injected Environment Variables by Makefile Lambda Builder Workflow per Function to be built. The injected environment variable has the path values modified
25+
based on if this build mechanism is being run on windows (powershell or git bash).
26+
27+
``
28+
ARTIFACTS_DIR=/Users/noname/sam-app/.aws-sam/build/HelloWorldFunction
29+
``
30+
31+
Sample Makefile:
32+
33+
````
34+
build-HelloWorldFunction:
35+
touch $(ARTIFACTS_DIR)/somefile
36+
37+
build-HelloWorldFunction2:
38+
touch $(ARTIFACTS_DIR)/somefile2
39+
````
40+
41+
The workflow expects that the name of the function logical id is passed into the workflow using the `options` dictionary. This helps to trigger the correct build target.
42+
43+
#### Step 1: Copy source to a scratch directory
44+
45+
This involves the given source to a temporary scratch directory, so that mutations can be given on the files in the scratch directory, but leave the original source directory untouched.
46+
47+
#### Step 2: Core Workflow Action - Invoke Makefile Build Target
48+
49+
Artifacts directory is created if it doesnt exist and passed into the makefile target process.
50+
51+
```python
52+
self.subprocess_make.run(["--makefile", self.manifest_path, f"build-{self.build_logical_id}"], env={"ARTIFACTS_DIR": self.artifacts_dir}, cwd=self.scratch_dir)
53+
```
54+
55+
It is then the responsibility of the make target to make sure the artifacts are built in the correct manner and dropped into the artifacts directory.
56+
57+
### Challenges
58+
59+
* Is `make` truly platform independent?
60+
* Make comes with linux subsystem on windows and can also be installed with `choco`.
61+
62+
* Does this become a way to introduce plugins, makefile can have any commands in it?
63+
* We only care about certain build targets. so essentially this is a pluggable builder, but nothing beyond that at this point in time.
64+
65+
* Which environment variables are usable in this makefile?
66+
* There are a series of whitelisted environment variables that need to be defined and not be overriden within the Makefile to work. Currently that is just `$ARTIFACTS_DIR`
67+
68+
* Can this be used even for runtimes that have builders associated with it? eg: python3.8?
69+
* Possibly, some changes would be needed be made to way the corresponding builder is picked up in sam cli. If we changed it such that there is a makefile we pick a makefile builder and if not fall back to the specified language builder.
70+
71+
72+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""
2+
Builds provided runtime lambda functions using a Makefile based builder.
3+
"""
4+
5+
from .workflow import CustomMakeWorkflow
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""
2+
Action to build a specific Makefile target
3+
"""
4+
5+
import logging
6+
from pathlib import Path
7+
8+
from aws_lambda_builders.actions import BaseAction, Purpose, ActionFailedError
9+
from .exceptions import MakeFileNotFoundError
10+
from .make import MakeExecutionError
11+
12+
LOG = logging.getLogger(__name__)
13+
14+
15+
class CustomMakeAction(BaseAction):
16+
17+
"""
18+
A Lambda Builder Action that builds and packages a provided runtime project using Make.
19+
"""
20+
21+
NAME = "MakeBuild"
22+
DESCRIPTION = "Running build target on Makefile"
23+
PURPOSE = Purpose.COMPILE_SOURCE
24+
25+
def __init__(self, artifacts_dir, scratch_dir, manifest_path, osutils, subprocess_make, build_logical_id):
26+
"""
27+
:type artifacts_dir: str
28+
:param artifacts_dir: directory where artifacts needs to be stored.
29+
30+
:type scratch_dir: str
31+
:param scratch_dir: an existing (writable) directory for temporary files
32+
33+
:type manifest_path: str
34+
:param manifest_path: path to Makefile of an Make project with the source in same folder.
35+
36+
:type osutils: aws_lambda_builders.workflows.custom_make.utils.OSUtils
37+
:param osutils: An instance of OS Utilities for file manipulation
38+
39+
:type subprocess_make aws_lambda_builders.workflows.custom_make.make.SubprocessMake
40+
:param subprocess_make: An instance of the Make process wrapper
41+
"""
42+
super(CustomMakeAction, self).__init__()
43+
self.artifacts_dir = artifacts_dir
44+
self.scratch_dir = scratch_dir
45+
self.manifest_path = manifest_path
46+
self.osutils = osutils
47+
self.subprocess_make = subprocess_make
48+
self.build_logical_id = build_logical_id
49+
50+
@property
51+
def artifact_dir_path(self):
52+
# This is required when running on windows to determine if we are running in linux
53+
# subsystem or on native cmd or powershell.
54+
if self.osutils.is_windows():
55+
return Path(self.artifacts_dir).as_posix() if self.osutils.which("sh") else self.artifacts_dir
56+
else:
57+
return self.artifacts_dir
58+
59+
def manifest_check(self):
60+
# Check for manifest file presence and if not present raise MakefileNotFoundError
61+
if not self.osutils.exists(self.manifest_path):
62+
raise MakeFileNotFoundError("Makefile not found at {}".format(self.manifest_path))
63+
64+
def execute(self):
65+
"""
66+
Runs the action.
67+
68+
:raises lambda_builders.actions.ActionFailedError: when Make Build fails.
69+
"""
70+
71+
# Check for manifest file
72+
try:
73+
self.manifest_check()
74+
except MakeFileNotFoundError as ex:
75+
raise ActionFailedError(str(ex))
76+
77+
# Create the Artifacts Directory if it doesnt exist.
78+
if not self.osutils.exists(self.artifacts_dir):
79+
self.osutils.makedirs(self.artifacts_dir)
80+
81+
try:
82+
current_env = self.osutils.environ()
83+
LOG.info("Current Artifacts Directory : %s", self.artifact_dir_path)
84+
current_env.update({"ARTIFACTS_DIR": self.artifact_dir_path})
85+
# Export environmental variables that might be needed by other binaries used
86+
# within the Makefile and also specify the makefile to be used as well.
87+
self.subprocess_make.run(
88+
[
89+
"--makefile",
90+
"{}".format(self.manifest_path),
91+
"build-{logical_id}".format(logical_id=self.build_logical_id),
92+
],
93+
env=current_env,
94+
cwd=self.scratch_dir,
95+
)
96+
except MakeExecutionError as ex:
97+
raise ActionFailedError(str(ex))
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""
2+
Series of Exceptions raised by Custom Makefile builder.
3+
"""
4+
5+
6+
class MakeFileNotFoundError(Exception):
7+
pass
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""
2+
Wrapper around calling make through a subprocess.
3+
"""
4+
5+
import logging
6+
7+
LOG = logging.getLogger(__name__)
8+
9+
10+
class MakeExecutionError(Exception):
11+
12+
"""
13+
Exception raised in case Make execution fails.
14+
It will pass on the standard error output from the Make console.
15+
"""
16+
17+
MESSAGE = "Make Failed: {message}"
18+
19+
def __init__(self, **kwargs):
20+
Exception.__init__(self, self.MESSAGE.format(**kwargs))
21+
22+
23+
class SubProcessMake(object):
24+
25+
"""
26+
Wrapper around the Make command line utility, making it
27+
easy to consume execution results.
28+
"""
29+
30+
def __init__(self, osutils, make_exe=None):
31+
"""
32+
:type osutils: aws_lambda_builders.workflows.custom_make.utils.OSUtils
33+
:param osutils: An instance of OS Utilities for file manipulation
34+
35+
:type make_exe: str
36+
:param make_exe: Path to the Make binary. If not set,
37+
the default executable path make will be used
38+
"""
39+
self.osutils = osutils
40+
41+
if make_exe is None:
42+
if osutils.is_windows():
43+
make_exe = "make.exe"
44+
else:
45+
make_exe = "make"
46+
47+
self.make_exe = make_exe
48+
49+
def run(self, args, env=None, cwd=None):
50+
51+
"""
52+
Runs the action.
53+
54+
:type args: list
55+
:param args: Command line arguments to pass to Make
56+
57+
:type env: dict
58+
:param env : environment variables dictionary to be passed into subprocess
59+
60+
:type cwd: str
61+
:param cwd: Directory where to execute the command (defaults to current dir)
62+
63+
:rtype: str
64+
:return: text of the standard output from the command
65+
66+
:raises aws_lambda_builders.workflows.custom_make.make.MakeExecutionError:
67+
when the command executes with a non-zero return code. The exception will
68+
contain the text of the standard error output from the command.
69+
70+
:raises ValueError: if arguments are not provided, or not a list
71+
"""
72+
73+
if not isinstance(args, list):
74+
raise ValueError("args must be a list")
75+
76+
if not args:
77+
raise ValueError("requires at least one arg")
78+
79+
invoke_make = [self.make_exe] + args
80+
81+
LOG.debug("executing Make: %s", invoke_make)
82+
83+
p = self.osutils.popen(invoke_make, stdout=self.osutils.pipe, stderr=self.osutils.pipe, cwd=cwd, env=env)
84+
85+
out, err = p.communicate()
86+
87+
if p.returncode != 0:
88+
raise MakeExecutionError(message=err.decode("utf8").strip())
89+
90+
return out.decode("utf8").strip()
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""
2+
Commonly used utilities
3+
"""
4+
5+
import os
6+
import platform
7+
import subprocess
8+
9+
from aws_lambda_builders.utils import which
10+
11+
12+
class OSUtils(object):
13+
14+
"""
15+
Wrapper around file system functions, to make it easy to
16+
unit test actions in memory
17+
"""
18+
19+
def exists(self, p):
20+
return os.path.exists(p)
21+
22+
def makedirs(self, path):
23+
return os.makedirs(path)
24+
25+
def popen(self, command, stdout=None, stderr=None, env=None, cwd=None):
26+
p = subprocess.Popen(command, stdout=stdout, stderr=stderr, env=env, cwd=cwd)
27+
return p
28+
29+
def environ(self):
30+
return os.environ.copy()
31+
32+
def normpath(self, path):
33+
return os.path.normpath(path)
34+
35+
def abspath(self, path):
36+
return os.path.abspath(path)
37+
38+
@property
39+
def pipe(self):
40+
return subprocess.PIPE
41+
42+
def is_windows(self):
43+
return platform.system().lower() == "windows"
44+
45+
def which(self, executable, executable_search_paths=None):
46+
return which(executable, executable_search_paths=executable_search_paths)

0 commit comments

Comments
 (0)