• 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.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      * "&lt;path&gt;:&lt;line&gt;: &lt;error&gt;"
78      * or
79      * "&lt;path&gt;:&lt;line&gt; &lt;error&gt;"
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