Mike Frysinger | e58c0e2 | 2017-10-04 15:43:30 -0400 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 2 | # Copyright 2014 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 | """Script to remove unused gconv charset modules from a build.""" |
| 7 | |
Mike Frysinger | 383367e | 2014-09-16 15:06:17 -0400 | [diff] [blame] | 8 | from __future__ import print_function |
| 9 | |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 10 | import ahocorasick |
| 11 | import glob |
Alex Deymo | da9dd40 | 2014-08-13 08:54:18 -0700 | [diff] [blame] | 12 | import lddtree |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 13 | import operator |
| 14 | import os |
| 15 | import stat |
| 16 | |
| 17 | from chromite.lib import commandline |
| 18 | from chromite.lib import cros_build_lib |
Ralph Nathan | 5a582ff | 2015-03-20 18:18:30 -0700 | [diff] [blame] | 19 | from chromite.lib import cros_logging as logging |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 20 | from chromite.lib import osutils |
| 21 | |
| 22 | |
| 23 | # Path pattern to search for the gconv-modules file. |
| 24 | GCONV_MODULES_PATH = 'usr/*/gconv/gconv-modules' |
| 25 | |
| 26 | # Sticky modules. These charsets modules are always included even if they |
| 27 | # aren't used. You can specify any charset name as supported by 'iconv_open', |
| 28 | # for example, 'LATIN1' or 'ISO-8859-1'. |
| 29 | STICKY_MODULES = ('UTF-16', 'UTF-32', 'UNICODE') |
| 30 | |
| 31 | # List of function names (symbols) known to use a charset as a parameter. |
| 32 | GCONV_SYMBOLS = ( |
| 33 | # glibc |
| 34 | 'iconv_open', |
| 35 | 'iconv', |
| 36 | # glib |
| 37 | 'g_convert', |
| 38 | 'g_convert_with_fallback', |
| 39 | 'g_iconv', |
| 40 | 'g_locale_to_utf8', |
| 41 | 'g_get_charset', |
| 42 | ) |
| 43 | |
| 44 | |
| 45 | class GconvModules(object): |
| 46 | """Class to manipulate the gconv/gconv-modules file and referenced modules. |
| 47 | |
| 48 | This class parses the contents of the gconv-modules file installed by glibc |
| 49 | which provides the definition of the charsets supported by iconv_open(3). It |
| 50 | allows to load the current gconv-modules file and rewrite it to include only |
| 51 | a subset of the supported modules, removing the other modules. |
| 52 | |
| 53 | Each charset is involved on some transformation between that charset and an |
| 54 | internal representation. This transformation is defined on a .so file loaded |
| 55 | dynamically with dlopen(3) when the charset defined in this file is requested |
| 56 | to iconv_open(3). |
| 57 | |
| 58 | See the comments on gconv-modules file for syntax details. |
| 59 | """ |
| 60 | |
Mike Frysinger | 22f6c5a | 2014-08-18 00:45:54 -0400 | [diff] [blame] | 61 | def __init__(self, gconv_modules_file): |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 62 | """Initialize the class. |
| 63 | |
| 64 | Args: |
Mike Frysinger | 22f6c5a | 2014-08-18 00:45:54 -0400 | [diff] [blame] | 65 | gconv_modules_file: Path to gconv/gconv-modules file. |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 66 | """ |
Mike Frysinger | 22f6c5a | 2014-08-18 00:45:54 -0400 | [diff] [blame] | 67 | self._filename = gconv_modules_file |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 68 | |
| 69 | # An alias map of charsets. The key (fromcharset) is the alias name and |
| 70 | # the value (tocharset) is the real charset name. We also support a value |
| 71 | # that is an alias for another charset. |
| 72 | self._alias = {} |
| 73 | |
| 74 | # The modules dict goes from charset to module names (the filenames without |
| 75 | # the .so extension). Since several transformations involving the same |
| 76 | # charset could be defined in different files, the values of this dict are |
| 77 | # a set of module names. |
| 78 | self._modules = {} |
| 79 | |
| 80 | def Load(self): |
| 81 | """Load the charsets from gconv-modules.""" |
| 82 | for line in open(self._filename): |
| 83 | line = line.split('#', 1)[0].strip() |
| 84 | if not line: # Comment |
| 85 | continue |
| 86 | |
| 87 | lst = line.split() |
| 88 | if lst[0] == 'module': |
| 89 | _, fromset, toset, filename = lst[:4] |
| 90 | for charset in (fromset, toset): |
| 91 | charset = charset.rstrip('/') |
| 92 | mods = self._modules.get(charset, set()) |
| 93 | mods.add(filename) |
| 94 | self._modules[charset] = mods |
| 95 | elif lst[0] == 'alias': |
| 96 | _, fromset, toset = lst |
| 97 | fromset = fromset.rstrip('/') |
| 98 | toset = toset.rstrip('/') |
| 99 | # Warn if the same charset is defined as two different aliases. |
| 100 | if self._alias.get(fromset, toset) != toset: |
Ralph Nathan | 5990042 | 2015-03-24 10:41:17 -0700 | [diff] [blame] | 101 | logging.error('charset "%s" already defined as "%s".', fromset, |
| 102 | self._alias[fromset]) |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 103 | self._alias[fromset] = toset |
| 104 | else: |
| 105 | cros_build_lib.Die('Unknown line: %s', line) |
| 106 | |
Ralph Nathan | 5a582ff | 2015-03-20 18:18:30 -0700 | [diff] [blame] | 107 | logging.debug('Found %d modules and %d alias in %s', len(self._modules), |
| 108 | len(self._alias), self._filename) |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 109 | charsets = sorted(self._alias.keys() + self._modules.keys()) |
| 110 | # Remove the 'INTERNAL' charset from the list, since it is not a charset |
| 111 | # but an internal representation used to convert to and from other charsets. |
| 112 | if 'INTERNAL' in charsets: |
| 113 | charsets.remove('INTERNAL') |
| 114 | return charsets |
| 115 | |
| 116 | def Rewrite(self, used_charsets, dry_run=False): |
| 117 | """Rewrite gconv-modules file with only the used charsets. |
| 118 | |
| 119 | Args: |
| 120 | used_charsets: A list of used charsets. This should be a subset of the |
| 121 | list returned by Load(). |
| 122 | dry_run: Whether this function should not change any file. |
| 123 | """ |
| 124 | |
| 125 | # Compute the used modules. |
| 126 | used_modules = set() |
| 127 | for charset in used_charsets: |
| 128 | while charset in self._alias: |
| 129 | charset = self._alias[charset] |
| 130 | used_modules.update(self._modules[charset]) |
| 131 | unused_modules = reduce(set.union, self._modules.values()) - used_modules |
| 132 | |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 133 | modules_dir = os.path.dirname(self._filename) |
Alex Deymo | da9dd40 | 2014-08-13 08:54:18 -0700 | [diff] [blame] | 134 | |
| 135 | all_modules = set.union(used_modules, unused_modules) |
| 136 | # The list of charsets that depend on a given library. For example, |
| 137 | # libdeps['libCNS.so'] is the set of all the modules that require that |
| 138 | # library. These libraries live in the same directory as the modules. |
| 139 | libdeps = {} |
| 140 | for module in all_modules: |
| 141 | deps = lddtree.ParseELF(os.path.join(modules_dir, '%s.so' % module), |
| 142 | modules_dir, []) |
Mike Frysinger | 266e4ff | 2018-07-14 00:41:05 -0400 | [diff] [blame] | 143 | if 'needed' not in deps: |
Alex Deymo | da9dd40 | 2014-08-13 08:54:18 -0700 | [diff] [blame] | 144 | continue |
| 145 | for lib in deps['needed']: |
| 146 | # Ignore the libs without a path defined (outside the modules_dir). |
| 147 | if deps['libs'][lib]['path']: |
| 148 | libdeps[lib] = libdeps.get(lib, set()).union([module]) |
| 149 | |
| 150 | used_libdeps = set(lib for lib, deps in libdeps.iteritems() |
| 151 | if deps.intersection(used_modules)) |
| 152 | unused_libdeps = set(libdeps).difference(used_libdeps) |
| 153 | |
Ralph Nathan | 5a582ff | 2015-03-20 18:18:30 -0700 | [diff] [blame] | 154 | logging.debug('Used modules: %s', ', '.join(sorted(used_modules))) |
| 155 | logging.debug('Used dependency libs: %s, '.join(sorted(used_libdeps))) |
Alex Deymo | da9dd40 | 2014-08-13 08:54:18 -0700 | [diff] [blame] | 156 | |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 157 | unused_size = 0 |
| 158 | for module in sorted(unused_modules): |
| 159 | module_path = os.path.join(modules_dir, '%s.so' % module) |
| 160 | unused_size += os.lstat(module_path).st_size |
Ralph Nathan | 5a582ff | 2015-03-20 18:18:30 -0700 | [diff] [blame] | 161 | logging.debug('rm %s', module_path) |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 162 | if not dry_run: |
| 163 | os.unlink(module_path) |
Alex Deymo | da9dd40 | 2014-08-13 08:54:18 -0700 | [diff] [blame] | 164 | |
| 165 | unused_libdeps_size = 0 |
| 166 | for lib in sorted(unused_libdeps): |
| 167 | lib_path = os.path.join(modules_dir, lib) |
| 168 | unused_libdeps_size += os.lstat(lib_path).st_size |
Ralph Nathan | 5a582ff | 2015-03-20 18:18:30 -0700 | [diff] [blame] | 169 | logging.debug('rm %s', lib_path) |
Alex Deymo | da9dd40 | 2014-08-13 08:54:18 -0700 | [diff] [blame] | 170 | if not dry_run: |
| 171 | os.unlink(lib_path) |
| 172 | |
Ralph Nathan | 0304728 | 2015-03-23 11:09:32 -0700 | [diff] [blame] | 173 | logging.info('Done. Using %d gconv modules. Removed %d unused modules' |
| 174 | ' (%.1f KiB) and %d unused dependencies (%.1f KiB)', |
| 175 | len(used_modules), len(unused_modules), unused_size / 1024., |
| 176 | len(unused_libdeps), unused_libdeps_size / 1024.) |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 177 | |
| 178 | # Recompute the gconv-modules file with only the included gconv modules. |
| 179 | result = [] |
| 180 | for line in open(self._filename): |
| 181 | lst = line.split('#', 1)[0].strip().split() |
| 182 | |
| 183 | if not lst: |
| 184 | result.append(line) # Keep comments and copyright headers. |
| 185 | elif lst[0] == 'module': |
| 186 | _, _, _, filename = lst[:4] |
| 187 | if filename in used_modules: |
| 188 | result.append(line) # Used module |
| 189 | elif lst[0] == 'alias': |
| 190 | _, charset, _ = lst |
| 191 | charset = charset.rstrip('/') |
| 192 | while charset in self._alias: |
| 193 | charset = self._alias[charset] |
| 194 | if used_modules.intersection(self._modules[charset]): |
| 195 | result.append(line) # Alias to an used module |
| 196 | else: |
| 197 | cros_build_lib.Die('Unknown line: %s', line) |
| 198 | |
| 199 | if not dry_run: |
| 200 | osutils.WriteFile(self._filename, ''.join(result)) |
| 201 | |
| 202 | |
| 203 | def MultipleStringMatch(patterns, corpus): |
| 204 | """Search a list of strings in a corpus string. |
| 205 | |
| 206 | Args: |
| 207 | patterns: A list of strings. |
| 208 | corpus: The text where to search for the strings. |
| 209 | |
Mike Frysinger | c6a67da | 2016-09-21 00:47:20 -0400 | [diff] [blame] | 210 | Returns: |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 211 | A list of Booleans stating whether each pattern string was found in the |
| 212 | corpus or not. |
| 213 | """ |
| 214 | tree = ahocorasick.KeywordTree() |
| 215 | for word in patterns: |
| 216 | tree.add(word) |
| 217 | tree.make() |
| 218 | |
| 219 | result = [False] * len(patterns) |
| 220 | for i, j in tree.findall(corpus): |
| 221 | match = corpus[i:j] |
| 222 | result[patterns.index(match)] = True |
| 223 | |
| 224 | return result |
| 225 | |
| 226 | |
| 227 | def GconvStrip(opts): |
| 228 | """Process gconv-modules and remove unused modules. |
| 229 | |
| 230 | Args: |
| 231 | opts: The command-line args passed to the script. |
| 232 | |
| 233 | Returns: |
| 234 | The exit code number indicating whether the process succeeded. |
| 235 | """ |
| 236 | root_st = os.lstat(opts.root) |
| 237 | if not stat.S_ISDIR(root_st.st_mode): |
| 238 | cros_build_lib.Die('root (%s) must be a directory.' % opts.root) |
| 239 | |
| 240 | # Detect the possible locations of the gconv-modules file. |
| 241 | gconv_modules_files = glob.glob(os.path.join(opts.root, GCONV_MODULES_PATH)) |
| 242 | |
| 243 | if not gconv_modules_files: |
Ralph Nathan | 446aee9 | 2015-03-23 14:44:56 -0700 | [diff] [blame] | 244 | logging.warning('gconv-modules file not found.') |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 245 | return 1 |
| 246 | |
| 247 | # Only one gconv-modules files should be present, either on /usr/lib or |
| 248 | # /usr/lib64, but not both. |
| 249 | if len(gconv_modules_files) > 1: |
| 250 | cros_build_lib.Die('Found several gconv-modules files.') |
| 251 | |
Mike Frysinger | 22f6c5a | 2014-08-18 00:45:54 -0400 | [diff] [blame] | 252 | gconv_modules_file = gconv_modules_files[0] |
Ralph Nathan | 0304728 | 2015-03-23 11:09:32 -0700 | [diff] [blame] | 253 | logging.info('Searching for unused gconv files defined in %s', |
| 254 | gconv_modules_file) |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 255 | |
Mike Frysinger | 22f6c5a | 2014-08-18 00:45:54 -0400 | [diff] [blame] | 256 | gmods = GconvModules(gconv_modules_file) |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 257 | charsets = gmods.Load() |
| 258 | |
| 259 | # Use scanelf to search for all the binary files on the rootfs that require |
| 260 | # or define the symbol iconv_open. We also include the binaries that define |
| 261 | # it since there could be internal calls to it from other functions. |
| 262 | files = set() |
| 263 | for symbol in GCONV_SYMBOLS: |
| 264 | cmd = ['scanelf', '--mount', '--quiet', '--recursive', '--format', '#s%F', |
| 265 | '--symbol', symbol, opts.root] |
| 266 | result = cros_build_lib.RunCommand(cmd, redirect_stdout=True, |
| 267 | print_cmd=False) |
| 268 | symbol_files = result.output.splitlines() |
Ralph Nathan | 5a582ff | 2015-03-20 18:18:30 -0700 | [diff] [blame] | 269 | logging.debug('Symbol %s found on %d files.', symbol, len(symbol_files)) |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 270 | files.update(symbol_files) |
| 271 | |
| 272 | # The charsets are represented as nul-terminated strings in the binary files, |
| 273 | # so we append the '\0' to each string. This prevents some false positives |
| 274 | # when the name of the charset is a substring of some other string. It doesn't |
| 275 | # prevent false positives when the charset name is the suffix of another |
| 276 | # string, for example a binary with the string "DON'T DO IT\0" will match the |
| 277 | # 'IT' charset. Empirical test on ChromeOS images suggests that only 4 |
| 278 | # charsets could fall in category. |
| 279 | strings = [s + '\0' for s in charsets] |
Ralph Nathan | 0304728 | 2015-03-23 11:09:32 -0700 | [diff] [blame] | 280 | logging.info('Will search for %d strings in %d files', len(strings), |
| 281 | len(files)) |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 282 | |
| 283 | # Charsets listed in STICKY_MOUDLES are initialized as used. Note that those |
| 284 | # strings should be listed in the gconv-modules file. |
| 285 | unknown_sticky_modules = set(STICKY_MODULES) - set(charsets) |
| 286 | if unknown_sticky_modules: |
Ralph Nathan | 446aee9 | 2015-03-23 14:44:56 -0700 | [diff] [blame] | 287 | logging.warning( |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 288 | 'The following charsets were explicitly requested in STICKY_MODULES ' |
| 289 | 'even though they don\'t exist: %s', |
| 290 | ', '.join(unknown_sticky_modules)) |
| 291 | global_used = [charset in STICKY_MODULES for charset in charsets] |
| 292 | |
Mike Frysinger | 22f6c5a | 2014-08-18 00:45:54 -0400 | [diff] [blame] | 293 | for filename in files: |
Mike Frysinger | 8960f7c | 2018-07-14 00:52:26 -0400 | [diff] [blame] | 294 | used_filenames = MultipleStringMatch(strings, |
| 295 | osutils.ReadFile(filename, mode='rb')) |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 296 | |
Mike Frysinger | 8960f7c | 2018-07-14 00:52:26 -0400 | [diff] [blame] | 297 | global_used = map(operator.or_, global_used, used_filenames) |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 298 | # Check the debug flag to avoid running an useless loop. |
Mike Frysinger | 8960f7c | 2018-07-14 00:52:26 -0400 | [diff] [blame] | 299 | if opts.debug and any(used_filenames): |
Ralph Nathan | 5a582ff | 2015-03-20 18:18:30 -0700 | [diff] [blame] | 300 | logging.debug('File %s:', filename) |
Mike Frysinger | 8960f7c | 2018-07-14 00:52:26 -0400 | [diff] [blame] | 301 | for i, used_filename in enumerate(used_filenames): |
| 302 | if used_filename: |
Ralph Nathan | 5a582ff | 2015-03-20 18:18:30 -0700 | [diff] [blame] | 303 | logging.debug(' - %s', strings[i]) |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 304 | |
| 305 | used_charsets = [cs for cs, used in zip(charsets, global_used) if used] |
| 306 | gmods.Rewrite(used_charsets, opts.dry_run) |
| 307 | return 0 |
| 308 | |
| 309 | |
| 310 | def ParseArgs(argv): |
| 311 | """Return parsed commandline arguments.""" |
| 312 | |
| 313 | parser = commandline.ArgumentParser() |
| 314 | parser.add_argument( |
| 315 | '--dry-run', action='store_true', default=False, |
| 316 | help='process but don\'t modify any file.') |
| 317 | parser.add_argument( |
| 318 | 'root', type='path', |
| 319 | help='path to the directory where the rootfs is mounted.') |
| 320 | |
| 321 | opts = parser.parse_args(argv) |
| 322 | opts.Freeze() |
| 323 | return opts |
| 324 | |
| 325 | |
| 326 | def main(argv): |
| 327 | """Main function to start the script.""" |
| 328 | opts = ParseArgs(argv) |
Ralph Nathan | 5a582ff | 2015-03-20 18:18:30 -0700 | [diff] [blame] | 329 | logging.debug('Options are %s', opts) |
Alex Deymo | 2bba381 | 2014-08-13 08:49:09 -0700 | [diff] [blame] | 330 | |
| 331 | return GconvStrip(opts) |