Skip to content

Commit 027f82d

Browse files
committed
Add tar xattr support
1 parent ec4ad13 commit 027f82d

File tree

3 files changed

+88
-15
lines changed

3 files changed

+88
-15
lines changed

pkg/private/tar/build_tar.py

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"""This tool build tar files from a list of inputs."""
1515

1616
import argparse
17+
import base64
1718
import os
1819
import tarfile
1920
import tempfile
@@ -36,27 +37,47 @@ def normpath(path):
3637
return os.path.normpath(path).replace(os.path.sep, '/')
3738

3839

40+
def parse_xattr(xattr_list):
41+
xattrs = {}
42+
if xattr_list:
43+
for item in xattr_list:
44+
idx = item.index("=")
45+
if idx < 0:
46+
raise ValueError("Unexpected xattr item format {} (want key=value)".format(item))
47+
key = item[:idx]
48+
raw = item[idx+1:]
49+
if raw.startswith("0x"):
50+
xattrs[key] = bytes.fromhex(raw[2:]).decode('latin-1')
51+
elif raw.startswith('"') and raw.endswith('"') and len(raw) >= 2:
52+
xattrs[key] = raw[1:-1]
53+
else:
54+
xattrs[key] = base64.b64decode(raw).decode('latin-1')
55+
return xattrs
56+
57+
3958
class TarFile(object):
4059
"""A class to generates a TAR file."""
4160

4261
class DebError(Exception):
4362
pass
4463

45-
def __init__(self, output, directory, compression, compressor, default_mtime):
64+
def __init__(self, output, directory, compression, compressor, default_mtime, use_xattr):
4665
# Directory prefix on all output paths
4766
d = directory.strip('/')
4867
self.directory = (d + '/') if d else None
4968
self.output = output
5069
self.compression = compression
5170
self.compressor = compressor
5271
self.default_mtime = default_mtime
72+
self.use_xattr = use_xattr
5373

5474
def __enter__(self):
5575
self.tarfile = tar_writer.TarFileWriter(
5676
self.output,
5777
self.compression,
5878
self.compressor,
59-
default_mtime=self.default_mtime)
79+
default_mtime=self.default_mtime,
80+
use_xattr=self.use_xattr)
6081
return self
6182

6283
def __exit__(self, t, v, traceback):
@@ -74,7 +95,7 @@ def normalize_path(self, path: str) -> str:
7495
dest = self.directory + dest
7596
return dest
7697

77-
def add_file(self, f, destfile, mode=None, ids=None, names=None):
98+
def add_file(self, f, destfile, mode=None, ids=None, names=None, xattr=None):
7899
"""Add a file to the tar file.
79100
80101
Args:
@@ -85,6 +106,7 @@ def add_file(self, f, destfile, mode=None, ids=None, names=None):
85106
ids: (uid, gid) for the file to set ownership
86107
names: (username, groupname) for the file to set ownership. `f` will be
87108
copied to `self.directory/destfile` in the layer.
109+
xattr: (strings) xattr list in getfattr-like output style.
88110
"""
89111
dest = self.normalize_path(destfile)
90112
# If mode is unspecified, derive the mode from the file's mode.
@@ -101,14 +123,16 @@ def add_file(self, f, destfile, mode=None, ids=None, names=None):
101123
uid=ids[0],
102124
gid=ids[1],
103125
uname=names[0],
104-
gname=names[1])
126+
gname=names[1],
127+
xattr=xattr)
105128

106129
def add_empty_file(self,
107130
destfile,
108131
mode=None,
109132
ids=None,
110133
names=None,
111-
kind=tarfile.REGTYPE):
134+
kind=tarfile.REGTYPE,
135+
xattr=None):
112136
"""Add a file to the tar file.
113137
114138
Args:
@@ -118,6 +142,7 @@ def add_empty_file(self,
118142
names: (username, groupname) for the file to set ownership.
119143
kind: type of the file. tarfile.DIRTYPE for directory. An empty file
120144
will be created as `destfile` in the layer.
145+
xattr: (strings) xattr list in getfattr-like output style.
121146
"""
122147
dest = destfile.lstrip('/') # Remove leading slashes
123148
# If mode is unspecified, assume read only
@@ -136,9 +161,10 @@ def add_empty_file(self,
136161
uid=ids[0],
137162
gid=ids[1],
138163
uname=names[0],
139-
gname=names[1])
164+
gname=names[1],
165+
xattr=xattr)
140166

