filescfg.py 7.0 KB

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