filescfg.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. #!/usr/bin/env python3
  2. # -*- coding: UTF-8 -*-
  3. # Copyright (c) 2019 The ungoogled-chromium Authors. All rights reserved.
  4. # Use of this source code is governed by a BSD-style license that can be
  5. # found in the LICENSE file.
  6. """
  7. Operations with FILES.cfg (for portable packages)
  8. """
  9. import argparse
  10. import datetime
  11. import platform
  12. import sys
  13. import tarfile
  14. import zipfile
  15. from pathlib import Path
  16. from _common import get_logger, add_common_params
  17. def filescfg_generator(cfg_path, build_outputs, cpu_arch, excluded_files=None):
  18. """
  19. Generator that yields pathlib.Path relative to the build outputs according to FILES.cfg
  20. cfg_path is a pathlib.Path to the FILES.cfg
  21. build_outputs is a pathlib.Path to the build outputs directory.
  22. cpu_arch is a platform.architecture() string
  23. """
  24. resolved_build_outputs = build_outputs.resolve()
  25. exec_globals = {'__builtins__': None}
  26. with cfg_path.open() as cfg_file:
  27. exec(cfg_file.read(), exec_globals) # pylint: disable=exec-used
  28. for file_spec in exec_globals['FILES']:
  29. # Only include files for official builds
  30. if 'official' not in file_spec['buildtype']:
  31. continue
  32. # If a file has an 'arch' field, it must have cpu_arch to be included
  33. if 'arch' in file_spec and cpu_arch not in file_spec['arch']:
  34. continue
  35. # From chrome/tools/build/make_zip.py, 'filename' is actually a glob pattern
  36. for file_path in resolved_build_outputs.glob(file_spec['filename']):
  37. # Do not package Windows debugging symbols
  38. if file_path.suffix.lower() == '.pdb':
  39. continue
  40. file_path_rel = file_path.relative_to(resolved_build_outputs)
  41. if excluded_files and file_path_rel in excluded_files:
  42. continue
  43. yield file_path_rel
  44. def _get_archive_writer(output_path, timestamp=None):
  45. """
  46. Detects and returns the appropriate archive writer
  47. output_path is the pathlib.Path of the archive to write.
  48. timestamp is a file timestamp to use for all files, if set.
  49. """
  50. if not output_path.suffixes:
  51. raise ValueError('Output name has no suffix: %s' % output_path.name)
  52. if output_path.suffixes[-1].lower() == '.zip':
  53. archive_root = Path(output_path.stem)
  54. output_archive = zipfile.ZipFile(str(output_path), 'w', zipfile.ZIP_DEFLATED)
  55. zip_date_time = None
  56. if timestamp:
  57. zip_date_time = datetime.datetime.fromtimestamp(timestamp).timetuple()[:6]
  58. def zip_write(in_path, arc_path):
  59. if zip_date_time:
  60. info = zipfile.ZipInfo.from_file(in_path, arc_path)
  61. info.date_time = zip_date_time
  62. with open(in_path, 'rb') as in_file:
  63. output_archive.writestr(info, in_file.read())
  64. else:
  65. output_archive.write(in_path, arc_path)
  66. def add_func(in_path, arc_path):
  67. """Add files to zip archive"""
  68. if in_path.is_dir():
  69. for sub_path in in_path.rglob('*'):
  70. zip_write(str(sub_path), str(arc_path / sub_path.relative_to(in_path)))
  71. else:
  72. zip_write(str(in_path), str(arc_path))
  73. elif '.tar' in output_path.name.lower():
  74. if len(output_path.suffixes) >= 2 and output_path.suffixes[-2].lower() == '.tar':
  75. tar_mode = 'w:%s' % output_path.suffixes[-1][1:]
  76. archive_root = Path(output_path.with_suffix('').stem)
  77. elif output_path.suffixes[-1].lower() == '.tar':
  78. tar_mode = 'w'
  79. archive_root = Path(output_path.stem)
  80. else:
  81. raise ValueError('Could not detect tar format for output: %s' % output_path.name)
  82. if timestamp:
  83. class TarInfoFixedTimestamp(tarfile.TarInfo):
  84. """TarInfo class with predefined constant mtime"""
  85. @property
  86. def mtime(self):
  87. """Return predefined timestamp"""
  88. return timestamp
  89. @mtime.setter
  90. def mtime(self, value):
  91. """Ignore incoming value"""
  92. tarinfo_class = TarInfoFixedTimestamp
  93. else:
  94. tarinfo_class = tarfile.TarInfo
  95. output_archive = tarfile.open(str(output_path), tar_mode, tarinfo=tarinfo_class)
  96. add_func = lambda in_path, arc_path: output_archive.add(str(in_path), str(arc_path))
  97. else:
  98. raise ValueError('Unknown archive extension with name: %s' % output_path.name)
  99. return output_archive, add_func, archive_root
  100. def create_archive(file_iter, include_iter, build_outputs, output_path, timestamp=None):
  101. """
  102. Create an archive of the build outputs. Supports zip and compressed tar archives.
  103. file_iter is an iterable of files to include in the archive.
  104. output_path is the pathlib.Path to write the new archive.
  105. build_outputs is a pathlib.Path to the build outputs.
  106. timestamp is a file timestamp (Unix format) to use for all files, if set.
  107. """
  108. output_archive, add_func, archive_root = _get_archive_writer(output_path, timestamp)
  109. with output_archive:
  110. for relative_path in file_iter:
  111. add_func(build_outputs / relative_path, archive_root / relative_path)
  112. for include_path in include_iter:
  113. add_func(include_path, archive_root / include_path.name)
  114. def _files_generator_by_args(args):
  115. """Returns a files_generator() instance from the CLI args"""
  116. # --build-outputs
  117. if not args.build_outputs.exists():
  118. get_logger().error('Could not find build outputs: %s', args.build_outputs)
  119. raise FileNotFoundError(args.build_outputs)
  120. # --cfg
  121. if not args.cfg.exists():
  122. get_logger().error('Could not find FILES.cfg at %s', args.cfg)
  123. raise FileNotFoundError(args.cfg)
  124. return filescfg_generator(args.cfg, args.build_outputs, args.cpu_arch)
  125. def _list_callback(args):
  126. """List files needed to run Chromium."""
  127. sys.stdout.writelines('%s\n' % x for x in _files_generator_by_args(args))
  128. def _archive_callback(args):
  129. """
  130. Create an archive of the build outputs. Supports zip and compressed tar archives.
  131. """
  132. create_archive(filescfg_generator(args.cfg, args.build_outputs, args.cpu_arch), args.include,
  133. args.build_outputs, args.output)
  134. def main():
  135. """CLI Entrypoint"""
  136. parser = argparse.ArgumentParser()
  137. parser.add_argument('-c',
  138. '--cfg',
  139. metavar='PATH',
  140. type=Path,
  141. required=True,
  142. help=('The FILES.cfg to use. They are usually located under a '
  143. 'directory in chrome/tools/build/ of the source tree.'))
  144. parser.add_argument('--build-outputs',
  145. metavar='PATH',
  146. type=Path,
  147. default='out/Default',
  148. help=('The path to the build outputs directory relative to the '
  149. 'source tree. Default: %(default)s'))
  150. parser.add_argument('--cpu-arch',
  151. metavar='ARCH',
  152. default=platform.architecture()[0],
  153. choices=('64bit', '32bit'),
  154. help=('Filter build outputs by a target CPU. '
  155. 'This is the same as the "arch" key in FILES.cfg. '
  156. 'Default (from platform.architecture()): %(default)s'))
  157. add_common_params(parser)
  158. subparsers = parser.add_subparsers(title='filescfg actions')
  159. # list
  160. list_parser = subparsers.add_parser('list', help=_list_callback.__doc__)
  161. list_parser.set_defaults(callback=_list_callback)
  162. # archive
  163. archive_parser = subparsers.add_parser('archive', help=_archive_callback.__doc__)
  164. archive_parser.add_argument(
  165. '-o',
  166. '--output',
  167. type=Path,
  168. metavar='PATH',
  169. required=True,
  170. help=('The output path for the archive. The type of archive is selected'
  171. ' by the file extension. Currently supported types: .zip and'
  172. ' .tar.{gz,bz2,xz}'))
  173. archive_parser.add_argument(
  174. '-i',
  175. '--include',
  176. type=Path,
  177. metavar='PATH',
  178. action='append',
  179. default=[],
  180. help=('File or directory to include in the root of the archive. Specify '
  181. 'multiple times to include multiple different items. '
  182. 'For zip files, these contents must only be regular files.'))
  183. archive_parser.set_defaults(callback=_archive_callback)
  184. args = parser.parse_args()
  185. args.callback(args)
  186. if __name__ == '__main__':
  187. main()