b277e003274b42d08b415ff52222d30d54c61199
[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 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 }