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