Java tutorial
/* * Maven Launch4j Plugin * Copyright (c) 2006 Paul Jungwirth * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package com.akathist.maven.plugins.launch4j; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Date; import java.util.Enumeration; import java.util.List; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import net.sf.launch4j.Builder; import net.sf.launch4j.BuilderException; import net.sf.launch4j.config.Config; import net.sf.launch4j.config.ConfigPersister; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.factory.ArtifactFactory; import org.apache.maven.artifact.repository.ArtifactRepository; import org.apache.maven.artifact.resolver.ArtifactNotFoundException; import org.apache.maven.artifact.resolver.ArtifactResolutionException; import org.apache.maven.artifact.resolver.ArtifactResolver; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; /** * Wraps a jar in a Windows executable. * * @goal launch4j * @phase package * @requiresDependencyResolution compile */ public class Launch4jMojo extends AbstractMojo { private static final String LAUNCH4J_ARTIFACT_ID = "launch4j"; private static final String LAUNCH4J_GROUP_ID = "net.sf.launch4j"; /** * The dependencies required by the project. * * @parameter default-value="${project.artifacts}" * @required * @readonly */ private Set dependencies; /** * The user's current project. * * @parameter default-value="${project}" * @required * @readonly */ private MavenProject project; /** * The user's plugins (including, I hope, this one). * * @parameter default-value="${project.build.plugins}" * @required * @readonly */ private List plugins; /** * Used to look up Artifacts in the remote repository. * * @@@parameter expression="${component.org.apache.maven.artifact.factory.ArtifactFactory}" * @component * @required * @readonly */ private ArtifactFactory factory; /** * The user's local repository * * @parameter default-value="${localRepository}" * @required * @readonly */ private ArtifactRepository localRepository; /** * The artifact resolver used to grab the binary bits that launch4j needs. * * @component */ private ArtifactResolver resolver; /** * The dependencies of this plugin. * Used to get the Launch4j artifact version. * * @parameter default-value="${plugin.artifacts}" */ private java.util.List<Artifact> pluginArtifacts; /** * The base of the current project. * * @parameter default-value="${basedir}" * @required * @readonly */ private File basedir; /** * Whether you want a gui or console app. * Valid values are "gui" and "console." * If you say gui, then launch4j will run your app from javaw instead of java * in order to avoid opening a DOS window. * Choosing gui also enables other options like taskbar icon and a splash screen. * * @parameter * @required */ private String headerType; /** * The name of the executable you want launch4j to produce. * The path, if relative, is relative to the pom.xml. * * @parameter default-value="${project.build.directory}/${project.artifactId}.exe" */ private File outfile; /** * The jar to bundle inside the executable. * The path, if relative, is relative to the pom.xml. * <p> * If you don't want to wrap the jar, then this value should be the runtime path * to the jar relative to the executable. You should also set dontWrapJar to true. * <p> * You can only bundle a single jar. Therefore, you should either create a jar that contains * your own code plus all your dependencies, or you should distribute your dependencies alongside * the executable. * * @parameter default-value="${project.build.directory}/${project.build.finalName}.jar" */ private String jar; /** * Whether the executable should wrap the jar or not. * * @parameter default-value=false */ private boolean dontWrapJar; /** * The title of the error popup if something goes wrong trying to run your program, * like if java can't be found. If this is a console app and not a gui, then this value * is used to prefix any error messages, as in ${errTitle}: ${errorMessage}. * * @parameter */ private String errTitle; /** * downloadUrl (?) * * @parameter */ private String downloadUrl; /** * supportUrl (?) * * @parameter */ private String supportUrl; /** * Constant command line arguments to pass to your program's main method. * Actual command line arguments entered by the user will appear after these. * * @parameter */ private String cmdLine; /** * Changes to the given directory, relative to the executable, before running your jar. * If set to <code>.</code> the current directory will be where the executable is. * If omitted, the directory will not be changed. * * @parameter */ private String chdir; /** * Priority class of windows process. * Valid values are "normal" (default), "idle" and "high". * @see <a href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms685100(v=vs.85).aspx">MSDN: Scheduling Priorities</a> * * @parameter default-value="normal" */ private String priority; /** * If true, the executable waits for the java application to finish before returning its exit code. * Defaults to false for gui applications. Has no effect for console applications, which always wait. * * @parameter default-value=false */ private boolean stayAlive; /** * The icon to use in the taskbar. Must be in ico format. * * @parameter */ private File icon; /** * Object files to include. Used for custom headers only. * * @parameter */ private List objs; /** * Win32 libraries to include. Used for custom headers only. * * @parameter */ private List libs; /** * Variables to set. * * @parameter */ private List vars; /** * Details about the supported jres. * * @parameter * @required */ private Jre jre; /** * Details about the classpath your application should have. * This is required if you are not wrapping a jar. * * @parameter */ private ClassPath classPath; /** * Details about whether to run as a single instance. * * @parameter */ private SingleInstance singleInstance; /** * Details about the splash screen. * * @parameter */ private Splash splash; /** * Lots of information you can attach to the windows process. * * @parameter */ private VersionInfo versionInfo; /** * Various messages you can display. * * @parameter */ private Messages messages; /** * Windows manifest file (a XML file) with the same name as .exe file (myapp.exe.manifest) * * @parameter */ private File manifest; private File getJar() { return new File(jar); } public void execute() throws MojoExecutionException { if (getLog().isDebugEnabled()) printState(); Config c = new Config(); c.setHeaderType(headerType); c.setOutfile(outfile); c.setJar(getJar()); c.setDontWrapJar(dontWrapJar); c.setErrTitle(errTitle); c.setDownloadUrl(downloadUrl); c.setSupportUrl(supportUrl); c.setCmdLine(cmdLine); c.setChdir(chdir); c.setPriority(priority); c.setStayAlive(stayAlive); c.setManifest(manifest); c.setIcon(icon); c.setHeaderObjects(objs); c.setLibs(libs); c.setVariables(vars); if (classPath != null) { c.setClassPath(classPath.toL4j(dependencies)); } if (jre != null) { c.setJre(jre.toL4j()); } if (singleInstance != null) { c.setSingleInstance(singleInstance.toL4j()); } if (splash != null) { c.setSplash(splash.toL4j()); } if (versionInfo != null) { c.setVersionInfo(versionInfo.toL4j()); } if (messages != null) { c.setMessages(messages.toL4j()); } ConfigPersister.getInstance().setAntConfig(c, getBaseDir()); File workdir = setupBuildEnvironment(); Builder b = new Builder(new MavenLog(getLog()), workdir); try { b.build(); } catch (BuilderException e) { getLog().error(e); throw new MojoExecutionException("Failed to build the executable; please verify your configuration.", e); } } /** * Prepares a little directory for launch4j to do its thing. Launch4j needs a bunch of object files * (in the w32api and head directories) and the ld and windres binaries (in the bin directory). * The tricky part is that launch4j picks this directory based on where its own jar is sitting. * In our case, the jar is going to be sitting in the user's ~/.m2 repository. That's okay: we know * maven is allowed to write there. So we'll just add our things to that directory. * <p> * This approach is not without flaws. * It risks two processes writing to the directory at the same time. * But fortunately, once the binary bits are in place, we don't do any more writing there, * and launch4j doesn't write there either. * Usually ~/.m2 will only be one system or another. * But if it's an NFS mount shared by several system types, this approach will break. * <p> * Okay, so here is a better proposal: package the plugin without these varying binary files, * and put each set of binaries in its own tarball. Download the tarball you need to ~/.m2 and * unpack it. Then different systems won't contend for the same space. But then I'll need to hack * the l4j code so it permits passing in a work directory and doesn't always base it on * the location of its own jarfile. * * @return the work directory. */ private File setupBuildEnvironment() throws MojoExecutionException { Artifact binaryBits = chooseBinaryBits(); retrieveBinaryBits(binaryBits); return unpackWorkDir(binaryBits); } /** * Unzips the given artifact in-place and returns the newly-unzipped top-level directory. * Writes a marker file to prevent unzipping more than once. */ private File unpackWorkDir(Artifact a) throws MojoExecutionException { String version = a.getVersion(); File platJar = a.getFile(); File dest = platJar.getParentFile(); File marker = new File(dest, platJar.getName() + ".unpacked"); // If the artifact is a SNAPSHOT, then a.getVersion() will report the long timestamp, // but getFile() will be 1.1-SNAPSHOT. // Since getFile() doesn't use the timestamp, all timestamps wind up in the same place. // Therefore we need to expand the jar every time, if the marker file is stale. if (marker.exists() && marker.lastModified() > platJar.lastModified()) { // if (marker.exists() && marker.platJar.getName().indexOf("SNAPSHOT") == -1) { getLog().info("Platform-specific work directory already exists: " + dest.getAbsolutePath()); } else { JarFile jf = null; try { // trying to use plexus-archiver here is a miserable waste of time: jf = new JarFile(platJar); Enumeration en = jf.entries(); while (en.hasMoreElements()) { JarEntry je = (JarEntry) en.nextElement(); File outFile = new File(dest, je.getName()); File parent = outFile.getParentFile(); if (parent != null) parent.mkdirs(); if (je.isDirectory()) { outFile.mkdirs(); } else { InputStream in = jf.getInputStream(je); byte[] buf = new byte[1024]; int len; FileOutputStream fout = null; try { fout = new FileOutputStream(outFile); while ((len = in.read(buf)) >= 0) { fout.write(buf, 0, len); } fout.close(); fout = null; } finally { if (fout != null) { try { fout.close(); } catch (IOException e2) { } // ignore } } outFile.setLastModified(je.getTime()); } } } catch (IOException e) { throw new MojoExecutionException("Error unarchiving " + platJar, e); } finally { try { if (jf != null) jf.close(); } catch (IOException e) { } // ignore } try { marker.createNewFile(); marker.setLastModified(new Date().getTime()); } catch (IOException e) { getLog().warn("Trouble creating marker file " + marker, e); } } String n = platJar.getName(); File workdir = new File(dest, n.substring(0, n.length() - 4)); setPermissions(workdir); return workdir; } /** * Chmods the helper executables ld and windres on systems where that is necessary. */ private void setPermissions(File workdir) throws MojoExecutionException { if (!System.getProperty("os.name").startsWith("Windows")) { Runtime r = Runtime.getRuntime(); try { r.exec("chmod 755 " + workdir + "/bin/ld").waitFor(); r.exec("chmod 755 " + workdir + "/bin/windres").waitFor(); } catch (InterruptedException e) { getLog().warn("Interrupted while chmodding platform-specific binaries", e); } catch (IOException e) { getLog().warn("Unable to set platform-specific binaries to 755", e); } } } /** * Downloads the platform-specific parts, if necessary. */ private void retrieveBinaryBits(Artifact a) throws MojoExecutionException { try { resolver.resolve(a, project.getRemoteArtifactRepositories(), localRepository); } catch (ArtifactNotFoundException e) { throw new MojoExecutionException("Can't find platform-specific components", e); } catch (ArtifactResolutionException e) { throw new MojoExecutionException("Can't retrieve platform-specific components", e); } } /** * Decides which platform-specific bundle we need, based on the current operating system. */ private Artifact chooseBinaryBits() throws MojoExecutionException { String plat; String os = System.getProperty("os.name"); getLog().debug("OS = " + os); // See here for possible values of os.name: // http://lopica.sourceforge.net/os.html if (os.startsWith("Windows")) { plat = "win32"; } else if ("Linux".equals(os)) { plat = "linux"; } else if ("Solaris".equals(os) || "SunOS".equals(os)) { plat = "solaris"; } else if ("Mac OS X".equals(os) || "Darwin".equals(os)) { plat = "mac"; } else { throw new MojoExecutionException("Sorry, Launch4j doesn't support the '" + os + "' OS."); } return factory.createArtifactWithClassifier(LAUNCH4J_GROUP_ID, LAUNCH4J_ARTIFACT_ID, getLaunch4jVersion(), "jar", "workdir-" + plat); } private File getBaseDir() { return basedir; } /** * Just prints out how we were configured. */ private void printState() { Log log = getLog(); log.debug("headerType = " + headerType); log.debug("outfile = " + outfile); log.debug("jar = " + jar); log.debug("dontWrapJar = " + dontWrapJar); log.debug("errTitle = " + errTitle); log.debug("downloadUrl = " + downloadUrl); log.debug("supportUrl = " + supportUrl); log.debug("cmdLine = " + cmdLine); log.debug("chdir = " + chdir); log.debug("priority = " + priority); log.debug("stayAlive = " + stayAlive); log.debug("icon = " + icon); log.debug("objs = " + objs); log.debug("libs = " + libs); log.debug("vars = " + vars); if (singleInstance != null) { log.debug("singleInstance.mutexName = " + singleInstance.mutexName); log.debug("singleInstance.windowTitle = " + singleInstance.windowTitle); } else { log.debug("singleInstance = null"); } if (jre != null) { log.debug("jre.path = " + jre.path); log.debug("jre.minVersion = " + jre.minVersion); log.debug("jre.maxVersion = " + jre.maxVersion); log.debug("jre.jdkPreference = " + jre.jdkPreference); log.debug("jre.initialHeapSize = " + jre.initialHeapSize); log.debug("jre.initialHeapPercent = " + jre.initialHeapPercent); log.debug("jre.maxHeapSize = " + jre.maxHeapSize); log.debug("jre.maxHeapPercent = " + jre.maxHeapPercent); log.debug("jre.opts = " + jre.opts); } else { log.debug("jre = null"); } if (classPath != null) { log.debug("classPath.mainClass = " + classPath.mainClass); log.debug("classPath.addDependencies = " + classPath.addDependencies); log.debug("classPath.jarLocation = " + classPath.jarLocation); log.debug("classPath.preCp = " + classPath.preCp); log.debug("classPath.postCp = " + classPath.postCp); } else { log.info("classpath = null"); } if (splash != null) { log.debug("splash.file = " + splash.file); log.debug("splash.waitForWindow = " + splash.waitForWindow); log.debug("splash.timeout = " + splash.timeout); log.debug("splash.timoutErr = " + splash.timeoutErr); } else { log.debug("splash = null"); } if (versionInfo != null) { log.debug("versionInfo.fileVersion = " + versionInfo.fileVersion); log.debug("versionInfo.txtFileVersion = " + versionInfo.txtFileVersion); log.debug("versionInfo.fileDescription = " + versionInfo.fileDescription); log.debug("versionInfo.copyright = " + versionInfo.copyright); log.debug("versionInfo.productVersion = " + versionInfo.productVersion); log.debug("versionInfo.txtProductVersion = " + versionInfo.txtProductVersion); log.debug("versionInfo.productName = " + versionInfo.productName); log.debug("versionInfo.companyName = " + versionInfo.companyName); log.debug("versionInfo.internalName = " + versionInfo.internalName); log.debug("versionInfo.originalFilename = " + versionInfo.originalFilename); } else { log.debug("versionInfo = null"); } if (messages != null) { log.debug("messages.startupErr = " + messages.startupErr); log.debug("messages.bundledJreErr = " + messages.bundledJreErr); log.debug("messages.jreVersionErr = " + messages.jreVersionErr); log.debug("messages.launcherErr = " + messages.launcherErr); log.debug("messages.instanceAlreadyExistsMsg = " + messages.instanceAlreadyExistsMsg); } else { log.debug("messages = null"); } } /** * The Launch4j version used by the plugin. * We want to download the platform-specific bundle whose version matches the Launch4j version, * so we have to figure out what version the plugin is using. * * @return * @throws MojoExecutionException */ private String getLaunch4jVersion() throws MojoExecutionException { String version = null; for (Artifact artifact : pluginArtifacts) { if (LAUNCH4J_GROUP_ID.equals(artifact.getGroupId()) && LAUNCH4J_ARTIFACT_ID.equals(artifact.getArtifactId()) && "core".equals(artifact.getClassifier())) { version = artifact.getVersion(); getLog().debug("Found launch4j version " + version); break; } } if (version == null) { throw new MojoExecutionException("Impossible to find which Launch4j version to use"); } return version; } }