/* * Copyright 2012 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package dorkbox.build; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import javax.tools.Diagnostic; import javax.tools.DiagnosticCollector; import javax.tools.JavaCompiler; import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; import com.esotericsoftware.wildcard.Paths; import com.esotericsoftware.yamlbeans.YamlConfig; import com.esotericsoftware.yamlbeans.YamlException; import com.esotericsoftware.yamlbeans.YamlWriter; import com.esotericsoftware.yamlbeans.scalar.ScalarSerializer; import dorkbox.Build; import dorkbox.BuildOptions; import dorkbox.build.util.ByteClassloader; import dorkbox.build.util.JavaMemFileManager; import dorkbox.build.util.PreJarAction; import dorkbox.build.util.jar.JarOptions; import dorkbox.build.util.jar.JarSigner; import dorkbox.build.util.jar.JarUtil; import dorkbox.license.License; import dorkbox.license.LicenseType; import dorkbox.util.FileUtil; import dorkbox.util.OS; public class ProjectJava extends ProjectBasics { protected ArrayList extraArgs; protected Paths sourcePaths = new Paths(); public Paths classPaths = new Paths(); private boolean includeSource; public List licenses = new ArrayList(); private transient PreJarAction preJarAction; private Class mainClass; private ByteClassloader bytesClassloader = null; public static ProjectJava create(String projectName) { ProjectJava project = new ProjectJava(projectName); deps.put(projectName, project); return project; } public ProjectJava(String projectName) { super(projectName); checksum(this.sourcePaths); checksum(this.classPaths); } public ProjectJava sourcePath(Paths sourcePaths) { if (sourcePaths == null) { throw new NullPointerException("Source paths cannot be null!"); } this.sourcePaths.add(sourcePaths); // ALWAYS add the source paths to be checksumed! checksum(sourcePaths); return this; } public ProjectJava sourcePath(String srcDir) { if (srcDir.endsWith("src")) { String parent = new File(srcDir).getAbsoluteFile().getParent(); checksum(new Paths(parent)); } return sourcePath(new Paths(srcDir, "./")); } public ProjectJava sourcePath(String dir, String... patterns) { return sourcePath(new Paths(dir, patterns)); } public ProjectJava classPath(Paths classPaths) { if (classPaths == null) { throw new NullPointerException("Class paths cannot be null!"); } this.classPaths.add(classPaths); return this; } public ProjectJava classPath(String dir, String... patterns) { return classPath(new Paths(dir, patterns)); } /** extra files to include when you jar the project */ @Override public ProjectJava extraFiles(Paths filePaths) { super.extraFiles(filePaths); return this; } @Override public final ProjectJava depends(String dependsProjectName) { super.depends(dependsProjectName); return this; } @Override public ProjectJava output() { String lowerCase_outputDir = this.outputDir.toLowerCase(); this.outputDir = STAGING + File.separator + lowerCase_outputDir; String outputFile = lowerCase_outputDir.substring(lowerCase_outputDir.lastIndexOf("/") + 1, lowerCase_outputDir.length()); this.outputFile = outputFile + getExtension(); return this; } public ProjectJava addArg(String arg) { if (this.extraArgs == null) { this.extraArgs = new ArrayList(); } this.extraArgs.add(arg); return this; } @Override protected ProjectJava build(BuildOptions properties) throws Exception { return build(properties, true, true, false, null); } /** always does a build, ignoring checksums */ public void forceBuild(BuildOptions options) throws Exception { forceBuild(options, false, false); } /** always does a build, ignoring checksums */ public void forceBuild(BuildOptions options, boolean deleteCompiledOnComplete, boolean buildJar) throws Exception { File file = FileUtil.normalize(new File(STAGING + File.separator + this.outputFile)); if (file.exists()) { FileUtil.delete(file); } File dir = new File(this.outputDir); if (dir.exists()) { FileUtil.delete(dir); } Build.settings.remove(this.outputDir); build(options, deleteCompiledOnComplete, buildJar, false, null); } // (and add them to the classpath) protected ProjectJava build(BuildOptions options, boolean deleteOnComplete, boolean buildJar, boolean signJar, String signName) throws Exception { Build.log().message(); Build.log().title(" Building").message(this.name, "Output - " + this.outputDir); // exit early if we already built this project if (checkAndBuildDependencies(options)) { return this; } boolean shouldBuild = !verifyChecksums(this, options); if (shouldBuild) { // barf if we don't have source files! if (this.sourcePaths.getFiles().isEmpty()) { throw new RuntimeException("No source files specified for project: " + this.name); } // make sure our dependencies are on the classpath. if (this.dependencies != null) { for (String dep : this.dependencies) { ProjectBasics project = deps.get(dep); this.classPaths.glob(STAGING, project.outputFile); } } compile(this.sourcePaths, this.classPaths, this.outputDir, options); Build.log().message("Compile success."); if (buildJar) { if (this.preJarAction != null) { Build.log().message("Running action before Jar is created..."); this.preJarAction.executeBeforeJarHappens(this.outputDir); } JarOptions jarOptions = new JarOptions(); jarOptions.outputFile = STAGING + File.separator + this.outputFile; jarOptions.inputPaths = new Paths(this.outputDir); jarOptions.extraPaths = this.extraFiles; if (this.mainClass != null) { jarOptions.mainClass = this.mainClass.getCanonicalName(); jarOptions.classpath = this.classPaths; } if (this.includeSource) { jarOptions.sourcePaths = this.sourcePaths; } if (!this.licenses.isEmpty()) { jarOptions.licenses = this.licenses; } jarOptions.createDebugVersion = options.compiler.debugEnabled; JarUtil.jar(jarOptions); if (signJar) { JarSigner.sign(this.outputFile, signName); } // calculate the hash of all the files in the source path saveChecksums(); } } else { Build.log().message("Skipped (nothing changed)"); } if (shouldBuild && deleteOnComplete) { FileUtil.delete(this.outputDir); } return this; } /** * @return true if the checksums for path match the saved checksums and the jar file exists */ boolean verifyChecksums(ProjectBasics project, BuildOptions properties) throws IOException { boolean sourceHashesSame = super.verifyChecksums(properties); if (!sourceHashesSame) { return false; } // if the sources are the same, check the jar file String fileName = project.outputDir; fileName += project.getExtension(); File file = new File(fileName); if (file.exists()) { String jarChecksum = generateChecksum(file); String checkContents = Build.settings.get(fileName, String.class); return jarChecksum != null && jarChecksum.equals(checkContents); } else { // output dir was removed return false; } } /** * Saves the checksums for a given path */ @Override void saveChecksums() throws IOException { super.saveChecksums(); // hash/save the jar file (if there was one) String fileName = this.outputDir; fileName += getExtension(); File file = new File(fileName); if (file.exists()) { String fileChecksum = generateChecksum(file); Build.settings.save(fileName, fileChecksum); } } /** * Compiles into class files. */ public void compile(Paths source, Paths classpath, String outputDir, BuildOptions buildOptions) throws IOException { // if you get messages, such as // warning: [path] bad path element "/x/y/z/lib/fubar-all.jar": no such file or directory // That is because that file exists in a MANIFEST.MF somewhere on the classpath! Find the jar that has that, and rip out // the offending manifest.mf file. // see: http://stackoverflow.com/questions/1344202/bad-path-warning-where-is-it-coming-from if (source.isEmpty()) { throw new IOException("No source files found."); } FileUtil.delete(outputDir); FileUtil.mkdir(outputDir); ArrayList args = new ArrayList(); if (buildOptions.compiler.enableCompilerTrace) { // TODO: Interesting to note, that when COMPILING this with verbose, we can get a list (from the compiler) of EVERY CLASS NEEDED // to run our application! This would be useful in "trimming" the necessary files needed by the JVM. args.add("-verbose"); } if (buildOptions.compiler.debugEnabled) { Build.log().message("Adding debug info."); args.add("-g"); // Generate all debugging information, including local variables. By default, only line number and source file information is generated. } else { args.add("-g:none"); } args.add("-d"); args.add(outputDir); args.add("-encoding"); args.add("UTF-8"); if (OS.getJavaVersion() > buildOptions.compiler.targetJavaVersion) { Build.log().message("Building cross-platform target!"); // if our runtime env. is NOT equal to our target env. args.add("-source"); args.add(buildOptions.getTargetVersion()); args.add("-target"); args.add(buildOptions.getTargetVersion()); args.add("-bootclasspath"); args.add(buildOptions.compiler.crossCompileLibrary.getCrossCompileLibraryLocation(buildOptions.compiler.targetJavaVersion)); } // suppress sun proprietary warnings if (buildOptions.compiler.suppressSunWarnings) { args.add("-XDignore.symbol.file"); } if (this.extraArgs != null) { boolean extraArgsHaveXlint = false; for (String arg : this.extraArgs) { if (arg.startsWith("-Xlint")) { extraArgsHaveXlint = true; break; } } if (!extraArgsHaveXlint) { args.add("-Xlint:all"); } // add any extra arguments args.addAll(this.extraArgs); } JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); if (compiler == null) { throw new RuntimeException("No compiler available. Ensure you are running from a JDK, and not a JRE."); } if (classpath != null && !classpath.isEmpty()) { args.add("-classpath"); args.add(classpath.toString(File.pathSeparator)); } // now compile the code DiagnosticCollector diagnostics = new DiagnosticCollector(); JavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); try { Iterable javaFileObjectsFromFiles; if (this.bytesClassloader == null) { javaFileObjectsFromFiles = ((StandardJavaFileManager)fileManager).getJavaFileObjectsFromFiles(source.getFiles()); } else { fileManager = new JavaMemFileManager((StandardJavaFileManager)fileManager, this.bytesClassloader); ((JavaMemFileManager)fileManager).setSource(source); javaFileObjectsFromFiles = ((JavaMemFileManager)fileManager).getSourceFiles(); } compiler.getTask(null, fileManager, diagnostics, args, null, javaFileObjectsFromFiles).call(); } finally { fileManager.close(); } boolean hasError = false; for (@SuppressWarnings("rawtypes") Diagnostic diagnostic : diagnostics.getDiagnostics()) { if (diagnostic.getKind() == javax.tools.Diagnostic.Kind.ERROR) { hasError = true; break; } } if (hasError) { StringBuilder buffer = new StringBuilder(1024); for (@SuppressWarnings("rawtypes") Diagnostic diagnostic : diagnostics.getDiagnostics()) { if (buffer.length() > 0) { buffer.append("\n"); } buffer.append("Line ").append(diagnostic.getLineNumber()).append(": "); buffer.append(diagnostic.getMessage(null)); } throw new RuntimeException("Compilation errors:\n" + buffer); } compiler = null; System.gc(); try { Thread.sleep(100); } catch (InterruptedException ex) { } } @Override protected String getExtension() { return ".jar"; } public static interface OnJarEntryAction { boolean canHandle(String fileName); int onEntry(String fileName, ByteArrayInputStream inputStream, OutputStream output) throws Exception; } public ProjectJava includeSourceInJar() { this.includeSource = true; return this; } public ProjectJava license(License license) { this.licenses.add(license); return this; } public ProjectJava license(List licenses) { this.licenses.addAll(licenses); return this; } /** Actions that might need to take place before the project is jar'd */ public ProjectJava preJarAction(PreJarAction preJarAction) { this.preJarAction = preJarAction; return this; } /** * Specify the main class. */ public ProjectJava mainClass(Class clazz) { this.mainClass = clazz; return this; } /** * Take all of the parameters of this project, and convert it to a text file. * @throws IOException */ public void toBuildFile() throws IOException { YamlWriter writer = new YamlWriter(new FileWriter("build.oak")); YamlConfig config = writer.getConfig(); config.writeConfig.setWriteRootTags(false); config.setPropertyElementType(ProjectJava.class, "licenses", License.class); config.setPrivateFields(true); config.readConfig.setConstructorParameters(License.class, new Class[]{String.class, LicenseType.class}, new String[] {"licenseName", "licenseType"}); config.readConfig.setConstructorParameters(ProjectJava.class, new Class[]{String.class}, new String[] {"projectName"}); config.setScalarSerializer(Paths.class, new ScalarSerializer() { @Override public Paths read (String value) throws YamlException { String[] split = value.split(File.pathSeparator); Paths paths = new Paths(); for (String s : split) { paths.addFile(s); } return paths; } @Override public String write (Paths paths) throws YamlException { return paths.toString(File.pathSeparator); } }); writer.write(this); writer.close(); } /** * The specified byte loading classloader to save the compiled class bytes into, */ public ProjectJava compilerClassloader(ByteClassloader bytesClassloader) { this.bytesClassloader = bytesClassloader; return this; } }