1/*
2 * Copyright (C) 2007 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
17package com.android.ide.eclipse.adt.internal.build.builders;
18
19import com.android.SdkConstants;
20import com.android.ide.eclipse.adt.AdtConstants;
21import com.android.ide.eclipse.adt.AdtPlugin;
22import com.android.ide.eclipse.adt.AndroidPrintStream;
23import com.android.ide.eclipse.adt.internal.build.AaptExecException;
24import com.android.ide.eclipse.adt.internal.build.AaptParser;
25import com.android.ide.eclipse.adt.internal.build.AaptResultException;
26import com.android.ide.eclipse.adt.internal.build.BuildHelper;
27import com.android.ide.eclipse.adt.internal.build.BuildHelper.ResourceMarker;
28import com.android.ide.eclipse.adt.internal.build.DexException;
29import com.android.ide.eclipse.adt.internal.build.Messages;
30import com.android.ide.eclipse.adt.internal.build.NativeLibInJarException;
31import com.android.ide.eclipse.adt.internal.lint.LintDeltaProcessor;
32import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
33import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity;
34import com.android.ide.eclipse.adt.internal.project.ApkInstallManager;
35import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
36import com.android.ide.eclipse.adt.internal.project.LibraryClasspathContainerInitializer;
37import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
38import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
39import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
40import com.android.ide.eclipse.adt.internal.sdk.Sdk;
41import com.android.ide.eclipse.adt.io.IFileWrapper;
42import com.android.prefs.AndroidLocation.AndroidLocationException;
43import com.android.sdklib.build.ApkBuilder;
44import com.android.sdklib.build.ApkCreationException;
45import com.android.sdklib.build.DuplicateFileException;
46import com.android.sdklib.build.IArchiveBuilder;
47import com.android.sdklib.build.SealedApkException;
48import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException;
49import com.android.xml.AndroidManifest;
50
51import org.eclipse.core.resources.IContainer;
52import org.eclipse.core.resources.IFile;
53import org.eclipse.core.resources.IFolder;
54import org.eclipse.core.resources.IMarker;
55import org.eclipse.core.resources.IProject;
56import org.eclipse.core.resources.IResource;
57import org.eclipse.core.resources.IResourceDelta;
58import org.eclipse.core.runtime.CoreException;
59import org.eclipse.core.runtime.IPath;
60import org.eclipse.core.runtime.IProgressMonitor;
61import org.eclipse.core.runtime.IStatus;
62import org.eclipse.jdt.core.IJavaModelMarker;
63import org.eclipse.jdt.core.IJavaProject;
64import org.eclipse.jdt.core.JavaCore;
65
66import java.io.File;
67import java.io.FileInputStream;
68import java.io.FileOutputStream;
69import java.io.IOException;
70import java.io.InputStream;
71import java.util.ArrayList;
72import java.util.Collection;
73import java.util.List;
74import java.util.Map;
75import java.util.jar.Attributes;
76import java.util.jar.JarEntry;
77import java.util.jar.JarOutputStream;
78import java.util.jar.Manifest;
79import java.util.regex.Pattern;
80
81public class PostCompilerBuilder extends BaseBuilder {
82
83    /** This ID is used in plugin.xml and in each project's .project file.
84     * It cannot be changed even if the class is renamed/moved */
85    public static final String ID = "com.android.ide.eclipse.adt.ApkBuilder"; //$NON-NLS-1$
86
87    private static final String PROPERTY_CONVERT_TO_DEX = "convertToDex"; //$NON-NLS-1$
88    private static final String PROPERTY_PACKAGE_RESOURCES = "packageResources"; //$NON-NLS-1$
89    private static final String PROPERTY_BUILD_APK = "buildApk"; //$NON-NLS-1$
90
91    /** Flag to pass to PostCompiler builder that sets if it runs or not.
92     *  Set this flag whenever calling build if PostCompiler is to run
93     */
94    public final static String POST_C_REQUESTED = "RunPostCompiler"; //$NON-NLS-1$
95
96    /**
97     * Dex conversion flag. This is set to true if one of the changed/added/removed
98     * file is a .class file. Upon visiting all the delta resource, if this
99     * flag is true, then we know we'll have to make the "classes.dex" file.
100     */
101    private boolean mConvertToDex = false;
102
103    /**
104     * Package resources flag. This is set to true if one of the changed/added/removed
105     * file is a resource file. Upon visiting all the delta resource, if
106     * this flag is true, then we know we'll have to repackage the resources.
107     */
108    private boolean mPackageResources = false;
109
110    /**
111     * Final package build flag.
112     */
113    private boolean mBuildFinalPackage = false;
114
115    private AndroidPrintStream mOutStream = null;
116    private AndroidPrintStream mErrStream = null;
117
118
119    private ResourceMarker mResourceMarker = new ResourceMarker() {
120        @Override
121        public void setWarning(IResource resource, String message) {
122            BaseProjectHelper.markResource(resource, AdtConstants.MARKER_PACKAGING,
123                    message, IMarker.SEVERITY_WARNING);
124        }
125    };
126
127
128    public PostCompilerBuilder() {
129        super();
130    }
131
132    @Override
133    protected void clean(IProgressMonitor monitor) throws CoreException {
134        super.clean(monitor);
135
136        // Get the project.
137        IProject project = getProject();
138
139        if (DEBUG_LOG) {
140            AdtPlugin.log(IStatus.INFO, "%s CLEAN(POST)", project.getName());
141        }
142
143        // Clear the project of the generic markers
144        removeMarkersFromContainer(project, AdtConstants.MARKER_AAPT_PACKAGE);
145        removeMarkersFromContainer(project, AdtConstants.MARKER_PACKAGING);
146
147        // also remove the files in the output folder (but not the Eclipse output folder).
148        IFolder javaOutput = BaseProjectHelper.getJavaOutputFolder(project);
149        IFolder androidOutput = BaseProjectHelper.getAndroidOutputFolder(project);
150
151        if (javaOutput.equals(androidOutput) == false) {
152            // get the content
153            IResource[] members = androidOutput.members();
154            for (IResource member : members) {
155                if (member.equals(javaOutput) == false) {
156                    member.delete(true /*force*/, monitor);
157                }
158            }
159        }
160    }
161
162    // build() returns a list of project from which this project depends for future compilation.
163    @Override
164    protected IProject[] build(
165            int kind,
166            @SuppressWarnings("rawtypes") Map args,
167            IProgressMonitor monitor)
168            throws CoreException {
169        // get a project object
170        IProject project = getProject();
171
172        if (DEBUG_LOG) {
173            AdtPlugin.log(IStatus.INFO, "%s BUILD(POST)", project.getName());
174        }
175
176        // Benchmarking start
177        long startBuildTime = 0;
178        if (BuildHelper.BENCHMARK_FLAG) {
179            // End JavaC Timer
180            String msg = "BENCHMARK ADT: Ending Compilation \n BENCHMARK ADT: Time Elapsed: " +    //$NON-NLS-1$
181                         (System.nanoTime() - BuildHelper.sStartJavaCTime)/Math.pow(10, 6) + "ms"; //$NON-NLS-1$
182            AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, project, msg);
183            msg = "BENCHMARK ADT: Starting PostCompilation";                                       //$NON-NLS-1$
184            AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, project, msg);
185            startBuildTime = System.nanoTime();
186        }
187
188        // list of referenced projects. This is a mix of java projects and library projects
189        // and is computed below.
190        IProject[] allRefProjects = null;
191
192        try {
193            // get the project info
194            ProjectState projectState = Sdk.getProjectState(project);
195
196            // this can happen if the project has no project.properties.
197            if (projectState == null) {
198                return null;
199            }
200
201            boolean isLibrary = projectState.isLibrary();
202
203            // get the libraries
204            List<IProject> libProjects = projectState.getFullLibraryProjects();
205
206            IJavaProject javaProject = JavaCore.create(project);
207
208            // get the list of referenced projects.
209            List<IProject> javaProjects = ProjectHelper.getReferencedProjects(project);
210            List<IJavaProject> referencedJavaProjects = BuildHelper.getJavaProjects(
211                    javaProjects);
212
213            // mix the java project and the library projects
214            final int size = libProjects.size() + javaProjects.size();
215            ArrayList<IProject> refList = new ArrayList<IProject>(size);
216            refList.addAll(libProjects);
217            refList.addAll(javaProjects);
218            allRefProjects = refList.toArray(new IProject[size]);
219
220            // get the android output folder
221            IFolder androidOutputFolder = BaseProjectHelper.getAndroidOutputFolder(project);
222            IFolder resOutputFolder = androidOutputFolder.getFolder(SdkConstants.FD_RES);
223
224            // First thing we do is go through the resource delta to not
225            // lose it if we have to abort the build for any reason.
226            if (args.containsKey(POST_C_REQUESTED)
227                    && AdtPrefs.getPrefs().getBuildSkipPostCompileOnFileSave()) {
228                // Skip over flag setting
229            } else if (kind == FULL_BUILD) {
230                AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project,
231                        Messages.Start_Full_Apk_Build);
232
233                if (DEBUG_LOG) {
234                    AdtPlugin.log(IStatus.INFO, "%s full build!", project.getName());
235                }
236
237                // Full build: we do all the steps.
238                mPackageResources = true;
239                mConvertToDex = true;
240                mBuildFinalPackage = true;
241            } else {
242                AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project,
243                        Messages.Start_Inc_Apk_Build);
244
245                // go through the resources and see if something changed.
246                IResourceDelta delta = getDelta(project);
247                if (delta == null) {
248                    // no delta? Same as full build: we do all the steps.
249                    mPackageResources = true;
250                    mConvertToDex = true;
251                    mBuildFinalPackage = true;
252                } else {
253
254                    if (ResourceManager.isAutoBuilding() && AdtPrefs.getPrefs().isLintOnSave()) {
255                        // Check for errors on save/build, if enabled
256                        LintDeltaProcessor.create().process(delta);
257                    }
258
259                    PatternBasedDeltaVisitor dv = new PatternBasedDeltaVisitor(
260                            project, project,
261                            "POST:Main");
262
263                    ChangedFileSet manifestCfs = ChangedFileSetHelper.getMergedManifestCfs(project);
264                    dv.addSet(manifestCfs);
265
266                    ChangedFileSet resCfs = ChangedFileSetHelper.getResCfs(project);
267                    dv.addSet(resCfs);
268
269                    ChangedFileSet androidCodeCfs = ChangedFileSetHelper.getCodeCfs(project);
270                    dv.addSet(androidCodeCfs);
271
272                    ChangedFileSet javaResCfs = ChangedFileSetHelper.getJavaResCfs(project);
273                    dv.addSet(javaResCfs);
274                    dv.addSet(ChangedFileSetHelper.NATIVE_LIBS);
275
276                    delta.accept(dv);
277
278                    // save the state
279                    mPackageResources |= dv.checkSet(manifestCfs) || dv.checkSet(resCfs);
280
281                    mConvertToDex |= dv.checkSet(androidCodeCfs);
282
283                    mBuildFinalPackage |= dv.checkSet(javaResCfs) ||
284                            dv.checkSet(ChangedFileSetHelper.NATIVE_LIBS);
285                }
286
287                // check the libraries
288                if (libProjects.size() > 0) {
289                    for (IProject libProject : libProjects) {
290                        delta = getDelta(libProject);
291                        if (delta != null) {
292                            PatternBasedDeltaVisitor visitor = new PatternBasedDeltaVisitor(
293                                    project, libProject,
294                                    "POST:Lib");
295
296                            ChangedFileSet libResCfs = ChangedFileSetHelper.getFullResCfs(
297                                    libProject);
298                            visitor.addSet(libResCfs);
299                            visitor.addSet(ChangedFileSetHelper.NATIVE_LIBS);
300                            // FIXME: add check on the library.jar?
301
302                            delta.accept(visitor);
303
304                            mPackageResources |= visitor.checkSet(libResCfs);
305                            mBuildFinalPackage |= visitor.checkSet(
306                                    ChangedFileSetHelper.NATIVE_LIBS);
307                        }
308                    }
309                }
310
311                // also go through the delta for all the referenced projects
312                final int referencedCount = referencedJavaProjects.size();
313                for (int i = 0 ; i < referencedCount; i++) {
314                    IJavaProject referencedJavaProject = referencedJavaProjects.get(i);
315                    delta = getDelta(referencedJavaProject.getProject());
316                    if (delta != null) {
317                        PatternBasedDeltaVisitor visitor = new PatternBasedDeltaVisitor(
318                                project, referencedJavaProject.getProject(),
319                                "POST:RefedProject");
320
321                        ChangedFileSet javaResCfs = ChangedFileSetHelper.getJavaResCfs(project);
322                        visitor.addSet(javaResCfs);
323
324                        ChangedFileSet bytecodeCfs = ChangedFileSetHelper.getByteCodeCfs(project);
325                        visitor.addSet(bytecodeCfs);
326
327                        delta.accept(visitor);
328
329                        // save the state
330                        mConvertToDex |= visitor.checkSet(bytecodeCfs);
331                        mBuildFinalPackage |= visitor.checkSet(javaResCfs);
332                    }
333                }
334            }
335
336            // store the build status in the persistent storage
337            saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, mConvertToDex);
338            saveProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, mPackageResources);
339            saveProjectBooleanProperty(PROPERTY_BUILD_APK, mBuildFinalPackage);
340
341            // Top level check to make sure the build can move forward. Only do this after recording
342            // delta changes.
343            abortOnBadSetup(javaProject);
344
345            // remove older packaging markers.
346            removeMarkersFromContainer(javaProject.getProject(), AdtConstants.MARKER_PACKAGING);
347
348            if (androidOutputFolder == null) {
349                // mark project and exit
350                markProject(AdtConstants.MARKER_PACKAGING, Messages.Failed_To_Get_Output,
351                        IMarker.SEVERITY_ERROR);
352                return allRefProjects;
353            }
354
355            // finished with the common init and tests. Special case of the library.
356            if (isLibrary) {
357                // check the jar output file is present, if not create it.
358                IFile jarIFile = androidOutputFolder.getFile(
359                        project.getName().toLowerCase() + SdkConstants.DOT_JAR);
360                if (mConvertToDex == false && jarIFile.exists() == false) {
361                    mConvertToDex = true;
362                }
363
364                // also update the crunch cache always since aapt does it smartly only
365                // on the files that need it.
366                if (DEBUG_LOG) {
367                    AdtPlugin.log(IStatus.INFO, "%s running crunch!", project.getName());
368                }
369                BuildHelper helper = new BuildHelper(project,
370                        mOutStream, mErrStream,
371                        true /*debugMode*/,
372                        AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE,
373                        mResourceMarker);
374                updateCrunchCache(project, helper);
375
376                // refresh recursively bin/res folder
377                resOutputFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor);
378
379                if (mConvertToDex) { // in this case this means some class files changed and
380                                     // we need to update the jar file.
381                    if (DEBUG_LOG) {
382                        AdtPlugin.log(IStatus.INFO, "%s updating jar!", project.getName());
383                    }
384
385                    // resource to the AndroidManifest.xml file
386                    IFile manifestFile = project.getFile(SdkConstants.FN_ANDROID_MANIFEST_XML);
387                    String appPackage = AndroidManifest.getPackage(new IFileWrapper(manifestFile));
388
389                    IFolder javaOutputFolder = BaseProjectHelper.getJavaOutputFolder(project);
390
391                    writeLibraryPackage(jarIFile, project, appPackage, javaOutputFolder);
392                    saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, mConvertToDex = false);
393
394                    // refresh the bin folder content with no recursion to update the library
395                    // jar file.
396                    androidOutputFolder.refreshLocal(IResource.DEPTH_ONE, monitor);
397
398                    // Also update the projects. The only way to force recompile them is to
399                    // reset the library container.
400                    List<ProjectState> parentProjects = projectState.getParentProjects();
401                    LibraryClasspathContainerInitializer.updateProject(parentProjects);
402                }
403
404                return allRefProjects;
405            }
406
407            // Check to see if we're going to launch or export. If not, we can skip
408            // the packaging and dexing process.
409            if (!args.containsKey(POST_C_REQUESTED)
410                    && AdtPrefs.getPrefs().getBuildSkipPostCompileOnFileSave()) {
411                AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project,
412                        Messages.Skip_Post_Compiler);
413                return allRefProjects;
414            } else {
415                AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project,
416                        Messages.Start_Full_Post_Compiler);
417            }
418
419            // first thing we do is check that the SDK directory has been setup.
420            String osSdkFolder = AdtPlugin.getOsSdkFolder();
421
422            if (osSdkFolder.length() == 0) {
423                // this has already been checked in the precompiler. Therefore,
424                // while we do have to cancel the build, we don't have to return
425                // any error or throw anything.
426                return allRefProjects;
427            }
428
429            // do some extra check, in case the output files are not present. This
430            // will force to recreate them.
431            IResource tmp = null;
432
433            if (mPackageResources == false) {
434                // check the full resource package
435                tmp = androidOutputFolder.findMember(AdtConstants.FN_RESOURCES_AP_);
436                if (tmp == null || tmp.exists() == false) {
437                    mPackageResources = true;
438                }
439            }
440
441            // check classes.dex is present. If not we force to recreate it.
442            if (mConvertToDex == false) {
443                tmp = androidOutputFolder.findMember(SdkConstants.FN_APK_CLASSES_DEX);
444                if (tmp == null || tmp.exists() == false) {
445                    mConvertToDex = true;
446                }
447            }
448
449            // also check the final file(s)!
450            String finalPackageName = ProjectHelper.getApkFilename(project, null /*config*/);
451            if (mBuildFinalPackage == false) {
452                tmp = androidOutputFolder.findMember(finalPackageName);
453                if (tmp == null || (tmp instanceof IFile &&
454                        tmp.exists() == false)) {
455                    String msg = String.format(Messages.s_Missing_Repackaging, finalPackageName);
456                    AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, msg);
457                    mBuildFinalPackage = true;
458                }
459            }
460
461            // at this point we know if we need to recreate the temporary apk
462            // or the dex file, but we don't know if we simply need to recreate them
463            // because they are missing
464
465            // refresh the output directory first
466            IContainer ic = androidOutputFolder.getParent();
467            if (ic != null) {
468                ic.refreshLocal(IResource.DEPTH_ONE, monitor);
469            }
470
471            // Get the DX output stream. Since the builder is created for the life of the
472            // project, they can be kept around.
473            if (mOutStream == null) {
474                mOutStream = new AndroidPrintStream(project, null /*prefix*/,
475                        AdtPlugin.getOutStream());
476                mErrStream = new AndroidPrintStream(project, null /*prefix*/,
477                        AdtPlugin.getOutStream());
478            }
479
480            // we need to test all three, as we may need to make the final package
481            // but not the intermediary ones.
482            if (mPackageResources || mConvertToDex || mBuildFinalPackage) {
483                BuildHelper helper = new BuildHelper(project,
484                        mOutStream, mErrStream,
485                        true /*debugMode*/,
486                        AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE,
487                        mResourceMarker);
488
489                IPath androidBinLocation = androidOutputFolder.getLocation();
490                if (androidBinLocation == null) {
491                    markProject(AdtConstants.MARKER_PACKAGING, Messages.Output_Missing,
492                            IMarker.SEVERITY_ERROR);
493                    return allRefProjects;
494                }
495                String osAndroidBinPath = androidBinLocation.toOSString();
496
497                // resource to the AndroidManifest.xml file
498                IFile manifestFile = androidOutputFolder.getFile(
499                        SdkConstants.FN_ANDROID_MANIFEST_XML);
500
501                if (manifestFile == null || manifestFile.exists() == false) {
502                    // mark project and exit
503                    String msg = String.format(Messages.s_File_Missing,
504                            SdkConstants.FN_ANDROID_MANIFEST_XML);
505                    markProject(AdtConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR);
506                    return allRefProjects;
507                }
508
509                // Remove the old .apk.
510                // This make sure that if the apk is corrupted, then dx (which would attempt
511                // to open it), will not fail.
512                String osFinalPackagePath = osAndroidBinPath + File.separator + finalPackageName;
513                File finalPackage = new File(osFinalPackagePath);
514
515                // if delete failed, this is not really a problem, as the final package generation
516                // handle already present .apk, and if that one failed as well, the user will be
517                // notified.
518                finalPackage.delete();
519
520                // Check if we need to package the resources.
521                if (mPackageResources) {
522                    // also update the crunch cache always since aapt does it smartly only
523                    // on the files that need it.
524                    if (DEBUG_LOG) {
525                        AdtPlugin.log(IStatus.INFO, "%s running crunch!", project.getName());
526                    }
527                    if (updateCrunchCache(project, helper) == false) {
528                        return allRefProjects;
529                    }
530
531                    // refresh recursively bin/res folder
532                    resOutputFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor);
533
534                    if (DEBUG_LOG) {
535                        AdtPlugin.log(IStatus.INFO, "%s packaging resources!", project.getName());
536                    }
537                    // remove some aapt_package only markers.
538                    removeMarkersFromContainer(project, AdtConstants.MARKER_AAPT_PACKAGE);
539
540                    try {
541                        helper.packageResources(manifestFile, libProjects, null /*resfilter*/,
542                                0 /*versionCode */, osAndroidBinPath,
543                                AdtConstants.FN_RESOURCES_AP_);
544                    } catch (AaptExecException e) {
545                        BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING,
546                                e.getMessage(), IMarker.SEVERITY_ERROR);
547                        return allRefProjects;
548                    } catch (AaptResultException e) {
549                        // attempt to parse the error output
550                        String[] aaptOutput = e.getOutput();
551                        boolean parsingError = AaptParser.parseOutput(aaptOutput, project);
552
553                        // if we couldn't parse the output we display it in the console.
554                        if (parsingError) {
555                            AdtPlugin.printErrorToConsole(project, (Object[]) aaptOutput);
556
557                            // if the exec failed, and we couldn't parse the error output (and
558                            // therefore not all files that should have been marked, were marked),
559                            // we put a generic marker on the project and abort.
560                            BaseProjectHelper.markResource(project,
561                                    AdtConstants.MARKER_PACKAGING,
562                                    Messages.Unparsed_AAPT_Errors,
563                                    IMarker.SEVERITY_ERROR);
564                        }
565                    }
566
567                    // build has been done. reset the state of the builder
568                    mPackageResources = false;
569
570                    // and store it
571                    saveProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, mPackageResources);
572                }
573
574                String classesDexPath = osAndroidBinPath + File.separator +
575                        SdkConstants.FN_APK_CLASSES_DEX;
576
577                // then we check if we need to package the .class into classes.dex
578                if (mConvertToDex) {
579                    if (DEBUG_LOG) {
580                        AdtPlugin.log(IStatus.INFO, "%s running dex!", project.getName());
581                    }
582                    try {
583                        Collection<String> dxInputPaths = helper.getCompiledCodePaths();
584
585                        helper.executeDx(javaProject, dxInputPaths, classesDexPath);
586                    } catch (DexException e) {
587                        String message = e.getMessage();
588
589                        AdtPlugin.printErrorToConsole(project, message);
590                        BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING,
591                                message, IMarker.SEVERITY_ERROR);
592
593                        Throwable cause = e.getCause();
594
595                        if (cause instanceof NoClassDefFoundError
596                                || cause instanceof NoSuchMethodError) {
597                            AdtPlugin.printErrorToConsole(project, Messages.Incompatible_VM_Warning,
598                                    Messages.Requires_1_5_Error);
599                        }
600
601                        // dx failed, we return
602                        return allRefProjects;
603                    }
604
605                    // build has been done. reset the state of the builder
606                    mConvertToDex = false;
607
608                    // and store it
609                    saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, mConvertToDex);
610                }
611
612                // now we need to make the final package from the intermediary apk
613                // and classes.dex.
614                // This is the default package with all the resources.
615
616                try {
617                    if (DEBUG_LOG) {
618                        AdtPlugin.log(IStatus.INFO, "%s making final package!", project.getName());
619                    }
620                    helper.finalDebugPackage(
621                            osAndroidBinPath + File.separator + AdtConstants.FN_RESOURCES_AP_,
622                        classesDexPath, osFinalPackagePath, libProjects, mResourceMarker);
623                } catch (KeytoolException e) {
624                    String eMessage = e.getMessage();
625
626                    // mark the project with the standard message
627                    String msg = String.format(Messages.Final_Archive_Error_s, eMessage);
628                    BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, msg,
629                            IMarker.SEVERITY_ERROR);
630
631                    // output more info in the console
632                    AdtPlugin.printErrorToConsole(project,
633                            msg,
634                            String.format(Messages.ApkBuilder_JAVA_HOME_is_s, e.getJavaHome()),
635                            Messages.ApkBuilder_Update_or_Execute_manually_s,
636                            e.getCommandLine());
637
638                    return allRefProjects;
639                } catch (ApkCreationException e) {
640                    String eMessage = e.getMessage();
641
642                    // mark the project with the standard message
643                    String msg = String.format(Messages.Final_Archive_Error_s, eMessage);
644                    BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, msg,
645                            IMarker.SEVERITY_ERROR);
646                } catch (AndroidLocationException e) {
647                    String eMessage = e.getMessage();
648
649                    // mark the project with the standard message
650                    String msg = String.format(Messages.Final_Archive_Error_s, eMessage);
651                    BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, msg,
652                            IMarker.SEVERITY_ERROR);
653                } catch (NativeLibInJarException e) {
654                    String msg = e.getMessage();
655
656                    BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING,
657                            msg, IMarker.SEVERITY_ERROR);
658
659                    AdtPlugin.printErrorToConsole(project, (Object[]) e.getAdditionalInfo());
660                } catch (CoreException e) {
661                    // mark project and return
662                    String msg = String.format(Messages.Final_Archive_Error_s, e.getMessage());
663                    AdtPlugin.printErrorToConsole(project, msg);
664                    BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, msg,
665                            IMarker.SEVERITY_ERROR);
666                } catch (DuplicateFileException e) {
667                    String msg1 = String.format(
668                            "Found duplicate file for APK: %1$s\nOrigin 1: %2$s\nOrigin 2: %3$s",
669                            e.getArchivePath(), e.getFile1(), e.getFile2());
670                    String msg2 = String.format(Messages.Final_Archive_Error_s, msg1);
671                    AdtPlugin.printErrorToConsole(project, msg2);
672                    BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, msg2,
673                            IMarker.SEVERITY_ERROR);
674                }
675
676                // we are done.
677
678                // refresh the bin folder content with no recursion.
679                androidOutputFolder.refreshLocal(IResource.DEPTH_ONE, monitor);
680
681                // build has been done. reset the state of the builder
682                mBuildFinalPackage = false;
683
684                // and store it
685                saveProjectBooleanProperty(PROPERTY_BUILD_APK, mBuildFinalPackage);
686
687                // reset the installation manager to force new installs of this project
688                ApkInstallManager.getInstance().resetInstallationFor(project);
689
690                AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, getProject(),
691                        "Build Success!");
692            }
693        } catch (AbortBuildException e) {
694            return allRefProjects;
695        } catch (Exception exception) {
696            // try to catch other exception to actually display an error. This will be useful
697            // if we get an NPE or something so that we can at least notify the user that something
698            // went wrong.
699
700            // first check if this is a CoreException we threw to cancel the build.
701            if (exception instanceof CoreException) {
702                if (((CoreException)exception).getStatus().getSeverity() == IStatus.CANCEL) {
703                    // Project is already marked with an error. Nothing to do
704                    return allRefProjects;
705                }
706            }
707
708            String msg = exception.getMessage();
709            if (msg == null) {
710                msg = exception.getClass().getCanonicalName();
711            }
712
713            msg = String.format("Unknown error: %1$s", msg);
714            AdtPlugin.logAndPrintError(exception, project.getName(), msg);
715            markProject(AdtConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR);
716        }
717
718        // Benchmarking end
719        if (BuildHelper.BENCHMARK_FLAG) {
720            String msg = "BENCHMARK ADT: Ending PostCompilation. \n BENCHMARK ADT: Time Elapsed: " + //$NON-NLS-1$
721                         ((System.nanoTime() - startBuildTime)/Math.pow(10, 6)) + "ms";              //$NON-NLS-1$
722            AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, project, msg);
723            // End Overall Timer
724            msg = "BENCHMARK ADT: Done with everything! \n BENCHMARK ADT: Time Elapsed: " +          //$NON-NLS-1$
725                  (System.nanoTime() - BuildHelper.sStartOverallTime)/Math.pow(10, 6) + "ms";        //$NON-NLS-1$
726            AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, project, msg);
727        }
728
729        return allRefProjects;
730    }
731
732    private static class JarBuilder implements IArchiveBuilder {
733
734        private static Pattern R_PATTERN = Pattern.compile("R(\\$.*)?\\.class"); //$NON-NLS-1$
735        private static String BUILD_CONFIG_CLASS = "BuildConfig.class"; //$NON-NLS-1$
736
737        private final byte[] buffer = new byte[1024];
738        private final JarOutputStream mOutputStream;
739        private final String mAppPackage;
740
741        JarBuilder(JarOutputStream outputStream, String appPackage) {
742            mOutputStream = outputStream;
743            mAppPackage = appPackage.replace('.', '/');
744        }
745
746        public void addFile(IFile file, IFolder rootFolder) throws ApkCreationException {
747            // we only package class file from the output folder
748            if (SdkConstants.EXT_CLASS.equals(file.getFileExtension()) == false) {
749                return;
750            }
751
752            IPath packageApp = file.getParent().getFullPath().makeRelativeTo(
753                    rootFolder.getFullPath());
754
755            String name = file.getName();
756            // Ignore the library's R/Manifest/BuildConfig classes.
757            if (mAppPackage.equals(packageApp.toString()) &&
758                            (BUILD_CONFIG_CLASS.equals(name) ||
759                            R_PATTERN.matcher(name).matches())) {
760                return;
761            }
762
763            IPath path = file.getFullPath().makeRelativeTo(rootFolder.getFullPath());
764            try {
765                addFile(file.getContents(), file.getLocalTimeStamp(), path.toString());
766            } catch (ApkCreationException e) {
767                throw e;
768            } catch (Exception e) {
769                throw new ApkCreationException(e, "Failed to add %s", file);
770            }
771        }
772
773        @Override
774        public void addFile(File file, String archivePath) throws ApkCreationException,
775                SealedApkException, DuplicateFileException {
776            try {
777                FileInputStream inputStream = new FileInputStream(file);
778                long lastModified = file.lastModified();
779                addFile(inputStream, lastModified, archivePath);
780            } catch (ApkCreationException e) {
781                throw e;
782            } catch (Exception e) {
783                throw new ApkCreationException(e, "Failed to add %s", file);
784            }
785        }
786
787        private void addFile(InputStream content, long lastModified, String archivePath)
788                throws IOException, ApkCreationException {
789            // create the jar entry
790            JarEntry entry = new JarEntry(archivePath);
791            entry.setTime(lastModified);
792
793            try {
794                // add the entry to the jar archive
795                mOutputStream.putNextEntry(entry);
796
797                // read the content of the entry from the input stream, and write
798                // it into the archive.
799                int count;
800                while ((count = content.read(buffer)) != -1) {
801                    mOutputStream.write(buffer, 0, count);
802                }
803            } finally {
804                try {
805                    if (content != null) {
806                        content.close();
807                    }
808                } catch (Exception e) {
809                    throw new ApkCreationException(e, "Failed to close stream");
810                }
811            }
812        }
813    }
814
815    /**
816     * Updates the crunch cache if needed and return true if the build must continue.
817     */
818    private boolean updateCrunchCache(IProject project, BuildHelper helper) {
819        try {
820            helper.updateCrunchCache();
821        } catch (AaptExecException e) {
822            BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING,
823                    e.getMessage(), IMarker.SEVERITY_ERROR);
824            return false;
825        } catch (AaptResultException e) {
826            // attempt to parse the error output
827            String[] aaptOutput = e.getOutput();
828            boolean parsingError = AaptParser.parseOutput(aaptOutput, project);
829            // if we couldn't parse the output we display it in the console.
830            if (parsingError) {
831                AdtPlugin.printErrorToConsole(project, (Object[]) aaptOutput);
832            }
833        }
834
835        return true;
836    }
837
838    /**
839     * Writes the library jar file.
840     * @param jarIFile the destination file
841     * @param project the library project
842     * @param appPackage the library android package
843     * @param javaOutputFolder the JDT output folder.
844     */
845    private void writeLibraryPackage(IFile jarIFile, IProject project, String appPackage,
846            IFolder javaOutputFolder) {
847
848        JarOutputStream jos = null;
849        try {
850            Manifest manifest = new Manifest();
851            Attributes mainAttributes = manifest.getMainAttributes();
852            mainAttributes.put(Attributes.Name.CLASS_PATH, "Android ADT"); //$NON-NLS-1$
853            mainAttributes.putValue("Created-By", "1.0 (Android)"); //$NON-NLS-1$  //$NON-NLS-2$
854            jos = new JarOutputStream(
855                    new FileOutputStream(jarIFile.getLocation().toFile()), manifest);
856
857            JarBuilder jarBuilder = new JarBuilder(jos, appPackage);
858
859            // write the class files
860            writeClassFilesIntoJar(jarBuilder, javaOutputFolder, javaOutputFolder);
861
862            // now write the standard Java resources from the output folder
863            ApkBuilder.addSourceFolder(jarBuilder, javaOutputFolder.getLocation().toFile());
864
865            saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, mConvertToDex);
866        } catch (Exception e) {
867            AdtPlugin.log(e, "Failed to write jar file %s", jarIFile.getLocation().toOSString());
868        } finally {
869            if (jos != null) {
870                try {
871                    jos.close();
872                } catch (IOException e) {
873                    // pass
874                }
875            }
876        }
877    }
878
879    private void writeClassFilesIntoJar(JarBuilder builder, IFolder folder, IFolder rootFolder)
880            throws CoreException, IOException, ApkCreationException {
881        IResource[] members = folder.members();
882        for (IResource member : members) {
883            if (member.getType() == IResource.FOLDER) {
884                writeClassFilesIntoJar(builder, (IFolder) member, rootFolder);
885            } else if (member.getType() == IResource.FILE) {
886                IFile file = (IFile) member;
887                builder.addFile(file, rootFolder);
888            }
889        }
890    }
891
892    @Override
893    protected void startupOnInitialize() {
894        super.startupOnInitialize();
895
896        // load the build status. We pass true as the default value to
897        // force a recompile in case the property was not found
898        mConvertToDex = loadProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, true);
899        mPackageResources = loadProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, true);
900        mBuildFinalPackage = loadProjectBooleanProperty(PROPERTY_BUILD_APK, true);
901    }
902
903    @Override
904    protected void abortOnBadSetup(IJavaProject javaProject) throws AbortBuildException {
905        super.abortOnBadSetup(javaProject);
906
907        IProject iProject = getProject();
908
909        // do a (hopefully quick) search for Precompiler type markers. Those are always only
910        // errors.
911        stopOnMarker(iProject, AdtConstants.MARKER_AAPT_COMPILE, IResource.DEPTH_INFINITE,
912                false /*checkSeverity*/);
913        stopOnMarker(iProject, AdtConstants.MARKER_AIDL, IResource.DEPTH_INFINITE,
914                false /*checkSeverity*/);
915        stopOnMarker(iProject, AdtConstants.MARKER_RENDERSCRIPT, IResource.DEPTH_INFINITE,
916                false /*checkSeverity*/);
917        stopOnMarker(iProject, AdtConstants.MARKER_ANDROID, IResource.DEPTH_ZERO,
918                false /*checkSeverity*/);
919
920        // do a search for JDT markers. Those can be errors or warnings
921        stopOnMarker(iProject, IJavaModelMarker.JAVA_MODEL_PROBLEM_MARKER,
922                IResource.DEPTH_INFINITE, true /*checkSeverity*/);
923        stopOnMarker(iProject, IJavaModelMarker.BUILDPATH_PROBLEM_MARKER,
924                IResource.DEPTH_INFINITE, true /*checkSeverity*/);
925    }
926}
927