6
6
raise RuntimeError ("A python version 3.7 or newer is required" )
7
7
8
8
import os
9
+ import re
9
10
import time
10
11
import stat
11
12
import json
16
17
import argparse
17
18
import datetime
18
19
import tempfile
20
+ import operator
19
21
import platform
20
22
import subprocess
21
23
from subprocess import check_call
@@ -321,7 +323,8 @@ def write_files(self, files_stream, prefix=None, timestamp=None):
321
323
Expects just files stream, directories will be created automatically
322
324
"""
323
325
self ._ensure_open ()
324
- raise NotImplementedError
326
+ for file_path , arcname in files_stream :
327
+ self ._write_file (file_path , prefix , arcname , timestamp )
325
328
326
329
def write_file (self , file_path , prefix = None , name = None , timestamp = None ):
327
330
"""
@@ -346,7 +349,7 @@ def _write_file(self, file_path, prefix=None, name=None, timestamp=None):
346
349
347
350
def write_file_obj (self , file_path , data , prefix = None , timestamp = None ):
348
351
"""
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
350
353
"""
351
354
self ._ensure_open ()
352
355
raise NotImplementedError
@@ -486,6 +489,85 @@ def str_int_to_timestamp(s):
486
489
################################################################################
487
490
# Building
488
491
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
+
489
571
class BuildPlanManager :
490
572
""""""
491
573
@@ -514,15 +596,84 @@ def plan(self, source_path, query):
514
596
515
597
source_paths = []
516
598
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
+
517
641
for claim in claims :
518
642
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 ):
521
645
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
+
524
653
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' )
526
677
else :
527
678
raise ValueError (
528
679
'Unsupported source_path item: {}' .format (claim ))
@@ -531,23 +682,55 @@ def plan(self, source_path, query):
531
682
return build_plan
532
683
533
684
def execute (self , build_plan , zip_stream , query ):
534
- runtime = query .runtime
535
-
536
685
zs = zip_stream
686
+ sh_work_dir = None
687
+ pf = None
688
+
537
689
for action in build_plan :
538
- cmd , source_path = action
690
+ cmd = action [ 0 ]
539
691
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 )
549
703
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
551
734
552
735
553
736
@contextmanager
@@ -570,15 +753,12 @@ def install_pip_requirements(query, zip_stream, requirements_file):
570
753
571
754
# Install dependencies into the temporary directory.
572
755
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' ,
578
758
'install' , '--no-compile' ,
579
759
'--prefix=' , '--target=.' ,
580
760
'--requirement={}' .format (requirements_filename ),
581
- ])
761
+ ]
582
762
if docker :
583
763
pip_cache_dir = docker .docker_pip_cache
584
764
if pip_cache_dir :
@@ -714,6 +894,9 @@ def prepare_command(args):
714
894
bpm = BuildPlanManager (logger = logger )
715
895
build_plan = bpm .plan (source_path , query )
716
896
897
+ if logger .isEnabledFor (DEBUG2 ):
898
+ logger .debug ('BUILD_PLAN: %s' , json .dumps (build_plan , indent = 2 ))
899
+
717
900
# Expand a Terraform path.<cwd|root|module> references
718
901
hash_extra_paths = [p .format (path = tf_paths ) for p in hash_extra_paths ]
719
902
0 commit comments