#!/usr/bin/env python3
# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Prints out du-style information about the files that will be deployed
for a given target
"""
import argparse
import os
import re
import sys
from typing import Iterable
from enum import Enum
def format_size(bytesize: float) -> str:
"""Convert bytes to human readable format.
Args:
bytesize: Number to humanize
Returns:
Size as string in human-readable format (e.g. 1.8MiB)
"""
if bytesize < 1024:
return f'{bytesize}B'
for suffix in 'BKMGTPEZY':
if bytesize < 1024:
break
bytesize /= 1024
return f'{bytesize:.1f}{suffix}iB' # pylint: disable=undefined-loop-variable
class FilesystemNode:
def __init__(self, path: str) -> None:
self.path = path
self.descendant_count = 0
try:
self.size = 0 if os.path.isdir(self.path) else os.path.getsize(self.path)
except FileNotFoundError:
print(f'{path} not found, please check that you have compiled '
'the target that generates this manifest.')
exit(1)
class Analysis(Enum):
FILE_COUNT = 'file_count'
SIZE = 'size'
def __str__(self):
return self.value
class SortOrder(Enum):
ASCENDING = 'ascending'
DESCENDING = 'descending'
def __str__(self):
return self.value
def compute_prefix_paths(path: str) -> Iterable[str]:
prefix = path.rpartition('/')[0]
while prefix:
yield prefix
prefix = prefix.rpartition('/')[0]
class ManifestAnalyzer:
def __init__(self) -> None:
self.path_map: dict[str, FilesystemNode] = dict()
def parse_manifest(self, manifest_path: str) -> None:
out_dir = re.match('out\/[^\/]+', manifest_path).group()
with open(manifest_path, 'r') as manifest:
for line in manifest:
relative_path = line.strip().partition('=')[2]
self.register_file(f'{out_dir}/{relative_path}')
def register_file(self, path: str) -> None:
if path in self.path_map:
return
leaf_node = FilesystemNode(path)
self.path_map[path] = leaf_node
for prefix in compute_prefix_paths(path):
if prefix in self.path_map:
parent_node = self.path_map[prefix]
else:
parent_node = FilesystemNode(prefix)
self.path_map[prefix] = parent_node
parent_node.descendant_count += 1
parent_node.size += leaf_node.size
def print_file_count(self, max_depth: int, sort_order: SortOrder) -> None:
sorted_nodes = sorted(self.path_map.values(),
key=lambda node: node.descendant_count,
reverse=sort_order == SortOrder.DESCENDING)
for node in sorted_nodes:
if node.descendant_count <= 0:
continue
depth = node.path.count('/')
if depth > max_depth:
continue
print(f'{node.descendant_count: >10}\t{node.path}')
def print_byte_size(self, max_depth: int, sort_order: SortOrder) -> None:
sorted_nodes = sorted(self.path_map.values(),
key=lambda node: node.size,
reverse=sort_order == SortOrder.DESCENDING)
for node in sorted_nodes:
depth = node.path.count('/')
if depth > max_depth:
continue
print(f'{format_size(node.size): >10}\t{node.path}')
def main():
parser = argparse.ArgumentParser(
description='Launches a long-running emulator that can '
'be re-used for multiple test runs.')
parser.add_argument(
'manifest_path',
type=str,
help='path to the .manifest '
'file. For example, the manifest for chrome/test:browser_tests can be '
'found at <out_dir>/gen/chrome/test/browser_tests/browser_tests.manifest')
parser.add_argument('--analysis',
type=Analysis,
choices=list(Analysis),
default=Analysis.SIZE,
help='which type of analysis to print')
parser.add_argument('--max-depth',
type=int,
default=sys.maxsize,
help='only print directories to the provided depth')
parser.add_argument(
'--sort-order',
type=SortOrder,
choices=list(SortOrder),
default=SortOrder.ASCENDING,
help='which order to use for sorting, defualts to ascending')
args = parser.parse_args()
analyzer = ManifestAnalyzer()
analyzer.parse_manifest(args.manifest_path)
if args.analysis == Analysis.FILE_COUNT:
analyzer.print_file_count(args.max_depth, args.sort_order)
else:
analyzer.print_byte_size(args.max_depth, args.sort_order)
if __name__ == '__main__':
main()