Skip to content

Commit 5c0b9bd

Browse files
wchengrumildanielborgoatsriram-mv
authored
Merge develop into master (#255)
* feat: Allow Python runtime to build without requiring a manifest (#243) * Allow Python to continue build without requirements.txt * Allow missing requirements.txt file for Python builds * Ruby optional Gemfile and test * Style changes * Add unit tests and additional comments * Fix assertLogs() not compatible with Python2.7 * Fix assertion string * Remove unused exception * Integ. test make sure no artifacts dir has no additional files after build * Kick off build * Check for manifest and skip package actions if not found * Revert Ruby changes until further discussion is had. Make Python workflow more readable. * Remove unused import * Whitespace fix * feat: Allow Ruby runtime to build without requiring a manifest (#245) * Allow Python to continue build without requirements.txt * Allow missing requirements.txt file for Python builds * Ruby optional Gemfile and test * Style changes * Add unit tests and additional comments * Fix assertLogs() not compatible with Python2.7 * Fix assertion string * Remove unused exception * Integ. test make sure no artifacts dir has no additional files after build * Kick off build * Check for manifest and skip package actions if not found * Revert Ruby changes until further discussion is had. Make Python workflow more readable. * Remove unused import * Whitespace fix * Readability changes for Ruby workflow * Remove magic number. Add link to Bundler error codes * Moved var declaration * docs: Guidance on integrating with Lambda Builders (#242) * fix: README - showcase Makefile support (#247) * Update VS2017 to VS2019 (#244) * fix: Pip not resolving local packages (#250) * Fix local packages not being built * Add int. test to catch future local dependency issues * Specify test requirements path from cwd * Removed redundant/superset pattern * Document the pip regex pattern change * Updated integ to match use case * Tests to check backward comp. * chore: bump version to 1.5.0 (#254) Co-authored-by: Daniel Mil <[email protected]> Co-authored-by: Giorgio Azzinnaro <[email protected]> Co-authored-by: Sriram Madapusi Vasudevan <[email protected]>
1 parent 2f9e46a commit 5c0b9bd

File tree

21 files changed

+271
-54
lines changed

21 files changed

+271
-54
lines changed

.appveyor.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
version: 1.0.{build}
22
image:
3-
- Visual Studio 2017
3+
- Visual Studio 2019
44
- Ubuntu
55

66
environment:
77
GOVERSION: 1.11
88
GRADLE_OPTS: -Dorg.gradle.daemon=false
9-
nodejs_version: "8.10.0"
9+
nodejs_version: "10.10.0"
1010

1111
matrix:
1212

@@ -49,7 +49,7 @@ for:
4949

5050
install:
5151
# To run Nodejs workflow integ tests
52-
- ps: Install-Product node 8.10
52+
- ps: Install-Product node $env:nodejs_version
5353

5454
- "set PATH=%PYTHON%;%PYTHON%\\Scripts;%PYTHON%\\bin;%PATH%"
5555
- "%PYTHON%\\python.exe -m pip install -r requirements/dev.txt"

DESIGN.md

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99

1010
Lambda Builders is a separate project that contains scripts to build Lambda functions, given a source location. It was
11-
built as part of SAM CLI `sam build` command.Read https://github.com/awslabs/aws-sam-cli/pull/743 for design document
11+
built as part of SAM CLI `sam build` command. Read https://github.com/awslabs/aws-sam-cli/pull/743 for design document
1212
explaining how Lambda Builders work in context of SAM CLI.
1313

1414
This project has certain attributes that make it unique:
@@ -60,22 +60,36 @@ to make use of the default implementation. It helps reduce the variance in behav
6060
customers with a standard expectation.
6161

6262
#### Command Line Interface (Internal)
63-
This library provides a wrapper CLI interface for convenience. This interface is not supported at the moment. So we
64-
don't provide any guarantees of back compatibility.
63+
This library provides a wrapper CLI interface for convenience. This interface **is not supported** at the moment. So we
64+
don't provide any guarantees of back compatibility.
6565

6666
It is a very thin wrapper over the library. It is meant to integrate
6767
with tools written in other programming languages that can't import Python libraries directly. The CLI provides
68-
a JSON-RPC interface over stdin/stdout to invoke the builder and get response.
68+
[a JSON-RPC 2.0 interface](https://www.jsonrpc.org/specification)
69+
over stdin/stdout to invoke the builder and get a response.
6970

70-
**Request Format**
71+
The CLI should be installed and available on the path:
72+
73+
```shell
74+
pip install aws-lambda-builders
75+
```
76+
77+
Each execution of `aws-lambda-builders` handles one JSON-RPC request.
78+
Provide the whole body of the request via stdin, terminated by `EOF`.
79+
80+
Currently, the only exposed method is `LambdaBuilder.build`.
81+
It closely maps to the
82+
[Python method `LambdaBuilder.build` in `aws_lambda_builders/builder.py`](aws_lambda_builders/builder.py).
83+
84+
#### Request Format
7185

7286
```json
7387
{
7488
"jsonrpc": "2.0",
7589
"method": "LambdaBuilder.build",
7690
"id": 1,
7791
"params": {
78-
"__protocol_version": "0.1", // Expected version of RPC protocol
92+
"__protocol_version": "0.3", // expected version of RPC protocol - from aws_lambda_builders/__main__.py
7993
"capability": {
8094
"language": "<programming language>",
8195
"dependency_manager": "<programming language framework>",
@@ -86,13 +100,13 @@ a JSON-RPC interface over stdin/stdout to invoke the builder and get response.
86100
"scratch_dir": "/path/to/tmp",
87101
"manifest_path": "/path/to/manifest.json",
88102
"runtime": "Function's runtime ex: nodejs8.10",
89-
"optimizations": {}, // not supported
90-
"options": {} // not supported
103+
"optimizations": {}, // not supported
104+
"options": {} // depending on the workflow
91105
}
92106
}
93107
```
94108

95-
**Successful Response Format**
109+
#### Successful Response Format
96110

97111
```json
98112
{
@@ -102,7 +116,7 @@ a JSON-RPC interface over stdin/stdout to invoke the builder and get response.
102116
}
103117
```
104118

105-
**Error Response Format**
119+
#### Error Response Format
106120

107121
```json
108122
{
@@ -116,7 +130,7 @@ a JSON-RPC interface over stdin/stdout to invoke the builder and get response.
116130
}
117131
```
118132

119-
**Error Codes**:
133+
#### Error Codes
120134

121135
Error codes returned by the application are similar to HTTP Status Codes.
122136

@@ -125,6 +139,19 @@ Error codes returned by the application are similar to HTTP Status Codes.
125139
- 505 - RPC Protocol unsupported
126140
- -32601 - Method unsupported (standard JSON-RPC protocol error code)
127141

142+
#### Params
143+
144+
##### `capability`
145+
The 3-tuple `capability` is used to identify different workflows.
146+
As of today, `application_framework` is unused and may be ignored.
147+
148+
##### `options`
149+
The parameter `options` should be configured depending on the selected workflow/capability.
150+
151+
For more detail around the capabilities and options,
152+
check out the corresponding _design document_ and `workflow.py` for
153+
[the workflows you're interested in](aws_lambda_builders/workflows).
154+
128155
### Project Meta
129156
#### Directory Structure
130157
This project's directories are laid as follows:
@@ -173,6 +200,3 @@ And essentially drop into the builders package (or maybe we can have a notion of
173200
- **builder**: The entire project is called builder, because it can build Lambda functions
174201
- **workflows**: Building for each language+framework combination is defined using a workflow.
175202
- **actions**: A workflow is implemented as a chain of actions.
176-
177-
178-

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ Lambda Builders currently contains the following workflows
1616
* Go with Dep
1717
* Go with Mod
1818

19+
In Addition to above workflows, AWS Lambda Builders also supports *Custom Workflows* through a Makefile.
20+
1921
Lambda Builders is the brains behind the `sam build` command from [AWS SAM CLI](https://github.com/awslabs/aws-sam-cli)
2022

23+
### Integrating with Lambda Builders
24+
25+
Lambda Builders is a Python library.
26+
It additionally exposes a JSON-RPC 2.0 interface to use from other languages.
27+
28+
If you intend to integrate with Lambda Builders,
29+
check out [this section of the DESIGN DOCUMENT](DESIGN.md#builders-library).
30+
31+
### Contributing
32+
2133
If you are a developer and interested in contributing, read the [DESIGN DOCUMENT](./DESIGN.md) to understand how this works.

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.4.0"
4+
__version__ = "1.5.0"
55
RPC_PROTOCOL_VERSION = "0.3"

aws_lambda_builders/workflows/python_pip/packager.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ class InvalidSourceDistributionNameError(PackagerError):
3535

3636

3737
class RequirementsFileNotFoundError(PackagerError):
38+
"""
39+
Exceptions is no longer raised.
40+
Keeping it here because this exception is 'public' and could still be used by a customer.
41+
"""
42+
3843
def __init__(self, requirements_path):
3944
super(RequirementsFileNotFoundError, self).__init__("Requirements file not found: %s" % requirements_path)
4045

@@ -131,9 +136,6 @@ def build_dependencies(self, artifacts_dir_path, scratch_dir_path, requirements_
131136
# by finding/creating a virtualenv of the correct version and when
132137
# pip is called set the appropriate env vars.
133138

134-
if not self.osutils.file_exists(requirements_path):
135-
raise RequirementsFileNotFoundError(requirements_path)
136-
137139
self._dependency_builder.build_site_packages(requirements_path, artifacts_dir_path, scratch_dir_path)
138140

139141

@@ -594,7 +596,10 @@ def main(self, args, env_vars=None, shim=None):
594596
class PipRunner(object):
595597
"""Wrapper around pip calls used by chalice."""
596598

597-
_LINK_IS_DIR_PATTERN = "Processing (.+?)\n Link is a directory, ignoring download_dir"
599+
# Update regex pattern to correspond with the updated output from pip
600+
# Specific commit:
601+
# https://github.com/pypa/pip/commit/b28e2c4928cc62d90b738a4613886fb1e2ad6a81#diff-5225c8e359020adb25dfc8c7a505950fd649c6c5775789c6f6517f7913f94542L529
602+
_LINK_IS_DIR_PATTERNS = ["Processing (.+?)\n"]
598603

599604
def __init__(self, python_exe, pip, osutils=None):
600605
if osutils is None:
@@ -642,10 +647,16 @@ def download_all_dependencies(self, requirements_filename, directory):
642647
package_name = match.group(1)
643648
raise NoSuchPackageError(str(package_name))
644649
raise PackageDownloadError(error)
650+
651+
# Extract local packages from pip output.
652+
# Iterate over possible pip outputs depending on pip version.
645653
stdout = out.decode()
646-
matches = re.finditer(self._LINK_IS_DIR_PATTERN, stdout)
647-
for match in matches:
648-
wheel_package_path = str(match.group(1))
654+
wheel_package_paths = set()
655+
for pattern in self._LINK_IS_DIR_PATTERNS:
656+
for match in re.finditer(pattern, stdout):
657+
wheel_package_paths.add(str(match.group(1)))
658+
659+
for wheel_package_path in wheel_package_paths:
649660
# Looks odd we do not check on the error status of building the
650661
# wheel here. We can assume this is a valid package path since
651662
# we already passed the pip download stage. This stage would have

aws_lambda_builders/workflows/python_pip/workflow.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
"""
22
Python PIP Workflow
33
"""
4+
import logging
5+
46
from aws_lambda_builders.workflow import BaseWorkflow, Capability
57
from aws_lambda_builders.actions import CopySourceAction
68
from aws_lambda_builders.workflows.python_pip.validator import PythonRuntimeValidator
79

810
from .actions import PythonPipBuildAction
11+
from .utils import OSUtils
12+
13+
LOG = logging.getLogger(__name__)
914

1015

1116
class PythonPipWorkflow(BaseWorkflow):
@@ -59,16 +64,26 @@ class PythonPipWorkflow(BaseWorkflow):
5964
".idea",
6065
)
6166

62-
def __init__(self, source_dir, artifacts_dir, scratch_dir, manifest_path, runtime=None, **kwargs):
67+
def __init__(self, source_dir, artifacts_dir, scratch_dir, manifest_path, runtime=None, osutils=None, **kwargs):
6368

6469
super(PythonPipWorkflow, self).__init__(
6570
source_dir, artifacts_dir, scratch_dir, manifest_path, runtime=runtime, **kwargs
6671
)
6772

68-
self.actions = [
69-
PythonPipBuildAction(artifacts_dir, scratch_dir, manifest_path, runtime, binaries=self.binaries),
70-
CopySourceAction(source_dir, artifacts_dir, excludes=self.EXCLUDED_FILES),
71-
]
73+
if osutils is None:
74+
osutils = OSUtils()
75+
76+
if osutils.file_exists(manifest_path):
77+
# If a requirements.txt exists, run pip builder before copy action.
78+
self.actions = [
79+
PythonPipBuildAction(artifacts_dir, scratch_dir, manifest_path, runtime, binaries=self.binaries),
80+
CopySourceAction(source_dir, artifacts_dir, excludes=self.EXCLUDED_FILES),
81+
]
82+
else:
83+
LOG.warning("requirements.txt file not found. Continuing the build without dependencies.")
84+
self.actions = [
85+
CopySourceAction(source_dir, artifacts_dir, excludes=self.EXCLUDED_FILES),
86+
]
7287

7388
def get_validators(self):
7489
return [PythonRuntimeValidator(runtime=self.runtime)]

aws_lambda_builders/workflows/ruby_bundler/bundler.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66

77
LOG = logging.getLogger(__name__)
88

9+
"""
10+
Bundler error codes can be found here:
11+
https://github.com/rubygems/bundler/blob/3f0638c6c8d340c2f2405ecb84eb3b39c433e36e/lib/bundler/errors.rb#L36
12+
"""
13+
GEMFILE_NOT_FOUND = 10
14+
915

1016
class BundlerExecutionError(Exception):
1117
"""
@@ -51,7 +57,15 @@ def run(self, args, cwd=None):
5157
out, _ = p.communicate()
5258

5359
if p.returncode != 0:
54-
# Bundler has relevant information in stdout, not stderr.
55-
raise BundlerExecutionError(message=out.decode("utf8").strip())
60+
if p.returncode == GEMFILE_NOT_FOUND:
61+
LOG.warning("Gemfile not found. Continuing the build without dependencies.")
62+
63+
# Clean up '.bundle' dir that gets generated before the build fails
64+
check_dir = self.osutils.get_bundle_dir(cwd)
65+
if self.osutils.directory_exists(check_dir):
66+
self.osutils.remove_directory(check_dir)
67+
else:
68+
# Bundler has relevant information in stdout, not stderr.
69+
raise BundlerExecutionError(message=out.decode("utf8").strip())
5670

5771
return out.decode("utf8").strip()

aws_lambda_builders/workflows/ruby_bundler/utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import platform
77
import tarfile
88
import subprocess
9+
import shutil
910

1011

1112
class OSUtils(object):
@@ -38,3 +39,12 @@ def abspath(self, path):
3839

3940
def is_windows(self):
4041
return platform.system().lower() == "windows"
42+
43+
def directory_exists(self, dirpath):
44+
return os.path.exists(dirpath) and os.path.isdir(dirpath)
45+
46+
def remove_directory(self, dirpath):
47+
shutil.rmtree(dirpath)
48+
49+
def get_bundle_dir(self, cwd):
50+
return os.path.join(cwd, ".bundle")

tests/functional/workflows/ruby_bundler/test_ruby_utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,28 @@ def test_popen_can_accept_cwd(self):
4848
out, err = p.communicate()
4949
self.assertEqual(p.returncode, 0)
5050
self.assertEqual(out.decode("utf8").strip(), os.path.abspath(testdata_dir))
51+
52+
def test_returns_true_if_directory_exists(self):
53+
testdata_dir = os.path.dirname(__file__)
54+
out = self.osutils.directory_exists(testdata_dir)
55+
self.assertTrue(out)
56+
57+
def test_returns_false_if_directory_not_found(self):
58+
testdata_dir = os.path.join(os.path.dirname(__file__), "test")
59+
out = self.osutils.directory_exists(testdata_dir)
60+
self.assertFalse(out)
61+
62+
def test_returns_bundle_directory(self):
63+
testdata_dir = os.path.dirname(__file__)
64+
out = self.osutils.get_bundle_dir(testdata_dir)
65+
self.assertEqual(out, os.path.join(os.path.dirname(__file__), ".bundle"))
66+
67+
def test_removes_directory_if_exists(self):
68+
test_dir = tempfile.mkdtemp()
69+
bundle_dir = os.path.join(test_dir, ".bundle")
70+
expected_files = set(os.listdir(test_dir))
71+
os.mkdir(bundle_dir)
72+
self.osutils.remove_directory(bundle_dir)
73+
actual_files = set(os.listdir(test_dir))
74+
shutil.rmtree(test_dir)
75+
self.assertEqual(actual_files, expected_files)

tests/integration/workflows/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)