• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.ide.eclipse.adt.internal.build;
18 
19 import com.android.SdkConstants;
20 import com.android.annotations.NonNull;
21 import com.android.annotations.Nullable;
22 import com.android.ide.eclipse.adt.AdtConstants;
23 import com.android.ide.eclipse.adt.AdtPlugin;
24 import com.android.ide.eclipse.adt.AndroidPrintStream;
25 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
26 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity;
27 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
28 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
29 import com.android.prefs.AndroidLocation.AndroidLocationException;
30 import com.android.sdklib.BuildToolInfo;
31 import com.android.sdklib.IAndroidTarget;
32 import com.android.sdklib.IAndroidTarget.IOptionalLibrary;
33 import com.android.sdklib.build.ApkBuilder;
34 import com.android.sdklib.build.ApkBuilder.JarStatus;
35 import com.android.sdklib.build.ApkBuilder.SigningInfo;
36 import com.android.sdklib.build.ApkCreationException;
37 import com.android.sdklib.build.DuplicateFileException;
38 import com.android.sdklib.build.SealedApkException;
39 import com.android.sdklib.internal.build.DebugKeyProvider;
40 import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException;
41 import com.android.sdklib.util.GrabProcessOutput;
42 import com.android.sdklib.util.GrabProcessOutput.IProcessOutput;
43 import com.android.sdklib.util.GrabProcessOutput.Wait;
44 import com.google.common.hash.HashCode;
45 import com.google.common.hash.HashFunction;
46 import com.google.common.hash.Hashing;
47 
48 import org.eclipse.core.resources.IFile;
49 import org.eclipse.core.resources.IFolder;
50 import org.eclipse.core.resources.IProject;
51 import org.eclipse.core.resources.IResource;
52 import org.eclipse.core.resources.IWorkspaceRoot;
53 import org.eclipse.core.resources.ResourcesPlugin;
54 import org.eclipse.core.runtime.CoreException;
55 import org.eclipse.core.runtime.IPath;
56 import org.eclipse.core.runtime.IStatus;
57 import org.eclipse.core.runtime.Status;
58 import org.eclipse.jdt.core.IClasspathContainer;
59 import org.eclipse.jdt.core.IClasspathEntry;
60 import org.eclipse.jdt.core.IJavaProject;
61 import org.eclipse.jdt.core.JavaCore;
62 import org.eclipse.jdt.core.JavaModelException;
63 import org.eclipse.jface.preference.IPreferenceStore;
64 
65 import java.io.File;
66 import java.io.FileWriter;
67 import java.io.IOException;
68 import java.io.PrintStream;
69 import java.security.PrivateKey;
70 import java.security.cert.X509Certificate;
71 import java.util.ArrayList;
72 import java.util.Collection;
73 import java.util.Collections;
74 import java.util.HashSet;
75 import java.util.List;
76 import java.util.Map;
77 import java.util.Set;
78 import java.util.TreeMap;
79 
80 /**
81  * Helper with methods for the last 3 steps of the generation of an APK.
82  *
83  * {@link #packageResources(IFile, IProject[], String, int, String, String)} packages the
84  * application resources using aapt into a zip file that is ready to be integrated into the apk.
85  *
86  * {@link #executeDx(IJavaProject, String, String, IJavaProject[])} will convert the Java byte
87  * code into the Dalvik bytecode.
88  *
89  * {@link #finalPackage(String, String, String, boolean, IJavaProject, IProject[], IJavaProject[], String, boolean)}
90  * will make the apk from all the previous components.
91  *
92  * This class only executes the 3 above actions. It does not handle the errors, and simply sends
93  * them back as custom exceptions.
94  *
95  * Warnings are handled by the {@link ResourceMarker} interface.
96  *
97  * Console output (verbose and non verbose) is handled through the {@link AndroidPrintStream} passed
98  * to the constructor.
99  *
100  */
101 public class BuildHelper {
102 
103     private static final String CONSOLE_PREFIX_DX = "Dx";   //$NON-NLS-1$
104     private final static String TEMP_PREFIX = "android_";   //$NON-NLS-1$
105 
106     private static final String COMMAND_CRUNCH = "crunch";  //$NON-NLS-1$
107     private static final String COMMAND_PACKAGE = "package"; //$NON-NLS-1$
108 
109     @NonNull
110     private final IProject mProject;
111     @NonNull
112     private final BuildToolInfo mBuildToolInfo;
113     @NonNull
114     private final AndroidPrintStream mOutStream;
115     @NonNull
116     private final AndroidPrintStream mErrStream;
117     private final boolean mForceJumbo;
118     private final boolean mDisableDexMerger;
119     private final boolean mVerbose;
120     private final boolean mDebugMode;
121 
122     private final Set<String> mCompiledCodePaths = new HashSet<String>();
123 
124     public static final boolean BENCHMARK_FLAG = false;
125     public static long sStartOverallTime = 0;
126     public static long sStartJavaCTime = 0;
127 
128     private final static int MILLION = 1000000;
129     private String mProguardFile;
130 
131     /**
132      * An object able to put a marker on a resource.
133      */
134     public interface ResourceMarker {
setWarning(IResource resource, String message)135         void setWarning(IResource resource, String message);
136     }
137 
138     /**
139      * Creates a new post-compiler helper
140      * @param project
141      * @param outStream
142      * @param errStream
143      * @param debugMode whether this is a debug build
144      * @param verbose
145      * @throws CoreException
146      */
BuildHelper(@onNull IProject project, @NonNull BuildToolInfo buildToolInfo, @NonNull AndroidPrintStream outStream, @NonNull AndroidPrintStream errStream, boolean forceJumbo, boolean disableDexMerger, boolean debugMode, boolean verbose, ResourceMarker resMarker)147     public BuildHelper(@NonNull IProject project,
148             @NonNull BuildToolInfo buildToolInfo,
149             @NonNull AndroidPrintStream outStream,
150             @NonNull AndroidPrintStream errStream,
151             boolean forceJumbo, boolean disableDexMerger, boolean debugMode,
152             boolean verbose, ResourceMarker resMarker) throws CoreException {
153         mProject = project;
154         mBuildToolInfo = buildToolInfo;
155         mOutStream = outStream;
156         mErrStream = errStream;
157         mDebugMode = debugMode;
158         mVerbose = verbose;
159         mForceJumbo = forceJumbo;
160         mDisableDexMerger = disableDexMerger;
161 
162         gatherPaths(resMarker);
163     }
164 
updateCrunchCache()165     public void updateCrunchCache() throws AaptExecException, AaptResultException {
166         // Benchmarking start
167         long startCrunchTime = 0;
168         if (BENCHMARK_FLAG) {
169             String msg = "BENCHMARK ADT: Starting Initial Packaging (.ap_)"; //$NON-NLS-1$
170             AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg);
171             startCrunchTime = System.nanoTime();
172         }
173 
174         // Get the resources folder to crunch from
175         IFolder resFolder = mProject.getFolder(AdtConstants.WS_RESOURCES);
176         List<String> resPaths = new ArrayList<String>();
177         resPaths.add(resFolder.getLocation().toOSString());
178 
179         // Get the output folder where the cache is stored.
180         IFolder cacheFolder = mProject.getFolder(AdtConstants.WS_CRUNCHCACHE);
181         String cachePath = cacheFolder.getLocation().toOSString();
182 
183         /* For crunching, we don't need the osManifestPath, osAssetsPath, or the configFilter
184          * parameters for executeAapt
185          */
186         executeAapt(COMMAND_CRUNCH, "", resPaths, "", cachePath, "", 0);
187 
188         // Benchmarking end
189         if (BENCHMARK_FLAG) {
190             String msg = "BENCHMARK ADT: Ending Initial Package (.ap_). \nTime Elapsed: " //$NON-NLS-1$
191                             + ((System.nanoTime() - startCrunchTime)/MILLION) + "ms";     //$NON-NLS-1$
192             AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg);
193         }
194     }
195 
196     /**
197      * Packages the resources of the projet into a .ap_ file.
198      * @param manifestFile the manifest of the project.
199      * @param libProjects the list of library projects that this project depends on.
200      * @param resFilter an optional resource filter to be used with the -c option of aapt. If null
201      * no filters are used.
202      * @param versionCode an optional versionCode to be inserted in the manifest during packaging.
203      * If the value is <=0, no values are inserted.
204      * @param outputFolder where to write the resource ap_ file.
205      * @param outputFilename the name of the resource ap_ file.
206      * @throws AaptExecException
207      * @throws AaptResultException
208      */
packageResources(IFile manifestFile, List<IProject> libProjects, String resFilter, int versionCode, String outputFolder, String outputFilename)209     public void packageResources(IFile manifestFile, List<IProject> libProjects, String resFilter,
210             int versionCode, String outputFolder, String outputFilename)
211             throws AaptExecException, AaptResultException {
212 
213         // Benchmarking start
214         long startPackageTime = 0;
215         if (BENCHMARK_FLAG) {
216             String msg = "BENCHMARK ADT: Starting Initial Packaging (.ap_)";    //$NON-NLS-1$
217             AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg);
218             startPackageTime = System.nanoTime();
219         }
220 
221         // need to figure out some path before we can execute aapt;
222 
223         // get the cache folder
224         IFolder cacheFolder = mProject.getFolder(AdtConstants.WS_CRUNCHCACHE);
225 
226         // get the resource folder
227         IFolder resFolder = mProject.getFolder(AdtConstants.WS_RESOURCES);
228 
229         // and the assets folder
230         IFolder assetsFolder = mProject.getFolder(AdtConstants.WS_ASSETS);
231 
232         // we need to make sure this one exists.
233         if (assetsFolder.exists() == false) {
234             assetsFolder = null;
235         }
236 
237         // list of res folder (main project + maybe libraries)
238         ArrayList<String> osResPaths = new ArrayList<String>();
239 
240         IPath resLocation = resFolder.getLocation();
241         IPath manifestLocation = manifestFile.getLocation();
242 
243         if (resLocation != null && manifestLocation != null) {
244 
245             // png cache folder first.
246             addFolderToList(osResPaths, cacheFolder);
247 
248             // regular res folder next.
249             osResPaths.add(resLocation.toOSString());
250 
251             // then libraries
252             if (libProjects != null) {
253                 for (IProject lib : libProjects) {
254                     // png cache folder first
255                     IFolder libCacheFolder = lib.getFolder(AdtConstants.WS_CRUNCHCACHE);
256                     addFolderToList(osResPaths, libCacheFolder);
257 
258                     // regular res folder next.
259                     IFolder libResFolder = lib.getFolder(AdtConstants.WS_RESOURCES);
260                     addFolderToList(osResPaths, libResFolder);
261                 }
262             }
263 
264             String osManifestPath = manifestLocation.toOSString();
265 
266             String osAssetsPath = null;
267             if (assetsFolder != null) {
268                 osAssetsPath = assetsFolder.getLocation().toOSString();
269             }
270 
271             // build the default resource package
272             executeAapt(COMMAND_PACKAGE, osManifestPath, osResPaths, osAssetsPath,
273                     outputFolder + File.separator + outputFilename, resFilter,
274                     versionCode);
275         }
276 
277         // Benchmarking end
278         if (BENCHMARK_FLAG) {
279             String msg = "BENCHMARK ADT: Ending Initial Package (.ap_). \nTime Elapsed: " //$NON-NLS-1$
280                             + ((System.nanoTime() - startPackageTime)/MILLION) + "ms";    //$NON-NLS-1$
281             AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg);
282         }
283     }
284 
285     /**
286      * Adds os path of a folder to a list only if the folder actually exists.
287      * @param pathList
288      * @param folder
289      */
addFolderToList(List<String> pathList, IFolder folder)290     private void addFolderToList(List<String> pathList, IFolder folder) {
291         // use a File instead of the IFolder API to ignore workspace refresh issue.
292         File testFile = new File(folder.getLocation().toOSString());
293         if (testFile.isDirectory()) {
294             pathList.add(testFile.getAbsolutePath());
295         }
296     }
297 
298     /**
299      * Makes a final package signed with the debug key.
300      *
301      * Packages the dex files, the temporary resource file into the final package file.
302      *
303      * Whether the package is a debug package is controlled with the <var>debugMode</var> parameter
304      * in {@link #PostCompilerHelper(IProject, PrintStream, PrintStream, boolean, boolean)}
305      *
306      * @param intermediateApk The path to the temporary resource file.
307      * @param dex The path to the dex file.
308      * @param output The path to the final package file to create.
309      * @param libProjects an optional list of library projects (can be null)
310      * @return true if success, false otherwise.
311      * @throws ApkCreationException
312      * @throws AndroidLocationException
313      * @throws KeytoolException
314      * @throws NativeLibInJarException
315      * @throws CoreException
316      * @throws DuplicateFileException
317      */
finalDebugPackage(String intermediateApk, String dex, String output, List<IProject> libProjects, ResourceMarker resMarker)318     public void finalDebugPackage(String intermediateApk, String dex, String output,
319             List<IProject> libProjects, ResourceMarker resMarker)
320             throws ApkCreationException, KeytoolException, AndroidLocationException,
321             NativeLibInJarException, DuplicateFileException, CoreException {
322 
323         AdtPlugin adt = AdtPlugin.getDefault();
324         if (adt == null) {
325             return;
326         }
327 
328         // get the debug keystore to use.
329         IPreferenceStore store = adt.getPreferenceStore();
330         String keystoreOsPath = store.getString(AdtPrefs.PREFS_CUSTOM_DEBUG_KEYSTORE);
331         if (keystoreOsPath == null || new File(keystoreOsPath).isFile() == false) {
332             keystoreOsPath = DebugKeyProvider.getDefaultKeyStoreOsPath();
333             AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, mProject,
334                     Messages.ApkBuilder_Using_Default_Key);
335         } else {
336             AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, mProject,
337                     String.format(Messages.ApkBuilder_Using_s_To_Sign, keystoreOsPath));
338         }
339 
340         // from the keystore, get the signing info
341         SigningInfo info = ApkBuilder.getDebugKey(keystoreOsPath, mVerbose ? mOutStream : null);
342 
343         finalPackage(intermediateApk, dex, output, libProjects,
344                 info != null ? info.key : null, info != null ? info.certificate : null, resMarker);
345     }
346 
347     /**
348      * Makes the final package.
349      *
350      * Packages the dex files, the temporary resource file into the final package file.
351      *
352      * Whether the package is a debug package is controlled with the <var>debugMode</var> parameter
353      * in {@link #PostCompilerHelper(IProject, PrintStream, PrintStream, boolean, boolean)}
354      *
355      * @param intermediateApk The path to the temporary resource file.
356      * @param dex The path to the dex file.
357      * @param output The path to the final package file to create.
358      * @param debugSign whether the apk must be signed with the debug key.
359      * @param libProjects an optional list of library projects (can be null)
360      * @param abiFilter an optional filter. If not null, then only the matching ABI is included in
361      * the final archive
362      * @return true if success, false otherwise.
363      * @throws NativeLibInJarException
364      * @throws ApkCreationException
365      * @throws CoreException
366      * @throws DuplicateFileException
367      */
finalPackage(String intermediateApk, String dex, String output, List<IProject> libProjects, PrivateKey key, X509Certificate certificate, ResourceMarker resMarker)368     public void finalPackage(String intermediateApk, String dex, String output,
369             List<IProject> libProjects,
370             PrivateKey key, X509Certificate certificate, ResourceMarker resMarker)
371             throws NativeLibInJarException, ApkCreationException, DuplicateFileException,
372             CoreException {
373 
374         try {
375             ApkBuilder apkBuilder = new ApkBuilder(output, intermediateApk, dex,
376                     key, certificate,
377                     mVerbose ? mOutStream: null);
378             apkBuilder.setDebugMode(mDebugMode);
379 
380             // either use the full compiled code paths or just the proguard file
381             // if present
382             Collection<String> pathsCollection = mCompiledCodePaths;
383             if (mProguardFile != null) {
384                 pathsCollection = Collections.singletonList(mProguardFile);
385                 mProguardFile = null;
386             }
387 
388             // Now we write the standard resources from all the output paths.
389             for (String path : pathsCollection) {
390                 File file = new File(path);
391                 if (file.isFile()) {
392                     JarStatus jarStatus = apkBuilder.addResourcesFromJar(file);
393 
394                     // check if we found native libraries in the external library. This
395                     // constitutes an error or warning depending on if they are in lib/
396                     if (jarStatus.getNativeLibs().size() > 0) {
397                         String libName = file.getName();
398 
399                         String msg = String.format(
400                                 "Native libraries detected in '%1$s'. See console for more information.",
401                                 libName);
402 
403                         ArrayList<String> consoleMsgs = new ArrayList<String>();
404 
405                         consoleMsgs.add(String.format(
406                                 "The library '%1$s' contains native libraries that will not run on the device.",
407                                 libName));
408 
409                         if (jarStatus.hasNativeLibsConflicts()) {
410                             consoleMsgs.add("Additionally some of those libraries will interfer with the installation of the application because of their location in lib/");
411                             consoleMsgs.add("lib/ is reserved for NDK libraries.");
412                         }
413 
414                         consoleMsgs.add("The following libraries were found:");
415 
416                         for (String lib : jarStatus.getNativeLibs()) {
417                             consoleMsgs.add(" - " + lib);
418                         }
419 
420                         String[] consoleStrings = consoleMsgs.toArray(new String[consoleMsgs.size()]);
421 
422                         // if there's a conflict or if the prefs force error on any native code in jar
423                         // files, throw an exception
424                         if (jarStatus.hasNativeLibsConflicts() ||
425                                 AdtPrefs.getPrefs().getBuildForceErrorOnNativeLibInJar()) {
426                             throw new NativeLibInJarException(jarStatus, msg, libName, consoleStrings);
427                         } else {
428                             // otherwise, put a warning, and output to the console also.
429                             if (resMarker != null) {
430                                 resMarker.setWarning(mProject, msg);
431                             }
432 
433                             for (String string : consoleStrings) {
434                                 mOutStream.println(string);
435                             }
436                         }
437                     }
438                 } else if (file.isDirectory()) {
439                     // this is technically not a source folder (class folder instead) but since we
440                     // only care about Java resources (ie non class/java files) this will do the
441                     // same
442                     apkBuilder.addSourceFolder(file);
443                 }
444             }
445 
446             // now write the native libraries.
447             // First look if the lib folder is there.
448             IResource libFolder = mProject.findMember(SdkConstants.FD_NATIVE_LIBS);
449             if (libFolder != null && libFolder.exists() &&
450                     libFolder.getType() == IResource.FOLDER) {
451                 // get a File for the folder.
452                 apkBuilder.addNativeLibraries(libFolder.getLocation().toFile());
453             }
454 
455             // write the native libraries for the library projects.
456             if (libProjects != null) {
457                 for (IProject lib : libProjects) {
458                     libFolder = lib.findMember(SdkConstants.FD_NATIVE_LIBS);
459                     if (libFolder != null && libFolder.exists() &&
460                             libFolder.getType() == IResource.FOLDER) {
461                         apkBuilder.addNativeLibraries(libFolder.getLocation().toFile());
462                     }
463                 }
464             }
465 
466             // seal the APK.
467             apkBuilder.sealApk();
468         } catch (SealedApkException e) {
469             // this won't happen as we control when the apk is sealed.
470         }
471     }
472 
setProguardOutput(String proguardFile)473     public void setProguardOutput(String proguardFile) {
474         mProguardFile = proguardFile;
475     }
476 
getCompiledCodePaths()477     public Collection<String> getCompiledCodePaths() {
478         return mCompiledCodePaths;
479     }
480 
runProguard(List<File> proguardConfigs, File inputJar, Collection<String> jarFiles, File obfuscatedJar, File logOutput)481     public void runProguard(List<File> proguardConfigs, File inputJar, Collection<String> jarFiles,
482                             File obfuscatedJar, File logOutput)
483             throws ProguardResultException, ProguardExecException, IOException {
484         IAndroidTarget target = Sdk.getCurrent().getTarget(mProject);
485 
486         // prepare the command line for proguard
487         List<String> command = new ArrayList<String>();
488         command.add(AdtPlugin.getOsAbsoluteProguard());
489 
490         for (File configFile : proguardConfigs) {
491             command.add("-include"); //$NON-NLS-1$
492             command.add(quotePath(configFile.getAbsolutePath()));
493         }
494 
495         command.add("-injars"); //$NON-NLS-1$
496         StringBuilder sb = new StringBuilder(quotePath(inputJar.getAbsolutePath()));
497         for (String jarFile : jarFiles) {
498             sb.append(File.pathSeparatorChar);
499             sb.append(quotePath(jarFile));
500         }
501         command.add(quoteWinArg(sb.toString()));
502 
503         command.add("-outjars"); //$NON-NLS-1$
504         command.add(quotePath(obfuscatedJar.getAbsolutePath()));
505 
506         command.add("-libraryjars"); //$NON-NLS-1$
507         sb = new StringBuilder(quotePath(target.getPath(IAndroidTarget.ANDROID_JAR)));
508         IOptionalLibrary[] libraries = target.getOptionalLibraries();
509         if (libraries != null) {
510             for (IOptionalLibrary lib : libraries) {
511                 sb.append(File.pathSeparatorChar);
512                 sb.append(quotePath(lib.getJarPath()));
513             }
514         }
515         command.add(quoteWinArg(sb.toString()));
516 
517         if (logOutput != null) {
518             if (logOutput.isDirectory() == false) {
519                 logOutput.mkdirs();
520             }
521 
522             command.add("-dump");                                              //$NON-NLS-1$
523             command.add(new File(logOutput, "dump.txt").getAbsolutePath());    //$NON-NLS-1$
524 
525             command.add("-printseeds");                                        //$NON-NLS-1$
526             command.add(new File(logOutput, "seeds.txt").getAbsolutePath());   //$NON-NLS-1$
527 
528             command.add("-printusage");                                        //$NON-NLS-1$
529             command.add(new File(logOutput, "usage.txt").getAbsolutePath());   //$NON-NLS-1$
530 
531             command.add("-printmapping");                                      //$NON-NLS-1$
532             command.add(new File(logOutput, "mapping.txt").getAbsolutePath()); //$NON-NLS-1$
533         }
534 
535         String commandArray[] = null;
536 
537         if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) {
538             commandArray = createWindowsProguardConfig(command);
539         }
540 
541         if (commandArray == null) {
542             // For Mac & Linux, use a regular command string array.
543             commandArray = command.toArray(new String[command.size()]);
544         }
545 
546         // Define PROGUARD_HOME to point to $SDK/tools/proguard if it's not yet defined.
547         // The Mac/Linux proguard.sh can infer it correctly but not the proguard.bat one.
548         String[] envp = null;
549         Map<String, String> envMap = new TreeMap<String, String>(System.getenv());
550         if (!envMap.containsKey("PROGUARD_HOME")) {                                    //$NON-NLS-1$
551             envMap.put("PROGUARD_HOME",    Sdk.getCurrent().getSdkLocation() +         //$NON-NLS-1$
552                                             SdkConstants.FD_TOOLS + File.separator +
553                                             SdkConstants.FD_PROGUARD);
554             envp = new String[envMap.size()];
555             int i = 0;
556             for (Map.Entry<String, String> entry : envMap.entrySet()) {
557                 envp[i++] = String.format("%1$s=%2$s",                                 //$NON-NLS-1$
558                                           entry.getKey(),
559                                           entry.getValue());
560             }
561         }
562 
563         if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) {
564             sb = new StringBuilder();
565             for (String c : commandArray) {
566                 sb.append(c).append(' ');
567             }
568             AdtPlugin.printToConsole(mProject, sb.toString());
569         }
570 
571         // launch
572         int execError = 1;
573         try {
574             // launch the command line process
575             Process process = Runtime.getRuntime().exec(commandArray, envp);
576 
577             // list to store each line of stderr
578             ArrayList<String> results = new ArrayList<String>();
579 
580             // get the output and return code from the process
581             execError = grabProcessOutput(mProject, process, results);
582 
583             if (mVerbose) {
584                 for (String resultString : results) {
585                     mOutStream.println(resultString);
586                 }
587             }
588 
589             if (execError != 0) {
590                 throw new ProguardResultException(execError,
591                         results.toArray(new String[results.size()]));
592             }
593 
594         } catch (IOException e) {
595             String msg = String.format(Messages.Proguard_Exec_Error, commandArray[0]);
596             throw new ProguardExecException(msg, e);
597         } catch (InterruptedException e) {
598             String msg = String.format(Messages.Proguard_Exec_Error, commandArray[0]);
599             throw new ProguardExecException(msg, e);
600         }
601     }
602 
603     /**
604      * For tools R8 up to R11, the proguard.bat launcher on Windows only accepts
605      * arguments %1..%9. Since we generally have about 15 arguments, we were working
606      * around this by generating a temporary config file for proguard and then using
607      * that.
608      * Starting with tools R12, the proguard.bat launcher has been fixed to take
609      * all arguments using %* so we no longer need this hack.
610      *
611      * @param command
612      * @return
613      * @throws IOException
614      */
createWindowsProguardConfig(List<String> command)615     private String[] createWindowsProguardConfig(List<String> command) throws IOException {
616 
617         // Arg 0 is the proguard.bat path and arg 1 is the user config file
618         String launcher = AdtPlugin.readFile(new File(command.get(0)));
619         if (launcher.contains("%*")) {                                      //$NON-NLS-1$
620             // This is the launcher from Tools R12. Don't work around it.
621             return null;
622         }
623 
624         // On Windows, proguard.bat can only pass %1...%9 to the java -jar proguard.jar
625         // call, but we have at least 15 arguments here so some get dropped silently
626         // and quoting is a big issue. So instead we'll work around that by writing
627         // all the arguments to a temporary config file.
628 
629         String[] commandArray = new String[3];
630 
631         commandArray[0] = command.get(0);
632         commandArray[1] = command.get(1);
633 
634         // Write all the other arguments to a config file
635         File argsFile = File.createTempFile(TEMP_PREFIX, ".pro");           //$NON-NLS-1$
636         // TODO FIXME this may leave a lot of temp files around on a long session.
637         // Should have a better way to clean up e.g. before each build.
638         argsFile.deleteOnExit();
639 
640         FileWriter fw = new FileWriter(argsFile);
641 
642         for (int i = 2; i < command.size(); i++) {
643             String s = command.get(i);
644             fw.write(s);
645             fw.write(s.startsWith("-") ? ' ' : '\n');                       //$NON-NLS-1$
646         }
647 
648         fw.close();
649 
650         commandArray[2] = "@" + argsFile.getAbsolutePath();                 //$NON-NLS-1$
651         return commandArray;
652     }
653 
654     /**
655      * Quotes a single path for proguard to deal with spaces.
656      *
657      * @param path The path to quote.
658      * @return The original path if it doesn't contain a space.
659      *   Or the original path surrounded by single quotes if it contains spaces.
660      */
quotePath(String path)661     private String quotePath(String path) {
662         if (path.indexOf(' ') != -1) {
663             path = '\'' + path + '\'';
664         }
665         return path;
666     }
667 
668     /**
669      * Quotes a compound proguard argument to deal with spaces.
670      * <p/>
671      * Proguard takes multi-path arguments such as "path1;path2" for some options.
672      * When the {@link #quotePath} methods adds quotes for such a path if it contains spaces,
673      * the proguard shell wrapper will absorb the quotes, so we need to quote around the
674      * quotes.
675      *
676      * @param path The path to quote.
677      * @return The original path if it doesn't contain a single quote.
678      *   Or on Windows the original path surrounded by double quotes if it contains a quote.
679      */
quoteWinArg(String path)680     private String quoteWinArg(String path) {
681         if (path.indexOf('\'') != -1 &&
682                 SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) {
683             path = '"' + path + '"';
684         }
685         return path;
686     }
687 
688 
689     /**
690      * Execute the Dx tool for dalvik code conversion.
691      * @param javaProject The java project
692      * @param inputPaths the input paths for DX
693      * @param osOutFilePath the path of the dex file to create.
694      *
695      * @throws CoreException
696      * @throws DexException
697      */
executeDx(IJavaProject javaProject, Collection<String> inputPaths, String osOutFilePath)698     public void executeDx(IJavaProject javaProject, Collection<String> inputPaths,
699             String osOutFilePath)
700             throws CoreException, DexException {
701 
702         // get the dex wrapper
703         Sdk sdk = Sdk.getCurrent();
704         DexWrapper wrapper = sdk.getDexWrapper(mBuildToolInfo);
705 
706         if (wrapper == null) {
707             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
708                     Messages.ApkBuilder_UnableBuild_Dex_Not_loaded));
709         }
710 
711         try {
712             // set a temporary prefix on the print streams.
713             mOutStream.setPrefix(CONSOLE_PREFIX_DX);
714             mErrStream.setPrefix(CONSOLE_PREFIX_DX);
715 
716             IFolder binFolder = BaseProjectHelper.getAndroidOutputFolder(javaProject.getProject());
717             File binFile = binFolder.getLocation().toFile();
718             File dexedLibs = new File(binFile, "dexedLibs");
719             if (dexedLibs.exists() == false) {
720                 dexedLibs.mkdir();
721             }
722 
723             // replace the libs by their dexed versions (dexing them if needed.)
724             List<String> finalInputPaths = new ArrayList<String>(inputPaths.size());
725             if (mDisableDexMerger || inputPaths.size() == 1) {
726                 // only one input, no need to put a pre-dexed version, even if this path is
727                 // just a jar file (case for proguard'ed builds)
728                 finalInputPaths.addAll(inputPaths);
729             } else {
730 
731                 for (String input : inputPaths) {
732                     File inputFile = new File(input);
733                     if (inputFile.isDirectory()) {
734                         finalInputPaths.add(input);
735                     } else if (inputFile.isFile()) {
736                         String fileName = getDexFileName(inputFile);
737 
738                         File dexedLib = new File(dexedLibs, fileName);
739                         String dexedLibPath = dexedLib.getAbsolutePath();
740 
741                         if (dexedLib.isFile() == false ||
742                                 dexedLib.lastModified() < inputFile.lastModified()) {
743 
744                             if (mVerbose) {
745                                 mOutStream.println(
746                                         String.format("Pre-Dexing %1$s -> %2$s", input, fileName));
747                             }
748 
749                             if (dexedLib.isFile()) {
750                                 dexedLib.delete();
751                             }
752 
753                             int res = wrapper.run(dexedLibPath, Collections.singleton(input),
754                                     mForceJumbo, mVerbose, mOutStream, mErrStream);
755 
756                             if (res != 0) {
757                                 // output error message and mark the project.
758                                 String message = String.format(Messages.Dalvik_Error_d, res);
759                                 throw new DexException(message);
760                             }
761                         } else {
762                             if (mVerbose) {
763                                 mOutStream.println(
764                                         String.format("Using Pre-Dexed %1$s <- %2$s",
765                                                 fileName, input));
766                             }
767                         }
768 
769                         finalInputPaths.add(dexedLibPath);
770                     }
771                 }
772             }
773 
774             if (mVerbose) {
775                 for (String input : finalInputPaths) {
776                     mOutStream.println("Input: " + input);
777                 }
778             }
779 
780             int res = wrapper.run(osOutFilePath,
781                     finalInputPaths,
782                     mForceJumbo,
783                     mVerbose,
784                     mOutStream, mErrStream);
785 
786             mOutStream.setPrefix(null);
787             mErrStream.setPrefix(null);
788 
789             if (res != 0) {
790                 // output error message and marker the project.
791                 String message = String.format(Messages.Dalvik_Error_d, res);
792                 throw new DexException(message);
793             }
794         } catch (DexException e) {
795             throw e;
796         } catch (Throwable t) {
797             String message = t.getMessage();
798             if (message == null) {
799                 message = t.getClass().getCanonicalName();
800             }
801             message = String.format(Messages.Dalvik_Error_s, message);
802 
803             throw new DexException(message, t);
804         }
805     }
806 
getDexFileName(File inputFile)807     private String getDexFileName(File inputFile) {
808         // get the filename
809         String name = inputFile.getName();
810         // remove the extension
811         int pos = name.lastIndexOf('.');
812         if (pos != -1) {
813             name = name.substring(0, pos);
814         }
815 
816         // add a hash of the original file path
817         HashFunction hashFunction = Hashing.md5();
818         HashCode hashCode = hashFunction.hashString(inputFile.getAbsolutePath());
819 
820         return name + "-" + hashCode.toString() + ".jar";
821     }
822 
823     /**
824      * Executes aapt. If any error happen, files or the project will be marked.
825      * @param command The command for aapt to execute. Currently supported: package and crunch
826      * @param osManifestPath The path to the manifest file
827      * @param osResPath The path to the res folder
828      * @param osAssetsPath The path to the assets folder. This can be null.
829      * @param osOutFilePath The path to the temporary resource file to create,
830      *   or in the case of crunching the path to the cache to create/update.
831      * @param configFilter The configuration filter for the resources to include
832      * (used with -c option, for example "port,en,fr" to include portrait, English and French
833      * resources.)
834      * @param versionCode optional version code to insert in the manifest during packaging. If <=0
835      * then no value is inserted
836      * @throws AaptExecException
837      * @throws AaptResultException
838      */
executeAapt(String aaptCommand, String osManifestPath, List<String> osResPaths, String osAssetsPath, String osOutFilePath, String configFilter, int versionCode)839     private void executeAapt(String aaptCommand, String osManifestPath,
840             List<String> osResPaths, String osAssetsPath, String osOutFilePath,
841             String configFilter, int versionCode) throws AaptExecException, AaptResultException {
842         IAndroidTarget target = Sdk.getCurrent().getTarget(mProject);
843 
844         String aapt = mBuildToolInfo.getPath(BuildToolInfo.PathId.AAPT);
845 
846         // Create the command line.
847         ArrayList<String> commandArray = new ArrayList<String>();
848         commandArray.add(aapt);
849         commandArray.add(aaptCommand);
850         if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) {
851             commandArray.add("-v"); //$NON-NLS-1$
852         }
853 
854         // Common to all commands
855         for (String path : osResPaths) {
856             commandArray.add("-S"); //$NON-NLS-1$
857             commandArray.add(path);
858         }
859 
860         if (aaptCommand.equals(COMMAND_PACKAGE)) {
861             commandArray.add("-f");          //$NON-NLS-1$
862             commandArray.add("--no-crunch"); //$NON-NLS-1$
863 
864             // if more than one res, this means there's a library (or more) and we need
865             // to activate the auto-add-overlay
866             if (osResPaths.size() > 1) {
867                 commandArray.add("--auto-add-overlay"); //$NON-NLS-1$
868             }
869 
870             if (mDebugMode) {
871                 commandArray.add("--debug-mode"); //$NON-NLS-1$
872             }
873 
874             if (versionCode > 0) {
875                 commandArray.add("--version-code"); //$NON-NLS-1$
876                 commandArray.add(Integer.toString(versionCode));
877             }
878 
879             if (configFilter != null) {
880                 commandArray.add("-c"); //$NON-NLS-1$
881                 commandArray.add(configFilter);
882             }
883 
884             commandArray.add("-M"); //$NON-NLS-1$
885             commandArray.add(osManifestPath);
886 
887             if (osAssetsPath != null) {
888                 commandArray.add("-A"); //$NON-NLS-1$
889                 commandArray.add(osAssetsPath);
890             }
891 
892             commandArray.add("-I"); //$NON-NLS-1$
893             commandArray.add(target.getPath(IAndroidTarget.ANDROID_JAR));
894 
895             commandArray.add("-F"); //$NON-NLS-1$
896             commandArray.add(osOutFilePath);
897         } else if (aaptCommand.equals(COMMAND_CRUNCH)) {
898             commandArray.add("-C"); //$NON-NLS-1$
899             commandArray.add(osOutFilePath);
900         }
901 
902         String command[] = commandArray.toArray(
903                 new String[commandArray.size()]);
904 
905         if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) {
906             StringBuilder sb = new StringBuilder();
907             for (String c : command) {
908                 sb.append(c);
909                 sb.append(' ');
910             }
911             AdtPlugin.printToConsole(mProject, sb.toString());
912         }
913 
914         // Benchmarking start
915         long startAaptTime = 0;
916         if (BENCHMARK_FLAG) {
917             String msg = "BENCHMARK ADT: Starting " + aaptCommand  //$NON-NLS-1$
918                          + " call to Aapt";                        //$NON-NLS-1$
919             AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg);
920             startAaptTime = System.nanoTime();
921         }
922 
923         // launch
924         try {
925             // launch the command line process
926             Process process = Runtime.getRuntime().exec(command);
927 
928             // list to store each line of stderr
929             ArrayList<String> stdErr = new ArrayList<String>();
930 
931             // get the output and return code from the process
932             int returnCode = grabProcessOutput(mProject, process, stdErr);
933 
934             if (mVerbose) {
935                 for (String stdErrString : stdErr) {
936                     mOutStream.println(stdErrString);
937                 }
938             }
939             if (returnCode != 0) {
940                 throw new AaptResultException(returnCode,
941                         stdErr.toArray(new String[stdErr.size()]));
942             }
943         } catch (IOException e) {
944             String msg = String.format(Messages.AAPT_Exec_Error_s, command[0]);
945             throw new AaptExecException(msg, e);
946         } catch (InterruptedException e) {
947             String msg = String.format(Messages.AAPT_Exec_Error_s, command[0]);
948             throw new AaptExecException(msg, e);
949         }
950 
951         // Benchmarking end
952         if (BENCHMARK_FLAG) {
953             String msg = "BENCHMARK ADT: Ending " + aaptCommand                  //$NON-NLS-1$
954                          + " call to Aapt.\nBENCHMARK ADT: Time Elapsed: "       //$NON-NLS-1$
955                          + ((System.nanoTime() - startAaptTime)/MILLION) + "ms"; //$NON-NLS-1$
956             AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg);
957         }
958     }
959 
960     /**
961      * Computes all the project output and dependencies that must go into building the apk.
962      *
963      * @param resMarker
964      * @throws CoreException
965      */
gatherPaths(ResourceMarker resMarker)966     private void gatherPaths(ResourceMarker resMarker)
967             throws CoreException {
968         IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
969 
970         // get a java project for the project.
971         IJavaProject javaProject = JavaCore.create(mProject);
972 
973 
974         // get the output of the main project
975         IPath path = javaProject.getOutputLocation();
976         IResource outputResource = wsRoot.findMember(path);
977         if (outputResource != null && outputResource.getType() == IResource.FOLDER) {
978             mCompiledCodePaths.add(outputResource.getLocation().toOSString());
979         }
980 
981         // we could use IJavaProject.getResolvedClasspath directly, but we actually
982         // want to see the containers themselves.
983         IClasspathEntry[] classpaths = javaProject.readRawClasspath();
984         if (classpaths != null) {
985             for (IClasspathEntry e : classpaths) {
986                 // ignore non exported entries, unless it's the LIBRARIES container,
987                 // in which case we always want it (there may be some older projects that
988                 // have it as non exported).
989                 if (e.isExported() ||
990                         (e.getEntryKind() == IClasspathEntry.CPE_CONTAINER &&
991                          e.getPath().toString().equals(AdtConstants.CONTAINER_LIBRARIES))) {
992                     handleCPE(e, javaProject, wsRoot, resMarker);
993                 }
994             }
995         }
996     }
997 
handleCPE(IClasspathEntry entry, IJavaProject javaProject, IWorkspaceRoot wsRoot, ResourceMarker resMarker)998     private void handleCPE(IClasspathEntry entry, IJavaProject javaProject,
999             IWorkspaceRoot wsRoot, ResourceMarker resMarker) {
1000 
1001         // if this is a classpath variable reference, we resolve it.
1002         if (entry.getEntryKind() == IClasspathEntry.CPE_VARIABLE) {
1003             entry = JavaCore.getResolvedClasspathEntry(entry);
1004         }
1005 
1006         if (entry.getEntryKind() == IClasspathEntry.CPE_PROJECT) {
1007             IProject refProject = wsRoot.getProject(entry.getPath().lastSegment());
1008             try {
1009                 // ignore if it's an Android project, or if it's not a Java Project
1010                 if (refProject.hasNature(JavaCore.NATURE_ID) &&
1011                         refProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
1012                     IJavaProject refJavaProject = JavaCore.create(refProject);
1013 
1014                     // get the output folder
1015                     IPath path = refJavaProject.getOutputLocation();
1016                     IResource outputResource = wsRoot.findMember(path);
1017                     if (outputResource != null && outputResource.getType() == IResource.FOLDER) {
1018                         mCompiledCodePaths.add(outputResource.getLocation().toOSString());
1019                     }
1020                 }
1021             } catch (CoreException exception) {
1022                 // can't query the project nature? ignore
1023             }
1024 
1025         } else if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) {
1026             handleClasspathLibrary(entry, wsRoot, resMarker);
1027         } else if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) {
1028             // get the container
1029             try {
1030                 IClasspathContainer container = JavaCore.getClasspathContainer(
1031                         entry.getPath(), javaProject);
1032                 // ignore the system and default_system types as they represent
1033                 // libraries that are part of the runtime.
1034                 if (container != null && container.getKind() == IClasspathContainer.K_APPLICATION) {
1035                     IClasspathEntry[] entries = container.getClasspathEntries();
1036                     for (IClasspathEntry cpe : entries) {
1037                         handleCPE(cpe, javaProject, wsRoot, resMarker);
1038                     }
1039                 }
1040             } catch (JavaModelException jme) {
1041                 // can't resolve the container? ignore it.
1042                 AdtPlugin.log(jme, "Failed to resolve ClasspathContainer: %s", entry.getPath());
1043             }
1044         }
1045     }
1046 
handleClasspathLibrary(IClasspathEntry e, IWorkspaceRoot wsRoot, ResourceMarker resMarker)1047     private void handleClasspathLibrary(IClasspathEntry e, IWorkspaceRoot wsRoot,
1048             ResourceMarker resMarker) {
1049         // get the IPath
1050         IPath path = e.getPath();
1051 
1052         IResource resource = wsRoot.findMember(path);
1053 
1054         if (resource != null && resource.getType() == IResource.PROJECT) {
1055             // if it's a project we should just ignore it because it's going to be added
1056             // later when we add all the referenced projects.
1057 
1058         } else if (SdkConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) {
1059             // case of a jar file (which could be relative to the workspace or a full path)
1060             if (resource != null && resource.exists() &&
1061                     resource.getType() == IResource.FILE) {
1062                 mCompiledCodePaths.add(resource.getLocation().toOSString());
1063             } else {
1064                 // if the jar path doesn't match a workspace resource,
1065                 // then we get an OSString and check if this links to a valid file.
1066                 String osFullPath = path.toOSString();
1067 
1068                 File f = new File(osFullPath);
1069                 if (f.isFile()) {
1070                     mCompiledCodePaths.add(osFullPath);
1071                 } else {
1072                     String message = String.format( Messages.Couldnt_Locate_s_Error,
1073                             path);
1074                     // always output to the console
1075                     mOutStream.println(message);
1076 
1077                     // put a marker
1078                     if (resMarker != null) {
1079                         resMarker.setWarning(mProject, message);
1080                     }
1081                 }
1082             }
1083         } else {
1084             // this can be the case for a class folder.
1085             if (resource != null && resource.exists() &&
1086                     resource.getType() == IResource.FOLDER) {
1087                 mCompiledCodePaths.add(resource.getLocation().toOSString());
1088             } else {
1089                 // if the path doesn't match a workspace resource,
1090                 // then we get an OSString and check if this links to a valid folder.
1091                 String osFullPath = path.toOSString();
1092 
1093                 File f = new File(osFullPath);
1094                 if (f.isDirectory()) {
1095                     mCompiledCodePaths.add(osFullPath);
1096                 }
1097             }
1098         }
1099     }
1100 
1101     /**
1102      * Checks a {@link IFile} to make sure it should be packaged as standard resources.
1103      * @param file the IFile representing the file.
1104      * @return true if the file should be packaged as standard java resources.
1105      */
checkFileForPackaging(IFile file)1106     public static boolean checkFileForPackaging(IFile file) {
1107         String name = file.getName();
1108 
1109         String ext = file.getFileExtension();
1110         return ApkBuilder.checkFileForPackaging(name, ext);
1111     }
1112 
1113     /**
1114      * Checks whether an {@link IFolder} and its content is valid for packaging into the .apk as
1115      * standard Java resource.
1116      * @param folder the {@link IFolder} to check.
1117      */
checkFolderForPackaging(IFolder folder)1118     public static boolean checkFolderForPackaging(IFolder folder) {
1119         String name = folder.getName();
1120         return ApkBuilder.checkFolderForPackaging(name);
1121     }
1122 
1123     /**
1124      * Returns a list of {@link IJavaProject} matching the provided {@link IProject} objects.
1125      * @param projects the IProject objects.
1126      * @return a new list object containing the IJavaProject object for the given IProject objects.
1127      * @throws CoreException
1128      */
getJavaProjects(List<IProject> projects)1129     public static List<IJavaProject> getJavaProjects(List<IProject> projects) throws CoreException {
1130         ArrayList<IJavaProject> list = new ArrayList<IJavaProject>();
1131 
1132         for (IProject p : projects) {
1133             if (p.isOpen() && p.hasNature(JavaCore.NATURE_ID)) {
1134 
1135                 list.add(JavaCore.create(p));
1136             }
1137         }
1138 
1139         return list;
1140     }
1141 
1142     /**
1143      * Get the stderr output of a process and return when the process is done.
1144      * @param process The process to get the output from
1145      * @param stderr The array to store the stderr output
1146      * @return the process return code.
1147      * @throws InterruptedException
1148      */
grabProcessOutput( final IProject project, final Process process, final ArrayList<String> stderr)1149     public final static int grabProcessOutput(
1150             final IProject project,
1151             final Process process,
1152             final ArrayList<String> stderr)
1153             throws InterruptedException {
1154 
1155         return GrabProcessOutput.grabProcessOutput(
1156                 process,
1157                 Wait.WAIT_FOR_READERS, // we really want to make sure we get all the output!
1158                 new IProcessOutput() {
1159 
1160                     @SuppressWarnings("unused")
1161                     @Override
1162                     public void out(@Nullable String line) {
1163                         if (line != null) {
1164                             // If benchmarking always print the lines that
1165                             // correspond to benchmarking info returned by ADT
1166                             if (BENCHMARK_FLAG && line.startsWith("BENCHMARK:")) {    //$NON-NLS-1$
1167                                 AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS,
1168                                         project, line);
1169                             } else {
1170                                 AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE,
1171                                         project, line);
1172                             }
1173                         }
1174                     }
1175 
1176                     @Override
1177                     public void err(@Nullable String line) {
1178                         if (line != null) {
1179                             stderr.add(line);
1180                             if (BuildVerbosity.VERBOSE == AdtPrefs.getPrefs().getBuildVerbosity()) {
1181                                 AdtPlugin.printErrorToConsole(project, line);
1182                             }
1183                         }
1184                     }
1185                 });
1186     }
1187 }
1188