#!/usr/bin/env python # Copyright (C) 2015 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Suggested call sequence: python tools/js/bower2bazel.py -w lib/js/bower_archives.bzl -b lib/js/bower_components.bzl """ from __future__ import print_function import collections import json import hashlib import optparse import os import subprocess import sys import tempfile import glob import bowerutil # list of licenses for packages that don't specify one in their bower.json file. package_licenses = { "es6-promise": "es6-promise", "fetch": "fetch", "iron-a11y-announcer": "polymer", "iron-a11y-keys-behavior": "polymer", "iron-autogrow-textarea": "polymer", "iron-behaviors": "polymer", "iron-dropdown": "polymer", "iron-fit-behavior": "polymer", "iron-flex-layout": "polymer", "iron-form-element-behavior": "polymer", "iron-input": "polymer", "iron-meta": "polymer", "iron-overlay-behavior": "polymer", "iron-resizable-behavior": "polymer", "iron-selector": "polymer", "iron-validatable-behavior": "polymer", "moment": "moment", "neon-animation": "polymer", "page": "page.js", "polymer": "polymer", "promise-polyfill": "promise-polyfill", "web-animations-js": "Apache2.0", "webcomponentsjs": "polymer", } def build_bower_json(version_targets, seeds): """Generate bower JSON file, return its path. Args: version_targets: bazel target names of the versions.json file. seeds: an iterable of bower package names of the seed packages, ie. the packages whose versions we control manually. """ bower_json = collections.OrderedDict() bower_json['name'] = 'bower2bazel-output' bower_json['version'] = '0.0.0' bower_json['description'] = 'Auto-generated bower.json for dependency management' bower_json['private'] = True bower_json['dependencies'] = {} seeds = set(seeds) for v in version_targets: path = os.path.join("bazel-out/*-fastbuild/bin", v.lstrip("/").replace(":", "/")) fs = glob.glob(path) assert len(fs) == 1, '%s: file not found or multiple files found: %s' % (path, fs) with open(fs[0]) as f: j = json.load(f) if "" in j: # drop dummy entries. del j[""] trimmed = {} for k, v in j.items(): if k in seeds: trimmed[k] = v bower_json['dependencies'].update(trimmed) tmpdir = tempfile.mkdtemp() ret = os.path.join(tmpdir, 'bower.json') with open(ret, 'w') as f: json.dump(bower_json, f, indent=2) return ret def bower_command(args): base = subprocess.check_output(["bazel", "info", "output_base"]).strip() exp = os.path.join(base, "external", "bower", "*npm_binary.tgz") fs = sorted(glob.glob(exp)) assert len(fs) == 1, "bower tarball not found or have multiple versions %s" % fs return ["python", os.getcwd() + "/tools/js/run_npm_binary.py", sorted(fs)[0]] + args def main(args): opts = optparse.OptionParser() opts.add_option('-w', help='.bzl output for WORKSPACE') opts.add_option('-b', help='.bzl output for //lib:BUILD') opts, args = opts.parse_args() target_str = subprocess.check_output([ "bazel", "query", "kind(bower_component_bundle, //polygerrit-ui/...)"]) seed_str = subprocess.check_output([ "bazel", "query", "attr(seed, 1, kind(bower_component, deps(//polygerrit-ui/...)))"]) targets = [s for s in target_str.split('\n') if s] seeds = [s for s in seed_str.split('\n') if s] prefix = "//lib/js:" non_seeds = [s for s in seeds if not s.startswith(prefix)] assert not non_seeds, non_seeds seeds = set([s[len(prefix):] for s in seeds]) version_targets = [t + "-versions.json" for t in targets] subprocess.check_call(['bazel', 'build'] + version_targets) bower_json_path = build_bower_json(version_targets, seeds) dir = os.path.dirname(bower_json_path) cmd = bower_command(["install"]) build_out = sys.stdout if opts.b: build_out = open(opts.b + ".tmp", 'w') ws_out = sys.stdout if opts.b: ws_out = open(opts.w + ".tmp", 'w') header = """# DO NOT EDIT # generated with the following command: # # %s # """ % ' '.join(sys.argv) ws_out.write(header) build_out.write(header) oldwd = os.getcwd() os.chdir(dir) subprocess.check_call(cmd) interpret_bower_json(seeds, ws_out, build_out) ws_out.close() build_out.close() os.chdir(oldwd) os.rename(opts.w + ".tmp", opts.w) os.rename(opts.b + ".tmp", opts.b) def dump_workspace(data, seeds, out): out.write('load("//tools/bzl:js.bzl", "bower_archive")\n\n') out.write('def load_bower_archives():\n') for d in data: if d["name"] in seeds: continue out.write(""" bower_archive( name = "%(name)s", package = "%(normalized-name)s", version = "%(version)s", sha1 = "%(bazel-sha1)s") """ % d) def dump_build(data, seeds, out): out.write('load("//tools/bzl:js.bzl", "bower_component")\n\n') out.write('def define_bower_components():\n') for d in data: out.write(" bower_component(\n") out.write(" name = \"%s\",\n" % d["name"]) out.write(" license = \"//lib:LICENSE-%s\",\n" % d["bazel-license"]) deps = sorted(d.get("dependencies", {}).keys()) if deps: if len(deps) == 1: out.write(" deps = [ \":%s\" ],\n" % deps[0]) else: out.write(" deps = [\n") for dep in deps: out.write(" \":%s\",\n" % dep) out.write(" ],\n") if d["name"] in seeds: out.write(" seed = True,\n") out.write(" )\n") # done def interpret_bower_json(seeds, ws_out, build_out): out = subprocess.check_output(["find", "bower_components/", "-name", ".bower.json"]) data = [] for f in sorted(out.split('\n')): if not f: continue pkg = json.load(open(f)) pkg_name = pkg["name"] pkg["bazel-sha1"] = bowerutil.hash_bower_component( hashlib.sha1(), os.path.dirname(f)).hexdigest() license = package_licenses.get(pkg_name, "DO_NOT_DISTRIBUTE") pkg["bazel-license"] = license # TODO(hanwen): bower packages can also have 'fully qualified' # names, ("PolymerElements/iron-ajax") as well as short names # ("iron-ajax"). It is possible for bower.json files to refer to # long names as their dependencies. If any package does this, we # will have to either 1) strip off the prefix (typically github # user?), or 2) build a map of short name <=> fully qualified # name. For now, we just ignore the problem. pkg["normalized-name"] = pkg["name"] data.append(pkg) dump_workspace(data, seeds, ws_out) dump_build(data, seeds, build_out) if __name__ == '__main__': main(sys.argv[1:])