Cmd line tool rulec to compile jar from prolog

Rulec takes rules.pl from the refs/meta/config branch and creates a
jar file named rules-(sha1 of rules.pl).jar in (sitepath)/cache/rules.
Generates temporary prolog, java src, and class files which are deleted
afterwards.

Change-Id: I385bce45c2261514683357c7a1b0fd9706d1c408
This commit is contained in:
Jason Tsay
2011-06-15 15:29:43 -07:00
committed by Shawn O. Pearce
parent 1b8bbb0f78
commit fa43ad2e63
5 changed files with 485 additions and 1 deletions

View File

@@ -24,6 +24,9 @@ link:pgm-ExportReviewNotes.html[ExportReviewNotes]::
link:pgm-ScanTrackingIds.html[ScanTrackingIds]::
Rescan all changes after configuring trackingids.
link:pgm-rulec.html[rulec]::
Compiles prolog rules into jar files
version::
Display the release version of Gerrit Code Review.

View File

@@ -0,0 +1,48 @@
rulec
=====
NAME
----
rulec - Compiles prolog rules into jar files
SYNOPSIS
--------
[verse]
'java' -jar gerrit.war 'rulec' -d <SITE_PATH> --name <PROJECT_NAME>
DESCRIPTION
-----------
Looks for a prolog rule file named rules.pl in the repository in the
refs/meta/config branch. If it exists, creates a jar file named
rules-`$sha1_of_rules.pl`.jar in `$site_path`/cache/rules. Caching needs
to be enabled on the server.
OPTIONS
-------
-d::
--site-path::
Location of the gerrit.config file, and all other per-site
configuration data, supporting libaries and log files.
--name::
Number of threads to perform the scan work with. Defaults to
twice the number of CPUs available.
CONTEXT
-------
This command can only be run on a server which has direct
connectivity to the metadata database, and local access to the
managed Git repositories.
EXAMPLES
--------
To create a jar file for test/project:
====
$ java -jar gerrit.war rulec -d site_path --name test/project
====
GERRIT
------
Part of link:index.html[Gerrit Code Review]

View File

@@ -0,0 +1,115 @@
// Copyright (C) 2011 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.
package com.google.gerrit.pgm;
import static com.google.gerrit.server.schema.DataSourceProvider.Context.SINGLE_USER;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.pgm.util.SiteProgram;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.rules.PrologCompiler;
import com.google.gerrit.server.config.FactoryModule;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.googlecode.prolog_cafe.compiler.CompileException;
import org.eclipse.jgit.lib.Repository;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
/**
* Gets rules.pl at refs/meta/config and compiles into jar file called
* rules-(sha1 of rules.pl).jar in (site-path)/cache/rules
*/
public class Rulec extends SiteProgram {
@Option(name = "--all", usage = "recompile all rules")
private boolean all;
@Option(name = "--quiet", usage = "supress some messsages")
private boolean quiet;
@Argument(index = 0, multiValued = true, metaVar = "PROJECT", usage = "project to compile rules for")
private List<String> projectNames = new ArrayList<String>();
private Injector dbInjector;
private final LifecycleManager manager = new LifecycleManager();
@Inject
private GitRepositoryManager gitManager;
@Inject
private PrologCompiler.Factory jarFactory;
@Override
public int run() throws Exception {
dbInjector = createDbInjector(SINGLE_USER);
manager.add(dbInjector);
manager.start();
dbInjector.createChildInjector(new FactoryModule() {
@Override
protected void configure() {
factory(PrologCompiler.Factory.class);
}
}).injectMembers(this);
LinkedHashSet<Project.NameKey> names = new LinkedHashSet<Project.NameKey>();
for (String name : projectNames) {
names.add(new Project.NameKey(name));
}
if (all) {
names.addAll(gitManager.list());
}
boolean error = false;
for (Project.NameKey project : names) {
Repository git = gitManager.openRepository(project);
try {
switch (jarFactory.create(git).call()) {
case NO_RULES:
if (!all || projectNames.contains(project.get())) {
System.err.println("error: No rules.pl in " + project.get());
error = true;
}
break;
case COMPILED:
if (!quiet) {
System.out.format("Compiled %-60s ... SUCCESS", project.get());
System.out.println();
}
break;
}
} catch (CompileException err) {
if (showStackTrace) {
err.printStackTrace();
} else {
System.err.println("fatal: " + err.getMessage());
}
error = true;
} finally {
git.close();
}
}
return !error ? 0 : 1;
}
}

