Rewrite jagidir compiler to call external javac.
[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
10 public class Compiler {
11     private final Map<Path, Module> modules = new HashMap<>();
12
13     public static class ClassOutput {
14         public final String name;
15         public final ByteArrayOutputStream buf = new ByteArrayOutputStream();
16
17         public ClassOutput(String name) {
18             this.name = name;
19         }
20     }
21
22     public static class CompilationException extends RuntimeException {
23         public final Path file;
24         private final List<? extends Object> messages;
25
26         public CompilationException(Path file, List<? extends Object> messages) {
27             this.file = file;
28             this.messages = messages;
29         }
30
31         public String getMessage() {
32             return(file + ": compilation failed");
33         }
34
35         public String messages() {
36             StringBuilder buf = new StringBuilder();
37             for(Object msg : messages)
38                 buf.append(msg.toString() + "\n");
39             return(buf.toString());
40         }
41
42         public void printStackTrace(PrintStream out) {
43             out.print(messages());
44             super.printStackTrace(out);
45         }
46     }
47
48     public static class Compilation implements AutoCloseable {
49         private final Path dir, srcdir, outdir;
50         private final List<Path> infiles = new ArrayList<>();
51         private List<String> output = null;
52
53         public Compilation() throws IOException {
54             dir = Files.createTempDirectory("javac");
55             srcdir = dir.resolve("src");
56             outdir = dir.resolve("out");
57             Files.createDirectory(srcdir);
58             Files.createDirectory(outdir);
59         }
60
61         public void add(Path p) {
62             infiles.add(p);
63         }
64
65         private static final Pattern classpat = Pattern.compile("^((public|abstract)\\s+)*(class|interface)\\s+(\\S+)");
66         public void split(Path infile) throws IOException {
67             StringBuilder head = new StringBuilder();
68             BufferedWriter cur = null;
69             try(BufferedReader fp = Files.newBufferedReader(infile)) {
70                 for(String ln = fp.readLine(); ln != null; ln = fp.readLine()) {
71                     Matcher m = classpat.matcher(ln);
72                     if(m.find()) {
73                         String clnm = m.group(4);
74                         Path sp = srcdir.resolve(clnm + ".java");
75                         add(sp);
76                         if(cur != null)
77                             cur.close();
78                         cur = Files.newBufferedWriter(sp);
79                         cur.append(head);
80                     }
81                     if(cur != null) {
82                         cur.append(ln); cur.append('\n');
83                     } else {
84                         head.append(ln); head.append('\n');
85                     }
86                 }
87             } finally {
88                 if(cur != null)
89                     cur.close();
90             }
91         }
92
93         public boolean compile() throws IOException {
94             List<String> args = new ArrayList<>();
95             args.add("javac");
96             args.add("-d");
97             args.add(outdir.toString());
98             for(Path p : infiles)
99                 args.add(p.toString());
100             ProcessBuilder cmd = new ProcessBuilder(args);
101             cmd.redirectErrorStream(true);
102             cmd.redirectOutput(ProcessBuilder.Redirect.PIPE);
103             Process proc = cmd.start();
104             List<String> output = new ArrayList<>();
105             BufferedReader fp = new BufferedReader(new InputStreamReader(proc.getInputStream(), Utils.UTF8));
106             while(true) {
107                 String ln = fp.readLine();
108                 if(ln == null)
109                     break;
110                 output.add(ln);
111             }
112             int status;
113             try {
114                 status = proc.waitFor();
115             } catch(InterruptedException e) {
116                 Thread.currentThread().interrupt();
117                 throw(new IOException("compilation interrupted"));
118             }
119             this.output = output;
120             return(status == 0);
121         }
122
123         public List<String> output() {
124             if(output == null)
125                 throw(new IllegalStateException());
126             return(output);
127         }
128
129         public Collection<ClassOutput> classes() throws IOException {
130             Collection<ClassOutput> ret = new ArrayList<>();
131             for(Path p : (Iterable<Path>)Files.walk(outdir)::iterator) {
132                 Path rel = outdir.relativize(p);
133                 String fn = rel.getName(rel.getNameCount() - 1).toString();
134                 if(!Files.isRegularFile(p) || !fn.endsWith(".class"))
135                     continue;
136                 StringBuilder clnm = new StringBuilder();
137                 for(int i = 0; i < rel.getNameCount() - 1; i++) {
138                     clnm.append(rel.getName(i));
139                     clnm.append('.');
140                 }
141                 clnm.append(fn.substring(0, fn.length() - 6));
142                 ClassOutput cls = new ClassOutput(clnm.toString());
143                 Files.copy(p, cls.buf);
144                 ret.add(cls);
145             }
146             return(ret);
147         }
148
149         private static void remove(Path p) throws IOException {
150             if(Files.isDirectory(p)) {
151                 for(Path ent : (Iterable<Path>)Files.list(p)::iterator)
152                     remove(ent);
153             }
154             Files.delete(p);
155         }
156
157         public void close() throws IOException {
158             remove(dir);
159         }
160     }
161
162     public static Collection<ClassOutput> compile(Path file) throws IOException {
163         try(Compilation c = new Compilation()) {
164             c.split(file);
165             if(!c.compile())
166                 throw(new CompilationException(file, c.output()));
167             return(c.classes());
168         }
169     }
170
171     public static class BufferedClassLoader extends ClassLoader {
172         public final Map<String, byte[]> contents;
173
174         public BufferedClassLoader(Collection<ClassOutput> contents) {
175             this.contents = new HashMap<>();
176             for(ClassOutput clc : contents)
177                 this.contents.put(clc.name, clc.buf.toByteArray());
178         }
179
180         public Class<?> findClass(String name) throws ClassNotFoundException {
181             byte[] c = contents.get(name);
182             if(c == null)
183                 throw(new ClassNotFoundException(name));
184             return(defineClass(name, c, 0, c.length));
185         }
186     }
187
188     public static class Module {
189         public final Path file;
190         private FileTime mtime = null;
191         private ClassLoader code = null;
192
193         private Module(Path file) {
194             this.file = file;
195         }
196
197         public void update() throws IOException {
198             synchronized(this) {
199                 FileTime mtime = Files.getLastModifiedTime(file);
200                 if((this.mtime == null) || (this.mtime.compareTo(mtime) < 0)) {
201                     code = new BufferedClassLoader(compile(file));
202                     this.mtime = mtime;
203                 }
204             }
205         }
206
207         public ClassLoader code() {
208             if(code == null)
209                 throw(new RuntimeException("module has not yet been updated"));
210             return(code);
211         }
212     }
213
214     public Module module(Path file) {
215         synchronized(modules) {
216             Module ret = modules.get(file);
217             if(ret == null)
218                 modules.put(file, ret = new Module(file));
219             return(ret);
220         }
221     }
222
223     private static Compiler global = null;
224     public static Compiler get() {
225         if(global == null) {
226             synchronized(Compiler.class) {
227                 if(global == null)
228                     global = new Compiler();
229             }
230         }
231         return(global);
232     }
233 }