Add classpath management to jagidir compiler.
[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         for(Path dir : searchpath) {
239             Path jar = dir.resolve(nm + ".jar");
240             if(Files.isRegularFile(jar))
241                 return(jar);
242         }
243         return(null);
244     }
245
246     private static final Pattern classpat = Pattern.compile("^((public|abstract)\\s+)*(class|interface)\\s+(\\S+)");
247     private static final Pattern libpat = Pattern.compile("\\$use\\s*:\\s*(\\S+)");
248     public class Module {
249         public final Path file;
250         public final ClassLoader code;
251         public final Collection<Path> classpath = new ArrayList<>();
252
253         public Module(Path file) throws IOException {
254             this.file = file;
255             try(Compilation c = new Compilation()) {
256                 split(c);
257                 for(Path cp : classpath)
258                     c.classpath(cp);
259                 if(!c.compile())
260                     throw(new CompilationException(file, c.output()));
261                 ClassLoader parent = Compiler.class.getClassLoader();
262                 if(!classpath.isEmpty()) {
263                     Collection<ClassLoader> libs = new ArrayList<>();
264                     for(Path cp : classpath)
265                         libs.add(libloader(cp));
266                     parent = new LibClassLoader(parent, libs);
267                 }
268                 code = new BufferedClassLoader(parent, c.classes());
269             }
270         }
271
272         public void split(Compilation c) throws IOException {
273             StringBuilder head = new StringBuilder();
274             BufferedWriter cur = null;
275             try(BufferedReader fp = Files.newBufferedReader(file)) {
276                 for(String ln = fp.readLine(); ln != null; ln = fp.readLine()) {
277                     Matcher m = libpat.matcher(ln);
278                     if(m.find()) {
279                         Path lib = findlib(m.group(1));
280                         if(lib == null)
281                             throw(new CompilationException(file, Arrays.asList("no such library: " + m.group(1))));
282                         classpath.add(lib);
283                     }
284                     m = classpat.matcher(ln);
285                     if(m.find()) {
286                         String clnm = m.group(4);
287                         Path sp = c.srcdir.resolve(clnm + ".java");
288                         c.add(sp);
289                         if(cur != null)
290                             cur.close();
291                         cur = Files.newBufferedWriter(sp);
292                         cur.append(head);
293                     }
294                     if(cur != null) {
295                         cur.append(ln); cur.append('\n');
296                     } else {
297                         head.append(ln); head.append('\n');
298                     }
299                 }
300             } finally {
301                 if(cur != null)
302                     cur.close();
303             }
304         }
305     }
306
307     public class File {
308         public final Path name;
309         private FileTime mtime = null;
310         private Module mod = null;
311
312         private File(Path name) {
313             this.name = name;
314         }
315
316         public void update() throws IOException {
317             synchronized(this) {
318                 FileTime mtime = Files.getLastModifiedTime(name);
319                 if((this.mtime == null) || (this.mtime.compareTo(mtime) < 0)) {
320                     mod = new Module(name);
321                     this.mtime = mtime;
322                 }
323             }
324         }
325
326         public Module mod() {
327             if(mod == null)
328                 throw(new RuntimeException("file has not yet been updated"));
329             return(mod);
330         }
331     }
332
333     public File file(Path name) {
334         synchronized(files) {
335             File ret = files.get(name);
336             if(ret == null)
337                 files.put(name, ret = new File(name));
338             return(ret);
339         }
340     }
341
342     private static Compiler global = null;
343     public static Compiler get() {
344         if(global == null) {
345             synchronized(Compiler.class) {
346                 if(global == null)
347                     global = new Compiler();
348             }
349         }
350         return(global);
351     }
352 }