OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 # Copyright 2015 The Chromium 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 """Suggests what Goop file entries can be removed. |
| 7 |
| 8 Horribly slow, but simple. Evaluates recursive dependencies of all packages |
| 9 in infra/go/... (including github ones) and compares them against Goopfile.lock |
| 10 entries. |
| 11 |
| 12 Supposed to be called under activated go environment. |
| 13 """ |
| 14 |
| 15 import os |
| 16 import subprocess |
| 17 import sys |
| 18 |
| 19 # For VENDORED_TOOLS. |
| 20 import bootstrap |
| 21 |
| 22 |
| 23 GO_DIR = os.path.dirname(os.path.abspath(__file__)) |
| 24 |
| 25 |
| 26 # List of Goopfile.lock entries we want to keep because they are imported only |
| 27 # when building on Windows (and thus detected as unused when script is used |
| 28 # on Linux). |
| 29 EXCEPTIONS = [ |
| 30 'github.com/mattn/go-isatty', |
| 31 'github.com/olekukonko/ts', |
| 32 ] |
| 33 |
| 34 |
| 35 def dumbcache(f): |
| 36 """Simple cache decorator for functions with hashable args.""" |
| 37 cache = {} |
| 38 def wrapper(*args): |
| 39 if args not in cache: |
| 40 cache[args] = f(*args) |
| 41 return cache[args] |
| 42 return wrapper |
| 43 |
| 44 |
| 45 @dumbcache |
| 46 def get_deps(pkg_glob): |
| 47 """Returns a set of imported dependencies.""" |
| 48 cmd = ['go', 'list', '-f', '{{ join .Deps "\\n" }}', pkg_glob] |
| 49 print 'Running ', cmd |
| 50 deps = subprocess.check_output(cmd).strip() |
| 51 return sorted(set(deps.splitlines())) |
| 52 |
| 53 |
| 54 @dumbcache |
| 55 def get_all_goop(): |
| 56 """Returns a list of packages specified in Goopfile.lock.""" |
| 57 with open(os.path.join(GO_DIR, 'Goopfile.lock')) as f: |
| 58 return [l.split()[0] for l in f] |
| 59 |
| 60 |
| 61 def get_used_goop(deps): |
| 62 """Returns set of packages in Goopfile.lock that cover a dependency list.""" |
| 63 used = set() |
| 64 for pkg in get_all_goop(): |
| 65 for dep in deps: |
| 66 # If goop pkg covers at least one dependency, it is used. |
| 67 if dep == pkg or dep.startswith(pkg + '/'): |
| 68 used.add(pkg) |
| 69 break |
| 70 return used |
| 71 |
| 72 |
| 73 def main(): |
| 74 all_deps = set(EXCEPTIONS) |
| 75 |
| 76 # We want to enumerate packages in infra/go/src/... and only them (not all |
| 77 # packages in GOPATH, since we don't want to enumerate vendored packages). |
| 78 # A package glob needs to start with '.' or '..' to be interpreted as file |
| 79 # system path. See 'go help packages'. |
| 80 go_dir_rel = os.path.relpath(GO_DIR) |
| 81 assert go_dir_rel.startswith('.') |
| 82 all_deps.update(get_deps(os.path.join(go_dir_rel, 'src', '...'))) |
| 83 |
| 84 # Tools build by bootstrap script may not be directly referenced by src/ code. |
| 85 for tool in bootstrap.VENDORED_TOOLS: |
| 86 all_deps.add(tool) |
| 87 all_deps.update(get_deps(tool)) |
| 88 |
| 89 # all_deps is all direct dependencies of infra/go/src code. Find what part of |
| 90 # Goopfile.lock covers them. Then recurse into them. Note that Goopfile |
| 91 # may specify a parent package of a package used in actual code. So recursing |
| 92 # into deps of that parent package may reveal more dependencies. |
| 93 goop_deps = set() |
| 94 while True: |
| 95 new_goop_deps = get_used_goop(all_deps) |
| 96 if new_goop_deps == goop_deps: |
| 97 break |
| 98 goop_deps = new_goop_deps |
| 99 for pkg in goop_deps: |
| 100 all_deps.update(get_deps(pkg + '/...')) |
| 101 |
| 102 # Find what is not used. |
| 103 unused = [p for p in get_all_goop() if p not in goop_deps] |
| 104 if unused: |
| 105 print '-' * 80 |
| 106 print 'Consider removing from Goopfile the following packages:' |
| 107 print '\n'.join(unused) |
| 108 else: |
| 109 print 'Hooray! All Goopfile packages seem to be in use.' |
| 110 |
| 111 return 0 |
| 112 |
| 113 |
| 114 if __name__ == '__main__': |
| 115 sys.exit(main()) |
OLD | NEW |