66 raise RuntimeError ("A python version 3.7 or newer is required" )
77
88import os
9+ import re
910import time
1011import stat
1112import json
1617import argparse
1718import datetime
1819import tempfile
20+ import operator
1921import platform
2022import subprocess
2123from 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+
489571class 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 = "{}\n pwd >&{}" .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