Make compression_level configurable for .tar.gz (#888)

diff --git a/pkg/private/tar/build_tar.py b/pkg/private/tar/build_tar.py
index ce80a9f..adaa781 100644
--- a/pkg/private/tar/build_tar.py
+++ b/pkg/private/tar/build_tar.py
@@ -42,7 +42,8 @@
   class DebError(Exception):
     pass
 
-  def __init__(self, output, directory, compression, compressor, create_parents, allow_dups_from_deps, default_mtime):
+  def __init__(self, output, directory, compression, compressor, create_parents,
+               allow_dups_from_deps, default_mtime, compression_level):
     # Directory prefix on all output paths
     d = directory.strip('/')
     self.directory = (d + '/') if d else None
@@ -52,6 +53,7 @@
     self.default_mtime = default_mtime
     self.create_parents = create_parents
     self.allow_dups_from_deps = allow_dups_from_deps
+    self.compression_level = compression_level
 
   def __enter__(self):
     self.tarfile = tar_writer.TarFileWriter(
@@ -60,7 +62,8 @@
         self.compressor,
         self.create_parents,
         self.allow_dups_from_deps,
-        default_mtime=self.default_mtime)
+        default_mtime=self.default_mtime,
+        compression_level=self.compression_level)
     return self
 
   def __exit__(self, t, v, traceback):
@@ -397,6 +400,9 @@
   parser.add_argument('--allow_dups_from_deps',
                       action='store_true',
                       help='')
+  parser.add_argument(
+      '--compression_level', default=-1,
+      help='Specify the numeric compress level in gzip mode; may be 0-9 or -1 (default to 6).')
   options = parser.parse_args()
 
   # Parse modes arguments
@@ -440,6 +446,10 @@
   if options.stamp_from:
     default_mtime = build_info.get_timestamp(options.stamp_from)
 
+  compression_level = -1
+  if options.compression_level:
+    compression_level = int(options.compression_level)
+
   # Add objects to the tar file
   with TarFile(
       options.output,
@@ -448,7 +458,8 @@
       compressor = options.compressor,
       default_mtime=default_mtime,
       create_parents=options.create_parents,
-      allow_dups_from_deps=options.allow_dups_from_deps) as output:
+      allow_dups_from_deps=options.allow_dups_from_deps,
+      compression_level = compression_level) as output:
 
     def file_attributes(filename):
       if filename.startswith('/'):
diff --git a/pkg/private/tar/tar.bzl b/pkg/private/tar/tar.bzl
index 120820a..a6d6844 100644
--- a/pkg/private/tar/tar.bzl
+++ b/pkg/private/tar/tar.bzl
@@ -114,6 +114,8 @@
                 "--owner_names",
                 "%s=%s" % (_quote(key), ctx.attr.ownernames[key]),
             )
+    if ctx.attr.compression_level:
+        args.add("--compression_level", ctx.attr.compression_level)
 
     # Now we begin processing the files.
     path_mapper = None
@@ -272,6 +274,10 @@
         ),
         "create_parents": attr.bool(default = True),
         "allow_duplicates_from_deps": attr.bool(default = False),
+        "compression_level": attr.int(
+            doc = """Specify the numeric compression level in gzip mode; may be 0-9 or -1 (default to 6).""",
+            default = -1,
+        ),
 
         # Common attributes
         "out": attr.output(mandatory = True),
diff --git a/pkg/private/tar/tar_writer.py b/pkg/private/tar/tar_writer.py
index cee232d..06d0cad 100644
--- a/pkg/private/tar/tar_writer.py
+++ b/pkg/private/tar/tar_writer.py
@@ -49,7 +49,8 @@
                create_parents=False,
                allow_dups_from_deps=True,
                default_mtime=None,
-               preserve_tar_mtimes=True):
+               preserve_tar_mtimes=True,
+               compression_level=-1):
     """TarFileWriter wraps tarfile.open().
 
     Args:
@@ -86,10 +87,11 @@
     else:
       mode = 'w:'
       if compression in ['tgz', 'gz']:
+        compression_level = min(compression_level, 9) if compression_level >= 0 else 6
         # The Tarfile class doesn't allow us to specify gzip's mtime attribute.
         # Instead, we manually reimplement gzopen from tarfile.py and set mtime.
         self.fileobj = gzip.GzipFile(
-            filename=name, mode='w', compresslevel=6, mtime=self.default_mtime)
+            filename=name, mode='w', compresslevel=compression_level, mtime=self.default_mtime)
     self.compressor_proc = None
     if self.compressor_cmd:
       mode = 'w|'
diff --git a/tests/tar/BUILD b/tests/tar/BUILD
index 9480a46..cc5d72d 100644
--- a/tests/tar/BUILD
+++ b/tests/tar/BUILD
@@ -477,6 +477,10 @@
         ":test-tar-strip_prefix-substring.tar",
         ":test-tar-tree-artifact",
         ":test-tar-tree-artifact-noroot",
+        ":test-tar-compression_level--1",
+        ":test-tar-compression_level-3",
+        ":test-tar-compression_level-6",
+        ":test-tar-compression_level-9",
         ":test-tree-input-with-strip-prefix",
         ":test_tar_leading_dotslash",
         ":test_tar_package_dir_substitution.tar",
@@ -756,3 +760,12 @@
     ],
     target = ":program_with_dir_runfiles_tar",
 )
+
+[pkg_tar(
+    name = "test-tar-compression_level-%s" % compression_level,
+    compression_level = compression_level,
+    deps = [
+        "//tests:testdata/tar_test.tar",
+    ],
+    extension = "tgz",
+) for compression_level in [-1, 3, 6, 9]]
diff --git a/tests/tar/pkg_tar_test.py b/tests/tar/pkg_tar_test.py
index 012c7de..835b9e1 100644
--- a/tests/tar/pkg_tar_test.py
+++ b/tests/tar/pkg_tar_test.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 """Testing for pkg_tar."""
 
+import os
 import tarfile
 import unittest
 
@@ -293,6 +294,17 @@
     ]
     self.assertTarFileContent('test-respect-externally-defined-duplicates.tar', content)
 
+  def test_compression_level(self):
+    sizes = [
+      ('test-tar-compression_level--1.tgz', 179),
+      ('test-tar-compression_level-3.tgz', 230),
+      ('test-tar-compression_level-6.tgz', 178),
+      ('test-tar-compression_level-9.tgz', 167),
+    ]
+    for file_name, expected_size in sizes:
+      file_path = runfiles.Create().Rlocation('rules_pkg/tests/tar/' + file_name)
+      file_size = os.stat(file_path).st_size
+      self.assertEqual(file_size, expected_size, 'size error for ' + file_name)
 
 if __name__ == '__main__':
   unittest.main()