|  | # Lint as: python3 | 
|  | # Copyright 2022 The Bazel Authors. All rights reserved. | 
|  | # | 
|  | # Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | # you may not use this file except in compliance with the License. | 
|  | # You may obtain a copy of the License at | 
|  | # | 
|  | #    http://www.apache.org/licenses/LICENSE-2.0 | 
|  | # | 
|  | # Unless required by applicable law or agreed to in writing, software | 
|  | # distributed under the License is distributed on an "AS IS" BASIS, | 
|  | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | # See the License for the specific language governing permissions and | 
|  | # limitations under the License. | 
|  | """This tool build tar files from a list of inputs.""" | 
|  |  | 
|  | import argparse | 
|  | import os | 
|  | import tarfile | 
|  |  | 
|  | # Use a deterministic mtime that doesn't confuse other programs. | 
|  | # See: https://github.com/bazelbuild/bazel/issues/1299 | 
|  | PORTABLE_MTIME = 946684800  # 2000-01-01 00:00:00.000 UTC | 
|  |  | 
|  |  | 
|  | class TarFileWriter(object): | 
|  | """A wrapper to write tar files.""" | 
|  |  | 
|  | class Error(Exception): | 
|  | pass | 
|  |  | 
|  | def __init__(self, | 
|  | name, | 
|  | root_directory='', | 
|  | default_uid=0, | 
|  | default_gid=0, | 
|  | default_mtime=None): | 
|  | """TarFileWriter wraps tarfile.open(). | 
|  |  | 
|  | Args: | 
|  | name: the tar file name. | 
|  | root_directory: virtual root to prepend to elements in the archive. | 
|  | default_uid: uid to assign to files in the archive. | 
|  | default_gid: gid to assign to files in the archive. | 
|  | default_mtime: default mtime to use for elements in the archive. May be an | 
|  | integer or the value 'portable' to use the date 2000-01-01, which is | 
|  | compatible with non *nix OSes'. | 
|  | """ | 
|  | mode = 'w:' | 
|  | self.name = name | 
|  | self.root_directory = root_directory.strip('/') | 
|  | self.default_gid = default_gid | 
|  | self.default_uid = default_uid | 
|  | if default_mtime is None: | 
|  | self.default_mtime = 0 | 
|  | elif default_mtime == 'portable': | 
|  | self.default_mtime = PORTABLE_MTIME | 
|  | else: | 
|  | self.default_mtime = int(default_mtime) | 
|  | self.tar = tarfile.open(name=name, mode=mode) | 
|  | self.members = set() | 
|  | self.directories = set(['.']) | 
|  |  | 
|  | def __enter__(self): | 
|  | return self | 
|  |  | 
|  | def __exit__(self, t, v, traceback): | 
|  | self.close() | 
|  |  | 
|  | def close(self): | 
|  | """Close the output tar file.""" | 
|  | self.tar.close() | 
|  |  | 
|  | def _addfile(self, info, fileobj=None): | 
|  | """Add a file in the tar file if there is no conflict.""" | 
|  | if info.type == tarfile.DIRTYPE: | 
|  | # Enforce the ending / for directories so we correctly deduplicate. | 
|  | if not info.name.endswith('/'): | 
|  | info.name += '/' | 
|  | if info.name not in self.members: | 
|  | self.tar.addfile(info, fileobj) | 
|  | self.members.add(info.name) | 
|  | elif info.type != tarfile.DIRTYPE: | 
|  | print(('Duplicate file in archive: %s, ' | 
|  | 'picking first occurrence' % info.name)) | 
|  |  | 
|  | def add_parents(self, path, mode=0o755): | 
|  | """Add the parents of this path to the archive. | 
|  |  | 
|  | Args: | 
|  | path: destination path in archive. | 
|  | mode: unix permission mode of the dir, default 0o755. | 
|  | """ | 
|  |  | 
|  | def add_dirs(path): | 
|  | """Helper to add dirs.""" | 
|  | path = path.strip('/') | 
|  | if not path: | 
|  | return | 
|  | if path in self.directories: | 
|  | return | 
|  | components = path.rsplit('/', 1) | 
|  | if len(components) > 1: | 
|  | add_dirs(components[0]) | 
|  | self.directories.add(path) | 
|  | tarinfo = tarfile.TarInfo(path + '/') | 
|  | tarinfo.mtime = self.default_mtime | 
|  | tarinfo.uid = self.default_uid | 
|  | tarinfo.gid = self.default_gid | 
|  | tarinfo.type = tarfile.DIRTYPE | 
|  | tarinfo.mode = mode or 0o755 | 
|  | self.tar.addfile(tarinfo, fileobj=None) | 
|  |  | 
|  | components = path.rsplit('/', 1) | 
|  | if len(components) > 1: | 
|  | add_dirs(components[0]) | 
|  |  | 
|  | def add_tree(self, input_path, dest_path, mode=None): | 
|  | """Recursively add a tree of files. | 
|  |  | 
|  | Args: | 
|  | input_path: the path of the directory to add. | 
|  | dest_path: the destination path of the directory to add. | 
|  | mode: unix permission mode of the file, default 0644 (0755). | 
|  | """ | 
|  | # Add the x bit to directories to prevent non-traversable directories. | 
|  | # The x bit is set only to if the read bit is set. | 
|  | dirmode = (mode | ((0o444 & mode) >> 2)) if mode else mode | 
|  | self.add_parents(dest_path, mode=dirmode) | 
|  |  | 
|  | if os.path.isdir(input_path): | 
|  | dest_path = dest_path.rstrip('/') + '/' | 
|  | # Iterate over the sorted list of file so we get a deterministic result. | 
|  | filelist = os.listdir(input_path) | 
|  | filelist.sort() | 
|  | for f in filelist: | 
|  | self.add_tree( | 
|  | input_path=input_path + '/' + f, dest_path=dest_path + f, mode=mode) | 
|  | else: | 
|  | self.add_file_and_parents( | 
|  | dest_path, tarfile.REGTYPE, file_content=input_path, mode=mode) | 
|  |  | 
|  | def add_file_and_parents(self, | 
|  | name, | 
|  | kind=tarfile.REGTYPE, | 
|  | link=None, | 
|  | file_content=None, | 
|  | mode=None): | 
|  | """Add a file to the current tar. | 
|  |  | 
|  | Creates parent directories if needed. | 
|  |  | 
|  | Args: | 
|  | name: the name of the file to add. | 
|  | kind: the type of the file to add, see tarfile.*TYPE. | 
|  | link: if the file is a link, the destination of the link. | 
|  | file_content: file to read the content from. Provide either this one or | 
|  | `content` to specifies a content for the file. | 
|  | mode: unix permission mode of the file, default 0644 (0755). | 
|  | """ | 
|  | if self.root_directory and ( | 
|  | not (name == self.root_directory or name.startswith('/') or | 
|  | name.startswith(self.root_directory + '/'))): | 
|  | name = self.root_directory + '/' + name | 
|  | self.add_parents(name, mode=0o755) | 
|  |  | 
|  | if kind == tarfile.DIRTYPE: | 
|  | name = name.rstrip('/') | 
|  | if name in self.directories: | 
|  | return | 
|  |  | 
|  | if file_content and os.path.isdir(file_content): | 
|  | self.add_tree(input_path=file_content, dest_path=name, mode=mode) | 
|  | return | 
|  |  | 
|  | tarinfo = tarfile.TarInfo(name) | 
|  | tarinfo.mtime = self.default_mtime | 
|  | tarinfo.uid = self.default_uid | 
|  | tarinfo.gid = self.default_gid | 
|  | tarinfo.type = kind | 
|  | if mode is None: | 
|  | tarinfo.mode = 0o644 if kind == tarfile.REGTYPE else 0o755 | 
|  | else: | 
|  | tarinfo.mode = mode | 
|  | if link: | 
|  | tarinfo.linkname = link | 
|  | if file_content: | 
|  | with open(file_content, 'rb') as f: | 
|  | tarinfo.size = os.fstat(f.fileno()).st_size | 
|  | self._addfile(tarinfo, fileobj=f) | 
|  | else: | 
|  | self._addfile(tarinfo, fileobj=None) | 
|  |  | 
|  | def add_file_at_dest(self, in_path, dest_path, mode=None): | 
|  | """Add a file to the tar file. | 
|  |  | 
|  | Args: | 
|  | in_path: the path of the file to add to the artifact | 
|  | dest_path: the name of the file in the artifact | 
|  | mode: force to the specified mode. Default is mode from the file. | 
|  | """ | 
|  | # Make a clean, '/' deliminted destination path | 
|  | dest = os.path.normpath(dest_path.strip('/')).replace(os.path.sep, '/') | 
|  | # If mode is unspecified, derive the mode from the file's mode. | 
|  | if mode is None: | 
|  | mode = 0o755 if os.access(dest, os.X_OK) else 0o644 | 
|  | self.add_file_and_parents(dest, file_content=in_path, mode=mode) | 
|  |  | 
|  |  | 
|  | def unquote_and_split(arg, c): | 
|  | """Split a string at the first unquoted occurrence of a character. | 
|  |  | 
|  | Split the string arg at the first unquoted occurrence of the character c. | 
|  | Here, in the first part of arg, the backslash is considered the | 
|  | quoting character indicating that the next character is to be | 
|  | added literally to the first part, even if it is the split character. | 
|  |  | 
|  | Args: | 
|  | arg: the string to be split | 
|  | c: the character at which to split | 
|  |  | 
|  | Returns: | 
|  | The unquoted string before the separator and the string after the | 
|  | separator. | 
|  | """ | 
|  | head = '' | 
|  | i = 0 | 
|  | while i < len(arg): | 
|  | if arg[i] == c: | 
|  | return (head, arg[i + 1:]) | 
|  | elif arg[i] == '\\': | 
|  | i += 1 | 
|  | if i == len(arg): | 
|  | # dangling quotation symbol | 
|  | return (head, '') | 
|  | else: | 
|  | head += arg[i] | 
|  | else: | 
|  | head += arg[i] | 
|  | i += 1 | 
|  | # if we leave the loop, the character c was not found unquoted | 
|  | return (head, '') | 
|  |  | 
|  |  | 
|  | def main(): | 
|  | parser = argparse.ArgumentParser( | 
|  | description='Helper for building tar packages', fromfile_prefix_chars='@') | 
|  | parser.add_argument( | 
|  | '--output', required=True, help='The output file, mandatory.') | 
|  | parser.add_argument( | 
|  | '--mode', help='Force the mode on the added files (in octal).') | 
|  | parser.add_argument( | 
|  | '--directory', | 
|  | help='Directory in which to store the file inside the layer') | 
|  | parser.add_argument('--file', action='append', help='input_paty=dest_path') | 
|  | parser.add_argument( | 
|  | '--owner', | 
|  | default='0.0', | 
|  | help='Specify the numeric default owner of all files. E.g. 0.0') | 
|  | options = parser.parse_args() | 
|  |  | 
|  | # Parse modes arguments | 
|  | default_mode = None | 
|  | if options.mode: | 
|  | # Convert from octal | 
|  | default_mode = int(options.mode, 8) | 
|  |  | 
|  | uid = gid = 0 | 
|  | if options.owner: | 
|  | ids = options.owner.split('.', 1) | 
|  | uid = int(ids[0]) | 
|  | gid = int(ids[1]) | 
|  |  | 
|  | # Add objects to the tar file | 
|  | with TarFileWriter( | 
|  | name=options.output, | 
|  | root_directory=options.directory or '', | 
|  | default_uid=uid, | 
|  | default_gid=gid, | 
|  | default_mtime=PORTABLE_MTIME) as output: | 
|  | for f in options.file: | 
|  | (input_path, dest) = unquote_and_split(f, '=') | 
|  | output.add_file_at_dest(input_path, dest, mode=default_mode) | 
|  |  | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | main() |