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