Support for empty_files and empty_dirs in pkg.

Backport recent changes to build_tar tool in bazelbuild/rules_docker: https://github.com/bazelbuild/rules_docker/pull/310
  - add --empty_file and --empty_dir flags to build_tar.py, allowing
    creation of empty files and dirs with specified mode.
  - expose those flags as empty_files and empty_dirs parameters to
    pkg_tar rule.

Closes #4837.

PiperOrigin-RevId: 190610460
diff --git a/tools/build_defs/pkg/BUILD b/tools/build_defs/pkg/BUILD
index 2f55d7f..cae1a4a 100644
--- a/tools/build_defs/pkg/BUILD
+++ b/tools/build_defs/pkg/BUILD
@@ -174,6 +174,26 @@
     },
 )
 
+pkg_tar(
+    name = "test-tar-empty_files",
+    build_tar = ":build_tar",
+    empty_files = [
+        "/a",
+        "/b",
+    ],
+    mode = "0o777",
+)
+
+pkg_tar(
+    name = "test-tar-empty_dirs",
+    build_tar = ":build_tar",
+    empty_dirs = [
+        "/tmp",
+        "/pmt",
+    ],
+    mode = "0o777",
+)
+
 pkg_deb(
     name = "test-deb",
     conffiles = [
@@ -205,6 +225,8 @@
         ":test-deb.deb",
         ":test-tar-.tar",
         ":test-tar-bz2.tar.bz2",
+        ":test-tar-empty_dirs.tar",
+        ":test-tar-empty_files.tar",
         ":test-tar-files_dict.tar",
         ":test-tar-gz.tar.gz",
         ":test-tar-inclusion-.tar",
diff --git a/tools/build_defs/pkg/build_tar.py b/tools/build_defs/pkg/build_tar.py
index a07fd43..e542804 100644
--- a/tools/build_defs/pkg/build_tar.py
+++ b/tools/build_defs/pkg/build_tar.py
@@ -30,6 +30,10 @@
 gflags.DEFINE_string(
     'mode', None, 'Force the mode on the added files (in octal).')
 
+gflags.DEFINE_multistring('empty_file', [], 'An empty file to add to the layer')
+
+gflags.DEFINE_multistring('empty_dir', [], 'An empty dir to add to the layer')
+
 gflags.DEFINE_multistring('tar', [], 'A tar file to add to the layer')
 
 gflags.DEFINE_multistring('deb', [], 'A debian package to add to the layer')
@@ -121,6 +125,56 @@
         uname=names[0],
         gname=names[1])
 
+  def add_empty_file(self,
+                     destfile,
+                     mode=None,
+                     ids=None,
+                     names=None,
+                     kind=tarfile.REGTYPE):
+    """Add a file to the tar file.
+
+    Args:
+       destfile: the name of the file in the layer
+       mode: force to set the specified mode, defaults to 644
+       ids: (uid, gid) for the file to set ownership
+       names: (username, groupname) for the file to set ownership.
+       kind: type of the file. tarfile.DIRTYPE for directory.
+
+    An empty file will be created as `destfile` in the layer.
+    """
+    dest = destfile.lstrip('/')  # Remove leading slashes
+    # If mode is unspecified, assume read only
+    if mode is None:
+      mode = 0o644
+    if ids is None:
+      ids = (0, 0)
+    if names is None:
+      names = ('', '')
+    dest = os.path.normpath(dest)
+    self.tarfile.add_file(
+        dest,
+        content='' if kind == tarfile.REGTYPE else None,
+        kind=kind,
+        mode=mode,
+        uid=ids[0],
+        gid=ids[1],
+        uname=names[0],
+        gname=names[1])
+
+  def add_empty_dir(self, destpath, mode=None, ids=None, names=None):
+    """Add a directory to the tar file.
+
+    Args:
+       destpath: the name of the directory in the layer
+       mode: force to set the specified mode, defaults to 644
+       ids: (uid, gid) for the file to set ownership
+       names: (username, groupname) for the file to set ownership.
+
+    An empty file will be created as `destfile` in the layer.
+    """
+    self.add_empty_file(
+        destpath, mode=mode, ids=ids, names=names, kind=tarfile.DIRTYPE)
+
   def add_tar(self, tar):
     """Merge a tar file into the destination tar file.
 
@@ -212,21 +266,23 @@
 
   # Add objects to the tar file
   with TarFile(FLAGS.output, FLAGS.directory, FLAGS.compression) as output:
+
+    def file_attributes(filename):
+      if filename[0] == '/':
+        filename = filename[1:]
+      return {
+          'mode': mode_map.get(filename, default_mode),
+          'ids': ids_map.get(filename, default_ids),
+          'names': names_map.get(filename, default_ownername),
+      }
+
     for f in FLAGS.file:
       (inf, tof) = f.split('=', 1)
-      mode = default_mode
-      ids = default_ids
-      names = default_ownername
-      map_filename = tof
-      if tof[0] == '/':
-        map_filename = tof[1:]
-      if map_filename in mode_map:
-        mode = mode_map[map_filename]
-      if map_filename in ids_map:
-        ids = ids_map[map_filename]
-      if map_filename in names_map:
-        names = names_map[map_filename]
-      output.add_file(inf, tof, mode, ids, names)
+      output.add_file(inf, tof, **file_attributes(tof))
+    for f in FLAGS.empty_file:
+      output.add_empty_file(f, **file_attributes(f))
+    for f in FLAGS.empty_dir:
+      output.add_empty_dir(f, **file_attributes(f))
     for tar in FLAGS.tar:
       output.add_tar(tar)
     for deb in FLAGS.deb:
diff --git a/tools/build_defs/pkg/build_test.sh b/tools/build_defs/pkg/build_test.sh
index 1322f50..99bae8e 100755
--- a/tools/build_defs/pkg/build_test.sh
+++ b/tools/build_defs/pkg/build_test.sh
@@ -22,9 +22,17 @@
 function get_tar_listing() {
   local input=$1
   local test_data="${TEST_DATA_DIR}/${input}"
+  # We strip unused prefixes rather than dropping "v" flag for tar, because we
+  # want to preserve symlink information.
   tar tvf "${test_data}" | sed -e 's/^.*:00 //'
 }
 
+function get_tar_verbose_listing() {
+  local input=$1
+  local test_data="${TEST_DATA_DIR}/${input}"
+  TZ="UTC" tar tvf "${test_data}"
+}
+
 function get_tar_owner() {
   local input=$1
   local file=$2
@@ -147,6 +155,14 @@
   check_eq "./
 ./not-etc/
 ./not-etc/mapped-filename.conf" "$(get_tar_listing test-tar-files_dict.tar)"
+  check_eq "drwxr-xr-x 0/0               0 1970-01-01 00:00 ./
+-rwxrwxrwx 0/0               0 1970-01-01 00:00 ./a
+-rwxrwxrwx 0/0               0 1970-01-01 00:00 ./b" \
+      "$(get_tar_verbose_listing test-tar-empty_files.tar)"
+  check_eq "drwxr-xr-x 0/0               0 1970-01-01 00:00 ./
+drwxrwxrwx 0/0               0 1970-01-01 00:00 ./tmp/
+drwxrwxrwx 0/0               0 1970-01-01 00:00 ./pmt/" \
+      "$(get_tar_verbose_listing test-tar-empty_dirs.tar)"
 }
 
 function test_deb() {
diff --git a/tools/build_defs/pkg/pkg.bzl b/tools/build_defs/pkg/pkg.bzl
index b12814d..24840ee 100644
--- a/tools/build_defs/pkg/pkg.bzl
+++ b/tools/build_defs/pkg/pkg.bzl
@@ -47,6 +47,10 @@
   if ctx.attr.ownernames:
     args += ["--owner_names=%s=%s" % (key, ctx.attr.ownernames[key])
              for key in ctx.attr.ownernames]
+  if ctx.attr.empty_files:
+    args += ["--empty_file=%s" % empty_file for empty_file in ctx.attr.empty_files]
+  if ctx.attr.empty_dirs:
+    args += ["--empty_dir=%s" % empty_dir for empty_dir in ctx.attr.empty_dirs]
   if ctx.attr.extension:
     dotPos = ctx.attr.extension.find('.')
     if dotPos > 0:
@@ -175,6 +179,8 @@
         "ownernames": attr.string_dict(),
         "extension": attr.string(default="tar"),
         "symlinks": attr.string_dict(),
+        "empty_files": attr.string_list(),
+        "empty_dirs": attr.string_list(),
         # Implicit dependencies.
         "build_tar": attr.label(
             default=Label("//tools/build_defs/pkg:build_tar"),