-
Notifications
You must be signed in to change notification settings - Fork 148
Support for Rust Lambda function with Cargo #67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
""" | ||
Builds Rust Lambda functions using Cargo | ||
""" | ||
|
||
from .workflow import RustCargoWorkflow |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
""" | ||
Actions for the Rust Cargo workflow | ||
""" | ||
import subprocess | ||
import logging | ||
import sys | ||
import os | ||
import shutil | ||
import platform | ||
|
||
from aws_lambda_builders.actions import BaseAction, Purpose, ActionFailedError | ||
from .cargo import CargoParser, CargoFileNotFoundError, CargoParsingError, CargoValidationError, PathNotFoundError | ||
|
||
LOG = logging.getLogger(__name__) | ||
TARGET_PLATFORM = "x86_64-unknown-linux-musl" | ||
RUNTIME_METADATA_FILE = "runtime_release" | ||
|
||
|
||
class CargoValidator(BaseAction): | ||
""" | ||
Validates that Cargo.toml is configured correctly to build a Lambda application | ||
""" | ||
NAME = 'CargoValidator' | ||
PURPOSE = Purpose.RESOLVE_DEPENDENCIES | ||
|
||
def __init__(self, source_dir, manifest_path, runtime): | ||
self.source_dir = source_dir | ||
self.manifest_path = manifest_path | ||
self.runtime = runtime | ||
self.cargo_parser = CargoParser(manifest_path) | ||
|
||
def execute(self): | ||
try: | ||
self.cargo_parser.validate(print_warnings=True) | ||
except (CargoFileNotFoundError, CargoParsingError, CargoValidationError) as ex: | ||
raise ActionFailedError(str(ex)) | ||
|
||
|
||
class RustCargoBuildAction(BaseAction): | ||
""" | ||
Uses Cargo to build a project | ||
""" | ||
NAME = 'RustCargoBuildAction' | ||
PURPOSE = Purpose.COMPILE_SOURCE | ||
|
||
def __init__(self, source_dir, manifest_path, runtime): | ||
self.source_dir = source_dir | ||
self.manifest_path = manifest_path | ||
self.runtime = runtime | ||
self.cargo_parser = CargoParser(manifest_path) | ||
|
||
def execute(self): | ||
try: | ||
LOG.info("Starting cargo release build for %s", self.source_dir) | ||
cmd = "cargo build --release" | ||
# if we are running on linux we assume that it's the Amazon Linux Docker container. | ||
# Otherwise we set the target pltform to musl linux. | ||
if platform.system() != "Linux": | ||
cmd += " --target " + TARGET_PLATFORM | ||
subprocess.run( | ||
cmd, | ||
stderr=sys.stderr, | ||
stdout=sys.stderr, | ||
shell=True, | ||
cwd=self.source_dir, | ||
check=True, | ||
) | ||
LOG.info("Built executable: %s", self.cargo_parser.get_executable_name()) | ||
#LOG.info("Done: %s", build_output) | ||
except subprocess.CalledProcessError as ex: | ||
LOG.info("Error while executing build: %i\n%s", ex.returncode, ex.output) | ||
raise ActionFailedError(str(ex)) | ||
|
||
|
||
class CopyAndRenameExecutableAction(BaseAction): | ||
NAME = 'CopyAndRenameExecutableAction' | ||
PURPOSE = Purpose.COPY_SOURCE | ||
|
||
def __init__(self, source_dir, atrifact_path, manifest_path, runtime): | ||
self.source_dir = source_dir | ||
self.manifest_path = manifest_path | ||
self.artifact_path = atrifact_path | ||
self.runtime = runtime | ||
self.cargo_parser = CargoParser(manifest_path) | ||
|
||
def execute(self): | ||
try: | ||
target = TARGET_PLATFORM if platform.system() != "Linux" else "" | ||
bin_path = self.cargo_parser.get_executable_path(target) | ||
LOG.info("Copying executable from %s to %s", bin_path, self.artifact_path) | ||
shutil.copyfile(bin_path, os.path.join(self.artifact_path, "bootstrap"), follow_symlinks=True) | ||
|
||
target_dir = self.cargo_parser.get_target_path(target) | ||
metadata_file = os.path.join(target_dir, RUNTIME_METADATA_FILE) | ||
LOG.info("Looking for metdata file: %s", metadata_file) | ||
if os.path.isfile(metadata_file): | ||
LOG.info("Found runtime metdata file, copying to %s", self.artifact_path) | ||
shutil.copyfile(metadata_file, os.path.join(self.artifact_path, | ||
RUNTIME_METADATA_FILE), follow_symlinks=True) | ||
except PathNotFoundError as ex: | ||
raise ActionFailedError(str(ex)) | ||
except (OSError, shutil.SpecialFileError) as ex: | ||
raise ActionFailedError(str(ex)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
""" | ||
Cargo validator utility | ||
""" | ||
import os | ||
import logging | ||
|
||
import toml | ||
|
||
LOG = logging.getLogger(__name__) | ||
|
||
|
||
class PackagerError(Exception): | ||
pass | ||
|
||
|
||
class CargoFileNotFoundError(PackagerError): | ||
def __init__(self, cargo_path): | ||
super(CargoFileNotFoundError, self).__init__( | ||
'Cargo file not found: %s' % cargo_path) | ||
|
||
|
||
class CargoParsingError(PackagerError): | ||
def __init__(self, ex): | ||
super(CargoParsingError, self).__init__( | ||
'Could not parse cargo file: %s' % ex) | ||
|
||
|
||
class CargoValidationError(PackagerError): | ||
def __init__(self, msg): | ||
super(CargoValidationError, self).__init__( | ||
'Invalid cargo file: %s' % msg) | ||
|
||
|
||
class PathNotFoundError(PackagerError): | ||
def __init__(self, bin_path): | ||
super(PathNotFoundError, self).__init__( | ||
'Path not found: %s' % bin_path) | ||
|
||
|
||
class CargoParser(object): | ||
def __init__(self, manifest): | ||
""" | ||
Given the path to a Cargo.toml file parses its contents. Use | ||
the validate() method sanity-check the cargo manifest for | ||
a Lambda function build. | ||
|
||
:raises CargoParsingError: If the Cargo.toml file could not be | ||
found or parsed. | ||
""" | ||
self.manifest = manifest | ||
self._parse(manifest) | ||
|
||
def _parse(self, manifest): | ||
if not os.path.isfile(manifest): | ||
raise CargoFileNotFoundError(manifest) | ||
|
||
try: | ||
with open(manifest, 'r') as cargo: | ||
package_properties = toml.load(cargo) | ||
LOG.debug("Cargo file: %s", package_properties) | ||
self.cargo = package_properties | ||
except (TypeError, toml.TomlDecodeError) as ex: | ||
raise CargoParsingError(ex) | ||
except ValueError as ex: | ||
raise CargoParsingError(ex) | ||
|
||
def get_executable_name(self): | ||
""" | ||
Returns the name of the executable file generated by the | ||
cargo build process | ||
""" | ||
self.validate(print_warnings=False) | ||
|
||
bin_name = self.cargo["package"]["name"] | ||
if "bin" in self.cargo: | ||
bin_props = self.cargo["bin"] | ||
for prop in bin_props: | ||
if "name" in prop: | ||
bin_name = prop["name"] | ||
|
||
return bin_name | ||
|
||
def get_target_path(self, target_platform): | ||
""" | ||
Returns the full path to the target directory where the binary is stored | ||
|
||
:type target_platform: str | ||
:param target_platform: | ||
The --target parameter that was passed to the cargo build process. | ||
|
||
:raise ExecutableNotFound: If the executable file does not exist. | ||
""" | ||
self.validate(print_warnings=False) | ||
proj_path = os.path.dirname(os.path.abspath(self.manifest)) | ||
target_dir = os.path.join(proj_path, "target", target_platform, "release") | ||
|
||
if not os.path.isdir(target_dir): | ||
raise PathNotFoundError(target_dir) | ||
|
||
return target_dir | ||
|
||
def get_executable_path(self, target_platform): | ||
""" | ||
Returns the full path to the compiled executable. | ||
|
||
:type target_platform: str | ||
:param target_platform: | ||
The --target parameter that was passed to the cargo build process. | ||
|
||
:raise ExecutableNotFound: If the executable file does not exist. | ||
""" | ||
self.validate(print_warnings=False) | ||
|
||
bin_name = self.get_executable_name() | ||
proj_path = os.path.dirname(os.path.abspath(self.manifest)) | ||
bin_full_path = os.path.join(proj_path, "target", target_platform, "release", bin_name) | ||
|
||
if not os.path.isfile(bin_full_path): | ||
raise PathNotFoundError(bin_full_path) | ||
|
||
return bin_full_path | ||
|
||
def validate(self, print_warnings=True): | ||
""" | ||
Validates a Cargo.toml file and optionally prints out warnings | ||
and suggestions for the Cargo structure. | ||
|
||
:type print_warnings: bool | ||
:param print_warnings: | ||
Whether the method should print warnings in the LOG object. | ||
|
||
:raise CargoValidationError: If the cargo file was not parsed correctly | ||
or it does not represent a valid package. | ||
""" | ||
if not self.cargo: | ||
raise CargoValidationError("Cargo file not parsed") | ||
|
||
if "package" not in self.cargo: | ||
raise CargoValidationError("Manifest does not contain package table") | ||
|
||
package = self.cargo["package"] | ||
if "name" not in package: | ||
raise CargoValidationError("Missing name property for package") | ||
|
||
if "bin" not in self.cargo: | ||
if print_warnings: | ||
LOG.warning(("Missing [[bin]] section from Cargo.toml. " | ||
"The builder will rename the executable from %s " | ||
"to bootstrap. Consider including a [[bin]] " | ||
"section in the Cargo.toml file with a " | ||
"name = \"bootstrap\" property."), package["name"]) | ||
else: | ||
bin_props = self.cargo["bin"] | ||
name_found = False | ||
for prop in bin_props: | ||
if "name" in prop: | ||
name_found = True | ||
break | ||
|
||
if not name_found and print_warnings: | ||
LOG.warning(("Missing name property from [[bin]] section " | ||
"in Cargo.toml file. Consider including a name " | ||
"property and setting its value to bootstrap. This " | ||
"is the executable name AWS Lambda expects.")) | ||
|
||
if "dependencies" in self.cargo: | ||
deps = self.cargo["dependencies"] | ||
if "lambda_runtime" not in deps and print_warnings: | ||
LOG.warning("""lambda_runtime is not included as a dependency in | ||
Your Cargo.toml file.""") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
""" | ||
Rust Cargo Workflow | ||
""" | ||
import os | ||
|
||
from aws_lambda_builders.workflow import BaseWorkflow, Capability | ||
from aws_lambda_builders.actions import CopySourceAction | ||
|
||
from .actions import RustCargoBuildAction, CopyAndRenameExecutableAction, CargoValidator | ||
|
||
|
||
class RustCargoWorkflow(BaseWorkflow): | ||
|
||
NAME = "RustCargoWorkflow" | ||
CAPABILITY = Capability(language="rust", | ||
dependency_manager="cargo", | ||
application_framework=None) | ||
|
||
def __init__(self, | ||
source_dir, | ||
artifacts_dir, | ||
scratch_dir, | ||
manifest_path, | ||
runtime=None, **kwargs): | ||
|
||
super(RustCargoWorkflow, self).__init__(source_dir, | ||
artifacts_dir, | ||
scratch_dir, | ||
manifest_path, | ||
runtime=runtime, | ||
**kwargs) | ||
|
||
self.actions = [ | ||
CargoValidator(source_dir, manifest_path, runtime), | ||
RustCargoBuildAction(source_dir, manifest_path, runtime), | ||
CopyAndRenameExecutableAction(source_dir, self.artifacts_dir, manifest_path, runtime), | ||
] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
six~=1.11 | ||
toml~=0.10.0 |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.