|
| 1 | +# Copyright 2015 The Bazel Authors. All rights reserved. |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | +"""This tool build tar files from a list of inputs.""" |
| 15 | + |
| 16 | +import os |
| 17 | +import os.path |
| 18 | +import sys |
| 19 | +import tarfile |
| 20 | +import tempfile |
| 21 | + |
| 22 | +from tools.build_defs.pkg import archive |
| 23 | +from third_party.py import gflags |
| 24 | + |
| 25 | +gflags.DEFINE_string('output', None, 'The output file, mandatory') |
| 26 | +gflags.MarkFlagAsRequired('output') |
| 27 | + |
| 28 | +gflags.DEFINE_multistring('file', [], 'A file to add to the layer') |
| 29 | + |
| 30 | +gflags.DEFINE_string( |
| 31 | + 'mode', None, 'Force the mode on the added files (in octal).') |
| 32 | + |
| 33 | +gflags.DEFINE_multistring('tar', [], 'A tar file to add to the layer') |
| 34 | + |
| 35 | +gflags.DEFINE_multistring('deb', [], 'A debian package to add to the layer') |
| 36 | + |
| 37 | +gflags.DEFINE_multistring( |
| 38 | + 'link', [], |
| 39 | + 'Add a symlink a inside the layer ponting to b if a:b is specified') |
| 40 | +gflags.RegisterValidator( |
| 41 | + 'link', |
| 42 | + lambda l: all(value.find(':') > 0 for value in l), |
| 43 | + message='--link value should contains a : separator') |
| 44 | + |
| 45 | +gflags.DEFINE_string( |
| 46 | + 'directory', None, 'Directory in which to store the file inside the layer') |
| 47 | + |
| 48 | +gflags.DEFINE_string( |
| 49 | + 'compression', None, 'Compression (`gz` or `bz2`), default is none.') |
| 50 | + |
| 51 | +gflags.DEFINE_multistring( |
| 52 | + 'modes', None, |
| 53 | + 'Specific mode to apply to specific file (from the file argument),' |
| 54 | + ' e.g., path/to/file=0455.') |
| 55 | + |
| 56 | +gflags.DEFINE_multistring('owners', None, |
| 57 | + 'Specify the numeric owners of individual files, ' |
| 58 | + 'e.g. path/to/file=0.0.') |
| 59 | + |
| 60 | +gflags.DEFINE_string('owner', '0.0', |
| 61 | + 'Specify the numeric default owner of all files,' |
| 62 | + ' e.g., 0.0') |
| 63 | + |
| 64 | +gflags.DEFINE_string('owner_name', None, |
| 65 | + 'Specify the owner name of all files, e.g. root.root.') |
| 66 | + |
| 67 | +gflags.DEFINE_multistring('owner_names', None, |
| 68 | + 'Specify the owner names of individual files, e.g. ' |
| 69 | + 'path/to/file=root.root.') |
| 70 | + |
| 71 | +FLAGS = gflags.FLAGS |
| 72 | + |
| 73 | + |
| 74 | +class TarFile(object): |
| 75 | + """A class to generates a Docker layer.""" |
| 76 | + |
| 77 | + class DebError(Exception): |
| 78 | + pass |
| 79 | + |
| 80 | + def __init__(self, output, directory, compression): |
| 81 | + self.directory = directory |
| 82 | + self.output = output |
| 83 | + self.compression = compression |
| 84 | + |
| 85 | + def __enter__(self): |
| 86 | + self.tarfile = archive.TarFileWriter(self.output, self.compression) |
| 87 | + return self |
| 88 | + |
| 89 | + def __exit__(self, t, v, traceback): |
| 90 | + self.tarfile.close() |
| 91 | + |
| 92 | + def add_file(self, f, destfile, mode=None, ids=None, names=None): |
| 93 | + """Add a file to the tar file. |
| 94 | +
|
| 95 | + Args: |
| 96 | + f: the file to add to the layer |
| 97 | + destfile: the name of the file in the layer |
| 98 | + mode: force to set the specified mode, by |
| 99 | + default the value from the source is taken. |
| 100 | + ids: (uid, gid) for the file to set ownership |
| 101 | + names: (username, groupname) for the file to set ownership. |
| 102 | + `f` will be copied to `self.directory/destfile` in the layer. |
| 103 | + """ |
| 104 | + dest = destfile.lstrip('/') # Remove leading slashes |
| 105 | + if self.directory and self.directory != '/': |
| 106 | + dest = self.directory.lstrip('/') + '/' + dest |
| 107 | + # If mode is unspecified, derive the mode from the file's mode. |
| 108 | + if mode is None: |
| 109 | + mode = 0o755 if os.access(f, os.X_OK) else 0o644 |
| 110 | + if ids is None: |
| 111 | + ids = (0, 0) |
| 112 | + if names is None: |
| 113 | + names = ('', '') |
| 114 | + dest = os.path.normpath(dest) |
| 115 | + self.tarfile.add_file( |
| 116 | + dest, |
| 117 | + file_content=f, |
| 118 | + mode=mode, |
| 119 | + uid=ids[0], |
| 120 | + gid=ids[1], |
| 121 | + uname=names[0], |
| 122 | + gname=names[1]) |
| 123 | + |
| 124 | + def add_tar(self, tar): |
| 125 | + """Merge a tar file into the destination tar file. |
| 126 | +
|
| 127 | + All files presents in that tar will be added to the output file |
| 128 | + under self.directory/path. No user name nor group name will be |
| 129 | + added to the output. |
| 130 | +
|
| 131 | + Args: |
| 132 | + tar: the tar file to add |
| 133 | + """ |
| 134 | + root = None |
| 135 | + if self.directory and self.directory != '/': |
| 136 | + root = self.directory |
| 137 | + self.tarfile.add_tar(tar, numeric=True, root=root) |
| 138 | + |
| 139 | + def add_link(self, symlink, destination): |
| 140 | + """Add a symbolic link pointing to `destination`. |
| 141 | +
|
| 142 | + Args: |
| 143 | + symlink: the name of the symbolic link to add. |
| 144 | + destination: where the symbolic link point to. |
| 145 | + """ |
| 146 | + symlink = os.path.normpath(symlink) |
| 147 | + self.tarfile.add_file(symlink, tarfile.SYMTYPE, link=destination) |
| 148 | + |
| 149 | + def add_deb(self, deb): |
| 150 | + """Extract a debian package in the output tar. |
| 151 | +
|
| 152 | + All files presents in that debian package will be added to the |
| 153 | + output tar under the same paths. No user name nor group names will |
| 154 | + be added to the output. |
| 155 | +
|
| 156 | + Args: |
| 157 | + deb: the tar file to add |
| 158 | +
|
| 159 | + Raises: |
| 160 | + DebError: if the format of the deb archive is incorrect. |
| 161 | + """ |
| 162 | + with archive.SimpleArFile(deb) as arfile: |
| 163 | + current = arfile.next() |
| 164 | + while current and not current.filename.startswith('data.'): |
| 165 | + current = arfile.next() |
| 166 | + if not current: |
| 167 | + raise self.DebError(deb + ' does not contains a data file!') |
| 168 | + tmpfile = tempfile.mkstemp(suffix=os.path.splitext(current.filename)[-1]) |
| 169 | + with open(tmpfile[1], 'wb') as f: |
| 170 | + f.write(current.data) |
| 171 | + self.add_tar(tmpfile[1]) |
| 172 | + os.remove(tmpfile[1]) |
| 173 | + |
| 174 | + |
| 175 | +def main(unused_argv): |
| 176 | + # Parse modes arguments |
| 177 | + default_mode = None |
| 178 | + if FLAGS.mode: |
| 179 | + # Convert from octal |
| 180 | + default_mode = int(FLAGS.mode, 8) |
| 181 | + |
| 182 | + mode_map = {} |
| 183 | + if FLAGS.modes: |
| 184 | + for filemode in FLAGS.modes: |
| 185 | + (f, mode) = filemode.split('=', 1) |
| 186 | + if f[0] == '/': |
| 187 | + f = f[1:] |
| 188 | + mode_map[f] = int(mode, 8) |
| 189 | + |
| 190 | + default_ownername = ('', '') |
| 191 | + if FLAGS.owner_name: |
| 192 | + default_ownername = FLAGS.owner_name.split('.', 1) |
| 193 | + names_map = {} |
| 194 | + if FLAGS.owner_names: |
| 195 | + for file_owner in FLAGS.owner_names: |
| 196 | + (f, owner) = file_owner.split('=', 1) |
| 197 | + (user, group) = owner.split('.', 1) |
| 198 | + if f[0] == '/': |
| 199 | + f = f[1:] |
| 200 | + names_map[f] = (user, group) |
| 201 | + |
| 202 | + default_ids = FLAGS.owner.split('.', 1) |
| 203 | + default_ids = (int(default_ids[0]), int(default_ids[1])) |
| 204 | + ids_map = {} |
| 205 | + if FLAGS.owners: |
| 206 | + for file_owner in FLAGS.owners: |
| 207 | + (f, owner) = file_owner.split('=', 1) |
| 208 | + (user, group) = owner.split('.', 1) |
| 209 | + if f[0] == '/': |
| 210 | + f = f[1:] |
| 211 | + ids_map[f] = (int(user), int(group)) |
| 212 | + |
| 213 | + # Add objects to the tar file |
| 214 | + with TarFile(FLAGS.output, FLAGS.directory, FLAGS.compression) as output: |
| 215 | + for f in FLAGS.file: |
| 216 | + (inf, tof) = f.split('=', 1) |
| 217 | + mode = default_mode |
| 218 | + ids = default_ids |
| 219 | + names = default_ownername |
| 220 | + map_filename = tof |
| 221 | + if tof[0] == '/': |
| 222 | + map_filename = tof[1:] |
| 223 | + if map_filename in mode_map: |
| 224 | + mode = mode_map[map_filename] |
| 225 | + if map_filename in ids_map: |
| 226 | + ids = ids_map[map_filename] |
| 227 | + if map_filename in names_map: |
| 228 | + names = names_map[map_filename] |
| 229 | + output.add_file(inf, tof, mode, ids, names) |
| 230 | + for tar in FLAGS.tar: |
| 231 | + output.add_tar(tar) |
| 232 | + for deb in FLAGS.deb: |
| 233 | + output.add_deb(deb) |
| 234 | + for link in FLAGS.link: |
| 235 | + l = link.split(':', 1) |
| 236 | + output.add_link(l[0], l[1]) |
| 237 | + |
| 238 | + |
| 239 | +if __name__ == '__main__': |
| 240 | + main(FLAGS(sys.argv)) |
0 commit comments