blob: 9c72d70f202bdeab48d0f0ce130b701428f87b6e [file] [log] [blame]
Allen Webb65ed16f2020-08-06 21:25:55 -05001#!/usr/bin/env python3
2# Copyright 2020 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Find ebuilds for rust crates that have been replaced by newer versions.
7
8Aids the process of removing unused rust ebuilds that have been replaced
9by newer versions:
10
11 1) Get the list of dev-rust ebuilds.
12 2) Exclude the newest version of each ebuild.
13 3) Generate a list of ebuilds that are installed for typical configurations.
14 4) List the dev-rust ebuilds that aren't included.
15
16For example:
17 ./cleanup_crates.py -c --log-level=debug
18"""
19
20import distutils.version # pylint: disable=no-name-in-module,import-error
21import logging
22import os
23import pickle
24import sys
25
26from chromite.lib import build_target_lib
27from chromite.lib import commandline
28from chromite.lib import constants
29from chromite.lib import cros_build_lib
30from chromite.lib import osutils
31from chromite.lib import portage_util
32from chromite.service import sysroot
33
34# The path of the cache.
35DEFAULT_CACHE_PATH = os.path.join(osutils.GetGlobalTempDir(),
36 'cleanup_crates.py')
37
38# build targets to include for the host.
39HOST_CONFIGURATIONS = {
40 'virtual/target-sdk',
41 'virtual/target-sdk-post-cross',
42}
43# build targets to include for each board.
44BOARD_CONFIGURATIONS = {
45 'virtual/target-os',
46 'virtual/target-os-dev',
47 'virtual/target-os-test',
48}
49
50# The set of boards to check. This only needs to be a representative set.
51BOARDS = {'eve', 'lakitu', 'tatl'} | (
52 set() if not os.path.isdir(os.path.join(constants.SOURCE_ROOT, 'src',
53 'private-overlays')) else
54 {'lasilla-ground', 'mistral'}
55)
56
57_GEN_CONFIG = lambda boards, configs: [(b, c) for b in boards for c in configs]
58# A tuple of (board, ebuild) pairs used to generate the set of installed
59# packages.
60CONFIGURATIONS = (
61 _GEN_CONFIG((None,), HOST_CONFIGURATIONS) +
62 _GEN_CONFIG(BOARDS, BOARD_CONFIGURATIONS)
63)
64
65
66def main(argv):
67 """List ebuilds for rust crates replaced by newer versions."""
68 opts = get_opts(argv)
69
70 cln = CachedPackageLists(use_cache=opts.cache,
71 clear_cache=opts.clear_cache,
72 cache_dir=opts.cache_dir)
73
74 ebuilds = exclude_latest_version(get_dev_rust_ebuilds())
75 used = cln.get_used_packages(CONFIGURATIONS)
76
77 unused_ebuilds = sorted(x.cpv for x in ebuilds if x.cpv not in used)
78 print('\n'.join(unused_ebuilds))
79 return 0
80
81
82def get_opts(argv):
83 """Parse the command-line options."""
84 parser = commandline.ArgumentParser(description=__doc__)
85 parser.add_argument(
86 '-c', '--cache', action='store_true',
87 help='Enables caching of the results of GetPackageDependencies.')
88 parser.add_argument(
89 '-x', '--clear-cache', action='store_true',
90 help='Clears the contents of the cache before executing.')
91 parser.add_argument(
92 '-C', '--cache-dir', action='store', default=DEFAULT_CACHE_PATH,
93 type='path',
94 help='The path to store the cache (default: %(default)s)')
95 opts = parser.parse_args(argv)
96 opts.Freeze()
97 return opts
98
99
100def get_dev_rust_ebuilds():
101 """Return a list of dev-rust ebuilds."""
102 return portage_util.FindPackageNameMatches('dev-rust/*')
103
104
105def exclude_latest_version(packages):
106 """Return a list of ebuilds that aren't the latest version."""
107 latest = {}
108 results = []
109 lv = distutils.version.LooseVersion
110 for pkg in packages:
111 name = pkg.cp
112 if name not in latest:
113 latest[name] = pkg
114 continue
115
116 version = lv(pkg.version_no_rev)
117 other_version = lv(latest[name].version_no_rev)
118 if version > other_version:
119 results.append(latest[name])
120 latest[name] = pkg
121 elif version != other_version:
122 results.append(pkg)
123 return results
124
125
126def _get_package_dependencies(board, package):
127 """List the ebuild-version dependencies for a specific board & package."""
128 if board and not os.path.isdir(cros_build_lib.GetSysroot(board)):
129 config = sysroot.SetupBoardRunConfig(
130 usepkg=True,
131 jobs=os.cpu_count(),
132 regen_configs=True,
133 update_toolchain=False,
134 upgrade_chroot=False,
135 init_board_pkgs=True,
136 local_build=False)
137 sysroot.SetupBoard(build_target_lib.BuildTarget(board),
138 run_configs=config)
139 return portage_util.GetPackageDependencies(board, package)
140
141
142class CachedPackageLists:
143 """Lists used packages with the specified cache configuration."""
144
145 def __init__(self, use_cache=False, clear_cache=False,
146 cache_dir=DEFAULT_CACHE_PATH):
147 """Initialize the cache if it is enabled."""
148 self.use_cache = bool(use_cache)
149 self.clear_cache = bool(clear_cache)
150 self.cache_dir = cache_dir
151 if self.clear_cache:
152 osutils.RmDir(self.cache_dir, ignore_missing=True)
153 if self.use_cache:
154 osutils.SafeMakedirs(self.cache_dir)
155
156 def _try_cache(self, name, fn):
157 """Caches the return value of a function."""
158 if not self.use_cache:
159 return fn()
160
161 try:
162 with open(os.path.join(self.cache_dir, name), 'rb') as fp:
163 logging.info('cache hit: %s', name)
164 return pickle.load(fp)
165 except FileNotFoundError:
166 pass
167
168 logging.info('cache miss: %s', name)
169 result = fn()
170
171 with open(os.path.join(self.cache_dir, name), 'wb+') as fp:
172 pickle.dump(result, fp)
173
174 return result
175
176 def get_used_packages(self, configurations):
177 """Return the packages installed in the specified configurations."""
178
179 def get_deps(board, package):
180 filename_package = package.replace('/', ':')
181 return self._try_cache(
182 f'deps:{board}:{filename_package}',
183 lambda: _get_package_dependencies(board, package))
184
185 used = set()
186 for board, package in configurations:
187 deps = get_deps(board, package)
188 if deps:
189 used.update(deps)
190 else:
191 logging.warning('No depts for (%s, %s)', board, package)
192 return used
193
194
195if __name__ == '__main__':
196 sys.exit(main(sys.argv[1:]))