141-
def add_empty_dir(self, destpath, mode=None, ids=None, names=None):
167+
def add_empty_dir(self, destpath, mode=None, ids=None, names=None, xattr=None):
142168
"""Add a directory to the tar file.
143169
144170
Args:
@@ -147,9 +173,10 @@ def add_empty_dir(self, destpath, mode=None, ids=None, names=None):
147173
ids: (uid, gid) for the file to set ownership
148174
names: (username, groupname) for the file to set ownership. An empty
149175
file will be created as `destfile` in the layer.
176+
xattr: (strings) xattr list in getfattr-like output style.
150177
"""
151178
self.add_empty_file(
152-
destpath, mode=mode, ids=ids, names=names, kind=tarfile.DIRTYPE)
179+
destpath, mode=mode, ids=ids, names=names, kind=tarfile.DIRTYPE, xattr=xattr)
153180

154181
def add_tar(self, tar):
155182
"""Merge a tar file into the destination tar file.
@@ -163,7 +190,7 @@ def add_tar(self, tar):
163190
"""
164191
self.tarfile.add_tar(tar, numeric=True, prefix=self.directory)
165192

166-
def add_link(self, symlink, destination, mode=None, ids=None, names=None):
193+
def add_link(self, symlink, destination, mode=None, ids=None, names=None, xattr=None):
167194
"""Add a symbolic link pointing to `destination`.
168195
169196
Args:
@@ -174,6 +201,7 @@ def add_link(self, symlink, destination, mode=None, ids=None, names=None):
174201
ids: (uid, gid) for the file to set ownership
175202
names: (username, groupname) for the file to set ownership. An empty
176203
file will be created as `destfile` in the layer.
204+
xattr: (strings) xattr list in getfattr-like output style.
177205
"""
178206
dest = self.normalize_path(symlink)
179207
self.tarfile.add_file(
@@ -184,7 +212,8 @@ def add_link(self, symlink, destination, mode=None, ids=None, names=None):
184212
uid=ids[0],
185213
gid=ids[1],
186214
uname=names[0],
187-
gname=names[1])
215+
gname=names[1],
216+
xattr=xattr)
188217

