Java tutorial
/* -*- mode: Java; c-basic-offset: 2; -*- */ /** * SWF9 calls to external compiler * * @author dda@ddanderson.com * @author ptw@openlaszlo.org * @description: JavaScript -> ActionScript3 translator, calling AS3 compiler -> SW9 * */ package org.openlaszlo.sc; import java.io.*; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.openlaszlo.sc.parser.*; import org.openlaszlo.utils.FileUtils; import org.openlaszlo.server.LPS; import org.apache.commons.collections.LRUMap; /** * The SWF9External manages communication with the * external compiler - generation of source files, * calling the compiler, packaging up the result and * interpreting error messages. * * It is expected that a new SWF9External object * be created for each chunk of compilation. Each * new SWF9External gets a new temporary 'work' directory * for compilation, and verifies classname uniqueness * within that space. */ public class SWF9External { /** Number of errors shown before truncating */ static public final int MAX_ERRORS_SHOWN = 50; /** * A directory we create under the Java runtime's temp dir, * that contains our compilation work directories, one for each compilation. */ public static final String WORK_DIR_PARENT = "lzswf9"; /** * The prefix for naming the work directories, which appear * under the WORK_DIR_PARENT in the Java runtime's temp dir. * For example, /tmp/lzswf9/lzgen...., although /tmp * may be replaced by something else when running within * tomcat or another application server. */ public static final String WORK_DIR_PREFIX = "lzgen"; private File workdir; private Compiler.OptionMap options; private ScriptCompilerInfo mInfo; static public String optionsFilename = "LZC_COMPILER_OPTIONS"; /* * Used by getFileNameForClassName to prevent filename conflicts. * The key is the 'tolower' name, the value is an ArrayList of * UniqueGlobalName objects. */ private HashMap uniqueFileNames = new HashMap(); public class UniqueGlobalName { String globalName; boolean isClass; int subdirnum; // subdirector number, 0 means top level } private int maxSubdirnum = 0; // For incremental mode, check if last compile had same options as current compile. boolean compilerOptionsChanged() { try { File optsfile = new File(workDirectoryName(optionsFilename)); String newOpts = optionsToString(); if (optsfile.exists()) { String prevOpts = FileUtils.readFileString(optsfile); return !newOpts.equals(prevOpts); } else { return true; } } catch (IOException ioe) { throw new CompilerError( "compilerOptionsChanged: error while reading incremental compiler options file: " + ioe); } } // For incremental mode, write out the compiler options in a persistent file in the working dir void writeOptionsFile() { try { File optsfile = new File(workDirectoryName(optionsFilename)); String newOpts = optionsToString(); StringReader from = new StringReader(newOpts); // Rewrite the compiler options file with current options FileWriter outw = new FileWriter(optsfile); FileUtils.send(from, outw); outw.close(); } catch (IOException ioe) { throw new CompilerError("writeOptionsFile: cannot write incremental compiler options file: " + ioe); } } private String optionsToString() { StringBuffer result = new StringBuffer(); result.append("{"); TreeMap sorted = new TreeMap(options); for (Iterator i = sorted.keySet().iterator(); i.hasNext();) { Object key = i.next(); result.append(key); result.append(": "); result.append(sorted.get(key)); if (i.hasNext()) { result.append(", "); } } result.append("}"); return result.toString(); } public SWF9External(Compiler.OptionMap options, boolean buildSharedLibrary) { this.options = options; mInfo = (ScriptCompilerInfo) options.get(Compiler.COMPILER_INFO); if (mInfo == null) { mInfo = new ScriptCompilerInfo(); } if (options.getBoolean(Compiler.REUSE_WORK_DIRECTORY)) { // Re-use the previous working directory from the ScriptCompilerInfo workdir = mInfo.workDir; } else { workdir = createCompilationWorkDir(options, buildSharedLibrary); // Copy pointer to working directory to the ScriptCompilerInfo, // so any subsequent <import> library compilations can use it. mInfo.workDir = workdir; } // If this is not an incremental compile, erase all files in the working directory if (!options.getBoolean(Compiler.DEBUG_EVAL)) { if (!options.getBoolean(Compiler.REUSE_WORK_DIRECTORY)) { if (options.getBoolean(Compiler.INCREMENTAL_COMPILE)) { // If the compiler options changed from the last compile, then clean the directory if (compilerOptionsChanged()) { System.err.println("swf9 compiler options changes, cleaning working dir"); deleteDirectoryFiles(workdir); } } else { System.err.println("cleaning working dir"); deleteDirectoryFiles(workdir); } } } writeOptionsFile(); } public static void deleteDirectoryFiles(File dir) { if (dir.isDirectory()) { File[] children = dir.listFiles(); for (int i = 0; i < children.length; i++) { File f = children[i]; if (f.isFile()) { f.delete(); } } } } /** * Return the bytes in a file */ public static byte[] getBytes(String filename) throws IOException { File f = new File(filename); long len = f.length(); // Passing around byte arrays has limitations. if (len > Integer.MAX_VALUE) throw new IOException(filename + ": output too large"); byte[] result = new byte[(int) len]; int pos = 0; FileInputStream fis = null; try { fis = new FileInputStream(filename); while (pos < len) { int nbytes = fis.read(result, pos, (int) len - pos); if (nbytes < 0) { // premature end of file. File.length() lied or the // length of the file changed out from under us. // Either way, we cannot trust it. throw new IOException(filename + ": file size discrepency byte " + pos + "/" + len); } pos += nbytes; } // Sanity check, make sure file hasn't been appended to if (fis.read() != -1) throw new IOException(filename + ": file growing during read at byte " + pos); } finally { closeit(fis); } return result; } /** * Create a temporary work directory for compilation * and return a File for it. * @throw CompilerError when directory creation fails */ private File createCompilationWorkDir(Compiler.OptionMap options, boolean buildSharedLibrary) { // TODO: [2007-11-20 dda] Need some provisions for file // cleanup on error, and on success too. File f = null; try { String tmpdirstr = System.getProperty("java.io.tmpdir"); String swf9tmpdirstr = tmpdirstr + File.separator + WORK_DIR_PARENT; (new File(swf9tmpdirstr)).mkdirs(); String appDirPrefix = mInfo.buildDirPathPrefix; // For Windows, we need to strip any "disk drive" prefix from // the path. e.g., "C:" if (appDirPrefix != null && appDirPrefix.matches("^[a-zA-Z]:.*")) { appDirPrefix = appDirPrefix.substring(3); } if (buildSharedLibrary) { // Compiling the LFC f = File.createTempFile(WORK_DIR_PREFIX, "", new File(swf9tmpdirstr)); if (!f.delete()) { throw new CompilerError("getCompilationWorkDir: temp file does not exist"); } if (!f.mkdir()) { throw new CompilerError("getCompilationWorkDir: cannot make workdir"); } } else { f = new File(swf9tmpdirstr + File.separator + appDirPrefix); f.mkdirs(); } } catch (IOException ioe) { throw new CompilerError("getCompilationWorkDir: cannot get temp directory: " + ioe); } // Copy the pointer to our work directory to the ScriptCompilerInfo object this.mInfo.workDir = f; return f; } /** * For a relative file name, return an absolute path name * as the file would appear in the work directory for the compiler. */ public String workDirectoryName(String file) { if (new File(file).isAbsolute()) { throw new IllegalArgumentException("workDirectoryName: file name must be relative"); } return workdir.getPath() + File.separator + file; } /** * Close an input stream unconditionally. */ public static void closeit(InputStream is) { try { if (is != null) is.close(); } catch (IOException ioe) { // don't rethrow, we can live with an error during cleanup // TODO: [2007-11-20 dda] log this System.err.println("Exception closing: " + ioe); } } /** * Close an output stream unconditionally. */ public static void closeit(OutputStream os) { try { if (os != null) os.close(); } catch (IOException ioe) { // don't rethrow, we can live with an error during cleanup // TODO: [2007-11-20 dda] log this System.err.println("Exception closing: " + ioe); } } /** * A collector for an output stream from an external process. */ public static class OutputCollector extends Thread { private Exception exception = null; private InputStream is; // we don't expect this to be terribly big, can fit in memory StringBuffer sb = new StringBuffer(); public OutputCollector(InputStream is) { this.is = is; } public void run() { try { BufferedReader reader = new BufferedReader(new InputStreamReader(is)); String line; while ((line = reader.readLine()) != null) { sb.append(line + "\n"); collect(line); } reader.close(); } catch (Exception ex) { exception = ex; } } public String getOutput() { return sb.toString(); } public Exception getException() { return exception; } public void collect(String str) { // this version does no more analysis with the output } } /** * A single error message from the external compiler. */ public static class ExternalCompilerError { private int origlinenum = -1; private int linenum; private int colnum; private String error; private String code = ""; private String cleanedCode = ""; private String orig = ""; private TranslationUnit tunit; ExternalCompilerError() { this(null, -1, -1, "", ""); } ExternalCompilerError(TranslationUnit tunit, int linenum, int colnum, String error, String orig) { this.tunit = tunit; this.linenum = linenum; this.colnum = colnum; this.error = error; this.orig = orig; } public String toString() { String tunitstr = (tunit == null) ? "unknown" : tunit.getName(); return "External error: " + tunitstr + ": " + linenum + ": " + colnum + ": " + error + ": for line:\n" + code; } public int getLineNumber() { return linenum; } // returns just the compiler error: e.g. // Error: variable 'x' undefined public String getErrorString() { return error; } // returns the original untouched compiler error message public String originalErrorString() { return orig; } // returns the complete the compiler error, // but without the positional 'caret', and // an indication of where the code starts, // other than just a newline. This is // meant to be read in the browser. // Error: variable 'x' undefined, in line: result = x + 4; public String cleanedErrorString() { String result = error.trim(); while (result.endsWith("\n") || result.endsWith(".")) { result = result.substring(0, result.length() - 1); } result += ", in line: " + cleanedCode; return result; } public void addCodeLine(String str) { if (code.length() > 0) { code += "\n"; } code += str; // In cleanedCode, don't keep lines with just spaces and caret (^) if (!str.matches("^[ ^]*$")) { if (cleanedCode.length() > 0) { cleanedCode += "\n"; } cleanedCode += str; } } public String getCode() { return code; } public String getCleanedCode() { return cleanedCode; } public TranslationUnit getTranslationUnit() { return tunit; } } /** * Parse and return an integer (a line number). * @throw CompilerError when the input string is not a number */ public static int safeInt(String s) { try { return Integer.parseInt(s); } catch (NumberFormatException nfe) { // should be 'impossible' as the pattern matcher should only // give us valid numbers. throw new CompilerError("Bad linenumber translation: " + s); } } /** * Collect the error stream, digesting them into individual * ExternalCompilerErrors. */ public class ExternalCompilerErrorCollector extends OutputCollector { private String inputFilename; private Pattern errPattern; private List errors = new ArrayList(); private ExternalCompilerError lastError = null; private TranslationUnit[] tunits; private List severe = new ArrayList(); // we don't expect this to be terribly big, can fit in memory StringBuffer sb = new StringBuffer(); public ExternalCompilerErrorCollector(InputStream is, List tunits) { super(is); this.inputFilename = inputFilename; this.tunits = (TranslationUnit[]) tunits.toArray(new TranslationUnit[0]); // Expect errors to look like File.as(48): col: 1 Error: some message String pat = "([^\\\\/]+)\\.as\\(([0-9]+)\\): *col: *([0-9]*) *(.*)"; errPattern = Pattern.compile(pat); //System.out.println("Using error pattern: " + pat); } public TranslationUnit locateTranslationUnit(String nm) { for (int i = 0; i < tunits.length; i++) { if (nm.equals(tunits[i].getName())) return tunits[i]; } return null; } // Do our best to identify severe errors that happen in practice. // We'll probably need to add to this list. public boolean matchSevere(String str) { return str.contains("OutOfMemoryError") || str.contains("Java heap space"); } public void collect(String str) { // We expect errors from this compiler to start with the file name. // anything else is just showing us the contents of the line. Matcher matcher = errPattern.matcher(str); if (matcher.find()) { String classnm = matcher.group(1); String linenumstr = matcher.group(2); String colstr = matcher.group(3); TranslationUnit tunit = locateTranslationUnit(classnm); lastError = new ExternalCompilerError(tunit, safeInt(linenumstr), safeInt(colstr), matcher.group(4), str); errors.add(lastError); } else if (matchSevere(str)) { severe.add(str); } else { if (lastError == null) { System.err.println("Stray error string from external compiler: " + str); // Capture it in an error message not tied to a particular line lastError = new ExternalCompilerError(); } lastError.addCodeLine(str); } } public List getErrors() { return errors; } public List getSevereErrors() { return severe; } } /** * True if UNIX quoting rules are in effect. */ public static boolean useUnixQuoting() { return !isWindows(); } /** * Return a more nicely formatted command line. * On UNIX systems, we change '$' to \$' so the * output line can be cut and pasted into a shell. */ public String prettyCommand(List cmd) { String cmdstr = ""; for (Iterator iter = cmd.iterator(); iter.hasNext();) { if (cmdstr.length() > 0) cmdstr += " "; String arg = (String) iter.next(); if (useUnixQuoting()) { // goodness, both $ and \ are special characters for regex. arg = arg.replaceAll("[$]", "\\\\\\$"); } if (arg.indexOf(' ') >= 0) { arg = "\"" + arg + "\""; } cmdstr += arg; } return cmdstr; } /** * Copy an environment variable from the current system environment. */ public static void copyEnvVar(List envvars, String varname) { String val = System.getenv(varname); if (val != null) { envvars.add(varname + "=" + val); } } // The string collected in BigErrorString // will be passed as an exception, so it can't be too large // public static class BigErrorString { int count = 0; String str = ""; public void add(String errstr) { if (str.length() > 0) { str += "\n"; } count++; if (count < MAX_ERRORS_SHOWN) { str += errstr; } else if (count == MAX_ERRORS_SHOWN) { str += ".... more than " + MAX_ERRORS_SHOWN + " errors, additional errors not shown."; } } } /** * Run the compiler using the command/arguments in cmd. * Collect and report any errors, and check for the existence * of the output file. * @throw CompilerError if there are errors messages from the external * compiler, or if any part of the compilation process has problems */ public void execCompileCommand(List cmd, String dir, List tunits, String outfileName) throws IOException // TODO: [2007-11-20 dda] clean up, why catch only some exceptions? { String compilerClass = (String) cmd.remove(0); String[] cmdstr = (String[]) cmd.toArray(new String[0]); String prettycmd = prettyCommand(cmd); System.err.println("Executing compiler: (cd " + dir + "; " + prettycmd + ")"); BigErrorString bigErrorString = new BigErrorString(); // Generate a small script (unix style) to document how // to build this batch of files. String buildsh = isWindows() ? "rem build script\n" : "#!/bin/sh\n"; buildsh += "cd \"" + dir + "\"\n"; buildsh += prettycmd + "\n"; String buildfn = isWindows() ? "build.bat" : "build.sh"; Compiler.emitFile(workDirectoryName(buildfn), buildsh); if (options.getBoolean(Compiler.EMIT_AS3_ONLY)) { return; } List newenv = new ArrayList(); newenv.add("FLEX_HOME=" + FLEX_HOME()); copyEnvVar(newenv, "HOME"); copyEnvVar(newenv, "PATH"); copyEnvVar(newenv, "JAVA_HOME"); Process proc = Runtime.getRuntime().exec(cmdstr, (String[]) newenv.toArray(new String[0]), null); try { OutputStream os = proc.getOutputStream(); OutputCollector outcollect = new OutputCollector(proc.getInputStream()); ExternalCompilerErrorCollector errcollect = new ExternalCompilerErrorCollector(proc.getErrorStream(), tunits); os.close(); outcollect.start(); errcollect.start(); int exitval = proc.waitFor(); outcollect.join(); errcollect.join(); if (outcollect.getException() != null) { System.err.println("Error collecting compiler output: " + outcollect.getException()); // TODO: [2007-11-20 dda] log this } String compilerOutput = outcollect.getOutput(); if (compilerOutput.length() > 0) { System.err.println("compiler output:\n" + compilerOutput); } if (errcollect.getException() != null) { System.err.println("Error collecting compiler output: " + errcollect.getException()); // TODO: [2007-11-20 dda] log this } List severe = errcollect.getSevereErrors(); if (severe.size() > 0) { for (Iterator iter = severe.iterator(); iter.hasNext();) { String errstr = "SEVERE ERROR: " + (String) iter.next(); bigErrorString.add(errstr); System.err.println(errstr); } } List errs = errcollect.getErrors(); if (errs.size() > 0) { System.err.println("ERRORS: "); for (Iterator iter = errs.iterator(); iter.hasNext();) { ExternalCompilerError err = (ExternalCompilerError) iter.next(); TranslationUnit tunit = err.getTranslationUnit(); String srcLineStr; TranslationUnit.SourceFileLine srcFileLine; // actualSrcLine is the name/linenumber of the actual files // used in compilation, not the original sources. String actualSrcFile = null; if (tunit == null) { actualSrcFile = "(unknown)"; } else { actualSrcFile = tunit.getSourceFileName(); if (actualSrcFile == null) actualSrcFile = "(" + tunit.getName() + ")"; } String actualSrcLine = "[" + actualSrcFile + ": " + err.getLineNumber() + "] "; if (tunit == null) { srcLineStr = "tunit/line unknown: "; } else if ((srcFileLine = tunit.originalLineNumber(err.getLineNumber())) == null) { srcLineStr = "line unknown: "; } else { srcLineStr = srcFileLine.sourcefile.name + ": " + srcFileLine.line + ": "; } System.err.println(actualSrcLine + srcLineStr + err.getErrorString()); bigErrorString.add(srcLineStr + err.cleanedErrorString()); } } if (exitval != 0) { System.err.println("FAIL: compiler returned " + exitval); } } catch (InterruptedException ie) { throw new CompilerError("Interrupted compiler"); } System.err.println("Done executing compiler"); if (!new File(outfileName).exists()) { System.err.println("Intermediate file " + outfileName + ": does not exist"); if (bigErrorString.str.length() > 0) { throw new CompilerError(bigErrorString.str); } else { throw new CompilerError("Errors from compiler, output file not created"); } } } /** * Run the compiler using the command/arguments in cmd. Invokes the Flex compiler classes * directly, does not exec a subprocess. * Collect and report any errors, and check for the existence * of the output file. * @throw CompilerError if there are errors messages from the external * compiler, or if any part of the compilation process has problems */ public void callJavaCompileCommand(List acmd, String dir, List tunits, String outfileName) throws IOException // TODO: [2007-11-20 dda] clean up, why catch only some exceptions? { final List cmd = acmd; final String compilerClass = (String) cmd.remove(0); String[] cmdstr = (String[]) cmd.toArray(new String[0]); String prettycmd = prettyCommand(cmd); System.err.println("Executing compiler: (cd " + dir + "; " + prettycmd + ")"); BigErrorString bigErrorString = new BigErrorString(); // Generate a small script (unix style) to document how // to build this batch of files. String buildsh = isWindows() ? "rem build script\n" : "#!/bin/sh\n"; buildsh += "cd \"" + dir + "\"\n"; buildsh += prettycmd + "\n"; String buildfn = isWindows() ? "build.bat" : "build.sh"; Compiler.emitFile(workDirectoryName(buildfn), buildsh); // Remove the shell script executable path from beginning of cmd arg list cmd.remove(0); if (options.getBoolean(Compiler.EMIT_AS3_ONLY)) { // write out the command line as the output instead PrintWriter outf = new PrintWriter(new FileWriter(outfileName)); for (Iterator iter = cmd.iterator(); iter.hasNext();) { String arg = (String) iter.next(); outf.println(arg); } outf.close(); System.err.println( "option EMIT_AS3_ONLY set, returning without invoking flex compiler, call 'lcompile #' to compile as3"); return; } // Save original System.err, System.out PrintStream sout = System.out; PrintStream serr = System.err; ByteArrayOutputStream bout = new ByteArrayOutputStream(); ByteArrayOutputStream berr = new ByteArrayOutputStream(); PrintStream nout = new PrintStream(bout); PrintStream nerr = new PrintStream(berr); // Rebind to capture output System.setErr(nerr); System.setOut(nout); // flex2.tools.Mxmlc +flexlib="$FLEX_HOME/frameworks" // flex2.tools.Compc // System.setProperty("FLEX_HOME", FLEX_HOME()); // The Mxlmc and Compc util classes need to see this arg first in the args list cmd.add(0, "+flexlib=" + FLEX_HOME() + "/frameworks"); final Integer exitval[] = new Integer[1]; Thread worker = new Thread() { public void run() { //Process proc = Runtime.getRuntime().exec(cmdstr, (String[])newenv.toArray(new String[0]), null); String args[] = (String[]) cmd.toArray(new String[0]); if (compilerClass.equals("mxmlc")) { flex2.tools.Mxmlc.mxmlc(args); exitval[0] = new Integer(flex2.compiler.util.ThreadLocalToolkit.errorCount()); } else if (compilerClass.equals("compc")) { flex2.tools.Compc.compc(args); exitval[0] = new Integer(flex2.compiler.util.ThreadLocalToolkit.errorCount()); } } }; try { worker.start(); worker.join(); } catch (java.lang.InterruptedException e) { throw new CompilerError("Errors from compiler, output file not created" + e); } finally { // Restore system output and err streams System.setErr(serr); System.setOut(sout); } try { nerr.flush(); nout.flush(); System.out.println("compiler output is " + bout.toString()); OutputCollector outcollect = new OutputCollector(new ByteArrayInputStream(bout.toByteArray())); ExternalCompilerErrorCollector errcollect = new ExternalCompilerErrorCollector( new ByteArrayInputStream(berr.toByteArray()), tunits); outcollect.start(); errcollect.start(); outcollect.join(); errcollect.join(); if (outcollect.getException() != null) { System.err.println("Error collecting compiler output: " + outcollect.getException()); // TODO: [2007-11-20 dda] log this } String compilerOutput = outcollect.getOutput(); if (compilerOutput.length() > 0) { System.err.println("compiler output:\n" + compilerOutput); } if (errcollect.getException() != null) { System.err.println("Error collecting compiler output: " + errcollect.getException()); // TODO: [2007-11-20 dda] log this } List severe = errcollect.getSevereErrors(); if (severe.size() > 0) { for (Iterator iter = severe.iterator(); iter.hasNext();) { String errstr = "SEVERE ERROR: " + (String) iter.next(); bigErrorString.add(errstr); System.err.println(errstr); } } List errs = errcollect.getErrors(); if (errs.size() > 0) { System.err.println("ERRORS: "); for (Iterator iter = errs.iterator(); iter.hasNext();) { ExternalCompilerError err = (ExternalCompilerError) iter.next(); TranslationUnit tunit = err.getTranslationUnit(); String srcLineStr; TranslationUnit.SourceFileLine srcFileLine; // actualSrcLine is the name/linenumber of the actual files // used in compilation, not the original sources. String actualSrcFile = null; if (tunit == null) { actualSrcFile = "(unknown)"; } else { actualSrcFile = tunit.getSourceFileName(); if (actualSrcFile == null) actualSrcFile = "(" + tunit.getName() + ")"; } String actualSrcLine = "[" + actualSrcFile + ": " + err.getLineNumber() + "] "; if (tunit == null) { srcLineStr = "tunit/line unknown: "; } else if ((srcFileLine = tunit.originalLineNumber(err.getLineNumber())) == null) { srcLineStr = "line unknown: "; } else { srcLineStr = srcFileLine.sourcefile.name + ": " + srcFileLine.line + ": "; } System.err.println(actualSrcLine + srcLineStr + err.getErrorString()); bigErrorString.add(srcLineStr + err.cleanedErrorString()); } } if (exitval[0].intValue() != 0) { System.err.println("FAIL: compiler returned " + exitval[0].intValue()); } } catch (InterruptedException ie) { throw new CompilerError("Interrupted compiler"); } System.err.println("Done executing compiler"); if (!new File(outfileName).exists()) { System.err.println("Intermediate file " + outfileName + ": does not exist"); if (bigErrorString.str.length() > 0) { throw new CompilerError(bigErrorString.str); } else { throw new CompilerError("Errors from compiler, output file not created"); } } } public static String FLEX_HOME() throws IOException { return new File(LPS.HOME() + File.separator + "WEB-INF").getCanonicalPath(); } /** * Return a pathname given by a property in the LPS properties. * If the path not absolute, it is relative to the LFC directory. */ public static String getFlexPathname(String subpath) throws IOException { return new File(FLEX_HOME() + File.separator + subpath).getCanonicalPath(); } /** * Tells whether to exec a subprocess to run the flex compiler. * Uses lps.properties entry for compiler.swf9.execflex */ public static boolean execFlex() { return "true".equals(LPS.getProperty("compiler.swf9.execflex")); } /** * Return a boolean value given by a property in the LPS properties. */ public static boolean getLPSBoolean(String propname, boolean defaultValue) { String valueString = LPS.getProperty(propname); if (valueString == null) return defaultValue; return Boolean.getBoolean(valueString); } /** * Get the file name of the LFC shared library for SWF9. */ public static String getLFCLibrary(String runtime, boolean debug, boolean backtrace) { return LPS.getLFCDirectory() + File.separator + LPS.getLFCname(runtime, debug, false, backtrace, false); } /** * Get the relative URL of the LFC shared library for SWF9. */ public static String getLFCLibraryRelativeURL(String runtime, boolean debug, boolean backtrace) { return LPS.getLFCname(runtime, debug, false, backtrace, false).replaceFirst("swc$", "swf"); } public static boolean isWindows() { String osname = System.getProperty("os.name"); assert osname != null; return osname.startsWith("Windows"); } /** * Compile the given translation units, producing a binary output. */ public byte[] compileTranslationUnits(List tunits, boolean buildSharedLibrary) throws IOException { List cmd = new ArrayList(); String outfilebase; String exeSuffix = isWindows() ? ".exe" : ""; boolean debug = options.getBoolean(Compiler.DEBUG_SWF9); boolean backtrace = options.getBoolean(Compiler.DEBUG_BACKTRACE); boolean nameFunctions = options.getBoolean(Compiler.NAME_FUNCTIONS); // NB: this code used to call execCompileCommand, and pass in the pathname of // a shell script to invoke the flex compiler. It now calls callJavaCompileCommand // to directly call into the flex jar file now. // // The first arg in the cmd list is the name 'compc' or 'mxmlc', // which will be mapped to the appropriate class in // callJavaCompileCommand if (buildSharedLibrary) { outfilebase = "app.swc"; cmd.add("compc"); cmd.add(getFlexPathname("bin" + File.separator + "compc" + exeSuffix)); } else { outfilebase = "app.swf"; cmd.add("mxmlc"); cmd.add(getFlexPathname("bin" + File.separator + "mxmlc" + exeSuffix)); } // Path to the flex compiler config file cmd.add("-load-config=" + getFlexPathname("frameworks/flex-config.xml")); // -compiler.source-path [path-element] [...] alias -sp //list of path elements that form the roots of ActionScript class //hierarchies (repeatable) // file-specs [path-element] [...] a list of source files to compile, the last file specified will be //used as the target application (repeatable, default variable) String outfilename = workdir.getPath() + File.separator + outfilebase; boolean swf9Warnings = getLPSBoolean("compiler.swf9.warnings", true); if (!swf9Warnings) { cmd.add("-compiler.show-actionscript-warnings=false"); } cmd.add("-compiler.source-path+=" + workdir.getPath()); for (int i = 1; i <= maxSubdirnum; i++) { cmd.add("-compiler.source-path+=" + workdir.getPath() + File.separator + i); } if (nameFunctions) { // Ensure function names and source location information is in // the binary for debugging cmd.add("-debug=true"); } cmd.add("-compiler.headless-server=true"); cmd.add("-compiler.fonts.advanced-anti-aliasing=true"); cmd.add("-output"); cmd.add(outfilename); String runtime = ((String) options.get(Compiler.RUNTIME)); if (buildSharedLibrary) { // must be last before list of classes to follow. cmd.add("-include-classes"); // For LFC library, we list all the classes. for (Iterator iter = tunits.iterator(); iter.hasNext();) { TranslationUnit tunit = (TranslationUnit) iter.next(); cmd.add(tunit.getName()); } } else { cmd.add("-default-size"); cmd.add(options.get(Compiler.CANVAS_WIDTH, "800")); cmd.add(options.get(Compiler.CANVAS_HEIGHT, "600")); if (options.getBoolean(Compiler.SWF9_USE_RUNTIME_SHARED_LIB)) { // // TODO [hqm 2008-11] This usage of the Flash // "runtime-shared-library" feature does not work yet. See LPP-7387 cmd.add("-runtime-shared-library-path=" + getLFCLibrary(runtime, debug, backtrace) + "," + "lib" + File.separator + getLFCLibraryRelativeURL(runtime, debug, backtrace) + ",," // specifies explicitly empty policy file arg ); } else { cmd.add("-compiler.library-path+=" + getLFCLibrary(runtime, debug, backtrace)); } if (options.getBoolean(Compiler.SWF9_LOADABLE_LIB) || options.getBoolean(Compiler.DEBUG_EVAL)) { // Don't include the LFC in this app cmd.add("-external-library-path+=" + getLFCLibrary(runtime, debug, backtrace)); } if (options.getBoolean(Compiler.SWF9_LOADABLE_LIB)) { // If it's a loadable lib, check links against the main app, // but don't link those classes in. We do this by declaring the main app // source working directory as a external-library-path cmd.add("-compiler.source-path+=" + workdir); cmd.add("-external-library-path+=" + workdir); } // Add in WEB-INF/flexlib and APPDIR/flexlib to flex library search paths if they exist if ((new File(getFlexPathname("flexlib"))).isDirectory()) { cmd.add("-compiler.library-path+=" + getFlexPathname("flexlib")); } if ((new File(workdir.getPath() + File.separator + "flexlib")).isDirectory()) { cmd.add("-compiler.library-path+=" + getFlexPathname("flexlib")); } // TODO [hqm 2009-01] SEE LPP-7589 - when one loadable library // is loaded by another loadable library at runtime, for some // reason references to global "$as3" get a runtime unknown // variable error. This is the workaround, explicitly including // these globals definitions. For some reason, other globals, // like 'canvas', don't seem to have this issue. cmd.add("-includes"); for (Iterator iter = org.openlaszlo.compiler.Compiler.GLOBAL_RUNTIME_VARS.iterator(); iter.hasNext();) { String varname = (String) iter.next(); cmd.add(varname); } } if ("swf10".equals((String) options.get(Compiler.RUNTIME))) { cmd.add("-target-player=10.0.0"); } else if ("swf9".equals((String) options.get(Compiler.RUNTIME))) { cmd.add("-target-player=9.0.0"); } if (options.getBoolean(Compiler.INCREMENTAL_COMPILE)) { cmd.add("-compiler.incremental=true"); } if (!buildSharedLibrary) { String mainclassname = (String) options.get(Compiler.SWF9_WRAPPER_CLASSNAME); // Insert preloader frame, unless we're compiling a loadable library if (!(options.getBoolean(Compiler.SWF9_LOADABLE_LIB) || options.getBoolean(Compiler.DEBUG_EVAL))) { // Put application on second frame... cmd.add("-frame"); cmd.add("two"); cmd.add(mainclassname); // List the preloader .as file - the application is on the second frame cmd.add("-file-specs=" + workdir.getPath() + File.separator + "LzPreloader.as"); } else { // For the application, we just list one .as file cmd.add("-file-specs=" + workdir.getPath() + File.separator + mainclassname + ".as"); } } // clear out any previously compiled object file new File(outfilename).delete(); mFlexTime = System.currentTimeMillis(); // Call the Flex compiler, either in its own exec'ed process or in a thread if (execFlex()) { execCompileCommand(cmd, workdir.getPath(), tunits, outfilename); } else { callJavaCompileCommand(cmd, workdir.getPath(), tunits, outfilename); } //System.err.println("elapsed time in .as file writing: "+(mElapsed/1000)+" msec"); //System.err.println("elapsed time in flex compilation: "+(System.currentTimeMillis() - mFlexTime)+" msec"); return getBytes(outfilename); } /** * Get a unique file name for the class or global variable. Flex * requires that classes and global variables must be defined * within matching file names. For example 'var FooBar' must be in * FooBar.as, and 'class foobar' must be in foobar.as. But some * file systems (like FAT32 and AFS+) do not allow file names in the * same directory that only differ by case. In order to allow * names that differ by case, we use subdirectories on an as needed * basis. For example, if we see names in the order: * "foo" "fOO" "BAR" "Foo" "bar" * the file/directory names will be used: * <pre> * "./foo.as" * "./1/fOO.as" * "./bar.as" * "./2/Foo.as" * "./1/bar.as" * </pre> * This system guarantees usable file names on all systems, * normally keeping all files in the top level directory, and * minimizes the number of subdirectories. Each subdirectory * requires an additional '-compiler.source-path+=...' argument * to flex, and we'd rather not test whether there is a limit. * * This method also checks for class/global var names that have been * used before and throws an error. * * A side effect of this method is to set the maxSubdirnum to the * maximum of the subdirectory numbers used. * * @throw CompilerError for class/var names used previously. */ private String getFileNameForClassName(String name, boolean isClass) { String lower = name.toLowerCase(); List list = (List) uniqueFileNames.get(lower); if (list == null) { list = new ArrayList(); uniqueFileNames.put(lower, list); } else { for (Iterator iter = list.iterator(); iter.hasNext();) { UniqueGlobalName unique = (UniqueGlobalName) iter.next(); if (name.equals(unique.globalName) && isClass == unique.isClass) { String what = isClass ? "class" : "global var"; throw new CompilerError("cannot declare " + what + " name more than once: \"" + name + "\""); } } } int dirnum = list.size(); UniqueGlobalName unique = new UniqueGlobalName(); unique.globalName = name; unique.isClass = isClass; unique.subdirnum = dirnum; list.add(unique); String subdirname = workdir.getPath(); if (dirnum > 0) { subdirname += File.separator + dirnum; } if (dirnum > maxSubdirnum) { (new File(subdirname)).mkdirs(); maxSubdirnum = dirnum; } return subdirname + File.separator + name + ".as"; } /** * Return the number of newlines in the string. */ public static int countLines(String str) { int count = 0; int pos = -1; while ((pos = str.indexOf('\n', pos + 1)) > 0) { count++; } return count; } // For performance metering, used for measuring compilation times long mElapsed = 0; long mFlexTime = 0; /** * Write a file given by the translation unit, and using the * given pre and post text. * @throw CompilerError for any write errors, or class name conflicts */ public void writeFile(TranslationUnit tunit, String pre, String post) { String name = tunit.getName(); String body = tunit.getContents(); String infilename = getFileNameForClassName(name, tunit.isClass()); tunit.setSourceFileName(infilename); tunit.setLineOffset(countLines(pre)); if (options.getBoolean(Compiler.PROGRESS)) { System.err.println("Creating: " + infilename); } FileOutputStream fos = null; String content = pre + body + post; File diskfile = new File(infilename); long startTime = System.nanoTime(); if (options.getBoolean(Compiler.INCREMENTAL_COMPILE)) { if (diskfile.exists()) { long lastWritten = diskfile.lastModified(); long classModified = tunit.getLastModified(); // If the lzx source file was modified more recently than the generated .as file, then // write out a new .as file. if (classModified >= lastWritten) { //System.err.print("!"); //System.err.println(" MODIFIED: "+tunit.getLZXFilename()+" asfile: "+tunit.getSourceFileName() + ":: "+new Date(lastWritten)+", lzxfile: "+new Date(classModified)+"\n"); } else { long elapsedTime = System.nanoTime() - startTime; mElapsed += (elapsedTime / 1000); // System.err.println("unmodified: "+tunit.getLZXFilename()+" asfile: "+tunit.getSourceFileName() + ":: "+new Date(lastWritten)+", lzxfile: "+new Date(classModified)+"\n"); return; } } else { //System.err.println("diskfile for tunit "+tunit+" does not exist: "+diskfile); } } try { fos = new FileOutputStream(infilename); fos.write(pre.getBytes()); fos.write(body.getBytes()); fos.write(post.getBytes()); fos.close(); } catch (IOException ioe) { System.err.println("Exception in postprocessing, file=" + infilename + ": " + ioe); throw new CompilerError("Exception creating files for external compilation: " + ioe); } finally { closeit(fos); } long elapsedTime = System.nanoTime() - startTime; mElapsed += (elapsedTime / 1000); } } /** * @copyright Copyright 2006-2010 Laszlo Systems, Inc. All Rights * Reserved. Use is subject to license terms. */