Skip to content

Commit 90f7b49

Browse files
committed
feat: Added ZipContentFilter class to apply patterns filtering
1 parent 0c8f26c commit 90f7b49

File tree

1 file changed

+209
-26
lines changed

1 file changed

+209
-26
lines changed

package.py

+209-26
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
raise RuntimeError("A python version 3.7 or newer is required")
77

88
import os
9+
import re
910
import time
1011
import stat
1112
import json
@@ -16,6 +17,7 @@
1617
import argparse
1718
import datetime
1819
import tempfile
20+
import operator
1921
import platform
2022
import subprocess
2123
from subprocess import check_call
@@ -321,7 +323,8 @@ def write_files(self, files_stream, prefix=None, timestamp=None):
321323
Expects just files stream, directories will be created automatically
322324
"""
323325
self._ensure_open()
324-
raise NotImplementedError
326+
for file_path, arcname in files_stream:
327+
self._write_file(file_path, prefix, arcname, timestamp)
325328

326329
def write_file(self, file_path, prefix=None, name=None, timestamp=None):
327330
"""
@@ -346,7 +349,7 @@ def _write_file(self, file_path, prefix=None, name=None, timestamp=None):
346349

347350
def write_file_obj(self, file_path, data, prefix=None, timestamp=None):
348351
"""
349-
Write a data to a zip archive by a full qualified file path
352+
Write a data to a zip archive by a full qualified archive file path
350353
"""
351354
self._ensure_open()
352355
raise NotImplementedError
@@ -486,6 +489,85 @@ def str_int_to_timestamp(s):
486489
################################################################################
487490
# Building
488491

492+
def patterns_list(patterns):
493+
if isinstance(patterns, str):
494+
return list(map(str.strip, patterns.splitlines()))
495+
return patterns
496+
497+
498+
class ZipContentFilter:
499+
""""""
500+
501+
def __init__(self):
502+
self._rules = None
503+
self._excludes = set()
504+
self._logger = logging.getLogger('zip')
505+
506+
def compile(self, patterns):
507+
rules = []
508+
for p in patterns_list(patterns):
509+
self._logger.debug("pattern '%s'", p)
510+
if p.startswith('!'):
511+
r = re.compile(p[1:])
512+
rules.append((operator.not_, r))
513+
else:
514+
r = re.compile(p)
515+
rules.append((None, r))
516+
self._rules = rules
517+
518+
def filter(self, path, prefix=None):
519+
path = os.path.normpath(path)
520+
if prefix:
521+
prefix = os.path.normpath(prefix)
522+
rules = self._rules
523+
524+
def norm_path(path, root, filename=None):
525+
p = os.path.relpath(root, path)
526+
if prefix:
527+
p = os.path.normpath(os.path.join(prefix, p))
528+
p = os.path.join(p, filename) if filename else p + os.sep
529+
op = os.path.join(root, filename) if filename else root
530+
return op, p
531+
532+
def apply(path):
533+
d = True
534+
for r in rules:
535+
op, regex = r
536+
neg = op is operator.not_
537+
m = regex.fullmatch(path)
538+
if neg and m:
539+
d = False
540+
elif m:
541+
d = True
542+
if d:
543+
return path
544+
545+
def emit_dir(dpath, opath):
546+
if apply(dpath):
547+
yield opath
548+
549+
def emit_file(fpath, opath):
550+
if apply(fpath):
551+
yield opath
552+
553+
if os.path.isfile(path):
554+
name = os.path.basename(path)
555+
if prefix:
556+
name = os.path.join(prefix, name)
557+
if apply(name):
558+
yield path
559+
else:
560+
for root, dirs, files in os.walk(path):
561+
o, d = norm_path(path, root)
562+
logger.info('od: %s %s', o, d)
563+
if root != path:
564+
yield from emit_dir(d, o)
565+
for name in files:
566+
o, f = norm_path(path, root, name)
567+
logger.info('of: %s %s', o, f)
568+
yield from emit_file(f, o)
569+
570+
489571
class BuildPlanManager:
490572
""""""
491573

@@ -514,15 +596,84 @@ def plan(self, source_path, query):
514596

