• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.ide.eclipse.adt.internal.build;
18 
19 import com.android.ide.eclipse.adt.AdtPlugin;
20 import com.android.ide.eclipse.adt.AndroidConstants;
21 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity;
22 import com.android.ide.eclipse.adt.internal.project.ApkInstallManager;
23 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
24 import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
25 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
26 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
27 import com.android.sdklib.SdkConstants;
28 import com.android.sdklib.xml.AndroidManifest;
29 import com.android.sdklib.xml.AndroidXPathFactory;
30 
31 import org.eclipse.core.resources.IContainer;
32 import org.eclipse.core.resources.IFile;
33 import org.eclipse.core.resources.IFolder;
34 import org.eclipse.core.resources.IMarker;
35 import org.eclipse.core.resources.IProject;
36 import org.eclipse.core.resources.IResource;
37 import org.eclipse.core.resources.IResourceDelta;
38 import org.eclipse.core.resources.IResourceDeltaVisitor;
39 import org.eclipse.core.runtime.CoreException;
40 import org.eclipse.core.runtime.IPath;
41 import org.eclipse.core.runtime.IProgressMonitor;
42 import org.eclipse.core.runtime.IStatus;
43 import org.eclipse.jdt.core.IJavaProject;
44 import org.eclipse.jdt.core.JavaCore;
45 import org.eclipse.jdt.core.JavaModelException;
46 import org.xml.sax.InputSource;
47 
48 import java.io.File;
49 import java.io.PrintStream;
50 import java.util.ArrayList;
51 import java.util.Map;
52 
53 import javax.xml.xpath.XPath;
54 
55 public class PostCompilerBuilder extends BaseBuilder {
56 
57     private static final String CONSOLE_PREFIX_DX = "Dx"; //$NON-NLS-1$
58 
59     /** This ID is used in plugin.xml and in each project's .project file.
60      * It cannot be changed even if the class is renamed/moved */
61     public static final String ID = "com.android.ide.eclipse.adt.ApkBuilder"; //$NON-NLS-1$
62 
63     private static final String PROPERTY_CONVERT_TO_DEX = "convertToDex"; //$NON-NLS-1$
64     private static final String PROPERTY_PACKAGE_RESOURCES = "packageResources"; //$NON-NLS-1$
65     private static final String PROPERTY_BUILD_APK = "buildApk"; //$NON-NLS-1$
66 
67     /**
68      * Dex conversion flag. This is set to true if one of the changed/added/removed
69      * file is a .class file. Upon visiting all the delta resource, if this
70      * flag is true, then we know we'll have to make the "classes.dex" file.
71      */
72     private boolean mConvertToDex = false;
73 
74     /**
75      * Package resources flag. This is set to true if one of the changed/added/removed
76      * file is a resource file. Upon visiting all the delta resource, if
77      * this flag is true, then we know we'll have to repackage the resources.
78      */
79     private boolean mPackageResources = false;
80 
81     /**
82      * Final package build flag.
83      */
84     private boolean mBuildFinalPackage = false;
85 
86     private PrintStream mDxOutStream = null;
87     private PrintStream mDxErrStream = null;
88 
89     /**
90      * Basic Resource Delta Visitor class to check if a referenced project had a change in its
91      * compiled java files.
92      */
93     private static class ReferencedProjectDeltaVisitor implements IResourceDeltaVisitor {
94 
95         private boolean mConvertToDex = false;
96         private boolean mMakeFinalPackage;
97 
98         private IPath mOutputFolder;
99         private ArrayList<IPath> mSourceFolders;
100 
ReferencedProjectDeltaVisitor(IJavaProject javaProject)101         private ReferencedProjectDeltaVisitor(IJavaProject javaProject) {
102             try {
103                 mOutputFolder = javaProject.getOutputLocation();
104                 mSourceFolders = BaseProjectHelper.getSourceClasspaths(javaProject);
105             } catch (JavaModelException e) {
106             } finally {
107             }
108         }
109 
110         /**
111          * {@inheritDoc}
112          * @throws CoreException
113          */
visit(IResourceDelta delta)114         public boolean visit(IResourceDelta delta) throws CoreException {
115             //  no need to keep looking if we already know we need to convert
116             // to dex and make the final package.
117             if (mConvertToDex && mMakeFinalPackage) {
118                 return false;
119             }
120 
121             // get the resource and the path segments.
122             IResource resource = delta.getResource();
123             IPath resourceFullPath = resource.getFullPath();
124 
125             if (mOutputFolder.isPrefixOf(resourceFullPath)) {
126                 int type = resource.getType();
127                 if (type == IResource.FILE) {
128                     String ext = resource.getFileExtension();
129                     if (AndroidConstants.EXT_CLASS.equals(ext)) {
130                         mConvertToDex = true;
131                     }
132                 }
133                 return true;
134             } else {
135                 for (IPath sourceFullPath : mSourceFolders) {
136                     if (sourceFullPath.isPrefixOf(resourceFullPath)) {
137                         int type = resource.getType();
138                         if (type == IResource.FILE) {
139                             // check if the file is a valid file that would be
140                             // included during the final packaging.
141                             if (PostCompilerHelper.checkFileForPackaging((IFile)resource)) {
142                                 mMakeFinalPackage = true;
143                             }
144 
145                             return false;
146                         } else if (type == IResource.FOLDER) {
147                             // if this is a folder, we check if this is a valid folder as well.
148                             // If this is a folder that needs to be ignored, we must return false,
149                             // so that we ignore its content.
150                             return PostCompilerHelper.checkFolderForPackaging((IFolder)resource);
151                         }
152                     }
153                 }
154             }
155 
156             return true;
157         }
158 
159         /**
160          * Returns if one of the .class file was modified.
161          */
needDexConvertion()162         boolean needDexConvertion() {
163             return mConvertToDex;
164         }
165 
needMakeFinalPackage()166         boolean needMakeFinalPackage() {
167             return mMakeFinalPackage;
168         }
169     }
170 
PostCompilerBuilder()171     public PostCompilerBuilder() {
172         super();
173     }
174 
175     @Override
clean(IProgressMonitor monitor)176     protected void clean(IProgressMonitor monitor) throws CoreException {
177         super.clean(monitor);
178 
179         // Get the project.
180         IProject project = getProject();
181 
182         // Clear the project of the generic markers
183         removeMarkersFromProject(project, AndroidConstants.MARKER_AAPT_COMPILE);
184         removeMarkersFromProject(project, AndroidConstants.MARKER_PACKAGING);
185     }
186 
187     // build() returns a list of project from which this project depends for future compilation.
188     @SuppressWarnings({"unchecked"})
189     @Override
build(int kind, Map args, IProgressMonitor monitor)190     protected IProject[] build(int kind, Map args, IProgressMonitor monitor)
191             throws CoreException {
192         // get a project object
193         IProject project = getProject();
194 
195         // list of referenced projects.
196         IProject[] libProjects = null;
197         IProject[] javaProjects = null;
198         IProject[] allRefProjects = null;
199 
200         try {
201             // get the project info
202             ProjectState projectState = Sdk.getProjectState(project);
203             if (projectState == null || projectState.isLibrary()) {
204                 // library project do not need to be dexified or packaged.
205                 return null;
206             }
207 
208             // get the libraries
209             libProjects = projectState.getFullLibraryProjects();
210 
211             IJavaProject javaProject = JavaCore.create(project);
212 
213             // Top level check to make sure the build can move forward.
214             abortOnBadSetup(javaProject);
215 
216             // get the list of referenced projects.
217             javaProjects = ProjectHelper.getReferencedProjects(project);
218             IJavaProject[] referencedJavaProjects = PostCompilerHelper.getJavaProjects(javaProjects);
219 
220             // mix the java project and the library projects
221             final int libCount = libProjects.length;
222             final int javaCount = javaProjects != null ? javaProjects.length : 0;
223             allRefProjects = new IProject[libCount + javaCount];
224             if (libCount > 0) {
225                 System.arraycopy(libProjects, 0, allRefProjects, 0, libCount);
226             }
227             if (javaCount > 0) {
228                 System.arraycopy(javaProjects, 0, allRefProjects, libCount, javaCount);
229             }
230 
231             // get the output folder, this method returns the path with a trailing
232             // separator
233             IFolder outputFolder = BaseProjectHelper.getOutputFolder(project);
234 
235             // now we need to get the classpath list
236             ArrayList<IPath> sourceList = BaseProjectHelper.getSourceClasspaths(javaProject);
237 
238             // First thing we do is go through the resource delta to not
239             // lose it if we have to abort the build for any reason.
240             PostCompilerDeltaVisitor dv = null;
241             if (kind == FULL_BUILD) {
242                 AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project,
243                         Messages.Start_Full_Apk_Build);
244 
245                 mPackageResources = true;
246                 mConvertToDex = true;
247                 mBuildFinalPackage = true;
248             } else {
249                 AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project,
250                         Messages.Start_Inc_Apk_Build);
251 
252                 // go through the resources and see if something changed.
253                 IResourceDelta delta = getDelta(project);
254                 if (delta == null) {
255                     mPackageResources = true;
256                     mConvertToDex = true;
257                     mBuildFinalPackage = true;
258                 } else {
259                     dv = new PostCompilerDeltaVisitor(this, sourceList, outputFolder);
260                     delta.accept(dv);
261 
262                     // save the state
263                     mPackageResources |= dv.getPackageResources();
264                     mConvertToDex |= dv.getConvertToDex();
265                     mBuildFinalPackage |= dv.getMakeFinalPackage();
266                 }
267 
268                 // if the main resources didn't change, then we check for the library
269                 // ones (will trigger resource repackaging too)
270                 if ((mPackageResources == false || mBuildFinalPackage == false) &&
271                         libProjects.length > 0) {
272                     for (IProject libProject : libProjects) {
273                         delta = getDelta(libProject);
274                         if (delta != null) {
275                             LibraryDeltaVisitor visitor = new LibraryDeltaVisitor();
276                             delta.accept(visitor);
277 
278                             mPackageResources |= visitor.getResChange();
279                             mBuildFinalPackage |= visitor.getLibChange();
280 
281                             if (mPackageResources && mBuildFinalPackage) {
282                                 break;
283                             }
284                         }
285                     }
286                 }
287 
288                 // also go through the delta for all the referenced projects, until we are forced to
289                 // compile anyway
290                 for (int i = 0 ; i < referencedJavaProjects.length &&
291                         (mBuildFinalPackage == false || mConvertToDex == false); i++) {
292                     IJavaProject referencedJavaProject = referencedJavaProjects[i];
293                     delta = getDelta(referencedJavaProject.getProject());
294                     if (delta != null) {
295                         ReferencedProjectDeltaVisitor refProjectDv = new ReferencedProjectDeltaVisitor(
296                                 referencedJavaProject);
297                         delta.accept(refProjectDv);
298 
299                         // save the state
300                         mConvertToDex |= refProjectDv.needDexConvertion();
301                         mBuildFinalPackage |= refProjectDv.needMakeFinalPackage();
302                     }
303                 }
304             }
305 
306             // store the build status in the persistent storage
307             saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX , mConvertToDex);
308             saveProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, mPackageResources);
309             saveProjectBooleanProperty(PROPERTY_BUILD_APK, mBuildFinalPackage);
310 
311             if (dv != null && dv.mXmlError) {
312                 AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project,
313                 Messages.Xml_Error);
314 
315                 // if there was some XML errors, we just return w/o doing
316                 // anything since we've put some markers in the files anyway
317                 return allRefProjects;
318             }
319 
320             // remove older packaging markers.
321             removeMarkersFromProject(javaProject.getProject(), AndroidConstants.MARKER_PACKAGING);
322 
323             if (outputFolder == null) {
324                 // mark project and exit
325                 markProject(AndroidConstants.MARKER_PACKAGING, Messages.Failed_To_Get_Output,
326                         IMarker.SEVERITY_ERROR);
327                 return allRefProjects;
328             }
329 
330             // first thing we do is check that the SDK directory has been setup.
331             String osSdkFolder = AdtPlugin.getOsSdkFolder();
332 
333             if (osSdkFolder.length() == 0) {
334                 // this has already been checked in the precompiler. Therefore,
335                 // while we do have to cancel the build, we don't have to return
336                 // any error or throw anything.
337                 return allRefProjects;
338             }
339 
340             // do some extra check, in case the output files are not present. This
341             // will force to recreate them.
342             IResource tmp = null;
343 
344             if (mPackageResources == false) {
345                 // check the full resource package
346                 tmp = outputFolder.findMember(AndroidConstants.FN_RESOURCES_AP_);
347                 if (tmp == null || tmp.exists() == false) {
348                     mPackageResources = true;
349                     mBuildFinalPackage = true;
350                 }
351             }
352 
353             // check classes.dex is present. If not we force to recreate it.
354             if (mConvertToDex == false) {
355                 tmp = outputFolder.findMember(SdkConstants.FN_APK_CLASSES_DEX);
356                 if (tmp == null || tmp.exists() == false) {
357                     mConvertToDex = true;
358                     mBuildFinalPackage = true;
359                 }
360             }
361 
362             // also check the final file(s)!
363             String finalPackageName = ProjectHelper.getApkFilename(project, null /*config*/);
364             if (mBuildFinalPackage == false) {
365                 tmp = outputFolder.findMember(finalPackageName);
366                 if (tmp == null || (tmp instanceof IFile &&
367                         tmp.exists() == false)) {
368                     String msg = String.format(Messages.s_Missing_Repackaging, finalPackageName);
369                     AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, msg);
370                     mBuildFinalPackage = true;
371                 }
372             }
373 
374             // at this point we know if we need to recreate the temporary apk
375             // or the dex file, but we don't know if we simply need to recreate them
376             // because they are missing
377 
378             // refresh the output directory first
379             IContainer ic = outputFolder.getParent();
380             if (ic != null) {
381                 ic.refreshLocal(IResource.DEPTH_ONE, monitor);
382             }
383 
384             // Get the DX output stream. Since the builder is created for the life of the
385             // project, they can be kept around.
386             if (mDxOutStream == null) {
387                 mDxOutStream = AdtPlugin.getOutPrintStream(project, CONSOLE_PREFIX_DX);
388                 mDxErrStream = AdtPlugin.getErrPrintStream(project, CONSOLE_PREFIX_DX);
389             }
390 
391             // we need to test all three, as we may need to make the final package
392             // but not the intermediary ones.
393             if (mPackageResources || mConvertToDex || mBuildFinalPackage) {
394                 PostCompilerHelper helper = new PostCompilerHelper(project, mDxOutStream, mDxErrStream);
395 
396                 // resource to the AndroidManifest.xml file
397                 IFile manifestFile = project.getFile(SdkConstants.FN_ANDROID_MANIFEST_XML);
398 
399                 if (manifestFile == null || manifestFile.exists() == false) {
400                     // mark project and exit
401                     String msg = String.format(Messages.s_File_Missing,
402                             SdkConstants.FN_ANDROID_MANIFEST_XML);
403                     markProject(AndroidConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR);
404                     return allRefProjects;
405                 }
406 
407                 IPath binLocation = outputFolder.getLocation();
408                 if (binLocation == null) {
409                     markProject(AndroidConstants.MARKER_PACKAGING, Messages.Output_Missing,
410                             IMarker.SEVERITY_ERROR);
411                     return allRefProjects;
412                 }
413                 String osBinPath = binLocation.toOSString();
414 
415                 // Remove the old .apk.
416                 // This make sure that if the apk is corrupted, then dx (which would attempt
417                 // to open it), will not fail.
418                 String osFinalPackagePath = osBinPath + File.separator + finalPackageName;
419                 File finalPackage = new File(osFinalPackagePath);
420 
421                 // if delete failed, this is not really a problem, as the final package generation
422                 // handle already present .apk, and if that one failed as well, the user will be
423                 // notified.
424                 finalPackage.delete();
425 
426                 // first we check if we need to package the resources.
427                 if (mPackageResources) {
428                     // remove some aapt_package only markers.
429                     removeMarkersFromContainer(project, AndroidConstants.MARKER_AAPT_PACKAGE);
430 
431                     // need to figure out some path before we can execute aapt;
432                     if (helper.packageResources( manifestFile, libProjects, null /*resfilter*/,
433                             0 /*versionCode */, osBinPath,
434                             AndroidConstants.FN_RESOURCES_AP_) == false) {
435                         // aapt failed. Whatever files that needed to be marked
436                         // have already been marked. We just return.
437                         return allRefProjects;
438                     }
439 
440                     // build has been done. reset the state of the builder
441                     mPackageResources = false;
442 
443                     // and store it
444                     saveProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, mPackageResources);
445                 }
446 
447                 // then we check if we need to package the .class into classes.dex
448                 if (mConvertToDex) {
449                     if (helper.executeDx(javaProject, osBinPath, osBinPath + File.separator +
450                             SdkConstants.FN_APK_CLASSES_DEX, referencedJavaProjects) == false) {
451                         // dx failed, we return
452                         return allRefProjects;
453                     }
454 
455                     // build has been done. reset the state of the builder
456                     mConvertToDex = false;
457 
458                     // and store it
459                     saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, mConvertToDex);
460                 }
461 
462                 // figure out whether the application is debuggable.
463                 // It is considered debuggable if the attribute debuggable is set to true
464                 // in the manifest
465                 boolean debuggable = false;
466                 XPath xpath = AndroidXPathFactory.newXPath();
467                 String result = xpath.evaluate(
468                         "/"  + AndroidManifest.NODE_MANIFEST +                //$NON-NLS-1$
469                         "/"  + AndroidManifest.NODE_APPLICATION +             //$NON-NLS-1$
470                         "/@" + AndroidXPathFactory.DEFAULT_NS_PREFIX +        //$NON-NLS-1$
471                                 ":" + AndroidManifest.ATTRIBUTE_DEBUGGABLE,   //$NON-NLS-1$
472                         new InputSource(manifestFile.getContents()));
473                 if (result.length() > 0) {
474                     debuggable = Boolean.valueOf(result);
475                 }
476 
477                 // now we need to make the final package from the intermediary apk
478                 // and classes.dex.
479                 // This is the default package with all the resources.
480 
481                 String classesDexPath = osBinPath + File.separator +
482                         SdkConstants.FN_APK_CLASSES_DEX;
483                 if (helper.finalPackage(
484                         osBinPath + File.separator + AndroidConstants.FN_RESOURCES_AP_,
485                         classesDexPath, osFinalPackagePath, true /*debugSign*/,
486                         javaProject, libProjects,
487                         referencedJavaProjects, null /*abiFilter*/, debuggable) == false) {
488                     return allRefProjects;
489                 }
490 
491                 // we are done.
492 
493                 // get the resource to bin
494                 outputFolder.refreshLocal(IResource.DEPTH_ONE, monitor);
495 
496                 // build has been done. reset the state of the builder
497                 mBuildFinalPackage = false;
498 
499                 // and store it
500                 saveProjectBooleanProperty(PROPERTY_BUILD_APK, mBuildFinalPackage);
501 
502                 // reset the installation manager to force new installs of this project
503                 ApkInstallManager.getInstance().resetInstallationFor(project);
504 
505                 AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, getProject(),
506                         "Build Success!");
507             }
508         } catch (Exception exception) {
509             // try to catch other exception to actually display an error. This will be useful
510             // if we get an NPE or something so that we can at least notify the user that something
511             // went wrong.
512 
513             // first check if this is a CoreException we threw to cancel the build.
514             if (exception instanceof CoreException) {
515                 if (((CoreException)exception).getStatus().getSeverity() == IStatus.CANCEL) {
516                     // Project is already marked with an error. Nothing to do
517                     return allRefProjects;
518                 }
519             }
520 
521             String msg = exception.getMessage();
522             if (msg == null) {
523                 msg = exception.getClass().getCanonicalName();
524             }
525 
526             msg = String.format("Unknown error: %1$s", msg);
527             AdtPlugin.logAndPrintError(exception, project.getName(), msg);
528             markProject(AndroidConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR);
529         }
530 
531         return allRefProjects;
532     }
533 
534     @Override
startupOnInitialize()535     protected void startupOnInitialize() {
536         super.startupOnInitialize();
537 
538         // load the build status. We pass true as the default value to
539         // force a recompile in case the property was not found
540         mConvertToDex = loadProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX , true);
541         mPackageResources = loadProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, true);
542         mBuildFinalPackage = loadProjectBooleanProperty(PROPERTY_BUILD_APK, true);
543     }
544 
545     @Override
abortOnBadSetup(IJavaProject javaProject)546     protected void abortOnBadSetup(IJavaProject javaProject) throws CoreException {
547         super.abortOnBadSetup(javaProject);
548 
549         // for this version, we stop on any marker (ie also markers coming from JDT).
550         // The depth is set to ZERO to make sure we don't stop on warning on resources.
551         // Only markers set directly on the project are considered.
552         IMarker[] markers = javaProject.getProject().findMarkers(null /*type*/,
553                 false /*includeSubtypes*/, IResource.DEPTH_ZERO);
554 
555         if (markers.length > 0) {
556             stopBuild("");
557         }
558     }
559 }
560