1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# Copyright 2019 The Chromium OS 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# pylint: disable=global-statement
8
9"""Creates the arguments for the patch manager for LLVM."""
10
11from __future__ import print_function
12
13import argparse
14import os
15
16from failure_modes import FailureModes
17import chroot
18import get_llvm_hash
19import patch_manager
20import subprocess_helpers
21
22# If set to `True`, then the contents of `stdout` after executing a command will
23# be displayed to the terminal.
24verbose = False
25
26
27def GetCommandLineArgs():
28  """Parses the commandline for the optional commandline arguments.
29
30  Returns:
31    An argument parser object that contains all the commandline arguments.
32  """
33
34  # Default path to the chroot if a path is not specified.
35  cros_root = os.path.expanduser('~')
36  cros_root = os.path.join(cros_root, 'chromiumos')
37
38  # Create parser and add optional command-line arguments.
39  parser = argparse.ArgumentParser(description='Patch management for packages.')
40
41  # Add argument for a specific chroot path.
42  parser.add_argument(
43      '--chroot_path',
44      type=patch_manager.is_directory,
45      default=cros_root,
46      help='the absolute path to the chroot (default: %(default)s)')
47
48  # Add argument for which packages to manage their patches.
49  parser.add_argument(
50      '--packages',
51      required=False,
52      nargs='+',
53      default=['sys-devel/llvm'],
54      help='the packages to manage their patches (default: %(default)s)')
55
56  # Add argument for whether to display command contents to `stdout`.
57  parser.add_argument(
58      '--verbose',
59      action='store_true',
60      help='display contents of a command to the terminal '
61      '(default: %(default)s)')
62
63  # Add argument for the LLVM version to use for patch management.
64  parser.add_argument(
65      '--llvm_version',
66      type=int,
67      help='the LLVM version to use for patch management. Alternatively, you '
68      'can pass "google3" or "google3-unstable". (Default: "google3")')
69
70  # Add argument for the mode of the patch management when handling patches.
71  parser.add_argument(
72      '--failure_mode',
73      default=FailureModes.FAIL.value,
74      choices=[FailureModes.FAIL.value, FailureModes.CONTINUE.value,
75               FailureModes.DISABLE_PATCHES.value,
76               FailureModes.REMOVE_PATCHES.value],
77      help='the mode of the patch manager when handling failed patches ' \
78          '(default: %(default)s)')
79
80  # Add argument for the patch metadata file in $FILESDIR of LLVM.
81  parser.add_argument(
82      '--patch_metadata_file',
83      default='PATCHES.json',
84      help='the .json file in $FILESDIR that has all the patches and their '
85      'metadata if applicable (default: %(default)s)')
86
87  # Parse the command line.
88  args_output = parser.parse_args()
89
90  global verbose
91
92  verbose = args_output.verbose
93
94  unique_packages = list(set(args_output.packages))
95
96  # Duplicate packages were passed into the command line
97  if len(unique_packages) != len(args_output.packages):
98    raise ValueError('Duplicate packages were passed in: %s' % ' '.join(
99        args_output.packages))
100
101  args_output.packages = unique_packages
102
103  return args_output
104
105
106def GetPathToFilesDirectory(chroot_path, package):
107  """Gets the absolute path to $FILESDIR of the package.
108
109  Args:
110    chroot_path: The absolute path to the chroot.
111    package: The package to find its absolute path to $FILESDIR.
112
113  Returns:
114    The  absolute path to $FILESDIR.
115
116  Raises:
117    ValueError: An invalid chroot path has been provided.
118  """
119
120  if not os.path.isdir(chroot_path):
121    raise ValueError('Invalid chroot provided: %s' % chroot_path)
122
123  # Get the absolute chroot path to the ebuild.
124  chroot_ebuild_path = subprocess_helpers.ChrootRunCommand(
125      chroot_path, ['equery', 'w', package], verbose=verbose)
126
127  # Get the absolute chroot path to $FILESDIR's parent directory.
128  filesdir_parent_path = os.path.dirname(chroot_ebuild_path.strip())
129
130  # Get the relative path to $FILESDIR's parent directory.
131  rel_path = _GetRelativePathOfChrootPath(filesdir_parent_path)
132
133  # Construct the absolute path to the package's 'files' directory.
134  return os.path.join(chroot_path, rel_path, 'files/')
135
136
137def _GetRelativePathOfChrootPath(chroot_path):
138  """Gets the relative path of the chroot path passed in.
139
140  Args:
141    chroot_path: The chroot path to get its relative path.
142
143  Returns:
144    The relative path after '/mnt/host/source/'.
145
146  Raises:
147    ValueError: The prefix of 'chroot_path' did not match '/mnt/host/source/'.
148  """
149
150  chroot_prefix = '/mnt/host/source/'
151
152  if not chroot_path.startswith(chroot_prefix):
153    raise ValueError('Invalid prefix for the chroot path: %s' % chroot_path)
154
155  return chroot_path[len(chroot_prefix):]
156
157
158def _CheckPatchMetadataPath(patch_metadata_path):
159  """Checks that the patch metadata path is valid.
160
161  Args:
162    patch_metadata_path: The absolute path to the .json file that has the
163    patches and their metadata.
164
165  Raises:
166    ValueError: The file does not exist or the file does not end in '.json'.
167  """
168
169  if not os.path.isfile(patch_metadata_path):
170    raise ValueError('Invalid file provided: %s' % patch_metadata_path)
171
172  if not patch_metadata_path.endswith('.json'):
173    raise ValueError('File does not end in ".json": %s' % patch_metadata_path)
174
175
176def _MoveSrcTreeHEADToGitHash(src_path, git_hash):
177  """Moves HEAD to 'git_hash'."""
178
179  move_head_cmd = ['git', '-C', src_path, 'checkout', git_hash]
180
181  subprocess_helpers.ExecCommandAndCaptureOutput(move_head_cmd, verbose=verbose)
182
183
184def UpdatePackagesPatchMetadataFile(chroot_path, svn_version,
185                                    patch_metadata_file, packages, mode):
186  """Updates the packages metadata file.
187
188  Args:
189    chroot_path: The absolute path to the chroot.
190    svn_version: The version to use for patch management.
191    patch_metadata_file: The patch metadta file where all the patches and
192    their metadata are.
193    packages: All the packages to update their patch metadata file.
194    mode: The mode for the patch manager to use when an applicable patch
195    fails to apply.
196      Ex: 'FailureModes.FAIL'
197
198  Returns:
199    A dictionary where the key is the package name and the value is a dictionary
200    that has information on the patches.
201  """
202
203  # A dictionary where the key is the package name and the value is a dictionary
204  # that has information on the patches.
205  package_info = {}
206
207  llvm_hash = get_llvm_hash.LLVMHash()
208
209  with llvm_hash.CreateTempDirectory() as temp_dir:
210    with get_llvm_hash.CreateTempLLVMRepo(temp_dir) as src_path:
211      # Ensure that 'svn_version' exists in the chromiumum mirror of LLVM by
212      # finding its corresponding git hash.
213      git_hash = get_llvm_hash.GetGitHashFrom(src_path, svn_version)
214
215      # Git hash of 'svn_version' exists, so move the source tree's HEAD to
216      # 'git_hash' via `git checkout`.
217      _MoveSrcTreeHEADToGitHash(src_path, git_hash)
218
219      for cur_package in packages:
220        # Get the absolute path to $FILESDIR of the package.
221        filesdir_path = GetPathToFilesDirectory(chroot_path, cur_package)
222
223        # Construct the absolute path to the patch metadata file where all the
224        # patches and their metadata are.
225        patch_metadata_path = os.path.join(filesdir_path, patch_metadata_file)
226
227        # Make sure the patch metadata path is valid.
228        _CheckPatchMetadataPath(patch_metadata_path)
229
230        patch_manager.CleanSrcTree(src_path)
231
232        # Get the patch results for the current package.
233        patches_info = patch_manager.HandlePatches(
234            svn_version, patch_metadata_path, filesdir_path, src_path, mode)
235
236        package_info[cur_package] = patches_info._asdict()
237
238  return package_info
239
240
241def main():
242  """Updates the patch metadata file of each package if possible.
243
244  Raises:
245    AssertionError: The script was run inside the chroot.
246  """
247
248  chroot.VerifyOutsideChroot()
249
250  args_output = GetCommandLineArgs()
251
252  # Get the google3 LLVM version if a LLVM version was not provided.
253  llvm_version = args_output.llvm_version
254  if llvm_version in ('', 'google3', 'google3-unstable'):
255    llvm_version = get_llvm_hash.GetGoogle3LLVMVersion(
256        stable=llvm_version != 'google3-unstable')
257
258  UpdatePackagesPatchMetadataFile(args_output.chroot_path, llvm_version,
259                                  args_output.patch_metadata_file,
260                                  args_output.packages,
261                                  FailureModes(args_output.failure_mode))
262
263  # Only 'disable_patches' and 'remove_patches' can potentially modify the patch
264  # metadata file.
265  if args_output.failure_mode == FailureModes.DISABLE_PATCHES.value or \
266      args_output.failure_mode == FailureModes.REMOVE_PATCHES.value:
267    print('The patch file %s has been modified for the packages:' %
268          args_output.patch_metadata_file)
269    print('\n'.join(args_output.packages))
270  else:
271    print('Applicable patches in %s applied successfully.' %
272          args_output.patch_metadata_file)
273
274
275if __name__ == '__main__':
276  main()
277