aboutsummaryrefslogtreecommitdiff
path: root/llvm_tools/llvm_patch_management.py
blob: 90f9a5c0bdf8b7425b298563a1f7224c9047ce94 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2019 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# pylint: disable=global-statement

"""Creates the arguments for the patch manager for LLVM."""

from __future__ import print_function

import argparse
import os

from failure_modes import FailureModes
import chroot
import get_llvm_hash
import patch_manager
import subprocess_helpers

# If set to `True`, then the contents of `stdout` after executing a command will
# be displayed to the terminal.
verbose = False


def GetCommandLineArgs():
  """Parses the commandline for the optional commandline arguments.

  Returns:
    An argument parser object that contains all the commandline arguments.
  """

  # Default path to the chroot if a path is not specified.
  cros_root = os.path.expanduser('~')
  cros_root = os.path.join(cros_root, 'chromiumos')

  # Create parser and add optional command-line arguments.
  parser = argparse.ArgumentParser(description='Patch management for packages.')

  # Add argument for a specific chroot path.
  parser.add_argument(
      '--chroot_path',
      type=patch_manager.is_directory,
      default=cros_root,
      help='the absolute path to the chroot (default: %(default)s)')

  # Add argument for which packages to manage their patches.
  parser.add_argument(
      '--packages',
      required=False,
      nargs='+',
      default=['sys-devel/llvm'],
      help='the packages to manage their patches (default: %(default)s)')

  # Add argument for whether to display command contents to `stdout`.
  parser.add_argument(
      '--verbose',
      action='store_true',
      help='display contents of a command to the terminal '
      '(default: %(default)s)')

  # Add argument for the LLVM version to use for patch management.
  parser.add_argument(
      '--llvm_version',
      type=int,
      help='the LLVM version to use for patch management. Alternatively, you '
      'can pass "google3" or "google3-unstable". (Default: "google3")')

  # Add argument for the mode of the patch management when handling patches.
  parser.add_argument(
      '--failure_mode',
      default=FailureModes.FAIL.value,
      choices=[FailureModes.FAIL.value, FailureModes.CONTINUE.value,
               FailureModes.DISABLE_PATCHES.value,
               FailureModes.REMOVE_PATCHES.value],
      help='the mode of the patch manager when handling failed patches ' \
          '(default: %(default)s)')

  # Add argument for the patch metadata file in $FILESDIR of LLVM.
  parser.add_argument(
      '--patch_metadata_file',
      default='PATCHES.json',
      help='the .json file in $FILESDIR that has all the patches and their '
      'metadata if applicable (default: %(default)s)')

  # Parse the command line.
  args_output = parser.parse_args()

  global verbose

  verbose = args_output.verbose

  unique_packages = list(set(args_output.packages))

  # Duplicate packages were passed into the command line
  if len(unique_packages) != len(args_output.packages):
    raise ValueError('Duplicate packages were passed in: %s' % ' '.join(
        args_output.packages))

  args_output.packages = unique_packages

  return args_output


def GetPathToFilesDirectory(chroot_path, package):
  """Gets the absolute path to $FILESDIR of the package.

  Args:
    chroot_path: The absolute path to the chroot.
    package: The package to find its absolute path to $FILESDIR.

  Returns:
    The  absolute path to $FILESDIR.

  Raises:
    ValueError: An invalid chroot path has been provided.
  """

  if not os.path.isdir(chroot_path):
    raise ValueError('Invalid chroot provided: %s' % chroot_path)

  # Get the absolute chroot path to the ebuild.
  chroot_ebuild_path = subprocess_helpers.ChrootRunCommand(
      chroot_path, ['equery', 'w', package], verbose=verbose)

  # Get the absolute chroot path to $FILESDIR's parent directory.
  filesdir_parent_path = os.path.dirname(chroot_ebuild_path.strip())

  # Get the relative path to $FILESDIR's parent directory.
  rel_path = _GetRelativePathOfChrootPath(filesdir_parent_path)

  # Construct the absolute path to the package's 'files' directory.
  return os.path.join(chroot_path, rel_path, 'files/')


def _GetRelativePathOfChrootPath(chroot_path):
  """Gets the relative path of the chroot path passed in.

  Args:
    chroot_path: The chroot path to get its relative path.

  Returns:
    The relative path after '/mnt/host/source/'.

  Raises:
    ValueError: The prefix of 'chroot_path' did not match '/mnt/host/source/'.
  """

  chroot_prefix = '/mnt/host/source/'

  if not chroot_path.startswith(chroot_prefix):
    raise ValueError('Invalid prefix for the chroot path: %s' % chroot_path)

  return chroot_path[len(chroot_prefix):]


