filescfg.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  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. import tarfile
  13. import zipfile
  14. from pathlib import Path
  15. from _common import get_logger, add_common_params
  16. def filescfg_generator(cfg_path, build_outputs, cpu_arch):
  17. """
  18. Generator that yields pathlib.Path relative to the build outputs according to FILES.cfg
  19. cfg_path is a pathlib.Path to the FILES.cfg
  20. build_outputs is a pathlib.Path to the build outputs directory.
  21. cpu_arch is a platform.architecture() string
  22. """
  23. resolved_build_outputs = build_outputs.resolve()
  24. exec_globals = {'__builtins__': None}
  25. with cfg_path.open() as cfg_file:
  26. exec(cfg_file.read(), exec_globals) # pylint: disable=exec-used
  27. for file_spec in exec_globals['FILES']:
  28. # Only include files for official builds
  29. if 'official' not in file_spec['buildtype']:
  30. continue
  31. # If a file has an 'arch' field, it must have cpu_arch to be included
  32. if 'arch' in file_spec and cpu_arch not in file_spec['arch']:
  33. continue
  34. # From chrome/tools/build/make_zip.py, 'filename' is actually a glob pattern
  35. for file_path in resolved_build_outputs.glob(file_spec['filename']):
  36. # Do not package Windows debugging symbols
  37. if file_path.suffix.lower() == '.pdb':
  38. continue
  39. yield file_path.relative_to(resolved_build_outputs)
  40. def _get_archive_writer(output_path):
  41. """
  42. Detects and returns the appropriate archive writer
  43. output_path is the pathlib.Path of the archive to write
  44. """
  45. if not output_path.suffixes:
  46. raise ValueError('Output name has no suffix: %s' % output_path.name)
  47. if output_path.suffixes[-1].lower() == '.zip':
  48. archive_root = Path(output_path.stem)
  49. output_archive = zipfile.ZipFile(str(output_path), 'w', zipfile.ZIP_DEFLATED)
  50. def add_func(in_path, arc_path):
  51. """Add files to zip archive"""
  52. if in_path.is_dir():
  53. for sub_path in in_path.rglob('*'):
  54. output_archive.write(str(sub_path),
  55. str(arc_path / sub_path.relative_to(in_path)))
  56. else:
  57. output_archive.write(str(in_path), str(arc_path))
  58. elif '.tar' in output_path.name.lower():
  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(filescfg_generator(args.cfg, args.build_outputs, args.cpu_arch), args.include,
  104. args.build_outputs, args.output)
  105. def main():
  106. """CLI Entrypoint"""
  107. parser = argparse.ArgumentParser()
  108. parser.add_argument('-c',
  109. '--cfg',
  110. metavar='PATH',
  111. type=Path,
  112. required=True,
  113. help=('The FILES.cfg to use. They are usually located under a '
  114. 'directory in chrome/tools/build/ of the source tree.'))
  115. parser.add_argument('--build-outputs',
  116. metavar='PATH',
  117. type=Path,
  118. default='out/Default',
  119. help=('The path to the build outputs directory relative to the '
  120. 'source tree. Default: %(default)s'))
  121. parser.add_argument('--cpu-arch',
  122. metavar='ARCH',
  123. default=platform.architecture()[0],
  124. choices=('64bit', '32bit'),
  125. help=('Filter build outputs by a target CPU. '
  126. 'This is the same as the "arch" key in FILES.cfg. '
  127. 'Default (from platform.architecture()): %(default)s'))
  128. add_common_params(parser)
  129. subparsers = parser.add_subparsers(title='filescfg actions')
  130. # list
  131. list_parser = subparsers.add_parser('list', help=_list_callback.__doc__)
  132. list_parser.set_defaults(callback=_list_callback)
  133. # archive
  134. archive_parser = subparsers.add_parser('archive', help=_archive_callback.__doc__)
  135. archive_parser.add_argument(
  136. '-o',
  137. '--output',
  138. type=Path,
  139. metavar='PATH',
  140. required=True,
  141. help=('The output path for the archive. The type of archive is selected'
  142. ' by the file extension. Currently supported types: .zip and'
  143. ' .tar.{gz,bz2,xz}'))
  144. archive_parser.add_argument(
  145. '-i',
  146. '--include',
  147. type=Path,
  148. metavar='PATH',
  149. action='append',
  150. default=[],
  151. help=('File or directory to include in the root of the archive. Specify '
  152. 'multiple times to include multiple different items. '
  153. 'For zip files, these contents must only be regular files.'))
  154. archive_parser.set_defaults(callback=_archive_callback)
  155. args = parser.parse_args()
  156. args.callback(args)
  157. if __name__ == '__main__':
  158. main()