diff --git a/BUILD b/BUILD index 7ae3589612..4fa30f2eee 100644 --- a/BUILD +++ b/BUILD @@ -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']) diff --git a/Documentation/BUILD b/Documentation/BUILD index c2acc9c2dc..0542b5d58d 100644 --- a/Documentation/BUILD +++ b/Documentation/BUILD @@ -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"], +) diff --git a/gerrit-prettify/BUILD b/gerrit-prettify/BUILD index 063feee411..b8d4dd603c 100644 --- a/gerrit-prettify/BUILD +++ b/gerrit-prettify/BUILD @@ -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', +]) diff --git a/lib/asciidoctor/BUILD b/lib/asciidoctor/BUILD index 4b4e9581c0..d1b98f82f2 100644 --- a/lib/asciidoctor/BUILD +++ b/lib/asciidoctor/BUILD @@ -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"], diff --git a/lib/asciidoctor/java/AsciiDoctor.java b/lib/asciidoctor/java/AsciiDoctor.java index 8e18feb137..c66c4aafcd 100644 --- a/lib/asciidoctor/java/AsciiDoctor.java +++ b/lib/asciidoctor/java/AsciiDoctor.java @@ -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 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 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 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); diff --git a/tools/bzl/asciidoc.bzl b/tools/bzl/asciidoc.bzl new file mode 100644 index 0000000000..2d03716330 --- /dev/null +++ b/tools/bzl/asciidoc.bzl @@ -0,0 +1,322 @@ +def documentation_attributes(): + return [ + "toc", + 'newline="\\n"', + 'asterisk="*"', + 'plus="+"', + 'caret="^"', + 'startsb="["', + 'endsb="]"', + 'tilde="~"', + "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, + ) diff --git a/tools/bzl/license.bzl b/tools/bzl/license.bzl index 37cc70c6b8..60ee60b948 100644 --- a/tools/bzl/license.bzl +++ b/tools/bzl/license.bzl @@ -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):