Allow jagidir $use directives to specify absolute paths.
[jagi.git] / src / jagi / fs / Compiler.java
... / ...
CommitLineData
1package jagi.fs;
2
3import jagi.*;
4import java.util.*;
5import java.util.regex.*;
6import java.nio.file.*;
7import java.nio.file.attribute.*;
8import java.io.*;
9import java.net.*;
10
11public 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}