• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# Copyright 2020 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"""Tool to automatically generate a new Rust uprev CL.
8
9This tool is intended to automatically generate a CL to uprev Rust to a
10newer version in Chrome OS, including creating a new Rust version or
11removing an old version. It's based on
12src/third_party/chromiumos-overlay/dev-lang/rust/UPGRADE.md. When using
13the tool, the progress can be saved to a JSON file, so the user can resume
14the process after a failing step is fixed. Example usage to create a new
15version:
16
171. (inside chroot) $ ./rust_tools/rust_uprev.py
18                     --state_file /tmp/state-file.json
19                     create --rust_version 1.45.0
202. Step "compile rust" failed due to the patches can't apply to new version
213. Manually fix the patches
224. Execute the command in step 1 again.
235. Iterate 1-4 for each failed step until the tool passes.
24
25Replace `create --rust_version 1.45.0` with `remove --rust_version 1.43.0`
26if you want to remove all 1.43.0 related stuff in the same CL. Remember to
27use a different state file if you choose to run different subcommands.
28
29If you want a hammer that can do everything for you, use the subcommand
30`roll`. It can create a Rust uprev CL with `create` and `remove` and upload
31the CL to chromium code review.
32
33See `--help` for all available options.
34"""
35
36import argparse
37import pathlib
38import json
39import logging
40import os
41import re
42import shutil
43import subprocess
44import sys
45from pathlib import Path
46from typing import Any, Callable, Dict, List, NamedTuple, Optional, T, Tuple
47
48from llvm_tools import chroot, git
49
50EQUERY = 'equery'
51GSUTIL = 'gsutil.py'
52MIRROR_PATH = 'gs://chromeos-localmirror/distfiles'
53RUST_PATH = Path(
54    '/mnt/host/source/src/third_party/chromiumos-overlay/dev-lang/rust')
55
56
57def get_command_output(command: List[str], *args, **kwargs) -> str:
58  return subprocess.check_output(command, encoding='utf-8', *args,
59                                 **kwargs).strip()
60
61
62def get_command_output_unchecked(command: List[str], *args, **kwargs) -> str:
63  return subprocess.run(command,
64                        check=False,
65                        stdout=subprocess.PIPE,
66                        encoding='utf-8',
67                        *args,
68                        **kwargs).stdout.strip()
69
70
71class RustVersion(NamedTuple):
72  """NamedTuple represents a Rust version"""
73  major: int
74  minor: int
75  patch: int
76
77  def __str__(self):
78    return f'{self.major}.{self.minor}.{self.patch}'
79
80  @staticmethod
81  def parse_from_ebuild(ebuild_name: str) -> 'RustVersion':
82    input_re = re.compile(r'^rust-'
83                          r'(?P<major>\d+)\.'
84                          r'(?P<minor>\d+)\.'
85                          r'(?P<patch>\d+)'
86                          r'(:?-r\d+)?'
87                          r'\.ebuild$')
88    m = input_re.match(ebuild_name)
89    assert m, f'failed to parse {ebuild_name!r}'
90    return RustVersion(int(m.group('major')), int(m.group('minor')),
91                       int(m.group('patch')))
92
93  @staticmethod
94  def parse(x: str) -> 'RustVersion':
95    input_re = re.compile(r'^(?:rust-)?'
96                          r'(?P<major>\d+)\.'
97                          r'(?P<minor>\d+)\.'
98                          r'(?P<patch>\d+)'
99                          r'(?:.ebuild)?$')
100    m = input_re.match(x)
101    assert m, f'failed to parse {x!r}'
102    return RustVersion(int(m.group('major')), int(m.group('minor')),
103                       int(m.group('patch')))
104
105
106def compute_rustc_src_name(version: RustVersion) -> str:
107  return f'rustc-{version}-src.tar.gz'
108
109
110def compute_rust_bootstrap_prebuilt_name(version: RustVersion) -> str:
111  return f'rust-bootstrap-{version}.tbz2'
112
113
114def find_ebuild_for_package(name: str) -> os.PathLike:
115  """Returns the path to the ebuild for the named package."""
116  return get_command_output([EQUERY, 'w', name])
117
118
119def find_ebuild_path(directory: Path,
120                     name: str,
121                     version: Optional[RustVersion] = None) -> Path:
122  """Finds an ebuild in a directory.
123
124  Returns the path to the ebuild file. Asserts if there is not
125  exactly one match. The match is constrained by name and optionally
126  by version, but can match any patch level. E.g. "rust" version
127  1.3.4 can match rust-1.3.4.ebuild but also rust-1.3.4-r6.ebuild.
128  """
129  if version:
130    pattern = f'{name}-{version}*.ebuild'
131  else:
132    pattern = f'{name}-*.ebuild'
133  matches = list(Path(directory).glob(pattern))
134  assert len(matches) == 1, matches
135  return matches[0]
136
137
138def get_rust_bootstrap_version():
139  """Get the version of the current rust-bootstrap package."""
140  bootstrap_ebuild = find_ebuild_path(rust_bootstrap_path(), 'rust-bootstrap')
141  m = re.match(r'^rust-bootstrap-(\d+).(\d+).(\d+)', bootstrap_ebuild.name)
142  assert m, bootstrap_ebuild.name
143  return RustVersion(int(m.group(1)), int(m.group(2)), int(m.group(3)))
144
145
146def parse_commandline_args() -> argparse.Namespace:
147  parser = argparse.ArgumentParser(
148      description=__doc__,
149      formatter_class=argparse.RawDescriptionHelpFormatter)
150  parser.add_argument(
151      '--state_file',
152      required=True,
153      help='A state file to hold previous completed steps. If the file '
154      'exists, it needs to be used together with --continue or --restart. '
155      'If not exist (do not use --continue in this case), we will create a '
156      'file for you.',
157  )
158  parser.add_argument(
159      '--restart',
160      action='store_true',
161      help='Restart from the first step. Ignore the completed steps in '
162      'the state file',
163  )
164  parser.add_argument(
165      '--continue',
166      dest='cont',
167      action='store_true',
168      help='Continue the steps from the state file',
169  )
170
171  create_parser_template = argparse.ArgumentParser(add_help=False)
172  create_parser_template.add_argument(
173      '--template',
174      type=RustVersion.parse,
175      default=None,
176      help='A template to use for creating a Rust uprev from, in the form '
177      'a.b.c The ebuild has to exist in the chroot. If not specified, the '
178      'tool will use the current Rust version in the chroot as template.',
179  )
180  create_parser_template.add_argument(
181      '--skip_compile',
182      action='store_true',
183      help='Skip compiling rust to test the tool. Only for testing',
184  )
185
186  subparsers = parser.add_subparsers(dest='subparser_name')
187  subparser_names = []
188  subparser_names.append('create')
189  create_parser = subparsers.add_parser(
190      'create',
191      parents=[create_parser_template],
192      help='Create changes uprevs Rust to a new version',
193  )
194  create_parser.add_argument(
195      '--rust_version',
196      type=RustVersion.parse,
197      required=True,
198      help='Rust version to uprev to, in the form a.b.c',
199  )
200
201  subparser_names.append('remove')
202  remove_parser = subparsers.add_parser(
203      'remove',
204      help='Clean up old Rust version from chroot',
205  )
206  remove_parser.add_argument(
207      '--rust_version',
208      type=RustVersion.parse,
209      default=None,
210      help='Rust version to remove, in the form a.b.c If not '
211      'specified, the tool will remove the oldest version in the chroot',
212  )
213
214  subparser_names.append('remove-bootstrap')
215  remove_bootstrap_parser = subparsers.add_parser(
216      'remove-bootstrap',
217      help='Remove an old rust-bootstrap version',
218  )
219  remove_bootstrap_parser.add_argument(
220      '--version',
221      type=RustVersion.parse,
222      required=True,
223      help='rust-bootstrap version to remove',
224  )
225
226  subparser_names.append('roll')
227  roll_parser = subparsers.add_parser(
228      'roll',
229      parents=[create_parser_template],
230      help='A command can create and upload a Rust uprev CL, including '
231      'preparing the repo, creating new Rust uprev, deleting old uprev, '
232      'and upload a CL to crrev.',
233  )
234  roll_parser.add_argument(
235      '--uprev',
236      type=RustVersion.parse,
237      required=True,
238      help='Rust version to uprev to, in the form a.b.c',
239  )
240  roll_parser.add_argument(
241      '--remove',
242      type=RustVersion.parse,
243      default=None,
244      help='Rust version to remove, in the form a.b.c If not '
245      'specified, the tool will remove the oldest version in the chroot',
246  )
247  roll_parser.add_argument(
248      '--skip_cross_compiler',
249      action='store_true',
250      help='Skip updating cross-compiler in the chroot',
251  )
252  roll_parser.add_argument(
253      '--no_upload',
254      action='store_true',
255      help='If specified, the tool will not upload the CL for review',
256  )
257
258  args = parser.parse_args()
259  if args.subparser_name not in subparser_names:
260    parser.error('one of %s must be specified' % subparser_names)
261
262  if args.cont and args.restart:
263    parser.error('Please select either --continue or --restart')
264
265  if os.path.exists(args.state_file):
266    if not args.cont and not args.restart:
267      parser.error('State file exists, so you should either --continue '
268                   'or --restart')
269  if args.cont and not os.path.exists(args.state_file):
270    parser.error('Indicate --continue but the state file does not exist')
271
272  if args.restart and os.path.exists(args.state_file):
273    os.remove(args.state_file)
274
275  return args
276
277
278def prepare_uprev(rust_version: RustVersion, template: Optional[RustVersion]
279                  ) -> Optional[Tuple[RustVersion, str, RustVersion]]:
280  if template is None:
281    ebuild_path = find_ebuild_for_package('rust')
282    ebuild_name = os.path.basename(ebuild_path)
283    template_version = RustVersion.parse_from_ebuild(ebuild_name)
284  else:
285    ebuild_path = find_ebuild_for_rust_version(template)
286    template_version = template
287
288  bootstrap_version = get_rust_bootstrap_version()
289
290  if rust_version <= template_version:
291    logging.info(
292        'Requested version %s is not newer than the template version %s.',
293        rust_version, template_version)
294    return None
295
296  logging.info('Template Rust version is %s (ebuild: %r)', template_version,
297               ebuild_path)
298  logging.info('rust-bootstrap version is %s', bootstrap_version)
299
300  return template_version, ebuild_path, bootstrap_version
301
302
303def copy_patches(directory: Path, template_version: RustVersion,
304                 new_version: RustVersion) -> None:
305  patch_path = directory.joinpath('files')
306  prefix = '%s-%s-' % (directory.name, template_version)
307  new_prefix = '%s-%s-' % (directory.name, new_version)
308  for f in os.listdir(patch_path):
309    if not f.startswith(prefix):
310      continue
311    logging.info('Copy patch %s to new version', f)
312    new_name = f.replace(str(template_version), str(new_version))
313    shutil.copyfile(
314        os.path.join(patch_path, f),
315        os.path.join(patch_path, new_name),
316    )
317
318  subprocess.check_call(['git', 'add', f'{new_prefix}*.patch'], cwd=patch_path)
319
320
321def create_ebuild(template_ebuild: str, new_version: RustVersion) -> str:
322  shutil.copyfile(template_ebuild,
323                  RUST_PATH.joinpath(f'rust-{new_version}.ebuild'))
324  subprocess.check_call(['git', 'add', f'rust-{new_version}.ebuild'],
325                        cwd=RUST_PATH)
326  return os.path.join(RUST_PATH, f'rust-{new_version}.ebuild')
327
328
329def update_bootstrap_ebuild(new_bootstrap_version: RustVersion) -> None:
330  old_ebuild = find_ebuild_path(rust_bootstrap_path(), 'rust-bootstrap')
331  m = re.match(r'^rust-bootstrap-(\d+).(\d+).(\d+)', old_ebuild.name)
332  assert m, old_ebuild.name
333  old_version = RustVersion(m.group(1), m.group(2), m.group(3))
334  new_ebuild = old_ebuild.parent.joinpath(
335      f'rust-bootstrap-{new_bootstrap_version}.ebuild')
336  old_text = old_ebuild.read_text(encoding='utf-8')
337  new_text, changes = re.subn(r'(RUSTC_RAW_FULL_BOOTSTRAP_SEQUENCE=\([^)]*)',
338                              f'\\1\t{old_version}\n',
339                              old_text,
340                              flags=re.MULTILINE)
341  assert changes == 1, 'Failed to update RUSTC_RAW_FULL_BOOTSTRAP_SEQUENCE'
342  new_ebuild.write_text(new_text, encoding='utf-8')
343
344
345def update_ebuild(ebuild_file: str,
346                  new_bootstrap_version: RustVersion) -> None:
347  contents = open(ebuild_file, encoding='utf-8').read()
348  contents, subs = re.subn(r'^BOOTSTRAP_VERSION=.*$',
349                           'BOOTSTRAP_VERSION="%s"' %
350                           (new_bootstrap_version, ),
351                           contents,
352                           flags=re.MULTILINE)
353  if not subs:
354    raise RuntimeError('BOOTSTRAP_VERSION not found in rust ebuild')
355  open(ebuild_file, 'w', encoding='utf-8').write(contents)
356  logging.info('Rust ebuild file has BOOTSTRAP_VERSION updated to %s',
357               new_bootstrap_version)
358
359
360def flip_mirror_in_ebuild(ebuild_file: Path, add: bool) -> None:
361  restrict_re = re.compile(
362      r'(?P<before>RESTRICT=")(?P<values>"[^"]*"|.*)(?P<after>")')
363  with open(ebuild_file, encoding='utf-8') as f:
364    contents = f.read()
365  m = restrict_re.search(contents)
366  assert m, 'failed to find RESTRICT variable in Rust ebuild'
367  values = m.group('values')
368  if add:
369    if 'mirror' in values:
370      return
371    values += ' mirror'
372  else:
373    if 'mirror' not in values:
374      return
375    values = values.replace(' mirror', '')
376  new_contents = restrict_re.sub(r'\g<before>%s\g<after>' % values, contents)
377  with open(ebuild_file, 'w', encoding='utf-8') as f:
378    f.write(new_contents)
379
380
381def ebuild_actions(package: str, actions: List[str],
382                   sudo: bool = False) -> None:
383  ebuild_path_inchroot = find_ebuild_for_package(package)
384  cmd = ['ebuild', ebuild_path_inchroot] + actions
385  if sudo:
386    cmd = ['sudo'] + cmd
387  subprocess.check_call(cmd)
388
389
390def fetch_distfile_from_mirror(name: str) -> None:
391  """Gets the named file from the local mirror.
392
393  This ensures that the file exists on the mirror, and
394  that we can read it. We overwrite any existing distfile
395  to ensure the checksums that update_manifest() records
396  match the file as it exists on the mirror.
397
398  This function also attempts to verify the ACL for
399  the file (which is expected to have READER permission
400  for allUsers). We can only see the ACL if the user
401  gsutil runs with is the owner of the file. If not,
402  we get an access denied error. We also count this
403  as a success, because it means we were able to fetch
404  the file even though we don't own it.
405  """
406  mirror_file = MIRROR_PATH + '/' + name
407  local_file = Path(get_distdir(), name)
408  cmd = [GSUTIL, 'cp', mirror_file, local_file]
409  logging.info('Running %r', cmd)
410  rc = subprocess.call(cmd)
411  if rc != 0:
412    logging.error(
413        """Could not fetch %s
414
415If the file does not yet exist at %s
416please download the file, verify its integrity
417with something like:
418
419curl -O https://static.rust-lang.org/dist/%s
420gpg --verify %s.asc
421
422You may need to import the signing key first, e.g.:
423
424gpg --recv-keys 85AB96E6FA1BE5FE
425
426Once you have verify the integrity of the file, upload
427it to the local mirror using gsutil cp.
428""", mirror_file, MIRROR_PATH, name, name)
429    raise Exception(f'Could not fetch {mirror_file}')
430  # Check that the ACL allows allUsers READER access.
431  # If we get an AccessDeniedAcception here, that also
432  # counts as a success, because we were able to fetch
433  # the file as a non-owner.
434  cmd = [GSUTIL, 'acl', 'get', mirror_file]
435  logging.info('Running %r', cmd)
436  output = get_command_output_unchecked(cmd, stderr=subprocess.STDOUT)
437  acl_verified = False
438  if 'AccessDeniedException:' in output:
439    acl_verified = True
440  else:
441    acl = json.loads(output)
442    for x in acl:
443      if x['entity'] == 'allUsers' and x['role'] == 'READER':
444        acl_verified = True
445        break
446  if not acl_verified:
447    logging.error('Output from acl get:\n%s', output)
448    raise Exception('Could not verify that allUsers has READER permission')
449
450
451def fetch_bootstrap_distfiles(old_version: RustVersion,
452                              new_version: RustVersion) -> None:
453  """Fetches rust-bootstrap distfiles from the local mirror
454
455  Fetches the distfiles for a rust-bootstrap ebuild to ensure they
456  are available on the mirror and the local copies are the same as
457  the ones on the mirror.
458  """
459  fetch_distfile_from_mirror(compute_rust_bootstrap_prebuilt_name(old_version))
460  fetch_distfile_from_mirror(compute_rustc_src_name(new_version))
461
462
463def fetch_rust_distfiles(version: RustVersion) -> None:
464  """Fetches rust distfiles from the local mirror
465
466  Fetches the distfiles for a rust ebuild to ensure they
467  are available on the mirror and the local copies are
468  the same as the ones on the mirror.
469  """
470  fetch_distfile_from_mirror(compute_rustc_src_name(version))
471
472
473def get_distdir() -> os.PathLike:
474  """Returns portage's distdir."""
475  return get_command_output(['portageq', 'distdir'])
476
477
478def update_manifest(ebuild_file: os.PathLike) -> None:
479  """Updates the MANIFEST for the ebuild at the given path."""
480  ebuild = Path(ebuild_file)
481  logging.info('Added "mirror" to RESTRICT to %s', ebuild.name)
482  flip_mirror_in_ebuild(ebuild, add=True)
483  ebuild_actions(ebuild.parent.name, ['manifest'])
484  logging.info('Removed "mirror" to RESTRICT from %s', ebuild.name)
485  flip_mirror_in_ebuild(ebuild, add=False)
486
487
488def update_rust_packages(rust_version: RustVersion, add: bool) -> None:
489  package_file = RUST_PATH.joinpath(
490      '../../profiles/targets/chromeos/package.provided')
491  with open(package_file, encoding='utf-8') as f:
492    contents = f.read()
493  if add:
494    rust_packages_re = re.compile(r'dev-lang/rust-(\d+\.\d+\.\d+)')
495    rust_packages = rust_packages_re.findall(contents)
496    # Assume all the rust packages are in alphabetical order, so insert the new
497    # version to the place after the last rust_packages
498    new_str = f'dev-lang/rust-{rust_version}'
499    new_contents = contents.replace(rust_packages[-1],
500                                    f'{rust_packages[-1]}\n{new_str}')
501    logging.info('%s has been inserted into package.provided', new_str)
502  else:
503    old_str = f'dev-lang/rust-{rust_version}\n'
504    assert old_str in contents, f'{old_str!r} not found in package.provided'
505    new_contents = contents.replace(old_str, '')
506    logging.info('%s has been removed from package.provided', old_str)
507
508  with open(package_file, 'w', encoding='utf-8') as f:
509    f.write(new_contents)
510
511
512def update_virtual_rust(template_version: RustVersion,
513                        new_version: RustVersion) -> None:
514  template_ebuild = find_ebuild_path(RUST_PATH.joinpath('../../virtual/rust'),
515                                     'rust', template_version)
516  virtual_rust_dir = template_ebuild.parent
517  new_name = f'rust-{new_version}.ebuild'
518  new_ebuild = virtual_rust_dir.joinpath(new_name)
519  shutil.copyfile(template_ebuild, new_ebuild)
520  subprocess.check_call(['git', 'add', new_name], cwd=virtual_rust_dir)
521
522
523def perform_step(state_file: pathlib.Path,
524                 tmp_state_file: pathlib.Path,
525                 completed_steps: Dict[str, Any],
526                 step_name: str,
527                 step_fn: Callable[[], T],
528                 result_from_json: Optional[Callable[[Any], T]] = None,
529                 result_to_json: Optional[Callable[[T], Any]] = None) -> T:
530  if step_name in completed_steps:
531    logging.info('Skipping previously completed step %s', step_name)
532    if result_from_json:
533      return result_from_json(completed_steps[step_name])
534    return completed_steps[step_name]
535
536  logging.info('Running step %s', step_name)
537  val = step_fn()
538  logging.info('Step %s complete', step_name)
539  if result_to_json:
540    completed_steps[step_name] = result_to_json(val)
541  else:
542    completed_steps[step_name] = val
543
544  with tmp_state_file.open('w', encoding='utf-8') as f:
545    json.dump(completed_steps, f, indent=4)
546  tmp_state_file.rename(state_file)
547  return val
548
549
550def prepare_uprev_from_json(
551    obj: Any) -> Optional[Tuple[RustVersion, str, RustVersion]]:
552  if not obj:
553    return None
554  version, ebuild_path, bootstrap_version = obj
555  return RustVersion(*version), ebuild_path, RustVersion(*bootstrap_version)
556
557
558def create_rust_uprev(rust_version: RustVersion,
559                      maybe_template_version: Optional[RustVersion],
560                      skip_compile: bool, run_step: Callable[[], T]) -> None:
561  template_version, template_ebuild, old_bootstrap_version = run_step(
562      'prepare uprev',
563      lambda: prepare_uprev(rust_version, maybe_template_version),
564      result_from_json=prepare_uprev_from_json,
565  )
566  if template_ebuild is None:
567    return
568
569  # The fetch steps will fail (on purpose) if the files they check for
570  # are not available on the mirror. To make them pass, fetch the
571  # required files yourself, verify their checksums, then upload them
572  # to the mirror.
573  run_step(
574      'fetch bootstrap distfiles', lambda: fetch_bootstrap_distfiles(
575          old_bootstrap_version, template_version))
576  run_step('fetch rust distfiles', lambda: fetch_rust_distfiles(rust_version))
577  run_step('update bootstrap ebuild', lambda: update_bootstrap_ebuild(
578      template_version))
579  run_step(
580      'update bootstrap manifest', lambda: update_manifest(rust_bootstrap_path(
581      ).joinpath(f'rust-bootstrap-{template_version}.ebuild')))
582  run_step('copy patches', lambda: copy_patches(RUST_PATH, template_version,
583                                                rust_version))
584  ebuild_file = run_step(
585      'create ebuild', lambda: create_ebuild(template_ebuild, rust_version))
586  run_step(
587      'update ebuild', lambda: update_ebuild(ebuild_file, template_version))
588  run_step('update manifest to add new version', lambda: update_manifest(
589      Path(ebuild_file)))
590  if not skip_compile:
591    run_step(
592        'emerge rust', lambda: subprocess.check_call(
593            ['sudo', 'emerge', 'dev-lang/rust']))
594  run_step('insert version into rust packages', lambda: update_rust_packages(
595      rust_version, add=True))
596  run_step('upgrade virtual/rust', lambda: update_virtual_rust(
597      template_version, rust_version))
598
599
600def find_rust_versions_in_chroot() -> List[Tuple[RustVersion, str]]:
601  return [(RustVersion.parse_from_ebuild(x), os.path.join(RUST_PATH, x))
602          for x in os.listdir(RUST_PATH) if x.endswith('.ebuild')]
603
604
605def find_oldest_rust_version_in_chroot() -> Tuple[RustVersion, str]:
606  rust_versions = find_rust_versions_in_chroot()
607  if len(rust_versions) <= 1:
608    raise RuntimeError('Expect to find more than one Rust versions')
609  return min(rust_versions)
610
611
612def find_ebuild_for_rust_version(version: RustVersion) -> str:
613  rust_ebuilds = [
614      ebuild for x, ebuild in find_rust_versions_in_chroot() if x == version
615  ]
616  if not rust_ebuilds:
617    raise ValueError(f'No Rust ebuilds found matching {version}')
618  if len(rust_ebuilds) > 1:
619    raise ValueError(f'Multiple Rust ebuilds found matching {version}: '
620                     f'{rust_ebuilds}')
621  return rust_ebuilds[0]
622
623
624def remove_files(filename: str, path: str) -> None:
625  subprocess.check_call(['git', 'rm', filename], cwd=path)
626
627
628def remove_rust_bootstrap_version(version: RustVersion,
629                                  run_step: Callable[[], T]) -> None:
630  prefix = f'rust-bootstrap-{version}'
631  run_step('remove old bootstrap ebuild', lambda: remove_files(
632      f'{prefix}*.ebuild', rust_bootstrap_path()))
633  ebuild_file = find_ebuild_for_package('rust-bootstrap')
634  run_step('update bootstrap manifest to delete old version', lambda:
635           update_manifest(ebuild_file))
636
637
638def remove_rust_uprev(rust_version: Optional[RustVersion],
639                      run_step: Callable[[], T]) -> None:
640  def find_desired_rust_version():
641    if rust_version:
642      return rust_version, find_ebuild_for_rust_version(rust_version)
643    return find_oldest_rust_version_in_chroot()
644
645  def find_desired_rust_version_from_json(obj: Any) -> Tuple[RustVersion, str]:
646    version, ebuild_path = obj
647    return RustVersion(*version), ebuild_path
648
649  delete_version, delete_ebuild = run_step(
650      'find rust version to delete',
651      find_desired_rust_version,
652      result_from_json=find_desired_rust_version_from_json,
653  )
654  run_step(
655      'remove patches', lambda: remove_files(
656          f'files/rust-{delete_version}-*.patch', RUST_PATH))
657  run_step('remove ebuild', lambda: remove_files(delete_ebuild, RUST_PATH))
658  ebuild_file = find_ebuild_for_package('rust')
659  run_step('update manifest to delete old version', lambda: update_manifest(
660      ebuild_file))
661  run_step('remove version from rust packages', lambda: update_rust_packages(
662      delete_version, add=False))
663  run_step('remove virtual/rust', lambda: remove_virtual_rust(delete_version))
664
665
666def remove_virtual_rust(delete_version: RustVersion) -> None:
667  ebuild = find_ebuild_path(RUST_PATH.joinpath('../../virtual/rust'), 'rust',
668                            delete_version)
669  subprocess.check_call(['git', 'rm', str(ebuild.name)], cwd=ebuild.parent)
670
671
672def rust_bootstrap_path() -> Path:
673  return RUST_PATH.joinpath('../rust-bootstrap')
674
675
676def create_new_repo(rust_version: RustVersion) -> None:
677  output = get_command_output(['git', 'status', '--porcelain'], cwd=RUST_PATH)
678  if output:
679    raise RuntimeError(
680        f'{RUST_PATH} has uncommitted changes, please either discard them '
681        'or commit them.')
682  git.CreateBranch(RUST_PATH, f'rust-to-{rust_version}')
683
684
685def build_cross_compiler() -> None:
686  # Get target triples in ebuild
687  rust_ebuild = find_ebuild_for_package('rust')
688  with open(rust_ebuild, encoding='utf-8') as f:
689    contents = f.read()
690
691  target_triples_re = re.compile(r'RUSTC_TARGET_TRIPLES=\(([^)]+)\)')
692  m = target_triples_re.search(contents)
693  assert m, 'RUST_TARGET_TRIPLES not found in rust ebuild'
694  target_triples = m.group(1).strip().split('\n')
695
696  compiler_targets_to_install = [
697      target.strip() for target in target_triples if 'cros-' in target
698  ]
699  for target in target_triples:
700    if 'cros-' not in target:
701      continue
702    target = target.strip()
703
704  # We also always need arm-none-eabi, though it's not mentioned in
705  # RUSTC_TARGET_TRIPLES.
706  compiler_targets_to_install.append('arm-none-eabi')
707
708  logging.info('Emerging cross compilers %s', compiler_targets_to_install)
709  subprocess.check_call(
710      ['sudo', 'emerge', '-j', '-G'] +
711      [f'cross-{target}/gcc' for target in compiler_targets_to_install])
712
713
714def create_new_commit(rust_version: RustVersion) -> None:
715  subprocess.check_call(['git', 'add', '-A'], cwd=RUST_PATH)
716  messages = [
717      f'[DO NOT SUBMIT] dev-lang/rust: upgrade to Rust {rust_version}',
718      '',
719      'This CL is created by rust_uprev tool automatically.'
720      '',
721      'BUG=None',
722      'TEST=Use CQ to test the new Rust version',
723  ]
724  git.UploadChanges(RUST_PATH, f'rust-to-{rust_version}', messages)
725
726
727def main() -> None:
728  if not chroot.InChroot():
729    raise RuntimeError('This script must be executed inside chroot')
730
731  logging.basicConfig(level=logging.INFO)
732
733  args = parse_commandline_args()
734
735  state_file = pathlib.Path(args.state_file)
736  tmp_state_file = state_file.with_suffix('.tmp')
737
738  try:
739    with state_file.open(encoding='utf-8') as f:
740      completed_steps = json.load(f)
741  except FileNotFoundError:
742    completed_steps = {}
743
744  def run_step(
745      step_name: str,
746      step_fn: Callable[[], T],
747      result_from_json: Optional[Callable[[Any], T]] = None,
748      result_to_json: Optional[Callable[[T], Any]] = None,
749  ) -> T:
750    return perform_step(state_file, tmp_state_file, completed_steps, step_name,
751                        step_fn, result_from_json, result_to_json)
752
753  if args.subparser_name == 'create':
754    create_rust_uprev(args.rust_version, args.template, args.skip_compile,
755                      run_step)
756  elif args.subparser_name == 'remove':
757    remove_rust_uprev(args.rust_version, run_step)
758  elif args.subparser_name == 'remove-bootstrap':
759    remove_rust_bootstrap_version(args.version, run_step)
760  else:
761    # If you have added more subparser_name, please also add the handlers above
762    assert args.subparser_name == 'roll'
763    run_step('create new repo', lambda: create_new_repo(args.uprev))
764    if not args.skip_cross_compiler:
765      run_step('build cross compiler', build_cross_compiler)
766    create_rust_uprev(args.uprev, args.template, args.skip_compile, run_step)
767    remove_rust_uprev(args.remove, run_step)
768    bootstrap_version = prepare_uprev_from_json(
769        completed_steps['prepare uprev'])[2]
770    remove_rust_bootstrap_version(bootstrap_version, run_step)
771    if not args.no_upload:
772      run_step('create rust uprev CL', lambda: create_new_commit(args.uprev))
773
774
775if __name__ == '__main__':
776  sys.exit(main())
777