189218
def add_deb(self, deb):
190219
"""Extract a debian package in the output tar.
@@ -211,7 +240,7 @@ def add_deb(self, deb):
211240
self.add_tar(tmpfile[1])
212241
os.remove(tmpfile[1])
213242

214-
def add_tree(self, tree_top, destpath, mode=None, ids=None, names=None):
243+
def add_tree(self, tree_top, destpath, mode=None, ids=None, names=None, xattr=None):
215244
"""Add a tree artifact to the tar file.
216245
217246
Args:
@@ -222,6 +251,7 @@ def add_tree(self, tree_top, destpath, mode=None, ids=None, names=None):
222251
ids: (uid, gid) for the file to set ownership
223252
names: (username, groupname) for the file to set ownership. `f` will be
224253
copied to `self.directory/destfile` in the layer.
254+
xattr: (strings) xattr list in getfattr-like output style.
225255
"""
226256
# We expect /-style paths.
227257
tree_top = normpath(tree_top)
@@ -374,6 +404,14 @@ def main():
374404
'--owner_names', action='append',
375405
help='Specify the owner names of individual files, e.g. '
376406
'path/to/file=root.root.')
407+
parser.add_argument(
408+
'--xattr', action='append',
409+
help='Specify the xattr of all files, e.g. '
410+
'security.capability=0x0100000200000001000000000000000000000000')
411+
parser.add_argument(
412+
'--file_xattr', action='append',
413+
help='Specify the xattr of individual files, e.g. '
414+
'path/to/file=security.capability=0x0100000200000001000000000000000000000000')
377415
parser.add_argument('--stamp_from', default='',
378416
help='File to find BUILD_STAMP in')
379417
options = parser.parse_args()
@@ -404,6 +442,18 @@ def main():
404442
f = f[1:]
405443
names_map[f] = (user, group)
406444

445+
default_xattr = parse_xattr(options.xattr)
446+
xattr_map = {}
447+
if options.file_xattr:
448+
xattr_by_file = {}
449+
for file_xattr in options.file_xattr:
450+
(f, xattr) = helpers.SplitNameValuePairAtSeparator(file_xattr, '=')
451+
xattrs = xattr_by_file.get(f, [])
452+
xattrs.append(xattr)
453+
xattr_by_file[f] = xattrs
454+
for f in xattr_by_file:
455+
xattr_map[f] = parse_xattr(xattr_by_file[f])
456+
407457
default_ids = options.owner.split('.', 1)
408458
default_ids = (int(default_ids[0]), int(default_ids[1]))
409459
ids_map = {}
@@ -425,7 +475,8 @@ def main():
425475
directory = helpers.GetFlagValue(options.directory),
426476
compression = options.compression,
427477
compressor = options.compressor,
428-
default_mtime=default_mtime) as output:
478+
default_mtime=default_mtime,
479+
use_xattr=bool(xattr_map or default_xattr)) as output:
429480

430481
def file_attributes(filename):
431482
if filename.startswith('/'):
@@ -434,6 +485,7 @@ def file_attributes(filename):
434485
'mode': mode_map.get(filename, default_mode),
435486
'ids': ids_map.get(filename, default_ids),
436487
'names': names_map.get(filename, default_ownername),
488+
'xattr': {**xattr_map.get(filename, {}), **default_xattr}
437489
}
438490

439491
if options.manifest:

pkg/private/tar/tar.bzl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,14 @@ def _pkg_tar_impl(ctx):
181181
"--owner_names",
182182
"%s=%s" % (_quote(key), ctx.attr.ownernames[key]),
183183
)
184+
if ctx.attr.xattr:
185+
for item in ctx.attr.xattr:
186+
args.add("--xattr", item)
187+
if ctx.attr.xattrs:
188+
for file in ctx.attr.xattrs:
189+
xattr = ctx.attr.xattrs[file]
190+
for item in xattr:
191+
args.add("--file_xattr", "%s=%s" % (_quote(file), item))
184192
for empty_file in ctx.attr.empty_files:
185193
add_empty_file(content_map, empty_file, ctx.label)
186194
for empty_dir in ctx.attr.empty_dirs or []:
@@ -264,6 +272,8 @@ pkg_tar_impl = rule(
264272
"ownername": attr.string(default = "."),
265273
"owners": attr.string_dict(),
266274
"ownernames": attr.string_dict(),
275+
"xattr": attr.string_list(),
276+
"xattrs": attr.string_list_dict(),
267277
"extension": attr.string(default = "tar"),
268278
"symlinks": attr.string_dict(),
269279
"empty_files": attr.string_list(),

pkg/private/tar/tar_writer.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ def __init__(self,
4747
compression='',
4848
compressor='',
4949
default_mtime=None,
50-
preserve_tar_mtimes=True):
50+
preserve_tar_mtimes=True,
51+
use_xattr=False):
5152
"""TarFileWriter wraps tarfile.open().
5253
5354
Args:
@@ -60,6 +61,7 @@ def __init__(self,
6061
preserve_tar_mtimes: if true, keep file mtimes from input tar file.
6162
"""
6263
self.preserve_mtime = preserve_tar_mtimes
64+
self.use_xattr = use_xattr
6365
if default_mtime is None:
6466
self.default_mtime = 0
6567
elif default_mtime == 'portable':
@@ -98,7 +100,7 @@ def __init__(self,
98100
self.name = name
99101

100102
self.tar = tarfile.open(name=name, mode=mode, fileobj=self.fileobj,
101-
format=tarfile.GNU_FORMAT)
103+
format=tarfile.PAX_FORMAT if use_xattr else tarfile.GNU_FORMAT)
102104
self.members = set()
103105
self.directories = set()
104106
# Preseed the added directory list with things we should not add. If we
@@ -191,7 +193,8 @@ def add_file(self,
191193
uname='',
192194
gname='',
193195
mtime=None,
194-
mode=None):
196+
mode=None,
197+
xattr=None):
195198
"""Add a file to the current tar.
196199
197200
Args:
@@ -234,6 +237,14 @@ def add_file(self,
234237
tarinfo.mode = mode
235238
if link:
236239
tarinfo.linkname = link
240+
if xattr:
241+
if not self.use_xattr:
242+
raise self.Error('This tar file was created without `use_xattr` flag but try to create file with xattr: {}, {}'.
243+
format(name, xattr))
244+
pax_headers = {}
245+
for key in xattr:
246+
pax_headers["SCHILY.xattr." + key] = xattr[key]
247+
tarinfo.pax_headers = pax_headers
237248
if content:
238249
content_bytes = content.encode('utf-8')
239250
tarinfo.size = len(content_bytes)

0 commit comments

Comments
 (0)