Add classpath management to jagidir compiler.
[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 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}