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