1010 replacePath
1111from .scm import Scm , ScmAudit
1212from http .client import HTTPException
13+ from abc import abstractmethod
1314import asyncio
1415import concurrent .futures .process
1516import contextlib
@@ -155,6 +156,123 @@ def dumpMode(mode):
155156
156157isWin32 = sys .platform == "win32"
157158
159+
160+ class Extractor ():
161+ def __init__ (self , dir , file , strip , separateDownload ):
162+ self .dir = dir
163+ self .file = file
164+ self .strip = strip
165+ self .separateDownload = separateDownload
166+
167+ async def _extract (self , cmds , invoker ):
168+ destination = self .getCompressedFilePath (invoker )
169+ canary = destination + ".extracted"
170+ if isYounger (destination , canary ):
171+ for cmd in cmds :
172+ if shutil .which (cmd [0 ]) is None : continue
173+ await invoker .checkCommand (cmd , cwd = self .dir )
174+ invoker .trace ("<touch>" , canary )
175+ with open (canary , "wb" ) as f :
176+ pass
177+ os .utime (canary )
178+ break
179+ else :
180+ invoker .fail ("No suitable extractor found!" )
181+
182+ def getCompressedFilePath (self , invoker ):
183+ downloadFolder = os .path .join (os .pardir , "download" ) if self .separateDownload else ""
184+ return os .path .abspath (invoker .joinPath (downloadFolder , self .dir , self .file )) \
185+
186+ @abstractmethod
187+ async def extract (self , invoker , destination , cwd ):
188+ return False
189+
190+ # Use the Python tar/zip extraction only on Windows. They are slower and in
191+ # case of tarfile broken in certain ways (e.g. tarfile will result in
192+ # different file modes!). But it shouldn't make a difference on Windows.
193+ class TarExtractor (Extractor ):
194+ def __init__ (self , dir , file , strip , separateDownload ):
195+ super ().__init__ (dir , file , strip , separateDownload )
196+
197+ async def extract (self , invoker ):
198+ cmds = []
199+ compressedFilePath = self .getCompressedFilePath (invoker )
200+ if isWin32 and self .strip == 0 :
201+ cmds .append (["python" , "-m" , "tarfile" , "-e" , compressedFilePath ])
202+
203+ cmd = ["tar" , "-x" , "--no-same-owner" , "--no-same-permissions" ,
204+ "-f" , compressedFilePath ]
205+ if self .strip > 0 :
206+ cmd .append ("--strip-components={}" .format (self .strip ))
207+ cmds .append (cmd )
208+
209+ await self ._extract (cmds , invoker )
210+
211+
212+ class ZipExtractor (Extractor ):
213+ def __init__ (self , dir , file , strip , separateDownload ):
214+ super ().__init__ (dir , file , strip , separateDownload )
215+ if strip != 0 :
216+ raise BuildError ("Extractor does not support 'stripComponents'!" )
217+
218+ async def extract (self , invoker ):
219+ cmds = []
220+ compressedFilePath = self .getCompressedFilePath (invoker )
221+ if isWin32 :
222+ cmds .append (["python" , "-m" , "zipfile" ,
223+ "-e" , compressedFilePath , "." ])
224+
225+ cmds .append (["unzip" , "-o" , compressedFilePath ])
226+ await self ._extract (cmds , invoker )
227+
228+
229+ class GZipExtractor (Extractor ):
230+ def __init__ (self , dir , file , strip , separateDownload ):
231+ super ().__init__ (dir , file , strip , separateDownload )
232+ if strip != 0 :
233+ raise BuildError ("Extractor does not support 'stripComponents'!" )
234+
235+ async def extract (self , invoker ):
236+ # gunzip extracts the file at the location of the input file. Copy the
237+ # downloaded file to the workspace directory prio to uncompressing it
238+ cmd = ["gunzip" ]
239+ if self .separateDownload :
240+ shutil .copyfile (self .getCompressedFilePath (invoker ),
241+ invoker .joinPath (self .dir , self .file ))
242+ else :
243+ cmd .append ("-k" )
244+ cmd .extend (["-f" , self .file ])
245+ await self ._extract ([cmd ], invoker )
246+
247+
248+ class XZExtractor (Extractor ):
249+ def __init__ (self , dir , file , strip , separateDownload ):
250+ super ().__init__ (dir , file , strip , separateDownload )
251+ if strip != 0 :
252+ raise BuildError ("Extractor does not support 'stripComponents'!" )
253+
254+ async def extract (self , invoker ):
255+ cmd = ["unxz" ]
256+ if self .separateDownload :
257+ shutil .copyfile (self .getCompressedFilePath (invoker ),
258+ invoker .joinPath (self .dir , self .file ))
259+ else :
260+ cmd .append ("-k" )
261+ cmd .extend (["-f" , self .file ])
262+ await self ._extract ([cmd ], invoker )
263+
264+
265+ class SevenZipExtractor (Extractor ):
266+ def __init__ (self , dir , file , strip , separateDownload ):
267+ super ().__init__ (dir , file , strip , separateDownload )
268+ if strip != 0 :
269+ raise BuildError ("Extractor does not support 'stripComponents'!" )
270+
271+ async def extract (self , invoker ):
272+ cmds = [["7z" , "x" , "-y" , self .getCompressedFilePath (invoker )]]
273+ await self ._extract (cmds , invoker )
274+
275+
158276class UrlScm (Scm ):
159277
160278 __DEFAULTS = {
@@ -212,31 +330,17 @@ class UrlScm(Scm):
212330 (".zip" , "zip" ),
213331 ]
214332
215- # Use the Python tar/zip extraction only on Windows. They are slower and in
216- # case of tarfile broken in certain ways (e.g. tarfile will result in
217- # different file modes!). But it shouldn't make a difference on Windows.
218333 EXTRACTORS = {
219- "tar" : [
220- (isWin32 , "python" , ["-m" , "tarfile" , "-e" , "{}" ], None ),
221- (True , "tar" , ["-x" , "--no-same-owner" , "--no-same-permissions" , "-f" , "{}" ], "--strip-components={}" ),
222- ],
223- "gzip" : [
224- (True , "gunzip" , ["-kf" , "{}" ], None ),
225- ],
226- "xz" : [
227- (True , "unxz" , ["-kf" , "{}" ], None ),
228- ],
229- "7z" : [
230- (True , "7z" , ["x" , "-y" , "{}" ], None ),
231- ],
232- "zip" : [
233- (isWin32 , "python" , ["-m" , "zipfile" , "-e" , "{}" , "." ], None ),
234- (True , "unzip" , ["-o" , "{}" ], None ),
235- ],
334+ "tar" : TarExtractor ,
335+ "gzip" : GZipExtractor ,
336+ "xz" : XZExtractor ,
337+ "7z" : SevenZipExtractor ,
338+ "zip" : ZipExtractor ,
236339 }
237340
238341 def __init__ (self , spec , overrides = [], stripUser = None ,
239- preMirrors = [], fallbackMirrors = [], defaultFileMode = None ):
342+ preMirrors = [], fallbackMirrors = [], defaultFileMode = None ,
343+ separateDownload = False ):
240344 super ().__init__ (spec , overrides )
241345 self .__url = spec ["url" ]
242346 self .__digestSha1 = spec .get ("digestSHA1" )
@@ -275,6 +379,7 @@ def __init__(self, spec, overrides=[], stripUser=None,
275379 self .__fallbackMirrorsUrls = spec .get ("fallbackMirrors" )
276380 self .__fallbackMirrorsUpload = spec .get ("__fallbackMirrorsUpload" )
277381 self .__fileMode = spec .get ("fileMode" , 0o600 if defaultFileMode else None )
382+ self .__separateDownload = spec .get ("__separateDownload" , separateDownload )
278383
279384 def getProperties (self , isJenkins , pretty = False ):
280385 ret = super ().getProperties (isJenkins )
@@ -295,6 +400,7 @@ def getProperties(self, isJenkins, pretty=False):
295400 'fallbackMirrors' : self .__getFallbackMirrorsUrls (),
296401 '__fallbackMirrorsUpload' : self .__getFallbackMirrorsUpload (),
297402 'fileMode' : dumpMode (self .__fileMode ) if pretty else self .__fileMode ,
403+ '__separateDownload' : self .__separateDownload ,
298404 })
299405 return ret
300406
@@ -517,6 +623,9 @@ async def _put(self, invoker, workspaceFile, source, url):
517623 invoker .fail ("Upload not supported for URL scheme: " + url .scheme )
518624
519625 def canSwitch (self , oldScm ):
626+ if self .__separateDownload != oldScm .__separateDownload :
627+ return False
628+
520629 diff = self ._diffSpec (oldScm )
521630 if "scm" in diff :
522631 return False
@@ -551,7 +660,16 @@ async def switch(self, invoker, oldScm):
551660 async def invoke (self , invoker ):
552661 os .makedirs (invoker .joinPath (self .__dir ), exist_ok = True )
553662 workspaceFile = os .path .join (self .__dir , self .__fn )
663+ extractor = self .__getExtractor ()
664+
554665 destination = invoker .joinPath (self .__dir , self .__fn )
666+ if extractor is not None and self .__separateDownload :
667+ downloadDestination = invoker .joinPath (os .pardir , "download" , self .__dir )
668+ # os.makedirs doc:
669+ # Note: makedirs() will become confused if the path elements to create include pardir (eg. “..” on UNIX systems).
670+ # -> use normpath to collaps up-level reference
671+ os .makedirs (os .path .normpath (downloadDestination ), exist_ok = True )
672+ destination = invoker .joinPath (os .pardir , "download" , self .__dir , self .__fn )
555673
556674 # Download only if necessary
557675 if not self .isDeterministic () or not os .path .isfile (destination ):
@@ -600,26 +718,17 @@ async def invoke(self, invoker):
600718 await self ._put (invoker , workspaceFile , destination , url )
601719
602720 # Run optional extractors
603- extractors = self .__getExtractors ()
604- canary = invoker .joinPath (self .__dir , "." + self .__fn + ".extracted" )
605- if extractors and isYounger (destination , canary ):
606- for cmd in extractors :
607- if shutil .which (cmd [0 ]) is None : continue
608- await invoker .checkCommand (cmd , cwd = self .__dir )
609- invoker .trace ("<touch>" , canary )
610- with open (canary , "wb" ) as f :
611- pass
612- os .utime (canary )
613- break
614- else :
615- invoker .fail ("No suitable extractor found!" )
721+ if extractor is not None :
722+ await extractor .extract (invoker )
616723
617724 def asDigestScript (self ):
618725 """Return forward compatible stable string describing this url.
619726
620727 The format is "digest dir extract" if a SHA checksum was specified.
621728 Otherwise it is "url dir extract". A "s#" is appended if leading paths
622- are stripped where # is the number of stripped elements.
729+ are stripped where # is the number of stripped elements. Also appended
730+ is "m<fileMode>" if fileMode is set.
731+ "sep" is appendend if the archive is not stored in the workspace.
623732 """
624733 if self .__stripUser :
625734 filt = removeUserFromUrl
@@ -629,7 +738,8 @@ def asDigestScript(self):
629738 self .__digestSha1 or filt (self .__url )
630739 ) + " " + posixpath .join (self .__dir , self .__fn ) + " " + str (self .__extract ) + \
631740 ( " s{}" .format (self .__strip ) if self .__strip > 0 else "" ) + \
632- ( " m{}" .format (self .__fileMode ) if self .__fileMode is not None else "" )
741+ ( " m{}" .format (self .__fileMode ) if self .__fileMode is not None else "" ) + \
742+ ( " sep" if self .__separateDownload else "" )
633743
634744 def getDirectory (self ):
635745 return self .__dir
@@ -659,39 +769,29 @@ def calcLiveBuildId(self, workspacePath):
659769 else :
660770 return None
661771
662- def __getExtractors (self ):
663- extractors = None
772+ def __getExtractor (self ):
773+ extractor = None
664774 if self .__extract in ["yes" , "auto" , True ]:
665775 for (ext , tool ) in UrlScm .EXTENSIONS :
666776 if self .__fn .endswith (ext ):
667- extractors = UrlScm .EXTRACTORS [tool ]
777+ extractor = UrlScm .EXTRACTORS [tool ](self .__dir , self .__fn ,
778+ self .__strip , self .__separateDownload )
668779 break
669- if not extractors and self .__extract != "auto" :
780+ if extractor is None and self .__extract != "auto" :
670781 raise ParseError ("Don't know how to extract '" + self .__fn + "' automatically." )
671782 elif self .__extract in UrlScm .EXTRACTORS :
672- extractors = UrlScm .EXTRACTORS [self .__extract ]
783+ extractor = UrlScm .EXTRACTORS [self .__extract ](self .__dir , self .__fn ,
784+ self .__strip , self .__separateDownload )
673785 elif self .__extract not in ["no" , False ]:
674786 raise ParseError ("Invalid extract mode: " + self .__extract )
675-
676- if extractors is None :
677- return []
678-
679- ret = []
680- for extractor in extractors :
681- if not extractor [0 ]: continue
682- if self .__strip > 0 :
683- if extractor [3 ] is None :
684- continue
685- strip = [extractor [3 ].format (self .__strip )]
686- else :
687- strip = []
688- ret .append ([extractor [1 ]] + [a .format (self .__fn ) for a in extractor [2 ]] + strip )
689-
690- if not ret :
691- raise BuildError ("Extractor does not support 'stripComponents'!" )
692-
693- return ret
694-
787+ return extractor
788+
789+ def postAttic (self , workspace ):
790+ if self .__separateDownload :
791+ # os.path.exists returns False if os.pardir is in the path -> normalize it
792+ downloadDestination = os .path .normpath (os .path .join (workspace , os .pardir , "download" , self .__dir ))
793+ if os .path .exists (downloadDestination ):
794+ shutil .rmtree (downloadDestination )
695795
696796class UrlAudit (ScmAudit ):
697797
0 commit comments