515597
source_paths = []
516598
build_plan = []
599+
600+
step = lambda *x: build_plan.append(x)
601+
hash = source_paths.append
602+
603+
def pip_requirements_step(path, prefix=None, required=False):
604+
requirements = path
605+
if os.path.isdir(path):
606+
requirements = os.path.join(path, 'requirements.txt')
607+
if not os.path.isfile(requirements):
608+
if required:
609+
raise RuntimeError(
610+
'File not found: {}'.format(requirements))
611+
else:
612+
step('pip', runtime, requirements, prefix)
613+
hash(requirements)
614+
615+
def commands_step(path, commands):
616+
path = os.path.normpath(path)
617+
batch = []
618+
for c in commands:
619+
if isinstance(c, str):
620+
if c.startswith(':zip'):
621+
if batch:
622+
step('sh', path, '\n'.join(batch))
623+
batch.clear()
624+
c = shlex.split(c)
625+
if len(c) == 3:
626+
_, _path, prefix = c
627+
prefix = prefix.strip()
628+
_path = os.path.normpath(os.path.join(path, _path))
629+
step('zip', _path, prefix)
630+
elif len(c) == 1:
631+
prefix = None
632+
step('zip', path, prefix)
633+
else:
634+
raise ValueError(
635+
':zip command can have zero '
636+
'or 2 arguments: {}'.format(c))
637+
hash(path)
638+
else:
639+
batch.append(c)
640+
517641
for claim in claims:
518642
if isinstance(claim, str):
519-
# Validate the query.
520-
if not os.path.exists(claim):
643+
path = claim
644+
if not os.path.exists(path):
521645
abort('source_path must be set.')
522-
build_plan.append(('zip', claim))
523-
source_paths.append(claim)
646+
runtime = query.runtime
647+
if runtime.startswith('python'):
648+
pip_requirements_step(
649+
os.path.join(path, 'requirements.txt'))
650+
step('zip', path, None)
651+
hash(path)
652+
524653
elif isinstance(claim, dict):
525-
pass
654+
path = claim.get('path')
655+
patterns = claim.get('patterns')
656+
commands = claim.get('commands')
657+
if patterns:
658+
step('set:filter', patterns_list(patterns))
659+
if commands:
660+
commands_step(path, commands)
661+
else:
662+
prefix = claim.get('prefix_in_zip')
663+
pip_requirements = claim.get('pip_requirements')
664+
runtime = claim.get('runtime', query.runtime)
665+
666+
if pip_requirements and runtime.startswith('python'):
667+
if isinstance(pip_requirements, bool) and path:
668+
pip_requirements_step(path, prefix, required=True)
669+
else:
670+
pip_requirements_step(pip_requirements, prefix,
671+
required=True)
672+
if path:
673+
step('zip', path, prefix)
674+
hash(path)
675+
if patterns:
676+
step('clear:filter')
526677
else:
527678
raise ValueError(
528679
'Unsupported source_path item: {}'.format(claim))
@@ -531,23 +682,55 @@ def plan(self, source_path, query):
531682
return build_plan
532683

533684
def execute(self, build_plan, zip_stream, query):
534-
runtime = query.runtime
535-
536685
zs = zip_stream
686+
sh_work_dir = None
687+
pf = None
688+
537689
for action in build_plan:
538-
cmd, source_path = action
690+
cmd = action[0]
539691
if cmd == 'zip':
540-
if os.path.isdir(source_path):
541-
if runtime.startswith('python'):
542-
with install_pip_requirements(
543-
query, zs,
544-
os.path.join(source_path, 'requirements.txt')
545-
) as rd:
546-
rd and zs.write_dirs(
547-
rd, timestamp=0) # XXX: temp ts=0
548-
zs.write_dirs(source_path)
692+
source_path, prefix = action[1:]
693+
if sh_work_dir:
694+
if source_path != sh_work_dir:
695+
source_path = sh_work_dir
696+
if pf:
697+
for path in pf.filter(source_path, prefix):
698+
if os.path.isdir(source_path):
699+
arcname = os.path.relpath(path, source_path)
700+
else:
701+
arcname = os.path.basename(path)
702+
zs.write_file(path, prefix, arcname)
549703
else:
550-
zs.write_file(source_path)
704+
if os.path.isdir(source_path):
705+
zs.write_dirs(source_path, prefix=prefix)
706+
else:
707+
zs.write_file(source_path, prefix=prefix)
708+
elif cmd == 'pip':
709+
runtime, pip_requirements, prefix = action[1:]
710+
with install_pip_requirements(query, zs,
711+
pip_requirements) as rd:
712+
if rd:
713+
# XXX: timestamp=0 - what actually do with it?
714+
zs.write_dirs(rd, prefix=prefix, timestamp=0)
715+
elif cmd == 'sh':
716+
r, w = os.pipe()
717+
side_ch = os.fdopen(r)
718+
path, script = action[1:]
719+
script = "{}\npwd >&{}".format(script, w)
720+
721+
p = subprocess.Popen(script, shell=True, cwd=path,
722+
pass_fds=(w,))
723+
os.close(w)
724+
sh_work_dir = side_ch.read().strip()
725+
p.wait()
726+
logger.info('WD: %s', sh_work_dir)
727+
side_ch.close()
728+
elif cmd == 'set:filter':
729+
patterns = action[1]
730+
pf = ZipContentFilter(args=self._args)
731+
pf.compile(patterns)
732+
elif cmd == 'clear:filter':
733+
pf = None
551734

552735

553736
@contextmanager
@@ -570,15 +753,12 @@ def install_pip_requirements(query, zip_stream, requirements_file):
570753

571754
# Install dependencies into the temporary directory.
572755
with cd(temp_dir):
573-
if runtime.startswith('python3'):
574-
pip_command = ['pip3']
575-
else:
576-
pip_command = ['pip2']
577-
pip_command.extend([
756+
pip_command = [
757+
runtime, '-m', 'pip',
578758
'install', '--no-compile',
579759
'--prefix=', '--target=.',
580760
'--requirement={}'.format(requirements_filename),
581-
])
761+
]
582762
if docker:
583763
pip_cache_dir = docker.docker_pip_cache
584764
if pip_cache_dir:
@@ -714,6 +894,9 @@ def prepare_command(args):
714894
bpm = BuildPlanManager(logger=logger)
715895
build_plan = bpm.plan(source_path, query)
716896

897+
if logger.isEnabledFor(DEBUG2):
898+
logger.debug('BUILD_PLAN: %s', json.dumps(build_plan, indent=2))
899+
717900
# Expand a Terraform path.<cwd|root|module> references
718901
hash_extra_paths = [p.format(path=tf_paths) for p in hash_extra_paths]
719902

0 commit comments

Comments
 (0)