| #!/usr/bin/env vpython |
| # Copyright 2016 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. |
| |
| """A tool to prepare a GOPATH with vendored Go dependenies. |
| |
| We abuse Glide by using it for GOPATH-based vendoring, while it is supposed to |
| be used for "Vendor Experiment"-based vendoring. |
| |
| It knows only how to install packages into $GOPATH/src/vendor (assuming |
| glide.yaml is in $GOPATH/src/glide.yaml), yet we need packages in a separate |
| $GOPATH, so that this separate $GOPATH can be reused by infra_internal |
| workspace. |
| |
| infra/go/src/vendor/* doesn't work for infra_internal packages because it is in |
| a different $GOPATH, and infra_internal packages can't find dependencies |
| vendored there. |
| |
| Making separate infra_internal/go/src/vendor/* leads to a bigger mess where |
| there are multiple copies of packages imported via infra/* and infra_internal/* |
| code in a single binary. |
| |
| So instead we let glide do its thing, but then rudely convert vendor/* directory |
| it produces into a new completely separate $GOPATH (in <workspace>/.vendor). |
| |
| To remove a temptation to use "glide update" directly (it won't work as |
| expected), we rename glide.yaml and glide.lock into deps.yaml and deps.lock. |
| """ |
| |
| import argparse |
| import collections |
| import contextlib |
| import hashlib |
| import json |
| import os |
| import re |
| import shutil |
| import stat |
| import subprocess |
| import sys |
| import tempfile |
| import time |
| |
| import yaml |
| |
| |
| # Whitelist of packages with executables we want to be available in PATH. Will |
| # be installed via "go install" after Glide fetches all vendored dependencies. |
| # See 'install' function below. |
| VENDORED_TOOLS = [ |
| 'github.com/golang/mock/mockgen', |
| 'github.com/golang/protobuf/protoc-gen-go', |
| 'github.com/kisielk/errcheck', |
| 'github.com/maruel/panicparse/cmd/pp', |
| 'github.com/maruel/pre-commit-go/cmd/covg', |
| 'github.com/maruel/pre-commit-go/cmd/pcg', |
| 'github.com/smartystreets/goconvey', |
| 'golang.org/x/lint/golint', |
| 'golang.org/x/mobile/cmd/gomobile', |
| 'golang.org/x/tools/cmd/callgraph', |
| 'golang.org/x/tools/cmd/fiximports', |
| 'golang.org/x/tools/cmd/godex', |
| 'golang.org/x/tools/cmd/goimports', |
| 'golang.org/x/tools/cmd/gomvpkg', |
| 'golang.org/x/tools/cmd/gorename', |
| 'golang.org/x/tools/cmd/guru', |
| 'golang.org/x/tools/cmd/stringer', |
| 'google.golang.org/api/google-api-go-generator', |
| 'google.golang.org/appengine/cmd/aedeploy', |
| ] |
| |
| |
| # By default we drop all non-source code files from the vendored packages to |
| # make the bundle with dependencies smaller and simpler. Some packages don't |
| # like that. |
| PACKAGES_TO_VENDOR_COMPLETELY = [ |
| 'github.com/smartystreets/goconvey', # needs *.css etc for Web UI |
| 'go.starlark.net', # needs *.star files |
| ] |
| |
| |
| # Paths (relative to src/) to avoid including in the bundle because they contain |
| # very nested directory trees that break MAX_PATH limit on Windows. When adding |
| # a path here, verify that all code still builds without it. |
| BLACKLISTED_PATHS = [ |
| 'github.com/docker/docker/' + |
| 'vendor/github.com/google/certificate-transparency/cpp', |
| ] |
| |
| |
| # infra/ |
| REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| |
| |
| _Workspace = collections.namedtuple('_Workspace', ( |
| # Path of the directory containing "deps", default: infra/go |
| 'gobase', |
| # Path of the vendor root directory, default: infra/go/.vendor |
| 'vendor_root', |
| # Path of the Go runtime root, default: golang/go |
| 'goroot', |
| # Name of a CIPD package with prefetched deps, default: infra/go-deps |
| 'deps_cipd_pkg', |
| # URL of a CIPD package repository to use, default: chrome-infra-packages |
| 'cipd_service_url', |
| # Path to a service account key to use when talking to CIPD, default: None |
| 'service_account_json', |
| )) |
| |
| |
| WORKSPACE = _Workspace( |
| gobase=os.path.join(REPO_ROOT, 'go'), |
| vendor_root=os.path.join(REPO_ROOT, 'go', '.vendor'), |
| goroot=os.path.join(os.path.dirname(REPO_ROOT), 'golang', 'go'), |
| deps_cipd_pkg='infra/go-deps', |
| cipd_service_url='https://chrome-infra-packages.appspot.com', |
| service_account_json=None, # usually set via CLI args |
| ) |
| |
| |
| # Name of a Glide lock file to put into .vendor/* when all packages there are |
| # fully installed. Acts as a marker of successful installation. |
| APPLIED_LOCK = 'applied.lock' |
| |
| # Name of a Glide lock file to put in a CIPD package with bundled dependencies. |
| # Descrbies what's there. |
| BUNDLED_LOCK = 'bundled.lock' |
| |
| # Name of a CIPD package tag that specifies the version of bundled deps. |
| BUNDLE_FORMAT_TAG = 'go_deps_lock' |
| |
| |
| # Printed to bring attention because users usually ignore walls of text. |
| BANNER_START = '-'*24 + 'WARNING! WARNING! WARNING!' + '-'*24 |
| BANNER_END = '-'*24 + '--------------------------' + '-'*24 |
| |
| |
| class CallFailed(Exception): |
| """Raised by 'call' on non-zero exit codes.""" |
| |
| |
| def flatten_deps(deps): |
| """Converts deps.lock dict into a list of go packages specified there.""" |
| out = [] |
| for p in deps['imports']: |
| # Each 'p' here have a form similar to: |
| # |
| # - name: golang.org/x/net |
| # version: 31df19d69da8728e9220def59b80ee577c3e48bf |
| # repo: https://go.googlesource.com/net.git |
| # subpackages: |
| # - context |
| # - context/ctxhttp |
| # |
| # where empty 'subpackages' means an entire repo is used. |
| sub = p.get('subpackages') |
| if not sub: |
| out.append(p['name']) |
| else: |
| out.extend(p['name'] + '/' + subpkg for subpkg in sub) |
| return sorted(out) |
| |
| |
| def contains_subpackages(workspace, path): |
| stdout, _ = call(workspace, 'go', ['list', path + '...'], capture_output=True) |
| return stdout != '' |
| |
| |
| def is_googlesource(url): |
| return re.match(r'https\://.*\.googlesource\.com/', url) |
| |
| |
| def compare_deps(before, after): |
| """Analyzes a difference in old and new deps, and prints some helpful stuff. |
| |
| Returns True if deps changed, False if not. |
| |
| Args: |
| before: dict with deps.lock content before an update. |
| after: dict with deps.lock content after the update. |
| """ |
| # Print a list of what has been added or removed. |
| flatten_before = set(flatten_deps(before)) |
| flatten_after = set(flatten_deps(after)) |
| |
| new_deps = flatten_after - flatten_before |
| if new_deps: |
| print '-'*74 |
| print 'Dependencies added to deps.lock:' |
| for p in sorted(new_deps): |
| print ' * %s' % p |
| print '-'*74 |
| |
| rem_deps = flatten_before - flatten_after |
| if rem_deps: |
| print '-'*74 |
| print 'Dependencies removed from deps.lock:' |
| for p in sorted(rem_deps): |
| print ' * %s' % p |
| print '-'*74 |
| |
| # Package name => mirror repo URL (usually on *.googlesource.com) or '' if |
| # no mirror defines. |
| mirrors = {p['name']: p.get('repo') or '' for p in after['imports']} |
| |
| # Print a list of what have been bumped. |
| def revisions(deps): |
| return {p['name']: p['version'] for p in deps['imports']} |
| rev_before = revisions(before) |
| rev_after = revisions(after) |
| |
| bumps = [] |
| for pkg, rev in rev_after.iteritems(): |
| if pkg in rev_before and rev != rev_before[pkg]: |
| bumps.append((pkg, rev_before[pkg], rev)) |
| bumps.sort() |
| if bumps: |
| print '-'*74 |
| print 'Updated repos:' |
| gs = [v for v in bumps if is_googlesource(mirrors[v[0]])] |
| rest = [v for v in bumps if not is_googlesource(mirrors[v[0]])] |
| for pkg, sha1_before, sha1_after in gs: |
| print '%s/+log/%s..%s' % (mirrors[pkg], sha1_before[:12], sha1_after[:12]) |
| for pkg, sha1_before, sha1_after in rest: |
| print '%s: %s -> %s' % (pkg, sha1_before, sha1_after) |
| print '-'*74 |
| print |
| print ( |
| 'IMPORTANT: Please include the above updated repos report in the ' |
| 'commit message.') |
| print |
| |
| # Print a list of packages that need git mirrors. All of them do, so it just |
| # prints a list of packages without 'repo' field set. |
| |
| bad_ones = [] |
| for pkg in after['imports']: |
| if not pkg.get('repo'): |
| bad_ones.append(pkg) |
| if bad_ones: |
| bad_ones.sort(key=lambda p: p['name']) |
| print BANNER_START |
| print ( |
| 'Some packages that have been added to deps.yaml directly or pulled\n' |
| 'as transitive dependencies of packages specified in deps.yaml don\'t\n' |
| 'have mirror repositories specified. It means CI builders will pull\n' |
| 'dependencies directly from source-of-truth repos. This is usually\n' |
| 'a bad idea. We\'d have to update deps.yaml with information about\n' |
| 'the mirrors.') |
| print |
| print ( |
| 'First setup the following git mirrors (you may need to file an\n' |
| '"Infra-Git" ticket to do that):') |
| for p in bad_ones: |
| print ' * https://chromium.googlesource.com/external/%s.git' % p['name'] |
| print |
| print ( |
| 'Once all mirrors are up, make sure deps.yaml has the following\n' |
| 'records (by putting them there or modifying existing ones) and\n' |
| 'rerun "deps.py update" again:') |
| print |
| for p in bad_ones: |
| print '- package: %s' % p['name'] |
| print ( |
| ' repo: https://chromium.googlesource.com/external/%s.git' % |
| p['name']) |
| print ' version: master' |
| if p.get('subpackages'): |
| print ' subpackages:' |
| for subpkg in p['subpackages']: |
| print ' - %s' % subpkg |
| print BANNER_END |
| |
| # Did 'after' change in a meaningful way? Glide likes to touch 'updated' field |
| # no matter what. We ignore it when comparing deps. |
| before = before.copy() |
| before.pop('updated') |
| after = after.copy() |
| after.pop('updated') |
| return before != after |
| |
| |
| @contextlib.contextmanager |
| def unhack_vendor(workspace): |
| """Renames .vendor/src into .vendor/vendor, copies YAMLs into .vendor/. |
| |
| Basically, makes .vendor/* look like it is glide-managed directory, and not |
| some horrible abomination it, in fact, is. |
| |
| It also tracks changes done by glide to glide.lock and prints various helpful |
| stuff (see 'compare_deps'). |
| |
| Args: |
| workspace: an initialized _Workspace object. |
| """ |
| # Make empty .vendor if it didn't exist before. Makes life a bit easier below. |
| src_path = os.path.join(workspace.vendor_root, 'src') |
| vendor_path = os.path.join(workspace.vendor_root, 'vendor') |
| if not os.path.exists(src_path): |
| os.makedirs(src_path) |
| |
| try: |
| # Rename 'src' to 'vendor', so that glide can work with it. |
| os.rename(src_path, vendor_path) |
| |
| # Glide still demands 'src' for some reason, even if it is empty. |
| os.mkdir(src_path) |
| |
| # Copy YAMLs used by glide. |
| shutil.copy( |
| os.path.join(workspace.gobase, 'deps.yaml'), |
| os.path.join(workspace.vendor_root, 'glide.yaml')) |
| shutil.copy( |
| os.path.join(workspace.gobase, 'deps.lock'), |
| os.path.join(workspace.vendor_root, 'glide.lock')) |
| |
| lock_before = read_file(os.path.join(workspace.vendor_root, 'glide.lock')) |
| deps_before = yaml.safe_load(lock_before) |
| yield |
| lock_after = read_file(os.path.join(workspace.vendor_root, 'glide.lock')) |
| deps_after = yaml.safe_load(lock_after) |
| |
| if compare_deps(deps_before, deps_after): |
| print 'Run "deps.py install" to reinstall dependencies when ready.' |
| else: |
| # No changes? Just put glide.lock back as it was, since new glide.lock |
| # differs only in not very useful 'updated' timestamp field. |
| write_file(os.path.join(workspace.vendor_root, 'glide.lock'), lock_before) |
| |
| # Copy modified yamls back only on success. |
| shutil.copy( |
| os.path.join(workspace.vendor_root, 'glide.yaml'), |
| os.path.join(workspace.gobase, 'deps.yaml')) |
| shutil.copy( |
| os.path.join(workspace.vendor_root, 'glide.lock'), |
| os.path.join(workspace.gobase, 'deps.lock')) |
| |
| except Exception: |
| # Remove 'applied.lock' to make sure 'deps.py install' reinstalls |
| # the packages next time, fixing the state of .vendor/. |
| applied_lock = os.path.join(workspace.vendor_root, APPLIED_LOCK) |
| if os.path.exists(applied_lock): |
| os.remove(applied_lock) |
| |
| print >> sys.stderr, BANNER_START |
| print >> sys.stderr, ( |
| '%s was probably left in an inconsistent state!\n' % ( |
| workspace.vendor_root,)) |
| print >> sys.stderr, ( |
| 'You may want to remove it completely and build it again by running\n' |
| '"deps.py install".') |
| print >> sys.stderr, BANNER_END |
| raise |
| |
| finally: |
| # Undo everything. |
| os.remove(os.path.join(workspace.vendor_root, 'glide.lock')) |
| os.remove(os.path.join(workspace.vendor_root, 'glide.yaml')) |
| os.rmdir(src_path) # must still be empty |
| os.rename(vendor_path, src_path) |
| |
| |
| def call(workspace, tool, args, capture_output=False): |
| """Invokes a tool from GOROOT/bin, setting GOPATH to <workspace>/.vendor. |
| |
| Note that 'glide' is installed by bootstrap.py into GOROOT/bin too. Raises |
| CallFailed exception on errors. |
| |
| Args: |
| workspace: an initialized _Workspace object. |
| tool: name of an executable to call, e.g. "go" or "glide". |
| args: additional command line arguments to pass to it. |
| capture_output: whether to capture return the child process' stdout and |
| stderr. |
| |
| Returns: |
| A string containing the child process' stdout if capture_output was True. |
| """ |
| sfx = '.exe' if sys.platform == 'win32' else '' |
| cmd = [os.path.join(workspace.goroot, 'bin', tool + sfx)] + args |
| |
| # Completely replace any existing Go env vars. |
| env = os.environ.copy() |
| env['GOROOT'] = workspace.goroot |
| env['GOPATH'] = workspace.vendor_root |
| env['GOBIN'] = os.path.join(workspace.vendor_root, 'bin') |
| |
| # Glide searches for 'go' in PATH. Make it available. |
| env['PATH'] = os.path.join(env['GOROOT'], 'bin') + os.pathsep + env['PATH'] |
| |
| # Forbid Glide to mess with global ~/.glide or system temp. |
| # TODO(vadimsh): .glide/cache can have really long paths, may be problematic |
| # on Windows. |
| env['GLIDE_HOME'] = os.path.join(workspace.gobase, '.glide') |
| env['GLIDE_TMP'] = os.path.join(workspace.gobase, '.glide') |
| |
| kwargs = { |
| 'env': env, |
| 'cwd': env['GOPATH'], |
| } |
| if capture_output: |
| kwargs['stdout'] = subprocess.PIPE |
| kwargs['stderr'] = subprocess.PIPE |
| |
| proc = subprocess.Popen(cmd, **kwargs) |
| stdout, stderr = proc.communicate() |
| |
| if proc.returncode: |
| raise CallFailed( |
| '"%s %s" FAILED, see the log' % (tool, ' '.join(args))) |
| return stdout, stderr |
| |
| |
| def read_file(path): |
| """Returns contents of a given file or None if not readable.""" |
| try: |
| with open(path, 'rb') as f: |
| return f.read() |
| except IOError: |
| return None |
| |
| |
| def write_file(path, blob): |
| """Writes a blob into a file.""" |
| # Files originally installed from CIPD packages are read-only, need to make |
| # them writable before overwriting. |
| try: |
| st = os.stat(path) |
| os.chmod(path, st.st_mode | stat.S_IWUSR) |
| except OSError: |
| pass # doesn't exist yet probably |
| with open(path, 'wb') as f: |
| return f.write(blob) |
| |
| |
| def remove_directory(p): |
| """Recursively removes a directory if it exists (works on Windows!).""" |
| if not os.path.exists(p): |
| return |
| # One does not simply remove a directory on Windows... Read-only files |
| # (.git/* in particular) need special treatment. |
| def onerror(func, path, _exc_info): |
| if not os.access(path, os.W_OK): |
| os.chmod(path, stat.S_IWUSR) |
| func(path) |
| else: |
| raise |
| shutil.rmtree(p, onerror=onerror if sys.platform == 'win32' else None) |
| |
| |
| @contextlib.contextmanager |
| def temp_file(body=None, root=None): |
| """Creates a temp file and returns path to it.""" |
| fd, tmp = tempfile.mkstemp(suffix='go_deps_py', dir=root) |
| try: |
| if body: |
| with os.fdopen(fd, 'wb') as f: |
| f.write(body) |
| else: |
| os.close(fd) |
| yield tmp |
| finally: |
| os.remove(tmp) |
| |
| |
| def purify_directory(root, path): |
| """Removes all non-important files from a directory. |
| |
| Also drops +x bit on remaining regular files. |
| |
| Works recursively. For each file calls 'is_source_or_license(rel_path)' |
| to detect whether it is important or not, and deletes the file if not. |
| |
| Returns True if 'path' still has direct children. |
| """ |
| # Note: recursively removing empty directories is not trivial with os.walk. |
| # Doing the recursion directly is simpler. |
| has_files = False |
| for name in os.listdir(os.path.join(root, path)): |
| rel_path = os.path.join(path, name) |
| full_path = os.path.join(root, rel_path) |
| mode = os.lstat(full_path).st_mode |
| if stat.S_ISDIR(mode): |
| if not purify_directory(root, rel_path): |
| # The child directory is empty now, can be removed. |
| os.rmdir(full_path) |
| continue |
| elif not is_source_or_license(rel_path): |
| os.remove(full_path) |
| continue |
| elif mode & stat.S_IXUSR: |
| # Some *.go files inexplicably have +x bit. Drop it. |
| os.chmod(full_path, 0644) |
| has_files = True |
| return has_files |
| |
| |
| def is_source_or_license(path): |
| """Returns True if 'path' point to a file we want to keep. |
| |
| The path is given relative to GOPATH/src. |
| """ |
| pkg = os.path.dirname(path).replace('\\', '/') |
| name = os.path.basename(path) |
| |
| def in_list(l): |
| return any(pkg == p or pkg.startswith(p+'/') for p in l) |
| |
| if in_list(PACKAGES_TO_VENDOR_COMPLETELY): |
| return True |
| if in_list(BLACKLISTED_PATHS): |
| return False |
| |
| return ( |
| name.startswith('LICENSE') or |
| name.endswith(('.c', '.h', '.go', '.s')) and |
| not name.endswith('_test.go')) |
| |
| |
| def cipd(workspace, args, silent=False): |
| """Calls 'cipd' tool (from PATH), returns the process exit code.""" |
| cmd = ['cipd.bat' if sys.platform == 'win32' else 'cipd'] |
| cmd += args |
| if args[0] not in ['pkg-build']: # non-local op? |
| if workspace.service_account_json: |
| cmd += ['-service-account-json', workspace.service_account_json] |
| cmd += ['-service-url', workspace.cipd_service_url] |
| proc = subprocess.Popen( |
| cmd, |
| stdout=subprocess.PIPE if silent else None, |
| stderr=subprocess.PIPE if silent else None) |
| proc.communicate() |
| return proc.returncode |
| |
| |
| def get_bundle_ver(lock_file_body): |
| """Returns a version with a CIPD bundle corresponding to this lock file.""" |
| # There's a "hash" field in glide.lock. It is a trap. It doesn't change when |
| # version of the dependencies change. So instead just hash the (sanitized) |
| # manifest itself. |
| glide_lock = yaml.safe_load(lock_file_body) |
| glide_lock.pop('hash', None) |
| glide_lock.pop('updated', None) |
| sha1 = hashlib.sha1(json.dumps(glide_lock, sort_keys=True)).hexdigest() |
| return '%s:%s' % (BUNDLE_FORMAT_TAG, sha1) |
| |
| |
| def get_cipd_pkg_url(name, ver): |
| """Returns URL to a package in CIPD web UI.""" |
| return 'https://chrome-infra-packages.appspot.com/p/%s/+/%s' % (name, ver) |
| |
| |
| def is_existing_bundle(workspace, pkg, ver): |
| """Returns True if there exists a bundle with given version.""" |
| return cipd(workspace, ['resolve', pkg, '-version', ver], silent=True) == 0 |
| |
| |
| def grab_doc(func): |
| """Extracts help for CLI from a function doc string.""" |
| return func.__doc__.splitlines()[0].lower().strip('.') |
| |
| |
| def obliterate_glide_cache(workspace): |
| """Deletes .glide cache directory. |
| |
| For some mysterious reasons Glide sometimes skips fetching commits for |
| repos in its cache. Delete the cache. We are not really benefiting from |
| it, since it is used only when updating deps.lock, which is rare. |
| """ |
| remove_directory(os.path.join(workspace.gobase, '.glide')) |
| |
| |
| ################################################################################ |
| ## Subcommands. |
| |
| |
| GLIDE_INSTALL_RETRIES = 4 |
| |
| |
| def install(workspace, force=False, update_out=None, skip_bundle=False): |
| """Installs all dependencies from deps.lock into .vendor/ GOPATH. |
| |
| Will try to use a CIPD bundle with dependencies if it exists. See 'bundle' |
| command. |
| |
| Args: |
| workspace: an initialized _Workspace object. |
| force: if True, will forcefully rebuild .vendor even if it is up-to-date. |
| update_out: path to write deps.lock to if did install something. |
| skip_bundle: if True, fetch everything from git, not from CIPD bundle. |
| |
| Returns: |
| Exit code. |
| """ |
| required = read_file(os.path.join(workspace.gobase, 'deps.lock')) |
| if not force: |
| installed = read_file(os.path.join(workspace.vendor_root, APPLIED_LOCK)) |
| if installed == required: |
| return 0 |
| |
| # Glide is supposed to delete vendored packages that are no longer specified |
| # in glide.lock, but it doesn't (at least in v0.10.1), even if '--delete' and |
| # '--force' is used. So nuke entire .vendor/* and refetch everything from |
| # scratch. It also helps us to avoid various lingering state corruption in |
| # .vendor/*. |
| remove_directory(workspace.vendor_root) |
| |
| # Now we need to fetch all the source code into empty 'vendor_root'. There |
| # are two choices: either we use Glide (and clone each deps repo one by one), |
| # or we fetch a single CIPD package with all deps bundled already. |
| use_bundle = False |
| pkg = workspace.deps_cipd_pkg |
| ver = get_bundle_ver(required) |
| if not skip_bundle: |
| print 'Searching for a bundle with dependencies in CIPD...' |
| print 'CIPD package: %s' % get_cipd_pkg_url(pkg, ver) |
| use_bundle = is_existing_bundle(workspace, pkg, ver) |
| if not use_bundle: |
| print 'Not found, falling back to using "glide install".' |
| |
| if use_bundle: |
| # Don't retry, cipd does retries itself. |
| ensure_spec = '%s %s' % (pkg, ver) |
| with temp_file(body=ensure_spec, root=workspace.gobase) as tmp: |
| ret = cipd(workspace, ['ensure', '-ensure-file', tmp, '-root', |
| workspace.vendor_root]) |
| if ret: |
| print 'Failed to install dependencies from the bundle. See logs.' |
| return ret |
| |
| # Double check we've got what we requested. |
| bundled = read_file(os.path.join(workspace.vendor_root, BUNDLED_LOCK)) |
| if get_bundle_ver(bundled) != ver: |
| print ( |
| 'deps.lock in repo doesn\'t match bundled.lock in CIPD bundle.\n' |
| 'Possibly the bundle was built from a corrupted checkout.\n' |
| 'Build a new version.') |
| return 1 |
| |
| else: |
| with unhack_vendor(workspace): |
| for retry in xrange(GLIDE_INSTALL_RETRIES): |
| try: |
| call(workspace, 'glide', ['install']) |
| break |
| except CallFailed as e: |
| if retry < GLIDE_INSTALL_RETRIES - 1: |
| delay = 2 ** retry |
| print( |
| 'Failed to install dependencies. Deleting Glide cache and ' |
| 'retrying after %d sec.' % delay) |
| obliterate_glide_cache(workspace) |
| time.sleep(delay) |
| else: |
| raise e |
| # Remove all garbage, we need only non-test source code to use dependencies. |
| print 'Removing non-source code files...' |
| purify_directory(os.path.join(workspace.vendor_root, 'src'), '') |
| |
| # We will install only interesting subset of executables below. Nuke |
| # everything else to avoid polluting PATH with unimportant stuff. |
| remove_directory(os.path.join(workspace.vendor_root, 'bin')) |
| |
| # Install only stuff that was vendored via glide. That way we can support |
| # multiple workspaces with third party code, but keep a single VENDORED_TOOLS |
| # whitelist of "important stuff". If a workspace is not interested in having |
| # a tool present, it just doesn't list it in deps.yaml. |
| to_install = [] |
| for pkg in VENDORED_TOOLS: |
| pkg_path = os.path.join( |
| workspace.vendor_root, 'src', pkg.replace('/', os.sep)) |
| if not os.path.isdir(pkg_path): |
| print 'No Go package for vendored tool [%s] at: %s' % (pkg, pkg_path) |
| continue |
| to_install.append(pkg) |
| |
| print 'Rebuilding tools...' |
| call(workspace, 'go', ['install', '-v'] + to_install) |
| |
| # Put a marker file that indicates we successfully installed all deps. |
| write_file(os.path.join(workspace.vendor_root, APPLIED_LOCK), required) |
| if update_out: |
| write_file(update_out, required) |
| return 0 |
| |
| |
| def update(workspace): |
| """Updates deps.lock file to point to most recent versions of packages. |
| |
| Args: |
| workspace: an initialized _Workspace object. |
| """ |
| lock_path = os.path.join(workspace.vendor_root, 'glide.lock') |
| with unhack_vendor(workspace): |
| # Glide cache is buggy. We need no cache. |
| obliterate_glide_cache(workspace) |
| # For a mysterious reason Glide doesn't update all dependencies on a first |
| # try. Run it until it reports there's nothing to update. |
| deps = yaml.safe_load(read_file(lock_path)) |
| while True: |
| call(workspace, 'glide', ['update', '--force']) |
| deps_after = yaml.safe_load(read_file(lock_path)) |
| if deps == deps_after: |
| break |
| print 'One more time...' |
| deps = deps_after |
| return 0 |
| |
| |
| def add(workspace, packages): |
| """Adds a bunch of packages into deps.yaml. |
| |
| Args: |
| workspace: an initialized _Workspace object. |
| packages: a list of go packages to add to deps.yaml. |
| """ |
| with unhack_vendor(workspace): |
| obliterate_glide_cache(workspace) |
| call(workspace, 'glide', ['get', '--force'] + packages) |
| return 0 |
| |
| |
| def remove(workspace, packages): |
| """Removes a bunch of packages from deps.yaml. |
| |
| Args: |
| workspace: an initialized _Workspace object. |
| packages: a list of go packages to remove from deps.yaml. |
| """ |
| with unhack_vendor(workspace): |
| obliterate_glide_cache(workspace) |
| call(workspace, 'glide', ['remove'] + packages) |
| return 0 |
| |
| |
| def bundle(workspace, out_file=None): |
| """Builds and uploads a CIPD package with all vendored dependencies. |
| |
| This CIPD package is then used by 'deps.py install' to speed up the |
| installation. Uses a digest of deps.lock as a version identifier for the CIPD |
| package. |
| |
| Expects 'cipd' tool to be in PATH. |
| """ |
| lock_file = read_file(os.path.join(workspace.gobase, 'deps.lock')) |
| |
| pkg = workspace.deps_cipd_pkg |
| ver = get_bundle_ver(lock_file) |
| print 'CIPD package: %s' % get_cipd_pkg_url(workspace.deps_cipd_pkg, ver) |
| |
| if not out_file: |
| print 'Checking whether the bundle is already uploaded...' |
| if is_existing_bundle(workspace, pkg, ver): |
| print 'Yep, no need to upload it.' |
| return 0 |
| print 'Nope. Uploading it...' |
| |
| # Make sure we have all deps installed for git. |
| if install(workspace, force=True, skip_bundle=True): |
| return 1 |
| |
| # Put a description of what's there in the bundle. |
| write_file(os.path.join(workspace.vendor_root, BUNDLED_LOCK), lock_file) |
| |
| # Bundle only the source code (no 'pkg' and 'bin'). |
| pkg_def = { |
| 'package': pkg, |
| 'root': '.', # assumes temp_file() creates files in vendor_root |
| 'install_mode': 'copy', |
| 'data': [ |
| { |
| 'dir': 'src', |
| }, |
| { |
| 'file': BUNDLED_LOCK, |
| }, |
| { |
| 'version_file': 'CIPD_VERSION.json', |
| }, |
| ], |
| } |
| with temp_file(body=json.dumps(pkg_def), root=workspace.vendor_root) as tmp: |
| if out_file: |
| cmd = ['pkg-build', '-pkg-def', tmp, '-out', out_file] |
| else: |
| cmd = ['create', '-pkg-def', tmp, '-tag', ver, '-ref', 'latest'] |
| cmd.extend(['-hash-algo', 'sha256']) |
| if cipd(workspace, cmd): |
| print 'FAILED! See logs.' |
| return 1 |
| |
| print 'Done!' |
| return 0 |
| |
| |
| def main(args): |
| parser = argparse.ArgumentParser( |
| description='Utility to manage go vendored dependencies.') |
| parser.add_argument( |
| '--workspace', action='store', default=WORKSPACE.gobase, |
| help='directory with deps.yaml, deps will be installed in .vendor subdir') |
| parser.add_argument( |
| '--goroot', action='store', default=WORKSPACE.goroot, |
| help='Go installation GOROOT directory') |
| |
| subparsers = parser.add_subparsers() |
| |
| parser_install = subparsers.add_parser('install', help=grab_doc(install)) |
| parser_install.set_defaults(action=install) |
| parser_install.add_argument( |
| '--service-account-json', action='store', default=None, |
| help='path to a service account key to pass to CIPD client') |
| parser_install.add_argument( |
| '--force', action='store_true', default=False, |
| help='forcefully reinstall all dependencies') |
| parser_install.add_argument( |
| '--update-out', action='store', default=None, |
| help='if supplied, the updated list of Go packages will be written here ' |
| 'if an update occurred.') |
| |
| parser_update = subparsers.add_parser('update', help=grab_doc(update)) |
| parser_update.set_defaults(action=update) |
| |
| parser_add = subparsers.add_parser('add', help=grab_doc(add)) |
| parser_add.set_defaults(action=add) |
| parser_add.add_argument( |
| 'pkg', nargs='+', help='a go package to add to deps.yaml') |
| |
| parser_remove = subparsers.add_parser('remove', help=grab_doc(remove)) |
| parser_remove.set_defaults(action=remove) |
| parser_remove.add_argument( |
| 'pkg', nargs='+', help='a go package to remove from deps.yaml') |
| |
| parser_bundle = subparsers.add_parser('bundle', help=grab_doc(bundle)) |
| parser_bundle.set_defaults(action=bundle) |
| parser_bundle.add_argument( |
| '--service-account-json', action='store', default=None, |
| help='path to a service account key to pass to CIPD client') |
| parser_bundle.add_argument( |
| '--to-file', action='store', default=None, |
| help='if given, will not contact CIPD backend and will dump the bundle ' |
| 'as a file on disk at the specified location') |
| |
| opts = parser.parse_args(args) |
| |
| workspace = WORKSPACE |
| if opts.workspace != workspace.gobase: |
| workspace = workspace._replace(gobase=opts.workspace) |
| if opts.goroot: |
| workspace = workspace._replace(goroot=opts.goroot) |
| if getattr(opts, 'service_account_json', None): |
| workspace = workspace._replace( |
| service_account_json=opts.service_account_json) |
| |
| try: |
| if opts.action == install: |
| return install(workspace, opts.force, opts.update_out) |
| if opts.action == update: |
| return update(workspace) |
| if opts.action == add: |
| return add(workspace, opts.pkg) |
| if opts.action == remove: |
| return remove(workspace, opts.pkg) |
| if opts.action == bundle: |
| return bundle(workspace, opts.to_file) |
| assert False, 'Unreachable' |
| except CallFailed as exc: |
| print >> sys.stderr, str(exc) |
| return 1 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main(sys.argv[1:])) |