bazel: genasciidoc and genasciidoc_zip rule

Implement genasciidoc rule for bazel. It's a filegroup containing all
the html and resource files.

Also implement genasciidoc_zip rule for bazel, which is similar to
buck's genasciidoc rule to produce a zip file containing all asciidoctor
generated and resource files.

TEST PLAN:
  bazel build Documentation
  buck build Documentation:html
  diff -u bazel-bin/Documentation/install.html buck-out/gen/Documentation/html__tmp/Documentation/install.html

Change-Id: I3065355800a982c6956d3bb634204baaa60c045e
This commit is contained in:
Yuxuan 'fishy' Wang 2016-09-30 07:59:54 +08:00
parent 20eddcc022
commit 14fdf9342e
7 changed files with 495 additions and 46 deletions

8
BUILD
View File

@ -8,6 +8,14 @@ genrule(
visibility = ['//visibility:public'],
)
genrule(
name = "LICENSES",
srcs = ["//Documentation:licenses.txt"],
cmd = "cp $< $@",
outs = ["LICENSES.txt"],
visibility = ['//visibility:public'],
)
pkg_war(name = 'gerrit')
pkg_war(name = 'headless', ui = None)
pkg_war(name = 'release', ui = 'ui_optdbg_r', context = ['//plugins:core'])

View File

