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.AdtConstants; 20 import com.android.ide.eclipse.adt.AdtPlugin; 21 import com.android.ide.eclipse.adt.AndroidConstants; 22 import com.android.ide.eclipse.adt.internal.project.AndroidManifestParser; 23 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; 24 import com.android.ide.eclipse.adt.internal.project.FixLaunchConfig; 25 import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler.BasicXmlErrorListener; 26 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 27 import com.android.sdklib.AndroidVersion; 28 import com.android.sdklib.IAndroidTarget; 29 import com.android.sdklib.SdkConstants; 30 import com.android.sdklib.xml.AndroidManifest; 31 32 import org.eclipse.core.resources.IContainer; 33 import org.eclipse.core.resources.IFile; 34 import org.eclipse.core.resources.IFolder; 35 import org.eclipse.core.resources.IMarker; 36 import org.eclipse.core.resources.IProject; 37 import org.eclipse.core.resources.IResource; 38 import org.eclipse.core.resources.IResourceDelta; 39 import org.eclipse.core.resources.IWorkspaceRoot; 40 import org.eclipse.core.resources.ResourcesPlugin; 41 import org.eclipse.core.runtime.CoreException; 42 import org.eclipse.core.runtime.IPath; 43 import org.eclipse.core.runtime.IProgressMonitor; 44 import org.eclipse.core.runtime.NullProgressMonitor; 45 import org.eclipse.core.runtime.Path; 46 import org.eclipse.core.runtime.SubProgressMonitor; 47 import org.eclipse.jdt.core.IJavaProject; 48 import org.eclipse.jdt.core.JavaCore; 49 50 import java.io.IOException; 51 import java.util.ArrayList; 52 import java.util.Map; 53 import java.util.regex.Matcher; 54 import java.util.regex.Pattern; 55 56 /** 57 * Pre Java Compiler. 58 * This incremental builder performs 2 tasks: 59 * <ul> 60 * <li>compiles the resources located in the res/ folder, along with the 61 * AndroidManifest.xml file into the R.java class.</li> 62 * <li>compiles any .aidl files into a corresponding java file.</li> 63 * </ul> 64 * 65 */ 66 public class PreCompilerBuilder extends BaseBuilder { 67 68 public static final String ID = "com.android.ide.eclipse.adt.PreCompilerBuilder"; //$NON-NLS-1$ 69 70 private static final String PROPERTY_PACKAGE = "manifestPackage"; //$NON-NLS-1$ 71 72 private static final String PROPERTY_COMPILE_RESOURCES = "compileResources"; //$NON-NLS-1$ 73 private static final String PROPERTY_COMPILE_AIDL = "compileAidl"; //$NON-NLS-1$ 74 75 /** 76 * Single line aidl error<br> 77 * "<path>:<line>: <error>" 78 * or 79 * "<path>:<line> <error>" 80 */ 81 private static Pattern sAidlPattern1 = Pattern.compile("^(.+?):(\\d+):?\\s(.+)$"); //$NON-NLS-1$ 82 83 /** 84 * Data to temporarly store aidl source file information 85 */ 86 static class AidlData { 87 IFile aidlFile; 88 IFolder sourceFolder; 89 AidlData(IFolder sourceFolder, IFile aidlFile)90 AidlData(IFolder sourceFolder, IFile aidlFile) { 91 this.sourceFolder = sourceFolder; 92 this.aidlFile = aidlFile; 93 } 94 95 @Override equals(Object obj)96 public boolean equals(Object obj) { 97 if (this == obj) { 98 return true; 99 } 100 101 if (obj instanceof AidlData) { 102 AidlData file = (AidlData)obj; 103 return aidlFile.equals(file.aidlFile) && sourceFolder.equals(file.sourceFolder); 104 } 105 106 return false; 107 } 108 } 109 110 /** 111 * Resource Compile flag. This flag is reset to false after each successful compilation, and 112 * stored in the project persistent properties. This allows the builder to remember its state 113 * when the project is closed/opened. 114 */ 115 private boolean mMustCompileResources = false; 116 117 /** List of .aidl files found that are modified or new. */ 118 private final ArrayList<AidlData> mAidlToCompile = new ArrayList<AidlData>(); 119 120 /** List of .aidl files that have been removed. */ 121 private final ArrayList<AidlData> mAidlToRemove = new ArrayList<AidlData>(); 122 123 /** cache of the java package defined in the manifest */ 124 private String mManifestPackage; 125 126 /** Output folder for generated Java File. Created on the Builder init 127 * @see #startupOnInitialize() 128 */ 129 private IFolder mGenFolder; 130 131 /** 132 * Progress monitor used at the end of every build to refresh the content of the 'gen' folder 133 * and set the generated files as derived. 134 */ 135 private DerivedProgressMonitor mDerivedProgressMonitor; 136 137 /** 138 * Progress monitor waiting the end of the process to set a persistent value 139 * in a file. This is typically used in conjunction with <code>IResource.refresh()</code>, 140 * since this call is asysnchronous, and we need to wait for it to finish for the file 141 * to be known by eclipse, before we can call <code>resource.setPersistentProperty</code> on 142 * a new file. 143 */ 144 private static class DerivedProgressMonitor implements IProgressMonitor { 145 private boolean mCancelled = false; 146 private final ArrayList<IFile> mFileList = new ArrayList<IFile>(); 147 private boolean mDone = false; DerivedProgressMonitor()148 public DerivedProgressMonitor() { 149 } 150 addFile(IFile file)151 void addFile(IFile file) { 152 mFileList.add(file); 153 } 154 reset()155 void reset() { 156 mFileList.clear(); 157 mDone = false; 158 } 159 beginTask(String name, int totalWork)160 public void beginTask(String name, int totalWork) { 161 } 162 done()163 public void done() { 164 if (mDone == false) { 165 mDone = true; 166 for (IFile file : mFileList) { 167 if (file.exists()) { 168 try { 169 file.setDerived(true); 170 } catch (CoreException e) { 171 // This really shouldn't happen since we check that the resource exist. 172 // Worst case scenario, the resource isn't marked as derived. 173 } 174 } 175 } 176 } 177 } 178 internalWorked(double work)179 public void internalWorked(double work) { 180 } 181 isCanceled()182 public boolean isCanceled() { 183 return mCancelled; 184 } 185 setCanceled(boolean value)186 public void setCanceled(boolean value) { 187 mCancelled = value; 188 } 189 setTaskName(String name)190 public void setTaskName(String name) { 191 } 192 subTask(String name)193 public void subTask(String name) { 194 } 195 worked(int work)196 public void worked(int work) { 197 } 198 } 199 PreCompilerBuilder()200 public PreCompilerBuilder() { 201 super(); 202 } 203 204 // build() returns a list of project from which this project depends for future compilation. 205 @SuppressWarnings("unchecked") 206 @Override build(int kind, Map args, IProgressMonitor monitor)207 protected IProject[] build(int kind, Map args, IProgressMonitor monitor) 208 throws CoreException { 209 try { 210 mDerivedProgressMonitor.reset(); 211 212 // First thing we do is go through the resource delta to not 213 // lose it if we have to abort the build for any reason. 214 215 // get the project objects 216 IProject project = getProject(); 217 218 // Top level check to make sure the build can move forward. 219 abortOnBadSetup(project); 220 221 IJavaProject javaProject = JavaCore.create(project); 222 IAndroidTarget projectTarget = Sdk.getCurrent().getTarget(project); 223 224 // now we need to get the classpath list 225 ArrayList<IPath> sourceFolderPathList = BaseProjectHelper.getSourceClasspaths( 226 javaProject); 227 228 PreCompilerDeltaVisitor dv = null; 229 String javaPackage = null; 230 String minSdkVersion = null; 231 232 if (kind == FULL_BUILD) { 233 AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, 234 Messages.Start_Full_Pre_Compiler); 235 mMustCompileResources = true; 236 buildAidlCompilationList(project, sourceFolderPathList); 237 } else { 238 AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, 239 Messages.Start_Inc_Pre_Compiler); 240 241 // Go through the resources and see if something changed. 242 // Even if the mCompileResources flag is true from a previously aborted 243 // build, we need to go through the Resource delta to get a possible 244 // list of aidl files to compile/remove. 245 IResourceDelta delta = getDelta(project); 246 if (delta == null) { 247 mMustCompileResources = true; 248 buildAidlCompilationList(project, sourceFolderPathList); 249 } else { 250 dv = new PreCompilerDeltaVisitor(this, sourceFolderPathList); 251 delta.accept(dv); 252 253 // record the state 254 mMustCompileResources |= dv.getCompileResources(); 255 256 if (dv.getForceAidlCompile()) { 257 buildAidlCompilationList(project, sourceFolderPathList); 258 } else { 259 // handle aidl modification, and update mMustCompileAidl 260 mergeAidlFileModifications(dv.getAidlToCompile(), 261 dv.getAidlToRemove()); 262 } 263 264 // get the java package from the visitor 265 javaPackage = dv.getManifestPackage(); 266 minSdkVersion = dv.getMinSdkVersion(); 267 } 268 } 269 270 // store the build status in the persistent storage 271 saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES , mMustCompileResources); 272 273 // if there was some XML errors, we just return w/o doing 274 // anything since we've put some markers in the files anyway. 275 if (dv != null && dv.mXmlError) { 276 AdtPlugin.printErrorToConsole(project, Messages.Xml_Error); 277 278 // This interrupts the build. The next builders will not run. 279 stopBuild(Messages.Xml_Error); 280 } 281 282 283 // get the manifest file 284 IFile manifest = AndroidManifestParser.getManifest(project); 285 286 if (manifest == null) { 287 String msg = String.format(Messages.s_File_Missing, 288 AndroidConstants.FN_ANDROID_MANIFEST); 289 AdtPlugin.printErrorToConsole(project, msg); 290 markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); 291 292 // This interrupts the build. The next builders will not run. 293 stopBuild(msg); 294 295 // TODO: document whether code below that uses manifest (which is now guaranteed 296 // to be null) will actually be executed or not. 297 } 298 299 // lets check the XML of the manifest first, if that hasn't been done by the 300 // resource delta visitor yet. 301 if (dv == null || dv.getCheckedManifestXml() == false) { 302 BasicXmlErrorListener errorListener = new BasicXmlErrorListener(); 303 AndroidManifestParser parser = BaseProjectHelper.parseManifestForError(manifest, 304 errorListener); 305 306 if (errorListener.mHasXmlError == true) { 307 // there was an error in the manifest, its file has been marked, 308 // by the XmlErrorHandler. 309 // We return; 310 String msg = String.format(Messages.s_Contains_Xml_Error, 311 AndroidConstants.FN_ANDROID_MANIFEST); 312 AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg); 313 314 // This interrupts the build. The next builders will not run. 315 stopBuild(msg); 316 } 317 318 // get the java package from the parser 319 javaPackage = parser.getPackage(); 320 minSdkVersion = parser.getApiLevelRequirement(); 321 } 322 323 if (minSdkVersion != null) { 324 int minSdkValue = -1; 325 try { 326 minSdkValue = Integer.parseInt(minSdkVersion); 327 } catch (NumberFormatException e) { 328 // it's ok, it means minSdkVersion contains a (hopefully) valid codename. 329 } 330 331 AndroidVersion projectVersion = projectTarget.getVersion(); 332 333 if (minSdkValue != -1) { 334 String codename = projectVersion.getCodename(); 335 if (codename != null) { 336 // integer minSdk when the target is a preview => fatal error 337 String msg = String.format( 338 "Platform %1$s is a preview and requires appication manifest to set %2$s to '%1$s'", 339 codename, AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION); 340 AdtPlugin.printErrorToConsole(project, msg); 341 BaseProjectHelper.addMarker(manifest, AdtConstants.MARKER_ADT, msg, 342 IMarker.SEVERITY_ERROR); 343 stopBuild(msg); 344 } else if (minSdkValue < projectVersion.getApiLevel()) { 345 // integer minSdk is not high enough for the target => warning 346 String msg = String.format( 347 "Attribute %1$s (%2$d) is lower than the project target API level (%3$d)", 348 AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION, 349 minSdkValue, projectVersion.getApiLevel()); 350 AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg); 351 BaseProjectHelper.addMarker(manifest, AdtConstants.MARKER_ADT, msg, 352 IMarker.SEVERITY_WARNING); 353 } else if (minSdkValue > projectVersion.getApiLevel()) { 354 // integer minSdk is too high for the target => warning 355 String msg = String.format( 356 "Attribute %1$s (%2$d) is higher than the project target API level (%3$d)", 357 AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION, 358 minSdkValue, projectVersion.getApiLevel()); 359 AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg); 360 BaseProjectHelper.addMarker(manifest, AdtConstants.MARKER_ADT, msg, 361 IMarker.SEVERITY_WARNING); 362 } 363 } else { 364 // looks like the min sdk is a codename, check it matches the codename 365 // of the platform 366 String codename = projectVersion.getCodename(); 367 if (codename == null) { 368 // platform is not a preview => fatal error 369 String msg = String.format( 370 "Manifest attribute '%1$s' is set to '%2$s'. Integer is expected.", 371 AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION, minSdkVersion); 372 AdtPlugin.printErrorToConsole(project, msg); 373 BaseProjectHelper.addMarker(manifest, AdtConstants.MARKER_ADT, msg, 374 IMarker.SEVERITY_ERROR); 375 stopBuild(msg); 376 } else if (codename.equals(minSdkVersion) == false) { 377 // platform and manifest codenames don't match => fatal error. 378 String msg = String.format( 379 "Value of manifest attribute '%1$s' does not match platform codename '%2$s'", 380 AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION, codename); 381 AdtPlugin.printErrorToConsole(project, msg); 382 BaseProjectHelper.addMarker(manifest, AdtConstants.MARKER_ADT, msg, 383 IMarker.SEVERITY_ERROR); 384 stopBuild(msg); 385 } 386 } 387 } else if (projectTarget.getVersion().isPreview()) { 388 // else the minSdkVersion is not set but we are using a preview target. 389 // Display an error 390 String codename = projectTarget.getVersion().getCodename(); 391 String msg = String.format( 392 "Platform %1$s is a preview and requires appication manifests to set %2$s to '%1$s'", 393 codename, AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION); 394 AdtPlugin.printErrorToConsole(project, msg); 395 BaseProjectHelper.addMarker(manifest, AdtConstants.MARKER_ADT, msg, 396 IMarker.SEVERITY_ERROR); 397 stopBuild(msg); 398 } 399 400 if (javaPackage == null || javaPackage.length() == 0) { 401 // looks like the AndroidManifest file isn't valid. 402 String msg = String.format(Messages.s_Doesnt_Declare_Package_Error, 403 AndroidConstants.FN_ANDROID_MANIFEST); 404 AdtPlugin.printErrorToConsole(project, msg); 405 markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); 406 407 // This interrupts the build. The next builders will not run. 408 stopBuild(msg); 409 410 // TODO: document whether code below that uses javaPackage (which is now guaranteed 411 // to be null) will actually be executed or not. 412 } 413 414 // at this point we have the java package. We need to make sure it's not a different 415 // package than the previous one that were built. 416 if (javaPackage != null && javaPackage.equals(mManifestPackage) == false) { 417 // The manifest package has changed, the user may want to update 418 // the launch configuration 419 if (mManifestPackage != null) { 420 AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, 421 Messages.Checking_Package_Change); 422 423 FixLaunchConfig flc = new FixLaunchConfig(project, mManifestPackage, 424 javaPackage); 425 flc.start(); 426 } 427 428 // now we delete the generated classes from their previous location 429 deleteObsoleteGeneratedClass(AndroidConstants.FN_RESOURCE_CLASS, 430 mManifestPackage); 431 deleteObsoleteGeneratedClass(AndroidConstants.FN_MANIFEST_CLASS, 432 mManifestPackage); 433 434 // record the new manifest package, and save it. 435 mManifestPackage = javaPackage; 436 saveProjectStringProperty(PROPERTY_PACKAGE, mManifestPackage); 437 } 438 439 if (mMustCompileResources) { 440 // we need to figure out where to store the R class. 441 // get the parent folder for R.java and update mManifestPackageSourceFolder 442 IFolder packageFolder = getGenManifestPackageFolder(project); 443 444 // get the resource folder 445 IFolder resFolder = project.getFolder(AndroidConstants.WS_RESOURCES); 446 447 // get the file system path 448 IPath outputLocation = mGenFolder.getLocation(); 449 IPath resLocation = resFolder.getLocation(); 450 IPath manifestLocation = manifest == null ? null : manifest.getLocation(); 451 452 // those locations have to exist for us to do something! 453 if (outputLocation != null && resLocation != null 454 && manifestLocation != null) { 455 String osOutputPath = outputLocation.toOSString(); 456 String osResPath = resLocation.toOSString(); 457 String osManifestPath = manifestLocation.toOSString(); 458 459 // remove the aapt markers 460 removeMarkersFromFile(manifest, AndroidConstants.MARKER_AAPT_COMPILE); 461 removeMarkersFromContainer(resFolder, AndroidConstants.MARKER_AAPT_COMPILE); 462 463 AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, 464 Messages.Preparing_Generated_Files); 465 466 // since the R.java file may be already existing in read-only 467 // mode we need to make it readable so that aapt can overwrite 468 // it 469 IFile rJavaFile = packageFolder.getFile(AndroidConstants.FN_RESOURCE_CLASS); 470 471 // do the same for the Manifest.java class 472 IFile manifestJavaFile = packageFolder.getFile( 473 AndroidConstants.FN_MANIFEST_CLASS); 474 475 // we actually need to delete the manifest.java as it may become empty and 476 // in this case aapt doesn't generate an empty one, but instead doesn't 477 // touch it. 478 manifestJavaFile.delete(true, null); 479 480 // launch aapt: create the command line 481 ArrayList<String> array = new ArrayList<String>(); 482 array.add(projectTarget.getPath(IAndroidTarget.AAPT)); 483 array.add("package"); //$NON-NLS-1$ 484 array.add("-m"); //$NON-NLS-1$ 485 if (AdtPlugin.getBuildVerbosity() == AdtConstants.BUILD_VERBOSE) { 486 array.add("-v"); //$NON-NLS-1$ 487 } 488 array.add("-J"); //$NON-NLS-1$ 489 array.add(osOutputPath); 490 array.add("-M"); //$NON-NLS-1$ 491 array.add(osManifestPath); 492 array.add("-S"); //$NON-NLS-1$ 493 array.add(osResPath); 494 array.add("-I"); //$NON-NLS-1$ 495 array.add(projectTarget.getPath(IAndroidTarget.ANDROID_JAR)); 496 497 if (AdtPlugin.getBuildVerbosity() == AdtConstants.BUILD_VERBOSE) { 498 StringBuilder sb = new StringBuilder(); 499 for (String c : array) { 500 sb.append(c); 501 sb.append(' '); 502 } 503 String cmd_line = sb.toString(); 504 AdtPlugin.printToConsole(project, cmd_line); 505 } 506 507 // launch 508 int execError = 1; 509 try { 510 // launch the command line process 511 Process process = Runtime.getRuntime().exec( 512 array.toArray(new String[array.size()])); 513 514 // list to store each line of stderr 515 ArrayList<String> results = new ArrayList<String>(); 516 517 // get the output and return code from the process 518 execError = grabProcessOutput(process, results); 519 520 // attempt to parse the error output 521 boolean parsingError = parseAaptOutput(results, project); 522 523 // if we couldn't parse the output we display it in the console. 524 if (parsingError) { 525 if (execError != 0) { 526 AdtPlugin.printErrorToConsole(project, results.toArray()); 527 } else { 528 AdtPlugin.printBuildToConsole(AdtConstants.BUILD_NORMAL, 529 project, results.toArray()); 530 } 531 } 532 533 if (execError != 0) { 534 // if the exec failed, and we couldn't parse the error output 535 // (and therefore not all files that should have been marked, 536 // were marked), we put a generic marker on the project and abort. 537 if (parsingError) { 538 markProject(AdtConstants.MARKER_ADT, Messages.Unparsed_AAPT_Errors, 539 IMarker.SEVERITY_ERROR); 540 } 541 542 AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, 543 Messages.AAPT_Error); 544 545 // abort if exec failed. 546 // This interrupts the build. The next builders will not run. 547 stopBuild(Messages.AAPT_Error); 548 } 549 } catch (IOException e1) { 550 // something happen while executing the process, 551 // mark the project and exit 552 String msg = String.format(Messages.AAPT_Exec_Error, array.get(0)); 553 markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); 554 555 // This interrupts the build. The next builders will not run. 556 stopBuild(msg); 557 } catch (InterruptedException e) { 558 // we got interrupted waiting for the process to end... 559 // mark the project and exit 560 String msg = String.format(Messages.AAPT_Exec_Error, array.get(0)); 561 markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); 562 563 // This interrupts the build. The next builders will not run. 564 stopBuild(msg); 565 } 566 567 // if the return code was OK, we refresh the folder that 568 // contains R.java to force a java recompile. 569 if (execError == 0) { 570 // now add the R.java/Manifest.java to the list of file to be marked 571 // as derived. 572 mDerivedProgressMonitor.addFile(rJavaFile); 573 mDerivedProgressMonitor.addFile(manifestJavaFile); 574 575 // build has been done. reset the state of the builder 576 mMustCompileResources = false; 577 578 // and store it 579 saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, 580 mMustCompileResources); 581 } 582 } 583 } else { 584 // nothing to do 585 } 586 587 // now handle the aidl stuff. 588 boolean aidlStatus = handleAidl(projectTarget, sourceFolderPathList, monitor); 589 590 if (aidlStatus == false && mMustCompileResources == false) { 591 AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, 592 Messages.Nothing_To_Compile); 593 } 594 } finally { 595 // refresh the 'gen' source folder. Once this is done with the custom progress 596 // monitor to mark all new files as derived 597 mGenFolder.refreshLocal(IResource.DEPTH_INFINITE, mDerivedProgressMonitor); 598 } 599 600 return null; 601 } 602 603 @Override clean(IProgressMonitor monitor)604 protected void clean(IProgressMonitor monitor) throws CoreException { 605 super.clean(monitor); 606 607 AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(), 608 Messages.Removing_Generated_Classes); 609 610 // remove all the derived resources from the 'gen' source folder. 611 removeDerivedResources(mGenFolder, monitor); 612 } 613 614 @Override startupOnInitialize()615 protected void startupOnInitialize() { 616 super.startupOnInitialize(); 617 618 mDerivedProgressMonitor = new DerivedProgressMonitor(); 619 620 IProject project = getProject(); 621 622 // load the previous IFolder and java package. 623 mManifestPackage = loadProjectStringProperty(PROPERTY_PACKAGE); 624 625 // get the source folder in which all the Java files are created 626 mGenFolder = project.getFolder(SdkConstants.FD_GEN_SOURCES); 627 628 // Load the current compile flags. We ask for true if not found to force a 629 // recompile. 630 mMustCompileResources = loadProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, true); 631 boolean mustCompileAidl = loadProjectBooleanProperty(PROPERTY_COMPILE_AIDL, true); 632 633 // if we stored that we have to compile some aidl, we build the list that will compile them 634 // all 635 if (mustCompileAidl) { 636 IJavaProject javaProject = JavaCore.create(project); 637 ArrayList<IPath> sourceFolderPathList = BaseProjectHelper.getSourceClasspaths( 638 javaProject); 639 640 buildAidlCompilationList(project, sourceFolderPathList); 641 } 642 } 643 644 /** 645 * Delete the a generated java class associated with the specified java package. 646 * @param filename Name of the generated file to remove. 647 * @param javaPackage the old java package 648 */ deleteObsoleteGeneratedClass(String filename, String javaPackage)649 private void deleteObsoleteGeneratedClass(String filename, String javaPackage) { 650 if (javaPackage == null) { 651 return; 652 } 653 654 IPath packagePath = getJavaPackagePath(javaPackage); 655 IPath iPath = packagePath.append(filename); 656 657 // Find a matching resource object. 658 IResource javaFile = mGenFolder.findMember(iPath); 659 if (javaFile != null && javaFile.exists() && javaFile.getType() == IResource.FILE) { 660 try { 661 // delete 662 javaFile.delete(true, null); 663 664 // refresh parent 665 javaFile.getParent().refreshLocal(IResource.DEPTH_ONE, new NullProgressMonitor()); 666 667 } catch (CoreException e) { 668 // failed to delete it, the user will have to delete it manually. 669 String message = String.format(Messages.Delete_Obsolete_Error, 670 javaFile.getFullPath()); 671 IProject project = getProject(); 672 AdtPlugin.printErrorToConsole(project, message); 673 AdtPlugin.printErrorToConsole(project, e.getMessage()); 674 } 675 } 676 } 677 678 /** 679 * Creates a relative {@link IPath} from a java package. 680 * @param javaPackageName the java package. 681 */ getJavaPackagePath(String javaPackageName)682 private IPath getJavaPackagePath(String javaPackageName) { 683 // convert the java package into path 684 String[] segments = javaPackageName.split(AndroidConstants.RE_DOT); 685 686 StringBuilder path = new StringBuilder(); 687 for (String s : segments) { 688 path.append(AndroidConstants.WS_SEP_CHAR); 689 path.append(s); 690 } 691 692 return new Path(path.toString()); 693 } 694 695 /** 696 * Returns an {@link IFolder} (located inside the 'gen' source folder), that matches the 697 * package defined in the manifest. This {@link IFolder} may not actually exist 698 * (aapt will create it anyway). 699 * @param project The project. 700 * @return the {@link IFolder} that will contain the R class or null if the folder was not found. 701 * @throws CoreException 702 */ getGenManifestPackageFolder(IProject project)703 private IFolder getGenManifestPackageFolder(IProject project) 704 throws CoreException { 705 // get the path for the package 706 IPath packagePath = getJavaPackagePath(mManifestPackage); 707 708 // get a folder for this path under the 'gen' source folder, and return it. 709 // This IFolder may not reference an actual existing folder. 710 return mGenFolder.getFolder(packagePath); 711 } 712 713 /** 714 * Compiles aidl files into java. This will also removes old java files 715 * created from aidl files that are now gone. 716 * @param projectTarget Target of the project 717 * @param sourceFolders the list of source folders, relative to the workspace. 718 * @param monitor the projess monitor 719 * @returns true if it did something 720 * @throws CoreException 721 */ handleAidl(IAndroidTarget projectTarget, ArrayList<IPath> sourceFolders, IProgressMonitor monitor)722 private boolean handleAidl(IAndroidTarget projectTarget, ArrayList<IPath> sourceFolders, 723 IProgressMonitor monitor) throws CoreException { 724 if (mAidlToCompile.size() == 0 && mAidlToRemove.size() == 0) { 725 return false; 726 } 727 728 // create the command line 729 String[] command = new String[4 + sourceFolders.size()]; 730 int index = 0; 731 command[index++] = projectTarget.getPath(IAndroidTarget.AIDL); 732 command[index++] = "-p" + Sdk.getCurrent().getTarget(getProject()).getPath( //$NON-NLS-1$ 733 IAndroidTarget.ANDROID_AIDL); 734 735 // since the path are relative to the workspace and not the project itself, we need 736 // the workspace root. 737 IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); 738 for (IPath p : sourceFolders) { 739 IFolder f = wsRoot.getFolder(p); 740 command[index++] = "-I" + f.getLocation().toOSString(); //$NON-NLS-1$ 741 } 742 743 // list of files that have failed compilation. 744 ArrayList<AidlData> stillNeedCompilation = new ArrayList<AidlData>(); 745 746 // if an aidl file is being removed before we managed to compile it, it'll be in 747 // both list. We *need* to remove it from the compile list or it'll never go away. 748 for (AidlData aidlFile : mAidlToRemove) { 749 int pos = mAidlToCompile.indexOf(aidlFile); 750 if (pos != -1) { 751 mAidlToCompile.remove(pos); 752 } 753 } 754 755 // loop until we've compile them all 756 for (AidlData aidlData : mAidlToCompile) { 757 // Remove the AIDL error markers from the aidl file 758 removeMarkersFromFile(aidlData.aidlFile, AndroidConstants.MARKER_AIDL); 759 760 // get the path of the source file. 761 IPath sourcePath = aidlData.aidlFile.getLocation(); 762 String osSourcePath = sourcePath.toOSString(); 763 764 IFile javaFile = getGenDestinationFile(aidlData, true /*createFolders*/, monitor); 765 766 // finish to set the command line. 767 command[index] = osSourcePath; 768 command[index + 1] = javaFile.getLocation().toOSString(); 769 770 // launch the process 771 if (execAidl(command, aidlData.aidlFile) == false) { 772 // aidl failed. File should be marked. We add the file to the list 773 // of file that will need compilation again. 774 stillNeedCompilation.add(aidlData); 775 776 // and we move on to the next one. 777 continue; 778 } else { 779 // make sure the file will be marked as derived once we refresh the 'gen' source 780 // folder. 781 mDerivedProgressMonitor.addFile(javaFile); 782 } 783 } 784 785 // change the list to only contains the file that have failed compilation 786 mAidlToCompile.clear(); 787 mAidlToCompile.addAll(stillNeedCompilation); 788 789 // Remove the java files created from aidl files that have been removed. 790 for (AidlData aidlData : mAidlToRemove) { 791 IFile javaFile = getGenDestinationFile(aidlData, false /*createFolders*/, monitor); 792 if (javaFile.exists()) { 793 // This confirms the java file was generated by the builder, 794 // we can delete the aidlFile. 795 javaFile.delete(true, null); 796 797 // Refresh parent. 798 javaFile.getParent().refreshLocal(IResource.DEPTH_ONE, monitor); 799 } 800 } 801 802 mAidlToRemove.clear(); 803 804 // store the build state. If there are any files that failed to compile, we will 805 // force a full aidl compile on the next project open. (unless a full compilation succeed 806 // before the project is closed/re-opened.) 807 // TODO: Optimize by saving only the files that need compilation 808 saveProjectBooleanProperty(PROPERTY_COMPILE_AIDL , mAidlToCompile.size() > 0); 809 810 return true; 811 } 812 813 /** 814 * Returns the {@link IFile} handle to the destination file for a given aild source file 815 * ({@link AidlData}). 816 * @param aidlData the data for the aidl source file. 817 * @param createFolders whether or not the parent folder of the destination should be created 818 * if it does not exist. 819 * @param monitor the progress monitor 820 * @return the handle to the destination file. 821 * @throws CoreException 822 */ getGenDestinationFile(AidlData aidlData, boolean createFolders, IProgressMonitor monitor)823 private IFile getGenDestinationFile(AidlData aidlData, boolean createFolders, 824 IProgressMonitor monitor) throws CoreException { 825 // build the destination folder path. 826 // Use the path of the source file, except for the path leading to its source folder, 827 // and for the last segment which is the filename. 828 int segmentToSourceFolderCount = aidlData.sourceFolder.getFullPath().segmentCount(); 829 IPath packagePath = aidlData.aidlFile.getFullPath().removeFirstSegments( 830 segmentToSourceFolderCount).removeLastSegments(1); 831 Path destinationPath = new Path(packagePath.toString()); 832 833 // get an IFolder for this path. It's relative to the 'gen' folder already 834 IFolder destinationFolder = mGenFolder.getFolder(destinationPath); 835 836 // create it if needed. 837 if (destinationFolder.exists() == false && createFolders) { 838 createFolder(destinationFolder, monitor); 839 } 840 841 // Build the Java file name from the aidl name. 842 String javaName = aidlData.aidlFile.getName().replaceAll(AndroidConstants.RE_AIDL_EXT, 843 AndroidConstants.DOT_JAVA); 844 845 // get the resource for the java file. 846 IFile javaFile = destinationFolder.getFile(javaName); 847 return javaFile; 848 } 849 850 /** 851 * Creates the destination folder. Because 852 * {@link IFolder#create(boolean, boolean, IProgressMonitor)} only works if the parent folder 853 * already exists, this goes and ensure that all the parent folders actually exist, or it 854 * creates them as well. 855 * @param destinationFolder The folder to create 856 * @param monitor the {@link IProgressMonitor}, 857 * @throws CoreException 858 */ createFolder(IFolder destinationFolder, IProgressMonitor monitor)859 private void createFolder(IFolder destinationFolder, IProgressMonitor monitor) 860 throws CoreException { 861 862 // check the parent exist and create if necessary. 863 IContainer parent = destinationFolder.getParent(); 864 if (parent.getType() == IResource.FOLDER && parent.exists() == false) { 865 createFolder((IFolder)parent, monitor); 866 } 867 868 // create the folder. 869 destinationFolder.create(true /*force*/, true /*local*/, 870 new SubProgressMonitor(monitor, 10)); 871 } 872 873 /** 874 * Execute the aidl command line, parse the output, and mark the aidl file 875 * with any reported errors. 876 * @param command the String array containing the command line to execute. 877 * @param file The IFile object representing the aidl file being 878 * compiled. 879 * @return false if the exec failed, and build needs to be aborted. 880 */ execAidl(String[] command, IFile file)881 private boolean execAidl(String[] command, IFile file) { 882 // do the exec 883 try { 884 Process p = Runtime.getRuntime().exec(command); 885 886 // list to store each line of stderr 887 ArrayList<String> results = new ArrayList<String>(); 888 889 // get the output and return code from the process 890 int result = grabProcessOutput(p, results); 891 892 // attempt to parse the error output 893 boolean error = parseAidlOutput(results, file); 894 895 // If the process failed and we couldn't parse the output 896 // we pring a message, mark the project and exit 897 if (result != 0 && error == true) { 898 // display the message in the console. 899 AdtPlugin.printErrorToConsole(getProject(), results.toArray()); 900 901 // mark the project and exit 902 markProject(AdtConstants.MARKER_ADT, Messages.Unparsed_AIDL_Errors, 903 IMarker.SEVERITY_ERROR); 904 return false; 905 } 906 } catch (IOException e) { 907 // mark the project and exit 908 String msg = String.format(Messages.AIDL_Exec_Error, command[0]); 909 markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); 910 return false; 911 } catch (InterruptedException e) { 912 // mark the project and exit 913 String msg = String.format(Messages.AIDL_Exec_Error, command[0]); 914 markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); 915 return false; 916 } 917 918 return true; 919 } 920 921 /** 922 * Goes through the build paths and fills the list of aidl files to compile 923 * ({@link #mAidlToCompile}). 924 * @param project The project. 925 * @param sourceFolderPathList The list of source folder paths. 926 */ buildAidlCompilationList(IProject project, ArrayList<IPath> sourceFolderPathList)927 private void buildAidlCompilationList(IProject project, 928 ArrayList<IPath> sourceFolderPathList) { 929 IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); 930 for (IPath sourceFolderPath : sourceFolderPathList) { 931 IFolder sourceFolder = root.getFolder(sourceFolderPath); 932 // we don't look in the 'gen' source folder as there will be no source in there. 933 if (sourceFolder.exists() && sourceFolder.equals(mGenFolder) == false) { 934 scanFolderForAidl(sourceFolder, sourceFolder); 935 } 936 } 937 } 938 939 /** 940 * Scans a folder and fills the list of aidl files to compile. 941 * @param sourceFolder the root source folder. 942 * @param folder The folder to scan. 943 */ scanFolderForAidl(IFolder sourceFolder, IFolder folder)944 private void scanFolderForAidl(IFolder sourceFolder, IFolder folder) { 945 try { 946 IResource[] members = folder.members(); 947 for (IResource r : members) { 948 // get the type of the resource 949 switch (r.getType()) { 950 case IResource.FILE: 951 // if this a file, check that the file actually exist 952 // and that it's an aidl file 953 if (r.exists() && 954 AndroidConstants.EXT_AIDL.equalsIgnoreCase(r.getFileExtension())) { 955 mAidlToCompile.add(new AidlData(sourceFolder, (IFile)r)); 956 } 957 break; 958 case IResource.FOLDER: 959 // recursively go through children 960 scanFolderForAidl(sourceFolder, (IFolder)r); 961 break; 962 default: 963 // this would mean it's a project or the workspace root 964 // which is unlikely to happen. we do nothing 965 break; 966 } 967 } 968 } catch (CoreException e) { 969 // Couldn't get the members list for some reason. Just return. 970 } 971 } 972 973 974 /** 975 * Parse the output of aidl and mark the file with any errors. 976 * @param lines The output to parse. 977 * @param file The file to mark with error. 978 * @return true if the parsing failed, false if success. 979 */ parseAidlOutput(ArrayList<String> lines, IFile file)980 private boolean parseAidlOutput(ArrayList<String> lines, IFile file) { 981 // nothing to parse? just return false; 982 if (lines.size() == 0) { 983 return false; 984 } 985 986 Matcher m; 987 988 for (int i = 0; i < lines.size(); i++) { 989 String p = lines.get(i); 990 991 m = sAidlPattern1.matcher(p); 992 if (m.matches()) { 993 // we can ignore group 1 which is the location since we already 994 // have a IFile object representing the aidl file. 995 String lineStr = m.group(2); 996 String msg = m.group(3); 997 998 // get the line number 999 int line = 0; 1000 try { 1001 line = Integer.parseInt(lineStr); 1002 } catch (NumberFormatException e) { 1003 // looks like the string we extracted wasn't a valid 1004 // file number. Parsing failed and we return true 1005 return true; 1006 } 1007 1008 // mark the file 1009 BaseProjectHelper.addMarker(file, AndroidConstants.MARKER_AIDL, msg, line, 1010 IMarker.SEVERITY_ERROR); 1011 1012 // success, go to the next line 1013 continue; 1014 } 1015 1016 // invalid line format, flag as error, and bail 1017 return true; 1018 } 1019 1020 return false; 1021 } 1022 1023 /** 1024 * Merge the current list of aidl file to compile/remove with the new one. 1025 * @param toCompile List of file to compile 1026 * @param toRemove List of file to remove 1027 */ mergeAidlFileModifications(ArrayList<AidlData> toCompile, ArrayList<AidlData> toRemove)1028 private void mergeAidlFileModifications(ArrayList<AidlData> toCompile, 1029 ArrayList<AidlData> toRemove) { 1030 // loop through the new toRemove list, and add it to the old one, 1031 // plus remove any file that was still to compile and that are now 1032 // removed 1033 for (AidlData r : toRemove) { 1034 if (mAidlToRemove.indexOf(r) == -1) { 1035 mAidlToRemove.add(r); 1036 } 1037 1038 int index = mAidlToCompile.indexOf(r); 1039 if (index != -1) { 1040 mAidlToCompile.remove(index); 1041 } 1042 } 1043 1044 // now loop through the new files to compile and add it to the list. 1045 // Also look for them in the remove list, this would mean that they 1046 // were removed, then added back, and we shouldn't remove them, just 1047 // recompile them. 1048 for (AidlData r : toCompile) { 1049 if (mAidlToCompile.indexOf(r) == -1) { 1050 mAidlToCompile.add(r); 1051 } 1052 1053 int index = mAidlToRemove.indexOf(r); 1054 if (index != -1) { 1055 mAidlToRemove.remove(index); 1056 } 1057 } 1058 } 1059 } 1060