View File

@@ -32,7 +32,7 @@ public abstract class AbstractProgram {
private boolean running = true;
@Option(name = "--show-stack-trace", usage = "display stack trace on failure")
private boolean showStackTrace;
protected boolean showStackTrace;
@Option(name = "--help", usage = "display this help text", aliases = {"-h"})
private boolean help;

View File

@@ -0,0 +1,318 @@
// Copyright (C) 2011 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.
package com.google.gerrit.rules;
import com.google.gerrit.common.Version;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.googlecode.prolog_cafe.compiler.CompileException;
import com.googlecode.prolog_cafe.compiler.Compiler;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Callable;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
/**
* Helper class for Rulec: does the actual prolog -> java src -> class -> jar work
* Finds rules.pl in refs/meta/config branch
* Creates rules-(sha1 of rules.pl).jar in (site-path)/cache/rules
*/
public class PrologCompiler implements Callable<PrologCompiler.Status> {
public interface Factory {
PrologCompiler create(Repository git);
}
public static enum Status {
NO_RULES, COMPILED;
}
private final File ruleDir;
private final Repository git;
@Inject
PrologCompiler(@GerritServerConfig Config config, SitePaths site,
@Assisted Repository gitRepository) {
File cacheDir = site.resolve(config.getString("cache", null, "directory"));
ruleDir = cacheDir != null ? new File(cacheDir, "rules") : null;
git = gitRepository;
}
public Status call() throws IOException, CompileException {
ObjectId metaConfig = git.resolve(GitRepositoryManager.REF_CONFIG);
if (metaConfig == null) {
return Status.NO_RULES;
}
ObjectId rulesId = git.resolve(metaConfig.name() + ":rules.pl");
if (rulesId == null) {
return Status.NO_RULES;
}
if (ruleDir == null) {
throw new CompileException("Caching not enabled");
}
if (!ruleDir.isDirectory() && !ruleDir.mkdir()) {
throw new IOException("Cannot create " + ruleDir);
}
File tempDir = File.createTempFile("GerritCodeReview_", ".rulec");
if (!tempDir.delete() || !tempDir.mkdir()) {
throw new IOException("Cannot create " + tempDir);
}
try {
// Try to make the directory accessible only by this process.
// This may help to prevent leaking rule data to outsiders.
tempDir.setReadable(true, true);
tempDir.setWritable(true, true);
tempDir.setExecutable(true, true);
compileProlog(rulesId, tempDir);
compileJava(tempDir);
File jarFile = new File(ruleDir, "rules-" + rulesId.getName() + ".jar");
List<String> classFiles = getRelativePaths(tempDir, ".class");
createJar(jarFile, classFiles, tempDir, metaConfig, rulesId);
return Status.COMPILED;
} finally {
deleteAllFiles(tempDir);
}
}
/** Creates a copy of rules.pl and compiles it into Java sources. */
private void compileProlog(ObjectId prolog, File tempDir)
throws IOException, CompileException {
File tempRules = copyToTempFile(prolog, tempDir);
try {
Compiler comp = new Compiler();
comp.prologToJavaSource(tempRules.getPath(), tempDir.getPath());
} finally {
tempRules.delete();
}
}
private File copyToTempFile(ObjectId blobId, File tempDir)
throws IOException, FileNotFoundException, MissingObjectException {
// Any leak of tmp caused by this method failing will be cleaned
// up by our caller when tempDir is recursively deleted.
File tmp = File.createTempFile("rules", ".pl", tempDir);
FileOutputStream out = new FileOutputStream(tmp);
try {
git.open(blobId).copyTo(out);
} finally {
out.close();
}
return tmp;
}
/** Compile java src into java .class files */
private void compileJava(File tempDir) throws IOException, CompileException {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
throw new CompileException("JDK required (running inside of JRE)");
}
DiagnosticCollector<JavaFileObject> diagnostics =
new DiagnosticCollector<JavaFileObject>();
StandardJavaFileManager fileManager =
compiler.getStandardFileManager(diagnostics, null, null);
try {
Iterable<? extends JavaFileObject> compilationUnits = fileManager
.getJavaFileObjectsFromFiles(getAllFiles(tempDir, ".java"));
ArrayList<String> options = new ArrayList<String>();
String classpath = getMyClasspath();
if (classpath != null) {
options.add("-classpath");
options.add(classpath);
}
options.add("-d");
options.add(tempDir.getPath());
JavaCompiler.CompilationTask task = compiler.getTask(
null,
fileManager,
diagnostics,
options,
null,
compilationUnits);
if (!task.call()) {
Locale myLocale = Locale.getDefault();
StringBuilder msg = new StringBuilder();
msg.append("Cannot compile to Java bytecode:");
for (Diagnostic<? extends JavaFileObject> err : diagnostics.getDiagnostics()) {
msg.append('\n');
msg.append(err.getKind());
msg.append(": ");
if (err.getSource() != null) {
msg.append(err.getSource().getName());
}
msg.append(':');
msg.append(err.getLineNumber());
msg.append(": ");
msg.append(err.getMessage(myLocale));
}
throw new CompileException(msg.toString());
}
} finally {
fileManager.close();
}
}
private String getMyClasspath() {
StringBuilder cp = new StringBuilder();
appendClasspath(cp, getClass().getClassLoader());
return 0 < cp.length() ? cp.toString() : null;
}
private void appendClasspath(StringBuilder cp, ClassLoader classLoader) {
if (classLoader.getParent() != null) {
appendClasspath(cp, classLoader.getParent());
}
if (classLoader instanceof URLClassLoader) {
for (URL url : ((URLClassLoader) classLoader).getURLs()) {
if ("file".equals(url.getProtocol())) {
if (0 < cp.length()) {
cp.append(File.pathSeparatorChar);
}
cp.append(url.getPath());
}
}
}
}
/** Takes compiled prolog .class files, puts them into the jar file. */
private void createJar(File archiveFile, List<String> toBeJared,
File tempDir, ObjectId metaConfig, ObjectId rulesId) throws IOException {
long now = System.currentTimeMillis();
File tmpjar = File.createTempFile(".rulec_", ".jar", archiveFile.getParentFile());
try {
Manifest mf = new Manifest();
mf.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
mf.getMainAttributes().putValue("Built-by", "Gerrit Code Review " + Version.getVersion());
if (git.getDirectory() != null) {
mf.getMainAttributes().putValue("Source-Repository", git.getDirectory().getPath());
}
mf.getMainAttributes().putValue("Source-Commit", metaConfig.name());
mf.getMainAttributes().putValue("Source-Blob", rulesId.name());
FileOutputStream stream = new FileOutputStream(tmpjar);
JarOutputStream out = new JarOutputStream(stream, mf);
byte buffer[] = new byte[10240];
try {
for (String path : toBeJared) {
JarEntry jarAdd = new JarEntry(path);
File f = new File(tempDir, path);
jarAdd.setTime(now);
out.putNextEntry(jarAdd);
if (f.isFile()) {
FileInputStream in = new FileInputStream(f);
try {
while (true) {
int nRead = in.read(buffer, 0, buffer.length);
if (nRead <= 0) {
break;
}
out.write(buffer, 0, nRead);
}
} finally {
in.close();
}
}
out.closeEntry();
}
} finally {
out.close();
}
if (!tmpjar.renameTo(archiveFile)) {
throw new IOException("Cannot replace " + archiveFile);
}
} finally {
tmpjar.delete();
}
}
private List<File> getAllFiles(File dir, String extension) {
ArrayList<File> fileList = new ArrayList<File>();
getAllFiles(dir, extension, fileList);
return fileList;
}
private void getAllFiles(File dir, String extension, List<File> fileList) {
for (File f : dir.listFiles()) {
if (f.getName().endsWith(extension)) {
fileList.add(f);
}
if (f.isDirectory()) {
getAllFiles(f, extension, fileList);
}
}
}
private List<String> getRelativePaths(File dir, String extension) {
ArrayList<String> pathList = new ArrayList<String>();
getRelativePaths(dir, extension, "", pathList);
return pathList;
}
private void getRelativePaths(File dir, String extension, String path, List<String> pathList) {
for (File f : dir.listFiles()) {
if (f.getName().endsWith(extension)) {
pathList.add(path + f.getName());
}
if (f.isDirectory()) {
getRelativePaths(f, extension, path + f.getName() + "/", pathList);
}
}
}
private void deleteAllFiles(File dir) {
for (File f : dir.listFiles()) {
if (f.isDirectory()) {
deleteAllFiles(f);
} else {
f.delete();
}
}
dir.delete();
}
}