| # Copyright 2020 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Analyze image sizes. |
| |
| Lists the sizes of any subdirectries in a Chrome OS image that are larger than |
| a minimum size. |
| |
| The image may exist locally or be fetched from Google Storage by version number. |
| |
| Live builds found here: |
| https://cros-goldeneye.corp.google.com/chromeos/console/liveBuilds |
| |
| Example run with board and version: |
| $ cros analyze-image --board=coral --version=R86-13421.0.0 --spreadsheet |
| |
| Example run, first download then process image numbers: |
| $ cros analyze-image --board=coral --version=R86-13421.0.0 \ |
| --local-path=/tmp/cros-analyze-coral-m86.bin |
| $ cros analyze-image --image=/tmp/cros-analyze-coral-m86.bin |
| """ |
| |
| import csv |
| import logging |
| import shutil |
| import sys |
| import typing |
| |
| from chromite.cli import command |
| from chromite.lib import commandline |
| from chromite.lib import constants |
| from chromite.lib import cros_build_lib |
| from chromite.lib import dev_server_wrapper as ds_wrapper |
| from chromite.lib import image_lib |
| from chromite.lib import osutils |
| from chromite.utils import file_util |
| from chromite.utils import pformat |
| |
| |
| IMAGE_NAME = "chromiumos_base_image" |
| TEMPFILE_PREFIX = "cros_analyze_image-" |
| DEFAULT_MIN_SIZE = 1024 * 1024 * 10 |
| |
| # This list is used by the --spreadsheet option. |
| # These are the list of paths we are watching on go/cros-image-size-spreadsheet. |
| WATCHED_PATHS = [ |
| "/", |
| "/lib/firmware", |
| "/lib/modules", |
| "/opt/google/chrome/locales", |
| "/opt/google/chrome/nacl_helper", |
| "/opt/google/chrome/pepper/libpepflashplayer.so", |
| "/opt/google/chrome/pnacl", |
| "/opt/google/chrome/resources.pak", |
| "/opt/google/chrome/resources/chromeos/accessibility/chromevox", |
| "/opt/google/containers/android", |
| "/opt/google/vms/android", |
| "/usr/bin", |
| "/usr/lib", |
| "/usr/lib64", # on 32-bit systems this check will come up as -1. |
| "/usr/sbin", |
| "/usr/share/chromeos-assets/demo_app", |
| "/usr/share/chromeos-assets/genius_app", |
| "/usr/share/chromeos-assets/input_methods/input_tools", |
| "/usr/share/chromeos-assets/quickoffice", |
| "/usr/share/chromeos-assets/speech_synthesis", |
| "/usr/share/fonts", |
| ] |
| |
| |
| def sub_paths_size_iter( |
| root_path: str, |
| ) -> typing.Generator[typing.Tuple[str, int], None, None]: |
| """Calculates sizes for paths below root_path. |
| |
| Args: |
| root_path: path to start analyzing from |
| |
| Returns: |
| Generator tuple of path and int size of path. |
| """ |
| root_path_len = len(root_path) |
| |
| cmd = ["du", "--all", "--one-file-system", "-B1", root_path] |
| result = cros_build_lib.sudo_run( |
| cmd, print_cmd=False, capture_output=True, encoding="utf-8" |
| ) |
| for line in result.stdout.splitlines(): |
| if not line: |
| continue |
| |
| size, path = line.split("\t") |
| path = path[root_path_len:] |
| if not path: |
| path = "/" |
| yield path, int(size) |
| |
| |
| def get_image_sizes( |
| image_filepath: str, required_paths: list = None, min_size: int = None |
| ) -> typing.Dict[str, int]: |
| """Extracts an image to a temporary directory and calls sub_paths_size_iter. |
| |
| Args: |
| image_filepath: The filepath of the image to analyze. |
| required_paths: get sizes for only these paths |
| min_size: if required_paths not set will filter to only return |
| paths where size of path is at least min_size |
| |
| Returns: |
| A dictionary of path -> size. |
| """ |
| sizes = {} |
| with osutils.TempDir( |
| prefix=TEMPFILE_PREFIX |
| ) as temp_dir, image_lib.LoopbackPartitions( |
| image_filepath, temp_dir |
| ) as image: |
| root_path = image.Mount([constants.PART_ROOT_A])[0] |
| for path, size in sub_paths_size_iter(root_path): |
| if required_paths: |
| if path not in required_paths: |
| continue |
| elif size < min_size: |
| continue |
| |
| sizes[path] = size |
| |
| return sizes |
| |
| |
| def fetch_image(board: str, version: str, local_path: str = None) -> str: |
| """Downloads an image from Google Storage. |
| |
| Args: |
| board: The name of the board. |
| version: The version to download. |
| local_path: directory to save image to. |
| |
| Returns: |
| Local path to image file. |
| """ |
| _, image_path = ds_wrapper.GetImagePathWithXbuddy( |
| "xBuddy://remote", board, version |
| ) |
| |
| if local_path: |
| try: |
| shutil.copyfile(image_path, local_path) |
| except OSError as e: |
| cros_build_lib.Die( |
| f"Copy error '{image_path}' to '{local_path}': {e}" |
| ) |
| |
| return local_path |
| |
| return image_path |
| |
| |
| def write_sizes( |
| sizes: dict, |
| required_paths: list, |
| human_readable: bool, |
| output_format: str, |
| output_path: typing.Union[str, typing.TextIO], |
| ): |
| """Writes the sizes in CSV format. |
| |
| Args: |
| sizes: A dictionary of path -> size. |
| required_paths: list of paths to order results by |
| human_readable: set to True to output in a human-readable format. |
| output_format: output format (json or csv) |
| output_path: path to write output to |
| """ |
| |
| def size_string(sz): |
| if human_readable: |
| return pformat.size(sz) |
| return sz |
| |
| output = [] |
| |
| # If required_paths passed in, emit output in same order as passed in. |
| if required_paths: |
| for path in required_paths: |
| if path not in sizes: |
| size = -1 |
| else: |
| size = size_string(sizes[path]) |
| output.append({"path": path, "size": size}) |
| else: |
| for path, size in sorted(sizes.items()): |
| output.append({"path": path, "size": size_string(sizes[path])}) |
| |
| with file_util.Open(output_path, mode="w") as f: |
| if output_format == "csv": |
| writer = csv.DictWriter(f, ["path", "size"]) |
| writer.writeheader() |
| for row in output: |
| writer.writerow(row) |
| elif output_format == "json": |
| pformat.json(output, f) |
| |
| |
| @command.command_decorator("analyze-image") |
| class AnalyzeImageCommand(command.CliCommand): |
| """Analyze cros images listing large directory and file sizes.""" |
| |
| @classmethod |
| def ProcessOptions(cls, parser, options): |
| """Post process options.""" |
| if options.image: |
| return |
| |
| if not options.board or not options.version: |
| parser.error("--image or (--board and --version) required") |
| |
| def Run(self): |
| """Perform the command.""" |
| # Get the sudo password immediately. |
| cros_build_lib.sudo_run(["echo"]) |
| |
| image_filepath = None |
| if self.options.image: |
| logging.notice("Getting sizes for image: %s", self.options.image) |
| image_filepath = self.options.image |
| else: |
| if self.options.local_path: |
| image_filepath = self.options.local_path |
| |
| logging.notice("Getting sizes for: %s", self.options.version) |
| image_filepath = fetch_image( |
| board=self.options.board, |
| version=self.options.version, |
| local_path=image_filepath, |
| ) |
| |
| required_paths = [] |
| if self.options.spreadsheet: |
| required_paths = WATCHED_PATHS |
| |
| logging.notice( |
| "Analyzing disk usage of locally-mounted image: %s", image_filepath |
| ) |
| sizes = get_image_sizes( |
| image_filepath=image_filepath, |
| required_paths=required_paths, |
| min_size=self.options.minsize, |
| ) |
| write_sizes( |
| sizes=sizes, |
| required_paths=required_paths, |
| human_readable=self.options.human_readable, |
| output_format=self.options.format, |
| output_path=self.options.output, |
| ) |
| |
| @classmethod |
| def AddParser(cls, parser: commandline.ArgumentParser): |
| """Add parser arguments.""" |
| super(AnalyzeImageCommand, cls).AddParser(parser) |
| |
| parser.add_argument( |
| "--board", |
| default=None, |
| help="Board name used for fetching an image.", |
| ) |
| parser.add_argument( |
| "--format", |
| choices=("csv", "json"), |
| default="csv", |
| help='Choose output format (from "csv" or "json").' |
| 'Default: "%(default)s".', |
| ) |
| parser.add_argument( |
| "--human", |
| dest="human_readable", |
| default=False, |
| action="store_true", |
| help="Output human readable sizes.", |
| ) |
| parser.add_argument( |
| "--image", |
| type="path", |
| help="Specify a local image file to analyze.", |
| ) |
| parser.add_argument( |
| "--local-path", type="path", help="Local path to fetch image to." |
| ) |
| parser.add_argument( |
| "--minsize", |
| default=DEFAULT_MIN_SIZE, |
| type=int, |
| help="Minimum file or directory size (in bytes) to list.", |
| ) |
| parser.add_argument( |
| "--output", |
| default=sys.stdout, |
| help="Write output to a specified path.", |
| ) |
| parser.add_argument( |
| "--spreadsheet", |
| default=False, |
| action="store_true", |
| help="Use a preset list of paths that are specifically monitored " |
| "in the size tracking spreadsheet " |
| "(go/cros-image-size-spreadsheet).", |
| ) |
| parser.add_argument( |
| "--version", |
| help="Version string used for fetching an image, e.g. R12-3456.7.0", |
| ) |