xref: /aosp_15_r20/external/angle/build/zip_helpers.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
1*8975f5c5SAndroid Build Coastguard Worker# Copyright 2023 The Chromium Authors
2*8975f5c5SAndroid Build Coastguard Worker# Use of this source code is governed by a BSD-style license that can be
3*8975f5c5SAndroid Build Coastguard Worker# found in the LICENSE file.
4*8975f5c5SAndroid Build Coastguard Worker"""Helper functions for dealing with .zip files."""
5*8975f5c5SAndroid Build Coastguard Worker
6*8975f5c5SAndroid Build Coastguard Workerimport os
7*8975f5c5SAndroid Build Coastguard Workerimport pathlib
8*8975f5c5SAndroid Build Coastguard Workerimport posixpath
9*8975f5c5SAndroid Build Coastguard Workerimport stat
10*8975f5c5SAndroid Build Coastguard Workerimport time
11*8975f5c5SAndroid Build Coastguard Workerimport zipfile
12*8975f5c5SAndroid Build Coastguard Worker
13*8975f5c5SAndroid Build Coastguard Worker_FIXED_ZIP_HEADER_LEN = 30
14*8975f5c5SAndroid Build Coastguard Worker
15*8975f5c5SAndroid Build Coastguard Worker
16*8975f5c5SAndroid Build Coastguard Workerdef _set_alignment(zip_obj, zip_info, alignment):
17*8975f5c5SAndroid Build Coastguard Worker  """Sets a ZipInfo's extra field such that the file will be aligned.
18*8975f5c5SAndroid Build Coastguard Worker
19*8975f5c5SAndroid Build Coastguard Worker  Args:
20*8975f5c5SAndroid Build Coastguard Worker    zip_obj: The ZipFile object that is being written.
21*8975f5c5SAndroid Build Coastguard Worker    zip_info: The ZipInfo object about to be written.
22*8975f5c5SAndroid Build Coastguard Worker    alignment: The amount of alignment (e.g. 4, or 4*1024).
23*8975f5c5SAndroid Build Coastguard Worker  """
24*8975f5c5SAndroid Build Coastguard Worker  header_size = _FIXED_ZIP_HEADER_LEN + len(zip_info.filename)
25*8975f5c5SAndroid Build Coastguard Worker  pos = zip_obj.fp.tell() + header_size
26*8975f5c5SAndroid Build Coastguard Worker  padding_needed = (alignment - (pos % alignment)) % alignment
27*8975f5c5SAndroid Build Coastguard Worker
28*8975f5c5SAndroid Build Coastguard Worker  # Python writes |extra| to both the local file header and the central
29*8975f5c5SAndroid Build Coastguard Worker  # directory's file header. Android's zipalign tool writes only to the
30*8975f5c5SAndroid Build Coastguard Worker  # local file header, so there is more overhead in using Python to align.
31*8975f5c5SAndroid Build Coastguard Worker  zip_info.extra = b'\0' * padding_needed
32*8975f5c5SAndroid Build Coastguard Worker
33*8975f5c5SAndroid Build Coastguard Worker
34*8975f5c5SAndroid Build Coastguard Workerdef _hermetic_date_time(timestamp=None):
35*8975f5c5SAndroid Build Coastguard Worker  if not timestamp:
36*8975f5c5SAndroid Build Coastguard Worker    return (2001, 1, 1, 0, 0, 0)
37*8975f5c5SAndroid Build Coastguard Worker  utc_time = time.gmtime(timestamp)
38*8975f5c5SAndroid Build Coastguard Worker  return (utc_time.tm_year, utc_time.tm_mon, utc_time.tm_mday, utc_time.tm_hour,
39*8975f5c5SAndroid Build Coastguard Worker          utc_time.tm_min, utc_time.tm_sec)
40*8975f5c5SAndroid Build Coastguard Worker
41*8975f5c5SAndroid Build Coastguard Worker
42*8975f5c5SAndroid Build Coastguard Workerdef add_to_zip_hermetic(zip_file,
43*8975f5c5SAndroid Build Coastguard Worker                        zip_path,
44*8975f5c5SAndroid Build Coastguard Worker                        *,
45*8975f5c5SAndroid Build Coastguard Worker                        src_path=None,
46*8975f5c5SAndroid Build Coastguard Worker                        data=None,
47*8975f5c5SAndroid Build Coastguard Worker                        compress=None,
48*8975f5c5SAndroid Build Coastguard Worker                        alignment=None,
49*8975f5c5SAndroid Build Coastguard Worker                        timestamp=None):
50*8975f5c5SAndroid Build Coastguard Worker  """Adds a file to the given ZipFile with a hard-coded modified time.
51*8975f5c5SAndroid Build Coastguard Worker
52*8975f5c5SAndroid Build Coastguard Worker  Args:
53*8975f5c5SAndroid Build Coastguard Worker    zip_file: ZipFile instance to add the file to.
54*8975f5c5SAndroid Build Coastguard Worker    zip_path: Destination path within the zip file (or ZipInfo instance).
55*8975f5c5SAndroid Build Coastguard Worker    src_path: Path of the source file. Mutually exclusive with |data|.
56*8975f5c5SAndroid Build Coastguard Worker    data: File data as a string.
57*8975f5c5SAndroid Build Coastguard Worker    compress: Whether to enable compression. Default is taken from ZipFile
58*8975f5c5SAndroid Build Coastguard Worker        constructor.
59*8975f5c5SAndroid Build Coastguard Worker    alignment: If set, align the data of the entry to this many bytes.
60*8975f5c5SAndroid Build Coastguard Worker    timestamp: The last modification date and time for the archive member.
61*8975f5c5SAndroid Build Coastguard Worker  """
62*8975f5c5SAndroid Build Coastguard Worker  assert (src_path is None) != (data is None), (
63*8975f5c5SAndroid Build Coastguard Worker      '|src_path| and |data| are mutually exclusive.')
64*8975f5c5SAndroid Build Coastguard Worker  if isinstance(zip_path, zipfile.ZipInfo):
65*8975f5c5SAndroid Build Coastguard Worker    zipinfo = zip_path
66*8975f5c5SAndroid Build Coastguard Worker    zip_path = zipinfo.filename
67*8975f5c5SAndroid Build Coastguard Worker  else:
68*8975f5c5SAndroid Build Coastguard Worker    zipinfo = zipfile.ZipInfo(filename=zip_path)
69*8975f5c5SAndroid Build Coastguard Worker    zipinfo.external_attr = 0o644 << 16
70*8975f5c5SAndroid Build Coastguard Worker
71*8975f5c5SAndroid Build Coastguard Worker  zipinfo.date_time = _hermetic_date_time(timestamp)
72*8975f5c5SAndroid Build Coastguard Worker
73*8975f5c5SAndroid Build Coastguard Worker  if alignment:
74*8975f5c5SAndroid Build Coastguard Worker    _set_alignment(zip_file, zipinfo, alignment)
75*8975f5c5SAndroid Build Coastguard Worker
76*8975f5c5SAndroid Build Coastguard Worker  # Filenames can contain backslashes, but it is more likely that we've
77*8975f5c5SAndroid Build Coastguard Worker  # forgotten to use forward slashes as a directory separator.
78*8975f5c5SAndroid Build Coastguard Worker  assert '\\' not in zip_path, 'zip_path should not contain \\: ' + zip_path
79*8975f5c5SAndroid Build Coastguard Worker  assert not posixpath.isabs(zip_path), 'Absolute zip path: ' + zip_path
80*8975f5c5SAndroid Build Coastguard Worker  assert not zip_path.startswith('..'), 'Should not start with ..: ' + zip_path
81*8975f5c5SAndroid Build Coastguard Worker  assert posixpath.normpath(zip_path) == zip_path, (
82*8975f5c5SAndroid Build Coastguard Worker      f'Non-canonical zip_path: {zip_path} vs: {posixpath.normpath(zip_path)}')
83*8975f5c5SAndroid Build Coastguard Worker  assert zip_path not in zip_file.namelist(), (
84*8975f5c5SAndroid Build Coastguard Worker      'Tried to add a duplicate zip entry: ' + zip_path)
85*8975f5c5SAndroid Build Coastguard Worker
86*8975f5c5SAndroid Build Coastguard Worker  if src_path and os.path.islink(src_path):
87*8975f5c5SAndroid Build Coastguard Worker    zipinfo.external_attr |= stat.S_IFLNK << 16  # mark as a symlink
88*8975f5c5SAndroid Build Coastguard Worker    zip_file.writestr(zipinfo, os.readlink(src_path))
89*8975f5c5SAndroid Build Coastguard Worker    return
90*8975f5c5SAndroid Build Coastguard Worker
91*8975f5c5SAndroid Build Coastguard Worker  # Maintain the executable bit.
92*8975f5c5SAndroid Build Coastguard Worker  if src_path:
93*8975f5c5SAndroid Build Coastguard Worker    st = os.stat(src_path)
94*8975f5c5SAndroid Build Coastguard Worker    for mode in (stat.S_IXUSR, stat.S_IXGRP, stat.S_IXOTH):
95*8975f5c5SAndroid Build Coastguard Worker      if st.st_mode & mode:
96*8975f5c5SAndroid Build Coastguard Worker        zipinfo.external_attr |= mode << 16
97*8975f5c5SAndroid Build Coastguard Worker
98*8975f5c5SAndroid Build Coastguard Worker  if src_path:
99*8975f5c5SAndroid Build Coastguard Worker    with open(src_path, 'rb') as f:
100*8975f5c5SAndroid Build Coastguard Worker      data = f.read()
101*8975f5c5SAndroid Build Coastguard Worker
102*8975f5c5SAndroid Build Coastguard Worker  # zipfile will deflate even when it makes the file bigger. To avoid
103*8975f5c5SAndroid Build Coastguard Worker  # growing files, disable compression at an arbitrary cut off point.
104*8975f5c5SAndroid Build Coastguard Worker  if len(data) < 16:
105*8975f5c5SAndroid Build Coastguard Worker    compress = False
106*8975f5c5SAndroid Build Coastguard Worker
107*8975f5c5SAndroid Build Coastguard Worker  # None converts to ZIP_STORED, when passed explicitly rather than the
108*8975f5c5SAndroid Build Coastguard Worker  # default passed to the ZipFile constructor.
109*8975f5c5SAndroid Build Coastguard Worker  compress_type = zip_file.compression
110*8975f5c5SAndroid Build Coastguard Worker  if compress is not None:
111*8975f5c5SAndroid Build Coastguard Worker    compress_type = zipfile.ZIP_DEFLATED if compress else zipfile.ZIP_STORED
112*8975f5c5SAndroid Build Coastguard Worker  zip_file.writestr(zipinfo, data, compress_type)
113*8975f5c5SAndroid Build Coastguard Worker
114*8975f5c5SAndroid Build Coastguard Worker
115*8975f5c5SAndroid Build Coastguard Workerdef add_files_to_zip(inputs,
116*8975f5c5SAndroid Build Coastguard Worker                     output,
117*8975f5c5SAndroid Build Coastguard Worker                     *,
118*8975f5c5SAndroid Build Coastguard Worker                     base_dir=None,
119*8975f5c5SAndroid Build Coastguard Worker                     compress=None,
120*8975f5c5SAndroid Build Coastguard Worker                     zip_prefix_path=None,
121*8975f5c5SAndroid Build Coastguard Worker                     timestamp=None):
122*8975f5c5SAndroid Build Coastguard Worker  """Creates a zip file from a list of files.
123*8975f5c5SAndroid Build Coastguard Worker
124*8975f5c5SAndroid Build Coastguard Worker  Args:
125*8975f5c5SAndroid Build Coastguard Worker    inputs: A list of paths to zip, or a list of (zip_path, fs_path) tuples.
126*8975f5c5SAndroid Build Coastguard Worker    output: Path, fileobj, or ZipFile instance to add files to.
127*8975f5c5SAndroid Build Coastguard Worker    base_dir: Prefix to strip from inputs.
128*8975f5c5SAndroid Build Coastguard Worker    compress: Whether to compress
129*8975f5c5SAndroid Build Coastguard Worker    zip_prefix_path: Path prepended to file path in zip file.
130*8975f5c5SAndroid Build Coastguard Worker    timestamp: Unix timestamp to use for files in the archive.
131*8975f5c5SAndroid Build Coastguard Worker  """
132*8975f5c5SAndroid Build Coastguard Worker  if base_dir is None:
133*8975f5c5SAndroid Build Coastguard Worker    base_dir = '.'
134*8975f5c5SAndroid Build Coastguard Worker  input_tuples = []
135*8975f5c5SAndroid Build Coastguard Worker  for tup in inputs:
136*8975f5c5SAndroid Build Coastguard Worker    if isinstance(tup, str):
137*8975f5c5SAndroid Build Coastguard Worker      src_path = tup
138*8975f5c5SAndroid Build Coastguard Worker      zip_path = os.path.relpath(src_path, base_dir)
139*8975f5c5SAndroid Build Coastguard Worker      # Zip files always use / as path separator.
140*8975f5c5SAndroid Build Coastguard Worker      if os.path.sep != posixpath.sep:
141*8975f5c5SAndroid Build Coastguard Worker        zip_path = str(pathlib.Path(zip_path).as_posix())
142*8975f5c5SAndroid Build Coastguard Worker      tup = (zip_path, src_path)
143*8975f5c5SAndroid Build Coastguard Worker    input_tuples.append(tup)
144*8975f5c5SAndroid Build Coastguard Worker
145*8975f5c5SAndroid Build Coastguard Worker  # Sort by zip path to ensure stable zip ordering.
146*8975f5c5SAndroid Build Coastguard Worker  input_tuples.sort(key=lambda tup: tup[0])
147*8975f5c5SAndroid Build Coastguard Worker
148*8975f5c5SAndroid Build Coastguard Worker  out_zip = output
149*8975f5c5SAndroid Build Coastguard Worker  if not isinstance(output, zipfile.ZipFile):
150*8975f5c5SAndroid Build Coastguard Worker    out_zip = zipfile.ZipFile(output, 'w')
151*8975f5c5SAndroid Build Coastguard Worker
152*8975f5c5SAndroid Build Coastguard Worker  try:
153*8975f5c5SAndroid Build Coastguard Worker    for zip_path, fs_path in input_tuples:
154*8975f5c5SAndroid Build Coastguard Worker      if zip_prefix_path:
155*8975f5c5SAndroid Build Coastguard Worker        zip_path = posixpath.join(zip_prefix_path, zip_path)
156*8975f5c5SAndroid Build Coastguard Worker      add_to_zip_hermetic(out_zip,
157*8975f5c5SAndroid Build Coastguard Worker                          zip_path,
158*8975f5c5SAndroid Build Coastguard Worker                          src_path=fs_path,
159*8975f5c5SAndroid Build Coastguard Worker                          compress=compress,
160*8975f5c5SAndroid Build Coastguard Worker                          timestamp=timestamp)
161*8975f5c5SAndroid Build Coastguard Worker  finally:
162*8975f5c5SAndroid Build Coastguard Worker    if output is not out_zip:
163*8975f5c5SAndroid Build Coastguard Worker      out_zip.close()
164*8975f5c5SAndroid Build Coastguard Worker
165*8975f5c5SAndroid Build Coastguard Worker
166*8975f5c5SAndroid Build Coastguard Workerdef zip_directory(output, base_dir, **kwargs):
167*8975f5c5SAndroid Build Coastguard Worker  """Zips all files in the given directory."""
168*8975f5c5SAndroid Build Coastguard Worker  inputs = []
169*8975f5c5SAndroid Build Coastguard Worker  for root, _, files in os.walk(base_dir):
170*8975f5c5SAndroid Build Coastguard Worker    for f in files:
171*8975f5c5SAndroid Build Coastguard Worker      inputs.append(os.path.join(root, f))
172*8975f5c5SAndroid Build Coastguard Worker
173*8975f5c5SAndroid Build Coastguard Worker  add_files_to_zip(inputs, output, base_dir=base_dir, **kwargs)
174*8975f5c5SAndroid Build Coastguard Worker
175*8975f5c5SAndroid Build Coastguard Worker
176*8975f5c5SAndroid Build Coastguard Workerdef merge_zips(output, input_zips, path_transform=None, compress=None):
177*8975f5c5SAndroid Build Coastguard Worker  """Combines all files from |input_zips| into |output|.
178*8975f5c5SAndroid Build Coastguard Worker
179*8975f5c5SAndroid Build Coastguard Worker  Args:
180*8975f5c5SAndroid Build Coastguard Worker    output: Path, fileobj, or ZipFile instance to add files to.
181*8975f5c5SAndroid Build Coastguard Worker    input_zips: Iterable of paths to zip files to merge.
182*8975f5c5SAndroid Build Coastguard Worker    path_transform: Called for each entry path. Returns a new path, or None to
183*8975f5c5SAndroid Build Coastguard Worker        skip the file.
184*8975f5c5SAndroid Build Coastguard Worker    compress: Overrides compression setting from origin zip entries.
185*8975f5c5SAndroid Build Coastguard Worker  """
186*8975f5c5SAndroid Build Coastguard Worker  assert not isinstance(input_zips, str)  # Easy mistake to make.
187*8975f5c5SAndroid Build Coastguard Worker  if isinstance(output, zipfile.ZipFile):
188*8975f5c5SAndroid Build Coastguard Worker    out_zip = output
189*8975f5c5SAndroid Build Coastguard Worker    out_filename = output.filename
190*8975f5c5SAndroid Build Coastguard Worker  else:
191*8975f5c5SAndroid Build Coastguard Worker    assert isinstance(output, str), 'Was: ' + repr(output)
192*8975f5c5SAndroid Build Coastguard Worker    out_zip = zipfile.ZipFile(output, 'w')
193*8975f5c5SAndroid Build Coastguard Worker    out_filename = output
194*8975f5c5SAndroid Build Coastguard Worker
195*8975f5c5SAndroid Build Coastguard Worker  # Include paths in the existing zip here to avoid adding duplicate files.
196*8975f5c5SAndroid Build Coastguard Worker  crc_by_name = {i.filename: (out_filename, i.CRC) for i in out_zip.infolist()}
197*8975f5c5SAndroid Build Coastguard Worker
198*8975f5c5SAndroid Build Coastguard Worker  try:
199*8975f5c5SAndroid Build Coastguard Worker    for in_file in input_zips:
200*8975f5c5SAndroid Build Coastguard Worker      with zipfile.ZipFile(in_file, 'r') as in_zip:
201*8975f5c5SAndroid Build Coastguard Worker        for info in in_zip.infolist():
202*8975f5c5SAndroid Build Coastguard Worker          # Ignore directories.
203*8975f5c5SAndroid Build Coastguard Worker          if info.filename[-1] == '/':
204*8975f5c5SAndroid Build Coastguard Worker            continue
205*8975f5c5SAndroid Build Coastguard Worker          if path_transform:
206*8975f5c5SAndroid Build Coastguard Worker            dst_name = path_transform(info.filename)
207*8975f5c5SAndroid Build Coastguard Worker            if dst_name is None:
208*8975f5c5SAndroid Build Coastguard Worker              continue
209*8975f5c5SAndroid Build Coastguard Worker          else:
210*8975f5c5SAndroid Build Coastguard Worker            dst_name = info.filename
211*8975f5c5SAndroid Build Coastguard Worker
212*8975f5c5SAndroid Build Coastguard Worker          data = in_zip.read(info)
213*8975f5c5SAndroid Build Coastguard Worker
214*8975f5c5SAndroid Build Coastguard Worker          # If there's a duplicate file, ensure contents is the same and skip
215*8975f5c5SAndroid Build Coastguard Worker          # adding it multiple times.
216*8975f5c5SAndroid Build Coastguard Worker          if dst_name in crc_by_name:
217*8975f5c5SAndroid Build Coastguard Worker            orig_filename, orig_crc = crc_by_name[dst_name]
218*8975f5c5SAndroid Build Coastguard Worker            new_crc = zipfile.crc32(data)
219*8975f5c5SAndroid Build Coastguard Worker            if new_crc == orig_crc:
220*8975f5c5SAndroid Build Coastguard Worker              continue
221*8975f5c5SAndroid Build Coastguard Worker            msg = f"""File appeared in multiple inputs with differing contents.
222*8975f5c5SAndroid Build Coastguard WorkerFile: {dst_name}
223*8975f5c5SAndroid Build Coastguard WorkerInput1: {orig_filename}
224*8975f5c5SAndroid Build Coastguard WorkerInput2: {in_file}"""
225*8975f5c5SAndroid Build Coastguard Worker            raise Exception(msg)
226*8975f5c5SAndroid Build Coastguard Worker
227*8975f5c5SAndroid Build Coastguard Worker          if compress is not None:
228*8975f5c5SAndroid Build Coastguard Worker            compress_entry = compress
229*8975f5c5SAndroid Build Coastguard Worker          else:
230*8975f5c5SAndroid Build Coastguard Worker            compress_entry = info.compress_type != zipfile.ZIP_STORED
231*8975f5c5SAndroid Build Coastguard Worker          add_to_zip_hermetic(out_zip,
232*8975f5c5SAndroid Build Coastguard Worker                              dst_name,
233*8975f5c5SAndroid Build Coastguard Worker                              data=data,
234*8975f5c5SAndroid Build Coastguard Worker                              compress=compress_entry)
235*8975f5c5SAndroid Build Coastguard Worker          crc_by_name[dst_name] = (in_file, out_zip.getinfo(dst_name).CRC)
236*8975f5c5SAndroid Build Coastguard Worker  finally:
237*8975f5c5SAndroid Build Coastguard Worker    if output is not out_zip:
238*8975f5c5SAndroid Build Coastguard Worker      out_zip.close()
239