Skip to content

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
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions aws_lambda_builders/workflows/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
import aws_lambda_builders.workflows.python_pip
import aws_lambda_builders.workflows.nodejs_npm
import aws_lambda_builders.workflows.ruby_bundler
import aws_lambda_builders.workflows.rust_cargo
5 changes: 5 additions & 0 deletions aws_lambda_builders/workflows/rust_cargo/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""
Builds Rust Lambda functions using Cargo
"""

from .workflow import RustCargoWorkflow
103 changes: 103 additions & 0 deletions aws_lambda_builders/workflows/rust_cargo/actions.py
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))
170 changes: 170 additions & 0 deletions aws_lambda_builders/workflows/rust_cargo/cargo.py
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.""")
37 changes: 37 additions & 0 deletions aws_lambda_builders/workflows/rust_cargo/workflow.py
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),
]
1 change: 1 addition & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
six~=1.11
toml~=0.10.0