Allow updated jagidir modules to be formally disposed.
[jagi.git] / src / jagi / fs / Compiler.java
... / ...
CommitLineData
1package jagi.fs;
2
3import jagi.*;
4import java.util.*;
5import java.util.regex.*;
6import java.util.logging.*;
7import java.nio.file.*;
8import java.nio.file.attribute.*;
9import java.io.*;
10import java.net.*;
11
12public class Compiler {
13 private static final Logger log = Logger.getLogger("jagi-fs");
14 private final Map<Path, File> files = new HashMap<>();
15 private final Map<Path, ClassLoader> libs = new HashMap<>();
16 private final Collection<Path> searchpath = new ArrayList<>();
17
18 public Compiler() {
19 String syspath = System.getenv("PATH");
20 if(syspath != null) {
21 String sep = java.io.File.pathSeparator;
22 int p1 = 0, p2 = syspath.indexOf(sep);
23 do {
24 String el;
25 if(p2 >= 0) {
26 el = syspath.substring(p1, p2);
27 p1 = p2 + sep.length();
28 p2 = syspath.indexOf(sep, p1);
29 } else {
30 el =syspath.substring(p1);
31 }
32 try {
33 Path p = Paths.get(el);
34 if(p.getParent() != null)
35 searchpath.add(p.getParent().resolve("share").resolve("java"));
36 } catch(InvalidPathException e) {
37 continue;
38 }
39 } while(p2 >= 0);
40 }
41 String proppath = System.getProperty("jagi.search-path");
42 if(proppath != null) {
43 for(String el : proppath.split(":")) {
44 try {
45 searchpath.add(Paths.get(el));
46 } catch(InvalidPathException e) {
47 continue;
48 }
49 }
50 }
51 }
52
53 public static class ClassOutput {
54 public final String name;
55 public final ByteArrayOutputStream buf = new ByteArrayOutputStream();
56
57 public ClassOutput(String name) {
58 this.name = name;
59 }
60 }
61
62 public static class CompilationException extends RuntimeException {
63 public final Path file;
64 private final List<? extends Object> messages;
65
66 public CompilationException(Path file, List<? extends Object> messages) {
67 this.file = file;
68 this.messages = messages;
69 }
70
71 public String getMessage() {
72 return(file + ": compilation failed");
73 }
74
75 public String messages() {
76 StringBuilder buf = new StringBuilder();
77 for(Object msg : messages)
78 buf.append(msg.toString() + "\n");
79 return(buf.toString());
80 }
81
82 public void printStackTrace(PrintStream out) {
83 out.print(messages());
84 super.printStackTrace(out);
85 }
86 }
87
88 public static class Compilation implements AutoCloseable {
89 public final Path dir, srcdir, outdir;
90 private final List<Path> infiles = new ArrayList<>();
91 private final List<Path> classpath = new ArrayList<>();
92 private List<String> output = null;
93
94 public Compilation() throws IOException {
95 dir = Files.createTempDirectory("javac");
96 srcdir = dir.resolve("src");
97 outdir = dir.resolve("out");
98 Files.createDirectory(srcdir);
99 Files.createDirectory(outdir);
100 }
101
102 public void add(Path p) {
103 infiles.add(p);
104 }
105
106 public void classpath(Path p) {
107 classpath.add(p);
108 }
109
110 public boolean compile() throws IOException {
111 List<String> args = new ArrayList<>();
112 args.add("javac");
113 args.add("-d");
114 args.add(outdir.toString());
115 if(!classpath.isEmpty()) {
116 StringBuilder buf = new StringBuilder();
117 for(Path cp : classpath) {
118 if(buf.length() > 0)
119 buf.append(":");
120 buf.append(cp.toString());
121 }
122 args.add("-cp");
123 args.add(buf.toString());
124 }
125 for(Path p : infiles)
126 args.add(p.toString());
127 ProcessBuilder cmd = new ProcessBuilder(args);
128 cmd.redirectErrorStream(true);
129 cmd.redirectOutput(ProcessBuilder.Redirect.PIPE);
130 Process proc = cmd.start();
131 List<String> output = new ArrayList<>();
132 BufferedReader fp = new BufferedReader(new InputStreamReader(proc.getInputStream(), Utils.UTF8));
133 while(true) {
134 String ln = fp.readLine();
135 if(ln == null)
136 break;
137 output.add(ln);
138 }
139 int status;
140 try {
141 status = proc.waitFor();
142 } catch(InterruptedException e) {
143 Thread.currentThread().interrupt();
144 throw(new IOException("compilation interrupted"));
145 }
146 this.output = output;
147 return(status == 0);
148 }
149
150 public List<String> output() {
151 if(output == null)
152 throw(new IllegalStateException());
153 return(output);
154 }
155
156 public Collection<ClassOutput> classes() throws IOException {
157 Collection<ClassOutput> ret = new ArrayList<>();
158 for(Path p : (Iterable<Path>)Files.walk(outdir)::iterator) {
159 Path rel = outdir.relativize(p);
160 String fn = rel.getName(rel.getNameCount() - 1).toString();
161 if(!Files.isRegularFile(p) || !fn.endsWith(".class"))
162 continue;
163 StringBuilder clnm = new StringBuilder();
164 for(int i = 0; i < rel.getNameCount() - 1; i++) {
165 clnm.append(rel.getName(i));
166 clnm.append('.');
167 }
168 clnm.append(fn.substring(0, fn.length() - 6));
169 ClassOutput cls = new ClassOutput(clnm.toString());
170 Files.copy(p, cls.buf);
171 ret.add(cls);
172 }
173 return(ret);
174 }
175
176 private static void remove(Path p) throws IOException {
177 if(Files.isDirectory(p)) {
178 for(Path ent : (Iterable<Path>)Files.list(p)::iterator)
179 remove(ent);
180 }
181 Files.delete(p);
182 }
183
184 public void close() throws IOException {
185 remove(dir);
186 }
187 }
188
189 public static class BufferedClassLoader extends ClassLoader {
190 public final Map<String, byte[]> contents;
191
192 public BufferedClassLoader(ClassLoader parent, Collection<ClassOutput> contents) {
193 super(parent);
194 this.contents = new HashMap<>();
195 for(ClassOutput clc : contents)
196 this.contents.put(clc.name, clc.buf.toByteArray());
197 }
198
199 public Class<?> findClass(String name) throws ClassNotFoundException {
200 byte[] c = contents.get(name);
201 if(c == null)
202 throw(new ClassNotFoundException(name));
203 return(defineClass(name, c, 0, c.length));
204 }
205 }
206
207 public static class LibClassLoader extends ClassLoader {
208 private final ClassLoader[] classpath;
209
210 public LibClassLoader(ClassLoader parent, Collection<ClassLoader> classpath) {
211 super(parent);
212 this.classpath = classpath.toArray(new ClassLoader[0]);
213 }
214
215 public Class<?> findClass(String name) throws ClassNotFoundException {
216 for(ClassLoader lib : classpath) {
217 try {
218 return(lib.loadClass(name));
219 } catch(ClassNotFoundException e) {}
220 }
221 throw(new ClassNotFoundException("Could not find " + name + " in any of " + Arrays.asList(classpath).toString()));
222 }
223 }
224
225 public ClassLoader libloader(Path p) {
226 synchronized(libs) {
227 ClassLoader ret = libs.get(p);
228 if(ret == null) {
229 try {
230 libs.put(p, ret = new URLClassLoader(new URL[] {p.toUri().toURL()}));
231 } catch(MalformedURLException e) {
232 throw(new RuntimeException(e));
233 }
234 }
235 return(ret);
236 }
237 }
238
239 private Path findlib(String nm) {
240 try {
241 Path p = Paths.get(nm);
242 if(Files.isRegularFile(p))
243 return(p);
244 } catch(InvalidPathException e) {
245 }
246 for(Path dir : searchpath) {
247 Path jar = dir.resolve(nm + ".jar");
248 if(Files.isRegularFile(jar))
249 return(jar);
250 }
251 return(null);
252 }
253
254 private static final Pattern classpat = Pattern.compile("^((public|abstract)\\s+)*(class|interface)\\s+(\\S+)");
255 private static final Pattern libpat = Pattern.compile("\\$use\\s*:\\s*(\\S+)");
256 public class Module {
257 public final Path file;
258 public final ClassLoader code;
259 public final Collection<Path> classpath = new ArrayList<>();
260
261 public Module(Path file) throws IOException {
262 this.file = file;
263 try(Compilation c = new Compilation()) {
264 split(c);
265 for(Path cp : classpath)
266 c.classpath(cp);
267 if(!c.compile())
268 throw(new CompilationException(file, c.output()));
269 ClassLoader parent = Compiler.class.getClassLoader();
270 if(!classpath.isEmpty()) {
271 Collection<ClassLoader> libs = new ArrayList<>();
272 for(Path cp : classpath)
273 libs.add(libloader(cp));
274 parent = new LibClassLoader(parent, libs);
275 }
276 code = new BufferedClassLoader(parent, c.classes());
277 }
278 }
279
280 public void split(Compilation c) throws IOException {
281 StringBuilder head = new StringBuilder();
282 BufferedWriter cur = null;
283 try(BufferedReader fp = Files.newBufferedReader(file)) {
284 for(String ln = fp.readLine(); ln != null; ln = fp.readLine()) {
285 Matcher m = libpat.matcher(ln);
286 if(m.find()) {
287 Path lib = findlib(m.group(1));
288 if(lib == null)
289 throw(new CompilationException(file, Arrays.asList("no such library: " + m.group(1))));
290 classpath.add(lib);
291 }
292 m = classpat.matcher(ln);
293 if(m.find()) {
294 String clnm = m.group(4);
295 Path sp = c.srcdir.resolve(clnm + ".java");
296 c.add(sp);
297 if(cur != null)
298 cur.close();
299 cur = Files.newBufferedWriter(sp);
300 cur.append(head);
301 }
302 if(cur != null) {
303 cur.append(ln); cur.append('\n');
304 } else {
305 head.append(ln); head.append('\n');
306 }
307 }
308 } finally {
309 if(cur != null)
310 cur.close();
311 }
312 }
313 }
314
315 public class File {
316 public final Path name;
317 private FileTime mtime = null;
318 private Module mod = null;
319
320 private File(Path name) {
321 this.name = name;
322 }
323
324 public void update() throws IOException {
325 synchronized(this) {
326 FileTime mtime = Files.getLastModifiedTime(name);
327 if((this.mtime == null) || (this.mtime.compareTo(mtime) < 0)) {
328 Module pmod = this.mod;
329 this.mod = new Module(name);
330 this.mtime = mtime;
331 if(pmod instanceof AutoCloseable) {
332 try {
333 ((AutoCloseable)pmod).close();
334 } catch(Exception e) {
335 log.log(Level.WARNING, String.format("Error when disposing updated module %s", pmod.file), e);
336 }
337 }
338 }
339 }
340 }
341
342 public Module mod() {
343 if(mod == null)
344 throw(new RuntimeException("file has not yet been updated"));
345 return(mod);
346 }
347 }
348
349 public File file(Path name) {
350 synchronized(files) {
351 File ret = files.get(name);
352 if(ret == null)
353 files.put(name, ret = new File(name));
354 return(ret);
355 }
356 }
357
358 private static Compiler global = null;
359 public static Compiler get() {
360 if(global == null) {
361 synchronized(Compiler.class) {
362 if(global == null)
363 global = new Compiler();
364 }
365 }
366 return(global);
367 }
368}