View Javadoc

1   /*
2    * Copyright 2011 SOFTEC sa. All rights reserved.
3    *
4    * This source code is licensed under the Creative Commons
5    * Attribution-NonCommercial-NoDerivs 3.0 Luxembourg
6    * License.
7    *
8    * To view a copy of this license, visit
9    * http://creativecommons.org/licenses/by-nc-nd/3.0/lu/
10   * or send a letter to Creative Commons, 171 Second Street,
11   * Suite 300, San Francisco, California, 94105, USA.
12   */
13  
14  package org.codehaus.mojo.javascript.titanium;
15  
16  import org.apache.maven.plugin.MojoExecutionException;
17  import org.apache.maven.plugin.MojoFailureException;
18  import org.apache.maven.plugin.logging.Log;
19  import org.codehaus.mojo.javascript.AndroidEmulatorThread;
20  
21  import java.io.BufferedReader;
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.InputStreamReader;
25  import java.util.List;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  /**
30   * Helper class to issue titanium builder calls.
31   */
32  public class TitaniumBuilder {
33  
34      /**
35       * The builder file.
36       */
37      private File androidBuilder;
38      private File iosBuilder;
39      private File androidSdk;
40  
41      /**
42       * Create a new TitaniumBuilder instance.
43       * @param androidBuilder The location of the android builder file.
44       * @param iosBuilder The location of the iphone/ipad builder file.
45       * @param androidSdk The location of the android SDK home folder.
46       */
47      public TitaniumBuilder(File androidBuilder, File iosBuilder, File androidSdk) {
48          this.androidBuilder = androidBuilder;
49          this.iosBuilder = iosBuilder;
50          this.androidSdk = androidSdk;
51      }
52  
53      /**
54       * Launch the android emulator and start the simulator after.
55       *
56       * @param projectName The project name.
57       * @param tiProjectDirectory The directory containing the titanium project files.
58       * @param appId The Titanium application id.
59       * <p>Must use the <code>com.company.name</code> format and not containing -.</p>
60       * @param androidAPI The API to use to build the application.
61       * @param androidDeviceAPI The API the android emulator should use.
62       * @param androidDeviceSkin The skin of the android emulator to use.
63       * @param androidDeviceWait The interval between the emulator launch and the simulator launch.
64       * In milliseconds.
65       * @param log The Maven logger
66       * @throws MojoFailureException When the builder process return an error.
67       * @throws MojoExecutionException When an error occurs during the call.
68       * @throws IOException  When an error occurs during the call.
69       * @throws InterruptedException  When an error occurs during the call.
70       */
71      public void launchOnAndroidEmulator(String projectName,
72                                          File tiProjectDirectory,
73                                          String appId,
74                                          String androidAPI,
75                                          String androidDeviceAPI,
76                                          String androidDeviceSkin,
77                                          Long androidDeviceWait,
78                                          Log log) throws MojoFailureException, MojoExecutionException, IOException, InterruptedException {
79          if (TitaniumUtils.createAvd(androidSdk, androidDeviceAPI,
80                  androidDeviceSkin,
81                  log)) {
82              log.info("AVD created for "
83                      + androidDeviceAPI
84                      + " " + androidDeviceSkin);
85          }
86          boolean isEmulatorRunning = false;
87          try {
88              isEmulatorRunning = TitaniumUtils.isAndroidEmulatorRunning(androidSdk);
89          } catch (Throwable t) {
90              log.error("Unable to retrieve launched emulators", t);
91          }
92          if (!isEmulatorRunning) {
93              log.info("Launching emulator");
94              ProcessBuilder emulatorBuilder = createAndroidBuilderProcess("emulator", projectName,
95                      tiProjectDirectory.getAbsolutePath(),
96                      appId,
97                      androidDeviceAPI, androidDeviceSkin);
98              AndroidEmulatorThread emulatorThread = new AndroidEmulatorThread(emulatorBuilder, log);
99              emulatorThread.start();
100             log.info("Waiting for emulator start ("
101                     + androidDeviceWait + " ms.)");
102 
103             Thread.sleep(androidDeviceWait);
104         } else {
105             log.info("Skipping emulator launching.");
106         }
107         log.info("Launching simulator");
108         ProcessBuilder simulatorBuilder = createAndroidBuilderProcess("simulator", projectName,
109                 tiProjectDirectory.getAbsolutePath(),
110                 appId,
111                 androidAPI, androidDeviceSkin);
112         //simulatorBuilder.redirectErrorStream(true);
113         Process simulator = simulatorBuilder.start();
114         logProcess(simulator, log, null, true);
115         log.info("Waiting for simulator end");
116         simulator.waitFor();
117         log.info("done");
118         if (simulator.exitValue() != 0) {
119             throw new MojoFailureException("The titanium builder failed");
120         }
121     }
122 
123 
124     /**
125      * Create an android ProcessBuilder.
126      * @param command The builder command.
127      * @param projectName The name of the project
128      * @param tiProjectDirectory The titanium project directory.
129      * @param appId The titanium application id.
130      * @param androidAPI The android API.
131      * @param skin The skin.
132      * @return The ProcessBuilder to use to launch the process.
133      * @throws MojoExecutionException When an error occured during the processbuilder creation.
134      */
135     public ProcessBuilder createAndroidBuilderProcess(String command, String projectName,
136                                                        String tiProjectDirectory,
137                                                        String appId,
138                                                        String androidAPI,
139                                                        String skin)
140             throws MojoExecutionException {
141 
142         if (androidBuilder == null) {
143             throw new MojoExecutionException("Unable to retrieve the android builder");
144         }
145         if (androidSdk == null) {
146             throw new MojoExecutionException("Unable to retrieve the android SDK");
147         }
148 
149         if (command.equals("install")) {
150             return new ProcessBuilder(androidBuilder.getAbsolutePath(),
151                     command,
152                     projectName,
153                     androidSdk.getAbsolutePath(),
154                     tiProjectDirectory,
155                     appId,
156                     androidAPI);
157         } else {
158             return new ProcessBuilder(androidBuilder.getAbsolutePath(),
159                     command,
160                     projectName,
161                     androidSdk.getAbsolutePath(),
162                     tiProjectDirectory,
163                     appId,
164                     androidAPI,
165                     skin);
166 
167         }
168     }
169 
170     /**
171      * Create an android distribute ProcessBuilder.
172      * @param outputDirectory The outputDirectory location.
173      * @param projectName The name of the project.
174      * @param tiProjectDirectory The titanium project directory.
175      * @param appId The titanium application id.
176      * @param keystore The keystore.
177      * @param keystorePassword The keystore password
178      * @param keystoreAlias The keystore alias.
179      * @param androidAPI The android API
180      * @param skin The skin
181      * @return The ProcessBuilder to use to create the android distribute process.
182      * @throws MojoExecutionException When an error occured during the processbuilder creation.
183      */
184     public ProcessBuilder createAndroidDistributeBuilderProcess(File outputDirectory,
185                                                                  String projectName,
186                                                                  String tiProjectDirectory,
187                                                                  String appId,
188                                                                  String keystore,
189                                                                  String keystorePassword,
190                                                                  String keystoreAlias,
191                                                                  String androidAPI,
192                                                                  String skin)
193             throws MojoExecutionException {
194 
195         if (androidBuilder == null) {
196             throw new MojoExecutionException("Unable to retrieve the android builder");
197         }
198         if (androidSdk == null) {
199             throw new MojoExecutionException("Unable to retrieve the android SDK");
200         }
201 
202         File targetDir = new File(outputDirectory, "android-bin");
203         targetDir.mkdirs();
204 
205         return new ProcessBuilder(androidBuilder.getAbsolutePath(),
206                 "distribute",
207                 projectName,
208                 androidSdk.getAbsolutePath(),
209                 tiProjectDirectory,
210                 appId,
211                 keystore,
212                 keystorePassword,
213                 keystoreAlias,
214                 targetDir.getAbsolutePath(),
215                 androidAPI,
216                 skin);
217     }
218 
219     /**
220      * Launch the specified titanium project on an iOs device.
221      * @param iosVersion The iOS version to use to build the application.
222      * @param tiProjectDirectory The titanium project directory.
223      * @param appId The titanium application identifier.
224      * @param projectName The project name.
225      * @param appuuid The project uuid
226      * @param distName The project dist name.
227      * @param family The family to use when building the project.
228      * @param log The Maven logger.
229      * @throws MojoFailureException When the builder process return an error.
230      * @throws MojoExecutionException When an error occured while launching on the device.
231      */
232     public void launchIphoneDevice(String iosVersion,
233                                    File tiProjectDirectory,
234                                    String appId,
235                                    String projectName,
236                                    String appuuid,
237                                    String distName,
238                                    String family,
239                                    Log log) throws MojoExecutionException, MojoFailureException {
240         if (iosBuilder == null) {
241             throw new MojoExecutionException("Unable to retrieve the iphone builder");
242         }
243 
244         ProcessBuilder pb = new ProcessBuilder(iosBuilder.getAbsolutePath(),
245                 "install",
246                 iosVersion,
247                 tiProjectDirectory.getAbsolutePath(),
248                 appId,
249                 projectName,
250                 appuuid,
251                 distName,
252                 family);
253         // The first call may fail
254         log.info("ProcessBuilder: " + getProcessBuilderString(pb));
255         boolean relaunch = false;
256         int result;
257         try {
258             StringBuilder logContent = new StringBuilder();
259             Process p = pb.start();
260             TitaniumBuilder.logProcess(p, log, logContent, false);
261             p.waitFor();
262             result = p.exitValue();
263             if (failOnTiapp(tiProjectDirectory.getAbsolutePath(), logContent.toString())) {
264                 relaunch = true;
265             }
266         } catch (Throwable t) {
267             throw new MojoExecutionException("Error while building iphone", t);
268         }
269         if (relaunch) {
270             log.warn("Relaunching builder as it failed on missing tiapp.xml");
271             try {
272                 Process p = pb.start();
273                 TitaniumBuilder.logProcess(p, log);
274                 p.waitFor();
275                 result = p.exitValue();
276             } catch (Throwable t) {
277                 throw new MojoExecutionException("Error while building iphone", t);
278             }
279         }
280         if (result != 0) {
281             throw new MojoFailureException("The titanium builder failed");
282         }
283     }
284 
285     /**
286      * Launch a titanium project on an iOs simulator.
287      * @param iosVersion The ios version to use to build the application.
288      * @param tiProjectDirectory The titanium project directory.
289      * @param projectName The name of the project.
290      * @param family The family to use to build the application.
291      * @param simulatorFamily family The family of the simulator ("universal" translates to "iphone").
292      * @param log The maven logger.
293      * @throws MojoFailureException When the builder process return an error.
294      * @throws MojoExecutionException When an error occurs during the simulator process.
295      */
296     public void launchIphoneEmulator(String iosVersion,
297                                      String tiProjectDirectory,
298                                      String projectName,
299                                      String family,
300                                      String simulatorFamily,
301                                      Log log) throws MojoFailureException, MojoExecutionException {
302         if (iosBuilder == null) {
303             throw new MojoExecutionException("Unable to retrieve the iphone builder");
304         }
305 
306         ProcessBuilder pb = new ProcessBuilder(iosBuilder.getAbsolutePath(),
307                 "simulator",
308                 iosVersion,
309                 tiProjectDirectory,
310                 "appid", //project.getGroupId() + "." + project.getArtifactId(),
311                 projectName,
312                 family,
313                 simulatorFamily);
314         //pb.directory(tiProjectDir);
315 
316         // The first call may fail
317         log.info("ProcessBuilder: " + getProcessBuilderString(pb));
318         boolean relaunch = false;
319         int result;
320         try {
321             StringBuilder logContent = new StringBuilder();
322             Process p = pb.start();
323             TitaniumBuilder.logProcess(p, log, logContent, false);
324             p.waitFor();
325             result = p.exitValue();
326             if (failOnTiapp(tiProjectDirectory, logContent.toString())) {
327                 relaunch = true;
328             }
329         } catch (Throwable t) {
330             throw new MojoExecutionException("Error while building iphone", t);
331         }
332         if (relaunch) {
333             log.warn("Relaunching builder as it failed on missing tiapp.xml");
334             try {
335                 Process p = pb.start();
336                 TitaniumBuilder.logProcess(p, log);
337                 p.waitFor();
338                 result = p.exitValue();
339             } catch (Throwable t) {
340                 throw new MojoExecutionException("Error while building iphone", t);
341             }
342         }
343         if (result != 0) {
344             throw new MojoFailureException("The titanium build failed");
345         }
346     }
347 
348     /**
349      * Launch the titanium ios builder distribute command and log its output.
350      * @param iosVersion The ios version.
351      * @param tiProjectDirectory The titanium project directory.
352      * @param appId The application identifier.
353      * @param projectName The project name.
354      * @param appuuid The application uuid.
355      * @param distName The distribution name.
356      * @param targetDir The folder where the resulting file will be created.
357      * @param family The device family.
358      * @param log The maven logger.
359      * @throws MojoFailureException When the builder process return an error.
360      * @throws MojoExecutionException When an error occurs while creating the distribution.
361      */
362     public void launchIphoneDistribute(String iosVersion,
363                                        File tiProjectDirectory,
364                                        String appId,
365                                        String projectName,
366                                        String appuuid,
367                                        String distName,
368                                        File targetDir,
369                                        String family,
370                                        Log log) throws MojoExecutionException, MojoFailureException {
371         if (iosBuilder == null) {
372             throw new MojoExecutionException("Unable to retrieve the iphone builder");
373         }
374         log.info("iphoneBuilder: " + iosBuilder.getAbsolutePath());
375 
376         targetDir.mkdirs();
377         ProcessBuilder pb = new ProcessBuilder(iosBuilder.getAbsolutePath(),
378                 "distribute",
379                 iosVersion,
380                 tiProjectDirectory.getAbsolutePath(),
381                 appId,
382                 projectName,
383                 appuuid,
384                 distName,
385                 targetDir.getAbsolutePath(),
386                 family);
387 
388         log.info("ProcessBuilder: " + getProcessBuilderString(pb));
389         boolean relaunch = false;
390         int result;
391         try {
392             StringBuilder logContent = new StringBuilder();
393 
394             Process p = pb.start();
395             TitaniumBuilder.logProcess(p, log, logContent, false);
396             p.waitFor();
397             result = p.exitValue();
398             if (failOnTiapp(tiProjectDirectory.getAbsolutePath(), logContent.toString())) {
399                 relaunch = true;
400             }
401         } catch (Throwable t) {
402             throw new MojoExecutionException("Error while building iphone", t);
403         }
404 
405         if (relaunch) {
406             log.warn("Relaunching builder as it failed on missing tiapp.xml");
407             try {
408                 Process p = pb.start();
409                 TitaniumBuilder.logProcess(p, log);
410                 p.waitFor();
411                 result = p.exitValue();
412             } catch (Throwable t) {
413                 throw new MojoExecutionException("Error while building iphone", t);
414             }
415         }
416         if (result != 0) {
417             throw new MojoFailureException("The titanium builder failed");
418         }
419     }
420 
421 
422     /**
423      * <p>Read a line from the reader and log it to the specified logger.</p>
424      * <p>This method doesn't block while no data is available.
425      * If no data is present when the method is called, nothing is outputed to the log</p>
426      * @param log The logger to use to log the message.
427      * @param reader The reader from which a line should be read.
428      */
429     public static void logProcessLine(Log log, BufferedReader reader) {
430         try {
431             readAndAppendLine(reader, log, null);
432         } catch (IOException ioe) {
433             log.error("Error while processing input", ioe);
434         }
435     }
436 
437     /**
438      * <p>Log the process output and error stream to the specified logger.</p>
439      * <p>This method will start by read and log lines from the output and error stream
440      * while the process is running.</p>
441      * @param p The process to log.
442      * @param log The logger where the messages should be outputted.
443      */
444     public static void logProcess(Process p, Log log) {
445         logProcess(p, log, null, false);
446     }
447 
448     /**
449      * <p>Log the process to the specified logger.</p>
450      * @param p The process to log.
451      * @param log The logger where the message should be outputted.
452      * @param builder A StringBuilder where each line will be appended.
453      * May be null.
454      * @param skipErrStream true if the error stream should not be logged.
455      */
456     public static void logProcess(Process p, Log log, StringBuilder builder, boolean skipErrStream) {
457         BufferedReader inReader = new BufferedReader(new InputStreamReader(p.getInputStream()));
458         BufferedReader errReader = new BufferedReader(new InputStreamReader(p.getErrorStream()));
459 
460         try {
461             while(isProcessRunning(p)) {
462                 readAndAppendLine(inReader, log, builder);
463                 if (!skipErrStream) {
464                     readAndAppendLine(errReader, log, builder);
465                 }
466             }
467             readAndAppendLine(inReader, log, builder);
468             if (!skipErrStream) {
469                 readAndAppendLine(errReader, log, builder);
470             }
471         } catch (IOException ioe) {
472             log.error("Error while processing builder input", ioe);
473         }
474     }
475 
476     /**
477      * Read a line from a reader and append it the the specified logger and builder.
478      * @param reader The reader from which the line should be read.
479      * @param log The logger where the line should be appended. May be null.
480      * @param builder The builder where the line should be appended. May be null.
481      * @throws IOException If an I/O exception occurs.
482      */
483     private static void readAndAppendLine(BufferedReader reader, Log log, StringBuilder builder)
484     throws IOException {
485         if (reader.ready()) {
486             String line = reader.readLine();
487             if (line != null) {
488                 if (log != null) {
489                     parseTitaniumBuilderLine(line, log);
490                 }
491                 if (builder != null) {
492                     builder.append(line);
493                     builder.append("\n");
494                 }
495             }
496         }
497     }
498 
499 
500     /**
501      * Check if a process is running.
502      * @param p The process to check.
503      * @return true if the process is running, false otherwise.
504      */
505     public static boolean isProcessRunning(final Process p) {
506         boolean isRunning = true;
507         try {
508             int status = p.exitValue();
509             isRunning = false;
510         } catch(IllegalThreadStateException e) {}
511         return isRunning;
512     }
513 
514     /**
515      * Parse a line and output to the specified logger using the appropriate level.
516      * @param line The line to parse.
517      * @param log The logger.
518      */
519     private static void parseTitaniumBuilderLine(String line, Log log) {
520         final Pattern pattern = Pattern.compile("\\[(TRACE|INFO|DEBUG|ERROR)\\] (.+)");
521         final Matcher matcher = pattern.matcher(line);
522         if (matcher.find()) {
523             String type = matcher.group(1);
524             String msg = matcher.group(2);
525             if (type.equals("TRACE") || type.equals("DEBUG")) {
526                 log.debug(msg);
527             } else if (type.equals("INFO")) {
528                 log.info(msg);
529             } else if (type.equals("ERROR")) {
530                 log.error(msg);
531             }
532         } else {
533             log.debug(line);
534         }
535     }
536 
537     /**
538      * Check if the iOS build process failed due to a missing tiapp.xml at an incorrect location.
539      * @param tiProjectDirectory The titanium project folder.
540      * @param logContent The log containing the error cause.
541      * @return true if the process failed due to a missing tiapp.xml
542      */
543     private boolean failOnTiapp(String tiProjectDirectory, String logContent) {
544         File tiWrongFile = new File(new File(tiProjectDirectory), "build" + File.separator + "iphone" + File.separator + "tiapp.xml");
545 
546         StringBuilder sb = new StringBuilder();
547         sb.append("IOError: [Errno 2] No such file or directory: u'");
548         sb.append(tiWrongFile.getAbsolutePath());
549         sb.append("'");
550         return (logContent.contains(sb.toString()));
551     }
552 
553     /**
554      * Retrieve a String representation of a ProcessBuilder.
555      * @param pb The ProcessBuilder.
556      * @return A String representing the ProcessBuilder.
557      */
558     private static String getProcessBuilderString(ProcessBuilder pb) {
559         StringBuilder sb = new StringBuilder();
560 
561         List<String> commands = pb.command();
562         if (pb.directory() != null) {
563             sb.append(pb.directory().getAbsolutePath() + ": ");
564         }
565         for (int i=0; i<commands.size(); i++) {
566             if (i > 0) {
567                 sb.append(" ");
568             }
569             if (commands.get(i) == null) {
570                 sb.append("NULL");
571             } else {
572                 sb.append(commands.get(i).replaceAll(" ", "\\\\ "));
573             }
574         }
575 
576         return sb.toString();
577     }
578 }