Index: go/clean_goop.py |
diff --git a/go/clean_goop.py b/go/clean_goop.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..ac4720522ee3ea59810a8d98459eae90f49de87c |
--- /dev/null |
+++ b/go/clean_goop.py |
@@ -0,0 +1,115 @@ |
+#!/usr/bin/env python |
+# Copyright 2015 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+ |
+"""Suggests what Goop file entries can be removed. |
+ |
+Horribly slow, but simple. Evaluates recursive dependencies of all packages |
+in infra/go/... (including github ones) and compares them against Goopfile.lock |
+entries. |
+ |
+Supposed to be called under activated go environment. |
+""" |
+ |
+import os |
+import subprocess |
+import sys |
+ |
+# For VENDORED_TOOLS. |
+import bootstrap |
+ |
+ |
+GO_DIR = os.path.dirname(os.path.abspath(__file__)) |
+ |
+ |
+# List of Goopfile.lock entries we want to keep because they are imported only |
+# when building on Windows (and thus detected as unused when script is used |
+# on Linux). |
+EXCEPTIONS = [ |
+ 'github.com/mattn/go-isatty', |
+ 'github.com/olekukonko/ts', |
+] |
+ |
+ |
+def dumbcache(f): |
+ """Simple cache decorator for functions with hashable args.""" |
+ cache = {} |
+ def wrapper(*args): |
+ if args not in cache: |
+ cache[args] = f(*args) |
+ return cache[args] |
+ return wrapper |
+ |
+ |
+@dumbcache |
+def get_deps(pkg_glob): |
+ """Returns a set of imported dependencies.""" |
+ cmd = ['go', 'list', '-f', '{{ join .Deps "\\n" }}', pkg_glob] |
+ print 'Running ', cmd |
+ deps = subprocess.check_output(cmd).strip() |
+ return sorted(set(deps.splitlines())) |
+ |
+ |
+@dumbcache |
+def get_all_goop(): |
+ """Returns a list of packages specified in Goopfile.lock.""" |
+ with open(os.path.join(GO_DIR, 'Goopfile.lock')) as f: |
+ return [l.split()[0] for l in f] |
+ |
+ |
+def get_used_goop(deps): |
+ """Returns set of packages in Goopfile.lock that cover a dependency list.""" |
+ used = set() |
+ for pkg in get_all_goop(): |
+ for dep in deps: |
+ # If goop pkg covers at least one dependency, it is used. |
+ if dep == pkg or dep.startswith(pkg + '/'): |
+ used.add(pkg) |
+ break |
+ return used |
+ |
+ |
+def main(): |
+ all_deps = set(EXCEPTIONS) |
+ |
+ # We want to enumerate packages in infra/go/src/... and only them (not all |
+ # packages in GOPATH, since we don't want to enumerate vendored packages). |
+ # A package glob needs to start with '.' or '..' to be interpreted as file |
+ # system path. See 'go help packages'. |
+ go_dir_rel = os.path.relpath(GO_DIR) |
+ assert go_dir_rel.startswith('.') |
+ all_deps.update(get_deps(os.path.join(go_dir_rel, 'src', '...'))) |
+ |
+ # Tools build by bootstrap script may not be directly referenced by src/ code. |
+ for tool in bootstrap.VENDORED_TOOLS: |
+ all_deps.add(tool) |
+ all_deps.update(get_deps(tool)) |
+ |
+ # all_deps is all direct dependencies of infra/go/src code. Find what part of |
+ # Goopfile.lock covers them. Then recurse into them. Note that Goopfile |
+ # may specify a parent package of a package used in actual code. So recursing |
+ # into deps of that parent package may reveal more dependencies. |
+ goop_deps = set() |
+ while True: |
+ new_goop_deps = get_used_goop(all_deps) |
+ if new_goop_deps == goop_deps: |
+ break |
+ goop_deps = new_goop_deps |
+ for pkg in goop_deps: |
+ all_deps.update(get_deps(pkg + '/...')) |
+ |
+ # Find what is not used. |
+ unused = [p for p in get_all_goop() if p not in goop_deps] |
+ if unused: |
+ print '-' * 80 |
+ print 'Consider removing from Goopfile the following packages:' |
+ print '\n'.join(unused) |
+ else: |
+ print 'Hooray! All Goopfile packages seem to be in use.' |
+ |
+ return 0 |
+ |
+ |
+if __name__ == '__main__': |
+ sys.exit(main()) |