Skip to content

Commit d021b99

Browse files
authored
Merge pull request #606 from rhubert/urlscm-bob-download
Place url-scm download files in .bob-download
2 parents 2d829b4 + c619d27 commit d021b99

File tree

20 files changed

+367
-109
lines changed

20 files changed

+367
-109
lines changed

doc/manual/policies.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,22 @@ New behavior
344344

345345
Unmanaged layers are expected in the same directory.
346346

347+
urlScmSeparateDownload
348+
~~~~~~~~~~~~~~~~~~~~~~
349+
350+
Introduced in: 1.0
351+
352+
This policy controls where bob places downloaded files of UrlScms if extraction is
353+
used.
354+
355+
Old behavior
356+
The downloaded file could be found in the workspace next to the extracted files.
357+
358+
New behavior
359+
The downloaded file is stored next to the workspace in a separate download folder.
360+
Only the extracted content is in the workspace.
361+
362+
347363
.. _policies-obsolete:
348364

349365
Obsolete policies

pym/bob/builder.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,6 +1209,8 @@ async def _cookCheckoutStep(self, checkoutStep, depth):
12091209
os.makedirs(atticPath)
12101210
atticPath = os.path.join(atticPath, atticName)
12111211
os.rename(scmPath, atticPath)
1212+
if scmDir in scmMap:
1213+
scmMap[scmDir].postAttic(prettySrcPath)
12121214
BobState().setAtticDirectoryState(atticPath, scmSpec)
12131215
atticPaths.add(scmPath, atticPath)
12141216
del oldCheckoutState[scmDir]

pym/bob/input.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3029,6 +3029,7 @@ class RecipeSet:
30293029
schema.Optional('defaultFileMode') : bool,
30303030
schema.Optional('substituteMetaEnv') : bool,
30313031
schema.Optional('managedLayers') : bool,
3032+
schema.Optional('urlScmSeparateDownload') : bool,
30323033
},
30333034
error="Invalid policy specified! Are you using an appropriate version of Bob?"
30343035
),
@@ -3083,6 +3084,11 @@ class RecipeSet:
30833084
InfoOnce("managedLayers policy is not set. Only unmanaged layers are supported.",
30843085
help="See http://bob-build-tool.readthedocs.io/en/latest/manual/policies.html#managedlayers for more information.")
30853086
),
3087+
"urlScmSeparateDownload": (
3088+
"0.25.1.dev27",
3089+
InfoOnce("urlScmSeparateDownload policy is not set. Extracted archives of the 'url' SCM are retained in the workspace.",
3090+
help="See http://bob-build-tool.readthedocs.io/en/latest/manual/policies.html#urlscmseparatedownload for more information.")
3091+
)
30863092
}
30873093

30883094
_ignoreCmdConfig = False

pym/bob/intermediate.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,7 @@ def fromRecipeSet(cls, recipeSet):
553553
'gitCommitOnBranch' : recipeSet.getPolicy('gitCommitOnBranch'),
554554
'fixImportScmVariant' : recipeSet.getPolicy('fixImportScmVariant'),
555555
'defaultFileMode' : recipeSet.getPolicy('defaultFileMode'),
556+
'urlScmSeparateDownload' : recipeSet.getPolicy('urlScmSeparateDownload'),
556557
}
557558
self.__data['archiveSpec'] = recipeSet.archiveSpec()
558559
self.__data['envWhiteList'] = sorted(recipeSet.envWhiteList())

pym/bob/scm/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def getScm(spec, overrides=[], recipeSet=None):
7777
recipeSet and recipeSet.getPolicy('scmIgnoreUser'),
7878
recipeSet.getPreMirrors() if recipeSet else [],
7979
recipeSet.getFallbackMirrors() if recipeSet else [],
80-
recipeSet and recipeSet.getPolicy('defaultFileMode'))
80+
recipeSet and recipeSet.getPolicy('defaultFileMode'),
81+
recipeSet and recipeSet.getPolicy('urlScmSeparateDownload'))
8182
else:
8283
raise ParseError("Unknown SCM '{}'".format(scm))

pym/bob/scm/scm.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,9 @@ def calcLiveBuildId(self, workspacePath):
361361
"""Calculate live build-id from workspace."""
362362
return None
363363

364+
def postAttic(self, workspace):
365+
pass
366+
364367
class ScmAudit(metaclass=ABCMeta):
365368
@classmethod
366369
async def fromDir(cls, workspace, dir, extra):

pym/bob/scm/url.py

Lines changed: 161 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
replacePath
1111
from .scm import Scm, ScmAudit
1212
from http.client import HTTPException
13+
from abc import abstractmethod
1314
import asyncio
1415
import concurrent.futures.process
1516
import contextlib
@@ -155,6 +156,123 @@ def dumpMode(mode):
155156

156157
isWin32 = 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+
158276
class 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

696796
class UrlAudit(ScmAudit):
697797

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
policies:
2+
urlScmSeparateDownload: True
127 Bytes
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1

0 commit comments

Comments
 (0)