View Javadoc
1   /*
2    * Copyright (c) 2012-present Sonatype, Inc. All rights reserved.
3    *
4    * This program is licensed to you under the Apache License Version 2.0,
5    * and you may not use this file except in compliance with the Apache License Version 2.0.
6    * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
7    *
8    * Unless required by applicable law or agreed to in writing,
9    * software distributed under the Apache License Version 2.0 is distributed on an
10   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
12   */
13  package org.sonatype.install4j.maven;
14  
15  import java.io.BufferedOutputStream;
16  import java.io.BufferedReader;
17  import java.io.File;
18  import java.io.FileOutputStream;
19  import java.io.FileReader;
20  import java.io.IOException;
21  import java.io.OutputStream;
22  import java.io.OutputStreamWriter;
23  import java.io.PrintWriter;
24  import java.util.ArrayList;
25  import java.util.Collections;
26  import java.util.Iterator;
27  import java.util.List;
28  import java.util.Map.Entry;
29  import java.util.Properties;
30  
31  import org.apache.maven.plugins.annotations.Component;
32  import org.apache.maven.plugins.annotations.Mojo;
33  import org.apache.maven.plugins.annotations.Parameter;
34  import org.apache.maven.project.MavenProjectHelper;
35  import org.apache.tools.ant.taskdefs.ExecTask;
36  
37  /**
38   * Compile installers (via install4jc).
39   *
40   * Execution will skip if <code>install4j.home</code> location is invalid.
41   *
42   * @see <a href="http://resources.ej-technologies.com/install4j/help/doc/cli/options.html">install4j cli options</a>
43   * @since 1.0
44   */
45  @Mojo(name = "compile")
46  public class CompileMojo
47      extends Install4jcMojoSupport
48  {
49    /**
50     * UTF-8 byte-order-mark.  Needed to inform install4j to load variables files as UTF-8 instead of platform encoding.
51     */
52    private static final byte[] BOM = new byte[] {(byte)0xEF, (byte)0xBB, (byte)0xBF};
53  
54    /**
55     * install4j project file.
56     */
57    @Parameter(property = "install4j.projectFile", required = true)
58    private File projectFile;
59  
60    /**
61     * Enables verbose mode. In verbose mode, install4j prints out information about internal processes.
62     */
63    @Parameter(property = "install4j.verbose", defaultValue = "false")
64    private boolean verbose;
65  
66    /**
67     * Enables quiet mode. In quiet mode, no terminal output short of a fatal error will be printed.
68     */
69    @Parameter(property = "install4j.quiet", defaultValue = "false")
70    private boolean quiet;
71  
72    /**
73     * Enables test mode. In test mode, no media files will be generated in the media file directory.
74     */
75    @Parameter(property = "install4j.test", defaultValue = "false")
76    private boolean test;
77  
78    /**
79     * Enables incremental test execution.
80     */
81    @Parameter(property = "install4j.incremental", defaultValue = "false")
82    private boolean incremental;
83  
84    /**
85     * Create additional debug installers for each media file.
86     */
87    @Parameter(property = "install4j.debug", defaultValue = "false")
88    private boolean debug;
89  
90    /**
91     * Disable LZMA and Pack200 compression.
92     */
93    @Parameter(property = "install4j.faster", defaultValue = "false")
94    private boolean faster;
95  
96    /**
97     * Disable code signing.
98     */
99    @Parameter(property = "install4j.disableSigning", defaultValue = "false")
100   private boolean disableSigning;
101 
102   /**
103    * Disable JRE bundling.
104    */
105   @Parameter(property = "install4j.disableBundling", defaultValue = "false")
106   private boolean disableBundling;
107 
108   /**
109    * Preserve temporary staging directory.
110    */
111   @Parameter(property = "install4j.preserve", defaultValue = "false")
112   private boolean preserve;
113 
114   /**
115    * Set the Windows keystore password for the private key that is configured for code signing.
116    */
117   @Parameter(property = "install4j.winKeystorePassword")
118   private String winKeystorePassword;
119 
120   /**
121    * Set the Mac OSX keystore password for the private key that is configured for code signing.
122    */
123   @Parameter(property = "install4j.macKeystorePassword")
124   private String macKeystorePassword;
125 
126   /**
127    * Set the app-specific password for notarizing macOS media files. This only has an effect when run on a macOS machine.
128    */
129   @Parameter(property = "install4j.appleIdPassword")
130   private String appleIdPassword;
131 
132   /**
133    * Override the application version.
134    */
135   @Parameter(property = "install4j.release", defaultValue = "${project.version}")
136   private String release;
137 
138   /**
139    * The output directory for the generated media files.
140    */
141   @Parameter(property = "install4j.destination", defaultValue = "${project.build.directory}/media")
142   private File destination;
143 
144   /**
145    * Only build the media files which have been selected in the install4j IDE.
146    */
147   @Parameter(property = "install4j.buildSelected", defaultValue = "false")
148   private boolean buildSelected;
149 
150   /**
151    * Only build the media files with the specified IDs.
152    */
153   @Parameter(property = "install4j.buildIds")
154   private String buildIds;
155 
156   /**
157    * Only build media files of the specified type.
158    */
159   @Parameter(property = "install4j.mediaTypes")
160   private String mediaTypes;
161 
162   /**
163    * Load variable definitions from a file.
164    */
165   @Parameter(property = "install4j.variableFile")
166   private File variableFile;
167 
168   /**
169    * Override compiler variables with a different values.
170    */
171   @Parameter
172   private Properties variables;
173 
174   /**
175    * File where custom variables are written to pass to install4j.
176    */
177   @Parameter(defaultValue = "${project.build.directory}/install4j-variables.txt")
178   private File variablesTempFile;
179 
180   /**
181    * Set custom jvm arguments on compiler.
182    */
183   @Parameter
184   private List<String> jvmArguments;
185 
186   /**
187    * Attach generated installers.
188    *
189    * Uses the media id as the classifier.
190    */
191   @Parameter(property = "install4j.attach", defaultValue = "false")
192   private boolean attach;
193 
194   @Component
195   private MavenProjectHelper projectHelper;
196 
197   @Override
198   protected void execute(final AntHelper ant, final ExecTask task) throws Exception {
199     task.createArg().setFile(projectFile);
200 
201     // Fail if any error occurs
202     task.setFailonerror(true);
203     task.setFailIfExecutionFails(true);
204 
205     if (jvmArguments != null) {
206       Iterator<String> iter = jvmArguments.iterator();
207       while (iter.hasNext()) {
208         String arg = String.valueOf(iter.next());
209         task.createArg().setValue("-J" + arg);
210       }
211     }
212 
213     if (verbose) {
214       task.createArg().setValue("--verbose");
215     }
216 
217     if (quiet) {
218       task.createArg().setValue("--quiet");
219     }
220 
221     if (test) {
222       task.createArg().setValue("--test");
223     }
224 
225     if (incremental) {
226       task.createArg().setValue("--incremental");
227     }
228 
229     if (debug) {
230       task.createArg().setValue("--debug");
231     }
232 
233     if (faster) {
234       task.createArg().setValue("--faster");
235     }
236 
237     if (disableSigning) {
238       task.createArg().setValue("--disable-signing");
239     }
240 
241     if (disableBundling) {
242       task.createArg().setValue("--disable-bundling");
243     }
244 
245     if (preserve) {
246       task.createArg().setValue("--preserve");
247     }
248 
249     if (winKeystorePassword != null) {
250       task.createArg().setValue("--win-keystore-password");
251       task.createArg().setValue(winKeystorePassword);
252     }
253 
254     if (macKeystorePassword != null) {
255       task.createArg().setValue("--mac-keystore-password");
256       task.createArg().setValue(macKeystorePassword);
257     }
258 
259     if (appleIdPassword != null) {
260       task.createArg().setValue("--apple-id-password");
261       task.createArg().setValue(appleIdPassword);
262     }
263 
264     if (release != null) {
265       task.createArg().setValue("--release");
266       task.createArg().setValue(release);
267     }
268 
269     if (destination != null) {
270       task.createArg().setValue("--destination");
271       task.createArg().setFile(destination);
272     }
273 
274     if (buildSelected) {
275       task.createArg().setValue("--build-selected");
276     }
277 
278     if (buildIds != null) {
279       task.createArg().setValue("--build-ids");
280       task.createArg().setValue(buildIds);
281     }
282 
283     if (mediaTypes != null) {
284       task.createArg().setValue("--media-types");
285       task.createArg().setValue(mediaTypes);
286     }
287 
288     if (variableFile != null || (variables != null && !variables.isEmpty())) {
289       StringBuilder buff = new StringBuilder();
290       if (variableFile != null) {
291         buff.append(variableFile.getPath());
292       }
293 
294       // variables are written to file for coping with command-line length limits
295       if (!variables.isEmpty()) {
296         if (buff.length() > 0) {
297           buff.append(";");
298         }
299         writeVariablesToFile(variables, variablesTempFile);
300         buff.append(variablesTempFile.getPath());
301       }
302 
303       task.createArg().setValue("--var-file");
304       task.createArg().setValue(buff.toString());
305     }
306 
307     task.execute();
308 
309     if (attach) {
310       for (AttachedFile attachedFile : parseAttachedFiles()) {
311         projectHelper.attachArtifact(
312             project,
313             attachedFile.type,
314             attachedFile.classifier,
315             attachedFile.file
316         );
317       }
318 
319       // attach non-media files which the compiler generates
320       maybeAttachFile("txt", "output", new File(destination, "output.txt"));
321       maybeAttachFile("xml", "updates", new File(destination, "updates.xml"));
322       maybeAttachFile("txt", "md5sums", new File(destination, "md5sums"));
323     }
324   }
325 
326   private void writeVariablesToFile(final Properties variables, final File file) throws IOException {
327     log.info("Writing variables to: " + file.getPath());
328     OutputStream output = new BufferedOutputStream(new FileOutputStream(file));
329     try {
330       // Force install4j to read file as UTF-8
331       output.write(BOM);
332 
333       PrintWriter writer = new PrintWriter(new OutputStreamWriter(output, "UTF-8"));
334       for (Entry<Object, Object> entry : variables.entrySet()) {
335         writer.print(entry.getKey());
336         writer.print('=');
337         writer.println(entry.getValue());
338       }
339 
340       writer.flush();
341     }
342     finally {
343       output.close();
344     }
345   }
346 
347   private void maybeAttachFile(final String type, final String classifier, final File file) {
348     if (!file.exists()) {
349       log.warn("File missing; unable to attach file: " + file);
350       return;
351     }
352     projectHelper.attachArtifact(project, type, classifier, file);
353   }
354 
355   private static class AttachedFile
356   {
357     public final File file;
358 
359     public final String type;
360 
361     public final String classifier;
362 
363     private AttachedFile(final File file, final String classifier) {
364       this.file = file;
365       this.type = getType(file);
366       // TODO: Should ensure this is a valid classifier (replace spaces, etc).
367       this.classifier = classifier;
368     }
369 
370     private static String getType(final File file) {
371       String path = file.getAbsolutePath();
372 
373       // special case for compound '.' extensions
374       if (path.endsWith(".tar.gz")) {
375         return "tar.gz";
376       }
377       else if (path.endsWith(".tar.bz2")) {
378         return "tar.bz2";
379       }
380 
381       int i = path.lastIndexOf(".");
382       return path.substring(i + 1, path.length());
383     }
384   }
385 
386   private List<AttachedFile> parseAttachedFiles() throws Exception {
387     File file = new File(destination, "output.txt");
388     if (!file.exists()) {
389       log.warn("Missing output.txt file: " + file);
390       return Collections.emptyList();
391     }
392 
393     log.debug("Parsing: " + file);
394 
395     BufferedReader reader = new BufferedReader(new FileReader(file));
396     List<AttachedFile> files = new ArrayList<AttachedFile>();
397     String line;
398     while ((line = reader.readLine()) != null) {
399       if (line.startsWith("#")) {
400         // ignore comments
401         continue;
402       }
403 
404       log.debug("Read: " + line);
405 
406       // fields are tab-delimited:
407       // id | media file type | display name | media file path
408       String[] parts = line.split("\t");
409       AttachedFile attachedFile = new AttachedFile(
410           normalize(destination, parts[3]), // media file path
411           parts[0]  // id
412       );
413       files.add(attachedFile);
414     }
415 
416     return files;
417   }
418 
419   /**
420    * Normalize given path to given base.
421    *
422    * This impacts some plugins which don't fully canonicalize before making assumptions about parent paths
423    * and w/o this can case some behavior differences depending on the path given to maven to execute
424    * if it had been given ./foo/pom.xml or foo/pom.xml.
425    *
426    * If base is "/foo/./bar" and path is "/foo/bar/baz", will normalize to "/foo/./bar/baz", etc.
427    */
428   private File normalize(final File base, final String path) throws Exception {
429     String basePath = base.getCanonicalPath();
430     String filePath = new File(path).getCanonicalPath();
431     if (filePath.startsWith(basePath)) {
432       String relPath = filePath.substring(basePath.length() + 1, filePath.length());
433       return new File(base, relPath);
434 
435     }
436     else {
437       return new File(path);
438     }
439   }
440 }