// 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.rules; import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.Version; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.SitePaths; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; import com.googlecode.prolog_cafe.compiler.Compiler; import com.googlecode.prolog_cafe.exceptions.CompileException; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; 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; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; /** * 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 enum Status { NO_RULES, COMPILED } private final Path ruleDir; private final Repository git; @Inject PrologCompiler( @GerritServerConfig Config config, SitePaths site, @Assisted Repository gitRepository) { Path cacheDir = site.resolve(config.getString("cache", null, "directory")); ruleDir = cacheDir != null ? cacheDir.resolve("rules") : null; git = gitRepository; } @Override public Status call() throws IOException, CompileException { ObjectId metaConfig = git.resolve(RefNames.REFS_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"); } Files.createDirectories(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); Path jarPath = ruleDir.resolve("rules-" + rulesId.getName() + ".jar"); List<String> classFiles = getRelativePaths(tempDir, ".class"); createJar(jarPath, 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); try (OutputStream out = Files.newOutputStream(tmp.toPath())) { git.open(blobId).copyTo(out); } 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<>(); try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null)) { Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(getAllFiles(tempDir, ".java")); ArrayList<String> options = new ArrayList<>(); 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()); } } } 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( Path archiveFile, List<String> toBeJared, File tempDir, ObjectId metaConfig, ObjectId rulesId) throws IOException { long now = TimeUtil.nowMs(); 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()); Path tmpjar = Files.createTempFile(archiveFile.getParent(), ".rulec_", ".jar"); try (OutputStream stream = Files.newOutputStream(tmpjar); JarOutputStream out = new JarOutputStream(stream, mf)) { byte[] buffer = new byte[10240]; // TODO: fixify this loop for (String path : toBeJared) { JarEntry jarAdd = new JarEntry(path); File f = new File(tempDir, path); jarAdd.setTime(now); out.putNextEntry(jarAdd); if (f.isFile()) { try (InputStream in = Files.newInputStream(f.toPath())) { while (true) { int nRead = in.read(buffer, 0, buffer.length); if (nRead <= 0) { break; } out.write(buffer, 0, nRead); } } } out.closeEntry(); } } try { Files.move(tmpjar, archiveFile); } catch (IOException e) { throw new IOException("Cannot replace " + archiveFile, e); } } private List<File> getAllFiles(File dir, String extension) throws IOException { ArrayList<File> fileList = new ArrayList<>(); getAllFiles(dir, extension, fileList); return fileList; } private void getAllFiles(File dir, String extension, List<File> fileList) throws IOException { for (File f : listFiles(dir)) { if (f.getName().endsWith(extension)) { fileList.add(f); } if (f.isDirectory()) { getAllFiles(f, extension, fileList); } } } private List<String> getRelativePaths(File dir, String extension) throws IOException { ArrayList<String> pathList = new ArrayList<>(); getRelativePaths(dir, extension, "", pathList); return pathList; } private static void getRelativePaths( File dir, String extension, String path, List<String> pathList) throws IOException { for (File f : listFiles(dir)) { if (f.getName().endsWith(extension)) { pathList.add(path + f.getName()); } if (f.isDirectory()) { getRelativePaths(f, extension, path + f.getName() + "/", pathList); } } } private static void deleteAllFiles(File dir) throws IOException { for (File f : listFiles(dir)) { if (f.isDirectory()) { deleteAllFiles(f); } else { f.delete(); } } dir.delete(); } private static File[] listFiles(File dir) throws IOException { File[] files = dir.listFiles(); if (files == null) { throw new IOException("Failed to list directory: " + dir); } return files; } }