Files
gerrit/tools/bzl/js.bzl
Han-Wen Nienhuys ec12294301 Make zip generation (more) reproducible
The 'touch' command uses the system default timezone if none is
supplied. This makes zip files dependent on the timezone of the
generating machine.

Zip by default stores extended attributes (eg. ctime, atime) on Unix
systems. These can be further sources of non-determinism, so switch it
off with --no-extra

Change-Id: I7e102be7368ef339cbaf47524b90688c32a09238
2018-04-25 08:25:31 +09:00

394 lines
11 KiB
Python

NPMJS = "NPMJS"
GERRIT = "GERRIT:"
NPM_VERSIONS = {
"bower": "1.8.0",
"crisper": "2.0.2",
"vulcanize": "1.14.8",
}
NPM_SHA1S = {
"bower": "55dbebef0ad9155382d9e9d3e497c1372345b44a",
"crisper": "7183c58cea33632fb036c91cefd1b43e390d22a2",
"vulcanize": "679107f251c19ab7539529b1e3fdd40829e6fc63",
}
def _npm_tarball(name):
return "%s@%s.npm_binary.tgz" % (name, NPM_VERSIONS[name])
def _npm_binary_impl(ctx):
"""rule to download a NPM archive."""
name = ctx.name
version= NPM_VERSIONS[name]
sha1 = NPM_VERSIONS[name]
dir = '%s-%s' % (name, version)
filename = '%s.tgz' % dir
base = '%s@%s.npm_binary.tgz' % (name, version)
dest = ctx.path(base)
repository = ctx.attr.repository
if repository == GERRIT:
url = 'http://gerrit-maven.storage.googleapis.com/npm-packages/%s' % filename
elif repository == NPMJS:
url = 'http://registry.npmjs.org/%s/-/%s' % (name, filename)
else:
fail('repository %s not in {%s,%s}' % (repository, GERRIT, NPMJS))
python = ctx.which("python")
script = ctx.path(ctx.attr._download_script)
sha1 = NPM_SHA1S[name]
args = [python, script, "-o", dest, "-u", url, "-v", sha1]
out = ctx.execute(args)
if out.return_code:
fail("failed %s: %s" % (args, out.stderr))
ctx.file("BUILD", "package(default_visibility=['//visibility:public'])\nfilegroup(name='tarball', srcs=['%s'])" % base, False)
npm_binary = repository_rule(
attrs = {
# Label resolves within repo of the .bzl file.
"_download_script": attr.label(default = Label("//tools:download_file.py")),
"repository": attr.string(default = NPMJS),
},
local = True,
implementation = _npm_binary_impl,
)
# for use in repo rules.
def _run_npm_binary_str(ctx, tarball, args):
python_bin = ctx.which("python")
return " ".join([
python_bin,
ctx.path(ctx.attr._run_npm),
ctx.path(tarball)] + args)
def _bower_archive(ctx):
"""Download a bower package."""
download_name = '%s__download_bower.zip' % ctx.name
renamed_name = '%s__renamed.zip' % ctx.name
version_name = '%s__version.json' % ctx.name
cmd = [
ctx.which("python"),
ctx.path(ctx.attr._download_bower),
'-b', '%s' % _run_npm_binary_str(ctx, ctx.attr._bower_archive, []),
'-n', ctx.name,
'-p', ctx.attr.package,
'-v', ctx.attr.version,
'-s', ctx.attr.sha1,
'-o', download_name,
]
out = ctx.execute(cmd)
if out.return_code:
fail("failed %s: %s" % (" ".join(cmd), out.stderr))
_bash(ctx, " && " .join([
"TMP=$(mktemp -d || mktemp -d -t bazel-tmp)",
"TZ=UTC",
"export UTC",
"cd $TMP",
"mkdir bower_components",
"cd bower_components",
"unzip %s" % ctx.path(download_name),
"cd ..",
"find . -exec touch -t 198001010000 '{}' ';'",
"zip -Xr %s bower_components" % renamed_name,
"cd ..",
"rm -rf ${TMP}",
]))
dep_version = ctx.attr.semver if ctx.attr.semver else ctx.attr.version
ctx.file(version_name,
'"%s":"%s#%s"' % (ctx.name, ctx.attr.package, dep_version))
ctx.file(
"BUILD",
"\n".join([
"package(default_visibility=['//visibility:public'])",
"filegroup(name = 'zipfile', srcs = ['%s'], )" % download_name,
"filegroup(name = 'version_json', srcs = ['%s'], visibility=['//visibility:public'])" % version_name,
]), False)
def _bash(ctx, cmd):
cmd_list = ["bash", "-c", cmd]
out = ctx.execute(cmd_list)
if out.return_code:
fail("failed %s: %s" % (" ".join(cmd_list), out.stderr))
bower_archive = repository_rule(
_bower_archive,
attrs = {
"_bower_archive": attr.label(default = Label("@bower//:%s" % _npm_tarball("bower"))),
"_run_npm": attr.label(default = Label("//tools/js:run_npm_binary.py")),
"_download_bower": attr.label(default = Label("//tools/js:download_bower.py")),
"sha1": attr.string(mandatory = True),
"version": attr.string(mandatory = True),
"package": attr.string(mandatory = True),
"semver": attr.string(),
},
)
def _bower_component_impl(ctx):
transitive_zipfiles = depset([ctx.file.zipfile])
for d in ctx.attr.deps:
transitive_zipfiles += d.transitive_zipfiles
transitive_licenses = depset()
if ctx.file.license:
transitive_licenses += depset([ctx.file.license])
for d in ctx.attr.deps:
transitive_licenses += d.transitive_licenses
transitive_versions = depset(ctx.files.version_json)
for d in ctx.attr.deps:
transitive_versions += d.transitive_versions
return struct(
transitive_zipfiles=transitive_zipfiles,
transitive_versions=transitive_versions,
transitive_licenses=transitive_licenses,
)
_common_attrs = {
"deps": attr.label_list(providers = [
"transitive_zipfiles",
"transitive_versions",
"transitive_licenses",
]),
}
def _js_component(ctx):
dir = ctx.outputs.zip.path + ".dir"
name = ctx.outputs.zip.basename
if name.endswith(".zip"):
name = name[:-4]
dest = "%s/%s" % (dir, name)
cmd = " && ".join([
"TZ=UTC",
"export TZ",
"mkdir -p %s" % dest,
"cp %s %s/" % (' '.join([s.path for s in ctx.files.srcs]), dest),
"cd %s" % dir,
"find . -exec touch -t 198001010000 '{}' ';'",
"zip -Xqr ../%s *" % ctx.outputs.zip.basename
])
ctx.action(
inputs = ctx.files.srcs,
outputs = [ctx.outputs.zip],
command = cmd,
mnemonic = "GenBowerZip")
licenses = depset()
if ctx.file.license:
licenses += depset([ctx.file.license])
return struct(
transitive_zipfiles=list([ctx.outputs.zip]),
transitive_versions=depset(),
transitive_licenses=licenses)
js_component = rule(
_js_component,
attrs = dict(_common_attrs.items() + {
"srcs": attr.label_list(allow_files = [".js"]),
"license": attr.label(allow_single_file = True),
}.items()),
outputs = {
"zip": "%{name}.zip",
},
)
_bower_component = rule(
_bower_component_impl,
attrs = dict(_common_attrs.items() + {
"zipfile": attr.label(allow_single_file = [".zip"]),
"license": attr.label(allow_single_file = True),
"version_json": attr.label(allow_files = [".json"]),
# If set, define by hand, and don't regenerate this entry in bower2bazel.
"seed": attr.bool(default = False),
}.items()),
)
# TODO(hanwen): make license mandatory.
def bower_component(name, license=None, **kwargs):
prefix = "//lib:LICENSE-"
if license and not license.startswith(prefix):
license = prefix + license
_bower_component(
name=name,
license=license,
zipfile="@%s//:zipfile"% name,
version_json="@%s//:version_json" % name,
**kwargs)
def _bower_component_bundle_impl(ctx):
"""A bunch of bower components zipped up."""
zips = depset()
for d in ctx.attr.deps:
zips += d.transitive_zipfiles
versions = depset()
for d in ctx.attr.deps:
versions += d.transitive_versions
licenses = depset()
for d in ctx.attr.deps:
licenses += d.transitive_versions
out_zip = ctx.outputs.zip
out_versions = ctx.outputs.version_json
ctx.action(
inputs=list(zips),
outputs=[out_zip],
command=" && ".join([
"p=$PWD",
"TZ=UTC",
"export TZ",
"rm -rf %s.dir" % out_zip.path,
"mkdir -p %s.dir/bower_components" % out_zip.path,
"cd %s.dir/bower_components" % out_zip.path,
"for z in %s; do unzip -q $p/$z ; done" % " ".join(sorted([z.path for z in zips])),
"cd ..",
"find . -exec touch -t 198001010000 '{}' ';'",
"zip -Xqr $p/%s bower_components/*" % out_zip.path,
]),
mnemonic="BowerCombine")
ctx.action(
inputs=list(versions),
outputs=[out_versions],
mnemonic="BowerVersions",
command="(echo '{' ; for j in %s ; do cat $j; echo ',' ; done ; echo \\\"\\\":\\\"\\\"; echo '}') > %s" % (" ".join([v.path for v in versions]), out_versions.path))
return struct(
transitive_zipfiles=zips,
transitive_versions=versions,
transitive_licenses=licenses)
bower_component_bundle = rule(
_bower_component_bundle_impl,
attrs = _common_attrs,
outputs = {
"zip": "%{name}.zip",
"version_json": "%{name}-versions.json",
},
)
"""Groups a set of bower components together in a zip file.
Outputs:
NAME-versions.json:
a JSON file containing a PKG-NAME => PKG-NAME#VERSION mapping for the
transitive dependencies.
NAME.zip:
a zip file containing the transitive dependencies for this bundle.
"""
def _vulcanize_impl(ctx):
# intermediate artifact.
vulcanized = ctx.new_file(
ctx.configuration.genfiles_dir, ctx.outputs.html, ".vulcanized.html")
destdir = ctx.outputs.html.path + ".dir"
zips = [z for d in ctx.attr.deps for z in d.transitive_zipfiles ]
hermetic_npm_binary = " ".join([
'python',
"$p/" + ctx.file._run_npm.path,
"$p/" + ctx.file._vulcanize_archive.path,
'--inline-scripts',
'--inline-css',
'--strip-comments',
'--out-html', "$p/" + vulcanized.path,
ctx.file.app.path
])
pkg_dir = ctx.attr.pkg.lstrip("/")
cmd = " && ".join([
# unpack dependencies.
"export PATH",
"p=$PWD",
"rm -rf %s" % destdir,
"mkdir -p %s/%s/bower_components" % (destdir, pkg_dir),
"for z in %s; do unzip -qd %s/%s/bower_components/ $z; done" % (
' '.join([z.path for z in zips]), destdir, pkg_dir),
"tar -cf - %s | tar -C %s -xf -" % (" ".join([s.path for s in ctx.files.srcs]), destdir),
"cd %s" % destdir,
hermetic_npm_binary,
])
# Node/NPM is not (yet) hermeticized, so we have to get the binary
# from the environment, and it may be under $HOME, so we can't run
# in the sandbox.
node_tweaks = dict(
use_default_shell_env = True,
execution_requirements = {"local": "1"},
)
ctx.action(
mnemonic = "Vulcanize",
inputs = [ctx.file._run_npm, ctx.file.app,
ctx.file._vulcanize_archive
] + list(zips) + ctx.files.srcs,
outputs = [vulcanized],
command = cmd,
**node_tweaks)
hermetic_npm_command = "export PATH && " + " ".join([
'python',
ctx.file._run_npm.path,
ctx.file._crisper_archive.path,
"--always-write-script",
"--source", vulcanized.path,
"--html", ctx.outputs.html.path,
"--js", ctx.outputs.js.path])
ctx.action(
mnemonic = "Crisper",
inputs = [ctx.file._run_npm, ctx.file.app,
ctx.file._crisper_archive, vulcanized],
outputs = [ctx.outputs.js, ctx.outputs.html],
command = hermetic_npm_command,
**node_tweaks)
_vulcanize_rule = rule(
_vulcanize_impl,
attrs = {
"deps": attr.label_list(providers = ["transitive_zipfiles"]),
"app": attr.label(
mandatory = True,
allow_single_file = True,
),
"srcs": attr.label_list(allow_files = [
".js",
".html",
".txt",
".css",
".ico",
]),
"pkg": attr.string(mandatory = True),
"_run_npm": attr.label(
default = Label("//tools/js:run_npm_binary.py"),
allow_single_file = True,
),
"_vulcanize_archive": attr.label(
default = Label("@vulcanize//:%s" % _npm_tarball("vulcanize")),
allow_single_file = True,
),
"_crisper_archive": attr.label(
default = Label("@crisper//:%s" % _npm_tarball("crisper")),
allow_single_file = True,
),
},
outputs = {
"html": "%{name}.html",
"js": "%{name}.js",
},
)
def vulcanize(*args, **kwargs):
"""Vulcanize runs vulcanize and crisper on a set of sources."""
_vulcanize_rule(*args, pkg=PACKAGE_NAME, **kwargs)