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 }