Skip to content
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

feat: Add support to build automatically npm dependencies #292

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,11 @@ source_path = [
"!vendor/colorful-.+.dist-info/.*",
"!vendor/colorful/__pycache__/?.*",
]
}, {
path = "src/nodejs14.x-app1",
npm_requirements = true,
nom_tmp_dir = "/tmp/dir/location"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
nom_tmp_dir = "/tmp/dir/location"
npm_tmp_dir = "/tmp/dir/location"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

prefix_in_zip = "foo/bar1",
}, {
path = "src/python3.8-app3",
commands = [
Expand Down
120 changes: 118 additions & 2 deletions package.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,18 @@ def pip_requirements_step(path, prefix=None, required=False, tmp_dir=None):
step('pip', runtime, requirements, prefix, tmp_dir)
hash(requirements)

def npm_requirements_step(path, prefix=None, required=False, tmp_dir=None):
requirements = path
if os.path.isdir(path):
requirements = os.path.join(path, 'package.json')
if not os.path.isfile(requirements):
if required:
raise RuntimeError(
'File not found: {}'.format(requirements))
else:
step('npm', runtime, requirements, prefix, tmp_dir)
hash(requirements)

def commands_step(path, commands):
if not commands:
return
Expand Down Expand Up @@ -717,6 +729,9 @@ def commands_step(path, commands):
if runtime.startswith('python'):
pip_requirements_step(
os.path.join(path, 'requirements.txt'))
elif runtime.startswith('nodejs'):
npm_requirements_step(
os.path.join(path, 'package.json'))
step('zip', path, None)
hash(path)

Expand All @@ -731,6 +746,7 @@ def commands_step(path, commands):
else:
prefix = claim.get('prefix_in_zip')
pip_requirements = claim.get('pip_requirements')
npm_requirements = claim.get('npm_package_json')
runtime = claim.get('runtime', query.runtime)

if pip_requirements and runtime.startswith('python'):
Expand All @@ -740,6 +756,13 @@ def commands_step(path, commands):
pip_requirements_step(pip_requirements, prefix,
required=True, tmp_dir=claim.get('pip_tmp_dir'))

if npm_requirements and runtime.startswith('nodejs'):
if isinstance(npm_requirements, bool) and path:
npm_requirements_step(path, prefix, required=True, tmp_dir=claim.get('npm_tmp_dir'))
else:
npm_requirements_step(npm_requirements, prefix,
required=True, tmp_dir=claim.get('npm_tmp_dir'))

if path:
step('zip', path, prefix)
if patterns:
Expand Down Expand Up @@ -793,6 +816,16 @@ def execute(self, build_plan, zip_stream, query):
else:
# XXX: timestamp=0 - what actually do with it?
zs.write_dirs(rd, prefix=prefix, timestamp=0)
elif cmd == 'npm':
runtime, npm_requirements, prefix, tmp_dir = action[1:]
with install_npm_requirements(query, npm_requirements, tmp_dir) as rd:
if rd:
if pf:
self._zip_write_with_filter(zs, pf, rd, prefix,
timestamp=0)
else:
# XXX: timestamp=0 - what actually do with it?
zs.write_dirs(rd, prefix=prefix, timestamp=0)
elif cmd == 'sh':
r, w = os.pipe()
side_ch = os.fdopen(r)
Expand Down Expand Up @@ -934,6 +967,89 @@ def install_pip_requirements(query, requirements_file, tmp_dir):
yield temp_dir


@contextmanager
def install_npm_requirements(query, requirements_file, tmp_dir):
# TODO:
# 1. Emit files instead of temp_dir

if not os.path.exists(requirements_file):
yield
return

runtime = query.runtime
artifacts_dir = query.artifacts_dir
temp_dir = query.temp_dir
docker = query.docker
docker_image_tag_id = None

if docker:
docker_file = docker.docker_file
docker_image = docker.docker_image
docker_build_root = docker.docker_build_root

if docker_image:
ok = False
while True:
output = check_output(docker_image_id_command(docker_image))
if output:
docker_image_tag_id = output.decode().strip()
log.debug("DOCKER TAG ID: %s -> %s",
docker_image, docker_image_tag_id)
ok = True
if ok:
break
docker_cmd = docker_build_command(
build_root=docker_build_root,
docker_file=docker_file,
tag=docker_image,
)
check_call(docker_cmd)
ok = True
elif docker_file or docker_build_root:
raise ValueError('docker_image must be specified '
'for a custom image future references')

log.info('Installing npm requirements: %s', requirements_file)
with tempdir(tmp_dir) as temp_dir:
requirements_filename = os.path.basename(requirements_file)
target_file = os.path.join(temp_dir, requirements_filename)
shutil.copyfile(requirements_file, target_file)

subproc_env = None
if not docker and OSX:
subproc_env = os.environ.copy()

# Install dependencies into the temporary directory.
with cd(temp_dir):
npm_command = ['npm', 'install']
if docker:
with_ssh_agent = docker.with_ssh_agent
chown_mask = '{}:{}'.format(os.getuid(), os.getgid())
shell_command = [shlex_join(npm_command), '&&',
shlex_join(['chown', '-R',
chown_mask, '.'])]
shell_command = [' '.join(shell_command)]
check_call(docker_run_command(
'.', shell_command, runtime,
image=docker_image_tag_id,
shell=True, ssh_agent=with_ssh_agent
))
else:
cmd_log.info(shlex_join(npm_command))
log_handler and log_handler.flush()
try:
check_call(npm_command, env=subproc_env)
except FileNotFoundError as e:
raise RuntimeError(
"Nodejs interpreter version equal "
"to defined lambda runtime ({}) should be "
"available in system PATH".format(runtime)
) from e

os.remove(target_file)
yield temp_dir


def docker_image_id_command(tag):
""""""
docker_cmd = ['docker', 'images', '--format={{.ID}}', tag]
Expand Down Expand Up @@ -1011,7 +1127,7 @@ def docker_run_command(build_root, command, runtime,
])

if not image:
image = 'lambci/lambda:build-{}'.format(runtime)
image = 'public.ecr.aws/sam/build-{}'.format(runtime)

docker_cmd.append(image)

Expand Down Expand Up @@ -1128,7 +1244,7 @@ def prepare_command(args):
def build_command(args):
"""
Builds a zip file from the source_dir or source_file.
Installs dependencies with pip automatically.
Installs dependencies with pip or npm automatically.
"""

log = logging.getLogger('build')
Expand Down