@ -1,6 +1,46 @@
load("//tools/bzl:asciidoc.bzl", "documentation_attributes")
load("//tools/bzl:asciidoc.bzl", "genasciidoc")
load("//tools/bzl:asciidoc.bzl", "genasciidoc_zip")
load("//tools/bzl:license.bzl", "license_map")
exports_files([
"replace_macros.py",
])
filegroup(
name = "prettify_files",
srcs = [
":prettify.min.css",
":prettify.min.js",
],
)
genrule(
name = "prettify_min_css",
srcs = ["//gerrit-prettify:src/main/resources/com/google/gerrit/prettify/client/prettify.css"],
cmd = "cp $< $@",
outs = ["prettify.min.css"],
)
genrule(
name = "prettify_min_js",
srcs = ["//gerrit-prettify:src/main/resources/com/google/gerrit/prettify/client/prettify.js"],
cmd = "cp $< $@",
outs = ["prettify.min.js"],
)
filegroup(
name = "resources",
srcs = glob([
"images/*.jpg",
"images/*.png",
]) + [
":prettify_files",
"//:LICENSES.txt",
],
visibility = ['//visibility:public'],
)
license_map(
name = "licenses",
targets = [
@ -8,10 +48,11 @@ license_map(
"//gerrit-gwtui:ui_module",
],
opts = ["--asciidoctor"],
visibility = ['//visibility:public'],
)
DOC_DIR = "Documentation"
SRCS = glob(["*.txt"])
SRCS = glob(["*.txt"]) + [":licenses.txt"]
genrule(
name = "index",
@ -20,12 +61,38 @@ genrule(
'--prefix "%s/" ' % DOC_DIR +
'--in-ext ".txt" ' +
'--out-ext ".html" ' +
"$(SRCS) " +
"$(location :licenses.txt)",
tools = [
":licenses.txt",
"//lib/asciidoctor:doc_indexer",
],
"$(SRCS)",
tools = ["//lib/asciidoctor:doc_indexer"],
srcs = SRCS,
outs = ["index.jar"],
)
# For the same srcs, we can have multiple genasciidoc_zip rules, but only one
# genasciidoc rule. Because multiple genasciidoc rules will have conflicting
# output files.
genasciidoc(
name = "Documentation",
srcs = SRCS,
attributes = documentation_attributes(),
backend = "html5",
visibility = ["//visibility:public"],
)
genasciidoc_zip(
name = "html",
srcs = SRCS,
attributes = documentation_attributes(),
backend = "html5",
directory = DOC_DIR,
visibility = ["//visibility:public"],
)
genasciidoc_zip(
name = "searchfree",
srcs = SRCS,
attributes = documentation_attributes(),
backend = "html5",
directory = DOC_DIR,
searchbox = False,
visibility = ["//visibility:public"],
)

View File

@ -33,3 +33,8 @@ java_library(
],
visibility = ['//visibility:public'],
)
exports_files([
'src/main/resources/com/google/gerrit/prettify/client/prettify.css',
'src/main/resources/com/google/gerrit/prettify/client/prettify.js',
])

View File

@ -1,3 +1,10 @@
java_binary(
name = "asciidoc",
main_class = "AsciiDoctor",
runtime_deps = [":asciidoc_lib"],
visibility = ["//visibility:public"],
)
java_library(
name = "asciidoc_lib",
srcs = ["java/AsciiDoctor.java"],

View File

@ -24,12 +24,14 @@ import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -41,6 +43,7 @@ public class AsciiDoctor {
private static final String DOCTYPE = "article";
private static final String ERUBY = "erb";
private static final String REVNUMBER_NAME = "revnumber";
@Option(name = "-b", usage = "set output format backend")
private String backend = "html5";
@ -60,13 +63,26 @@ public class AsciiDoctor {
@Option(name = "--tmp", usage = "temporary output path")
private File tmpdir;
@Option(name = "--mktmp", usage = "create a temporary output path")
private boolean mktmp;
@Option(name = "-a", usage =
"a list of attributes, in the form key or key=value pair")
private List<String> attributes = new ArrayList<>();
@Option(name = "--bazel", usage =
"bazel mode: generate multiple output files instead of a single zip file")
private boolean bazel;
@Option(name = "--revnumber-file", usage =
"the file contains revnumber string")
private File revnumberFile;
@Argument(usage = "input files")
private List<String> inputFiles = new ArrayList<>();
private String revnumber;
public static String mapInFileToOutFile(
String inFile, String inExt, String outExt) {
String basename = new File(inFile).getName();
@ -82,19 +98,26 @@ public class AsciiDoctor {
return basename + outExt;
}
private Options createOptions(File outputFile) {
private Options createOptions(File base, File outputFile) {
OptionsBuilder optionsBuilder = OptionsBuilder.options();
optionsBuilder.backend(backend).docType(DOCTYPE).eruby(ERUBY)
.safe(SafeMode.UNSAFE).baseDir(basedir);
// XXX(fishywang): ideally we should just output to a string and add the
// content into zip. But asciidoctor will actually ignore all attributes if
// not output to a file. So we *have* to output to a file then read the
// content of the file into zip.
optionsBuilder.toFile(outputFile);
optionsBuilder
.backend(backend)
.docType(DOCTYPE)
.eruby(ERUBY)
.safe(SafeMode.UNSAFE)
.baseDir(base)
// XXX(fishywang): ideally we should just output to a string and add the
// content into zip. But asciidoctor will actually ignore all attributes
// if not output to a file. So we *have* to output to a file then read
// the content of the file into zip.
.toFile(outputFile);
AttributesBuilder attributesBuilder = AttributesBuilder.attributes();
attributesBuilder.attributes(getAttributes());
if (revnumber != null) {
attributesBuilder.attribute(REVNUMBER_NAME, revnumber);
}
optionsBuilder.attributes(attributesBuilder.get());
return optionsBuilder.get();
@ -133,31 +156,52 @@ public class AsciiDoctor {
return;
}
try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(zipFile))) {
for (String inputFile : inputFiles) {
if (!inputFile.endsWith(inExt)) {
// We have to use UNSAFE mode in order to make embedding work. But in
// UNSAFE mode we'll also need css file in the same directory, so we
// have to add css files into the SRCS.
continue;
}
String outName = mapInFileToOutFile(inputFile, inExt, outExt);
File out = new File(tmpdir, outName);
out.getParentFile().mkdirs();
Options options = createOptions(out);
renderInput(options, new File(inputFile));
zipFile(out, outName, zip);
if (revnumberFile != null) {
try (BufferedReader reader =
new BufferedReader(new FileReader(revnumberFile))) {
revnumber = reader.readLine();
}
}
File[] cssFiles = tmpdir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".css");
if (mktmp) {
tmpdir = Files.createTempDirectory("asciidoctor-").toFile();
}
if (bazel) {
renderFiles(inputFiles, null);
} else {
try (ZipOutputStream zip =
new ZipOutputStream(new FileOutputStream(zipFile))) {
renderFiles(inputFiles, zip);
File[] cssFiles = tmpdir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".css");
}
});
for (File css : cssFiles) {
zipFile(css, css.getName(), zip);
}
});
for (File css : cssFiles) {
zipFile(css, css.getName(), zip);
}
}
}
private void renderFiles(List<String> inputFiles, ZipOutputStream zip)
throws IOException {
Asciidoctor asciidoctor = JRubyAsciidoctor.create();
for (String inputFile : inputFiles) {
String outName = mapInFileToOutFile(inputFile, inExt, outExt);
File out = bazel ? new File(outName) : new File(tmpdir, outName);
if (!bazel) {
out.getParentFile().mkdirs();
}
File input = new File(inputFile);
Options options =
createOptions(basedir != null ? basedir : input.getParentFile(), out);
asciidoctor.renderFile(input, options);
if (zip != null) {
zipFile(out, outName, zip);
}
}
}
@ -171,11 +215,6 @@ public class AsciiDoctor {
zip.closeEntry();
}
private void renderInput(Options options, File inputFile) {
Asciidoctor asciidoctor = JRubyAsciidoctor.create();
asciidoctor.renderFile(inputFile, options);
}
public static void main(String[] args) {
try {
new AsciiDoctor().invoke(args);

322
tools/bzl/asciidoc.bzl Normal file
View File

@ -0,0 +1,322 @@
def documentation_attributes():
return [
"toc",
'newline="\\n"',
'asterisk="&#42;"',
'plus="&#43;"',
'caret="&#94;"',
'startsb="&#91;"',
'endsb="&#93;"',
'tilde="&#126;"',
"last-update-label!",
"source-highlighter=prettify",
"stylesheet=DEFAULT",
"linkcss=true",
"prettifydir=.",
# Just a placeholder, will be filled in asciidoctor java binary:
"revnumber=%s",
]
def _replace_macros_impl(ctx):
cmd = [
ctx.file._exe.path,
'--suffix', ctx.attr.suffix,
"-s", ctx.file.src.path,
"-o", ctx.outputs.out.path,
]
if ctx.attr.searchbox:
cmd.append('--searchbox')
else:
cmd.append('--no-searchbox')
ctx.action(
inputs = [ctx.file._exe, ctx.file.src],
outputs = [ctx.outputs.out],
command = cmd,
progress_message = "Replacing macros in %s" % ctx.file.src.short_path,
)
_replace_macros = rule(
implementation = _replace_macros_impl,
attrs = {
"_exe": attr.label(
default = Label("//Documentation:replace_macros.py"),
allow_single_file = True,
),
"src": attr.label(
mandatory = True,
allow_single_file = [".txt"],
),
"suffix": attr.string(mandatory = True),
"searchbox": attr.bool(default = True),
"out": attr.output(mandatory = True),
},
)
def _generate_asciidoc_args(ctx):
args = []
if ctx.attr.backend:
args.extend(["-b", ctx.attr.backend])
revnumber = False
for attribute in ctx.attr.attributes:
if attribute.startswith("revnumber="):
revnumber = True
else:
args.extend(["-a", attribute])
if revnumber:
args.extend([
"--revnumber-file", ctx.file.version.path,
])
for src in ctx.files.srcs:
args.append(src.path)
return args
def _invoke_replace_macros(name, src, suffix, searchbox):
fn = src
if fn.startswith(":"):
fn = src[1:]
_replace_macros(
name = "macros_%s_%s" % (name, fn),
src = src,
out = fn + suffix,
suffix = suffix,
searchbox = searchbox,
)
return ":" + fn + suffix, fn.replace(".txt", ".html")
def _asciidoc_impl(ctx):
args = [
"--bazel",
"--in-ext", ".txt" + ctx.attr.suffix,
"--out-ext", ".html",
]
args.extend(_generate_asciidoc_args(ctx))
ctx.action(
inputs = ctx.files.srcs + [ctx.executable._exe, ctx.file.version],
outputs = ctx.outputs.outs,
executable = ctx.executable._exe,
arguments = args,
progress_message = "Rendering asciidoctor files for %s" % ctx.label.name,
)
_asciidoc = rule(
implementation = _asciidoc_impl,
attrs = {
"_exe": attr.label(
default = Label("//lib/asciidoctor:asciidoc"),
allow_files = True,
executable = True,
),
"srcs": attr.label_list(mandatory = True, allow_files = True),
"version": attr.label(
default = Label("//:version.txt"),
allow_single_file = True,
),
"suffix": attr.string(mandatory = True),
"backend": attr.string(),
"attributes": attr.string_list(),
"outs": attr.output_list(mandatory = True),
},
)
def _genasciidoc_htmlonly(
name,
srcs = [],
attributes = [],
backend = None,
searchbox = True,
**kwargs):
SUFFIX = "." + name + "_macros"
new_srcs = []
outs = ["asciidoctor.css"]
for src in srcs:
new_src, html_name = _invoke_replace_macros(name, src, SUFFIX, searchbox)
new_srcs.append(new_src)
outs.append(html_name)
_asciidoc(
name = name + "_gen",
srcs = new_srcs,
suffix = SUFFIX,
backend = backend,
attributes = attributes,
outs = outs,
)
native.filegroup(
name = name,
data = outs,
**kwargs
)
def genasciidoc(
name,
srcs = [],
attributes = [],
backend = None,
searchbox = True,
resources = True,
**kwargs):
SUFFIX = "_htmlonly"
_genasciidoc_htmlonly(
name = name + SUFFIX if resources else name,
srcs = srcs,
attributes = attributes,
backend = backend,
searchbox = searchbox,
**kwargs
)
if resources:
htmlonly = ":" + name + SUFFIX
native.filegroup(
name = name,
srcs = [
htmlonly,
"//Documentation:resources",
],
**kwargs
)
def _asciidoc_html_zip_impl(ctx):
args = [
"--mktmp",
"-z", ctx.outputs.out.path,
"--in-ext", ".txt" + ctx.attr.suffix,
"--out-ext", ".html",
]
args.extend(_generate_asciidoc_args(ctx))
ctx.action(
inputs = ctx.files.srcs + [ctx.executable._exe, ctx.file.version],
outputs = [ctx.outputs.out],
executable = ctx.executable._exe,
arguments = args,
progress_message = "Rendering asciidoctor files for %s" % ctx.label.name,
)
_asciidoc_html_zip = rule(
implementation = _asciidoc_html_zip_impl,
attrs = {
"_exe": attr.label(
default = Label("//lib/asciidoctor:asciidoc"),
allow_files = True,
executable = True,
),
"srcs": attr.label_list(mandatory = True, allow_files = True),
"version": attr.label(
default = Label("//:version.txt"),
allow_single_file = True,
),
"suffix": attr.string(mandatory = True),
"backend": attr.string(),
"attributes": attr.string_list(),
},
outputs = {
"out": "%{name}.zip",
}
)
def _genasciidoc_htmlonly_zip(
name,
srcs = [],
attributes = [],
backend = None,
searchbox = True,
**kwargs):
SUFFIX = "." + name + "_expn"
new_srcs = []
for src in srcs:
new_src, _ = _invoke_replace_macros(name, src, SUFFIX, searchbox)
new_srcs.append(new_src)
_asciidoc_html_zip(
name = name,
srcs = new_srcs,
suffix = SUFFIX,
backend = backend,
attributes = attributes,
)
def _asciidoc_zip_impl(ctx):
tmpdir = ctx.outputs.out.path + "_tmpdir"
cmd = [
"p=$PWD",
"mkdir -p %s" % tmpdir,
"unzip -q %s -d %s/%s/" % (ctx.file.src.path, tmpdir, ctx.attr.directory),
]
for r in ctx.files.resources:
if r.path == r.short_path:
cmd.append("tar -cf- %s | tar -C %s -xf-" % (r.short_path, tmpdir))
else:
parent = r.path[:-len(r.short_path)]
cmd.append(
"tar -C %s -cf- %s | tar -C %s -xf-" % (parent, r.short_path, tmpdir))
cmd.extend([
"cd %s" % tmpdir,
"zip -qr $p/%s *" % ctx.outputs.out.path,
])
ctx.action(
inputs = [ctx.file.src] + ctx.files.resources,
outputs = [ctx.outputs.out],
command = " && ".join(cmd),
progress_message =
"Generating asciidoctor zip file %s" % ctx.outputs.out.short_path,
)
_asciidoc_zip = rule(
implementation = _asciidoc_zip_impl,
attrs = {
"src": attr.label(
mandatory = True,
allow_single_file = [".zip"],
),
"resources": attr.label_list(mandatory = True, allow_files = True),
"directory": attr.string(mandatory = True),
},
outputs = {
"out": "%{name}.zip",
}
)
def genasciidoc_zip(
name,
srcs = [],
attributes = [],
directory = None,
backend = None,
searchbox = True,
resources = True,
**kwargs):
SUFFIX = "_htmlonly"
_genasciidoc_htmlonly_zip(
name = name + SUFFIX if resources else name,
srcs = srcs,
attributes = attributes,
backend = backend,
searchbox = searchbox,
**kwargs
)
if resources:
htmlonly = ":" + name + SUFFIX
_asciidoc_zip(
name = name,
src = htmlonly,
resources = ["//Documentation:resources"],
directory = directory,
)

View File

@ -2,7 +2,7 @@
def normalize_target_name(target):
return target.replace("//", "").replace("/", "__").replace(":", "___")
def license_map(name, targets = [], opts = []):
def license_map(name, targets = [], opts = [], **kwargs):
"""Generate XML for all targets that depend directly on a LICENSE file"""
xmls = []
tools = [ "//tools/bzl:license-map.py", "//lib:all-licenses" ]
@ -28,7 +28,8 @@ def license_map(name, targets = [], opts = []):
name = "gen_license_txt_" + name,
cmd = "python $(location //tools/bzl:license-map.py) %s %s > $@" % (" ".join(opts), " ".join(xmls)),
outs = [ name + ".txt" ],
tools = tools
tools = tools,
**kwargs
)
def license_test(name, target):