| // 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 static com.google.gerrit.server.project.ProjectConfig.RULES_PL_FILE; | |
| import com.google.gerrit.common.Nullable; | |
| import com.google.gerrit.common.Version; | |
| import com.google.gerrit.entities.RefNames; | |
| import com.google.gerrit.server.config.GerritServerConfig; | |
| import com.google.gerrit.server.config.SitePaths; | |
| import com.google.gerrit.server.util.time.TimeUtil; | |
| 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_FILE); | |
| 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()); | |
| } | |
| } | |
| } | |
| @Nullable | |
| 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; | |
| } | |
| } |