@@ -42,7 +42,8 @@ class TarFile(object):
4242 class DebError (Exception ):
4343 pass
4444
45- def __init__ (self , output , directory , compression , compressor , create_parents , allow_dups_from_deps , default_mtime ):
45+ def __init__ (self , output , directory , compression , compressor , create_parents ,
46+ allow_dups_from_deps , auto_deduplicate , default_mtime , compresslevel = None ):
4647 # Directory prefix on all output paths
4748 d = directory .strip ('/' )
4849 self .directory = (d + '/' ) if d else None
@@ -52,6 +53,8 @@ def __init__(self, output, directory, compression, compressor, create_parents, a
5253 self .default_mtime = default_mtime
5354 self .create_parents = create_parents
5455 self .allow_dups_from_deps = allow_dups_from_deps
56+ self .compresslevel = compresslevel
57+ self .src_to_first_dest_map = {} if auto_deduplicate else None
5558
5659 def __enter__ (self ):
5760 self .tarfile = tar_writer .TarFileWriter (
@@ -60,7 +63,8 @@ def __enter__(self):
6063 self .compressor ,
6164 self .create_parents ,
6265 self .allow_dups_from_deps ,
63- default_mtime = self .default_mtime )
66+ default_mtime = self .default_mtime ,
67+ compresslevel = self .compresslevel )
6468 return self
6569
6670 def __exit__ (self , t , v , traceback ):
@@ -98,6 +102,12 @@ def add_file(self, f, destfile, mode=None, ids=None, names=None):
98102 copied to `self.directory/destfile` in the layer.
99103 """
100104 dest = self .normalize_path (destfile )
105+ if self .src_to_first_dest_map is not None :
106+ normalized_src = normpath (f )
107+ relative_path_to_link_to = self .auto_deduplicate (normalized_src , dest )
108+ if relative_path_to_link_to :
109+ self .add_link (dest , relative_path_to_link_to , mode = mode , ids = ids , names = names )
110+ return
101111 # If mode is unspecified, derive the mode from the file's mode.
102112 if mode is None :
103113 mode = 0o755 if os .access (f , os .X_OK ) else 0o644
@@ -114,6 +124,23 @@ def add_file(self, f, destfile, mode=None, ids=None, names=None):
114124 uname = names [0 ],
115125 gname = names [1 ])
116126
127+ def auto_deduplicate (self , src_file , dest_file ):
128+ """Detect whether to de-duplicate the destination file
129+
130+ Returns:
131+ The relative path to create a symlink to or None
132+ """
133+ if self .src_to_first_dest_map is not None :
134+ first_dest = self .src_to_first_dest_map .get (src_file )
135+ if first_dest is None :
136+ real_src_file = os .path .realpath (src_file )
137+ first_dest = self .src_to_first_dest_map .setdefault (real_src_file , dest_file )
138+ self .src_to_first_dest_map [src_file ] = first_dest
139+ if first_dest != dest_file :
140+ return os .path .relpath (first_dest , os .path .dirname (dest_file ))
141+ return None
142+
143+
117144 def add_empty_file (self ,
118145 destfile ,
119146 mode = None ,
@@ -269,13 +296,13 @@ def add_tree(self, tree_top, destpath, mode=None, ids=None, names=None):
269296 for dir in dirs :
270297 to_write [dest_dir + dir ] = None
271298 for file in sorted (files ):
272- content_path = os .path .abspath ( os . path . join (root , file ) )
299+ content_path = os .path .join (root , file )
273300 if os .name == "nt" :
274301 # "To specify an extended-length path, use the `\\?\` prefix. For
275302 # example, `\\?\D:\very long path`."[1]
276303 #
277304 # [1]: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
278- to_write [dest_dir + file ] = "\\ \\ ?\\ " + content_path
305+ to_write [dest_dir + file ] = "\\ \\ ?\\ " + os . path . abspath ( content_path )
279306 else :
280307 to_write [dest_dir + file ] = content_path
281308
@@ -297,6 +324,10 @@ def add_tree(self, tree_top, destpath, mode=None, ids=None, names=None):
297324 f_mode = 0o755 if os .access (content_path , os .X_OK ) else 0o644
298325 else :
299326 f_mode = mode
327+ relative_path_to_link_to = self .auto_deduplicate (content_path , dest )
328+ if relative_path_to_link_to :
329+ self .add_link (dest , relative_path_to_link_to , mode = f_mode , ids = ids , names = names )
330+ continue
300331 self .tarfile .add_file (
301332 path ,
302333 file_content = content_path ,
@@ -345,7 +376,7 @@ def main():
345376 fromfile_prefix_chars = '@' )
346377 parser .add_argument ('--output' , required = True ,
347378 help = 'The output file, mandatory.' )
348- parser .add_argument ('--manifest' ,
379+ parser .add_argument ('--manifest' , action = 'append' ,
349380 help = 'manifest of contents to add to the layer.' )
350381 parser .add_argument ('--mode' ,
351382 help = 'Force the mode on the added files (in octal).' )
@@ -359,7 +390,7 @@ def main():
359390 parser .add_argument ('--deb' , action = 'append' ,
360391 help = 'A debian package to add to the layer' )
361392 parser .add_argument (
362- '--directory' ,
393+ '--directory' , action = 'append' ,
363394 help = 'Directory in which to store the file inside the layer' )
364395
365396 compression = parser .add_mutually_exclusive_group ()
@@ -397,6 +428,12 @@ def main():
397428 parser .add_argument ('--allow_dups_from_deps' ,
398429 action = 'store_true' ,
399430 help = '' )
431+ parser .add_argument ('--auto_deduplicate' ,
432+ action = 'store_true' ,
433+ help = 'Auto create symlinks for files mapped from a same source in manifests.' )
434+ parser .add_argument (
435+ '--compresslevel' , default = '' ,
436+ help = 'Specify the numeric compress level in gzip mode; may be 0-9 or empty(6).' )
400437 options = parser .parse_args ()
401438
402439 # Parse modes arguments
@@ -443,12 +480,14 @@ def main():
443480 # Add objects to the tar file
444481 with TarFile (
445482 options .output ,
446- directory = helpers .GetFlagValue (options .directory ),
483+ directory = helpers .GetFlagValue (options .directory [ 0 ] ),
447484 compression = options .compression ,
448485 compressor = options .compressor ,
449486 default_mtime = default_mtime ,
450487 create_parents = options .create_parents ,
451- allow_dups_from_deps = options .allow_dups_from_deps ) as output :
488+ allow_dups_from_deps = options .allow_dups_from_deps ,
489+ auto_deduplicate = options .auto_deduplicate ,
490+ compresslevel = options .compresslevel ) as output :
452491
453492 def file_attributes (filename ):
454493 if filename .startswith ('/' ):
@@ -459,12 +498,18 @@ def file_attributes(filename):
459498 'names' : names_map .get (filename , default_ownername ),
460499 }
461500
462- if options .manifest :
463- with open (options .manifest , 'r' ) as manifest_fp :
501+ formatted_first_directory = output .directory
502+ manifest_list = zip (options .directory , options .manifest )
503+ if options .auto_deduplicate :
504+ manifest_list = list (manifest_list )[::- 1 ]
505+ for directory , manifest_path in manifest_list :
506+ output .directory = (directory .strip ('/' ) + '/' ) if directory .strip ('/' ) else None
507+ with open (manifest_path , 'r' ) as manifest_fp :
464508 manifest_entries = manifest .read_entries_from (manifest_fp )
465509 for entry in manifest_entries :
466510 output .add_manifest_entry (entry , file_attributes )
467511
512+ output .directory = formatted_first_directory
468513 for tar in options .tar or []:
469514 output .add_tar (tar )
470515 for deb in options .deb or []:
0 commit comments