Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

build.py 7.8KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import io
  2. import os
  3. import re
  4. import tarfile
  5. import tempfile
  6. from .fnmatch import fnmatch
  7. from ..constants import IS_WINDOWS_PLATFORM
  8. _SEP = re.compile('/|\\\\') if IS_WINDOWS_PLATFORM else re.compile('/')
  9. def tar(path, exclude=None, dockerfile=None, fileobj=None, gzip=False):
  10. root = os.path.abspath(path)
  11. exclude = exclude or []
  12. dockerfile = dockerfile or (None, None)
  13. extra_files = []
  14. if dockerfile[1] is not None:
  15. dockerignore_contents = '\n'.join(
  16. (exclude or ['.dockerignore']) + [dockerfile[0]]
  17. )
  18. extra_files = [
  19. ('.dockerignore', dockerignore_contents),
  20. dockerfile,
  21. ]
  22. return create_archive(
  23. files=sorted(exclude_paths(root, exclude, dockerfile=dockerfile[0])),
  24. root=root, fileobj=fileobj, gzip=gzip, extra_files=extra_files
  25. )
  26. def exclude_paths(root, patterns, dockerfile=None):
  27. """
  28. Given a root directory path and a list of .dockerignore patterns, return
  29. an iterator of all paths (both regular files and directories) in the root
  30. directory that do *not* match any of the patterns.
  31. All paths returned are relative to the root.
  32. """
  33. if dockerfile is None:
  34. dockerfile = 'Dockerfile'
  35. patterns.append('!' + dockerfile)
  36. pm = PatternMatcher(patterns)
  37. return set(pm.walk(root))
  38. def build_file_list(root):
  39. files = []
  40. for dirname, dirnames, fnames in os.walk(root):
  41. for filename in fnames + dirnames:
  42. longpath = os.path.join(dirname, filename)
  43. files.append(
  44. longpath.replace(root, '', 1).lstrip('/')
  45. )
  46. return files
  47. def create_archive(root, files=None, fileobj=None, gzip=False,
  48. extra_files=None):
  49. extra_files = extra_files or []
  50. if not fileobj:
  51. fileobj = tempfile.NamedTemporaryFile()
  52. t = tarfile.open(mode='w:gz' if gzip else 'w', fileobj=fileobj)
  53. if files is None:
  54. files = build_file_list(root)
  55. extra_names = {e[0] for e in extra_files}
  56. for path in files:
  57. if path in extra_names:
  58. # Extra files override context files with the same name
  59. continue
  60. full_path = os.path.join(root, path)
  61. i = t.gettarinfo(full_path, arcname=path)
  62. if i is None:
  63. # This happens when we encounter a socket file. We can safely
  64. # ignore it and proceed.
  65. continue
  66. # Workaround https://bugs.python.org/issue32713
  67. if i.mtime < 0 or i.mtime > 8**11 - 1:
  68. i.mtime = int(i.mtime)
  69. if IS_WINDOWS_PLATFORM:
  70. # Windows doesn't keep track of the execute bit, so we make files
  71. # and directories executable by default.
  72. i.mode = i.mode & 0o755 | 0o111
  73. if i.isfile():
  74. try:
  75. with open(full_path, 'rb') as f:
  76. t.addfile(i, f)
  77. except OSError:
  78. raise OSError(
  79. f'Can not read file in context: {full_path}'
  80. )
  81. else:
  82. # Directories, FIFOs, symlinks... don't need to be read.
  83. t.addfile(i, None)
  84. for name, contents in extra_files:
  85. info = tarfile.TarInfo(name)
  86. contents_encoded = contents.encode('utf-8')
  87. info.size = len(contents_encoded)
  88. t.addfile(info, io.BytesIO(contents_encoded))
  89. t.close()
  90. fileobj.seek(0)
  91. return fileobj
  92. def mkbuildcontext(dockerfile):
  93. f = tempfile.NamedTemporaryFile()
  94. t = tarfile.open(mode='w', fileobj=f)
  95. if isinstance(dockerfile, io.StringIO):
  96. dfinfo = tarfile.TarInfo('Dockerfile')
  97. raise TypeError('Please use io.BytesIO to create in-memory '
  98. 'Dockerfiles with Python 3')
  99. elif isinstance(dockerfile, io.BytesIO):
  100. dfinfo = tarfile.TarInfo('Dockerfile')
  101. dfinfo.size = len(dockerfile.getvalue())
  102. dockerfile.seek(0)
  103. else:
  104. dfinfo = t.gettarinfo(fileobj=dockerfile, arcname='Dockerfile')
  105. t.addfile(dfinfo, dockerfile)
  106. t.close()
  107. f.seek(0)
  108. return f
  109. def split_path(p):
  110. return [pt for pt in re.split(_SEP, p) if pt and pt != '.']
  111. def normalize_slashes(p):
  112. if IS_WINDOWS_PLATFORM:
  113. return '/'.join(split_path(p))
  114. return p
  115. def walk(root, patterns, default=True):
  116. pm = PatternMatcher(patterns)
  117. return pm.walk(root)
  118. # Heavily based on
  119. # https://github.com/moby/moby/blob/master/pkg/fileutils/fileutils.go
  120. class PatternMatcher:
  121. def __init__(self, patterns):
  122. self.patterns = list(filter(
  123. lambda p: p.dirs, [Pattern(p) for p in patterns]
  124. ))
  125. self.patterns.append(Pattern('!.dockerignore'))
  126. def matches(self, filepath):
  127. matched = False
  128. parent_path = os.path.dirname(filepath)
  129. parent_path_dirs = split_path(parent_path)
  130. for pattern in self.patterns:
  131. negative = pattern.exclusion
  132. match = pattern.match(filepath)
  133. if not match and parent_path != '':
  134. if len(pattern.dirs) <= len(parent_path_dirs):
  135. match = pattern.match(
  136. os.path.sep.join(parent_path_dirs[:len(pattern.dirs)])
  137. )
  138. if match:
  139. matched = not negative
  140. return matched
  141. def walk(self, root):
  142. def rec_walk(current_dir):
  143. for f in os.listdir(current_dir):
  144. fpath = os.path.join(
  145. os.path.relpath(current_dir, root), f
  146. )
  147. if fpath.startswith('.' + os.path.sep):
  148. fpath = fpath[2:]
  149. match = self.matches(fpath)
  150. if not match:
  151. yield fpath
  152. cur = os.path.join(root, fpath)
  153. if not os.path.isdir(cur) or os.path.islink(cur):
  154. continue
  155. if match:
  156. # If we want to skip this file and it's a directory
  157. # then we should first check to see if there's an
  158. # excludes pattern (e.g. !dir/file) that starts with this
  159. # dir. If so then we can't skip this dir.
  160. skip = True
  161. for pat in self.patterns:
  162. if not pat.exclusion:
  163. continue
  164. if pat.cleaned_pattern.startswith(
  165. normalize_slashes(fpath)):
  166. skip = False
  167. break
  168. if skip:
  169. continue
  170. yield from rec_walk(cur)
  171. return rec_walk(root)
  172. class Pattern:
  173. def __init__(self, pattern_str):
  174. self.exclusion = False
  175. if pattern_str.startswith('!'):
  176. self.exclusion = True
  177. pattern_str = pattern_str[1:]
  178. self.dirs = self.normalize(pattern_str)
  179. self.cleaned_pattern = '/'.join(self.dirs)
  180. @classmethod
  181. def normalize(cls, p):
  182. # Remove trailing spaces
  183. p = p.strip()
  184. # Leading and trailing slashes are not relevant. Yes,
  185. # "foo.py/" must exclude the "foo.py" regular file. "."
  186. # components are not relevant either, even if the whole
  187. # pattern is only ".", as the Docker reference states: "For
  188. # historical reasons, the pattern . is ignored."
  189. # ".." component must be cleared with the potential previous
  190. # component, regardless of whether it exists: "A preprocessing
  191. # step [...] eliminates . and .. elements using Go's
  192. # filepath.".
  193. i = 0
  194. split = split_path(p)
  195. while i < len(split):
  196. if split[i] == '..':
  197. del split[i]
  198. if i > 0:
  199. del split[i - 1]
  200. i -= 1
  201. else:
  202. i += 1
  203. return split
  204. def match(self, filepath):
  205. return fnmatch(normalize_slashes(filepath), self.cleaned_pattern)