def _CheckPatchMetadataPath(patch_metadata_path):
  """Checks that the patch metadata path is valid.

  Args:
    patch_metadata_path: The absolute path to the .json file that has the
    patches and their metadata.

  Raises:
    ValueError: The file does not exist or the file does not end in '.json'.
  """

  if not os.path.isfile(patch_metadata_path):
    raise ValueError('Invalid file provided: %s' % patch_metadata_path)

  if not patch_metadata_path.endswith('.json'):
    raise ValueError('File does not end in ".json": %s' % patch_metadata_path)


def _MoveSrcTreeHEADToGitHash(src_path, git_hash):
  """Moves HEAD to 'git_hash'."""

  move_head_cmd = ['git', '-C', src_path, 'checkout', git_hash]

  subprocess_helpers.ExecCommandAndCaptureOutput(move_head_cmd, verbose=verbose)


def UpdatePackagesPatchMetadataFile(chroot_path, svn_version,
                                    patch_metadata_file, packages, mode):
  """Updates the packages metadata file.

  Args:
    chroot_path: The absolute path to the chroot.
    svn_version: The version to use for patch management.
    patch_metadata_file: The patch metadta file where all the patches and
    their metadata are.
    packages: All the packages to update their patch metadata file.
    mode: The mode for the patch manager to use when an applicable patch
    fails to apply.
      Ex: 'FailureModes.FAIL'

  Returns:
    A dictionary where the key is the package name and the value is a dictionary
    that has information on the patches.
  """

  # A dictionary where the key is the package name and the value is a dictionary
  # that has information on the patches.
  package_info = {}

  llvm_hash = get_llvm_hash.LLVMHash()

  with llvm_hash.CreateTempDirectory() as temp_dir:
    with get_llvm_hash.CreateTempLLVMRepo(temp_dir) as src_path:
      # Ensure that 'svn_version' exists in the chromiumum mirror of LLVM by
      # finding its corresponding git hash.
      git_hash = get_llvm_hash.GetGitHashFrom(src_path, svn_version)

      # Git hash of 'svn_version' exists, so move the source tree's HEAD to
      # 'git_hash' via `git checkout`.
      _MoveSrcTreeHEADToGitHash(src_path, git_hash)

      for cur_package in packages:
        # Get the absolute path to $FILESDIR of the package.
        filesdir_path = GetPathToFilesDirectory(chroot_path, cur_package)

        # Construct the absolute path to the patch metadata file where all the
        # patches and their metadata are.
        patch_metadata_path = os.path.join(filesdir_path, patch_metadata_file)

        # Make sure the patch metadata path is valid.
        _CheckPatchMetadataPath(patch_metadata_path)

        patch_manager.CleanSrcTree(src_path)

        # Get the patch results for the current package.
        patches_info = patch_manager.HandlePatches(
            svn_version, patch_metadata_path, filesdir_path, src_path, mode)

        package_info[cur_package] = patches_info._asdict()

  return package_info


def main():
  """Updates the patch metadata file of each package if possible.

  Raises:
    AssertionError: The script was run inside the chroot.
  """

  chroot.VerifyOutsideChroot()

  args_output = GetCommandLineArgs()

  # Get the google3 LLVM version if a LLVM version was not provided.
  llvm_version = args_output.llvm_version
  if llvm_version in ('', 'google3', 'google3-unstable'):
    llvm_version = get_llvm_hash.GetGoogle3LLVMVersion(
        stable=llvm_version != 'google3-unstable')

  UpdatePackagesPatchMetadataFile(args_output.chroot_path, llvm_version,
                                  args_output.patch_metadata_file,
                                  args_output.packages,
                                  FailureModes(args_output.failure_mode))

  # Only 'disable_patches' and 'remove_patches' can potentially modify the patch
  # metadata file.
  if args_output.failure_mode == FailureModes.DISABLE_PATCHES.value or \
      args_output.failure_mode == FailureModes.REMOVE_PATCHES.value:
    print('The patch file %s has been modified for the packages:' %
          args_output.patch_metadata_file)
    print('\n'.join(args_output.packages))
  else:
    print('Applicable patches in %s applied successfully.' %
          args_output.patch_metadata_file)


if __name__ == '__main__':
  main()