Allow jagidir modules to include other compiled modules.
[jagi.git] / src / jagi / fs / Compiler.java
1 package jagi.fs;
2
3 import jagi.*;
4 import java.util.*;
5 import java.util.regex.*;
6 import java.util.logging.*;
7 import java.nio.file.*;
8 import java.nio.file.attribute.*;
9 import java.io.*;
10 import java.net.*;
11
12 public class Compiler {
13     private static final Logger log = Logger.getLogger("jagi-fs");
14     private static final Path cwd = Paths.get("").toAbsolutePath();
15     private final Map<Path, File> files = new HashMap<>();
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     }
53
54     public static class ClassOutput {
55         public final String name;
56         public final ByteArrayOutputStream buf = new ByteArrayOutputStream();
57
58         public ClassOutput(String name) {
59             this.name = name;
60         }
61     }
62
63     public static class CompilationException extends RuntimeException {
64         public final Path file;
65         private final List<? extends Object> messages;
66
67         public CompilationException(Path file, List<? extends Object> messages) {
68             this.file = file;
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 {
90         public final Path dir, srcdir, outdir, libdir;
91         private final List<Path> infiles = new ArrayList<>();
92         private final List<Path> classpath = new ArrayList<>();
93         private List<String> output = null;
94         private boolean haslib;
95
96         public Compilation() throws IOException {
97             dir = Files.createTempDirectory("javac");
98             srcdir = dir.resolve("src");
99             outdir = dir.resolve("out");
100             libdir = dir.resolve("lib");
101             Files.createDirectory(srcdir);
102             Files.createDirectory(outdir);
103         }
104
105         public void add(Path p) {
106             infiles.add(p);
107         }
108
109         public void classpath(Path p) {
110             classpath.add(p);
111         }
112
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
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());
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             }
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);
177         }
178
179         public List<String> output() {
180             if(output == null)
181                 throw(new IllegalStateException());
182             return(output);
183         }
184
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);
203         }
204
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);
211         }
212
213         public void close() throws IOException {
214             remove(dir);
215         }
216     }
217
218     public static class BufferedClassLoader extends ClassLoader {
219         public final Map<String, byte[]> contents;
220
221         public BufferedClassLoader(ClassLoader parent, Collection<ClassOutput> contents) {
222             super(parent);
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
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) {
269         try {
270             Path p = Paths.get(nm);
271             if(Files.isRegularFile(p))
272                 return(p);
273         } catch(InvalidPathException e) {
274         }
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+)");
285     private static final Pattern incpat = Pattern.compile("\\$include\\s*:\\s*(\\S+)");
286     public class Module {
287         public final Path file;
288         public final BufferedClassLoader code;
289         public final Collection<Path> classpath = new ArrayList<>();
290         public final Collection<Module> include = new ArrayList<>();
291
292         public Module(Path file) throws IOException {
293             this.file = file;
294             try(Compilation c = new Compilation()) {
295                 split(c);
296                 for(Path cp : classpath)
297                     c.classpath(cp);
298                 if(!c.compile())
299                     throw(new CompilationException(file, c.output()));
300                 ClassLoader parent = Compiler.class.getClassLoader();
301                 if(!classpath.isEmpty() || !include.isEmpty()) {
302                     Collection<ClassLoader> libs = new ArrayList<>();
303                     for(Path cp : classpath)
304                         libs.add(libloader(cp));
305                     for(Module mod : include)
306                         libs.add(mod.code);
307                     parent = new LibClassLoader(parent, libs);
308                 }
309                 code = new BufferedClassLoader(parent, c.classes());
310             }
311         }
312
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()) {
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                     }
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                     }
345                     m = classpat.matcher(ln);
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
368     public class File {
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;
375         }
376
377         public void update() throws IOException {
378             synchronized(this) {
379                 FileTime mtime = Files.getLastModifiedTime(name);
380                 if((this.mtime == null) || (this.mtime.compareTo(mtime) < 0)) {
381                     Module pmod = this.mod;
382                     this.mod = new Module(name);
383                     this.mtime = mtime;
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                     }
391                 }
392             }
393         }
394
395         public Module mod() {
396             if(mod == null)
397                 throw(new RuntimeException("file has not yet been updated"));
398             return(mod);
399         }
400     }
401
402     public File file(Path name) {
403         synchronized(files) {
404             File ret = files.get(name);
405             if(ret == null)
406                 files.put(name, ret = new File(name));
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 }