Home | History | Annotate | Download | only in build
      1 /*
      2  * Copyright (C) 2011 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.SdkConstants;
     20 import com.android.annotations.NonNull;
     21 import com.android.ide.eclipse.adt.internal.build.builders.BaseBuilder;
     22 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
     23 import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
     24 import com.android.sdklib.BuildToolInfo;
     25 import com.android.sdklib.IAndroidTarget;
     26 
     27 import org.eclipse.core.resources.IFile;
     28 import org.eclipse.core.resources.IFolder;
     29 import org.eclipse.core.resources.IProject;
     30 import org.eclipse.core.resources.IResource;
     31 import org.eclipse.core.resources.IWorkspaceRoot;
     32 import org.eclipse.core.resources.ResourcesPlugin;
     33 import org.eclipse.core.runtime.CoreException;
     34 import org.eclipse.core.runtime.IPath;
     35 import org.eclipse.core.runtime.IProgressMonitor;
     36 import org.eclipse.jdt.core.IJavaProject;
     37 
     38 import java.io.File;
     39 import java.util.ArrayList;
     40 import java.util.Collection;
     41 import java.util.HashMap;
     42 import java.util.List;
     43 import java.util.Locale;
     44 import java.util.Map;
     45 import java.util.Set;
     46 
     47 /**
     48  * Base class to handle generated java code.
     49  *
     50  * It provides management for modified source file list, deleted source file list, reconciliation
     51  * of previous lists, storing the current state of the build.
     52  *
     53  */
     54 public abstract class SourceProcessor {
     55 
     56     public final static int COMPILE_STATUS_NONE = 0;
     57     public final static int COMPILE_STATUS_CODE = 0x1;
     58     public final static int COMPILE_STATUS_RES = 0x2;
     59 
     60     /** List of all source files, their dependencies, and their output. */
     61     private final Map<IFile, SourceFileData> mFiles = new HashMap<IFile, SourceFileData>();
     62 
     63     private final IJavaProject mJavaProject;
     64     private BuildToolInfo mBuildToolInfo;
     65     private final IFolder mGenFolder;
     66     private final SourceChangeHandler mDeltaVisitor;
     67 
     68     /** List of source files pending compilation at the next build */
     69     private final List<IFile> mToCompile = new ArrayList<IFile>();
     70 
     71     /** List of removed source files pending cleaning at the next build. */
     72     private final List<IFile> mRemoved = new ArrayList<IFile>();
     73 
     74     private int mLastCompilationStatus = COMPILE_STATUS_NONE;
     75 
     76     /**
     77      * Quotes a path inside "". If the platform is not windows, the path is returned as is.
     78      * @param path the path to quote
     79      * @return the quoted path.
     80      */
     81     public static String quote(String path) {
     82         if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) {
     83             return "\"" + path + "\"";
     84         }
     85 
     86         return path;
     87     }
     88 
     89     protected SourceProcessor(@NonNull IJavaProject javaProject,
     90             @NonNull BuildToolInfo buildToolInfo, @NonNull IFolder genFolder,
     91             @NonNull SourceChangeHandler deltaVisitor) {
     92         mJavaProject = javaProject;
     93         mBuildToolInfo = buildToolInfo;
     94         mGenFolder = genFolder;
     95         mDeltaVisitor = deltaVisitor;
     96 
     97         mDeltaVisitor.init(this);
     98 
     99         IProject project = javaProject.getProject();
    100 
    101         // get all the source files
    102         buildSourceFileList();
    103 
    104         // load the known dependencies
    105         loadOutputAndDependencies();
    106 
    107         boolean mustCompile = loadState(project);
    108 
    109         // if we stored that we have to compile some files, we build the list that will compile them
    110         // all. For now we have to reuse the full list since we don't know which files needed
    111         // compilation.
    112         if (mustCompile) {
    113             mToCompile.addAll(mFiles.keySet());
    114         }
    115     }
    116 
    117     protected SourceProcessor(@NonNull IJavaProject javaProject,
    118             @NonNull BuildToolInfo buildToolInfo, @NonNull IFolder genFolder) {
    119         this(javaProject, buildToolInfo, genFolder, new SourceChangeHandler());
    120     }
    121 
    122     public void setBuildToolInfo(BuildToolInfo buildToolInfo) {
    123         mBuildToolInfo = buildToolInfo;
    124     }
    125 
    126 
    127     /**
    128      * Returns whether the given file is an output of this processor by return the source
    129      * file that generated it.
    130      * @param file the file to test.
    131      * @return the source file that generated the given file or null.
    132      */
    133     IFile isOutput(IFile file) {
    134         for (SourceFileData data : mFiles.values()) {
    135             if (data.generated(file)) {
    136                 return data.getSourceFile();
    137             }
    138         }
    139 
    140         return null;
    141     }
    142 
    143     /**
    144      * Returns whether the given file is a dependency for other files by returning a list
    145      * of file depending on the given file.
    146      * @param file the file to test.
    147      * @return a list of files that depend on the given file or an empty list if there
    148      *    are no matches.
    149      */
    150     List<IFile> isDependency(IFile file) {
    151         ArrayList<IFile> files = new ArrayList<IFile>();
    152         for (SourceFileData data : mFiles.values()) {
    153             if (data.dependsOn(file)) {
    154                 files.add(data.getSourceFile());
    155             }
    156         }
    157 
    158         return files;
    159     }
    160 
    161     void addData(SourceFileData data) {
    162         mFiles.put(data.getSourceFile(), data);
    163     }
    164 
    165     SourceFileData getFileData(IFile file) {
    166         return mFiles.get(file);
    167     }
    168 
    169     Collection<SourceFileData> getAllFileData() {
    170         return mFiles.values();
    171     }
    172 
    173     public final SourceChangeHandler getChangeHandler() {
    174         return mDeltaVisitor;
    175     }
    176 
    177     final IJavaProject getJavaProject() {
    178         return mJavaProject;
    179     }
    180 
    181     final BuildToolInfo getBuildToolInfo() {
    182         return mBuildToolInfo;
    183     }
    184 
    185     final IFolder getGenFolder() {
    186         return mGenFolder;
    187     }
    188 
    189     final List<IFile> getToCompile() {
    190         return mToCompile;
    191     }
    192 
    193     final List<IFile> getRemovedFile() {
    194         return mRemoved;
    195     }
    196 
    197     final void addFileToCompile(IFile file) {
    198         mToCompile.add(file);
    199     }
    200 
    201     public final void prepareFullBuild(IProject project) {
    202         mDeltaVisitor.reset();
    203 
    204         mToCompile.clear();
    205         mRemoved.clear();
    206 
    207         // get all the source files
    208         buildSourceFileList();
    209 
    210         mToCompile.addAll(mFiles.keySet());
    211 
    212         saveState(project);
    213     }
    214 
    215     public final void doneVisiting(IProject project) {
    216         // merge the previous file modification lists and the new one.
    217         mergeFileModifications(mDeltaVisitor);
    218 
    219         mDeltaVisitor.reset();
    220 
    221         saveState(project);
    222     }
    223 
    224     /**
    225      * Returns the extension of the source files handled by this processor.
    226      * @return
    227      */
    228     protected abstract Set<String> getExtensions();
    229 
    230     protected abstract String getSavePropertyName();
    231 
    232     /**
    233      * Compiles the source files and return a status bitmask of the type of file that was generated.
    234      *
    235      */
    236     public final int compileFiles(BaseBuilder builder,
    237             IProject project, IAndroidTarget projectTarget,
    238             List<IPath> sourceFolders, List<File> libraryProjectsOut, IProgressMonitor monitor)
    239             throws CoreException {
    240 
    241         mLastCompilationStatus = COMPILE_STATUS_NONE;
    242 
    243         if (mToCompile.size() == 0 && mRemoved.size() == 0) {
    244             return mLastCompilationStatus;
    245         }
    246 
    247         // if a source file is being removed before we managed to compile it, it'll be in
    248         // both list. We *need* to remove it from the compile list or it'll never go away.
    249         for (IFile sourceFile : mRemoved) {
    250             int pos = mToCompile.indexOf(sourceFile);
    251             if (pos != -1) {
    252                 mToCompile.remove(pos);
    253             }
    254         }
    255 
    256         // list of files that have failed compilation.
    257         List<IFile> stillNeedCompilation = new ArrayList<IFile>();
    258 
    259         doCompileFiles(mToCompile, builder, project, projectTarget, sourceFolders,
    260                 stillNeedCompilation, libraryProjectsOut, monitor);
    261 
    262         mToCompile.clear();
    263         mToCompile.addAll(stillNeedCompilation);
    264 
    265         // Remove the files created from source files that have been removed.
    266         for (IFile sourceFile : mRemoved) {
    267             // look if we already know the output
    268             SourceFileData data = getFileData(sourceFile);
    269             if (data != null) {
    270                 doRemoveFiles(data);
    271             }
    272         }
    273 
    274         // remove the associated file data.
    275         for (IFile removedFile : mRemoved) {
    276             mFiles.remove(removedFile);
    277         }
    278 
    279         mRemoved.clear();
    280 
    281         // store the build state. If there are any files that failed to compile, we will
    282         // force a full aidl compile on the next project open. (unless a full compilation succeed
    283         // before the project is closed/re-opened.)
    284         saveState(project);
    285 
    286         return mLastCompilationStatus;
    287     }
    288 
    289     protected abstract void doCompileFiles(
    290             List<IFile> filesToCompile, BaseBuilder builder,
    291             IProject project, IAndroidTarget projectTarget,
    292             List<IPath> sourceFolders, List<IFile> notCompiledOut,
    293             List<File> libraryProjectsOut, IProgressMonitor monitor) throws CoreException;
    294 
    295     /**
    296      * Adds a compilation status. It can be any of (in combination too):
    297      * <p/>
    298      * {@link #COMPILE_STATUS_CODE} means this processor created source code files.
    299      * {@link #COMPILE_STATUS_RES} means this process created resources.
    300      */
    301     protected void setCompilationStatus(int status) {
    302         mLastCompilationStatus |= status;
    303     }
    304 
    305     protected void doRemoveFiles(SourceFileData data) throws CoreException {
    306         List<IFile> outputFiles = data.getOutputFiles();
    307         for (IFile outputFile : outputFiles) {
    308             if (outputFile.exists()) {
    309                 outputFile.getLocation().toFile().delete();
    310             }
    311         }
    312     }
    313 
    314     public final boolean loadState(IProject project) {
    315         return ProjectHelper.loadBooleanProperty(project, getSavePropertyName(),
    316                 true /*defaultValue*/);
    317     }
    318 
    319     public final void saveState(IProject project) {
    320         // TODO: Optimize by saving only the files that need compilation
    321         ProjectHelper.saveStringProperty(project, getSavePropertyName(),
    322                 Boolean.toString(mToCompile.size() > 0));
    323     }
    324 
    325     protected abstract void loadOutputAndDependencies();
    326 
    327 
    328     protected IPath getSourceFolderFor(IFile file) {
    329         // find the source folder for the class so that we can infer the package from the
    330         // difference between the file and its source folder.
    331         List<IPath> sourceFolders = BaseProjectHelper.getSourceClasspaths(getJavaProject());
    332         IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
    333 
    334         for (IPath sourceFolderPath : sourceFolders) {
    335             IFolder sourceFolder = root.getFolder(sourceFolderPath);
    336             // we don't look in the 'gen' source folder as there will be no source in there.
    337             if (sourceFolder.exists() && sourceFolder.equals(getGenFolder()) == false) {
    338                 // look for the source file parent, until we find this source folder.
    339                 IResource parent = file;
    340                 while ((parent = parent.getParent()) != null) {
    341                     if (parent.equals(sourceFolder)) {
    342                         return sourceFolderPath;
    343                     }
    344                 }
    345             }
    346         }
    347 
    348         return null;
    349     }
    350 
    351     /**
    352      * Goes through the build paths and fills the list of files to compile.
    353      *
    354      * @param project The project.
    355      * @param sourceFolderPathList The list of source folder paths.
    356      */
    357     private final void buildSourceFileList() {
    358         mFiles.clear();
    359 
    360         IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
    361         List<IPath> sourceFolderPathList = BaseProjectHelper.getSourceClasspaths(mJavaProject);
    362 
    363         for (IPath sourceFolderPath : sourceFolderPathList) {
    364             IFolder sourceFolder = root.getFolder(sourceFolderPath);
    365             // we don't look in the 'gen' source folder as there will be no source in there.
    366             if (sourceFolder.exists() && sourceFolder.equals(getGenFolder()) == false) {
    367                 scanFolderForSourceFiles(sourceFolder, sourceFolder);
    368             }
    369         }
    370     }
    371 
    372     /**
    373      * Scans a folder and fills the list of files to compile.
    374      * @param sourceFolder the root source folder.
    375      * @param folder The folder to scan.
    376      */
    377     private void scanFolderForSourceFiles(IFolder sourceFolder, IFolder folder) {
    378         try {
    379             IResource[] members = folder.members();
    380             for (IResource r : members) {
    381                 // get the type of the resource
    382                switch (r.getType()) {
    383                    case IResource.FILE: {
    384                        // if this a file, check that the file actually exist
    385                        // and that it's the type of of file that's used in this processor
    386                        String extension = r.exists() ? r.getFileExtension() : null;
    387                        if (extension != null &&
    388                                getExtensions().contains(extension.toLowerCase(Locale.US))) {
    389                            mFiles.put((IFile) r, new SourceFileData((IFile) r));
    390                        }
    391                        break;
    392                    }
    393                    case IResource.FOLDER:
    394                        // recursively go through children
    395                        scanFolderForSourceFiles(sourceFolder, (IFolder)r);
    396                        break;
    397                    default:
    398                        // this would mean it's a project or the workspace root
    399                        // which is unlikely to happen. we do nothing
    400                        break;
    401                }
    402             }
    403         } catch (CoreException e) {
    404             // Couldn't get the members list for some reason. Just return.
    405         }
    406     }
    407 
    408 
    409     /**
    410      * Merge the current list of source file to compile/remove with the one coming from the
    411      * delta visitor
    412      * @param visitor the delta visitor.
    413      */
    414     private void mergeFileModifications(SourceChangeHandler visitor) {
    415         Set<IFile> toRemove = visitor.getRemovedFiles();
    416         Set<IFile> toCompile = visitor.getFilesToCompile();
    417 
    418         // loop through the new toRemove list, and add it to the old one,
    419         // plus remove any file that was still to compile and that are now
    420         // removed
    421         for (IFile r : toRemove) {
    422             if (mRemoved.indexOf(r) == -1) {
    423                 mRemoved.add(r);
    424             }
    425 
    426             int index = mToCompile.indexOf(r);
    427             if (index != -1) {
    428                 mToCompile.remove(index);
    429             }
    430         }
    431 
    432         // now loop through the new files to compile and add it to the list.
    433         // Also look for them in the remove list, this would mean that they
    434         // were removed, then added back, and we shouldn't remove them, just
    435         // recompile them.
    436         for (IFile r : toCompile) {
    437             if (mToCompile.indexOf(r) == -1) {
    438                 mToCompile.add(r);
    439             }
    440 
    441             int index = mRemoved.indexOf(r);
    442             if (index != -1) {
    443                 mRemoved.remove(index);
    444             }
    445         }
    446     }
    447 }
    448