Line data Source code
1 : // Copyright (C) 2011 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.pgm.rules;
16 :
17 : import com.google.gerrit.common.Nullable;
18 : import com.google.gerrit.common.Version;
19 : import com.google.gerrit.entities.RefNames;
20 : import com.google.gerrit.server.config.GerritServerConfig;
21 : import com.google.gerrit.server.config.SitePaths;
22 : import com.google.gerrit.server.util.time.TimeUtil;
23 : import com.google.inject.Inject;
24 : import com.google.inject.assistedinject.Assisted;
25 : import com.googlecode.prolog_cafe.compiler.Compiler;
26 : import com.googlecode.prolog_cafe.exceptions.CompileException;
27 : import java.io.File;
28 : import java.io.FileNotFoundException;
29 : import java.io.IOException;
30 : import java.io.InputStream;
31 : import java.io.OutputStream;
32 : import java.net.URL;
33 : import java.net.URLClassLoader;
34 : import java.nio.file.Files;
35 : import java.nio.file.Path;
36 : import java.util.ArrayList;
37 : import java.util.List;
38 : import java.util.Locale;
39 : import java.util.concurrent.Callable;
40 : import java.util.jar.Attributes;
41 : import java.util.jar.JarEntry;
42 : import java.util.jar.JarOutputStream;
43 : import java.util.jar.Manifest;
44 : import javax.tools.Diagnostic;
45 : import javax.tools.DiagnosticCollector;
46 : import javax.tools.JavaCompiler;
47 : import javax.tools.JavaFileObject;
48 : import javax.tools.StandardJavaFileManager;
49 : import javax.tools.ToolProvider;
50 : import org.eclipse.jgit.errors.MissingObjectException;
51 : import org.eclipse.jgit.lib.Config;
52 : import org.eclipse.jgit.lib.ObjectId;
53 : import org.eclipse.jgit.lib.Repository;
54 :
55 : /**
56 : * Helper class for Rulec: does the actual prolog -> java src -> class -> jar work Finds rules.pl in
57 : * refs/meta/config branch Creates rules-(sha1 of rules.pl).jar in (site-path)/cache/rules
58 : */
59 : public class PrologCompiler implements Callable<PrologCompiler.Status> {
60 : public interface Factory {
61 : PrologCompiler create(Repository git);
62 : }
63 :
64 0 : public enum Status {
65 0 : NO_RULES,
66 0 : COMPILED
67 : }
68 :
69 : private final Path ruleDir;
70 : private final Repository git;
71 :
72 : @Inject
73 : PrologCompiler(
74 0 : @GerritServerConfig Config config, SitePaths site, @Assisted Repository gitRepository) {
75 0 : Path cacheDir = site.resolve(config.getString("cache", null, "directory"));
76 0 : ruleDir = cacheDir != null ? cacheDir.resolve("rules") : null;
77 0 : git = gitRepository;
78 0 : }
79 :
80 : @Override
81 : public Status call() throws IOException, CompileException {
82 0 : ObjectId metaConfig = git.resolve(RefNames.REFS_CONFIG);
83 0 : if (metaConfig == null) {
84 0 : return Status.NO_RULES;
85 : }
86 :
87 0 : ObjectId rulesId = git.resolve(metaConfig.name() + ":rules.pl");
88 0 : if (rulesId == null) {
89 0 : return Status.NO_RULES;
90 : }
91 :
92 0 : if (ruleDir == null) {
93 0 : throw new CompileException("Caching not enabled");
94 : }
95 0 : Files.createDirectories(ruleDir);
96 :
97 0 : File tempDir = File.createTempFile("GerritCodeReview_", ".rulec");
98 0 : if (!tempDir.delete() || !tempDir.mkdir()) {
99 0 : throw new IOException("Cannot create " + tempDir);
100 : }
101 : try {
102 : // Try to make the directory accessible only by this process.
103 : // This may help to prevent leaking rule data to outsiders.
104 0 : tempDir.setReadable(true, true);
105 0 : tempDir.setWritable(true, true);
106 0 : tempDir.setExecutable(true, true);
107 :
108 0 : compileProlog(rulesId, tempDir);
109 0 : compileJava(tempDir);
110 :
111 0 : Path jarPath = ruleDir.resolve("rules-" + rulesId.getName() + ".jar");
112 0 : List<String> classFiles = getRelativePaths(tempDir, ".class");
113 0 : createJar(jarPath, classFiles, tempDir, metaConfig, rulesId);
114 :
115 0 : return Status.COMPILED;
116 : } finally {
117 0 : deleteAllFiles(tempDir);
118 : }
119 : }
120 :
121 : /** Creates a copy of rules.pl and compiles it into Java sources. */
122 : private void compileProlog(ObjectId prolog, File tempDir) throws IOException, CompileException {
123 0 : File tempRules = copyToTempFile(prolog, tempDir);
124 : try {
125 0 : Compiler comp = new Compiler();
126 0 : comp.prologToJavaSource(tempRules.getPath(), tempDir.getPath());
127 : } finally {
128 0 : tempRules.delete();
129 : }
130 0 : }
131 :
132 : private File copyToTempFile(ObjectId blobId, File tempDir)
133 : throws IOException, FileNotFoundException, MissingObjectException {
134 : // Any leak of tmp caused by this method failing will be cleaned
135 : // up by our caller when tempDir is recursively deleted.
136 0 : File tmp = File.createTempFile("rules", ".pl", tempDir);
137 0 : try (OutputStream out = Files.newOutputStream(tmp.toPath())) {
138 0 : git.open(blobId).copyTo(out);
139 : }
140 0 : return tmp;
141 : }
142 :
143 : /** Compile java src into java .class files */
144 : private void compileJava(File tempDir) throws IOException, CompileException {
145 0 : JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
146 0 : if (compiler == null) {
147 0 : throw new CompileException("JDK required (running inside of JRE)");
148 : }
149 :
150 0 : DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
151 0 : try (StandardJavaFileManager fileManager =
152 0 : compiler.getStandardFileManager(diagnostics, null, null)) {
153 0 : Iterable<? extends JavaFileObject> compilationUnits =
154 0 : fileManager.getJavaFileObjectsFromFiles(getAllFiles(tempDir, ".java"));
155 0 : ArrayList<String> options = new ArrayList<>();
156 0 : String classpath = getMyClasspath();
157 0 : if (classpath != null) {
158 0 : options.add("-classpath");
159 0 : options.add(classpath);
160 : }
161 0 : options.add("-d");
162 0 : options.add(tempDir.getPath());
163 0 : JavaCompiler.CompilationTask task =
164 0 : compiler.getTask(null, fileManager, diagnostics, options, null, compilationUnits);
165 0 : if (!task.call()) {
166 0 : Locale myLocale = Locale.getDefault();
167 0 : StringBuilder msg = new StringBuilder();
168 0 : msg.append("Cannot compile to Java bytecode:");
169 0 : for (Diagnostic<? extends JavaFileObject> err : diagnostics.getDiagnostics()) {
170 0 : msg.append('\n');
171 0 : msg.append(err.getKind());
172 0 : msg.append(": ");
173 0 : if (err.getSource() != null) {
174 0 : msg.append(err.getSource().getName());
175 : }
176 0 : msg.append(':');
177 0 : msg.append(err.getLineNumber());
178 0 : msg.append(": ");
179 0 : msg.append(err.getMessage(myLocale));
180 0 : }
181 0 : throw new CompileException(msg.toString());
182 : }
183 : }
184 0 : }
185 :
186 : @Nullable
187 : private String getMyClasspath() {
188 0 : StringBuilder cp = new StringBuilder();
189 0 : appendClasspath(cp, getClass().getClassLoader());
190 0 : return 0 < cp.length() ? cp.toString() : null;
191 : }
192 :
193 : private void appendClasspath(StringBuilder cp, ClassLoader classLoader) {
194 0 : if (classLoader.getParent() != null) {
195 0 : appendClasspath(cp, classLoader.getParent());
196 : }
197 0 : if (classLoader instanceof URLClassLoader) {
198 0 : for (URL url : ((URLClassLoader) classLoader).getURLs()) {
199 0 : if ("file".equals(url.getProtocol())) {
200 0 : if (0 < cp.length()) {
201 0 : cp.append(File.pathSeparatorChar);
202 : }
203 0 : cp.append(url.getPath());
204 : }
205 : }
206 : }
207 0 : }
208 :
209 : /** Takes compiled prolog .class files, puts them into the jar file. */
210 : private void createJar(
211 : Path archiveFile, List<String> toBeJared, File tempDir, ObjectId metaConfig, ObjectId rulesId)
212 : throws IOException {
213 0 : long now = TimeUtil.nowMs();
214 0 : Manifest mf = new Manifest();
215 0 : mf.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
216 0 : mf.getMainAttributes().putValue("Built-by", "Gerrit Code Review " + Version.getVersion());
217 0 : if (git.getDirectory() != null) {
218 0 : mf.getMainAttributes().putValue("Source-Repository", git.getDirectory().getPath());
219 : }
220 0 : mf.getMainAttributes().putValue("Source-Commit", metaConfig.name());
221 0 : mf.getMainAttributes().putValue("Source-Blob", rulesId.name());
222 :
223 0 : Path tmpjar = Files.createTempFile(archiveFile.getParent(), ".rulec_", ".jar");
224 0 : try (OutputStream stream = Files.newOutputStream(tmpjar);
225 0 : JarOutputStream out = new JarOutputStream(stream, mf)) {
226 0 : byte[] buffer = new byte[10240];
227 : // TODO: fixify this loop
228 0 : for (String path : toBeJared) {
229 0 : JarEntry jarAdd = new JarEntry(path);
230 0 : File f = new File(tempDir, path);
231 0 : jarAdd.setTime(now);
232 0 : out.putNextEntry(jarAdd);
233 0 : if (f.isFile()) {
234 0 : try (InputStream in = Files.newInputStream(f.toPath())) {
235 : while (true) {
236 0 : int nRead = in.read(buffer, 0, buffer.length);
237 0 : if (nRead <= 0) {
238 0 : break;
239 : }
240 0 : out.write(buffer, 0, nRead);
241 0 : }
242 : }
243 : }
244 0 : out.closeEntry();
245 0 : }
246 : }
247 :
248 : try {
249 0 : Files.move(tmpjar, archiveFile);
250 0 : } catch (IOException e) {
251 0 : throw new IOException("Cannot replace " + archiveFile, e);
252 0 : }
253 0 : }
254 :
255 : private List<File> getAllFiles(File dir, String extension) throws IOException {
256 0 : ArrayList<File> fileList = new ArrayList<>();
257 0 : getAllFiles(dir, extension, fileList);
258 0 : return fileList;
259 : }
260 :
261 : private void getAllFiles(File dir, String extension, List<File> fileList) throws IOException {
262 0 : for (File f : listFiles(dir)) {
263 0 : if (f.getName().endsWith(extension)) {
264 0 : fileList.add(f);
265 : }
266 0 : if (f.isDirectory()) {
267 0 : getAllFiles(f, extension, fileList);
268 : }
269 : }
270 0 : }
271 :
272 : private List<String> getRelativePaths(File dir, String extension) throws IOException {
273 0 : ArrayList<String> pathList = new ArrayList<>();
274 0 : getRelativePaths(dir, extension, "", pathList);
275 0 : return pathList;
276 : }
277 :
278 : private static void getRelativePaths(
279 : File dir, String extension, String path, List<String> pathList) throws IOException {
280 0 : for (File f : listFiles(dir)) {
281 0 : if (f.getName().endsWith(extension)) {
282 0 : pathList.add(path + f.getName());
283 : }
284 0 : if (f.isDirectory()) {
285 0 : getRelativePaths(f, extension, path + f.getName() + "/", pathList);
286 : }
287 : }
288 0 : }
289 :
290 : private static void deleteAllFiles(File dir) throws IOException {
291 0 : for (File f : listFiles(dir)) {
292 0 : if (f.isDirectory()) {
293 0 : deleteAllFiles(f);
294 : } else {
295 0 : f.delete();
296 : }
297 : }
298 0 : dir.delete();
299 0 : }
300 :
301 : private static File[] listFiles(File dir) throws IOException {
302 0 : File[] files = dir.listFiles();
303 0 : if (files == null) {
304 0 : throw new IOException("Failed to list directory: " + dir);
305 : }
306 0 : return files;
307 : }
308 : }
|