Allow jagidir modules to include other compiled modules.
[jagi.git] / src / jagi / fs / Compiler.java
index dcfee03..2f0d81c 100644 (file)
@@ -1,99 +1,70 @@
 package jagi.fs;
 
+import jagi.*;
 import java.util.*;
 import java.util.regex.*;
+import java.util.logging.*;
 import java.nio.file.*;
 import java.nio.file.attribute.*;
 import java.io.*;
 import java.net.*;
-import javax.tools.*;
 
 public class Compiler {
-    private final Map<Path, Module> modules = new HashMap<>();
+    private static final Logger log = Logger.getLogger("jagi-fs");
+    private static final Path cwd = Paths.get("").toAbsolutePath();
+    private final Map<Path, File> files = new HashMap<>();
+    private final Map<Path, ClassLoader> libs = new HashMap<>();
+    private final Collection<Path> searchpath = new ArrayList<>();
 
-    public static class FilePart extends SimpleJavaFileObject {
-       public final Path file;
-       public final String clnm;
-       public final CharSequence src;
-
-       private static URI dummyuri(Path file, String clnm) {
-           String clp = clnm.replace('.', '/') + Kind.SOURCE.extension;
-           return(URI.create(file.toUri().toString() + "!/" + clp));
-       }
-
-       public FilePart(Path file, String clnm, CharSequence src) {
-           super(dummyuri(file, clnm), Kind.SOURCE);
-           this.file = file;
-           this.clnm = clnm;
-           this.src = src;
-       }
-
-       public CharSequence getCharContent(boolean ice) {
-           return(src);
+    public Compiler() {
+       String syspath = System.getenv("PATH");
+       if(syspath != null) {
+           String sep = java.io.File.pathSeparator;
+           int p1 = 0, p2 = syspath.indexOf(sep);
+           do {
+               String el;
+               if(p2 >= 0) {
+                   el = syspath.substring(p1, p2);
+                   p1 = p2 + sep.length();
+                   p2 = syspath.indexOf(sep, p1);
+               } else {
+                   el =syspath.substring(p1);
+               }
+               try {
+                   Path p = Paths.get(el);
+                   if(p.getParent() != null)
+                       searchpath.add(p.getParent().resolve("share").resolve("java"));
+               } catch(InvalidPathException e) {
+                   continue;
+               }
+           } while(p2 >= 0);
        }
-
-       private static final Pattern classpat = Pattern.compile("^((public|abstract)\\s+)*(class|interface)\\s+(\\S+)");
-       public static Collection<FilePart> split(Path file) throws IOException {
-           Collection<FilePart> ret = new ArrayList<>();
-           StringBuilder head = new StringBuilder();
-           StringBuilder cur = null;
-           String clnm = null;
-           try(BufferedReader fp = Files.newBufferedReader(file)) {
-               for(String ln = fp.readLine(); ln != null; ln = fp.readLine()) {
-                   Matcher m = classpat.matcher(ln);
-                   if(m.find()) {
-                       if(cur != null)
-                           ret.add(new FilePart(file, clnm, cur));
-                       clnm = m.group(4);
-                       cur = new StringBuilder();
-                       cur.append(head);
-                   }
-                   if(cur != null) {
-                       cur.append(ln); cur.append('\n');
-                   } else {
-                       head.append(ln); head.append('\n');
-                   }
+       String proppath = System.getProperty("jagi.search-path");
+       if(proppath != null) {
+           for(String el : proppath.split(":")) {
+               try {
+                   searchpath.add(Paths.get(el));
+               } catch(InvalidPathException e) {
+                   continue;
                }
-               if(cur != null)
-                   ret.add(new FilePart(file, clnm, cur));
            }
-           return(ret);
        }
     }
 
-    public static class ClassOutput extends SimpleJavaFileObject {
+    public static class ClassOutput {
        public final String name;
-       private final ByteArrayOutputStream buf = new ByteArrayOutputStream();
+       public final ByteArrayOutputStream buf = new ByteArrayOutputStream();
 
        public ClassOutput(String name) {
-           super(URI.create("mem://" + name), Kind.CLASS);
            this.name = name;
        }
-
-       public OutputStream openOutputStream() {
-           return(buf);
-       }
-    }
-
-    public static class FileContext extends ForwardingJavaFileManager<JavaFileManager> {
-       public final Collection<ClassOutput> output = new ArrayList<>();
-
-       public FileContext(JavaCompiler javac) {
-           super(javac.getStandardFileManager(null, null, null));
-       }
-
-       public JavaFileObject getJavaFileForOutput(Location location, String name, JavaFileObject.Kind kind, FileObject sibling) {
-           ClassOutput cl = new ClassOutput(name);
-           output.add(cl);
-           return(cl);
-       }
     }
 
     public static class CompilationException extends RuntimeException {
        public final Path file;
-       private final List<Diagnostic<? extends JavaFileObject>> messages;
+       private final List<? extends Object> messages;
 
-       public CompilationException(Path file, List<Diagnostic<? extends JavaFileObject>> messages) {
+       public CompilationException(Path file, List<? extends Object> messages) {
            this.file = file;
            this.messages = messages;
        }
@@ -104,7 +75,7 @@ public class Compiler {
 
        public String messages() {
            StringBuilder buf = new StringBuilder();
-           for(Diagnostic msg : messages)
+           for(Object msg : messages)
                buf.append(msg.toString() + "\n");
            return(buf.toString());
        }
@@ -115,25 +86,140 @@ public class Compiler {
        }
     }
 
-    public static Collection<ClassOutput> compile(Path file) throws IOException {
-       List<String> opt = Arrays.asList();
-       JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
-       if(javac == null)
-           throw(new RuntimeException("no javac present"));
-       Collection<FilePart> files;
-       files = FilePart.split(file);
-       DiagnosticCollector<JavaFileObject> out = new DiagnosticCollector<>();
-       FileContext fs = new FileContext(javac);
-       JavaCompiler.CompilationTask job = javac.getTask(null, fs, out, opt, null, files);
-       if(!job.call())
-           throw(new CompilationException(file, out.getDiagnostics()));
-       return(fs.output);
+    public static class Compilation implements AutoCloseable {
+       public final Path dir, srcdir, outdir, libdir;
+       private final List<Path> infiles = new ArrayList<>();
+       private final List<Path> classpath = new ArrayList<>();
+       private List<String> output = null;
+       private boolean haslib;
+
+       public Compilation() throws IOException {
+           dir = Files.createTempDirectory("javac");
+           srcdir = dir.resolve("src");
+           outdir = dir.resolve("out");
+           libdir = dir.resolve("lib");
+           Files.createDirectory(srcdir);
+           Files.createDirectory(outdir);
+       }
+
+       public void add(Path p) {
+           infiles.add(p);
+       }
+
+       public void classpath(Path p) {
+           classpath.add(p);
+       }
+
+       public void addlib(String clnm, byte[] contents) throws IOException {
+           if(!haslib) {
+               Files.createDirectory(libdir);
+               classpath(libdir);
+               haslib = true;
+           }
+           Path p = libdir;
+           int p1 = 0;
+           while(true) {
+               int p2 = clnm.indexOf('.', p1);
+               if(p2 < 0)
+                   break;
+               p = p.resolve(clnm.substring(p1, p2));
+               if(!Files.isDirectory(p))
+                   Files.createDirectory(p);
+               p1 = p2 + 1;
+           }
+           p = p.resolve(clnm.substring(p1) + ".class");
+           Files.write(p, contents);
+       }
+
+       public void addlib(Map<String, byte[]> classes) throws IOException {
+           for(Map.Entry<String, byte[]> ent : classes.entrySet())
+               addlib(ent.getKey(), ent.getValue());
+       }
+
+       public boolean compile() throws IOException {
+           List<String> args = new ArrayList<>();
+           args.add("javac");
+           args.add("-d");
+           args.add(outdir.toString());
+           if(!classpath.isEmpty()) {
+               StringBuilder buf = new StringBuilder();
+               for(Path cp : classpath) {
+                   if(buf.length() > 0)
+                       buf.append(":");
+                   buf.append(cp.toString());
+               }
+               args.add("-cp");
+               args.add(buf.toString());
+           }
+           for(Path p : infiles)
+               args.add(p.toString());
+           ProcessBuilder cmd = new ProcessBuilder(args);
+           cmd.redirectErrorStream(true);
+           cmd.redirectOutput(ProcessBuilder.Redirect.PIPE);
+           Process proc = cmd.start();
+           List<String> output = new ArrayList<>();
+           BufferedReader fp = new BufferedReader(new InputStreamReader(proc.getInputStream(), Utils.UTF8));
+           while(true) {
+               String ln = fp.readLine();
+               if(ln == null)
+                   break;
+               output.add(ln);
+           }
+           int status;
+           try {
+               status = proc.waitFor();
+           } catch(InterruptedException e) {
+               Thread.currentThread().interrupt();
+               throw(new IOException("compilation interrupted"));
+           }
+           this.output = output;
+           return(status == 0);
+       }
+
+       public List<String> output() {
+           if(output == null)
+               throw(new IllegalStateException());
+           return(output);
+       }
+
+       public Collection<ClassOutput> classes() throws IOException {
+           Collection<ClassOutput> ret = new ArrayList<>();
+           for(Path p : (Iterable<Path>)Files.walk(outdir)::iterator) {
+               Path rel = outdir.relativize(p);
+               String fn = rel.getName(rel.getNameCount() - 1).toString();
+               if(!Files.isRegularFile(p) || !fn.endsWith(".class"))
+                   continue;
+               StringBuilder clnm = new StringBuilder();
+               for(int i = 0; i < rel.getNameCount() - 1; i++) {
+                   clnm.append(rel.getName(i));
+                   clnm.append('.');
+               }
+               clnm.append(fn.substring(0, fn.length() - 6));
+               ClassOutput cls = new ClassOutput(clnm.toString());
+               Files.copy(p, cls.buf);
+               ret.add(cls);
+           }
+           return(ret);
+       }
+
+       private static void remove(Path p) throws IOException {
+           if(Files.isDirectory(p)) {
+               for(Path ent : (Iterable<Path>)Files.list(p)::iterator)
+                   remove(ent);
+           }
+           Files.delete(p);
+       }
+
+       public void close() throws IOException {
+           remove(dir);
+       }
     }
 
     public static class BufferedClassLoader extends ClassLoader {
        public final Map<String, byte[]> contents;
 
-       public BufferedClassLoader(Collection<ClassOutput> contents) {
+       public BufferedClassLoader(ClassLoader parent, Collection<ClassOutput> contents) {
+           super(parent);
            this.contents = new HashMap<>();
            for(ClassOutput clc : contents)
                this.contents.put(clc.name, clc.buf.toByteArray());
@@ -147,37 +233,177 @@ public class Compiler {
        }
     }
 
-    public static class Module {
+    public static class LibClassLoader extends ClassLoader {
+       private final ClassLoader[] classpath;
+
+       public LibClassLoader(ClassLoader parent, Collection<ClassLoader> classpath) {
+           super(parent);
+           this.classpath = classpath.toArray(new ClassLoader[0]);
+       }
+
+       public Class<?> findClass(String name) throws ClassNotFoundException {
+           for(ClassLoader lib : classpath) {
+               try {
+                   return(lib.loadClass(name));
+               } catch(ClassNotFoundException e) {}
+           }
+           throw(new ClassNotFoundException("Could not find " + name + " in any of " + Arrays.asList(classpath).toString()));
+       }
+    }
+
+    public ClassLoader libloader(Path p) {
+       synchronized(libs) {
+           ClassLoader ret = libs.get(p);
+           if(ret == null) {
+               try {
+                   libs.put(p, ret = new URLClassLoader(new URL[] {p.toUri().toURL()}));
+               } catch(MalformedURLException e) {
+                   throw(new RuntimeException(e));
+               }
+           }
+           return(ret);
+       }
+    }
+
+    private Path findlib(String nm) {
+       try {
+           Path p = Paths.get(nm);
+           if(Files.isRegularFile(p))
+               return(p);
+       } catch(InvalidPathException e) {
+       }
+       for(Path dir : searchpath) {
+           Path jar = dir.resolve(nm + ".jar");
+           if(Files.isRegularFile(jar))
+               return(jar);
+       }
+       return(null);
+    }
+
+    private static final Pattern classpat = Pattern.compile("^((public|abstract)\\s+)*(class|interface)\\s+(\\S+)");
+    private static final Pattern libpat = Pattern.compile("\\$use\\s*:\\s*(\\S+)");
+    private static final Pattern incpat = Pattern.compile("\\$include\\s*:\\s*(\\S+)");
+    public class Module {
        public final Path file;
-       private FileTime mtime = null;
-       private ClassLoader code = null;
+       public final BufferedClassLoader code;
+       public final Collection<Path> classpath = new ArrayList<>();
+       public final Collection<Module> include = new ArrayList<>();
 
-       private Module(Path file) {
+       public Module(Path file) throws IOException {
            this.file = file;
+           try(Compilation c = new Compilation()) {
+               split(c);
+               for(Path cp : classpath)
+                   c.classpath(cp);
+               if(!c.compile())
+                   throw(new CompilationException(file, c.output()));
+               ClassLoader parent = Compiler.class.getClassLoader();
+               if(!classpath.isEmpty() || !include.isEmpty()) {
+                   Collection<ClassLoader> libs = new ArrayList<>();
+                   for(Path cp : classpath)
+                       libs.add(libloader(cp));
+                   for(Module mod : include)
+                       libs.add(mod.code);
+                   parent = new LibClassLoader(parent, libs);
+               }
+               code = new BufferedClassLoader(parent, c.classes());
+           }
+       }
+
+       public void split(Compilation c) throws IOException {
+           StringBuilder head = new StringBuilder();
+           BufferedWriter cur = null;
+           try(BufferedReader fp = Files.newBufferedReader(file)) {
+               for(String ln = fp.readLine(); ln != null; ln = fp.readLine()) {
+                   Matcher m = libpat.matcher(ln);
+                   if(m.find()) {
+                       Path lib = findlib(m.group(1));
+                       if(lib == null)
+                           throw(new CompilationException(file, Arrays.asList("no such library: " + m.group(1))));
+                       classpath.add(lib);
+                   }
+                   m = incpat.matcher(ln);
+                   if(m.find()) {
+                       String nm = m.group(1);
+                       File f= null;
+                       if(f == null) {
+                           Path p = file.resolveSibling(nm);
+                           if(Files.isRegularFile(p))
+                               f = file(p);
+                       }
+                       if(f == null) {
+                           Path p = cwd.resolve(nm);
+                           if(Files.isRegularFile(p))
+                               f = file(p);
+                       }
+                       if(f == null)
+                           throw(new CompilationException(file, Arrays.asList("no such file to include: " + nm)));
+                       f.update();
+                       c.addlib(f.mod().code.contents);
+                       include.add(f.mod());
+                   }
+                   m = classpat.matcher(ln);
+                   if(m.find()) {
+                       String clnm = m.group(4);
+                       Path sp = c.srcdir.resolve(clnm + ".java");
+                       c.add(sp);
+                       if(cur != null)
+                           cur.close();
+                       cur = Files.newBufferedWriter(sp);
+                       cur.append(head);
+                   }
+                   if(cur != null) {
+                       cur.append(ln); cur.append('\n');
+                   } else {
+                       head.append(ln); head.append('\n');
+                   }
+               }
+           } finally {
+               if(cur != null)
+                   cur.close();
+           }
+       }
+    }
+
+    public class File {
+       public final Path name;
+       private FileTime mtime = null;
+       private Module mod = null;
+
+       private File(Path name) {
+           this.name = name;
        }
 
        public void update() throws IOException {
            synchronized(this) {
-               FileTime mtime = Files.getLastModifiedTime(file);
+               FileTime mtime = Files.getLastModifiedTime(name);
                if((this.mtime == null) || (this.mtime.compareTo(mtime) < 0)) {
-                   code = new BufferedClassLoader(compile(file));
+                   Module pmod = this.mod;
+                   this.mod = new Module(name);
                    this.mtime = mtime;
+                   if(pmod instanceof AutoCloseable) {
+                       try {
+                           ((AutoCloseable)pmod).close();
+                       } catch(Exception e) {
+                           log.log(Level.WARNING, String.format("Error when disposing updated module %s", pmod.file), e);
+                       }
+                   }
                }
            }
        }
 
-       public ClassLoader code() {
-           if(code == null)
-               throw(new RuntimeException("module has not yet been updated"));
-           return(code);
+       public Module mod() {
+           if(mod == null)
+               throw(new RuntimeException("file has not yet been updated"));
+           return(mod);
        }
     }
 
-    public Module module(Path file) {
-       synchronized(modules) {
-           Module ret = modules.get(file);
+    public File file(Path name) {
+       synchronized(files) {
+           File ret = files.get(name);
            if(ret == null)
-               modules.put(file, ret = new Module(file));
+               files.put(name, ret = new File(name));
            return(ret);
        }
     }