diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build')
31 files changed, 9083 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptExecException.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptExecException.java new file mode 100644 index 000000000..23b1baa92 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptExecException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build; + +/** + * Exception thrown when the execution of aapt fails. + * + */ +public final class AaptExecException extends Exception { + private static final long serialVersionUID = 1L; + + AaptExecException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptParser.java new file mode 100644 index 000000000..1f17fb7c6 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptParser.java @@ -0,0 +1,783 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build; + +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jface.text.FindReplaceDocumentAdapter; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.Region; +import org.eclipse.ui.editors.text.TextFileDocumentProvider; +import org.eclipse.ui.texteditor.IDocumentProvider; + +import java.io.File; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class AaptParser { + + // TODO: rename the pattern to something that makes sense + javadoc comments. + /** + * Single line aapt warning for skipping files.<br> + * " (skipping hidden file '<file path>'" + */ + private final static Pattern sPattern0Line1 = Pattern.compile( + "^\\s+\\(skipping hidden file\\s'(.*)'\\)$"); //$NON-NLS-1$ + + /** + * First line of dual line aapt error.<br> + * "ERROR at line <line>: <error>"<br> + * " (Occurred while parsing <path>)" + */ + private final static Pattern sPattern1Line1 = Pattern.compile( + "^ERROR\\s+at\\s+line\\s+(\\d+):\\s+(.*)$"); //$NON-NLS-1$ + /** + * Second line of dual line aapt error.<br> + * "ERROR at line <line>: <error>"<br> + * " (Occurred while parsing <path>)"<br> + * @see #sPattern1Line1 + */ + private final static Pattern sPattern1Line2 = Pattern.compile( + "^\\s+\\(Occurred while parsing\\s+(.*)\\)$"); //$NON-NLS-1$ + /** + * First line of dual line aapt error.<br> + * "ERROR: <error>"<br> + * "Defined at file <path> line <line>" + */ + private final static Pattern sPattern2Line1 = Pattern.compile( + "^ERROR:\\s+(.+)$"); //$NON-NLS-1$ + /** + * Second line of dual line aapt error.<br> + * "ERROR: <error>"<br> + * "Defined at file <path> line <line>"<br> + * @see #sPattern2Line1 + */ + private final static Pattern sPattern2Line2 = Pattern.compile( + "Defined\\s+at\\s+file\\s+(.+)\\s+line\\s+(\\d+)"); //$NON-NLS-1$ + /** + * Single line aapt error<br> + * "<path> line <line>: <error>" + */ + private final static Pattern sPattern3Line1 = Pattern.compile( + "^(.+)\\sline\\s(\\d+):\\s(.+)$"); //$NON-NLS-1$ + /** + * First line of dual line aapt error.<br> + * "ERROR parsing XML file <path>"<br> + * "<error> at line <line>" + */ + private final static Pattern sPattern4Line1 = Pattern.compile( + "^Error\\s+parsing\\s+XML\\s+file\\s(.+)$"); //$NON-NLS-1$ + /** + * Second line of dual line aapt error.<br> + * "ERROR parsing XML file <path>"<br> + * "<error> at line <line>"<br> + * @see #sPattern4Line1 + */ + private final static Pattern sPattern4Line2 = Pattern.compile( + "^(.+)\\s+at\\s+line\\s+(\\d+)$"); //$NON-NLS-1$ + + /** + * Single line aapt warning<br> + * "<path>:<line>: <error>" + */ + private final static Pattern sPattern5Line1 = Pattern.compile( + "^(.+?):(\\d+):\\s+WARNING:(.+)$"); //$NON-NLS-1$ + + /** + * Single line aapt error<br> + * "<path>:<line>: <error>" + */ + private final static Pattern sPattern6Line1 = Pattern.compile( + "^(.+?):(\\d+):\\s+(.+)$"); //$NON-NLS-1$ + + /** + * 4 line aapt error<br> + * "ERROR: 9-path image <path> malformed"<br> + * Line 2 and 3 are taken as-is while line 4 is ignored (it repeats with<br> + * 'ERROR: failure processing <path>) + */ + private final static Pattern sPattern7Line1 = Pattern.compile( + "^ERROR:\\s+9-patch\\s+image\\s+(.+)\\s+malformed\\.$"); //$NON-NLS-1$ + + private final static Pattern sPattern8Line1 = Pattern.compile( + "^(invalid resource directory name): (.*)$"); //$NON-NLS-1$ + + /** + * Portion of the error message which states the context in which the error occurred, + * such as which property was being processed and what the string value was that + * caused the error. + * <p> + * Example: + * error: No resource found that matches the given name (at 'text' with value '@string/foo') + */ + private static final Pattern sValueRangePattern = + Pattern.compile("\\(at '(.+)' with value '(.*)'\\)"); //$NON-NLS-1$ + + + /** + * Portion of error message which points to the second occurrence of a repeated resource + * definition. + * <p> + * Example: + * error: Resource entry repeatedStyle1 already has bag item android:gravity. + */ + private static final Pattern sRepeatedRangePattern = + Pattern.compile("Resource entry (.+) already has bag item (.+)\\."); //$NON-NLS-1$ + + /** + * Error message emitted when aapt skips a file because for example it's name is + * invalid, such as a layout file name which starts with _. + * <p> + * This error message is used by AAPT in Tools 19 and earlier. + */ + private static final Pattern sSkippingPattern = + Pattern.compile(" \\(skipping (.+) .+ '(.*)'\\)"); //$NON-NLS-1$ + + /** + * Error message emitted when aapt skips a file because for example it's name is + * invalid, such as a layout file name which starts with _. + * <p> + * This error message is used by AAPT in Tools 20 and later. + */ + private static final Pattern sNewSkippingPattern = + Pattern.compile(" \\(skipping .+ '(.+)' due to ANDROID_AAPT_IGNORE pattern '.+'\\)"); //$NON-NLS-1$ + + /** + * Suffix of error message which points to the first occurrence of a repeated resource + * definition. + * Example: + * Originally defined here. + */ + private static final String ORIGINALLY_DEFINED_MSG = "Originally defined here."; //$NON-NLS-1$ + + /** + * Portion of error message which points to the second occurrence of a repeated resource + * definition. + * <p> + * Example: + * error: Resource entry repeatedStyle1 already has bag item android:gravity. + */ + private static final Pattern sNoResourcePattern = + Pattern.compile("No resource found that matches the given name: attr '(.+)'\\."); //$NON-NLS-1$ + + /** + * Portion of error message which points to a missing required attribute in a + * resource definition. + * <p> + * Example: + * error: error: A 'name' attribute is required for <style> + */ + private static final Pattern sRequiredPattern = + Pattern.compile("A '(.+)' attribute is required for <(.+)>"); //$NON-NLS-1$ + + /** + * 2 line aapt error<br> + * "ERROR: Invalid configuration: foo"<br> + * " ^^^"<br> + * There's no need to parse the 2nd line. + */ + private final static Pattern sPattern9Line1 = Pattern.compile( + "^Invalid configuration: (.+)$"); //$NON-NLS-1$ + + private final static Pattern sXmlBlockPattern = Pattern.compile( + "W/ResourceType\\(.*\\): Bad XML block: no root element node found"); //$NON-NLS-1$ + + /** + * Parse the output of aapt and mark the incorrect file with error markers + * + * @param results the output of aapt + * @param project the project containing the file to mark + * @return true if the parsing failed, false if success. + */ + public static boolean parseOutput(List<String> results, IProject project) { + int size = results.size(); + if (size > 0) { + return parseOutput(results.toArray(new String[size]), project); + } + + return false; + } + + /** + * Parse the output of aapt and mark the incorrect file with error markers + * + * @param results the output of aapt + * @param project the project containing the file to mark + * @return true if the parsing failed, false if success. + */ + public static boolean parseOutput(String[] results, IProject project) { + // nothing to parse? just return false; + if (results.length == 0) { + return false; + } + + // get the root of the project so that we can make IFile from full + // file path + String osRoot = project.getLocation().toOSString(); + + Matcher m; + + for (int i = 0; i < results.length ; i++) { + String p = results[i]; + + m = sPattern0Line1.matcher(p); + if (m.matches()) { + // we ignore those (as this is an ignore message from aapt) + continue; + } + + m = sPattern1Line1.matcher(p); + if (m.matches()) { + String lineStr = m.group(1); + String msg = m.group(2); + + // get the matcher for the next line. + m = getNextLineMatcher(results, ++i, sPattern1Line2); + if (m == null) { + return true; + } + + String location = m.group(1); + + // check the values and attempt to mark the file. + if (checkAndMark(location, lineStr, msg, osRoot, project, + AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + continue; + } + + // this needs to be tested before Pattern2 since they both start with 'ERROR:' + m = sPattern7Line1.matcher(p); + if (m.matches()) { + String location = m.group(1); + String msg = p; // default msg is the line in case we don't find anything else + + if (++i < results.length) { + msg = results[i].trim(); + if (++i < results.length) { + msg = msg + " - " + results[i].trim(); //$NON-NLS-1$ + + // skip the next line + i++; + } + } + + // display the error + if (checkAndMark(location, null, msg, osRoot, project, + AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sPattern2Line1.matcher(p); + if (m.matches()) { + // get the msg + String msg = m.group(1); + + // get the matcher for the next line. + m = getNextLineMatcher(results, ++i, sPattern2Line2); + if (m == null) { + return true; + } + + String location = m.group(1); + String lineStr = m.group(2); + + // check the values and attempt to mark the file. + if (checkAndMark(location, lineStr, msg, osRoot, project, + AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + continue; + } + + m = sPattern3Line1.matcher(p); + if (m.matches()) { + String location = m.group(1); + String lineStr = m.group(2); + String msg = m.group(3); + + // check the values and attempt to mark the file. + if (checkAndMark(location, lineStr, msg, osRoot, project, + AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sPattern4Line1.matcher(p); + if (m.matches()) { + // get the filename. + String location = m.group(1); + + // get the matcher for the next line. + m = getNextLineMatcher(results, ++i, sPattern4Line2); + if (m == null) { + return true; + } + + String msg = m.group(1); + String lineStr = m.group(2); + + // check the values and attempt to mark the file. + if (checkAndMark(location, lineStr, msg, osRoot, project, + AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sPattern5Line1.matcher(p); + if (m.matches()) { + String location = m.group(1); + String lineStr = m.group(2); + String msg = m.group(3); + + // check the values and attempt to mark the file. + if (checkAndMark(location, lineStr, msg, osRoot, project, + AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_WARNING) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sPattern6Line1.matcher(p); + if (m.matches()) { + String location = m.group(1); + String lineStr = m.group(2); + String msg = m.group(3); + + // check the values and attempt to mark the file. + if (checkAndMark(location, lineStr, msg, osRoot, project, + AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sPattern8Line1.matcher(p); + if (m.matches()) { + String location = m.group(2); + String msg = m.group(1); + + // check the values and attempt to mark the file. + if (checkAndMark(location, null, msg, osRoot, project, + AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sPattern9Line1.matcher(p); + if (m.matches()) { + String badConfig = m.group(1); + String msg = String.format("APK Configuration filter '%1$s' is invalid", badConfig); + + // skip the next line + i++; + + // check the values and attempt to mark the file. + if (checkAndMark(null /*location*/, null, msg, osRoot, project, + AdtConstants.MARKER_AAPT_PACKAGE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sNewSkippingPattern.matcher(p); + if (m.matches()) { + String location = m.group(1); + + if (location.startsWith(".") //$NON-NLS-1$ + || location.endsWith("~")) { //$NON-NLS-1$ + continue; + } + + // check the values and attempt to mark the file. + if (checkAndMark(location, null, p.trim(), osRoot, project, + AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_WARNING) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sSkippingPattern.matcher(p); + if (m.matches()) { + String location = m.group(2); + + // Certain files can safely be skipped without marking the project + // as having errors. See isHidden() in AaptAssets.cpp: + String type = m.group(1); + if (type.equals("backup") //$NON-NLS-1$ // main.xml~, etc + || type.equals("hidden") //$NON-NLS-1$ // .gitignore, etc + || type.equals("index")) { //$NON-NLS-1$ // thumbs.db, etc + continue; + } + + // check the values and attempt to mark the file. + if (checkAndMark(location, null, p.trim(), osRoot, project, + AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_WARNING) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sXmlBlockPattern.matcher(p); + if (m.matches()) { + // W/ResourceType(12345): Bad XML block: no root element node found + // Sadly there's NO filename reference; this error typically describes the + // error *after* this line. + if (results.length == 1) { + // This is the only error message: dump to console and quit + return true; + } + // Continue: the real culprit is displayed next and should get a marker + continue; + } + + return true; + } + + return false; + } + + /** + * Check if the parameters gotten from the error output are valid, and mark + * the file with an AAPT marker. + * @param location the full OS path of the error file. If null, the project is marked + * @param lineStr + * @param message + * @param root The root directory of the project, in OS specific format. + * @param project + * @param markerId The marker id to put. + * @param severity The severity of the marker to put (IMarker.SEVERITY_*) + * @return true if the parameters were valid and the file was marked successfully. + * + * @see IMarker + */ + private static final boolean checkAndMark(String location, String lineStr, + String message, String root, IProject project, String markerId, int severity) { + // check this is in fact a file + if (location != null) { + File f = new File(location); + if (f.exists() == false) { + return false; + } + } + + // get the line number + int line = -1; // default value for error with no line. + + if (lineStr != null) { + try { + line = Integer.parseInt(lineStr); + } catch (NumberFormatException e) { + // looks like the string we extracted wasn't a valid + // file number. Parsing failed and we return true + return false; + } + } + + // add the marker + IResource f2 = project; + if (location != null) { + f2 = getResourceFromFullPath(location, root, project); + if (f2 == null) { + return false; + } + } + + // Attempt to determine the exact range of characters affected by this error. + // This will look up the actual text of the file, go to the particular error line + // and scan for the specific string mentioned in the error. + int startOffset = -1; + int endOffset = -1; + if (f2 instanceof IFile) { + IRegion region = findRange((IFile) f2, line, message); + if (region != null) { + startOffset = region.getOffset(); + endOffset = startOffset + region.getLength(); + } + } + + // check if there's a similar marker already, since aapt is launched twice + boolean markerAlreadyExists = false; + try { + IMarker[] markers = f2.findMarkers(markerId, true, IResource.DEPTH_ZERO); + + for (IMarker marker : markers) { + if (startOffset != -1) { + int tmpBegin = marker.getAttribute(IMarker.CHAR_START, -1); + if (tmpBegin != startOffset) { + break; + } + int tmpEnd = marker.getAttribute(IMarker.CHAR_END, -1); + if (tmpEnd != startOffset) { + break; + } + } + + int tmpLine = marker.getAttribute(IMarker.LINE_NUMBER, -1); + if (tmpLine != line) { + break; + } + + int tmpSeverity = marker.getAttribute(IMarker.SEVERITY, -1); + if (tmpSeverity != severity) { + break; + } + + String tmpMsg = marker.getAttribute(IMarker.MESSAGE, null); + if (tmpMsg == null || tmpMsg.equals(message) == false) { + break; + } + + // if we're here, all the marker attributes are equals, we found it + // and exit + markerAlreadyExists = true; + break; + } + + } catch (CoreException e) { + // if we couldn't get the markers, then we just mark the file again + // (since markerAlreadyExists is initialized to false, we do nothing) + } + + if (markerAlreadyExists == false) { + BaseProjectHelper.markResource(f2, markerId, message, line, + startOffset, endOffset, severity); + } + + return true; + } + + /** + * Given an aapt error message in a given file and a given (initial) line number, + * return the corresponding offset range for the error, or null. + */ + private static IRegion findRange(IFile file, int line, String message) { + Matcher matcher = sValueRangePattern.matcher(message); + if (matcher.find()) { + String property = matcher.group(1); + String value = matcher.group(2); + + // First find the property. We can't just immediately look for the + // value, because there could be other attributes in this element + // earlier than the one in error, and we might accidentally pick + // up on a different occurrence of the value in a context where + // it is valid. + if (value.length() > 0) { + return findRange(file, line, property, value); + } else { + // Find first occurrence of property followed by '' or "" + IRegion region1 = findRange(file, line, property, "\"\""); //$NON-NLS-1$ + IRegion region2 = findRange(file, line, property, "''"); //$NON-NLS-1$ + if (region1 == null) { + if (region2 == null) { + // Highlight the property instead + return findRange(file, line, property, null); + } + return region2; + } else if (region2 == null) { + return region1; + } else if (region1.getOffset() < region2.getOffset()) { + return region1; + } else { + return region2; + } + } + } + + matcher = sRepeatedRangePattern.matcher(message); + if (matcher.find()) { + String property = matcher.group(2); + return findRange(file, line, property, null); + } + + matcher = sNoResourcePattern.matcher(message); + if (matcher.find()) { + String property = matcher.group(1); + return findRange(file, line, property, null); + } + + matcher = sRequiredPattern.matcher(message); + if (matcher.find()) { + String elementName = matcher.group(2); + IRegion region = findRange(file, line, '<' + elementName, null); + if (region != null && region.getLength() > 1) { + // Skip the opening < + region = new Region(region.getOffset() + 1, region.getLength() - 1); + } + return region; + } + + if (message.endsWith(ORIGINALLY_DEFINED_MSG)) { + return findLineTextRange(file, line); + } + + return null; + } + + /** + * Given a file and line number, return the range of the first match starting on the + * given line. If second is non null, also search for the second string starting at he + * location of the first string. + */ + private static IRegion findRange(IFile file, int line, String first, + String second) { + IRegion region = null; + IDocumentProvider provider = new TextFileDocumentProvider(); + try { + provider.connect(file); + IDocument document = provider.getDocument(file); + if (document != null) { + IRegion lineInfo = document.getLineInformation(line - 1); + int lineStartOffset = lineInfo.getOffset(); + // The aapt errors will be anchored on the line where the + // element starts - which means that with formatting where + // attributes end up on subsequent lines we don't find it on + // the error line indicated by aapt. + // Therefore, search forwards in the document. + FindReplaceDocumentAdapter adapter = + new FindReplaceDocumentAdapter(document); + + region = adapter.find(lineStartOffset, first, + true /*forwardSearch*/, true /*caseSensitive*/, + false /*wholeWord*/, false /*regExSearch*/); + if (region != null && second != null) { + region = adapter.find(region.getOffset() + first.length(), second, + true /*forwardSearch*/, true /*caseSensitive*/, + false /*wholeWord*/, false /*regExSearch*/); + } + } + } catch (Exception e) { + AdtPlugin.log(e, "Can't find range information for %1$s", file.getName()); + } finally { + provider.disconnect(file); + } + return region; + } + + /** Returns the non-whitespace line range at the given line number. */ + private static IRegion findLineTextRange(IFile file, int line) { + IDocumentProvider provider = new TextFileDocumentProvider(); + try { + provider.connect(file); + IDocument document = provider.getDocument(file); + if (document != null) { + IRegion lineInfo = document.getLineInformation(line - 1); + String lineContents = document.get(lineInfo.getOffset(), lineInfo.getLength()); + int lineBegin = 0; + int lineEnd = lineContents.length()-1; + + for (; lineEnd >= 0; lineEnd--) { + char c = lineContents.charAt(lineEnd); + if (!Character.isWhitespace(c)) { + break; + } + } + lineEnd++; + for (; lineBegin < lineEnd; lineBegin++) { + char c = lineContents.charAt(lineBegin); + if (!Character.isWhitespace(c)) { + break; + } + } + if (lineBegin < lineEnd) { + return new Region(lineInfo.getOffset() + lineBegin, lineEnd - lineBegin); + } + } + } catch (Exception e) { + AdtPlugin.log(e, "Can't find range information for %1$s", file.getName()); + } finally { + provider.disconnect(file); + } + + return null; + } + + /** + * Returns a matching matcher for the next line + * @param lines The array of lines + * @param nextIndex The index of the next line + * @param pattern The pattern to match + * @return null if error or no match, the matcher otherwise. + */ + private static final Matcher getNextLineMatcher(String[] lines, + int nextIndex, Pattern pattern) { + // unless we can't, because we reached the last line + if (nextIndex == lines.length) { + // we expected a 2nd line, so we flag as error + // and we bail + return null; + } + + Matcher m = pattern.matcher(lines[nextIndex]); + if (m.matches()) { + return m; + } + + return null; + } + + private static IResource getResourceFromFullPath(String filename, String root, + IProject project) { + if (filename.startsWith(root)) { + String file = filename.substring(root.length()); + + // get the resource + IResource r = project.findMember(file); + + // if the resource is valid, we add the marker + if (r != null && r.exists()) { + return r; + } + } + + return null; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptQuickFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptQuickFix.java new file mode 100644 index 000000000..3db380832 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptQuickFix.java @@ -0,0 +1,431 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build; + +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.XMLNS_ANDROID; +import static com.android.SdkConstants.XMLNS_URI; + +import com.android.ide.common.resources.ResourceUrl; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; +import com.android.resources.ResourceType; +import com.android.utils.Pair; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.Region; +import org.eclipse.jface.text.contentassist.ICompletionProposal; +import org.eclipse.jface.text.contentassist.IContextInformation; +import org.eclipse.jface.text.quickassist.IQuickAssistInvocationContext; +import org.eclipse.jface.text.quickassist.IQuickAssistProcessor; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.ISourceViewer; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.ui.IMarkerResolution; +import org.eclipse.ui.IMarkerResolution2; +import org.eclipse.ui.IMarkerResolutionGenerator2; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.editors.text.TextFileDocumentProvider; +import org.eclipse.ui.texteditor.IDocumentProvider; +import org.eclipse.wst.sse.core.StructuredModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; +import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.List; + +/** + * Shared handler for both quick assist processors (Control key handler) and quick fix + * marker resolution (Problem view handling), since there is a lot of overlap between + * these two UI handlers. + */ +@SuppressWarnings("restriction") // XML model +public class AaptQuickFix implements IMarkerResolutionGenerator2, IQuickAssistProcessor { + + public AaptQuickFix() { + } + + /** Returns the error message from aapt that signals missing resources */ + private static String getTargetMarkerErrorMessage() { + return "No resource found that matches the given name"; + } + + /** Returns the error message from aapt that signals a missing namespace declaration */ + private static String getUnboundErrorMessage() { + return "Error parsing XML: unbound prefix"; + } + + // ---- Implements IMarkerResolution2 ---- + + @Override + public boolean hasResolutions(IMarker marker) { + String message = null; + try { + message = (String) marker.getAttribute(IMarker.MESSAGE); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + return message != null + && (message.contains(getTargetMarkerErrorMessage()) + || message.contains(getUnboundErrorMessage())); + } + + @Override + public IMarkerResolution[] getResolutions(IMarker marker) { + IResource markerResource = marker.getResource(); + IProject project = markerResource.getProject(); + try { + String message = (String) marker.getAttribute(IMarker.MESSAGE); + if (message.contains(getUnboundErrorMessage()) && markerResource instanceof IFile) { + return new IMarkerResolution[] { + new CreateNamespaceFix((IFile) markerResource) + }; + } + } catch (CoreException e1) { + AdtPlugin.log(e1, null); + } + + int start = marker.getAttribute(IMarker.CHAR_START, 0); + int end = marker.getAttribute(IMarker.CHAR_END, 0); + if (end > start) { + int length = end - start; + IDocumentProvider provider = new TextFileDocumentProvider(); + try { + provider.connect(markerResource); + IDocument document = provider.getDocument(markerResource); + String resource = document.get(start, length); + if (ResourceHelper.canCreateResource(resource)) { + return new IMarkerResolution[] { + new CreateResourceProposal(project, resource) + }; + } + } catch (Exception e) { + AdtPlugin.log(e, "Can't find range information for %1$s", markerResource); + } finally { + provider.disconnect(markerResource); + } + } + + return null; + } + + // ---- Implements IQuickAssistProcessor ---- + + @Override + public boolean canAssist(IQuickAssistInvocationContext invocationContext) { + return true; + } + + @Override + public boolean canFix(Annotation annotation) { + return true; + } + + @Override + public ICompletionProposal[] computeQuickAssistProposals( + IQuickAssistInvocationContext invocationContext) { + + // We have to find the corresponding project/file (so we can look up the aapt + // error markers). Unfortunately, an IQuickAssistProcessor only gets + // access to an ISourceViewer which has no hooks back to the surrounding + // editor. + // + // However, the IQuickAssistProcessor will only be used interactively by a file + // being edited, so we can cheat like the hyperlink detector and simply + // look up the currently active file in the IDE. To be on the safe side, + // we'll make sure that that editor has the same sourceViewer such that + // we are indeed looking at the right file: + ISourceViewer sourceViewer = invocationContext.getSourceViewer(); + AndroidXmlEditor editor = AndroidXmlEditor.fromTextViewer(sourceViewer); + if (editor != null) { + IFile file = editor.getInputFile(); + if (file == null) { + return null; + } + IDocument document = sourceViewer.getDocument(); + List<IMarker> markers = AdtUtils.findMarkersOnLine(AdtConstants.MARKER_AAPT_COMPILE, + file, document, invocationContext.getOffset()); + try { + for (IMarker marker : markers) { + String message = marker.getAttribute(IMarker.MESSAGE, ""); //$NON-NLS-1$ + if (message.contains(getTargetMarkerErrorMessage())) { + int start = marker.getAttribute(IMarker.CHAR_START, 0); + int end = marker.getAttribute(IMarker.CHAR_END, 0); + int length = end - start; + String resource = document.get(start, length); + // Can only offer create value for non-framework value + // resources + if (ResourceHelper.canCreateResource(resource)) { + IProject project = editor.getProject(); + return new ICompletionProposal[] { + new CreateResourceProposal(project, resource) + }; + } + } else if (message.contains(getUnboundErrorMessage())) { + return new ICompletionProposal[] { + new CreateNamespaceFix(null) + }; + } + } + } catch (BadLocationException e) { + AdtPlugin.log(e, null); + } + } + + return null; + } + + @Override + public String getErrorMessage() { + return null; + } + + /** Quick fix to insert namespace binding when missing */ + private final static class CreateNamespaceFix + implements ICompletionProposal, IMarkerResolution2 { + private IFile mFile; + + public CreateNamespaceFix(IFile file) { + mFile = file; + } + + private IndexedRegion perform(IDocument doc) { + IModelManager manager = StructuredModelManager.getModelManager(); + IStructuredModel model = manager.getExistingModelForEdit(doc); + if (model != null) { + try { + perform(model); + } finally { + model.releaseFromEdit(); + } + } + + return null; + } + + private IndexedRegion perform(IFile file) { + IModelManager manager = StructuredModelManager.getModelManager(); + IStructuredModel model; + try { + model = manager.getModelForEdit(file); + if (model != null) { + try { + perform(model); + } finally { + model.releaseFromEdit(); + } + } + } catch (Exception e) { + AdtPlugin.log(e, "Can't look up XML model"); + } + + return null; + } + + private IndexedRegion perform(IStructuredModel model) { + if (model instanceof IDOMModel) { + IDOMModel domModel = (IDOMModel) model; + Document document = domModel.getDocument(); + Element element = document.getDocumentElement(); + Attr attr = document.createAttributeNS(XMLNS_URI, XMLNS_ANDROID); + attr.setValue(ANDROID_URI); + element.getAttributes().setNamedItemNS(attr); + return (IndexedRegion) attr; + } + + return null; + } + + // ---- Implements ICompletionProposal ---- + + @Override + public void apply(IDocument document) { + perform(document); + } + + @Override + public String getAdditionalProposalInfo() { + return "Adds an Android namespace declaratiopn to the root element."; + } + + @Override + public IContextInformation getContextInformation() { + return null; + } + + @Override + public String getDisplayString() { + return "Insert namespace binding"; + } + + @Override + public Image getImage() { + return AdtPlugin.getAndroidLogo(); + } + + @Override + public Point getSelection(IDocument doc) { + return null; + } + + + // ---- Implements MarkerResolution2 ---- + + @Override + public String getLabel() { + return getDisplayString(); + } + + @Override + public void run(IMarker marker) { + try { + AdtPlugin.openFile(mFile, null); + } catch (PartInitException e) { + AdtPlugin.log(e, "Can't open file %1$s", mFile.getName()); + } + + IndexedRegion indexedRegion = perform(mFile); + if (indexedRegion != null) { + try { + IRegion region = + new Region(indexedRegion.getStartOffset(), indexedRegion.getLength()); + AdtPlugin.openFile(mFile, region); + } catch (PartInitException e) { + AdtPlugin.log(e, "Can't open file %1$s", mFile.getName()); + } + } + } + + @Override + public String getDescription() { + return getAdditionalProposalInfo(); + } + } + + private static class CreateResourceProposal + implements ICompletionProposal, IMarkerResolution2 { + private final IProject mProject; + private final String mResource; + + CreateResourceProposal(IProject project, String resource) { + super(); + mProject = project; + mResource = resource; + } + + private void perform() { + ResourceUrl resource = ResourceUrl.parse(mResource); + if (resource == null) { + return; + } + ResourceType type = resource.type; + String name = resource.name; + assert !resource.framework; + String value = ""; //$NON-NLS-1$ + + // Try to pick a reasonable first guess. The new value will be highlighted and + // selected for editing, but if we have an initial value then the new file + // won't show an error. + switch (type) { + case STRING: value = "TODO"; break; //$NON-NLS-1$ + case DIMEN: value = "1dp"; break; //$NON-NLS-1$ + case BOOL: value = "true"; break; //$NON-NLS-1$ + case COLOR: value = "#000000"; break; //$NON-NLS-1$ + case INTEGER: value = "1"; break; //$NON-NLS-1$ + case ARRAY: value = "<item>1</item>"; break; //$NON-NLS-1$ + } + + Pair<IFile, IRegion> location = + ResourceHelper.createResource(mProject, type, name, value); + if (location != null) { + IFile file = location.getFirst(); + IRegion region = location.getSecond(); + try { + AdtPlugin.openFile(file, region); + } catch (PartInitException e) { + AdtPlugin.log(e, "Can't open file %1$s", file.getName()); + } + } + } + + // ---- Implements ICompletionProposal ---- + + @Override + public void apply(IDocument document) { + perform(); + } + + @Override + public String getAdditionalProposalInfo() { + return "Creates an XML file entry for the given missing resource " + + "and opens it in the editor."; + } + + @Override + public IContextInformation getContextInformation() { + return null; + } + + @Override + public String getDisplayString() { + return String.format("Create resource %1$s", mResource); + } + + @Override + public Image getImage() { + return AdtPlugin.getAndroidLogo(); + } + + @Override + public Point getSelection(IDocument document) { + return null; + } + + // ---- Implements MarkerResolution2 ---- + + @Override + public String getLabel() { + return getDisplayString(); + } + + @Override + public void run(IMarker marker) { + perform(); + } + + @Override + public String getDescription() { + return getAdditionalProposalInfo(); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptResultException.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptResultException.java new file mode 100644 index 000000000..8d8fe683f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptResultException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build; + +/** + * Exception thrown when aapt reports an error in the resources. + * + */ +public final class AaptResultException extends ExecResultException { + private static final long serialVersionUID = 1L; + + AaptResultException(int errorCode, String[] output) { + super(errorCode, output); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AidlProcessor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AidlProcessor.java new file mode 100644 index 000000000..806fa9c40 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AidlProcessor.java @@ -0,0 +1,484 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.build.builders.BaseBuilder; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.sdklib.BuildToolInfo; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.io.FileOp; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.SubProgressMonitor; +import org.eclipse.jdt.core.IJavaProject; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A {@link SourceProcessor} for aidl files. + * + */ +public class AidlProcessor extends SourceProcessor { + + private static final String PROPERTY_COMPILE_AIDL = "compileAidl"; //$NON-NLS-1$ + + /** + * Single line aidl error<br> + * {@code <path>:<line>: <error>}<br> + * or<br> + * {@code <path>:<line> <error>}<br> + */ + private static Pattern sAidlPattern1 = Pattern.compile("^(.+?):(\\d+):?\\s(.+)$"); //$NON-NLS-1$ + + private final static Set<String> EXTENSIONS = Collections.singleton(SdkConstants.EXT_AIDL); + + private enum AidlType { + UNKNOWN, INTERFACE, PARCELABLE; + } + + // See comment in #getAidlType() +// private final static Pattern sParcelablePattern = Pattern.compile( +// "^\\s*parcelable\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*;\\s*$"); +// +// private final static Pattern sInterfacePattern = Pattern.compile( +// "^\\s*interface\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?:\\{.*)?$"); + + + public AidlProcessor(@NonNull IJavaProject javaProject, @NonNull BuildToolInfo buildToolInfo, + @NonNull IFolder genFolder) { + super(javaProject, buildToolInfo, genFolder); + } + + @Override + protected Set<String> getExtensions() { + return EXTENSIONS; + } + + @Override + protected String getSavePropertyName() { + return PROPERTY_COMPILE_AIDL; + } + + @Override + protected void doCompileFiles(List<IFile> sources, BaseBuilder builder, + IProject project, IAndroidTarget projectTarget, + List<IPath> sourceFolders, List<IFile> notCompiledOut, List<File> libraryProjectsOut, + IProgressMonitor monitor) throws CoreException { + // create the command line + List<String> commandList = new ArrayList<String>( + 4 + sourceFolders.size() + libraryProjectsOut.size()); + commandList.add(getBuildToolInfo().getPath(BuildToolInfo.PathId.AIDL)); + commandList.add(quote("-p" + projectTarget.getPath(IAndroidTarget.ANDROID_AIDL))); //$NON-NLS-1$ + + // since the path are relative to the workspace and not the project itself, we need + // the workspace root. + IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); + for (IPath p : sourceFolders) { + IFolder f = wsRoot.getFolder(p); + if (f.exists()) { // if the resource doesn't exist, getLocation will return null. + commandList.add(quote("-I" + f.getLocation().toOSString())); //$NON-NLS-1$ + } + } + + for (File libOut : libraryProjectsOut) { + // FIXME: make folder configurable + File aidlFile = new File(libOut, SdkConstants.FD_AIDL); + if (aidlFile.isDirectory()) { + commandList.add(quote("-I" + aidlFile.getAbsolutePath())); //$NON-NLS-1$ + } + } + + // convert to array with 2 extra strings for the in/out file paths. + int index = commandList.size(); + String[] commands = commandList.toArray(new String[index + 2]); + + boolean verbose = AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE; + + // remove the generic marker from the project + builder.removeMarkersFromResource(project, AdtConstants.MARKER_AIDL); + + // prepare the two output folders. + IFolder genFolder = getGenFolder(); + IFolder projectOut = BaseProjectHelper.getAndroidOutputFolder(project); + IFolder aidlOutFolder = projectOut.getFolder(SdkConstants.FD_AIDL); + if (aidlOutFolder.exists() == false) { + aidlOutFolder.create(true /*force*/, true /*local*/, + new SubProgressMonitor(monitor, 10)); + } + + boolean success = false; + + // loop until we've compile them all + for (IFile sourceFile : sources) { + if (verbose) { + String name = sourceFile.getName(); + IPath sourceFolderPath = getSourceFolderFor(sourceFile); + if (sourceFolderPath != null) { + // make a path to the source file relative to the source folder. + IPath relative = sourceFile.getFullPath().makeRelativeTo(sourceFolderPath); + name = relative.toString(); + } + AdtPlugin.printToConsole(project, "AIDL: " + name); + } + + // Remove the AIDL error markers from the aidl file + builder.removeMarkersFromResource(sourceFile, AdtConstants.MARKER_AIDL); + + // get the path of the source file. + IPath sourcePath = sourceFile.getLocation(); + String osSourcePath = sourcePath.toOSString(); + + // look if we already know the output + SourceFileData data = getFileData(sourceFile); + if (data == null) { + data = new SourceFileData(sourceFile); + addData(data); + } + + // if there's no output file yet, compute it. + if (data.getOutput() == null) { + IFile javaFile = getAidlOutputFile(sourceFile, genFolder, + true /*replaceExt*/, true /*createFolders*/, monitor); + data.setOutputFile(javaFile); + } + + // finish to set the command line. + commands[index] = quote(osSourcePath); + commands[index + 1] = quote(data.getOutput().getLocation().toOSString()); + + // launch the process + if (execAidl(builder, project, commands, sourceFile, verbose) == false) { + // aidl failed. File should be marked. We add the file to the list + // of file that will need compilation again. + notCompiledOut.add(sourceFile); + } else { + // Success. we'll return that we generated code + setCompilationStatus(COMPILE_STATUS_CODE); + success = true; + + // Also copy the file to the bin folder. + IFile aidlOutFile = getAidlOutputFile(sourceFile, aidlOutFolder, + false /*replaceExt*/, true /*createFolders*/, monitor); + + FileOp op = new FileOp(); + try { + op.copyFile(sourceFile.getLocation().toFile(), + aidlOutFile.getLocation().toFile()); + } catch (IOException e) { + } + } + } + + if (success) { + aidlOutFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor); + } + } + + @Override + protected void loadOutputAndDependencies() { + IProgressMonitor monitor = new NullProgressMonitor(); + IFolder genFolder = getGenFolder(); + + Collection<SourceFileData> dataList = getAllFileData(); + for (SourceFileData data : dataList) { + try { + IFile javaFile = getAidlOutputFile(data.getSourceFile(), genFolder, + true /*replaceExt*/, false /*createFolders*/, monitor); + data.setOutputFile(javaFile); + } catch (CoreException e) { + // ignore, we're not asking to create the folder so this won't happen anyway. + } + + } + } + + /** + * Execute the aidl command line, parse the output, and mark the aidl file + * with any reported errors. + * @param command the String array containing the command line to execute. + * @param file The IFile object representing the aidl file being + * compiled. + * @param verbose the build verbosity + * @return false if the exec failed, and build needs to be aborted. + */ + private boolean execAidl(BaseBuilder builder, IProject project, String[] command, IFile file, + boolean verbose) { + // do the exec + try { + if (verbose) { + StringBuilder sb = new StringBuilder(); + for (String c : command) { + sb.append(c); + sb.append(' '); + } + String cmd_line = sb.toString(); + AdtPlugin.printToConsole(project, cmd_line); + } + + Process p = Runtime.getRuntime().exec(command); + + // list to store each line of stderr + ArrayList<String> stdErr = new ArrayList<String>(); + + // get the output and return code from the process + int returnCode = BuildHelper.grabProcessOutput(project, p, stdErr); + + if (stdErr.size() > 0) { + // attempt to parse the error output + boolean parsingError = parseAidlOutput(stdErr, file); + + // If the process failed and we couldn't parse the output + // we print a message, mark the project and exit + if (returnCode != 0) { + + if (parsingError || verbose) { + // display the message in the console. + if (parsingError) { + AdtPlugin.printErrorToConsole(project, stdErr.toArray()); + + // mark the project + BaseProjectHelper.markResource(project, AdtConstants.MARKER_AIDL, + Messages.Unparsed_AIDL_Errors, IMarker.SEVERITY_ERROR); + } else { + AdtPlugin.printToConsole(project, stdErr.toArray()); + } + } + return false; + } + } else if (returnCode != 0) { + // no stderr output but exec failed. + String msg = String.format(Messages.AIDL_Exec_Error_d, returnCode); + + BaseProjectHelper.markResource(project, AdtConstants.MARKER_AIDL, + msg, IMarker.SEVERITY_ERROR); + + return false; + } + } catch (IOException e) { + // mark the project and exit + String msg = String.format(Messages.AIDL_Exec_Error_s, command[0]); + BaseProjectHelper.markResource(project, AdtConstants.MARKER_AIDL, msg, + IMarker.SEVERITY_ERROR); + return false; + } catch (InterruptedException e) { + // mark the project and exit + String msg = String.format(Messages.AIDL_Exec_Error_s, command[0]); + BaseProjectHelper.markResource(project, AdtConstants.MARKER_AIDL, msg, + IMarker.SEVERITY_ERROR); + return false; + } + + return true; + } + + /** + * Parse the output of aidl and mark the file with any errors. + * @param lines The output to parse. + * @param file The file to mark with error. + * @return true if the parsing failed, false if success. + */ + private boolean parseAidlOutput(ArrayList<String> lines, IFile file) { + // nothing to parse? just return false; + if (lines.size() == 0) { + return false; + } + + Matcher m; + + for (int i = 0; i < lines.size(); i++) { + String p = lines.get(i); + + m = sAidlPattern1.matcher(p); + if (m.matches()) { + // we can ignore group 1 which is the location since we already + // have a IFile object representing the aidl file. + String lineStr = m.group(2); + String msg = m.group(3); + + // get the line number + int line = 0; + try { + line = Integer.parseInt(lineStr); + } catch (NumberFormatException e) { + // looks like the string we extracted wasn't a valid + // file number. Parsing failed and we return true + return true; + } + + // mark the file + BaseProjectHelper.markResource(file, AdtConstants.MARKER_AIDL, msg, line, + IMarker.SEVERITY_ERROR); + + // success, go to the next line + continue; + } + + // invalid line format, flag as error, and bail + return true; + } + + return false; + } + + /** + * Returns the {@link IFile} handle to the destination file for a given aidl source file + * ({@link AidlData}). + * @param sourceFile The source file + * @param outputFolder the top level output folder (not including the package folders) + * @param createFolders whether or not the parent folder of the destination should be created + * if it does not exist. + * @param monitor the progress monitor + * @return the handle to the destination file. + * @throws CoreException + */ + private IFile getAidlOutputFile(IFile sourceFile, IFolder outputFolder, boolean replaceExt, + boolean createFolders, IProgressMonitor monitor) throws CoreException { + + IPath sourceFolderPath = getSourceFolderFor(sourceFile); + + // this really shouldn't happen since the sourceFile must be in a source folder + // since it comes from the delta visitor + if (sourceFolderPath != null) { + // make a path to the source file relative to the source folder. + IPath relative = sourceFile.getFullPath().makeRelativeTo(sourceFolderPath); + // remove the file name. This is now the destination folder. + relative = relative.removeLastSegments(1); + + // get an IFolder for this path. + IFolder destinationFolder = outputFolder.getFolder(relative); + + // create it if needed. + if (destinationFolder.exists() == false && createFolders) { + createFolder(destinationFolder, monitor); + } + + // Build the Java file name from the aidl name. + String javaName; + if (replaceExt) { + javaName = sourceFile.getName().replaceAll( + AdtConstants.RE_AIDL_EXT, SdkConstants.DOT_JAVA); + } else { + javaName = sourceFile.getName(); + } + + // get the resource for the java file. + IFile javaFile = destinationFolder.getFile(javaName); + return javaFile; + } + + return null; + } + + /** + * Creates the destination folder. Because + * {@link IFolder#create(boolean, boolean, IProgressMonitor)} only works if the parent folder + * already exists, this goes and ensure that all the parent folders actually exist, or it + * creates them as well. + * @param destinationFolder The folder to create + * @param monitor the {@link IProgressMonitor}, + * @throws CoreException + */ + private void createFolder(IFolder destinationFolder, IProgressMonitor monitor) + throws CoreException { + + // check the parent exist and create if necessary. + IContainer parent = destinationFolder.getParent(); + if (parent.getType() == IResource.FOLDER && parent.exists() == false) { + createFolder((IFolder)parent, monitor); + } + + // create the folder. + destinationFolder.create(true /*force*/, true /*local*/, + new SubProgressMonitor(monitor, 10)); + } + + /** + * Returns the type of the aidl file. Aidl files can either declare interfaces, or declare + * parcelables. This method will attempt to parse the file and return the type. If the type + * cannot be determined, then it will return {@link AidlType#UNKNOWN}. + * @param file The aidl file + * @return the type of the aidl. + */ + private static AidlType getAidlType(IFile file) { + // At this time, parsing isn't available, so we return UNKNOWN. This will force + // a recompilation of all aidl file as soon as one is changed. + return AidlType.UNKNOWN; + + // TODO: properly parse aidl file to determine type and generate dependency graphs. +// +// String className = file.getName().substring(0, +// file.getName().length() - SdkConstants.DOT_AIDL.length()); +// +// InputStream input = file.getContents(true /* force*/); +// try { +// BufferedReader reader = new BufferedReader(new InputStreateader(input)); +// String line; +// while ((line = reader.readLine()) != null) { +// if (line.length() == 0) { +// continue; +// } +// +// Matcher m = sParcelablePattern.matcher(line); +// if (m.matches() && m.group(1).equals(className)) { +// return AidlType.PARCELABLE; +// } +// +// m = sInterfacePattern.matcher(line); +// if (m.matches() && m.group(1).equals(className)) { +// return AidlType.INTERFACE; +// } +// } +// } catch (IOException e) { +// throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, +// "Error parsing aidl file", e)); +// } finally { +// try { +// input.close(); +// } catch (IOException e) { +// throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, +// "Error parsing aidl file", e)); +// } +// } +// +// return AidlType.UNKNOWN; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BuildHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BuildHelper.java new file mode 100644 index 000000000..78d9d94e4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BuildHelper.java @@ -0,0 +1,1225 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AndroidPrintStream; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.BuildToolInfo; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.IAndroidTarget.IOptionalLibrary; +import com.android.sdklib.build.ApkBuilder; +import com.android.sdklib.build.ApkBuilder.JarStatus; +import com.android.sdklib.build.ApkBuilder.SigningInfo; +import com.android.sdklib.build.ApkCreationException; +import com.android.sdklib.build.DuplicateFileException; +import com.android.sdklib.build.RenderScriptProcessor; +import com.android.sdklib.build.SealedApkException; +import com.android.sdklib.internal.build.DebugKeyProvider; +import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException; +import com.android.utils.GrabProcessOutput; +import com.android.utils.GrabProcessOutput.IProcessOutput; +import com.android.utils.GrabProcessOutput.Wait; +import com.google.common.base.Charsets; +import com.google.common.hash.HashCode; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jdt.core.IClasspathContainer; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jface.preference.IPreferenceStore; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintStream; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * Helper with methods for the last 3 steps of the generation of an APK. + * + * {@link #packageResources(IFile, IProject[], String, int, String, String)} packages the + * application resources using aapt into a zip file that is ready to be integrated into the apk. + * + * {@link #executeDx(IJavaProject, String, String, IJavaProject[])} will convert the Java byte + * code into the Dalvik bytecode. + * + * {@link #finalPackage(String, String, String, boolean, IJavaProject, IProject[], IJavaProject[], String, boolean)} + * will make the apk from all the previous components. + * + * This class only executes the 3 above actions. It does not handle the errors, and simply sends + * them back as custom exceptions. + * + * Warnings are handled by the {@link ResourceMarker} interface. + * + * Console output (verbose and non verbose) is handled through the {@link AndroidPrintStream} passed + * to the constructor. + * + */ +public class BuildHelper { + + private static final String CONSOLE_PREFIX_DX = "Dx"; //$NON-NLS-1$ + private final static String TEMP_PREFIX = "android_"; //$NON-NLS-1$ + + private static final String COMMAND_CRUNCH = "crunch"; //$NON-NLS-1$ + private static final String COMMAND_PACKAGE = "package"; //$NON-NLS-1$ + + @NonNull + private final ProjectState mProjectState; + @NonNull + private final IProject mProject; + @NonNull + private final BuildToolInfo mBuildToolInfo; + @NonNull + private final AndroidPrintStream mOutStream; + @NonNull + private final AndroidPrintStream mErrStream; + private final boolean mForceJumbo; + private final boolean mDisableDexMerger; + private final boolean mVerbose; + private final boolean mDebugMode; + + private final Set<String> mCompiledCodePaths = new HashSet<String>(); + + public static final boolean BENCHMARK_FLAG = false; + public static long sStartOverallTime = 0; + public static long sStartJavaCTime = 0; + + private final static int MILLION = 1000000; + private String mProguardFile; + + /** + * An object able to put a marker on a resource. + */ + public interface ResourceMarker { + void setWarning(IResource resource, String message); + } + + /** + * Creates a new post-compiler helper + * @param project + * @param outStream + * @param errStream + * @param debugMode whether this is a debug build + * @param verbose + * @throws CoreException + */ + public BuildHelper(@NonNull ProjectState projectState, + @NonNull BuildToolInfo buildToolInfo, + @NonNull AndroidPrintStream outStream, + @NonNull AndroidPrintStream errStream, + boolean forceJumbo, boolean disableDexMerger, boolean debugMode, + boolean verbose, ResourceMarker resMarker) throws CoreException { + mProjectState = projectState; + mProject = projectState.getProject(); + mBuildToolInfo = buildToolInfo; + mOutStream = outStream; + mErrStream = errStream; + mDebugMode = debugMode; + mVerbose = verbose; + mForceJumbo = forceJumbo; + mDisableDexMerger = disableDexMerger; + + gatherPaths(resMarker); + } + + public void updateCrunchCache() throws AaptExecException, AaptResultException { + // Benchmarking start + long startCrunchTime = 0; + if (BENCHMARK_FLAG) { + String msg = "BENCHMARK ADT: Starting Initial Packaging (.ap_)"; //$NON-NLS-1$ + AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg); + startCrunchTime = System.nanoTime(); + } + + // Get the resources folder to crunch from + IFolder resFolder = mProject.getFolder(AdtConstants.WS_RESOURCES); + List<String> resPaths = new ArrayList<String>(); + resPaths.add(resFolder.getLocation().toOSString()); + + // Get the output folder where the cache is stored. + IFolder binFolder = BaseProjectHelper.getAndroidOutputFolder(mProject); + IFolder cacheFolder = binFolder.getFolder(AdtConstants.WS_BIN_RELATIVE_CRUNCHCACHE); + String cachePath = cacheFolder.getLocation().toOSString(); + + /* For crunching, we don't need the osManifestPath, osAssetsPath, or the configFilter + * parameters for executeAapt + */ + executeAapt(COMMAND_CRUNCH, "", resPaths, "", cachePath, "", 0); + + // Benchmarking end + if (BENCHMARK_FLAG) { + String msg = "BENCHMARK ADT: Ending Initial Package (.ap_). \nTime Elapsed: " //$NON-NLS-1$ + + ((System.nanoTime() - startCrunchTime)/MILLION) + "ms"; //$NON-NLS-1$ + AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg); + } + } + + /** + * Packages the resources of the projet into a .ap_ file. + * @param manifestFile the manifest of the project. + * @param libProjects the list of library projects that this project depends on. + * @param resFilter an optional resource filter to be used with the -c option of aapt. If null + * no filters are used. + * @param versionCode an optional versionCode to be inserted in the manifest during packaging. + * If the value is <=0, no values are inserted. + * @param outputFolder where to write the resource ap_ file. + * @param outputFilename the name of the resource ap_ file. + * @throws AaptExecException + * @throws AaptResultException + */ + public void packageResources(IFile manifestFile, List<IProject> libProjects, String resFilter, + int versionCode, String outputFolder, String outputFilename) + throws AaptExecException, AaptResultException { + + // Benchmarking start + long startPackageTime = 0; + if (BENCHMARK_FLAG) { + String msg = "BENCHMARK ADT: Starting Initial Packaging (.ap_)"; //$NON-NLS-1$ + AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg); + startPackageTime = System.nanoTime(); + } + + // need to figure out some path before we can execute aapt; + IFolder binFolder = BaseProjectHelper.getAndroidOutputFolder(mProject); + + // get the cache folder + IFolder cacheFolder = binFolder.getFolder(AdtConstants.WS_BIN_RELATIVE_CRUNCHCACHE); + + // get the BC folder + IFolder bcFolder = binFolder.getFolder(AdtConstants.WS_BIN_RELATIVE_BC); + + // get the resource folder + IFolder resFolder = mProject.getFolder(AdtConstants.WS_RESOURCES); + + // and the assets folder + IFolder assetsFolder = mProject.getFolder(AdtConstants.WS_ASSETS); + + // we need to make sure this one exists. + if (assetsFolder.exists() == false) { + assetsFolder = null; + } + + // list of res folder (main project + maybe libraries) + ArrayList<String> osResPaths = new ArrayList<String>(); + + IPath resLocation = resFolder.getLocation(); + IPath manifestLocation = manifestFile.getLocation(); + + if (resLocation != null && manifestLocation != null) { + + // png cache folder first. + addFolderToList(osResPaths, cacheFolder); + addFolderToList(osResPaths, bcFolder); + + // regular res folder next. + osResPaths.add(resLocation.toOSString()); + + // then libraries + if (libProjects != null) { + for (IProject lib : libProjects) { + // png cache folder first + IFolder libBinFolder = BaseProjectHelper.getAndroidOutputFolder(lib); + + IFolder libCacheFolder = libBinFolder.getFolder(AdtConstants.WS_BIN_RELATIVE_CRUNCHCACHE); + addFolderToList(osResPaths, libCacheFolder); + + IFolder libBcFolder = libBinFolder.getFolder(AdtConstants.WS_BIN_RELATIVE_BC); + addFolderToList(osResPaths, libBcFolder); + + // regular res folder next. + IFolder libResFolder = lib.getFolder(AdtConstants.WS_RESOURCES); + addFolderToList(osResPaths, libResFolder); + } + } + + String osManifestPath = manifestLocation.toOSString(); + + String osAssetsPath = null; + if (assetsFolder != null) { + osAssetsPath = assetsFolder.getLocation().toOSString(); + } + + // build the default resource package + executeAapt(COMMAND_PACKAGE, osManifestPath, osResPaths, osAssetsPath, + outputFolder + File.separator + outputFilename, resFilter, + versionCode); + } + + // Benchmarking end + if (BENCHMARK_FLAG) { + String msg = "BENCHMARK ADT: Ending Initial Package (.ap_). \nTime Elapsed: " //$NON-NLS-1$ + + ((System.nanoTime() - startPackageTime)/MILLION) + "ms"; //$NON-NLS-1$ + AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg); + } + } + + /** + * Adds os path of a folder to a list only if the folder actually exists. + * @param pathList + * @param folder + */ + private void addFolderToList(List<String> pathList, IFolder folder) { + // use a File instead of the IFolder API to ignore workspace refresh issue. + File testFile = new File(folder.getLocation().toOSString()); + if (testFile.isDirectory()) { + pathList.add(testFile.getAbsolutePath()); + } + } + + /** + * Makes a final package signed with the debug key. + * + * Packages the dex files, the temporary resource file into the final package file. + * + * Whether the package is a debug package is controlled with the <var>debugMode</var> parameter + * in {@link #PostCompilerHelper(IProject, PrintStream, PrintStream, boolean, boolean)} + * + * @param intermediateApk The path to the temporary resource file. + * @param dex The path to the dex file. + * @param output The path to the final package file to create. + * @param libProjects an optional list of library projects (can be null) + * @return true if success, false otherwise. + * @throws ApkCreationException + * @throws AndroidLocationException + * @throws KeytoolException + * @throws NativeLibInJarException + * @throws CoreException + * @throws DuplicateFileException + */ + public void finalDebugPackage(String intermediateApk, String dex, String output, + List<IProject> libProjects, ResourceMarker resMarker) + throws ApkCreationException, KeytoolException, AndroidLocationException, + NativeLibInJarException, DuplicateFileException, CoreException { + + AdtPlugin adt = AdtPlugin.getDefault(); + if (adt == null) { + return; + } + + // get the debug keystore to use. + IPreferenceStore store = adt.getPreferenceStore(); + String keystoreOsPath = store.getString(AdtPrefs.PREFS_CUSTOM_DEBUG_KEYSTORE); + if (keystoreOsPath == null || new File(keystoreOsPath).isFile() == false) { + keystoreOsPath = DebugKeyProvider.getDefaultKeyStoreOsPath(); + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, mProject, + Messages.ApkBuilder_Using_Default_Key); + } else { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, mProject, + String.format(Messages.ApkBuilder_Using_s_To_Sign, keystoreOsPath)); + } + + // from the keystore, get the signing info + SigningInfo info = ApkBuilder.getDebugKey(keystoreOsPath, mVerbose ? mOutStream : null); + + finalPackage(intermediateApk, dex, output, libProjects, + info != null ? info.key : null, info != null ? info.certificate : null, resMarker); + } + + /** + * Makes the final package. + * + * Packages the dex files, the temporary resource file into the final package file. + * + * Whether the package is a debug package is controlled with the <var>debugMode</var> parameter + * in {@link #PostCompilerHelper(IProject, PrintStream, PrintStream, boolean, boolean)} + * + * @param intermediateApk The path to the temporary resource file. + * @param dex The path to the dex file. + * @param output The path to the final package file to create. + * @param debugSign whether the apk must be signed with the debug key. + * @param libProjects an optional list of library projects (can be null) + * @param abiFilter an optional filter. If not null, then only the matching ABI is included in + * the final archive + * @return true if success, false otherwise. + * @throws NativeLibInJarException + * @throws ApkCreationException + * @throws CoreException + * @throws DuplicateFileException + */ + public void finalPackage(String intermediateApk, String dex, String output, + List<IProject> libProjects, + PrivateKey key, X509Certificate certificate, ResourceMarker resMarker) + throws NativeLibInJarException, ApkCreationException, DuplicateFileException, + CoreException { + + try { + ApkBuilder apkBuilder = new ApkBuilder(output, intermediateApk, dex, + key, certificate, + mVerbose ? mOutStream: null); + apkBuilder.setDebugMode(mDebugMode); + + // either use the full compiled code paths or just the proguard file + // if present + Collection<String> pathsCollection = mCompiledCodePaths; + if (mProguardFile != null) { + pathsCollection = Collections.singletonList(mProguardFile); + mProguardFile = null; + } + + // Now we write the standard resources from all the output paths. + for (String path : pathsCollection) { + File file = new File(path); + if (file.isFile()) { + JarStatus jarStatus = apkBuilder.addResourcesFromJar(file); + + // check if we found native libraries in the external library. This + // constitutes an error or warning depending on if they are in lib/ + if (jarStatus.getNativeLibs().size() > 0) { + String libName = file.getName(); + + String msg = String.format( + "Native libraries detected in '%1$s'. See console for more information.", + libName); + + ArrayList<String> consoleMsgs = new ArrayList<String>(); + + consoleMsgs.add(String.format( + "The library '%1$s' contains native libraries that will not run on the device.", + libName)); + + if (jarStatus.hasNativeLibsConflicts()) { + consoleMsgs.add("Additionally some of those libraries will interfer with the installation of the application because of their location in lib/"); + consoleMsgs.add("lib/ is reserved for NDK libraries."); + } + + consoleMsgs.add("The following libraries were found:"); + + for (String lib : jarStatus.getNativeLibs()) { + consoleMsgs.add(" - " + lib); + } + + String[] consoleStrings = consoleMsgs.toArray(new String[consoleMsgs.size()]); + + // if there's a conflict or if the prefs force error on any native code in jar + // files, throw an exception + if (jarStatus.hasNativeLibsConflicts() || + AdtPrefs.getPrefs().getBuildForceErrorOnNativeLibInJar()) { + throw new NativeLibInJarException(jarStatus, msg, libName, consoleStrings); + } else { + // otherwise, put a warning, and output to the console also. + if (resMarker != null) { + resMarker.setWarning(mProject, msg); + } + + for (String string : consoleStrings) { + mOutStream.println(string); + } + } + } + } else if (file.isDirectory()) { + // this is technically not a source folder (class folder instead) but since we + // only care about Java resources (ie non class/java files) this will do the + // same + apkBuilder.addSourceFolder(file); + } + } + + // now write the native libraries. + // First look if the lib folder is there. + IResource libFolder = mProject.findMember(SdkConstants.FD_NATIVE_LIBS); + if (libFolder != null && libFolder.exists() && + libFolder.getType() == IResource.FOLDER) { + // get a File for the folder. + apkBuilder.addNativeLibraries(libFolder.getLocation().toFile()); + } + + // next the native libraries for the renderscript support mode. + if (mProjectState.getRenderScriptSupportMode()) { + IFolder androidOutputFolder = BaseProjectHelper.getAndroidOutputFolder(mProject); + IResource rsLibFolder = androidOutputFolder.getFolder( + AdtConstants.WS_BIN_RELATIVE_RS_LIBS); + File rsLibFolderFile = rsLibFolder.getLocation().toFile(); + if (rsLibFolderFile.isDirectory()) { + apkBuilder.addNativeLibraries(rsLibFolderFile); + } + + File rsLibs = RenderScriptProcessor.getSupportNativeLibFolder( + mBuildToolInfo.getLocation().getAbsolutePath()); + if (rsLibs.isDirectory()) { + apkBuilder.addNativeLibraries(rsLibs); + } + } + + // write the native libraries for the library projects. + if (libProjects != null) { + for (IProject lib : libProjects) { + libFolder = lib.findMember(SdkConstants.FD_NATIVE_LIBS); + if (libFolder != null && libFolder.exists() && + libFolder.getType() == IResource.FOLDER) { + apkBuilder.addNativeLibraries(libFolder.getLocation().toFile()); + } + } + } + + // seal the APK. + apkBuilder.sealApk(); + } catch (SealedApkException e) { + // this won't happen as we control when the apk is sealed. + } + } + + public void setProguardOutput(String proguardFile) { + mProguardFile = proguardFile; + } + + public Collection<String> getCompiledCodePaths() { + return mCompiledCodePaths; + } + + public void runProguard(List<File> proguardConfigs, File inputJar, Collection<String> jarFiles, + File obfuscatedJar, File logOutput) + throws ProguardResultException, ProguardExecException, IOException { + IAndroidTarget target = Sdk.getCurrent().getTarget(mProject); + + // prepare the command line for proguard + List<String> command = new ArrayList<String>(); + command.add(AdtPlugin.getOsAbsoluteProguard()); + + for (File configFile : proguardConfigs) { + command.add("-include"); //$NON-NLS-1$ + command.add(quotePath(configFile.getAbsolutePath())); + } + + command.add("-injars"); //$NON-NLS-1$ + StringBuilder sb = new StringBuilder(quotePath(inputJar.getAbsolutePath())); + for (String jarFile : jarFiles) { + sb.append(File.pathSeparatorChar); + sb.append(quotePath(jarFile)); + } + command.add(quoteWinArg(sb.toString())); + + command.add("-outjars"); //$NON-NLS-1$ + command.add(quotePath(obfuscatedJar.getAbsolutePath())); + + command.add("-libraryjars"); //$NON-NLS-1$ + sb = new StringBuilder(quotePath(target.getPath(IAndroidTarget.ANDROID_JAR))); + IOptionalLibrary[] libraries = target.getOptionalLibraries(); + if (libraries != null) { + for (IOptionalLibrary lib : libraries) { + sb.append(File.pathSeparatorChar); + sb.append(quotePath(lib.getJarPath())); + } + } + command.add(quoteWinArg(sb.toString())); + + if (logOutput != null) { + if (logOutput.isDirectory() == false) { + logOutput.mkdirs(); + } + + command.add("-dump"); //$NON-NLS-1$ + command.add(new File(logOutput, "dump.txt").getAbsolutePath()); //$NON-NLS-1$ + + command.add("-printseeds"); //$NON-NLS-1$ + command.add(new File(logOutput, "seeds.txt").getAbsolutePath()); //$NON-NLS-1$ + + command.add("-printusage"); //$NON-NLS-1$ + command.add(new File(logOutput, "usage.txt").getAbsolutePath()); //$NON-NLS-1$ + + command.add("-printmapping"); //$NON-NLS-1$ + command.add(new File(logOutput, "mapping.txt").getAbsolutePath()); //$NON-NLS-1$ + } + + String commandArray[] = null; + + if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { + commandArray = createWindowsProguardConfig(command); + } + + if (commandArray == null) { + // For Mac & Linux, use a regular command string array. + commandArray = command.toArray(new String[command.size()]); + } + + // Define PROGUARD_HOME to point to $SDK/tools/proguard if it's not yet defined. + // The Mac/Linux proguard.sh can infer it correctly but not the proguard.bat one. + String[] envp = null; + Map<String, String> envMap = new TreeMap<String, String>(System.getenv()); + if (!envMap.containsKey("PROGUARD_HOME")) { //$NON-NLS-1$ + envMap.put("PROGUARD_HOME", Sdk.getCurrent().getSdkOsLocation() + //$NON-NLS-1$ + SdkConstants.FD_TOOLS + File.separator + + SdkConstants.FD_PROGUARD); + envp = new String[envMap.size()]; + int i = 0; + for (Map.Entry<String, String> entry : envMap.entrySet()) { + envp[i++] = String.format("%1$s=%2$s", //$NON-NLS-1$ + entry.getKey(), + entry.getValue()); + } + } + + if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) { + sb = new StringBuilder(); + for (String c : commandArray) { + sb.append(c).append(' '); + } + AdtPlugin.printToConsole(mProject, sb.toString()); + } + + // launch + int execError = 1; + try { + // launch the command line process + Process process = Runtime.getRuntime().exec(commandArray, envp); + + // list to store each line of stderr + ArrayList<String> results = new ArrayList<String>(); + + // get the output and return code from the process + execError = grabProcessOutput(mProject, process, results); + + if (mVerbose) { + for (String resultString : results) { + mOutStream.println(resultString); + } + } + + if (execError != 0) { + throw new ProguardResultException(execError, + results.toArray(new String[results.size()])); + } + + } catch (IOException e) { + String msg = String.format(Messages.Proguard_Exec_Error, commandArray[0]); + throw new ProguardExecException(msg, e); + } catch (InterruptedException e) { + String msg = String.format(Messages.Proguard_Exec_Error, commandArray[0]); + throw new ProguardExecException(msg, e); + } + } + + /** + * For tools R8 up to R11, the proguard.bat launcher on Windows only accepts + * arguments %1..%9. Since we generally have about 15 arguments, we were working + * around this by generating a temporary config file for proguard and then using + * that. + * Starting with tools R12, the proguard.bat launcher has been fixed to take + * all arguments using %* so we no longer need this hack. + * + * @param command + * @return + * @throws IOException + */ + private String[] createWindowsProguardConfig(List<String> command) throws IOException { + + // Arg 0 is the proguard.bat path and arg 1 is the user config file + String launcher = AdtPlugin.readFile(new File(command.get(0))); + if (launcher.contains("%*")) { //$NON-NLS-1$ + // This is the launcher from Tools R12. Don't work around it. + return null; + } + + // On Windows, proguard.bat can only pass %1...%9 to the java -jar proguard.jar + // call, but we have at least 15 arguments here so some get dropped silently + // and quoting is a big issue. So instead we'll work around that by writing + // all the arguments to a temporary config file. + + String[] commandArray = new String[3]; + + commandArray[0] = command.get(0); + commandArray[1] = command.get(1); + + // Write all the other arguments to a config file + File argsFile = File.createTempFile(TEMP_PREFIX, ".pro"); //$NON-NLS-1$ + // TODO FIXME this may leave a lot of temp files around on a long session. + // Should have a better way to clean up e.g. before each build. + argsFile.deleteOnExit(); + + FileWriter fw = new FileWriter(argsFile); + + for (int i = 2; i < command.size(); i++) { + String s = command.get(i); + fw.write(s); + fw.write(s.startsWith("-") ? ' ' : '\n'); //$NON-NLS-1$ + } + + fw.close(); + + commandArray[2] = "@" + argsFile.getAbsolutePath(); //$NON-NLS-1$ + return commandArray; + } + + /** + * Quotes a single path for proguard to deal with spaces. + * + * @param path The path to quote. + * @return The original path if it doesn't contain a space. + * Or the original path surrounded by single quotes if it contains spaces. + */ + private String quotePath(String path) { + if (path.indexOf(' ') != -1) { + path = '\'' + path + '\''; + } + return path; + } + + /** + * Quotes a compound proguard argument to deal with spaces. + * <p/> + * Proguard takes multi-path arguments such as "path1;path2" for some options. + * When the {@link #quotePath} methods adds quotes for such a path if it contains spaces, + * the proguard shell wrapper will absorb the quotes, so we need to quote around the + * quotes. + * + * @param path The path to quote. + * @return The original path if it doesn't contain a single quote. + * Or on Windows the original path surrounded by double quotes if it contains a quote. + */ + private String quoteWinArg(String path) { + if (path.indexOf('\'') != -1 && + SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { + path = '"' + path + '"'; + } + return path; + } + + + /** + * Execute the Dx tool for dalvik code conversion. + * @param javaProject The java project + * @param inputPaths the input paths for DX + * @param osOutFilePath the path of the dex file to create. + * + * @throws CoreException + * @throws DexException + */ + public void executeDx(IJavaProject javaProject, Collection<String> inputPaths, + String osOutFilePath) + throws CoreException, DexException { + + // get the dex wrapper + Sdk sdk = Sdk.getCurrent(); + DexWrapper wrapper = sdk.getDexWrapper(mBuildToolInfo); + + if (wrapper == null) { + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + Messages.ApkBuilder_UnableBuild_Dex_Not_loaded)); + } + + try { + // set a temporary prefix on the print streams. + mOutStream.setPrefix(CONSOLE_PREFIX_DX); + mErrStream.setPrefix(CONSOLE_PREFIX_DX); + + IFolder binFolder = BaseProjectHelper.getAndroidOutputFolder(javaProject.getProject()); + File binFile = binFolder.getLocation().toFile(); + File dexedLibs = new File(binFile, "dexedLibs"); + if (dexedLibs.exists() == false) { + dexedLibs.mkdir(); + } + + // replace the libs by their dexed versions (dexing them if needed.) + List<String> finalInputPaths = new ArrayList<String>(inputPaths.size()); + if (mDisableDexMerger || inputPaths.size() == 1) { + // only one input, no need to put a pre-dexed version, even if this path is + // just a jar file (case for proguard'ed builds) + finalInputPaths.addAll(inputPaths); + } else { + + for (String input : inputPaths) { + File inputFile = new File(input); + if (inputFile.isDirectory()) { + finalInputPaths.add(input); + } else if (inputFile.isFile()) { + String fileName = getDexFileName(inputFile); + + File dexedLib = new File(dexedLibs, fileName); + String dexedLibPath = dexedLib.getAbsolutePath(); + + if (dexedLib.isFile() == false || + dexedLib.lastModified() < inputFile.lastModified()) { + + if (mVerbose) { + mOutStream.println( + String.format("Pre-Dexing %1$s -> %2$s", input, fileName)); + } + + if (dexedLib.isFile()) { + dexedLib.delete(); + } + + int res = wrapper.run(dexedLibPath, Collections.singleton(input), + mForceJumbo, mVerbose, mOutStream, mErrStream); + + if (res != 0) { + // output error message and mark the project. + String message = String.format(Messages.Dalvik_Error_d, res); + throw new DexException(message); + } + } else { + if (mVerbose) { + mOutStream.println( + String.format("Using Pre-Dexed %1$s <- %2$s", + fileName, input)); + } + } + + finalInputPaths.add(dexedLibPath); + } + } + } + + if (mVerbose) { + for (String input : finalInputPaths) { + mOutStream.println("Input: " + input); + } + } + + int res = wrapper.run(osOutFilePath, + finalInputPaths, + mForceJumbo, + mVerbose, + mOutStream, mErrStream); + + mOutStream.setPrefix(null); + mErrStream.setPrefix(null); + + if (res != 0) { + // output error message and marker the project. + String message = String.format(Messages.Dalvik_Error_d, res); + throw new DexException(message); + } + } catch (DexException e) { + throw e; + } catch (Throwable t) { + String message = t.getMessage(); + if (message == null) { + message = t.getClass().getCanonicalName(); + } + message = String.format(Messages.Dalvik_Error_s, message); + + throw new DexException(message, t); + } + } + + private String getDexFileName(File inputFile) { + // get the filename + String name = inputFile.getName(); + // remove the extension + int pos = name.lastIndexOf('.'); + if (pos != -1) { + name = name.substring(0, pos); + } + + // add a hash of the original file path + HashFunction hashFunction = Hashing.md5(); + HashCode hashCode = hashFunction.hashString(inputFile.getAbsolutePath(), Charsets.UTF_8); + + return name + "-" + hashCode.toString() + ".jar"; + } + + /** + * Executes aapt. If any error happen, files or the project will be marked. + * @param command The command for aapt to execute. Currently supported: package and crunch + * @param osManifestPath The path to the manifest file + * @param osResPath The path to the res folder + * @param osAssetsPath The path to the assets folder. This can be null. + * @param osOutFilePath The path to the temporary resource file to create, + * or in the case of crunching the path to the cache to create/update. + * @param configFilter The configuration filter for the resources to include + * (used with -c option, for example "port,en,fr" to include portrait, English and French + * resources.) + * @param versionCode optional version code to insert in the manifest during packaging. If <=0 + * then no value is inserted + * @throws AaptExecException + * @throws AaptResultException + */ + private void executeAapt(String aaptCommand, String osManifestPath, + List<String> osResPaths, String osAssetsPath, String osOutFilePath, + String configFilter, int versionCode) throws AaptExecException, AaptResultException { + IAndroidTarget target = Sdk.getCurrent().getTarget(mProject); + + String aapt = mBuildToolInfo.getPath(BuildToolInfo.PathId.AAPT); + + // Create the command line. + ArrayList<String> commandArray = new ArrayList<String>(); + commandArray.add(aapt); + commandArray.add(aaptCommand); + if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) { + commandArray.add("-v"); //$NON-NLS-1$ + } + + // Common to all commands + for (String path : osResPaths) { + commandArray.add("-S"); //$NON-NLS-1$ + commandArray.add(path); + } + + if (aaptCommand.equals(COMMAND_PACKAGE)) { + commandArray.add("-f"); //$NON-NLS-1$ + commandArray.add("--no-crunch"); //$NON-NLS-1$ + + // if more than one res, this means there's a library (or more) and we need + // to activate the auto-add-overlay + if (osResPaths.size() > 1) { + commandArray.add("--auto-add-overlay"); //$NON-NLS-1$ + } + + if (mDebugMode) { + commandArray.add("--debug-mode"); //$NON-NLS-1$ + } + + if (versionCode > 0) { + commandArray.add("--version-code"); //$NON-NLS-1$ + commandArray.add(Integer.toString(versionCode)); + } + + if (configFilter != null) { + commandArray.add("-c"); //$NON-NLS-1$ + commandArray.add(configFilter); + } + + // never compress apks. + commandArray.add("-0"); + commandArray.add("apk"); + + commandArray.add("-M"); //$NON-NLS-1$ + commandArray.add(osManifestPath); + + if (osAssetsPath != null) { + commandArray.add("-A"); //$NON-NLS-1$ + commandArray.add(osAssetsPath); + } + + commandArray.add("-I"); //$NON-NLS-1$ + commandArray.add(target.getPath(IAndroidTarget.ANDROID_JAR)); + + commandArray.add("-F"); //$NON-NLS-1$ + commandArray.add(osOutFilePath); + } else if (aaptCommand.equals(COMMAND_CRUNCH)) { + commandArray.add("-C"); //$NON-NLS-1$ + commandArray.add(osOutFilePath); + } + + String command[] = commandArray.toArray( + new String[commandArray.size()]); + + if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) { + StringBuilder sb = new StringBuilder(); + for (String c : command) { + sb.append(c); + sb.append(' '); + } + AdtPlugin.printToConsole(mProject, sb.toString()); + } + + // Benchmarking start + long startAaptTime = 0; + if (BENCHMARK_FLAG) { + String msg = "BENCHMARK ADT: Starting " + aaptCommand //$NON-NLS-1$ + + " call to Aapt"; //$NON-NLS-1$ + AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg); + startAaptTime = System.nanoTime(); + } + + // launch + try { + // launch the command line process + Process process = Runtime.getRuntime().exec(command); + + // list to store each line of stderr + ArrayList<String> stdErr = new ArrayList<String>(); + + // get the output and return code from the process + int returnCode = grabProcessOutput(mProject, process, stdErr); + + if (mVerbose) { + for (String stdErrString : stdErr) { + mOutStream.println(stdErrString); + } + } + if (returnCode != 0) { + throw new AaptResultException(returnCode, + stdErr.toArray(new String[stdErr.size()])); + } + } catch (IOException e) { + String msg = String.format(Messages.AAPT_Exec_Error_s, command[0]); + throw new AaptExecException(msg, e); + } catch (InterruptedException e) { + String msg = String.format(Messages.AAPT_Exec_Error_s, command[0]); + throw new AaptExecException(msg, e); + } + + // Benchmarking end + if (BENCHMARK_FLAG) { + String msg = "BENCHMARK ADT: Ending " + aaptCommand //$NON-NLS-1$ + + " call to Aapt.\nBENCHMARK ADT: Time Elapsed: " //$NON-NLS-1$ + + ((System.nanoTime() - startAaptTime)/MILLION) + "ms"; //$NON-NLS-1$ + AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg); + } + } + + /** + * Computes all the project output and dependencies that must go into building the apk. + * + * @param resMarker + * @throws CoreException + */ + private void gatherPaths(ResourceMarker resMarker) + throws CoreException { + IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); + + // get a java project for the project. + IJavaProject javaProject = JavaCore.create(mProject); + + + // get the output of the main project + IPath path = javaProject.getOutputLocation(); + IResource outputResource = wsRoot.findMember(path); + if (outputResource != null && outputResource.getType() == IResource.FOLDER) { + mCompiledCodePaths.add(outputResource.getLocation().toOSString()); + } + + // we could use IJavaProject.getResolvedClasspath directly, but we actually + // want to see the containers themselves. + IClasspathEntry[] classpaths = javaProject.readRawClasspath(); + if (classpaths != null) { + for (IClasspathEntry e : classpaths) { + // ignore non exported entries, unless they're in the DEPEDENCIES container, + // in which case we always want it (there may be some older projects that + // have it as non exported). + if (e.isExported() || + (e.getEntryKind() == IClasspathEntry.CPE_CONTAINER && + e.getPath().toString().equals(AdtConstants.CONTAINER_DEPENDENCIES))) { + handleCPE(e, javaProject, wsRoot, resMarker); + } + } + } + } + + private void handleCPE(IClasspathEntry entry, IJavaProject javaProject, + IWorkspaceRoot wsRoot, ResourceMarker resMarker) { + + // if this is a classpath variable reference, we resolve it. + if (entry.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { + entry = JavaCore.getResolvedClasspathEntry(entry); + } + + if (entry.getEntryKind() == IClasspathEntry.CPE_PROJECT) { + IProject refProject = wsRoot.getProject(entry.getPath().lastSegment()); + try { + // ignore if it's an Android project, or if it's not a Java Project + if (refProject.hasNature(JavaCore.NATURE_ID) && + refProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) { + IJavaProject refJavaProject = JavaCore.create(refProject); + + // get the output folder + IPath path = refJavaProject.getOutputLocation(); + IResource outputResource = wsRoot.findMember(path); + if (outputResource != null && outputResource.getType() == IResource.FOLDER) { + mCompiledCodePaths.add(outputResource.getLocation().toOSString()); + } + } + } catch (CoreException exception) { + // can't query the project nature? ignore + } + + } else if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) { + handleClasspathLibrary(entry, wsRoot, resMarker); + } else if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) { + // get the container + try { + IClasspathContainer container = JavaCore.getClasspathContainer( + entry.getPath(), javaProject); + // ignore the system and default_system types as they represent + // libraries that are part of the runtime. + if (container != null && container.getKind() == IClasspathContainer.K_APPLICATION) { + IClasspathEntry[] entries = container.getClasspathEntries(); + for (IClasspathEntry cpe : entries) { + handleCPE(cpe, javaProject, wsRoot, resMarker); + } + } + } catch (JavaModelException jme) { + // can't resolve the container? ignore it. + AdtPlugin.log(jme, "Failed to resolve ClasspathContainer: %s", entry.getPath()); + } + } + } + + private void handleClasspathLibrary(IClasspathEntry e, IWorkspaceRoot wsRoot, + ResourceMarker resMarker) { + // get the IPath + IPath path = e.getPath(); + + IResource resource = wsRoot.findMember(path); + + if (resource != null && resource.getType() == IResource.PROJECT) { + // if it's a project we should just ignore it because it's going to be added + // later when we add all the referenced projects. + + } else if (SdkConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) { + // case of a jar file (which could be relative to the workspace or a full path) + if (resource != null && resource.exists() && + resource.getType() == IResource.FILE) { + mCompiledCodePaths.add(resource.getLocation().toOSString()); + } else { + // if the jar path doesn't match a workspace resource, + // then we get an OSString and check if this links to a valid file. + String osFullPath = path.toOSString(); + + File f = new File(osFullPath); + if (f.isFile()) { + mCompiledCodePaths.add(osFullPath); + } else { + String message = String.format( Messages.Couldnt_Locate_s_Error, + path); + // always output to the console + mOutStream.println(message); + + // put a marker + if (resMarker != null) { + resMarker.setWarning(mProject, message); + } + } + } + } else { + // this can be the case for a class folder. + if (resource != null && resource.exists() && + resource.getType() == IResource.FOLDER) { + mCompiledCodePaths.add(resource.getLocation().toOSString()); + } else { + // if the path doesn't match a workspace resource, + // then we get an OSString and check if this links to a valid folder. + String osFullPath = path.toOSString(); + + File f = new File(osFullPath); + if (f.isDirectory()) { + mCompiledCodePaths.add(osFullPath); + } + } + } + } + + /** + * Checks a {@link IFile} to make sure it should be packaged as standard resources. + * @param file the IFile representing the file. + * @return true if the file should be packaged as standard java resources. + */ + public static boolean checkFileForPackaging(IFile file) { + String name = file.getName(); + + String ext = file.getFileExtension(); + return ApkBuilder.checkFileForPackaging(name, ext); + } + + /** + * Checks whether an {@link IFolder} and its content is valid for packaging into the .apk as + * standard Java resource. + * @param folder the {@link IFolder} to check. + */ + public static boolean checkFolderForPackaging(IFolder folder) { + String name = folder.getName(); + return ApkBuilder.checkFolderForPackaging(name); + } + + /** + * Returns a list of {@link IJavaProject} matching the provided {@link IProject} objects. + * @param projects the IProject objects. + * @return a new list object containing the IJavaProject object for the given IProject objects. + * @throws CoreException + */ + public static List<IJavaProject> getJavaProjects(List<IProject> projects) throws CoreException { + ArrayList<IJavaProject> list = new ArrayList<IJavaProject>(); + + for (IProject p : projects) { + if (p.isOpen() && p.hasNature(JavaCore.NATURE_ID)) { + + list.add(JavaCore.create(p)); + } + } + + return list; + } + + /** + * Get the stderr output of a process and return when the process is done. + * @param process The process to get the output from + * @param stderr The array to store the stderr output + * @return the process return code. + * @throws InterruptedException + */ + public final static int grabProcessOutput( + final IProject project, + final Process process, + final ArrayList<String> stderr) + throws InterruptedException { + + return GrabProcessOutput.grabProcessOutput( + process, + Wait.WAIT_FOR_READERS, // we really want to make sure we get all the output! + new IProcessOutput() { + + @SuppressWarnings("unused") + @Override + public void out(@Nullable String line) { + if (line != null) { + // If benchmarking always print the lines that + // correspond to benchmarking info returned by ADT + if (BENCHMARK_FLAG && line.startsWith("BENCHMARK:")) { //$NON-NLS-1$ + AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, + project, line); + } else { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, + project, line); + } + } + } + + @Override + public void err(@Nullable String line) { + if (line != null) { + stderr.add(line); + if (BuildVerbosity.VERBOSE == AdtPrefs.getPrefs().getBuildVerbosity()) { + AdtPlugin.printErrorToConsole(project, line); + } + } + } + }); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ConvertSwitchDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ConvertSwitchDialog.java new file mode 100644 index 000000000..20bee5dac --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ConvertSwitchDialog.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.build; + +import com.android.ide.eclipse.adt.internal.editors.IconFactory; + +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.dialogs.TitleAreaDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Link; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.browser.IWebBrowser; + +import java.net.URL; + +/** + * Dialog shown by the {@link ConvertSwitchQuickFixProcessor}. This is a custom + * dialog rather than a plain {@link MessageDialog} such that we can show a link + * and point to a web page for more info. + */ +class ConvertSwitchDialog extends TitleAreaDialog implements SelectionListener { + /** URL containing more info */ + private static final String URL = "http://tools.android.com/tips/non-constant-fields"; //$NON-NLS-1$ + + private final String mField; + + private Link mLink; + + /** + * Create the dialog. + * @param parentShell the parent shell + * @param field the field name we're warning about + */ + public ConvertSwitchDialog(Shell parentShell, String field) { + super(parentShell); + mField = field; + Image image = IconFactory.getInstance().getIcon("android-64"); //$NON-NLS-1$ + setTitleImage(image); + } + + @Override + protected Control createDialogArea(Composite parent) { + String text = String.format( + "As of ADT 14, the resource fields (such as %1$s) are no longer constants " + + "when defined in library projects. This is necessary to make library " + + "projects reusable without recompiling them.\n" + + "\n" + + "One consequence of this is that you can no longer use the fields directly " + + "in switch statements. You must use an if-else chain instead.\n" + + "\n" + + "Eclipse can automatically convert from a switch statement to an if-else " + + "statement. Just place the caret on the switch keyword and invoke " + + "Quick Fix (Ctrl-1 on Windows and Linux, Cmd-1 on Mac), then select " + + "\"Convert 'switch' to 'if-else'\".\n" + + "\n" + + "For more information, see <a href=\"" + URL + "\">" + URL + "</a>", + mField); + + Composite area = (Composite) super.createDialogArea(parent); + Composite container = new Composite(area, SWT.NONE); + container.setLayout(new GridLayout(1, false)); + container.setLayoutData(new GridData(GridData.FILL_BOTH)); + + mLink = new Link(container, SWT.NONE); + mLink.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, true, true, 1, 1)); + mLink.setText(text); + mLink.addSelectionListener(this); + + setMessage("Non-Constant Expressions: Migration Necessary", IMessageProvider.INFORMATION); + + return area; + } + + @Override + protected void createButtonsForButtonBar(Composite parent) { + createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true); + createButton(parent, IDialogConstants.HELP_ID, IDialogConstants.HELP_LABEL, false); + } + + @Override + protected Point getInitialSize() { + return new Point(500, 400); + } + + private void showWebPage() { + try { + IWorkbench workbench = PlatformUI.getWorkbench(); + IWebBrowser browser = workbench.getBrowserSupport().getExternalBrowser(); + browser.openURL(new URL(URL)); + } catch (Exception e) { + String message = String.format("Could not open browser. Vist\n%1$s\ninstead.", + URL); + MessageDialog.openError(getShell(), "Browser Error", message); + } + + } + + @Override + protected void buttonPressed(int buttonId) { + if (buttonId == IDialogConstants.HELP_ID) { + showWebPage(); + } else { + super.buttonPressed(buttonId); + } + } + + // ---- Implements SelectionListener ---- + + @Override + public void widgetSelected(SelectionEvent e) { + if (e.getSource() == mLink) { + showWebPage(); + } + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ConvertSwitchQuickFixProcessor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ConvertSwitchQuickFixProcessor.java new file mode 100644 index 000000000..a99dc7601 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ConvertSwitchQuickFixProcessor.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.IBuffer; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.compiler.IProblem; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.QualifiedName; +import org.eclipse.jdt.ui.text.java.IInvocationContext; +import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal; +import org.eclipse.jdt.ui.text.java.IProblemLocation; +import org.eclipse.jdt.ui.text.java.IQuickFixProcessor; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.contentassist.IContextInformation; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.editors.text.TextFileDocumentProvider; +import org.eclipse.ui.texteditor.IDocumentProvider; + +import java.util.List; + +/** + * A quickfix processor which looks for "case expressions must be constant + * expressions" errors, and if they apply to fields in a class named R, it + * assumes this is code related to library projects that are no longer final and + * will need to be rewritten to use if-else chains instead. + */ +public class ConvertSwitchQuickFixProcessor implements IQuickFixProcessor { + /** Constructs a new {@link ConvertSwitchQuickFixProcessor} */ + public ConvertSwitchQuickFixProcessor() { + } + + @Override + public boolean hasCorrections(ICompilationUnit cu, int problemId) { + return problemId == IProblem.NonConstantExpression; + } + + @Override + public IJavaCompletionProposal[] getCorrections(IInvocationContext context, + IProblemLocation[] location) throws CoreException { + if (location == null || location.length == 0) { + return null; + } + ASTNode coveringNode = context.getCoveringNode(); + if (coveringNode == null) { + return null; + } + + // Look up the fully qualified name of the non-constant expression, if any, and + // make sure it's R-something. + if (coveringNode.getNodeType() == ASTNode.SIMPLE_NAME) { + coveringNode = coveringNode.getParent(); + if (coveringNode == null) { + return null; + } + } + if (coveringNode.getNodeType() != ASTNode.QUALIFIED_NAME) { + return null; + } + QualifiedName name = (QualifiedName) coveringNode; + if (!name.getFullyQualifiedName().startsWith("R.")) { //$NON-NLS-1$ + return null; + } + + IProblemLocation error = location[0]; + int errorStart = error.getOffset(); + int errorLength = error.getLength(); + int caret = context.getSelectionOffset(); + + // Even though the hasCorrections() method above will return false for everything + // other than non-constant expression errors, it turns out this getCorrections() + // method will ALSO be called on lines where there is no such error. In particular, + // if you have an invalid cast expression like this: + // Button button = findViewById(R.id.textView); + // then this method will be called, and the expression will pass all of the above + // checks. However, we -don't- want to show a migrate code suggestion in that case! + // Therefore, we'll need to check if we're *actually* on a line with the given + // problem. + // + // Unfortunately, we don't get passed the problemId again, and there's no access + // to it. So instead we'll need to look up the markers on the line, and see + // if we actually have a constant expression warning. This is not pretty!! + + boolean foundError = false; + ICompilationUnit compilationUnit = context.getCompilationUnit(); + IResource file = compilationUnit.getResource(); + if (file != null) { + IDocumentProvider provider = new TextFileDocumentProvider(); + try { + provider.connect(file); + IDocument document = provider.getDocument(file); + if (document != null) { + List<IMarker> markers = AdtUtils.findMarkersOnLine(IMarker.PROBLEM, + file, document, errorStart); + for (IMarker marker : markers) { + String message = marker.getAttribute(IMarker.MESSAGE, ""); + // There are no other attributes in the marker we can use to identify + // the exact error, so we'll need to resort to the actual message + // text even though that would not work if the messages had been + // localized... This can also break if the error messages change. Yuck. + if (message.contains("constant expressions")) { //$NON-NLS-1$ + foundError = true; + } + } + } + } catch (Exception e) { + AdtPlugin.log(e, "Can't validate error message in %1$s", file.getName()); + } finally { + provider.disconnect(file); + } + } + if (!foundError) { + // Not a constant-expression warning, so do nothing + return null; + } + + IBuffer buffer = compilationUnit.getBuffer(); + boolean sameLine = false; + // See if the caret is on the same line as the error + if (caret <= errorStart) { + // Search backwards to beginning of line + for (int i = errorStart; i >= 0; i--) { + if (i <= caret) { + sameLine = true; + break; + } + char c = buffer.getChar(i); + if (c == '\n') { + break; + } + } + } else { + // Search forwards to the end of the line + for (int i = errorStart + errorLength, n = buffer.getLength(); i < n; i++) { + if (i >= caret) { + sameLine = true; + break; + } + char c = buffer.getChar(i); + if (c == '\n') { + break; + } + } + } + + if (sameLine) { + String expression = buffer.getText(errorStart, errorLength); + return new IJavaCompletionProposal[] { + new MigrateProposal(expression) + }; + } + + return null; + } + + /** Proposal for the quick fix which displays an explanation message to the user */ + private class MigrateProposal implements IJavaCompletionProposal { + private String mExpression; + + private MigrateProposal(String expression) { + mExpression = expression; + } + + @Override + public void apply(IDocument document) { + Shell shell = AdtPlugin.getShell(); + ConvertSwitchDialog dialog = new ConvertSwitchDialog(shell, mExpression); + dialog.open(); + } + + @Override + public Point getSelection(IDocument document) { + return null; + } + + @Override + public String getAdditionalProposalInfo() { + return "As of ADT 14, resource fields cannot be used as switch cases. Invoke this " + + "fix to get more information."; + } + + @Override + public String getDisplayString() { + return "Migrate Android Code"; + } + + @Override + public Image getImage() { + return AdtPlugin.getAndroidLogo(); + } + + @Override + public IContextInformation getContextInformation() { + return null; + } + + @Override + public int getRelevance() { + return 50; + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DefaultSourceChangeHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DefaultSourceChangeHandler.java new file mode 100644 index 000000000..ea0d695d1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DefaultSourceChangeHandler.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IResourceDelta; + +import java.util.HashSet; +import java.util.Set; + +/** + * Base source change handler for the {@link SourceProcessor} classes. + * + * It can be used as is, as long as the matching {@link SourceProcessor} properly implements + * its abstract methods, and the processor does not output resource files, + * or can be extended to provide custom implementation for: + * {@link #handleSourceFile(IFile, int)} + * {@link #handleGeneratedFile(IFile, int)} + * {@link #handleResourceFile(IFile, int)} + * {@link #filterResourceFolder(IContainer)} + * + */ +public class DefaultSourceChangeHandler implements SourceChangeHandler { + + private SourceProcessor mProcessor; + + /** List of source files found that are modified or new. */ + private final Set<IFile> mToCompile = new HashSet<IFile>(); + + /** List of source files that have been removed. */ + private final Set<IFile> mRemoved = new HashSet<IFile>(); + + @Override + public boolean handleGeneratedFile(IFile file, int kind) { + if (kind == IResourceDelta.REMOVED || kind == IResourceDelta.CHANGED) { + IFile sourceFile = mProcessor.isOutput(file); + if (sourceFile != null) { + mToCompile.add(sourceFile); + return true; + } + } + + return false; + } + + @Override + public void handleSourceFile(IFile file, int kind) { + // first the file itself if this is a match for the processor's extension + if (mProcessor.getExtensions().contains(file.getFileExtension())) { + if (kind == IResourceDelta.REMOVED) { + mRemoved.add(file); + } else { + mToCompile.add(file); + } + } + + // now the dependencies. In all case we compile the files that depend on the + // added/changed/removed file. + mToCompile.addAll(mProcessor.isDependency(file)); + } + + protected void addFileToCompile(IFile file) { + mToCompile.add(file); + } + + Set<IFile> getFilesToCompile() { + return mToCompile; + } + + protected void addRemovedFile(IFile file) { + mRemoved.add(file); + } + + Set<IFile> getRemovedFiles() { + return mRemoved; + } + + public void reset() { + mToCompile.clear(); + mRemoved.clear(); + } + + protected SourceProcessor getProcessor() { + return mProcessor; + } + + void init(SourceProcessor processor) { + mProcessor = processor; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DexException.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DexException.java new file mode 100644 index 000000000..032e5e646 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DexException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build; + +/** + * Exception throw when dx fails. + * + */ +public final class DexException extends Exception { + private static final long serialVersionUID = 1L; + + DexException(String message) { + super(message); + } + + DexException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DexWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DexWrapper.java new file mode 100644 index 000000000..3f882842b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DexWrapper.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build; + +import com.android.SdkConstants; +import com.android.ide.eclipse.adt.AdtPlugin; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; + +import java.io.File; +import java.io.PrintStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Collection; + +/** + * Wrapper to access dx.jar through reflection. + * <p/>Since there is no proper api to call the method in the dex library, this wrapper is going + * to access it through reflection. + */ +public final class DexWrapper { + + private final static String DEX_MAIN = "com.android.dx.command.dexer.Main"; //$NON-NLS-1$ + private final static String DEX_CONSOLE = "com.android.dx.command.DxConsole"; //$NON-NLS-1$ + private final static String DEX_ARGS = "com.android.dx.command.dexer.Main$Arguments"; //$NON-NLS-1$ + + private final static String MAIN_RUN = "run"; //$NON-NLS-1$ + + private Method mRunMethod; + + private Constructor<?> mArgConstructor; + private Field mArgOutName; + private Field mArgVerbose; + private Field mArgJarOutput; + private Field mArgFileNames; + private Field mArgForceJumbo; + + private Field mConsoleOut; + private Field mConsoleErr; + + /** + * Loads the dex library from a file path. + * + * The loaded library can be used via + * {@link DexWrapper#run(String, String[], boolean, PrintStream, PrintStream)}. + * + * @param osFilepath the location of the dx.jar file. + * @return an IStatus indicating the result of the load. + */ + public synchronized IStatus loadDex(String osFilepath) { + try { + File f = new File(osFilepath); + if (f.isFile() == false) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, String.format( + Messages.DexWrapper_s_does_not_exists, osFilepath)); + } + URL url = f.toURI().toURL(); + + @SuppressWarnings("resource") + URLClassLoader loader = new URLClassLoader(new URL[] { url }, + DexWrapper.class.getClassLoader()); + + // get the classes. + Class<?> mainClass = loader.loadClass(DEX_MAIN); + Class<?> consoleClass = loader.loadClass(DEX_CONSOLE); + Class<?> argClass = loader.loadClass(DEX_ARGS); + + try { + // now get the fields/methods we need + mRunMethod = mainClass.getMethod(MAIN_RUN, argClass); + + mArgConstructor = argClass.getConstructor(); + mArgOutName = argClass.getField("outName"); //$NON-NLS-1$ + mArgJarOutput = argClass.getField("jarOutput"); //$NON-NLS-1$ + mArgFileNames = argClass.getField("fileNames"); //$NON-NLS-1$ + mArgVerbose = argClass.getField("verbose"); //$NON-NLS-1$ + mArgForceJumbo = argClass.getField("forceJumbo"); //$NON-NLS-1$ + + mConsoleOut = consoleClass.getField("out"); //$NON-NLS-1$ + mConsoleErr = consoleClass.getField("err"); //$NON-NLS-1$ + + } catch (SecurityException e) { + return createErrorStatus(Messages.DexWrapper_SecuryEx_Unable_To_Find_API, e); + } catch (NoSuchMethodException e) { + return createErrorStatus(Messages.DexWrapper_SecuryEx_Unable_To_Find_Method, e); + } catch (NoSuchFieldException e) { + return createErrorStatus(Messages.DexWrapper_SecuryEx_Unable_To_Find_Field, e); + } + + return Status.OK_STATUS; + } catch (MalformedURLException e) { + // really this should not happen. + return createErrorStatus( + String.format(Messages.DexWrapper_Failed_to_load_s, osFilepath), e); + } catch (ClassNotFoundException e) { + return createErrorStatus( + String.format(Messages.DexWrapper_Failed_to_load_s, osFilepath), e); + } + } + + /** + * Removes any reference to the dex library. + * <p/> + * {@link #loadDex(String)} must be called on the wrapper + * before {@link #run(String, String[], boolean, PrintStream, PrintStream)} can + * be used again. + */ + public synchronized void unload() { + mRunMethod = null; + mArgConstructor = null; + mArgOutName = null; + mArgJarOutput = null; + mArgFileNames = null; + mArgVerbose = null; + mConsoleOut = null; + mConsoleErr = null; + System.gc(); + } + + /** + * Runs the dex command. + * The wrapper must have been initialized via {@link #loadDex(String)} first. + * + * @param osOutFilePath the OS path to the outputfile (classes.dex + * @param osFilenames list of input source files (.class and .jar files) + * @param forceJumbo force jumbo mode. + * @param verbose verbose mode. + * @param outStream the stdout console + * @param errStream the stderr console + * @return the integer return code of com.android.dx.command.dexer.Main.run() + * @throws CoreException + */ + public synchronized int run(String osOutFilePath, Collection<String> osFilenames, + boolean forceJumbo, boolean verbose, + PrintStream outStream, PrintStream errStream) throws CoreException { + + assert mRunMethod != null; + assert mArgConstructor != null; + assert mArgOutName != null; + assert mArgJarOutput != null; + assert mArgFileNames != null; + assert mArgForceJumbo != null; + assert mArgVerbose != null; + assert mConsoleOut != null; + assert mConsoleErr != null; + + if (mRunMethod == null) { + throw new CoreException(createErrorStatus( + String.format(Messages.DexWrapper_Unable_To_Execute_Dex_s, + "wrapper was not properly loaded first"), + null /*exception*/)); + } + + try { + // set the stream + mConsoleErr.set(null /* obj: static field */, errStream); + mConsoleOut.set(null /* obj: static field */, outStream); + + // create the Arguments object. + Object args = mArgConstructor.newInstance(); + mArgOutName.set(args, osOutFilePath); + mArgFileNames.set(args, osFilenames.toArray(new String[osFilenames.size()])); + mArgJarOutput.set(args, osOutFilePath.endsWith(SdkConstants.DOT_JAR)); + mArgForceJumbo.set(args, forceJumbo); + mArgVerbose.set(args, verbose); + + // call the run method + Object res = mRunMethod.invoke(null /* obj: static method */, args); + + if (res instanceof Integer) { + return ((Integer)res).intValue(); + } + + return -1; + } catch (Exception e) { + Throwable t = e; + while (t.getCause() != null) { + t = t.getCause(); + } + + String msg = t.getMessage(); + if (msg == null) { + msg = String.format("%s. Check the Eclipse log for stack trace.", + t.getClass().getName()); + } + + throw new CoreException(createErrorStatus( + String.format(Messages.DexWrapper_Unable_To_Execute_Dex_s, msg), t)); + } + } + + private static IStatus createErrorStatus(String message, Throwable e) { + AdtPlugin.log(e, message); + AdtPlugin.printErrorToConsole(Messages.DexWrapper_Dex_Loader, message); + + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message, e); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ExecResultException.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ExecResultException.java new file mode 100644 index 000000000..63a7a6946 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ExecResultException.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build; + +/** + * Base exception class containing the error code and output of an external tool failed exec. + * + */ +class ExecResultException extends Exception { + private static final long serialVersionUID = 1L; + + private final int mErrorCode; + private final String[] mOutput; + + protected ExecResultException(int errorCode, String[] output) { + mErrorCode = errorCode; + mOutput = output; + } + + /** + * Returns the full output of aapt. + */ + public String[] getOutput() { + return mOutput; + } + + /** + * Returns the aapt return code. + */ + public int getErrorCode() { + return mErrorCode; + } + + public String getLabel() { + return "Command-line"; + } + + @Override + public String toString() { + String result = String.format("%1$s Error %2$d", getLabel(), mErrorCode); + if (mOutput != null && mOutput.length > 0) { + // Note : the "error detail" window in Eclipse seem to ignore the \n, + // so we prefix them with a space. It's not optimal but it's slightly readable. + result += " \nOutput:"; + for (String o : mOutput) { + if (o != null) { + result += " \n" + o; + } + } + } + return result; + } + + @Override + public String getMessage() { + return toString(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/Messages.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/Messages.java new file mode 100644 index 000000000..9ceba205d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/Messages.java @@ -0,0 +1,150 @@ + +package com.android.ide.eclipse.adt.internal.build; + +import org.eclipse.osgi.util.NLS; + +public class Messages extends NLS { + private static final String BUNDLE_NAME = "com.android.ide.eclipse.adt.internal.build.build_messages"; //$NON-NLS-1$ + + public static String AAPT_Error; + + public static String AAPT_Exec_Error_s; + + public static String AAPT_Exec_Error_d; + + public static String AAPT_Exec_Error_Other_s; + + public static String Added_s_s_Needs_Updating; + + public static String AIDL_Exec_Error_s; + + public static String AIDL_Exec_Error_d; + + public static String AIDL_Java_Conflict; + + public static String ApkBuilder_Certificate_Expired_on_s; + + public static String ApkBuilder_JAVA_HOME_is_s; + + public static String ApkBuilder_Packaging_s; + + public static String ApkBuilder_Packaging_s_into_s; + + public static String ApkBuilder_s_Conflict_with_file_s; + + public static String ApkBuilder_Signing_Key_Creation_s; + + public static String ApkBuilder_Unable_To_Gey_Key; + + public static String ApkBuilder_UnableBuild_Dex_Not_loaded; + + public static String ApkBuilder_Update_or_Execute_manually_s; + + public static String ApkBuilder_Using_Default_Key; + + public static String ApkBuilder_Using_s_To_Sign; + + public static String Checking_Package_Change; + + public static String Compiler_Compliance_Error; + + public static String Couldnt_Locate_s_Error; + + public static String Dalvik_Error_d; + + public static String Dalvik_Error_s; + + public static String Delete_Obsolete_Error; + + public static String DexWrapper_Dex_Loader; + + public static String DexWrapper_Failed_to_load_s; + + public static String DexWrapper_s_does_not_exists; + + public static String DexWrapper_SecuryEx_Unable_To_Find_API; + + public static String DexWrapper_SecuryEx_Unable_To_Find_Field; + + public static String DexWrapper_SecuryEx_Unable_To_Find_Method; + + public static String DexWrapper_Unable_To_Execute_Dex_s; + + public static String DX_Jar_Error; + + public static String Failed_To_Get_Output; + + public static String Final_Archive_Error_s; + + public static String Incompatible_VM_Warning; + + public static String Marker_Delete_Error; + + public static String No_SDK_Setup_Error; + + public static String Nothing_To_Compile; + + public static String Output_Missing; + + public static String Package_s_Doesnt_Exist_Error; + + public static String Preparing_Generated_Files; + + public static String Project_Has_Errors; + + public static String Refreshing_Res; + + public static String Removing_Generated_Classes; + + public static String Requires_1_5_Error; + + public static String Requires_Class_Compatibility_s; + + public static String Requires_Compiler_Compliance_s; + + public static String Requires_Source_Compatibility_s; + + public static String s_Contains_Xml_Error; + + public static String s_Doesnt_Declare_Package_Error; + + public static String s_File_Missing; + + public static String s_Missing_Repackaging; + + public static String s_Modified_Manually_Recreating_s; + + public static String s_Modified_Recreating_s; + + public static String s_Removed_Recreating_s; + + public static String s_Removed_s_Needs_Updating; + + public static String Start_Full_Apk_Build; + + public static String Start_Full_Pre_Compiler; + + public static String Skip_Post_Compiler; + + public static String Start_Full_Post_Compiler; + + public static String Start_Inc_Apk_Build; + + public static String Start_Inc_Pre_Compiler; + + public static String Unparsed_AAPT_Errors; + + public static String Unparsed_AIDL_Errors; + + public static String Xml_Error; + + public static String Proguard_Exec_Error; + + static { + // initialize resource bundle + NLS.initializeMessages(BUNDLE_NAME, Messages.class); + } + + private Messages() { + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/NativeLibInJarException.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/NativeLibInJarException.java new file mode 100644 index 000000000..18f4ae7aa --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/NativeLibInJarException.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build; + +import com.android.sdklib.build.ApkBuilder.JarStatus; + +/** + * Exception throw when native libraries are detected in jar file. + * + */ +public final class NativeLibInJarException extends Exception { + private static final long serialVersionUID = 1L; + + private final JarStatus mStatus; + private final String mLibName; + private final String[] mConsoleMsgs; + + NativeLibInJarException(JarStatus status, String message, String libName, + String[] consoleMsgs) { + super(message); + mStatus = status; + mLibName = libName; + mConsoleMsgs = consoleMsgs; + } + + /** + * Returns the {@link JarStatus} object containing the information about the libraries that + * were found. + */ + public JarStatus getStatus() { + return mStatus; + } + + /** + * Returns the name of the jar file containing the native libraries. + */ + public String getJarName() { + return mLibName; + } + + /** + * Returns additional information that should be shown to the user. + */ + public String[] getAdditionalInfo() { + return mConsoleMsgs; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ProguardExecException.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ProguardExecException.java new file mode 100644 index 000000000..3eb688b3e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ProguardExecException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build; + +/** + * Exception thrown when the execution of proguard fails. + * + */ +public final class ProguardExecException extends Exception { + private static final long serialVersionUID = 1L; + + ProguardExecException(String message, Throwable cause) { + super(message, cause); + } +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ProguardResultException.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ProguardResultException.java new file mode 100644 index 000000000..54246b337 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ProguardResultException.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build; + +/** + * Exception thrown when aapt reports an error in the resources. + * + */ +public final class ProguardResultException extends ExecResultException { + private static final long serialVersionUID = 1L; + + ProguardResultException(int errorCode, String[] output) { + super(errorCode, output); + } + + @Override + public String getLabel() { + return "Proguard"; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/RenderScriptLauncher.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/RenderScriptLauncher.java new file mode 100644 index 000000000..1d3c7bd3a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/RenderScriptLauncher.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build; + +import com.android.annotations.NonNull; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.sdklib.build.RenderScriptProcessor.CommandLineLauncher; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Path; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A {@link SourceProcessor} for RenderScript files. + */ +public class RenderScriptLauncher implements CommandLineLauncher { + + /** + * Single line llvm-rs-cc error: {@code <path>:<line>:<col>: <error>} + */ + private static Pattern sLlvmPattern1 = Pattern.compile("^(.+?):(\\d+):(\\d+):\\s(.+)$"); //$NON-NLS-1$ + + @NonNull + private final IProject mProject; + @NonNull + private final IFolder mSourceOutFolder; + @NonNull + private final IFolder mResOutFolder; + @NonNull + private final IProgressMonitor mMonitor; + private final boolean mVerbose; + + public RenderScriptLauncher( + @NonNull IProject project, + @NonNull IFolder sourceOutFolder, + @NonNull IFolder resOutFolder, + @NonNull IProgressMonitor monitor, + boolean verbose) { + mProject = project; + mSourceOutFolder = sourceOutFolder; + mResOutFolder = resOutFolder; + mMonitor = monitor; + mVerbose = verbose; + } + + @Override + public void launch(File executable, List<String> arguments, Map<String, String> envVariableMap) + throws IOException, InterruptedException { + // do the exec + try { + if (mVerbose) { + StringBuilder sb = new StringBuilder(executable.getAbsolutePath()); + for (String c : arguments) { + sb.append(' ').append(c); + } + String cmd_line = sb.toString(); + AdtPlugin.printToConsole(mProject, cmd_line); + } + + String[] commandArray = new String[1 + arguments.size()]; + commandArray[0] = executable.getAbsolutePath(); + System.arraycopy(arguments.toArray(), 0, commandArray, 1, arguments.size()); + + ProcessBuilder processBuilder = new ProcessBuilder(commandArray); + Map<String, String> processEnvs = processBuilder.environment(); + for (Map.Entry<String, String> entry : envVariableMap.entrySet()) { + processEnvs.put(entry.getKey(), entry.getValue()); + } + + Process p = processBuilder.start(); + + // list to store each line of stderr + ArrayList<String> stdErr = new ArrayList<String>(); + + // get the output and return code from the process + int returnCode = BuildHelper.grabProcessOutput(mProject, p, stdErr); + + if (stdErr.size() > 0) { + // attempt to parse the error output + boolean parsingError = parseLlvmOutput(stdErr); + + // If the process failed and we couldn't parse the output + // we print a message, mark the project and exit + if (returnCode != 0) { + + if (parsingError || mVerbose) { + // display the message in the console. + if (parsingError) { + AdtPlugin.printErrorToConsole(mProject, stdErr.toArray()); + + // mark the project + BaseProjectHelper.markResource(mProject, + AdtConstants.MARKER_RENDERSCRIPT, + "Unparsed Renderscript error! Check the console for output.", + IMarker.SEVERITY_ERROR); + } else { + AdtPlugin.printToConsole(mProject, stdErr.toArray()); + } + } + return; + } + } else if (returnCode != 0) { + // no stderr output but exec failed. + String msg = String.format("Error executing Renderscript: Return code %1$d", + returnCode); + + BaseProjectHelper.markResource(mProject, AdtConstants.MARKER_AIDL, + msg, IMarker.SEVERITY_ERROR); + + return; + } + } catch (IOException e) { + // mark the project and exit + String msg = String.format( + "Error executing Renderscript. Please check %1$s is present at %2$s", + executable.getName(), executable.getAbsolutePath()); + AdtPlugin.log(IStatus.ERROR, msg); + BaseProjectHelper.markResource(mProject, AdtConstants.MARKER_RENDERSCRIPT, msg, + IMarker.SEVERITY_ERROR); + throw e; + } catch (InterruptedException e) { + // mark the project and exit + String msg = String.format( + "Error executing Renderscript. Please check %1$s is present at %2$s", + executable.getName(), executable.getAbsolutePath()); + AdtPlugin.log(IStatus.ERROR, msg); + BaseProjectHelper.markResource(mProject, AdtConstants.MARKER_RENDERSCRIPT, msg, + IMarker.SEVERITY_ERROR); + throw e; + } + + try { + mSourceOutFolder.refreshLocal(IResource.DEPTH_ONE, mMonitor); + mResOutFolder.refreshLocal(IResource.DEPTH_ONE, mMonitor); + } catch (CoreException e) { + AdtPlugin.log(e, "failed to refresh folders"); + } + + return; + } + + /** + * Parse the output of llvm-rs-cc and mark the file with any errors. + * @param lines The output to parse. + * @return true if the parsing failed, false if success. + */ + private boolean parseLlvmOutput(ArrayList<String> lines) { + // nothing to parse? just return false; + if (lines.size() == 0) { + return false; + } + + // get the root folder for the project as we're going to ignore everything that's + // not in the project + String rootPath = mProject.getLocation().toOSString(); + int rootPathLength = rootPath.length(); + + Matcher m; + + boolean parsing = false; + + for (int i = 0; i < lines.size(); i++) { + String p = lines.get(i); + + m = sLlvmPattern1.matcher(p); + if (m.matches()) { + // get the file path. This may, or may not be the main file being compiled. + String filePath = m.group(1); + if (filePath.startsWith(rootPath) == false) { + // looks like the error in a non-project file. Keep parsing, but + // we'll return true + parsing = true; + continue; + } + + // get the actual file. + filePath = filePath.substring(rootPathLength); + // remove starting separator since we want the path to be relative + if (filePath.startsWith(File.separator)) { + filePath = filePath.substring(1); + } + + // get the file + IFile f = mProject.getFile(new Path(filePath)); + + String lineStr = m.group(2); + // ignore group 3 for now, this is the col number + String msg = m.group(4); + + // get the line number + int line = 0; + try { + line = Integer.parseInt(lineStr); + } catch (NumberFormatException e) { + // looks like the string we extracted wasn't a valid + // file number. Parsing failed and we return true + return true; + } + + // mark the file + BaseProjectHelper.markResource(f, AdtConstants.MARKER_RENDERSCRIPT, msg, line, + IMarker.SEVERITY_ERROR); + + // success, go to the next line + continue; + } + + // invalid line format, flag as error, and keep going + parsing = true; + } + + return parsing; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/RsSourceChangeHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/RsSourceChangeHandler.java new file mode 100644 index 000000000..715895a0c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/RsSourceChangeHandler.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.sdklib.build.RenderScriptChecker; + +import org.eclipse.core.resources.IFile; + +import java.io.File; +import java.io.IOException; +import java.util.Set; + +public class RsSourceChangeHandler implements SourceChangeHandler { + + private final RenderScriptChecker mChecker; + private boolean mIsCheckerLoaded = false; + + private boolean mMustCompile = false; + + public RsSourceChangeHandler(@NonNull RenderScriptChecker checker) { + mChecker = checker; + } + + @Override + public boolean handleGeneratedFile(IFile file, int kind) { + if (mMustCompile) { + return false; + } + + if (!mIsCheckerLoaded) { + try { + mChecker.loadDependencies(); + } catch (IOException e) { + // failed to load the dependency files, force a compilation, log the error. + AdtPlugin.log(e, "Failed to read dependency files"); + mMustCompile = true; + return false; + } + } + + Set<File> oldOutputs = mChecker.getOldOutputs(); + // mustCompile is always false here. + mMustCompile = oldOutputs.contains(file.getLocation().toFile()); + return mMustCompile; + } + + @Override + public void handleSourceFile(IFile file, int kind) { + if (mMustCompile) { + return; + } + + String ext = file.getFileExtension(); + if (SdkConstants.EXT_RS.equals(ext) || + SdkConstants.EXT_FS.equals(ext) || + SdkConstants.EXT_RSH.equals(ext)) { + mMustCompile = true; + } + } + + public boolean mustCompile() { + return mMustCompile; + } + + @NonNull + public RenderScriptChecker getChecker() { + return mChecker; + } + + public void prepareFullBuild() { + mMustCompile = true; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceChangeHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceChangeHandler.java new file mode 100644 index 000000000..12a055106 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceChangeHandler.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build; + +import org.eclipse.core.resources.IFile; + +public interface SourceChangeHandler { + + boolean handleGeneratedFile(IFile file, int kind); + void handleSourceFile(IFile file, int kind); +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceFileData.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceFileData.java new file mode 100644 index 000000000..d06bf1613 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceFileData.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build; + +import org.eclipse.core.resources.IFile; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Data for Android-specific source files. It contains a list of output files and a list + * of dependencies. + * The source file itself is a implied dependency and is not meant to be in the dependency list. + */ +public class SourceFileData { + + private final IFile mSourceFile; + private final List<IFile> mOutputFiles = new ArrayList<IFile>(); + private final List<IFile> mDependencyFiles = new ArrayList<IFile>(); + + public SourceFileData(IFile sourceFile) { + this(sourceFile, null, null); + } + + SourceFileData(IFile sourceFile, + List<IFile> outputFiles, List<IFile> dependencyFiles) { + mSourceFile = sourceFile; + if (outputFiles != null) { + mOutputFiles.addAll(outputFiles); + } + if (dependencyFiles != null) { + mDependencyFiles.addAll(dependencyFiles); + } + } + + SourceFileData(IFile sourceFile, IFile outputFile) { + mSourceFile = sourceFile; + if (outputFile != null) { + mOutputFiles.add(outputFile); + } + } + + /** + * Returns the source file as an {@link IFile} + */ + public IFile getSourceFile() { + return mSourceFile; + } + + /** + * Returns whether the given file is a dependency for this source file. + * <p/>Note that the source file itself is not tested against. Therefore if + * {@code file.equals(getSourceFile()} returns {@code true}, this method will return + * {@code false}. + * @param file the file to check against + * @return true if the given file is a dependency for this source file. + */ + public boolean dependsOn(IFile file) { + return mDependencyFiles.contains(file); + } + + /** + * Returns whether the given file is an ouput of this source file. + * @param file the file to test. + * @return true if the file is an output file. + */ + public boolean generated(IFile file) { + return mOutputFiles.contains(file); + } + + void setOutputFiles(List<IFile> outputFiles) { + mOutputFiles.clear(); + if (outputFiles != null) { + mOutputFiles.addAll(outputFiles); + } + } + + void setOutputFile(IFile outputFile) { + mOutputFiles.clear(); + if (outputFile != null) { + mOutputFiles.add(outputFile); + } + } + + void setDependencyFiles(List<IFile> depFiles) { + mDependencyFiles.clear(); + if (depFiles != null) { + mDependencyFiles.addAll(depFiles); + } + } + + public List<IFile> getDependencyFiles() { + return mDependencyFiles; + } + + /** + * Shortcut access to the first output file. This is useful for generator that only output + * one file. + */ + public IFile getOutput() { + if (mOutputFiles.size() > 0) { + return mOutputFiles.get(0); + } + + return null; + } + + public List<IFile> getOutputFiles() { + return Collections.unmodifiableList(mOutputFiles); + } + + @Override + public String toString() { + return "NonJavaFileBundle [mSourceFile=" + mSourceFile + ", mGeneratedFiles=" + + mOutputFiles + ", mDependencies=" + mDependencyFiles + "]"; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceProcessor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceProcessor.java new file mode 100644 index 000000000..1fc3e4e53 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceProcessor.java @@ -0,0 +1,454 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.ide.eclipse.adt.internal.build.builders.BaseBuilder; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.sdklib.BuildToolInfo; +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.IJavaProject; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/** + * Base class to handle generated java code. + * + * It provides management for modified source file list, deleted source file list, reconciliation + * of previous lists, storing the current state of the build. + * + */ +public abstract class SourceProcessor { + + public final static int COMPILE_STATUS_NONE = 0; + public final static int COMPILE_STATUS_CODE = 0x1; + public final static int COMPILE_STATUS_RES = 0x2; + + /** List of all source files, their dependencies, and their output. */ + private final Map<IFile, SourceFileData> mFiles = new HashMap<IFile, SourceFileData>(); + + private final IJavaProject mJavaProject; + private BuildToolInfo mBuildToolInfo; + private final IFolder mGenFolder; + private final DefaultSourceChangeHandler mDeltaVisitor; + + /** List of source files pending compilation at the next build */ + private final List<IFile> mToCompile = new ArrayList<IFile>(); + + /** List of removed source files pending cleaning at the next build. */ + private final List<IFile> mRemoved = new ArrayList<IFile>(); + + private int mLastCompilationStatus = COMPILE_STATUS_NONE; + + /** + * Quotes a path inside "". If the platform is not windows, the path is returned as is. + * @param path the path to quote + * @return the quoted path. + */ + public static String quote(String path) { + if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) { + if (path.endsWith(File.separator)) { + path = path.substring(0, path.length() -1); + } + return "\"" + path + "\""; + } + + return path; + } + + protected SourceProcessor( + @NonNull IJavaProject javaProject, + @NonNull BuildToolInfo buildToolInfo, + @NonNull IFolder genFolder, + @NonNull DefaultSourceChangeHandler deltaVisitor) { + mJavaProject = javaProject; + mBuildToolInfo = buildToolInfo; + mGenFolder = genFolder; + mDeltaVisitor = deltaVisitor; + + mDeltaVisitor.init(this); + + IProject project = javaProject.getProject(); + + // get all the source files + buildSourceFileList(); + + // load the known dependencies + loadOutputAndDependencies(); + + boolean mustCompile = loadState(project); + + // if we stored that we have to compile some files, we build the list that will compile them + // all. For now we have to reuse the full list since we don't know which files needed + // compilation. + if (mustCompile) { + mToCompile.addAll(mFiles.keySet()); + } + } + + protected SourceProcessor( + @NonNull IJavaProject javaProject, + @NonNull BuildToolInfo buildToolInfo, + @NonNull IFolder genFolder) { + this(javaProject, buildToolInfo, genFolder, new DefaultSourceChangeHandler()); + } + + public void setBuildToolInfo(BuildToolInfo buildToolInfo) { + mBuildToolInfo = buildToolInfo; + } + + + /** + * Returns whether the given file is an output of this processor by return the source + * file that generated it. + * @param file the file to test. + * @return the source file that generated the given file or null. + */ + IFile isOutput(IFile file) { + for (SourceFileData data : mFiles.values()) { + if (data.generated(file)) { + return data.getSourceFile(); + } + } + + return null; + } + + /** + * Returns whether the given file is a dependency for other files by returning a list + * of file depending on the given file. + * @param file the file to test. + * @return a list of files that depend on the given file or an empty list if there + * are no matches. + */ + List<IFile> isDependency(IFile file) { + ArrayList<IFile> files = new ArrayList<IFile>(); + for (SourceFileData data : mFiles.values()) { + if (data.dependsOn(file)) { + files.add(data.getSourceFile()); + } + } + + return files; + } + + void addData(SourceFileData data) { + mFiles.put(data.getSourceFile(), data); + } + + SourceFileData getFileData(IFile file) { + return mFiles.get(file); + } + + Collection<SourceFileData> getAllFileData() { + return mFiles.values(); + } + + public final DefaultSourceChangeHandler getChangeHandler() { + return mDeltaVisitor; + } + + final IJavaProject getJavaProject() { + return mJavaProject; + } + + final BuildToolInfo getBuildToolInfo() { + return mBuildToolInfo; + } + + final IFolder getGenFolder() { + return mGenFolder; + } + + final List<IFile> getToCompile() { + return mToCompile; + } + + final List<IFile> getRemovedFile() { + return mRemoved; + } + + final void addFileToCompile(IFile file) { + mToCompile.add(file); + } + + public final void prepareFullBuild(IProject project) { + mDeltaVisitor.reset(); + + mToCompile.clear(); + mRemoved.clear(); + + // get all the source files + buildSourceFileList(); + + mToCompile.addAll(mFiles.keySet()); + + saveState(project); + } + + public final void doneVisiting(IProject project) { + // merge the previous file modification lists and the new one. + mergeFileModifications(mDeltaVisitor); + + mDeltaVisitor.reset(); + + saveState(project); + } + + /** + * Returns the extension of the source files handled by this processor. + * @return + */ + protected abstract Set<String> getExtensions(); + + protected abstract String getSavePropertyName(); + + /** + * Compiles the source files and return a status bitmask of the type of file that was generated. + * + */ + public final int compileFiles(BaseBuilder builder, + IProject project, IAndroidTarget projectTarget, + List<IPath> sourceFolders, List<File> libraryProjectsOut, IProgressMonitor monitor) + throws CoreException { + + mLastCompilationStatus = COMPILE_STATUS_NONE; + + if (mToCompile.size() == 0 && mRemoved.size() == 0) { + return mLastCompilationStatus; + } + + // if a source file is being removed before we managed to compile it, it'll be in + // both list. We *need* to remove it from the compile list or it'll never go away. + for (IFile sourceFile : mRemoved) { + int pos = mToCompile.indexOf(sourceFile); + if (pos != -1) { + mToCompile.remove(pos); + } + } + + // list of files that have failed compilation. + List<IFile> stillNeedCompilation = new ArrayList<IFile>(); + + doCompileFiles(mToCompile, builder, project, projectTarget, sourceFolders, + stillNeedCompilation, libraryProjectsOut, monitor); + + mToCompile.clear(); + mToCompile.addAll(stillNeedCompilation); + + // Remove the files created from source files that have been removed. + for (IFile sourceFile : mRemoved) { + // look if we already know the output + SourceFileData data = getFileData(sourceFile); + if (data != null) { + doRemoveFiles(data); + } + } + + // remove the associated file data. + for (IFile removedFile : mRemoved) { + mFiles.remove(removedFile); + } + + mRemoved.clear(); + + // store the build state. If there are any files that failed to compile, we will + // force a full aidl compile on the next project open. (unless a full compilation succeed + // before the project is closed/re-opened.) + saveState(project); + + return mLastCompilationStatus; + } + + protected abstract void doCompileFiles( + List<IFile> filesToCompile, BaseBuilder builder, + IProject project, IAndroidTarget projectTarget, + List<IPath> sourceFolders, List<IFile> notCompiledOut, + List<File> libraryProjectsOut, IProgressMonitor monitor) throws CoreException; + + /** + * Adds a compilation status. It can be any of (in combination too): + * <p/> + * {@link #COMPILE_STATUS_CODE} means this processor created source code files. + * {@link #COMPILE_STATUS_RES} means this process created resources. + */ + protected void setCompilationStatus(int status) { + mLastCompilationStatus |= status; + } + + protected void doRemoveFiles(SourceFileData data) throws CoreException { + List<IFile> outputFiles = data.getOutputFiles(); + for (IFile outputFile : outputFiles) { + if (outputFile.exists()) { + outputFile.getLocation().toFile().delete(); + } + } + } + + public final boolean loadState(IProject project) { + return ProjectHelper.loadBooleanProperty(project, getSavePropertyName(), + true /*defaultValue*/); + } + + public final void saveState(IProject project) { + // TODO: Optimize by saving only the files that need compilation + ProjectHelper.saveStringProperty(project, getSavePropertyName(), + Boolean.toString(mToCompile.size() > 0)); + } + + protected abstract void loadOutputAndDependencies(); + + + protected IPath getSourceFolderFor(IFile file) { + // find the source folder for the class so that we can infer the package from the + // difference between the file and its source folder. + List<IPath> sourceFolders = BaseProjectHelper.getSourceClasspaths(getJavaProject()); + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + + for (IPath sourceFolderPath : sourceFolders) { + IFolder sourceFolder = root.getFolder(sourceFolderPath); + // we don't look in the 'gen' source folder as there will be no source in there. + if (sourceFolder.exists() && sourceFolder.equals(getGenFolder()) == false) { + // look for the source file parent, until we find this source folder. + IResource parent = file; + while ((parent = parent.getParent()) != null) { + if (parent.equals(sourceFolder)) { + return sourceFolderPath; + } + } + } + } + + return null; + } + + /** + * Goes through the build paths and fills the list of files to compile. + * + * @param project The project. + * @param sourceFolderPathList The list of source folder paths. + */ + private final void buildSourceFileList() { + mFiles.clear(); + + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + List<IPath> sourceFolderPathList = BaseProjectHelper.getSourceClasspaths(mJavaProject); + + for (IPath sourceFolderPath : sourceFolderPathList) { + IFolder sourceFolder = root.getFolder(sourceFolderPath); + // we don't look in the 'gen' source folder as there will be no source in there. + if (sourceFolder.exists() && sourceFolder.equals(getGenFolder()) == false) { + scanFolderForSourceFiles(sourceFolder, sourceFolder); + } + } + } + + /** + * Scans a folder and fills the list of files to compile. + * @param sourceFolder the root source folder. + * @param folder The folder to scan. + */ + private void scanFolderForSourceFiles(IFolder sourceFolder, IFolder folder) { + try { + IResource[] members = folder.members(); + for (IResource r : members) { + // get the type of the resource + switch (r.getType()) { + case IResource.FILE: { + // if this a file, check that the file actually exist + // and that it's the type of of file that's used in this processor + String extension = r.exists() ? r.getFileExtension() : null; + if (extension != null && + getExtensions().contains(extension.toLowerCase(Locale.US))) { + mFiles.put((IFile) r, new SourceFileData((IFile) r)); + } + break; + } + case IResource.FOLDER: + // recursively go through children + scanFolderForSourceFiles(sourceFolder, (IFolder)r); + break; + default: + // this would mean it's a project or the workspace root + // which is unlikely to happen. we do nothing + break; + } + } + } catch (CoreException e) { + // Couldn't get the members list for some reason. Just return. + } + } + + + /** + * Merge the current list of source file to compile/remove with the one coming from the + * delta visitor + * @param visitor the delta visitor. + */ + private void mergeFileModifications(DefaultSourceChangeHandler visitor) { + Set<IFile> toRemove = visitor.getRemovedFiles(); + Set<IFile> toCompile = visitor.getFilesToCompile(); + + // loop through the new toRemove list, and add it to the old one, + // plus remove any file that was still to compile and that are now + // removed + for (IFile r : toRemove) { + if (mRemoved.indexOf(r) == -1) { + mRemoved.add(r); + } + + int index = mToCompile.indexOf(r); + if (index != -1) { + mToCompile.remove(index); + } + } + + // now loop through the new files to compile and add it to the list. + // Also look for them in the remove list, this would mean that they + // were removed, then added back, and we shouldn't remove them, just + // recompile them. + for (IFile r : toCompile) { + if (mToCompile.indexOf(r) == -1) { + mToCompile.add(r); + } + + int index = mRemoved.indexOf(r); + if (index != -1) { + mRemoved.remove(index); + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/build_messages.properties b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/build_messages.properties new file mode 100644 index 000000000..f387ab5a9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/build_messages.properties @@ -0,0 +1,67 @@ +Start_Full_Apk_Build=Starting full Package build. +Start_Full_Pre_Compiler=Starting full Pre Compiler. +Skip_Post_Compiler=Skipping over Post Compiler. +Start_Full_Post_Compiler=Starting full Post Compiler. +Start_Inc_Apk_Build=Starting incremental Package build: Checking resource changes. +Start_Inc_Pre_Compiler=Starting incremental Pre Compiler: Checking resource changes. +Xml_Error=Error in an XML file: aborting build. +s_Missing_Repackaging=%1$s missing. Repackaging. +Project_Has_Errors=Project contains error(s). Package Builder aborted. +Failed_To_Get_Output=Failed to get project output folder\! +Output_Missing=Output folder missing\! Make sure your project is configured properly. +s_File_Missing=%1$s file missing\! +Unparsed_AAPT_Errors=Unparsed aapt error(s)\! Check the console for output. +Unparsed_AIDL_Errors=Unparsed aidl error\! Check the console for output. +AAPT_Exec_Error_s=Error executing aapt. Please check aapt is present at %1$s +AAPT_Exec_Error_d=Error executing aapt: Return code %1$d +AAPT_Exec_Error_Other_s=Error executing aapt: %1$s +Dalvik_Error_d=Conversion to Dalvik format failed with error %1$d +DX_Jar_Error=Dx.jar is not found inside the plugin. Reinstall ADT\! +Dalvik_Error_s=Conversion to Dalvik format failed: %1$s +Incompatible_VM_Warning=Note: You may be using an incompatible virtual machine or class library. +Requires_1_5_Error=This program requires JDK 1.5 compatibility. +Final_Archive_Error_s=Error generating final archive: %1$s +Marker_Delete_Error=Failed to delete marker '%1$s' for %2$s +Couldnt_Locate_s_Error=Could not locate '%1$s'. This will not be added to the package. +Compiler_Compliance_Error=Compiler compliance level not compatible: Build aborted. +No_SDK_Setup_Error=SDK directory has not been setup. Please go to the Android preferences and enter the location of the SDK. +s_Contains_Xml_Error=%1$s contains XML error: Build aborted. +s_Doesnt_Declare_Package_Error=%1$s does not declare a Java package: Build aborted. +Checking_Package_Change=Checking Java package value did not change... +Package_s_Doesnt_Exist_Error=Package '%1$s' does not exist\! +Preparing_Generated_Files=Preparing generated java files for update/creation. +AAPT_Error='aapt' error. Pre Compiler Build aborted. +Nothing_To_Compile=Nothing to pre compile\! +Removing_Generated_Classes=Removing generated java classes. +Delete_Obsolete_Error=Failed to delete obsolete %1$s, please delete it manually +DexWrapper_Dex_Loader=Dex Loader +AIDL_Java_Conflict=%1$s is in the way of %2$s, remove it or rename of one the files. +AIDL_Exec_Error_d=Error executing aidl: Return code %1$d +AIDL_Exec_Error_s=Error executing aidl. Please check aidl is present at %1$s +s_Removed_Recreating_s=%1$s was removed\! Recreating %1$s\! +s_Modified_Manually_Recreating_s=%1$s was modified manually\! Reverting to generated version\! +s_Modified_Recreating_s='%1$s' was modified. +Added_s_s_Needs_Updating=New resource file: '%1$s', %2$s needs to be updated. +s_Removed_s_Needs_Updating='%1$s' was removed, %2$s needs to be updated. +Requires_Compiler_Compliance_s=Android requires compiler compliance level 5.0 or 6.0. Found '%1$s' instead. Please use Android Tools > Fix Project Properties. +Requires_Source_Compatibility_s=Android requires source compatibility set to 5.0 or 6.0. Found '%1$s' instead. Please use Android Tools > Fix Project Properties. +Requires_Class_Compatibility_s=Android requires .class compatibility set to 5.0 or 6.0. Found '%1$s' instead. Please use Android Tools > Fix Project Properties. +Refreshing_Res=Refreshing resource folders. +DexWrapper_s_does_not_exists=%1$s does not exist or is not a file +DexWrapper_Failed_to_load_s=Failed to load %1$s +DexWrapper_Unable_To_Execute_Dex_s=Unable to execute dex: %1$s +DexWrapper_SecuryEx_Unable_To_Find_API=SecurityException: Unable to find API for dex.jar +DexWrapper_SecuryEx_Unable_To_Find_Method=SecurityException: Unable to find method for dex.jar +DexWrapper_SecuryEx_Unable_To_Find_Field=SecurityException: Unable to find field for dex.jar +ApkBuilder_UnableBuild_Dex_Not_loaded=Unable to build: the file dx.jar was not loaded from the SDK folder\! +ApkBuilder_Using_Default_Key=Using default debug key to sign package +ApkBuilder_Using_s_To_Sign=Using '%1$s' to sign package +ApkBuilder_Signing_Key_Creation_s=Signing Key Creation: +ApkBuilder_Unable_To_Gey_Key=Unable to get debug signature key +ApkBuilder_Certificate_Expired_on_s=Debug certificate expired on %1$s\! +ApkBuilder_Packaging_s=Packaging %1$s +ApkBuilder_JAVA_HOME_is_s=The Java VM Home used is: %1$s +ApkBuilder_Update_or_Execute_manually_s=Update it if necessary, or manually execute the following command: +ApkBuilder_s_Conflict_with_file_s=%1$s conflicts with another file already put at %2$s +ApkBuilder_Packaging_s_into_s=Packaging %1$s into %2$s +Proguard_Exec_Error=Error executing Proguard. Please check Proguard is present at %1$s diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/BaseBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/BaseBuilder.java new file mode 100644 index 000000000..162591406 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/BaseBuilder.java @@ -0,0 +1,483 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build.builders; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.sdk.LoadStatus; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.build.BuildHelper; +import com.android.ide.eclipse.adt.internal.build.Messages; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler; +import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler.XmlErrorListener; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.io.IFileWrapper; +import com.android.io.IAbstractFile; +import com.android.io.StreamException; +import com.android.sdklib.BuildToolInfo; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.repository.FullRevision; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jdt.core.IJavaProject; +import org.xml.sax.SAXException; + +import java.util.ArrayList; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +/** + * Base builder for XML files. This class allows for basic XML parsing with + * error checking and marking the files for errors/warnings. + */ +public abstract class BaseBuilder extends IncrementalProjectBuilder { + + protected static final boolean DEBUG_LOG = "1".equals( //$NON-NLS-1$ + System.getenv("ANDROID_BUILD_DEBUG")); //$NON-NLS-1$ + + /** SAX Parser factory. */ + private SAXParserFactory mParserFactory; + + /** + * The build tool to use to build. This is guaranteed to be non null after a call to + * {@link #abortOnBadSetup(IJavaProject, ProjectState)} since this will throw if it can't be + * queried. + */ + protected BuildToolInfo mBuildToolInfo; + + /** + * Base Resource Delta Visitor to handle XML error + */ + protected static class BaseDeltaVisitor implements XmlErrorListener { + + /** The Xml builder used to validate XML correctness. */ + protected BaseBuilder mBuilder; + + /** + * XML error flag. if true, we keep parsing the ResourceDelta but the + * compilation will not happen (we're putting markers) + */ + public boolean mXmlError = false; + + public BaseDeltaVisitor(BaseBuilder builder) { + mBuilder = builder; + } + + /** + * Finds a matching Source folder for the current path. This checks if the current path + * leads to, or is a source folder. + * @param sourceFolders The list of source folders + * @param pathSegments The segments of the current path + * @return The segments of the source folder, or null if no match was found + */ + protected static String[] findMatchingSourceFolder(ArrayList<IPath> sourceFolders, + String[] pathSegments) { + + for (IPath p : sourceFolders) { + // check if we are inside one of those source class path + + // get the segments + String[] srcSegments = p.segments(); + + // compare segments. We want the path of the resource + // we're visiting to be + boolean valid = true; + int segmentCount = pathSegments.length; + + for (int i = 0 ; i < segmentCount; i++) { + String s1 = pathSegments[i]; + String s2 = srcSegments[i]; + + if (s1.equalsIgnoreCase(s2) == false) { + valid = false; + break; + } + } + + if (valid) { + // this folder, or one of this children is a source + // folder! + // we return its segments + return srcSegments; + } + } + + return null; + } + + /** + * Sent when an XML error is detected. + * @see XmlErrorListener + */ + @Override + public void errorFound() { + mXmlError = true; + } + } + + protected static class AbortBuildException extends Exception { + private static final long serialVersionUID = 1L; + } + + public BaseBuilder() { + super(); + mParserFactory = SAXParserFactory.newInstance(); + + // FIXME when the compiled XML support for namespace is in, set this to true. + mParserFactory.setNamespaceAware(false); + } + + /** + * Checks an Xml file for validity. Errors/warnings will be marked on the + * file + * @param resource the resource to check + * @param visitor a valid resource delta visitor + */ + protected final void checkXML(IResource resource, BaseDeltaVisitor visitor) { + + // first make sure this is an xml file + if (resource instanceof IFile) { + IFile file = (IFile)resource; + + // remove previous markers + removeMarkersFromResource(file, AdtConstants.MARKER_XML); + + // create the error handler + XmlErrorHandler reporter = new XmlErrorHandler(file, visitor); + try { + // parse + getParser().parse(file.getContents(), reporter); + } catch (Exception e1) { + } + } + } + + /** + * Returns the SAXParserFactory, instantiating it first if it's not already + * created. + * @return the SAXParserFactory object + * @throws ParserConfigurationException + * @throws SAXException + */ + protected final SAXParser getParser() throws ParserConfigurationException, + SAXException { + return mParserFactory.newSAXParser(); + } + + /** + * Adds a marker to the current project. This methods catches thrown {@link CoreException}, + * and returns null instead. + * + * @param markerId The id of the marker to add. + * @param message the message associated with the mark + * @param severity the severity of the marker. + * @return the marker that was created (or null if failure) + * @see IMarker + */ + protected final IMarker markProject(String markerId, String message, int severity) { + return BaseProjectHelper.markResource(getProject(), markerId, message, severity); + } + + /** + * Removes markers from a resource and only the resource (not its children). + * @param file The file from which to delete the markers. + * @param markerId The id of the markers to remove. If null, all marker of + * type <code>IMarker.PROBLEM</code> will be removed. + */ + public final void removeMarkersFromResource(IResource resource, String markerId) { + try { + if (resource.exists()) { + resource.deleteMarkers(markerId, true, IResource.DEPTH_ZERO); + } + } catch (CoreException ce) { + String msg = String.format(Messages.Marker_Delete_Error, markerId, resource.toString()); + AdtPlugin.printErrorToConsole(getProject(), msg); + } + } + + /** + * Removes markers from a container and its children. + * @param folder The container from which to delete the markers. + * @param markerId The id of the markers to remove. If null, all marker of + * type <code>IMarker.PROBLEM</code> will be removed. + */ + protected final void removeMarkersFromContainer(IContainer folder, String markerId) { + try { + if (folder.exists()) { + folder.deleteMarkers(markerId, true, IResource.DEPTH_INFINITE); + } + } catch (CoreException ce) { + String msg = String.format(Messages.Marker_Delete_Error, markerId, folder.toString()); + AdtPlugin.printErrorToConsole(getProject(), msg); + } + } + + /** + * Get the stderr output of a process and return when the process is done. + * @param process The process to get the ouput from + * @param stdErr The array to store the stderr output + * @return the process return code. + * @throws InterruptedException + */ + protected final int grabProcessOutput(final Process process, + final ArrayList<String> stdErr) throws InterruptedException { + return BuildHelper.grabProcessOutput(getProject(), process, stdErr); + } + + + + /** + * Saves a String property into the persistent storage of the project. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @param value the value to save + * @return true if the save succeeded. + */ + protected boolean saveProjectStringProperty(String propertyName, String value) { + IProject project = getProject(); + return ProjectHelper.saveStringProperty(project, propertyName, value); + } + + + /** + * Loads a String property from the persistent storage of the project. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @return the property value or null if it was not found. + */ + protected String loadProjectStringProperty(String propertyName) { + IProject project = getProject(); + return ProjectHelper.loadStringProperty(project, propertyName); + } + + /** + * Saves a property into the persistent storage of the project. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @param value the value to save + * @return true if the save succeeded. + */ + protected boolean saveProjectBooleanProperty(String propertyName, boolean value) { + IProject project = getProject(); + return ProjectHelper.saveStringProperty(project, propertyName, Boolean.toString(value)); + } + + /** + * Loads a boolean property from the persistent storage of the project. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @param defaultValue The default value to return if the property was not found. + * @return the property value or the default value if the property was not found. + */ + protected boolean loadProjectBooleanProperty(String propertyName, boolean defaultValue) { + IProject project = getProject(); + return ProjectHelper.loadBooleanProperty(project, propertyName, defaultValue); + } + + /** + * Aborts the build if the SDK/project setups are broken. This does not + * display any errors. + * + * @param javaProject The {@link IJavaProject} being compiled. + * @param projectState the project state, optional. will be queried if null. + * @throws CoreException + */ + protected void abortOnBadSetup(@NonNull IJavaProject javaProject, + @Nullable ProjectState projectState) throws AbortBuildException, CoreException { + IProject iProject = javaProject.getProject(); + // check if we have finished loading the project target. + Sdk sdk = Sdk.getCurrent(); + if (sdk == null) { + throw new AbortBuildException(); + } + + if (projectState == null) { + projectState = Sdk.getProjectState(javaProject.getProject()); + } + + // get the target for the project + IAndroidTarget target = projectState.getTarget(); + + if (target == null) { + throw new AbortBuildException(); + } + + // check on the target data. + if (sdk.checkAndLoadTargetData(target, javaProject) != LoadStatus.LOADED) { + throw new AbortBuildException(); + } + + mBuildToolInfo = projectState.getBuildToolInfo(); + if (mBuildToolInfo == null) { + mBuildToolInfo = sdk.getLatestBuildTool(); + + if (mBuildToolInfo == null) { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, iProject, + "No \"Build Tools\" package available; use SDK Manager to install one."); + throw new AbortBuildException(); + } else { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, iProject, + String.format("Using default Build Tools revision %s", + mBuildToolInfo.getRevision()) + ); + } + } + + // abort if there are TARGET or ADT type markers + stopOnMarker(iProject, AdtConstants.MARKER_TARGET, IResource.DEPTH_ZERO, + false /*checkSeverity*/); + stopOnMarker(iProject, AdtConstants.MARKER_ADT, IResource.DEPTH_ZERO, + false /*checkSeverity*/); + } + + protected void stopOnMarker(IProject project, String markerType, int depth, + boolean checkSeverity) + throws AbortBuildException { + try { + IMarker[] markers = project.findMarkers(markerType, false /*includeSubtypes*/, depth); + + if (markers.length > 0) { + if (checkSeverity == false) { + throw new AbortBuildException(); + } else { + for (IMarker marker : markers) { + int severity = marker.getAttribute(IMarker.SEVERITY, -1 /*defaultValue*/); + if (severity == IMarker.SEVERITY_ERROR) { + throw new AbortBuildException(); + } + } + } + } + } catch (CoreException e) { + // don't stop, something's really screwed up and the build will break later with + // a better error message. + } + } + + /** + * Handles a {@link StreamException} by logging the info and marking the project. + * This should generally be followed by exiting the build process. + * + * @param e the exception + */ + protected void handleStreamException(StreamException e) { + IAbstractFile file = e.getFile(); + + String msg; + + IResource target = getProject(); + if (file instanceof IFileWrapper) { + target = ((IFileWrapper) file).getIFile(); + + if (e.getError() == StreamException.Error.OUTOFSYNC) { + msg = "File is Out of sync"; + } else { + msg = "Error reading file. Read log for details"; + } + + } else { + if (e.getError() == StreamException.Error.OUTOFSYNC) { + msg = String.format("Out of sync file: %s", file.getOsLocation()); + } else { + msg = String.format("Error reading file %s. Read log for details", + file.getOsLocation()); + } + } + + AdtPlugin.logAndPrintError(e, getProject().getName(), msg); + BaseProjectHelper.markResource(target, AdtConstants.MARKER_ADT, msg, + IMarker.SEVERITY_ERROR); + } + + /** + * Handles a generic {@link Throwable} by logging the info and marking the project. + * This should generally be followed by exiting the build process. + * + * @param t the {@link Throwable}. + * @param message the message to log and to associate with the marker. + */ + protected void handleException(Throwable t, String message) { + AdtPlugin.logAndPrintError(t, getProject().getName(), message); + markProject(AdtConstants.MARKER_ADT, message, IMarker.SEVERITY_ERROR); + } + + /** + * Recursively delete all the derived resources from a root resource. The root resource is not + * deleted. + * @param rootResource the root resource + * @param monitor a progress monitor. + * @throws CoreException + * + */ + protected void removeDerivedResources(IResource rootResource, IProgressMonitor monitor) + throws CoreException { + removeDerivedResources(rootResource, false, monitor); + } + + /** + * delete a resource and its children. returns true if the root resource was deleted. All + * sub-folders *will* be deleted if they were emptied (not if they started empty). + * @param rootResource the root resource + * @param deleteRoot whether to delete the root folder. + * @param monitor a progress monitor. + * @throws CoreException + */ + private void removeDerivedResources(IResource rootResource, boolean deleteRoot, + IProgressMonitor monitor) throws CoreException { + if (rootResource.exists()) { + // if it's a folder, delete derived member. + if (rootResource.getType() == IResource.FOLDER) { + IFolder folder = (IFolder)rootResource; + IResource[] members = folder.members(); + boolean wasNotEmpty = members.length > 0; + for (IResource member : members) { + removeDerivedResources(member, true /*deleteRoot*/, monitor); + } + + // if the folder had content that is now all removed, delete the folder. + if (deleteRoot && wasNotEmpty && folder.members().length == 0) { + rootResource.getLocation().toFile().delete(); + } + } + + // if the root resource is derived, delete it. + if (rootResource.isDerived()) { + rootResource.getLocation().toFile().delete(); + } + } + } + + protected void launchJob(Job newJob) { + newJob.setPriority(Job.BUILD); + newJob.setRule(ResourcesPlugin.getWorkspace().getRoot()); + newJob.schedule(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ChangedFileSet.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ChangedFileSet.java new file mode 100644 index 000000000..4f5b47f6d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ChangedFileSet.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build.builders; + +import com.android.annotations.NonNull; + +import org.apache.tools.ant.types.selectors.SelectorUtils; +import org.eclipse.core.runtime.IPath; + +/** + * Collection of file path or path patterns to be checked for changes. + * + * All paths should be relative to the project they belong to. + * Patterns can use Ant-type glob patterns. + * + * This is an immutable class that does not store any info beyond the list of paths. This is to + * be used in conjunction with {@link PatternBasedDeltaVisitor}. + */ +class ChangedFileSet { + + private final String mLogName; + + private final String[] mInputs; + private String mOutput; + + ChangedFileSet(String logName, String... inputs) { + mLogName = logName; + mInputs = inputs; + } + + public void setOutput(@NonNull String output) { + mOutput = output; + } + + public boolean isInput(@NonNull String path, @NonNull IPath iPath) { + for (String i : mInputs) { + if (SelectorUtils.matchPath(i, path)) { + return true; + } + } + + return false; + } + + public boolean isOutput(@NonNull String path, @NonNull IPath iPath) { + if (mOutput != null) { + return SelectorUtils.matchPath(mOutput, path); + } + + return false; + } + + public String getLogName() { + return mLogName; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ChangedFileSetHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ChangedFileSetHelper.java new file mode 100644 index 000000000..9fc19a7a6 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ChangedFileSetHelper.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build.builders; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; + +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.IPath; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class to generate {@link ChangedFileSet} for given projects. + * + * Also contains non project specific {@link ChangedFileSet} such as {@link #MANIFEST} + * and {@link #NATIVE_LIBS} + */ +class ChangedFileSetHelper { + + final static ChangedFileSet MANIFEST; + final static ChangedFileSet NATIVE_LIBS; + + static { + MANIFEST = new ChangedFileSet("manifest", //$NON-NLS-1$ + SdkConstants.FN_ANDROID_MANIFEST_XML); + + // FIXME: move compiled native libs to bin/libs/ + NATIVE_LIBS = new ChangedFileSet( + "nativeLibs", + SdkConstants.FD_NATIVE_LIBS + "/*/*.so", //$NON-NLS-1$ + SdkConstants.FD_NATIVE_LIBS + "/*/" + SdkConstants.FN_GDBSERVER); //$NON-NLS-1$ + } + + /** + * Returns a ChangedFileSet for Java resources inside a given project's source folders. + * @param project the project. + * @return a ChangedFileSet + */ + static ChangedFileSet getJavaResCfs(@NonNull IProject project) { + + // get the source folder for the given project. + IPath projectPath = project.getFullPath(); + + // get the source folders. + List<IPath> srcPaths = BaseProjectHelper.getSourceClasspaths(project); + List<String> paths = new ArrayList<String>(srcPaths.size()); + + // create a pattern for each of them. + for (IPath path : srcPaths) { + paths.add(path.makeRelativeTo(projectPath).toString() + "/**"); //$NON-NLS-1$ + } + + // custom ChangedFileSet to ignore .java files. + return new JavaResChangedSet("javaRes", //$NON-NLS-1$ + paths.toArray(new String[paths.size()])); + } + + /** + * Returns a {@link ChangedFileSet} for all the resources (included assets), and the output + * file (compiled resources + * @param project the project + * @return a ChangeFileSet + */ + static ChangedFileSet getResCfs(@NonNull IProject project) { + // generated res is inside the project's android output folder + String path = getRelativeAndroidOut(project); + + ChangedFileSet set = new ChangedFileSet( + "resources", //$NON-NLS-1$ + SdkConstants.FD_RES + "/**", //$NON-NLS-1$ + SdkConstants.FD_ASSETS + "/**", //$NON-NLS-1$ + path + '/' + AdtConstants.WS_BIN_RELATIVE_BC + "/**"); //$NON-NLS-1$ + + // output file is based on the project's android output folder + set.setOutput(path + '/' + AdtConstants.FN_RESOURCES_AP_); + + return set; + } + + /** + * Returns a {@link ChangedFileSet} for all the resources (included assets), and the output + * file (compiled resources + * @param project the project + * @return a ChangeFileSet + */ + static ChangedFileSet getMergedManifestCfs(@NonNull IProject project) { + // input path is inside the project's android output folder + String path = getRelativeAndroidOut(project); + + ChangedFileSet set = new ChangedFileSet( + "mergedManifest", //$NON-NLS-1$ + path + '/' + SdkConstants.FN_ANDROID_MANIFEST_XML); + + return set; + } + + /** + * Returns a {@link ChangedFileSet} for the generated R.txt file + * @param project the project + * @return a ChangeFileSet + */ + static ChangedFileSet getTextSymbols(@NonNull IProject project) { + // input path is inside the project's android output folder + String path = getRelativeAndroidOut(project); + + ChangedFileSet set = new ChangedFileSet( + "textSymbols", //$NON-NLS-1$ + path + '/' + SdkConstants.FN_RESOURCE_TEXT); + + return set; + } + + /** + * Returns a {@link ChangedFileSet} for a project's javac output. + * @param project the project + * @return a ChangedFileSet + */ + static ChangedFileSet getByteCodeCfs(@NonNull IProject project) { + // input pattern is based on the project's Java compiler's output folder + String path = getRelativeJavaCOut(project); + + ChangedFileSet set = new ChangedFileSet("compiledCode", //$NON-NLS-1$ + path + "/**/*" + SdkConstants.DOT_CLASS); //$NON-NLS-1$ + + return set; + } + + /** + * Returns a {@link ChangedFileSet} for a project's complete resources, including + * generated resources and crunch cache. + * @param project the project + * @return a ChangeFileSet + */ + static ChangedFileSet getFullResCfs(@NonNull IProject project) { + // generated res are in the project's android output folder + String path = getRelativeAndroidOut(project); + + ChangedFileSet set = new ChangedFileSet("libResources", //$NON-NLS-1$ + SdkConstants.FD_RES + "/**", //$NON-NLS-1$ + path + '/' + SdkConstants.FD_RES + "/**"); //$NON-NLS-1$ + + return set; + } + + /** + * Returns a {@link ChangedFileSet} for a project's whole code, including + * compiled bytecode, 3rd party libs, and the output file containing the Dalvik + * bytecode file. + * @param project the project + * @return a ChangeFileSet + */ + static ChangedFileSet getCodeCfs(@NonNull IProject project) { + // input pattern is based on the project's Java compiler's output folder + String path = getRelativeJavaCOut(project); + + ChangedFileSet set = new ChangedFileSet("classAndJars", //$NON-NLS-1$ + path + "/**/*" + SdkConstants.DOT_CLASS, //$NON-NLS-1$ + SdkConstants.FD_NATIVE_LIBS + "/*" + SdkConstants.DOT_JAR); //$NON-NLS-1$ + + // output file is based on the project's android output folder + path = getRelativeAndroidOut(project); + set.setOutput(path + '/' + SdkConstants.FN_APK_CLASSES_DEX); + + return set; + } + + private static String getRelativePath(@NonNull IProject project, @NonNull IResource resource) { + return resource.getFullPath().makeRelativeTo(project.getFullPath()).toString(); + } + + private static String getRelativeAndroidOut(@NonNull IProject project) { + IFolder folder = BaseProjectHelper.getAndroidOutputFolder(project); + return getRelativePath(project, folder); + } + + private static String getRelativeJavaCOut(@NonNull IProject project) { + IFolder folder = BaseProjectHelper.getJavaOutputFolder(project); + return getRelativePath(project, folder); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/JavaResChangedSet.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/JavaResChangedSet.java new file mode 100644 index 000000000..6b257efbf --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/JavaResChangedSet.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build.builders; + +import com.android.annotations.NonNull; +import com.android.sdklib.build.ApkBuilder; + +import org.eclipse.core.runtime.IPath; + +/** + * Custom {@link ChangedFileSet} for java resources. + * + * This builds the set of inputs to be all the source folders of the given project, + * and excludes files that won't be packaged. + * This exclusion can't be easily described as a glob-pattern so it's overriding the default + * behavior instead. + * + */ +class JavaResChangedSet extends ChangedFileSet { + + JavaResChangedSet(String logName, String... inputs) { + super(logName, inputs); + } + + @Override + public boolean isInput(@NonNull String path, @NonNull IPath iPath) { + if (!ApkBuilder.checkFileForPackaging(iPath.lastSegment(), iPath.getFileExtension())) { + return false; + } + return super.isInput(path, iPath); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PatternBasedDeltaVisitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PatternBasedDeltaVisitor.java new file mode 100644 index 000000000..b52ede90c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PatternBasedDeltaVisitor.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build.builders; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.build.BuildHelper; + +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IResourceDeltaVisitor; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IStatus; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +/** + * Delta visitor checking changed files against given glob-patterns. + * + * The visitor is given {@link ChangedFileSet} objects which contains patterns to detect change + * in input and output files. (Output files are only tested if the delta indicate the file + * was removed). + * + * After the visitor has visited the whole delta, it can be queried to see which ChangedFileSet + * recognized a file change. (ChangedFileSet are immutable and do not record this info). + */ +class PatternBasedDeltaVisitor implements IResourceDeltaVisitor { + + private final static boolean DEBUG_LOG = "1".equals( //$NON-NLS-1$ + System.getenv("ANDROID_VISITOR_DEBUG")); //$NON-NLS-1$ + + private final IProject mMainProject; + private final IProject mDeltaProject; + + private final List<ChangedFileSet> mSets = new ArrayList<ChangedFileSet>(); + private final Map<ChangedFileSet, Boolean> mResults = + new IdentityHashMap<ChangedFileSet, Boolean>(); + + private final String mLogName; + + PatternBasedDeltaVisitor(IProject mainProject, IProject deltaProject, String logName) { + mMainProject = mainProject; + mDeltaProject = deltaProject; + mLogName = logName; + if (DEBUG_LOG) { + AdtPlugin.log(IStatus.INFO, "%s (%s): Delta for %s", //$NON-NLS-1$ + mMainProject.getName(), mLogName, mDeltaProject.getName()); + } + } + + void addSet(ChangedFileSet bundle) { + mSets.add(bundle); + } + + boolean checkSet(ChangedFileSet bundle) { + Boolean r = mResults.get(bundle); + if (r != null) { + return r.booleanValue(); + } + + return false; + } + + @Override + public boolean visit(IResourceDelta delta) throws CoreException { + IResource resource = delta.getResource(); + + if (resource.getType() == IResource.FOLDER) { + // always visit the subfolders, unless the folder is not to be included + return BuildHelper.checkFolderForPackaging((IFolder)resource); + + } else if (resource.getType() == IResource.FILE) { + IPath path = resource.getFullPath().makeRelativeTo(mDeltaProject.getFullPath()); + String pathStr = path.toString(); + + // FIXME: no need to loop through all the sets once they have all said they need something (return false below and above) + for (ChangedFileSet set : mSets) { + // FIXME: should ignore sets that have already returned true. + + if (set.isInput(pathStr, path)) { + mResults.put(set, Boolean.TRUE); + + if (DEBUG_LOG) { + String cfs_logName = set.getLogName(); + + if (cfs_logName != null) { + AdtPlugin.log(IStatus.INFO, "%s (%s:%s): %s", //$NON-NLS-1$ + mMainProject.getName(), mLogName, cfs_logName, + resource.getFullPath().toString()); + } else { + AdtPlugin.log(IStatus.INFO, "%s (%s): %s", //$NON-NLS-1$ + mMainProject.getName(), mLogName, + resource.getFullPath().toString()); + } + } + + } else if (delta.getKind() == IResourceDelta.REMOVED && + set.isOutput(pathStr, path)) { + mResults.put(set, Boolean.TRUE); + + if (DEBUG_LOG) { + String cfs_logName = set.getLogName(); + + if (cfs_logName != null) { + AdtPlugin.log(IStatus.INFO, "%s (%s:%s): %s", //$NON-NLS-1$ + mMainProject.getName(), mLogName, cfs_logName, + resource.getFullPath().toString()); + } else { + AdtPlugin.log(IStatus.INFO, "%s (%s): %s", //$NON-NLS-1$ + mMainProject.getName(), mLogName, + resource.getFullPath().toString()); + } + } + } + } + } + + return true; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PostCompilerBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PostCompilerBuilder.java new file mode 100644 index 000000000..8aacb44ef --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PostCompilerBuilder.java @@ -0,0 +1,946 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build.builders; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AndroidPrintStream; +import com.android.ide.eclipse.adt.internal.build.AaptExecException; +import com.android.ide.eclipse.adt.internal.build.AaptParser; +import com.android.ide.eclipse.adt.internal.build.AaptResultException; +import com.android.ide.eclipse.adt.internal.build.BuildHelper; +import com.android.ide.eclipse.adt.internal.build.BuildHelper.ResourceMarker; +import com.android.ide.eclipse.adt.internal.build.DexException; +import com.android.ide.eclipse.adt.internal.build.Messages; +import com.android.ide.eclipse.adt.internal.build.NativeLibInJarException; +import com.android.ide.eclipse.adt.internal.lint.LintDeltaProcessor; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; +import com.android.ide.eclipse.adt.internal.project.ApkInstallManager; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.project.LibraryClasspathContainerInitializer; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.io.IFileWrapper; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.build.ApkBuilder; +import com.android.sdklib.build.ApkCreationException; +import com.android.sdklib.build.DuplicateFileException; +import com.android.sdklib.build.IArchiveBuilder; +import com.android.sdklib.build.SealedApkException; +import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException; +import com.android.xml.AndroidManifest; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.jdt.core.IJavaModelMarker; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.regex.Pattern; + +public class PostCompilerBuilder extends BaseBuilder { + + /** This ID is used in plugin.xml and in each project's .project file. + * It cannot be changed even if the class is renamed/moved */ + public static final String ID = "com.android.ide.eclipse.adt.ApkBuilder"; //$NON-NLS-1$ + + private static final String PROPERTY_CONVERT_TO_DEX = "convertToDex"; //$NON-NLS-1$ + private static final String PROPERTY_PACKAGE_RESOURCES = "packageResources"; //$NON-NLS-1$ + private static final String PROPERTY_BUILD_APK = "buildApk"; //$NON-NLS-1$ + + /** Flag to pass to PostCompiler builder that sets if it runs or not. + * Set this flag whenever calling build if PostCompiler is to run + */ + public final static String POST_C_REQUESTED = "RunPostCompiler"; //$NON-NLS-1$ + + /** + * Dex conversion flag. This is set to true if one of the changed/added/removed + * file is a .class file. Upon visiting all the delta resource, if this + * flag is true, then we know we'll have to make the "classes.dex" file. + */ + private boolean mConvertToDex = false; + + /** + * Package resources flag. This is set to true if one of the changed/added/removed + * file is a resource file. Upon visiting all the delta resource, if + * this flag is true, then we know we'll have to repackage the resources. + */ + private boolean mPackageResources = false; + + /** + * Final package build flag. + */ + private boolean mBuildFinalPackage = false; + + private AndroidPrintStream mOutStream = null; + private AndroidPrintStream mErrStream = null; + + + private ResourceMarker mResourceMarker = new ResourceMarker() { + @Override + public void setWarning(IResource resource, String message) { + BaseProjectHelper.markResource(resource, AdtConstants.MARKER_PACKAGING, + message, IMarker.SEVERITY_WARNING); + } + }; + + + public PostCompilerBuilder() { + super(); + } + + @Override + protected void clean(IProgressMonitor monitor) throws CoreException { + super.clean(monitor); + + // Get the project. + IProject project = getProject(); + + if (DEBUG_LOG) { + AdtPlugin.log(IStatus.INFO, "%s CLEAN(POST)", project.getName()); + } + + // Clear the project of the generic markers + removeMarkersFromContainer(project, AdtConstants.MARKER_AAPT_PACKAGE); + removeMarkersFromContainer(project, AdtConstants.MARKER_PACKAGING); + + // also remove the files in the output folder (but not the Eclipse output folder). + IFolder javaOutput = BaseProjectHelper.getJavaOutputFolder(project); + IFolder androidOutput = BaseProjectHelper.getAndroidOutputFolder(project); + + if (javaOutput.equals(androidOutput) == false) { + // get the content + IResource[] members = androidOutput.members(); + for (IResource member : members) { + if (member.equals(javaOutput) == false) { + member.delete(true /*force*/, monitor); + } + } + } + } + + // build() returns a list of project from which this project depends for future compilation. + @Override + protected IProject[] build( + int kind, + @SuppressWarnings("rawtypes") Map args, + IProgressMonitor monitor) + throws CoreException { + // get a project object + IProject project = getProject(); + + if (DEBUG_LOG) { + AdtPlugin.log(IStatus.INFO, "%s BUILD(POST)", project.getName()); + } + + // Benchmarking start + long startBuildTime = 0; + if (BuildHelper.BENCHMARK_FLAG) { + // End JavaC Timer + String msg = "BENCHMARK ADT: Ending Compilation \n BENCHMARK ADT: Time Elapsed: " + //$NON-NLS-1$ + (System.nanoTime() - BuildHelper.sStartJavaCTime)/Math.pow(10, 6) + "ms"; //$NON-NLS-1$ + AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, project, msg); + msg = "BENCHMARK ADT: Starting PostCompilation"; //$NON-NLS-1$ + AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, project, msg); + startBuildTime = System.nanoTime(); + } + + // list of referenced projects. This is a mix of java projects and library projects + // and is computed below. + IProject[] allRefProjects = null; + + try { + // get the project info + ProjectState projectState = Sdk.getProjectState(project); + + // this can happen if the project has no project.properties. + if (projectState == null) { + return null; + } + + boolean isLibrary = projectState.isLibrary(); + + // get the libraries + List<IProject> libProjects = projectState.getFullLibraryProjects(); + + IJavaProject javaProject = JavaCore.create(project); + + // get the list of referenced projects. + List<IProject> javaProjects = ProjectHelper.getReferencedProjects(project); + List<IJavaProject> referencedJavaProjects = BuildHelper.getJavaProjects( + javaProjects); + + // mix the java project and the library projects + final int size = libProjects.size() + javaProjects.size(); + ArrayList<IProject> refList = new ArrayList<IProject>(size); + refList.addAll(libProjects); + refList.addAll(javaProjects); + allRefProjects = refList.toArray(new IProject[size]); + + // get the android output folder + IFolder androidOutputFolder = BaseProjectHelper.getAndroidOutputFolder(project); + IFolder resOutputFolder = androidOutputFolder.getFolder(SdkConstants.FD_RES); + + // First thing we do is go through the resource delta to not + // lose it if we have to abort the build for any reason. + if (args.containsKey(POST_C_REQUESTED) + && AdtPrefs.getPrefs().getBuildSkipPostCompileOnFileSave()) { + // Skip over flag setting + } else if (kind == FULL_BUILD) { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.Start_Full_Apk_Build); + + if (DEBUG_LOG) { + AdtPlugin.log(IStatus.INFO, "%s full build!", project.getName()); + } + + // Full build: we do all the steps. + mPackageResources = true; + mConvertToDex = true; + mBuildFinalPackage = true; + } else { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.Start_Inc_Apk_Build); + + // go through the resources and see if something changed. + IResourceDelta delta = getDelta(project); + if (delta == null) { + // no delta? Same as full build: we do all the steps. + mPackageResources = true; + mConvertToDex = true; + mBuildFinalPackage = true; + } else { + + if (ResourceManager.isAutoBuilding() && AdtPrefs.getPrefs().isLintOnSave()) { + // Check for errors on save/build, if enabled + LintDeltaProcessor.create().process(delta); + } + + PatternBasedDeltaVisitor dv = new PatternBasedDeltaVisitor( + project, project, + "POST:Main"); + + ChangedFileSet manifestCfs = ChangedFileSetHelper.getMergedManifestCfs(project); + dv.addSet(manifestCfs); + + ChangedFileSet resCfs = ChangedFileSetHelper.getResCfs(project); + dv.addSet(resCfs); + + ChangedFileSet androidCodeCfs = ChangedFileSetHelper.getCodeCfs(project); + dv.addSet(androidCodeCfs); + + ChangedFileSet javaResCfs = ChangedFileSetHelper.getJavaResCfs(project); + dv.addSet(javaResCfs); + dv.addSet(ChangedFileSetHelper.NATIVE_LIBS); + + delta.accept(dv); + + // save the state + mPackageResources |= dv.checkSet(manifestCfs) || dv.checkSet(resCfs); + + mConvertToDex |= dv.checkSet(androidCodeCfs); + + mBuildFinalPackage |= dv.checkSet(javaResCfs) || + dv.checkSet(ChangedFileSetHelper.NATIVE_LIBS); + } + + // check the libraries + if (libProjects.size() > 0) { + for (IProject libProject : libProjects) { + delta = getDelta(libProject); + if (delta != null) { + PatternBasedDeltaVisitor visitor = new PatternBasedDeltaVisitor( + project, libProject, + "POST:Lib"); + + ChangedFileSet libResCfs = ChangedFileSetHelper.getFullResCfs( + libProject); + visitor.addSet(libResCfs); + visitor.addSet(ChangedFileSetHelper.NATIVE_LIBS); + // FIXME: add check on the library.jar? + + delta.accept(visitor); + + mPackageResources |= visitor.checkSet(libResCfs); + mBuildFinalPackage |= visitor.checkSet( + ChangedFileSetHelper.NATIVE_LIBS); + } + } + } + + // also go through the delta for all the referenced projects + final int referencedCount = referencedJavaProjects.size(); + for (int i = 0 ; i < referencedCount; i++) { + IJavaProject referencedJavaProject = referencedJavaProjects.get(i); + delta = getDelta(referencedJavaProject.getProject()); + if (delta != null) { + IProject referencedProject = referencedJavaProject.getProject(); + PatternBasedDeltaVisitor visitor = new PatternBasedDeltaVisitor( + project, referencedProject, + "POST:RefedProject"); + + ChangedFileSet javaResCfs = ChangedFileSetHelper.getJavaResCfs(referencedProject); + visitor.addSet(javaResCfs); + + ChangedFileSet bytecodeCfs = ChangedFileSetHelper.getByteCodeCfs(referencedProject); + visitor.addSet(bytecodeCfs); + + delta.accept(visitor); + + // save the state + mConvertToDex |= visitor.checkSet(bytecodeCfs); + mBuildFinalPackage |= visitor.checkSet(javaResCfs); + } + } + } + + // store the build status in the persistent storage + saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, mConvertToDex); + saveProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, mPackageResources); + saveProjectBooleanProperty(PROPERTY_BUILD_APK, mBuildFinalPackage); + + // Top level check to make sure the build can move forward. Only do this after recording + // delta changes. + abortOnBadSetup(javaProject, projectState); + + // Get the output stream. Since the builder is created for the life of the + // project, they can be kept around. + if (mOutStream == null) { + mOutStream = new AndroidPrintStream(project, null /*prefix*/, + AdtPlugin.getOutStream()); + mErrStream = new AndroidPrintStream(project, null /*prefix*/, + AdtPlugin.getOutStream()); + } + + // remove older packaging markers. + removeMarkersFromContainer(javaProject.getProject(), AdtConstants.MARKER_PACKAGING); + + // finished with the common init and tests. Special case of the library. + if (isLibrary) { + // check the jar output file is present, if not create it. + IFile jarIFile = androidOutputFolder.getFile( + project.getName().toLowerCase() + SdkConstants.DOT_JAR); + if (mConvertToDex == false && jarIFile.exists() == false) { + mConvertToDex = true; + } + + // also update the crunch cache always since aapt does it smartly only + // on the files that need it. + if (DEBUG_LOG) { + AdtPlugin.log(IStatus.INFO, "%s running crunch!", project.getName()); + } + BuildHelper helper = new BuildHelper( + projectState, + mBuildToolInfo, + mOutStream, mErrStream, + false /*jumbo mode doesn't matter here*/, + false /*dex merger doesn't matter here*/, + true /*debugMode*/, + AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE, + mResourceMarker); + updateCrunchCache(project, helper); + + // refresh recursively bin/res folder + resOutputFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor); + + if (mConvertToDex) { // in this case this means some class files changed and + // we need to update the jar file. + if (DEBUG_LOG) { + AdtPlugin.log(IStatus.INFO, "%s updating jar!", project.getName()); + } + + // resource to the AndroidManifest.xml file + IFile manifestFile = project.getFile(SdkConstants.FN_ANDROID_MANIFEST_XML); + String appPackage = AndroidManifest.getPackage(new IFileWrapper(manifestFile)); + + IFolder javaOutputFolder = BaseProjectHelper.getJavaOutputFolder(project); + + writeLibraryPackage(jarIFile, project, appPackage, javaOutputFolder); + saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, mConvertToDex = false); + + // refresh the bin folder content with no recursion to update the library + // jar file. + androidOutputFolder.refreshLocal(IResource.DEPTH_ONE, monitor); + + // Also update the projects. The only way to force recompile them is to + // reset the library container. + List<ProjectState> parentProjects = projectState.getParentProjects(); + LibraryClasspathContainerInitializer.updateProject(parentProjects); + } + + return allRefProjects; + } + + // Check to see if we're going to launch or export. If not, we can skip + // the packaging and dexing process. + if (!args.containsKey(POST_C_REQUESTED) + && AdtPrefs.getPrefs().getBuildSkipPostCompileOnFileSave()) { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.Skip_Post_Compiler); + return allRefProjects; + } else { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.Start_Full_Post_Compiler); + } + + // first thing we do is check that the SDK directory has been setup. + String osSdkFolder = AdtPlugin.getOsSdkFolder(); + + if (osSdkFolder.length() == 0) { + // this has already been checked in the precompiler. Therefore, + // while we do have to cancel the build, we don't have to return + // any error or throw anything. + return allRefProjects; + } + + // do some extra check, in case the output files are not present. This + // will force to recreate them. + IResource tmp = null; + + if (mPackageResources == false) { + // check the full resource package + tmp = androidOutputFolder.findMember(AdtConstants.FN_RESOURCES_AP_); + if (tmp == null || tmp.exists() == false) { + mPackageResources = true; + } + } + + // check classes.dex is present. If not we force to recreate it. + if (mConvertToDex == false) { + tmp = androidOutputFolder.findMember(SdkConstants.FN_APK_CLASSES_DEX); + if (tmp == null || tmp.exists() == false) { + mConvertToDex = true; + } + } + + // also check the final file(s)! + String finalPackageName = ProjectHelper.getApkFilename(project, null /*config*/); + if (mBuildFinalPackage == false) { + tmp = androidOutputFolder.findMember(finalPackageName); + if (tmp == null || (tmp instanceof IFile && + tmp.exists() == false)) { + String msg = String.format(Messages.s_Missing_Repackaging, finalPackageName); + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, msg); + mBuildFinalPackage = true; + } + } + + // at this point we know if we need to recreate the temporary apk + // or the dex file, but we don't know if we simply need to recreate them + // because they are missing + + // refresh the output directory first + IContainer ic = androidOutputFolder.getParent(); + if (ic != null) { + ic.refreshLocal(IResource.DEPTH_ONE, monitor); + } + + // we need to test all three, as we may need to make the final package + // but not the intermediary ones. + if (mPackageResources || mConvertToDex || mBuildFinalPackage) { + String forceJumboStr = projectState.getProperty( + AdtConstants.DEX_OPTIONS_FORCEJUMBO); + Boolean jumbo = Boolean.valueOf(forceJumboStr); + + String dexMergerStr = projectState.getProperty( + AdtConstants.DEX_OPTIONS_DISABLE_MERGER); + Boolean dexMerger = Boolean.valueOf(dexMergerStr); + + BuildHelper helper = new BuildHelper( + projectState, + mBuildToolInfo, + mOutStream, mErrStream, + jumbo.booleanValue(), + dexMerger.booleanValue(), + true /*debugMode*/, + AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE, + mResourceMarker); + + IPath androidBinLocation = androidOutputFolder.getLocation(); + if (androidBinLocation == null) { + markProject(AdtConstants.MARKER_PACKAGING, Messages.Output_Missing, + IMarker.SEVERITY_ERROR); + return allRefProjects; + } + String osAndroidBinPath = androidBinLocation.toOSString(); + + // resource to the AndroidManifest.xml file + IFile manifestFile = androidOutputFolder.getFile( + SdkConstants.FN_ANDROID_MANIFEST_XML); + + if (manifestFile == null || manifestFile.exists() == false) { + // mark project and exit + String msg = String.format(Messages.s_File_Missing, + SdkConstants.FN_ANDROID_MANIFEST_XML); + markProject(AdtConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR); + return allRefProjects; + } + + // Remove the old .apk. + // This make sure that if the apk is corrupted, then dx (which would attempt + // to open it), will not fail. + String osFinalPackagePath = osAndroidBinPath + File.separator + finalPackageName; + File finalPackage = new File(osFinalPackagePath); + + // if delete failed, this is not really a problem, as the final package generation + // handle already present .apk, and if that one failed as well, the user will be + // notified. + finalPackage.delete(); + + // Check if we need to package the resources. + if (mPackageResources) { + // also update the crunch cache always since aapt does it smartly only + // on the files that need it. + if (DEBUG_LOG) { + AdtPlugin.log(IStatus.INFO, "%s running crunch!", project.getName()); + } + if (updateCrunchCache(project, helper) == false) { + return allRefProjects; + } + + // refresh recursively bin/res folder + resOutputFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor); + + if (DEBUG_LOG) { + AdtPlugin.log(IStatus.INFO, "%s packaging resources!", project.getName()); + } + // remove some aapt_package only markers. + removeMarkersFromContainer(project, AdtConstants.MARKER_AAPT_PACKAGE); + + try { + helper.packageResources(manifestFile, libProjects, null /*resfilter*/, + 0 /*versionCode */, osAndroidBinPath, + AdtConstants.FN_RESOURCES_AP_); + } catch (AaptExecException e) { + BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, + e.getMessage(), IMarker.SEVERITY_ERROR); + return allRefProjects; + } catch (AaptResultException e) { + // attempt to parse the error output + String[] aaptOutput = e.getOutput(); + boolean parsingError = AaptParser.parseOutput(aaptOutput, project); + + // if we couldn't parse the output we display it in the console. + if (parsingError) { + AdtPlugin.printErrorToConsole(project, (Object[]) aaptOutput); + + // if the exec failed, and we couldn't parse the error output (and + // therefore not all files that should have been marked, were marked), + // we put a generic marker on the project and abort. + BaseProjectHelper.markResource(project, + AdtConstants.MARKER_PACKAGING, + Messages.Unparsed_AAPT_Errors, + IMarker.SEVERITY_ERROR); + } + } + + // build has been done. reset the state of the builder + mPackageResources = false; + + // and store it + saveProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, mPackageResources); + } + + String classesDexPath = osAndroidBinPath + File.separator + + SdkConstants.FN_APK_CLASSES_DEX; + + // then we check if we need to package the .class into classes.dex + if (mConvertToDex) { + if (DEBUG_LOG) { + AdtPlugin.log(IStatus.INFO, "%s running dex!", project.getName()); + } + try { + Collection<String> dxInputPaths = helper.getCompiledCodePaths(); + + helper.executeDx(javaProject, dxInputPaths, classesDexPath); + } catch (DexException e) { + String message = e.getMessage(); + + AdtPlugin.printErrorToConsole(project, message); + BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, + message, IMarker.SEVERITY_ERROR); + + Throwable cause = e.getCause(); + + if (cause instanceof NoClassDefFoundError + || cause instanceof NoSuchMethodError) { + AdtPlugin.printErrorToConsole(project, Messages.Incompatible_VM_Warning, + Messages.Requires_1_5_Error); + } + + // dx failed, we return + return allRefProjects; + } + + // build has been done. reset the state of the builder + mConvertToDex = false; + + // and store it + saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, mConvertToDex); + } + + // now we need to make the final package from the intermediary apk + // and classes.dex. + // This is the default package with all the resources. + + try { + if (DEBUG_LOG) { + AdtPlugin.log(IStatus.INFO, "%s making final package!", project.getName()); + } + helper.finalDebugPackage( + osAndroidBinPath + File.separator + AdtConstants.FN_RESOURCES_AP_, + classesDexPath, osFinalPackagePath, libProjects, mResourceMarker); + } catch (KeytoolException e) { + String eMessage = e.getMessage(); + + // mark the project with the standard message + String msg = String.format(Messages.Final_Archive_Error_s, eMessage); + BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, msg, + IMarker.SEVERITY_ERROR); + + // output more info in the console + AdtPlugin.printErrorToConsole(project, + msg, + String.format(Messages.ApkBuilder_JAVA_HOME_is_s, e.getJavaHome()), + Messages.ApkBuilder_Update_or_Execute_manually_s, + e.getCommandLine()); + + AdtPlugin.log(e, msg); + + return allRefProjects; + } catch (ApkCreationException e) { + String eMessage = e.getMessage(); + + // mark the project with the standard message + String msg = String.format(Messages.Final_Archive_Error_s, eMessage); + BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, msg, + IMarker.SEVERITY_ERROR); + + AdtPlugin.log(e, msg); + } catch (AndroidLocationException e) { + String eMessage = e.getMessage(); + + // mark the project with the standard message + String msg = String.format(Messages.Final_Archive_Error_s, eMessage); + BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, msg, + IMarker.SEVERITY_ERROR); + AdtPlugin.log(e, msg); + } catch (NativeLibInJarException e) { + String msg = e.getMessage(); + + BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, + msg, IMarker.SEVERITY_ERROR); + + AdtPlugin.printErrorToConsole(project, (Object[]) e.getAdditionalInfo()); + } catch (CoreException e) { + // mark project and return + String msg = String.format(Messages.Final_Archive_Error_s, e.getMessage()); + AdtPlugin.printErrorToConsole(project, msg); + BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, msg, + IMarker.SEVERITY_ERROR); + AdtPlugin.log(e, msg); + } catch (DuplicateFileException e) { + String msg1 = String.format( + "Found duplicate file for APK: %1$s\nOrigin 1: %2$s\nOrigin 2: %3$s", + e.getArchivePath(), e.getFile1(), e.getFile2()); + String msg2 = String.format(Messages.Final_Archive_Error_s, msg1); + AdtPlugin.printErrorToConsole(project, msg2); + BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, msg2, + IMarker.SEVERITY_ERROR); + } + + // we are done. + + // refresh the bin folder content with no recursion. + androidOutputFolder.refreshLocal(IResource.DEPTH_ONE, monitor); + + // build has been done. reset the state of the builder + mBuildFinalPackage = false; + + // and store it + saveProjectBooleanProperty(PROPERTY_BUILD_APK, mBuildFinalPackage); + + // reset the installation manager to force new installs of this project + ApkInstallManager.getInstance().resetInstallationFor(project); + + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, getProject(), + "Build Success!"); + } + } catch (AbortBuildException e) { + return allRefProjects; + } catch (Exception exception) { + // try to catch other exception to actually display an error. This will be useful + // if we get an NPE or something so that we can at least notify the user that something + // went wrong. + + // first check if this is a CoreException we threw to cancel the build. + if (exception instanceof CoreException) { + if (((CoreException)exception).getStatus().getSeverity() == IStatus.CANCEL) { + // Project is already marked with an error. Nothing to do + return allRefProjects; + } + } + + String msg = exception.getMessage(); + if (msg == null) { + msg = exception.getClass().getCanonicalName(); + } + + msg = String.format("Unknown error: %1$s", msg); + AdtPlugin.logAndPrintError(exception, project.getName(), msg); + markProject(AdtConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR); + } + + // Benchmarking end + if (BuildHelper.BENCHMARK_FLAG) { + String msg = "BENCHMARK ADT: Ending PostCompilation. \n BENCHMARK ADT: Time Elapsed: " + //$NON-NLS-1$ + ((System.nanoTime() - startBuildTime)/Math.pow(10, 6)) + "ms"; //$NON-NLS-1$ + AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, project, msg); + // End Overall Timer + msg = "BENCHMARK ADT: Done with everything! \n BENCHMARK ADT: Time Elapsed: " + //$NON-NLS-1$ + (System.nanoTime() - BuildHelper.sStartOverallTime)/Math.pow(10, 6) + "ms"; //$NON-NLS-1$ + AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, project, msg); + } + + return allRefProjects; + } + + private static class JarBuilder implements IArchiveBuilder { + + private static Pattern R_PATTERN = Pattern.compile("R(\\$.*)?\\.class"); //$NON-NLS-1$ + private static String BUILD_CONFIG_CLASS = "BuildConfig.class"; //$NON-NLS-1$ + + private final byte[] buffer = new byte[1024]; + private final JarOutputStream mOutputStream; + private final String mAppPackage; + + JarBuilder(JarOutputStream outputStream, String appPackage) { + mOutputStream = outputStream; + mAppPackage = appPackage.replace('.', '/'); + } + + public void addFile(IFile file, IFolder rootFolder) throws ApkCreationException { + // we only package class file from the output folder + if (SdkConstants.EXT_CLASS.equals(file.getFileExtension()) == false) { + return; + } + + IPath packageApp = file.getParent().getFullPath().makeRelativeTo( + rootFolder.getFullPath()); + + String name = file.getName(); + // Ignore the library's R/Manifest/BuildConfig classes. + if (mAppPackage.equals(packageApp.toString()) && + (BUILD_CONFIG_CLASS.equals(name) || + R_PATTERN.matcher(name).matches())) { + return; + } + + IPath path = file.getFullPath().makeRelativeTo(rootFolder.getFullPath()); + try { + addFile(file.getContents(), file.getLocalTimeStamp(), path.toString()); + } catch (ApkCreationException e) { + throw e; + } catch (Exception e) { + throw new ApkCreationException(e, "Failed to add %s", file); + } + } + + @Override + public void addFile(File file, String archivePath) throws ApkCreationException, + SealedApkException, DuplicateFileException { + try { + FileInputStream inputStream = new FileInputStream(file); + long lastModified = file.lastModified(); + addFile(inputStream, lastModified, archivePath); + } catch (ApkCreationException e) { + throw e; + } catch (Exception e) { + throw new ApkCreationException(e, "Failed to add %s", file); + } + } + + private void addFile(InputStream content, long lastModified, String archivePath) + throws IOException, ApkCreationException { + // create the jar entry + JarEntry entry = new JarEntry(archivePath); + entry.setTime(lastModified); + + try { + // add the entry to the jar archive + mOutputStream.putNextEntry(entry); + + // read the content of the entry from the input stream, and write + // it into the archive. + int count; + while ((count = content.read(buffer)) != -1) { + mOutputStream.write(buffer, 0, count); + } + } finally { + try { + if (content != null) { + content.close(); + } + } catch (Exception e) { + throw new ApkCreationException(e, "Failed to close stream"); + } + } + } + } + + /** + * Updates the crunch cache if needed and return true if the build must continue. + */ + private boolean updateCrunchCache(IProject project, BuildHelper helper) { + try { + helper.updateCrunchCache(); + } catch (AaptExecException e) { + BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, + e.getMessage(), IMarker.SEVERITY_ERROR); + return false; + } catch (AaptResultException e) { + // attempt to parse the error output + String[] aaptOutput = e.getOutput(); + boolean parsingError = AaptParser.parseOutput(aaptOutput, project); + // if we couldn't parse the output we display it in the console. + if (parsingError) { + AdtPlugin.printErrorToConsole(project, (Object[]) aaptOutput); + } + } + + return true; + } + + /** + * Writes the library jar file. + * @param jarIFile the destination file + * @param project the library project + * @param appPackage the library android package + * @param javaOutputFolder the JDT output folder. + */ + private void writeLibraryPackage(IFile jarIFile, IProject project, String appPackage, + IFolder javaOutputFolder) { + + JarOutputStream jos = null; + try { + Manifest manifest = new Manifest(); + Attributes mainAttributes = manifest.getMainAttributes(); + mainAttributes.put(Attributes.Name.CLASS_PATH, "Android ADT"); //$NON-NLS-1$ + mainAttributes.putValue("Created-By", "1.0 (Android)"); //$NON-NLS-1$ //$NON-NLS-2$ + jos = new JarOutputStream( + new FileOutputStream(jarIFile.getLocation().toFile()), manifest); + + JarBuilder jarBuilder = new JarBuilder(jos, appPackage); + + // write the class files + writeClassFilesIntoJar(jarBuilder, javaOutputFolder, javaOutputFolder); + + // now write the standard Java resources from the output folder + ApkBuilder.addSourceFolder(jarBuilder, javaOutputFolder.getLocation().toFile()); + + saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, mConvertToDex); + } catch (Exception e) { + AdtPlugin.log(e, "Failed to write jar file %s", jarIFile.getLocation().toOSString()); + } finally { + if (jos != null) { + try { + jos.close(); + } catch (IOException e) { + // pass + } + } + } + } + + private void writeClassFilesIntoJar(JarBuilder builder, IFolder folder, IFolder rootFolder) + throws CoreException, IOException, ApkCreationException { + IResource[] members = folder.members(); + for (IResource member : members) { + if (member.getType() == IResource.FOLDER) { + writeClassFilesIntoJar(builder, (IFolder) member, rootFolder); + } else if (member.getType() == IResource.FILE) { + IFile file = (IFile) member; + builder.addFile(file, rootFolder); + } + } + } + + @Override + protected void startupOnInitialize() { + super.startupOnInitialize(); + + // load the build status. We pass true as the default value to + // force a recompile in case the property was not found + mConvertToDex = loadProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, true); + mPackageResources = loadProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, true); + mBuildFinalPackage = loadProjectBooleanProperty(PROPERTY_BUILD_APK, true); + } + + @Override + protected void abortOnBadSetup( + @NonNull IJavaProject javaProject, + @Nullable ProjectState projectState) throws AbortBuildException, CoreException { + super.abortOnBadSetup(javaProject, projectState); + + IProject iProject = getProject(); + + // do a (hopefully quick) search for Precompiler type markers. Those are always only + // errors. + stopOnMarker(iProject, AdtConstants.MARKER_AAPT_COMPILE, IResource.DEPTH_INFINITE, + false /*checkSeverity*/); + stopOnMarker(iProject, AdtConstants.MARKER_AIDL, IResource.DEPTH_INFINITE, + false /*checkSeverity*/); + stopOnMarker(iProject, AdtConstants.MARKER_RENDERSCRIPT, IResource.DEPTH_INFINITE, + false /*checkSeverity*/); + stopOnMarker(iProject, AdtConstants.MARKER_ANDROID, IResource.DEPTH_ZERO, + false /*checkSeverity*/); + + // do a search for JDT markers. Those can be errors or warnings + stopOnMarker(iProject, IJavaModelMarker.JAVA_MODEL_PROBLEM_MARKER, + IResource.DEPTH_INFINITE, true /*checkSeverity*/); + stopOnMarker(iProject, IJavaModelMarker.BUILDPATH_PROBLEM_MARKER, + IResource.DEPTH_INFINITE, true /*checkSeverity*/); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerBuilder.java new file mode 100644 index 000000000..0d9ee4897 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerBuilder.java @@ -0,0 +1,1401 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build.builders; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.xml.ManifestData; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.build.AaptParser; +import com.android.ide.eclipse.adt.internal.build.AidlProcessor; +import com.android.ide.eclipse.adt.internal.build.Messages; +import com.android.ide.eclipse.adt.internal.build.RenderScriptLauncher; +import com.android.ide.eclipse.adt.internal.build.RsSourceChangeHandler; +import com.android.ide.eclipse.adt.internal.build.SourceProcessor; +import com.android.ide.eclipse.adt.internal.build.builders.BaseBuilder.AbortBuildException; +import com.android.ide.eclipse.adt.internal.lint.EclipseLintClient; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; +import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.project.FixLaunchConfig; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler.BasicXmlErrorListener; +import com.android.ide.eclipse.adt.internal.resources.manager.IdeScanningContext; +import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; +import com.android.ide.eclipse.adt.internal.sdk.AdtManifestMergeCallback; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.io.IFileWrapper; +import com.android.ide.eclipse.adt.io.IFolderWrapper; +import com.android.io.StreamException; +import com.android.manifmerger.ManifestMerger; +import com.android.manifmerger.MergerLog; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.BuildToolInfo; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.build.RenderScriptChecker; +import com.android.sdklib.build.RenderScriptProcessor; +import com.android.sdklib.internal.build.BuildConfigGenerator; +import com.android.sdklib.internal.build.SymbolLoader; +import com.android.sdklib.internal.build.SymbolWriter; +import com.android.sdklib.internal.project.ProjectProperties; +import com.android.sdklib.io.FileOp; +import com.android.sdklib.repository.FullRevision; +import com.android.utils.ILogger; +import com.android.utils.Pair; +import com.android.xml.AndroidManifest; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import javax.xml.parsers.ParserConfigurationException; + +/** + * Pre Java Compiler. + * This incremental builder performs 2 tasks: + * <ul> + * <li>compiles the resources located in the res/ folder, along with the + * AndroidManifest.xml file into the R.java class.</li> + * <li>compiles any .aidl files into a corresponding java file.</li> + * </ul> + * + */ +public class PreCompilerBuilder extends BaseBuilder { + + /** This ID is used in plugin.xml and in each project's .project file. + * It cannot be changed even if the class is renamed/moved */ + public static final String ID = "com.android.ide.eclipse.adt.PreCompilerBuilder"; //$NON-NLS-1$ + + /** Flag to pass to PreCompiler builder that the build is a release build. + */ + public final static String RELEASE_REQUESTED = "android.releaseBuild"; //$NON-NLS-1$ + + private static final String PROPERTY_PACKAGE = "manifestPackage"; //$NON-NLS-1$ + private static final String PROPERTY_MERGE_MANIFEST = "mergeManifest"; //$NON-NLS-1$ + private static final String PROPERTY_COMPILE_RESOURCES = "compileResources"; //$NON-NLS-1$ + private static final String PROPERTY_COMPILE_BUILDCONFIG = "createBuildConfig"; //$NON-NLS-1$ + private static final String PROPERTY_BUILDCONFIG_MODE = "buildConfigMode"; //$NON-NLS-1$ + + private static final boolean MANIFEST_MERGER_ENABLED_DEFAULT = false; + private static final String MANIFEST_MERGER_PROPERTY = "manifestmerger.enabled"; //$NON-NLS-1$ + + /** Merge Manifest Flag. Computed from resource delta, reset after action is taken. + * Stored persistently in the project. */ + private boolean mMustMergeManifest = false; + /** Resource compilation Flag. Computed from resource delta, reset after action is taken. + * Stored persistently in the project. */ + private boolean mMustCompileResources = false; + /** BuildConfig Flag. Computed from resource delta, reset after action is taken. + * Stored persistently in the project. */ + private boolean mMustCreateBuildConfig = false; + /** BuildConfig last more Flag. Computed from resource delta, reset after action is taken. + * Stored persistently in the project. */ + private boolean mLastBuildConfigMode; + + /** cache of the java package defined in the manifest */ + private String mManifestPackage; + + /** Output folder for generated Java File. Created on the Builder init + * @see #startupOnInitialize() + */ + private IFolder mGenFolder; + + /** + * Progress monitor used at the end of every build to refresh the content of the 'gen' folder + * and set the generated files as derived. + */ + private DerivedProgressMonitor mDerivedProgressMonitor; + + private AidlProcessor mAidlProcessor; + private RsSourceChangeHandler mRenderScriptSourceChangeHandler; + + /** + * Progress monitor waiting the end of the process to set a persistent value + * in a file. This is typically used in conjunction with <code>IResource.refresh()</code>, + * since this call is asynchronous, and we need to wait for it to finish for the file + * to be known by eclipse, before we can call <code>resource.setPersistentProperty</code> on + * a new file. + */ + private static class DerivedProgressMonitor implements IProgressMonitor { + private boolean mCancelled = false; + private boolean mDone = false; + private final IFolder mGenFolder; + + public DerivedProgressMonitor(IFolder genFolder) { + mGenFolder = genFolder; + } + + void reset() { + mDone = false; + } + + @Override + public void beginTask(String name, int totalWork) { + } + + @Override + public void done() { + if (mDone == false) { + mDone = true; + processChildrenOf(mGenFolder); + } + } + + private void processChildrenOf(IFolder folder) { + IResource[] list; + try { + list = folder.members(); + } catch (CoreException e) { + return; + } + + for (IResource member : list) { + if (member.exists()) { + if (member.getType() == IResource.FOLDER) { + processChildrenOf((IFolder) member); + } + + try { + member.setDerived(true, new NullProgressMonitor()); + } catch (CoreException e) { + // This really shouldn't happen since we check that the resource + // exist. + // Worst case scenario, the resource isn't marked as derived. + } + } + } + } + + @Override + public void internalWorked(double work) { + } + + @Override + public boolean isCanceled() { + return mCancelled; + } + + @Override + public void setCanceled(boolean value) { + mCancelled = value; + } + + @Override + public void setTaskName(String name) { + } + + @Override + public void subTask(String name) { + } + + @Override + public void worked(int work) { + } + } + + public PreCompilerBuilder() { + super(); + } + + // build() returns a list of project from which this project depends for future compilation. + @Override + protected IProject[] build( + int kind, + @SuppressWarnings("rawtypes") Map args, + IProgressMonitor monitor) + throws CoreException { + // get a project object + IProject project = getProject(); + + if (DEBUG_LOG) { + AdtPlugin.log(IStatus.INFO, "%s BUILD(PRE)", project.getName()); + } + + // For the PreCompiler, only the library projects are considered Referenced projects, + // as only those projects have an impact on what is generated by this builder. + IProject[] result = null; + + IFolder resOutFolder = null; + + try { + assert mDerivedProgressMonitor != null; + + mDerivedProgressMonitor.reset(); + + // get the project info + ProjectState projectState = Sdk.getProjectState(project); + + // this can happen if the project has no project.properties. + if (projectState == null) { + return null; + } + + boolean isLibrary = projectState.isLibrary(); + + IAndroidTarget projectTarget = projectState.getTarget(); + + // get the libraries + List<IProject> libProjects = projectState.getFullLibraryProjects(); + result = libProjects.toArray(new IProject[libProjects.size()]); + + IJavaProject javaProject = JavaCore.create(project); + + // Top level check to make sure the build can move forward. + abortOnBadSetup(javaProject, projectState); + + // now we need to get the classpath list + List<IPath> sourceFolderPathList = BaseProjectHelper.getSourceClasspaths(javaProject); + + IFolder androidOutputFolder = BaseProjectHelper.getAndroidOutputFolder(project); + + resOutFolder = getResOutFolder(androidOutputFolder); + + setupSourceProcessors(javaProject, projectState, sourceFolderPathList, + androidOutputFolder); + + PreCompilerDeltaVisitor dv = null; + String javaPackage = null; + String minSdkVersion = null; + + if (kind == FULL_BUILD) { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.Start_Full_Pre_Compiler); + + if (DEBUG_LOG) { + AdtPlugin.log(IStatus.INFO, "%s full build!", project.getName()); + } + + // do some clean up. + doClean(project, monitor); + + mMustMergeManifest = true; + mMustCompileResources = true; + mMustCreateBuildConfig = true; + + mAidlProcessor.prepareFullBuild(project); + mRenderScriptSourceChangeHandler.prepareFullBuild(); + } else { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.Start_Inc_Pre_Compiler); + + // Go through the resources and see if something changed. + // Even if the mCompileResources flag is true from a previously aborted + // build, we need to go through the Resource delta to get a possible + // list of aidl files to compile/remove. + IResourceDelta delta = getDelta(project); + if (delta == null) { + mMustCompileResources = true; + + mAidlProcessor.prepareFullBuild(project); + mRenderScriptSourceChangeHandler.prepareFullBuild(); + } else { + dv = new PreCompilerDeltaVisitor(this, sourceFolderPathList, + mAidlProcessor.getChangeHandler(), + mRenderScriptSourceChangeHandler); + delta.accept(dv); + + // Check to see if Manifest.xml, Manifest.java, or R.java have changed: + mMustCompileResources |= dv.getCompileResources(); + mMustMergeManifest |= dv.hasManifestChanged(); + + // Notify the ResourceManager: + ResourceManager resManager = ResourceManager.getInstance(); + + if (ResourceManager.isAutoBuilding()) { + ProjectResources projectResources = resManager.getProjectResources(project); + + IdeScanningContext context = new IdeScanningContext(projectResources, + project, true); + + boolean wasCleared = projectResources.ensureInitialized(); + + if (!wasCleared) { + resManager.processDelta(delta, context); + } + + // Check whether this project or its dependencies (libraries) have + // resources that need compilation + if (wasCleared || context.needsFullAapt()) { + mMustCompileResources = true; + + // Must also call markAaptRequested on the project to not just + // store "aapt required" on this project, but also on any projects + // depending on this project if it's a library project + ResourceManager.markAaptRequested(project); + } + + // Update error markers in the source editor + if (!mMustCompileResources) { + context.updateMarkers(false /* async */); + } + } // else: already processed the deltas in ResourceManager's IRawDeltaListener + + mAidlProcessor.doneVisiting(project); + + // get the java package from the visitor + javaPackage = dv.getManifestPackage(); + minSdkVersion = dv.getMinSdkVersion(); + } + } + + // Has anyone marked this project as needing aapt? Typically done when + // one of the library projects this project depends on has changed + mMustCompileResources |= ResourceManager.isAaptRequested(project); + + // if the main manifest didn't change, then we check for the library + // ones (will trigger manifest merging too) + if (libProjects.size() > 0) { + for (IProject libProject : libProjects) { + IResourceDelta delta = getDelta(libProject); + if (delta != null) { + PatternBasedDeltaVisitor visitor = new PatternBasedDeltaVisitor( + project, libProject, + "PRE:LibManifest"); //$NON-NLS-1$ + visitor.addSet(ChangedFileSetHelper.MANIFEST); + + ChangedFileSet textSymbolCFS = null; + if (isLibrary == false) { + textSymbolCFS = ChangedFileSetHelper.getTextSymbols( + libProject); + visitor.addSet(textSymbolCFS); + } + + delta.accept(visitor); + + mMustMergeManifest |= visitor.checkSet(ChangedFileSetHelper.MANIFEST); + + if (textSymbolCFS != null) { + mMustCompileResources |= visitor.checkSet(textSymbolCFS); + } + + // no need to test others if we have all flags at true. + if (mMustMergeManifest && + (mMustCompileResources || textSymbolCFS == null)) { + break; + } + } + } + } + + // store the build status in the persistent storage + saveProjectBooleanProperty(PROPERTY_MERGE_MANIFEST, mMustMergeManifest); + saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, mMustCompileResources); + saveProjectBooleanProperty(PROPERTY_COMPILE_BUILDCONFIG, mMustCreateBuildConfig); + + // if there was some XML errors, we just return w/o doing + // anything since we've put some markers in the files anyway. + if (dv != null && dv.mXmlError) { + AdtPlugin.printErrorToConsole(project, Messages.Xml_Error); + + return result; + } + + if (projectState.getRenderScriptSupportMode()) { + FullRevision minBuildToolsRev = new FullRevision(19,0,3); + if (mBuildToolInfo.getRevision().compareTo(minBuildToolsRev) == -1) { + String msg = "RenderScript support mode requires Build-Tools 19.0.3 or later."; + AdtPlugin.printErrorToConsole(project, msg); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + + return result; + } + } + + // get the manifest file + IFile manifestFile = ProjectHelper.getManifest(project); + + if (manifestFile == null) { + String msg = String.format(Messages.s_File_Missing, + SdkConstants.FN_ANDROID_MANIFEST_XML); + AdtPlugin.printErrorToConsole(project, msg); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + + return result; + + // TODO: document whether code below that uses manifest (which is now guaranteed + // to be null) will actually be executed or not. + } + + // lets check the XML of the manifest first, if that hasn't been done by the + // resource delta visitor yet. + if (dv == null || dv.getCheckedManifestXml() == false) { + BasicXmlErrorListener errorListener = new BasicXmlErrorListener(); + try { + ManifestData parser = AndroidManifestHelper.parseUnchecked( + new IFileWrapper(manifestFile), + true /*gather data*/, + errorListener); + + if (errorListener.mHasXmlError == true) { + // There was an error in the manifest, its file has been marked + // by the XmlErrorHandler. The stopBuild() call below will abort + // this with an exception. + String msg = String.format(Messages.s_Contains_Xml_Error, + SdkConstants.FN_ANDROID_MANIFEST_XML); + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, msg); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + + return result; + } + + // Get the java package from the parser. + // This can be null if the parsing failed because the resource is out of sync, + // in which case the error will already have been logged anyway. + if (parser != null) { + javaPackage = parser.getPackage(); + minSdkVersion = parser.getMinSdkVersionString(); + } + } catch (StreamException e) { + handleStreamException(e); + + return result; + } catch (ParserConfigurationException e) { + String msg = String.format( + "Bad parser configuration for %s: %s", + manifestFile.getFullPath(), + e.getMessage()); + + handleException(e, msg); + return result; + + } catch (SAXException e) { + String msg = String.format( + "Parser exception for %s: %s", + manifestFile.getFullPath(), + e.getMessage()); + + handleException(e, msg); + return result; + } catch (IOException e) { + String msg = String.format( + "I/O error for %s: %s", + manifestFile.getFullPath(), + e.getMessage()); + + handleException(e, msg); + return result; + } + } + + int minSdkValue = -1; + + if (minSdkVersion != null) { + try { + minSdkValue = Integer.parseInt(minSdkVersion); + } catch (NumberFormatException e) { + // it's ok, it means minSdkVersion contains a (hopefully) valid codename. + } + + AndroidVersion targetVersion = projectTarget.getVersion(); + + // remove earlier marker from the manifest + removeMarkersFromResource(manifestFile, AdtConstants.MARKER_ADT); + + if (minSdkValue != -1) { + String codename = targetVersion.getCodename(); + if (codename != null) { + // integer minSdk when the target is a preview => fatal error + String msg = String.format( + "Platform %1$s is a preview and requires application manifest to set %2$s to '%1$s'", + codename, AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION); + AdtPlugin.printErrorToConsole(project, msg); + BaseProjectHelper.markResource(manifestFile, AdtConstants.MARKER_ADT, + msg, IMarker.SEVERITY_ERROR); + return result; + } else if (minSdkValue > targetVersion.getApiLevel()) { + // integer minSdk is too high for the target => warning + String msg = String.format( + "Attribute %1$s (%2$d) is higher than the project target API level (%3$d)", + AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION, + minSdkValue, targetVersion.getApiLevel()); + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, msg); + BaseProjectHelper.markResource(manifestFile, AdtConstants.MARKER_ADT, + msg, IMarker.SEVERITY_WARNING); + } + } else { + // looks like the min sdk is a codename, check it matches the codename + // of the platform + String codename = targetVersion.getCodename(); + if (codename == null) { + // platform is not a preview => fatal error + String msg = String.format( + "Manifest attribute '%1$s' is set to '%2$s'. Integer is expected.", + AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION, minSdkVersion); + AdtPlugin.printErrorToConsole(project, msg); + BaseProjectHelper.markResource(manifestFile, AdtConstants.MARKER_ADT, + msg, IMarker.SEVERITY_ERROR); + return result; + } else if (codename.equals(minSdkVersion) == false) { + // platform and manifest codenames don't match => fatal error. + String msg = String.format( + "Value of manifest attribute '%1$s' does not match platform codename '%2$s'", + AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION, codename); + AdtPlugin.printErrorToConsole(project, msg); + BaseProjectHelper.markResource(manifestFile, AdtConstants.MARKER_ADT, + msg, IMarker.SEVERITY_ERROR); + return result; + } + + // if we get there, the minSdkVersion is a codename matching the target + // platform codename. In this case we set minSdkValue to the previous API + // level, as it's used by source processors. + minSdkValue = targetVersion.getApiLevel(); + } + } else if (projectTarget.getVersion().isPreview()) { + // else the minSdkVersion is not set but we are using a preview target. + // Display an error + String codename = projectTarget.getVersion().getCodename(); + String msg = String.format( + "Platform %1$s is a preview and requires application manifests to set %2$s to '%1$s'", + codename, AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION); + AdtPlugin.printErrorToConsole(project, msg); + BaseProjectHelper.markResource(manifestFile, AdtConstants.MARKER_ADT, msg, + IMarker.SEVERITY_ERROR); + return result; + } + + if (javaPackage == null || javaPackage.length() == 0) { + // looks like the AndroidManifest file isn't valid. + String msg = String.format(Messages.s_Doesnt_Declare_Package_Error, + SdkConstants.FN_ANDROID_MANIFEST_XML); + AdtPlugin.printErrorToConsole(project, msg); + BaseProjectHelper.markResource(manifestFile, AdtConstants.MARKER_ADT, + msg, IMarker.SEVERITY_ERROR); + + return result; + } else if (javaPackage.indexOf('.') == -1) { + // The application package name does not contain 2+ segments! + String msg = String.format( + "Application package '%1$s' must have a minimum of 2 segments.", + SdkConstants.FN_ANDROID_MANIFEST_XML); + AdtPlugin.printErrorToConsole(project, msg); + BaseProjectHelper.markResource(manifestFile, AdtConstants.MARKER_ADT, + msg, IMarker.SEVERITY_ERROR); + + return result; + } + + // at this point we have the java package. We need to make sure it's not a different + // package than the previous one that were built. + if (javaPackage.equals(mManifestPackage) == false) { + // The manifest package has changed, the user may want to update + // the launch configuration + if (mManifestPackage != null) { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.Checking_Package_Change); + + FixLaunchConfig flc = new FixLaunchConfig(project, mManifestPackage, + javaPackage); + flc.start(); + } + + // record the new manifest package, and save it. + mManifestPackage = javaPackage; + saveProjectStringProperty(PROPERTY_PACKAGE, mManifestPackage); + + // force a clean + doClean(project, monitor); + mMustMergeManifest = true; + mMustCompileResources = true; + mMustCreateBuildConfig = true; + mAidlProcessor.prepareFullBuild(project); + mRenderScriptSourceChangeHandler.prepareFullBuild(); + + saveProjectBooleanProperty(PROPERTY_MERGE_MANIFEST, mMustMergeManifest); + saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, mMustCompileResources); + saveProjectBooleanProperty(PROPERTY_COMPILE_BUILDCONFIG, mMustCreateBuildConfig); + } + + try { + handleBuildConfig(args); + } catch (IOException e) { + handleException(e, "Failed to create BuildConfig class"); + return result; + } + + // merge the manifest + if (mMustMergeManifest) { + boolean enabled = MANIFEST_MERGER_ENABLED_DEFAULT; + String propValue = projectState.getProperty(MANIFEST_MERGER_PROPERTY); + if (propValue != null) { + enabled = Boolean.valueOf(propValue); + } + + if (mergeManifest(androidOutputFolder, libProjects, enabled) == false) { + return result; + } + } + + List<File> libProjectsOut = new ArrayList<File>(libProjects.size()); + for (IProject libProject : libProjects) { + libProjectsOut.add( + BaseProjectHelper.getAndroidOutputFolder(libProject) + .getLocation().toFile()); + } + + // run the source processors + int processorStatus = SourceProcessor.COMPILE_STATUS_NONE; + + + try { + processorStatus |= mAidlProcessor.compileFiles(this, + project, projectTarget, sourceFolderPathList, + libProjectsOut, monitor); + } catch (Throwable t) { + handleException(t, "Failed to run aidl. Check workspace log for detail."); + return result; + } + + try { + processorStatus |= compileRs(minSdkValue, projectState, androidOutputFolder, + resOutFolder, monitor); + } catch (Throwable t) { + handleException(t, "Failed to run renderscript. Check workspace log for detail."); + return result; + } + + // if a processor created some resources file, force recompilation of the resources. + if ((processorStatus & SourceProcessor.COMPILE_STATUS_RES) != 0) { + mMustCompileResources = true; + // save the current state before attempting the compilation + saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, mMustCompileResources); + } + + // handle the resources, after the processors are run since some (renderscript) + // generate resources. + boolean compiledTheResources = mMustCompileResources; + if (mMustCompileResources) { + if (DEBUG_LOG) { + AdtPlugin.log(IStatus.INFO, "%s compiling resources!", project.getName()); + } + + IFile proguardFile = null; + if (projectState.getProperty(ProjectProperties.PROPERTY_PROGUARD_CONFIG) != null) { + proguardFile = androidOutputFolder.getFile(AdtConstants.FN_AAPT_PROGUARD); + } + + handleResources(project, javaPackage, projectTarget, manifestFile, resOutFolder, + libProjects, isLibrary, proguardFile); + } + + if (processorStatus == SourceProcessor.COMPILE_STATUS_NONE && + compiledTheResources == false) { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.Nothing_To_Compile); + } + } catch (AbortBuildException e) { + return result; + } finally { + // refresh the 'gen' source folder. Once this is done with the custom progress + // monitor to mark all new files as derived + mGenFolder.refreshLocal(IResource.DEPTH_INFINITE, mDerivedProgressMonitor); + if (resOutFolder != null) { + resOutFolder.refreshLocal(IResource.DEPTH_INFINITE, mDerivedProgressMonitor); + } + } + + return result; + } + + private IFolder getResOutFolder(IFolder androidOutputFolder) { + return androidOutputFolder.getFolder(AdtConstants.WS_BIN_RELATIVE_BC); + } + + @Override + protected void clean(IProgressMonitor monitor) throws CoreException { + super.clean(monitor); + + if (DEBUG_LOG) { + AdtPlugin.log(IStatus.INFO, "%s CLEAN(PRE)", getProject().getName()); + } + + doClean(getProject(), monitor); + if (mGenFolder != null) { + mGenFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor); + } + } + + private void doClean(IProject project, IProgressMonitor monitor) throws CoreException { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.Removing_Generated_Classes); + + // remove all the derived resources from the 'gen' source folder. + if (mGenFolder != null && mGenFolder.exists()) { + // gen folder should not be derived, but previous version could set it to derived + // so we make sure this isn't the case (or it'll get deleted by the clean) + mGenFolder.setDerived(false, monitor); + + removeDerivedResources(mGenFolder, monitor); + } + + // Clear the project of the generic markers + removeMarkersFromContainer(project, AdtConstants.MARKER_AAPT_COMPILE); + removeMarkersFromContainer(project, AdtConstants.MARKER_XML); + removeMarkersFromContainer(project, AdtConstants.MARKER_AIDL); + removeMarkersFromContainer(project, AdtConstants.MARKER_RENDERSCRIPT); + removeMarkersFromContainer(project, AdtConstants.MARKER_MANIFMERGER); + removeMarkersFromContainer(project, AdtConstants.MARKER_ANDROID); + + // Also clean up lint + EclipseLintClient.clearMarkers(project); + + // clean the project repo + ProjectResources res = ResourceManager.getInstance().getProjectResources(project); + res.clear(); + } + + @Override + protected void startupOnInitialize() { + try { + super.startupOnInitialize(); + + IProject project = getProject(); + + // load the previous IFolder and java package. + mManifestPackage = loadProjectStringProperty(PROPERTY_PACKAGE); + + // get the source folder in which all the Java files are created + mGenFolder = project.getFolder(SdkConstants.FD_GEN_SOURCES); + mDerivedProgressMonitor = new DerivedProgressMonitor(mGenFolder); + + // Load the current compile flags. We ask for true if not found to force a recompile. + mMustMergeManifest = loadProjectBooleanProperty(PROPERTY_MERGE_MANIFEST, true); + mMustCompileResources = loadProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, true); + mMustCreateBuildConfig = loadProjectBooleanProperty(PROPERTY_COMPILE_BUILDCONFIG, true); + Boolean v = ProjectHelper.loadBooleanProperty(project, PROPERTY_BUILDCONFIG_MODE); + if (v == null) { + // no previous build config mode? force regenerate + mMustCreateBuildConfig = true; + } else { + mLastBuildConfigMode = v; + } + + } catch (Throwable throwable) { + AdtPlugin.log(throwable, "Failed to finish PrecompilerBuilder#startupOnInitialize()"); + } + } + + private void setupSourceProcessors(@NonNull IJavaProject javaProject, + @NonNull ProjectState projectState, + @NonNull List<IPath> sourceFolderPathList, + @NonNull IFolder androidOutputFolder) { + if (mAidlProcessor == null) { + mAidlProcessor = new AidlProcessor(javaProject, mBuildToolInfo, mGenFolder); + } else { + mAidlProcessor.setBuildToolInfo(mBuildToolInfo); + } + + List<File> sourceFolders = Lists.newArrayListWithCapacity(sourceFolderPathList.size()); + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + + for (IPath path : sourceFolderPathList) { + IResource resource = root.findMember(path); + if (resource != null && resource.exists() && resource.getType() == IResource.FOLDER) { + IPath fullPath = resource.getLocation(); + if (fullPath != null) { + sourceFolders.add(fullPath.toFile()); + } + } + } + + RenderScriptChecker checker = new RenderScriptChecker(sourceFolders, + androidOutputFolder.getLocation().toFile()); + mRenderScriptSourceChangeHandler = new RsSourceChangeHandler(checker); + } + + private int compileRs(int minSdkValue, + @NonNull ProjectState projectState, + @NonNull IFolder androidOutputFolder, + @NonNull IFolder resOutFolder, + @NonNull IProgressMonitor monitor) + throws IOException, InterruptedException { + if (!mRenderScriptSourceChangeHandler.mustCompile()) { + return SourceProcessor.COMPILE_STATUS_NONE; + } + + RenderScriptChecker checker = mRenderScriptSourceChangeHandler.getChecker(); + + List<File> inputs = checker.findInputFiles(); + List<File> importFolders = checker.getSourceFolders(); + File buildFolder = androidOutputFolder.getLocation().toFile(); + + + // get the renderscript target + int rsTarget = minSdkValue == -1 ? 11 : minSdkValue; + String rsTargetStr = projectState.getProperty(ProjectProperties.PROPERTY_RS_TARGET); + if (rsTargetStr != null) { + try { + rsTarget = Integer.parseInt(rsTargetStr); + } catch (NumberFormatException e) { + handleException(e, String.format( + "Property %s is not an integer.", + ProjectProperties.PROPERTY_RS_TARGET)); + return SourceProcessor.COMPILE_STATUS_NONE; + } + } + + RenderScriptProcessor processor = new RenderScriptProcessor( + inputs, + importFolders, + buildFolder, + mGenFolder.getLocation().toFile(), + resOutFolder.getLocation().toFile(), + new File(buildFolder, SdkConstants.FD_RS_OBJ), + new File(buildFolder, SdkConstants.FD_RS_LIBS), + mBuildToolInfo, + rsTarget, + false /*debugBuild, always false for now*/, + 3, + projectState.getRenderScriptSupportMode()); + + // clean old dependency files fiest + checker.cleanDependencies(); + + // then clean old output files + processor.cleanOldOutput(checker.getOldOutputs()); + + RenderScriptLauncher launcher = new RenderScriptLauncher( + getProject(), + mGenFolder, + resOutFolder, + monitor, + AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE /*verbose*/); + + // and run the build + processor.build(launcher); + + return SourceProcessor.COMPILE_STATUS_CODE | SourceProcessor.COMPILE_STATUS_RES; + } + + @SuppressWarnings("deprecation") + private void handleBuildConfig(@SuppressWarnings("rawtypes") Map args) + throws IOException, CoreException { + boolean debugMode = !args.containsKey(RELEASE_REQUESTED); + + BuildConfigGenerator generator = new BuildConfigGenerator( + mGenFolder.getLocation().toOSString(), mManifestPackage, debugMode); + + if (mMustCreateBuildConfig == false) { + // check the file is present. + IFolder folder = getGenManifestPackageFolder(); + if (folder.exists(new Path(BuildConfigGenerator.BUILD_CONFIG_NAME)) == false) { + mMustCreateBuildConfig = true; + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, getProject(), + String.format("Class %1$s is missing!", + BuildConfigGenerator.BUILD_CONFIG_NAME)); + } else if (debugMode != mLastBuildConfigMode) { + // else if the build mode changed, force creation + mMustCreateBuildConfig = true; + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, getProject(), + String.format("Different build mode, must update %1$s!", + BuildConfigGenerator.BUILD_CONFIG_NAME)); + } + } + + if (mMustCreateBuildConfig) { + if (DEBUG_LOG) { + AdtPlugin.log(IStatus.INFO, "%s generating BuilderConfig!", getProject().getName()); + } + + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, getProject(), + String.format("Generating %1$s...", BuildConfigGenerator.BUILD_CONFIG_NAME)); + generator.generate(); + + mMustCreateBuildConfig = false; + saveProjectBooleanProperty(PROPERTY_COMPILE_BUILDCONFIG, mMustCreateBuildConfig); + saveProjectBooleanProperty(PROPERTY_BUILDCONFIG_MODE, mLastBuildConfigMode = debugMode); + } + } + + private boolean mergeManifest(IFolder androidOutFolder, List<IProject> libProjects, + boolean enabled) throws CoreException { + if (DEBUG_LOG) { + AdtPlugin.log(IStatus.INFO, "%s merging manifests!", getProject().getName()); + } + + IFile outFile = androidOutFolder.getFile(SdkConstants.FN_ANDROID_MANIFEST_XML); + IFile manifest = getProject().getFile(SdkConstants.FN_ANDROID_MANIFEST_XML); + + // remove existing markers from the manifest. + // FIXME: only remove from manifest once the markers are put there. + removeMarkersFromResource(getProject(), AdtConstants.MARKER_MANIFMERGER); + + // If the merging is not enabled or if there's no library then we simply copy the + // manifest over. + if (enabled == false || libProjects.size() == 0) { + try { + new FileOp().copyFile(manifest.getLocation().toFile(), + outFile.getLocation().toFile()); + + outFile.refreshLocal(IResource.DEPTH_INFINITE, mDerivedProgressMonitor); + + saveProjectBooleanProperty(PROPERTY_MERGE_MANIFEST, mMustMergeManifest = false); + } catch (IOException e) { + handleException(e, "Failed to copy Manifest"); + return false; + } + } else { + final ArrayList<String> errors = new ArrayList<String>(); + + // TODO change MergerLog.wrapSdkLog by a custom IMergerLog that will create + // and maintain error markers. + ManifestMerger merger = new ManifestMerger( + MergerLog.wrapSdkLog(new ILogger() { + @Override + public void warning(@NonNull String warningFormat, Object... args) { + AdtPlugin.printToConsole(getProject(), String.format(warningFormat, args)); + } + + @Override + public void info(@NonNull String msgFormat, Object... args) { + AdtPlugin.printToConsole(getProject(), String.format(msgFormat, args)); + } + + @Override + public void verbose(@NonNull String msgFormat, Object... args) { + info(msgFormat, args); + } + + @Override + public void error(@Nullable Throwable t, @Nullable String errorFormat, + Object... args) { + errors.add(String.format(errorFormat, args)); + } + }), + new AdtManifestMergeCallback()); + + File[] libManifests = new File[libProjects.size()]; + int libIndex = 0; + for (IProject lib : libProjects) { + libManifests[libIndex++] = lib.getFile(SdkConstants.FN_ANDROID_MANIFEST_XML) + .getLocation().toFile(); + } + + if (merger.process( + outFile.getLocation().toFile(), + manifest.getLocation().toFile(), + libManifests, + null /*injectAttributes*/, null /*packageOverride*/) == false) { + if (errors.size() > 1) { + StringBuilder sb = new StringBuilder(); + for (String s : errors) { + sb.append(s).append('\n'); + } + + markProject(AdtConstants.MARKER_MANIFMERGER, sb.toString(), + IMarker.SEVERITY_ERROR); + + } else if (errors.size() == 1) { + markProject(AdtConstants.MARKER_MANIFMERGER, errors.get(0), + IMarker.SEVERITY_ERROR); + } else { + markProject(AdtConstants.MARKER_MANIFMERGER, "Unknown error merging manifest", + IMarker.SEVERITY_ERROR); + } + return false; + } + + outFile.refreshLocal(IResource.DEPTH_INFINITE, mDerivedProgressMonitor); + saveProjectBooleanProperty(PROPERTY_MERGE_MANIFEST, mMustMergeManifest = false); + } + + return true; + } + + /** + * Handles resource changes and regenerate whatever files need regenerating. + * @param project the main project + * @param javaPackage the app package for the main project + * @param projectTarget the target of the main project + * @param manifest the {@link IFile} representing the project manifest + * @param libProjects the library dependencies + * @param isLibrary if the project is a library project + * @throws CoreException + * @throws AbortBuildException + */ + private void handleResources(IProject project, String javaPackage, IAndroidTarget projectTarget, + IFile manifest, IFolder resOutFolder, List<IProject> libProjects, boolean isLibrary, + IFile proguardFile) throws CoreException, AbortBuildException { + // get the resource folder + IFolder resFolder = project.getFolder(AdtConstants.WS_RESOURCES); + + // get the file system path + IPath outputLocation = mGenFolder.getLocation(); + IPath resLocation = resFolder.getLocation(); + IPath manifestLocation = manifest == null ? null : manifest.getLocation(); + + // those locations have to exist for us to do something! + if (outputLocation != null && resLocation != null + && manifestLocation != null) { + String osOutputPath = outputLocation.toOSString(); + String osResPath = resLocation.toOSString(); + String osManifestPath = manifestLocation.toOSString(); + + // remove the aapt markers + removeMarkersFromResource(manifest, AdtConstants.MARKER_AAPT_COMPILE); + removeMarkersFromContainer(resFolder, AdtConstants.MARKER_AAPT_COMPILE); + + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.Preparing_Generated_Files); + + // we need to figure out where to store the R class. + // get the parent folder for R.java and update mManifestPackageSourceFolder + IFolder mainPackageFolder = getGenManifestPackageFolder(); + + // handle libraries + ArrayList<IFolder> libResFolders = Lists.newArrayList(); + ArrayList<Pair<File, String>> libRFiles = Lists.newArrayList(); + if (libProjects != null) { + for (IProject lib : libProjects) { + IFolder libResFolder = lib.getFolder(SdkConstants.FD_RES); + if (libResFolder.exists()) { + libResFolders.add(libResFolder); + } + + try { + // get the package of the library, and if it's different form the + // main project, generate the R class for it too. + String libJavaPackage = AndroidManifest.getPackage(new IFolderWrapper(lib)); + if (libJavaPackage.equals(javaPackage) == false) { + + IFolder libOutput = BaseProjectHelper.getAndroidOutputFolder(lib); + File libOutputFolder = libOutput.getLocation().toFile(); + + libRFiles.add(Pair.of( + new File(libOutputFolder, "R.txt"), + libJavaPackage)); + + } + } catch (Exception e) { + } + } + } + + String proguardFilePath = proguardFile != null ? + proguardFile.getLocation().toOSString(): null; + + File resOutFile = resOutFolder.getLocation().toFile(); + String resOutPath = resOutFile.isDirectory() ? resOutFile.getAbsolutePath() : null; + + execAapt(project, projectTarget, osOutputPath, resOutPath, osResPath, osManifestPath, + mainPackageFolder, libResFolders, libRFiles, isLibrary, proguardFilePath); + } + } + + /** + * Executes AAPT to generate R.java/Manifest.java + * @param project the main project + * @param projectTarget the main project target + * @param osOutputPath the OS output path for the generated file. This is the source folder, not + * the package folder. + * @param osResPath the OS path to the res folder for the main project + * @param osManifestPath the OS path to the manifest of the main project + * @param packageFolder the IFolder that will contain the generated file. Unlike + * <var>osOutputPath</var> this is the direct parent of the generated files. + * If <var>customJavaPackage</var> is not null, this must match the new destination triggered + * by its value. + * @param libResFolders the list of res folders for the library. + * @param libRFiles a list of R files for the libraries. + * @param isLibrary if the project is a library project + * @param proguardFile an optional path to store proguard information + * @throws AbortBuildException + */ + @SuppressWarnings("deprecation") + private void execAapt(IProject project, IAndroidTarget projectTarget, String osOutputPath, + String osBcOutPath, String osResPath, String osManifestPath, IFolder packageFolder, + ArrayList<IFolder> libResFolders, List<Pair<File, String>> libRFiles, + boolean isLibrary, String proguardFile) + throws AbortBuildException { + + // We actually need to delete the manifest.java as it may become empty and + // in this case aapt doesn't generate an empty one, but instead doesn't + // touch it. + IFile manifestJavaFile = packageFolder.getFile(SdkConstants.FN_MANIFEST_CLASS); + manifestJavaFile.getLocation().toFile().delete(); + + // launch aapt: create the command line + ArrayList<String> array = new ArrayList<String>(); + + String aaptPath = mBuildToolInfo.getPath(BuildToolInfo.PathId.AAPT); + + array.add(aaptPath); + array.add("package"); //$NON-NLS-1$ + array.add("-m"); //$NON-NLS-1$ + if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) { + array.add("-v"); //$NON-NLS-1$ + } + + if (isLibrary) { + array.add("--non-constant-id"); //$NON-NLS-1$ + } + + if (libResFolders.size() > 0) { + array.add("--auto-add-overlay"); //$NON-NLS-1$ + } + + // If a library or has libraries, generate a text version of the R symbols. + File outputFolder = BaseProjectHelper.getAndroidOutputFolder(project).getLocation() + .toFile(); + + if (isLibrary || !libRFiles.isEmpty()) { + array.add("--output-text-symbols"); //$NON-NLS-1$ + array.add(outputFolder.getAbsolutePath()); + } + + array.add("-J"); //$NON-NLS-1$ + array.add(osOutputPath); + array.add("-M"); //$NON-NLS-1$ + array.add(osManifestPath); + if (osBcOutPath != null) { + array.add("-S"); //$NON-NLS-1$ + array.add(osBcOutPath); + } + array.add("-S"); //$NON-NLS-1$ + array.add(osResPath); + for (IFolder libResFolder : libResFolders) { + array.add("-S"); //$NON-NLS-1$ + array.add(libResFolder.getLocation().toOSString()); + } + + array.add("-I"); //$NON-NLS-1$ + array.add(projectTarget.getPath(IAndroidTarget.ANDROID_JAR)); + + // use the proguard file + if (proguardFile != null && proguardFile.length() > 0) { + array.add("-G"); + array.add(proguardFile); + } + + if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) { + StringBuilder sb = new StringBuilder(); + for (String c : array) { + sb.append(c); + sb.append(' '); + } + String cmd_line = sb.toString(); + AdtPlugin.printToConsole(project, cmd_line); + } + + // launch + try { + // launch the command line process + Process process = Runtime.getRuntime().exec( + array.toArray(new String[array.size()])); + + // list to store each line of stderr + ArrayList<String> stdErr = new ArrayList<String>(); + + // get the output and return code from the process + int returnCode = grabProcessOutput(process, stdErr); + + // attempt to parse the error output + boolean parsingError = AaptParser.parseOutput(stdErr, project); + + // if we couldn't parse the output we display it in the console. + if (parsingError) { + if (returnCode != 0) { + AdtPlugin.printErrorToConsole(project, stdErr.toArray()); + } else { + AdtPlugin.printBuildToConsole(BuildVerbosity.NORMAL, + project, stdErr.toArray()); + } + } + + if (returnCode != 0) { + // if the exec failed, and we couldn't parse the error output + // (and therefore not all files that should have been marked, + // were marked), we put a generic marker on the project and abort. + if (parsingError) { + markProject(AdtConstants.MARKER_ADT, + Messages.Unparsed_AAPT_Errors, IMarker.SEVERITY_ERROR); + } else if (stdErr.size() == 0) { + // no parsing error because sdterr was empty. We still need to put + // a marker otherwise there's no user visible feedback. + markProject(AdtConstants.MARKER_ADT, + String.format(Messages.AAPT_Exec_Error_d, returnCode), + IMarker.SEVERITY_ERROR); + } + + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + Messages.AAPT_Error); + + // abort if exec failed. + throw new AbortBuildException(); + } + + // now if the project has libraries, R needs to be created for each libraries + // unless this is a library. + if (isLibrary == false && !libRFiles.isEmpty()) { + File rFile = new File(outputFolder, SdkConstants.FN_RESOURCE_TEXT); + // if the project has no resources, the file could not exist. + if (rFile.isFile()) { + // Load the full symbols from the full R.txt file. + SymbolLoader fullSymbolValues = new SymbolLoader(rFile); + fullSymbolValues.load(); + + Multimap<String, SymbolLoader> libMap = ArrayListMultimap.create(); + + // First pass processing the libraries, collecting them by packageName, + // and ignoring the ones that have the same package name as the application + // (since that R class was already created). + + for (Pair<File, String> lib : libRFiles) { + String libPackage = lib.getSecond(); + File rText = lib.getFirst(); + + if (rText.isFile()) { + // load the lib symbols + SymbolLoader libSymbols = new SymbolLoader(rText); + libSymbols.load(); + + // store these symbols by associating them with the package name. + libMap.put(libPackage, libSymbols); + } + } + + // now loop on all the package names, merge all the symbols to write, + // and write them + for (String packageName : libMap.keySet()) { + Collection<SymbolLoader> symbols = libMap.get(packageName); + + SymbolWriter writer = new SymbolWriter(osOutputPath, packageName, + fullSymbolValues); + for (SymbolLoader symbolLoader : symbols) { + writer.addSymbolsToWrite(symbolLoader); + } + writer.write(); + } + } + } + + } catch (IOException e1) { + // something happen while executing the process, + // mark the project and exit + String msg; + String path = array.get(0); + if (!new File(path).exists()) { + msg = String.format(Messages.AAPT_Exec_Error_s, path); + } else { + String description = e1.getLocalizedMessage(); + if (e1.getCause() != null && e1.getCause() != e1) { + description = description + ": " + e1.getCause().getLocalizedMessage(); + } + msg = String.format(Messages.AAPT_Exec_Error_Other_s, description); + } + + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + + // Add workaround for the Linux problem described here: + // http://developer.android.com/sdk/installing.html#troubleshooting + // There are various posts on StackOverflow elsewhere where people are asking + // about aapt failing to run, so even though this is documented in the + // Troubleshooting section add an error message to help with this + // scenario. + if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX + && System.getProperty("os.arch").endsWith("64") //$NON-NLS-1$ //$NON-NLS-2$ + && new File(aaptPath).exists() + && new File("/usr/bin/apt-get").exists()) { //$NON-NLS-1$ + markProject(AdtConstants.MARKER_ADT, + "Hint: On 64-bit systems, make sure the 32-bit libraries are installed: \"sudo apt-get install ia32-libs\" or on some systems, \"sudo apt-get install lib32z1\"", + IMarker.SEVERITY_ERROR); + // Note - this uses SEVERITY_ERROR even though it's really SEVERITY_INFO because + // we want this error message to show up adjacent to the aapt error message + // (and Eclipse sorts by priority) + } + + // This interrupts the build. + throw new AbortBuildException(); + } catch (InterruptedException e) { + // we got interrupted waiting for the process to end... + // mark the project and exit + String msg = String.format(Messages.AAPT_Exec_Error_s, array.get(0)); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + + // This interrupts the build. + throw new AbortBuildException(); + } finally { + // we've at least attempted to run aapt, save the fact that we don't have to + // run it again, unless there's a new resource change. + saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, + mMustCompileResources = false); + ResourceManager.clearAaptRequest(project); + } + } + + /** + * Creates a relative {@link IPath} from a java package. + * @param javaPackageName the java package. + */ + private IPath getJavaPackagePath(String javaPackageName) { + // convert the java package into path + String[] segments = javaPackageName.split(AdtConstants.RE_DOT); + + StringBuilder path = new StringBuilder(); + for (String s : segments) { + path.append(AdtConstants.WS_SEP_CHAR); + path.append(s); + } + + return new Path(path.toString()); + } + + /** + * Returns an {@link IFolder} (located inside the 'gen' source folder), that matches the + * package defined in the manifest. This {@link IFolder} may not actually exist + * (aapt will create it anyway). + * @return the {@link IFolder} that will contain the R class or null if + * the folder was not found. + * @throws CoreException + */ + private IFolder getGenManifestPackageFolder() throws CoreException { + // get the path for the package + IPath packagePath = getJavaPackagePath(mManifestPackage); + + // get a folder for this path under the 'gen' source folder, and return it. + // This IFolder may not reference an actual existing folder. + return mGenFolder.getFolder(packagePath); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerDeltaVisitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerDeltaVisitor.java new file mode 100644 index 000000000..57316f568 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerDeltaVisitor.java @@ -0,0 +1,417 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build.builders; + +import com.android.SdkConstants; +import com.android.ide.common.xml.ManifestData; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.build.Messages; +import com.android.ide.eclipse.adt.internal.build.SourceChangeHandler; +import com.android.ide.eclipse.adt.internal.build.builders.BaseBuilder.BaseDeltaVisitor; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; +import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.io.IFileWrapper; +import com.google.common.collect.Lists; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IResourceDeltaVisitor; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; + +import java.util.Arrays; +import java.util.List; + +/** + * Resource Delta visitor for the pre-compiler. + * <p/>This delta visitor only cares about files that are the source or the result of actions of the + * {@link PreCompilerBuilder}: + * <ul><li>R.java/Manifest.java generated by compiling the resources</li> + * <li>Any Java files generated by <code>aidl</code></li></ul>. + * + * Therefore it looks for the following: + * <ul><li>Any modification in the resource folder</li> + * <li>Removed files from the source folder receiving generated Java files</li> + * <li>Any modification to aidl files.</li> + * + */ +class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements IResourceDeltaVisitor { + + // Result fields. + private boolean mChangedManifest = false; + + /** + * Compile flag. This is set to true if one of the changed/added/removed + * files is Manifest.java, or R.java. All other file changes + * will be taken care of by ResourceManager. + */ + private boolean mCompileResources = false; + + /** Manifest check/parsing flag. */ + private boolean mCheckedManifestXml = false; + + /** Application Package, gathered from the parsing of the manifest */ + private String mJavaPackage = null; + /** minSDKVersion attribute value, gathered from the parsing of the manifest */ + private String mMinSdkVersion = null; + + // Internal usage fields. + /** + * In Resource folder flag. This allows us to know if we're in the + * resource folder. + */ + private boolean mInRes = false; + + /** + * Current Source folder. This allows us to know if we're in a source + * folder, and which folder. + */ + private IFolder mSourceFolder = null; + + /** List of source folders. */ + private final List<IPath> mSourceFolders; + private boolean mIsGenSourceFolder = false; + + private final List<SourceChangeHandler> mSourceChangeHandlers = Lists.newArrayList(); + private final IWorkspaceRoot mRoot; + + private IFolder mAndroidOutputFolder; + + public PreCompilerDeltaVisitor(BaseBuilder builder, List<IPath> sourceFolders, + SourceChangeHandler... handlers) { + super(builder); + mSourceFolders = sourceFolders; + mRoot = ResourcesPlugin.getWorkspace().getRoot(); + + mSourceChangeHandlers.addAll(Arrays.asList(handlers)); + + mAndroidOutputFolder = BaseProjectHelper.getAndroidOutputFolder(builder.getProject()); + } + + /** + * Get whether Manifest.java, Manifest.xml, or R.java have changed + * @return true if any of Manifest.xml, Manifest.java, or R.java have been modified + */ + public boolean getCompileResources() { + return mCompileResources || mChangedManifest; + } + + public boolean hasManifestChanged() { + return mChangedManifest; + } + + /** + * Returns whether the manifest file was parsed/checked for error during the resource delta + * visiting. + */ + public boolean getCheckedManifestXml() { + return mCheckedManifestXml; + } + + /** + * Returns the manifest package if the manifest was checked/parsed. + * <p/> + * This can return null in two cases: + * <ul> + * <li>The manifest was not part of the resource change delta, and the manifest was + * not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li> + * <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>), + * but the package declaration is missing</li> + * </ul> + * @return the manifest package or null. + */ + public String getManifestPackage() { + return mJavaPackage; + } + + /** + * Returns the minSDkVersion attribute from the manifest if it was checked/parsed. + * <p/> + * This can return null in two cases: + * <ul> + * <li>The manifest was not part of the resource change delta, and the manifest was + * not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li> + * <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>), + * but the package declaration is missing</li> + * </ul> + * @return the minSdkVersion or null. + */ + public String getMinSdkVersion() { + return mMinSdkVersion; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.core.resources.IResourceDeltaVisitor + * #visit(org.eclipse.core.resources.IResourceDelta) + */ + @Override + public boolean visit(IResourceDelta delta) throws CoreException { + // we are only going to look for changes in res/, source folders and in + // AndroidManifest.xml since the delta visitor goes through the main + // folder before its children we can check when the path segment + // count is 2 (format will be /$Project/folder) and make sure we are + // processing res/, source folders or AndroidManifest.xml + + IResource resource = delta.getResource(); + IPath path = resource.getFullPath(); + String[] segments = path.segments(); + + // since the delta visitor also visits the root we return true if + // segments.length = 1 + if (segments.length == 1) { + // this is always the Android project since we call + // Builder#getDelta(IProject) on the project itself. + return true; + } else if (segments.length == 2) { + // if we are at an item directly under the root directory, + // then we are not yet in a source or resource folder + mInRes = false; + mSourceFolder = null; + + if (SdkConstants.FD_RESOURCES.equalsIgnoreCase(segments[1])) { + // this is the resource folder that was modified. we want to + // see its content. + + // since we're going to visit its children next, we set the + // flag + mInRes = true; + mSourceFolder = null; + return true; + } else if (SdkConstants.FN_ANDROID_MANIFEST_XML.equalsIgnoreCase(segments[1])) { + // any change in the manifest could trigger a new R.java + // class, so we don't need to check the delta kind + if (delta.getKind() != IResourceDelta.REMOVED) { + // clean the error markers on the file. + IFile manifestFile = (IFile)resource; + + if (manifestFile.exists()) { + manifestFile.deleteMarkers(AdtConstants.MARKER_XML, true, + IResource.DEPTH_ZERO); + manifestFile.deleteMarkers(AdtConstants.MARKER_ANDROID, true, + IResource.DEPTH_ZERO); + } + + // parse the manifest for data and error + ManifestData manifestData = AndroidManifestHelper.parse( + new IFileWrapper(manifestFile), true /*gatherData*/, this); + + if (manifestData != null) { + mJavaPackage = manifestData.getPackage(); + mMinSdkVersion = manifestData.getMinSdkVersionString(); + } + + mCheckedManifestXml = true; + } + mChangedManifest = true; + + // we don't want to go to the children, not like they are + // any for this resource anyway. + return false; + } + } + + // at this point we can either be in the source folder or in the + // resource folder or in a different folder that contains a source + // folder. + // This is due to not all source folder being src/. Some could be + // something/somethingelse/src/ + + // so first we test if we already know we are in a source or + // resource folder. + + if (mSourceFolder != null) { + // if we are in the res folder, we are looking for the following changes: + // - added/removed/modified aidl files. + // - missing R.java file + + // if the resource is a folder, we just go straight to the children + if (resource.getType() == IResource.FOLDER) { + return true; + } + + if (resource.getType() != IResource.FILE) { + return false; + } + IFile file = (IFile)resource; + + // get the modification kind + int kind = delta.getKind(); + + // we process normal source folder and the 'gen' source folder differently. + if (mIsGenSourceFolder) { + // this is the generated java file source folder. + // - if R.java/Manifest.java are removed/modified, we recompile the resources + // - if aidl files are removed/modified, we recompile them. + + boolean outputWarning = false; + + String fileName = resource.getName(); + + // Special case of R.java/Manifest.java. + if (SdkConstants.FN_RESOURCE_CLASS.equals(fileName) || + SdkConstants.FN_MANIFEST_CLASS.equals(fileName)) { + // if it was removed, there's a possibility that it was removed due to a + // package change, or an aidl that was removed, but the only thing + // that will happen is that we'll have an extra build. Not much of a problem. + mCompileResources = true; + + // we want a warning + outputWarning = true; + } else { + // look to see if this file was generated by a processor. + for (SourceChangeHandler handler : mSourceChangeHandlers) { + if (handler.handleGeneratedFile(file, kind)) { + outputWarning = true; + break; // there shouldn't be 2 processors that handle the same file. + } + } + } + + if (outputWarning) { + if (kind == IResourceDelta.REMOVED) { + // We print an error just so that it's red, but it's just a warning really. + String msg = String.format(Messages.s_Removed_Recreating_s, fileName); + AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg); + } else if (kind == IResourceDelta.CHANGED) { + // the file was modified manually! we can't allow it. + String msg = String.format(Messages.s_Modified_Manually_Recreating_s, + fileName); + AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg); + } + } + } else { + // this is another source folder. + for (SourceChangeHandler handler : mSourceChangeHandlers) { + handler.handleSourceFile(file, kind); + } + } + + // no children. + return false; + } else if (mInRes) { + // if we are in the res folder, we are looking for the following + // changes: + // - added/removed/modified xml files. + // - added/removed files of any other type + + // if the resource is a folder, we just go straight to the + // children + if (resource.getType() == IResource.FOLDER) { + return true; + } + + // get the extension of the resource + String ext = resource.getFileExtension(); + int kind = delta.getKind(); + + String p = resource.getProjectRelativePath().toString(); + String message = null; + switch (kind) { + case IResourceDelta.CHANGED: + // display verbose message + message = String.format(Messages.s_Modified_Recreating_s, p); + break; + case IResourceDelta.ADDED: + // display verbose message + message = String.format(Messages.Added_s_s_Needs_Updating, p, + SdkConstants.FN_RESOURCE_CLASS); + break; + case IResourceDelta.REMOVED: + // display verbose message + message = String.format(Messages.s_Removed_s_Needs_Updating, p, + SdkConstants.FN_RESOURCE_CLASS); + break; + } + if (message != null) { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, + mBuilder.getProject(), message); + } + + // If it's an XML resource, check the syntax + if (SdkConstants.EXT_XML.equalsIgnoreCase(ext) && kind != IResourceDelta.REMOVED) { + // check xml Validity + mBuilder.checkXML(resource, this); + } + // Whether or not to generate R.java for a changed resource is taken care of by the + // Resource Manager. + } else if (resource instanceof IFolder) { + // first check if we are in the android output folder. + if (resource.equals(mAndroidOutputFolder)) { + // we want to visit the merged manifest. + return true; + } + + // in this case we may be inside a folder that contains a source + // folder, go through the list of known source folders + + for (IPath sourceFolderPath : mSourceFolders) { + // first check if they match exactly. + if (sourceFolderPath.equals(path)) { + // this is a source folder! + mInRes = false; + mSourceFolder = getFolder(sourceFolderPath); // all non null due to test above + mIsGenSourceFolder = path.segmentCount() == 2 && + path.segment(1).equals(SdkConstants.FD_GEN_SOURCES); + return true; + } + + // check if we are on the way to a source folder. + int count = sourceFolderPath.matchingFirstSegments(path); + if (count == path.segmentCount()) { + mInRes = false; + return true; + } + } + + // if we're here, we are visiting another folder + // like /$Project/bin/ for instance (we get notified for changes + // in .class!) + // This could also be another source folder and we have found + // R.java in a previous source folder + // We don't want to visit its children + return false; + } + + return false; + } + + /** + * Returns a handle to the folder identified by the given path in this container. + * <p/>The different with {@link IContainer#getFolder(IPath)} is that this returns a non + * null object only if the resource actually exists and is a folder (and not a file) + * @param path the path of the folder to return. + * @return a handle to the folder if it exists, or null otherwise. + */ + private IFolder getFolder(IPath path) { + IResource resource = mRoot.findMember(path); + if (resource != null && resource.exists() && resource.getType() == IResource.FOLDER) { + return (IFolder)resource; + } + + return null; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ResourceManagerBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ResourceManagerBuilder.java new file mode 100644 index 000000000..8e01cca29 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ResourceManagerBuilder.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.build.builders; + +import com.android.SdkConstants; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.build.Messages; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.sdklib.BuildToolInfo; +import com.android.sdklib.IAndroidTarget; +import com.android.utils.Pair; + +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.SubProgressMonitor; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; + +import java.util.List; +import java.util.Map; + +/** + * Resource manager builder whose only purpose is to refresh the resource folder + * so that the other builder use an up to date version. + */ +public class ResourceManagerBuilder extends BaseBuilder { + + public static final String ID = "com.android.ide.eclipse.adt.ResourceManagerBuilder"; //$NON-NLS-1$ + + public ResourceManagerBuilder() { + super(); + } + + @Override + protected void clean(IProgressMonitor monitor) throws CoreException { + super.clean(monitor); + + // Get the project. + IProject project = getProject(); + + // Clear the project of the generic markers + removeMarkersFromContainer(project, AdtConstants.MARKER_ADT); + } + + // build() returns a list of project from which this project depends for future compilation. + @SuppressWarnings("unchecked") + @Override + protected IProject[] build(int kind, Map args, IProgressMonitor monitor) + throws CoreException { + // Get the project. + final IProject project = getProject(); + IJavaProject javaProject = JavaCore.create(project); + + // Clear the project of the generic markers + removeMarkersFromContainer(project, AdtConstants.MARKER_ADT); + + // check for existing target marker, in which case we abort. + // (this means: no SDK, no target, or unresolvable target.) + try { + abortOnBadSetup(javaProject, null); + } catch (AbortBuildException e) { + return null; + } + + // Check the compiler compliance level, displaying the error message + // since this is the first builder. + Pair<Integer, String> result = ProjectHelper.checkCompilerCompliance(project); + String errorMessage = null; + switch (result.getFirst().intValue()) { + case ProjectHelper.COMPILER_COMPLIANCE_LEVEL: + errorMessage = Messages.Requires_Compiler_Compliance_s; + break; + case ProjectHelper.COMPILER_COMPLIANCE_SOURCE: + errorMessage = Messages.Requires_Source_Compatibility_s; + break; + case ProjectHelper.COMPILER_COMPLIANCE_CODEGEN_TARGET: + errorMessage = Messages.Requires_Class_Compatibility_s; + break; + } + + if (errorMessage != null) { + errorMessage = String.format(errorMessage, + result.getSecond() == null ? "(no value)" : result.getSecond()); + + if (JavaCore.VERSION_1_7.equals(result.getSecond())) { + // If the user is trying to target 1.7 but compiling with something older, + // the error message can be a bit misleading; instead point them in the + // direction of updating the project's build target. + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk != null) { + IAndroidTarget target = currentSdk.getTarget(project.getProject()); + if (target != null && target.getVersion().getApiLevel() < 19) { + errorMessage = "Using 1.7 requires compiling with Android 4.4 " + + "(KitKat); currently using " + target.getVersion(); + } + + ProjectState projectState = Sdk.getProjectState(project); + if (projectState != null) { + BuildToolInfo buildToolInfo = projectState.getBuildToolInfo(); + if (buildToolInfo == null) { + buildToolInfo = currentSdk.getLatestBuildTool(); + } + if (buildToolInfo != null && buildToolInfo.getRevision().getMajor() < 19) { + errorMessage = "Using 1.7 requires using Android Build Tools " + + "version 19 or later; currently using " + + buildToolInfo.getRevision(); + } + } + } + } + + markProject(AdtConstants.MARKER_ADT, errorMessage, IMarker.SEVERITY_ERROR); + AdtPlugin.printErrorToConsole(project, errorMessage); + + return null; + } + + // Check that the SDK directory has been setup. + String osSdkFolder = AdtPlugin.getOsSdkFolder(); + + if (osSdkFolder == null || osSdkFolder.length() == 0) { + AdtPlugin.printErrorToConsole(project, Messages.No_SDK_Setup_Error); + markProject(AdtConstants.MARKER_ADT, Messages.No_SDK_Setup_Error, + IMarker.SEVERITY_ERROR); + + return null; + } + + // check the 'gen' source folder is present + boolean hasGenSrcFolder = false; // whether the project has a 'gen' source folder setup + + IClasspathEntry[] classpaths = javaProject.readRawClasspath(); + if (classpaths != null) { + for (IClasspathEntry e : classpaths) { + if (e.getEntryKind() == IClasspathEntry.CPE_SOURCE) { + IPath path = e.getPath(); + if (path.segmentCount() == 2 && + path.segment(1).equals(SdkConstants.FD_GEN_SOURCES)) { + hasGenSrcFolder = true; + break; + } + } + } + } + + boolean genFolderPresent = false; // whether the gen folder actually exists + IResource resource = project.findMember(SdkConstants.FD_GEN_SOURCES); + genFolderPresent = resource != null && resource.exists(); + + if (hasGenSrcFolder == false && genFolderPresent) { + // No source folder setup for 'gen' in the project, but there's already a + // 'gen' resource (file or folder). + String message; + if (resource.getType() == IResource.FOLDER) { + // folder exists already! This is an error. If the folder had been created + // by the NewProjectWizard, it'd be a source folder. + message = String.format("%1$s already exists but is not a source folder. Convert to a source folder or rename it.", + resource.getFullPath().toString()); + } else { + // resource exists but is not a folder. + message = String.format( + "Resource %1$s is in the way. ADT needs a source folder called 'gen' to work. Rename or delete resource.", + resource.getFullPath().toString()); + } + + AdtPlugin.printErrorToConsole(project, message); + markProject(AdtConstants.MARKER_ADT, message, IMarker.SEVERITY_ERROR); + + return null; + } else if (hasGenSrcFolder == false || genFolderPresent == false) { + // either there is no 'gen' source folder in the project (older SDK), + // or the folder does not exist (was deleted, or was a fresh svn checkout maybe.) + + // In case we are migrating from an older SDK, we go through the current source + // folders and delete the generated Java files. + List<IPath> sourceFolders = BaseProjectHelper.getSourceClasspaths(javaProject); + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + for (IPath path : sourceFolders) { + IResource member = root.findMember(path); + if (member != null) { + removeDerivedResources(member, monitor); + } + } + + // create the new source folder, if needed + IFolder genFolder = project.getFolder(SdkConstants.FD_GEN_SOURCES); + if (genFolderPresent == false) { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, + "Creating 'gen' source folder for generated Java files"); + genFolder.create(true /* force */, true /* local */, + new SubProgressMonitor(monitor, 10)); + } + + // add it to the source folder list, if needed only (or it will throw) + if (hasGenSrcFolder == false) { + IClasspathEntry[] entries = javaProject.getRawClasspath(); + entries = ProjectHelper.addEntryToClasspath(entries, + JavaCore.newSourceEntry(genFolder.getFullPath())); + javaProject.setRawClasspath(entries, new SubProgressMonitor(monitor, 10)); + } + + // refresh specifically the gen folder first, as it may break the build + // if it doesn't arrive in time then refresh the whole project as usual. + genFolder.refreshLocal(IResource.DEPTH_ZERO, new SubProgressMonitor(monitor, 10)); + project.refreshLocal(IResource.DEPTH_INFINITE, new SubProgressMonitor(monitor, 10)); + + // it seems like doing this fails to properly rebuild the project. the Java builder + // running right after this builder will not see the gen folder, and will not be + // restarted after this build. Therefore in this particular case, we start another + // build asynchronously so that it's rebuilt after this build. + launchJob(new Job("rebuild") { + @Override + protected IStatus run(IProgressMonitor m) { + try { + project.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, m); + return Status.OK_STATUS; + } catch (CoreException e) { + return e.getStatus(); + } + } + }); + + } + + // convert older projects which use bin as the eclipse output folder into projects + // using bin/classes + IFolder androidOutput = BaseProjectHelper.getAndroidOutputFolder(project); + IFolder javaOutput = BaseProjectHelper.getJavaOutputFolder(project); + if (androidOutput.exists() == false || javaOutput == null || + javaOutput.getParent().equals(androidOutput) == false) { + // get what we want as the new java output. + IFolder newJavaOutput = androidOutput.getFolder(SdkConstants.FD_CLASSES_OUTPUT); + + if (androidOutput.exists() == false) { + androidOutput.create(true /*force*/, true /*local*/, monitor); + } + + if (newJavaOutput.exists() == false) { + newJavaOutput.create(true /*force*/, true /*local*/, monitor); + } + + // set the java output to this project. + javaProject.setOutputLocation(newJavaOutput.getFullPath(), monitor); + + // need to do a full build. Can't build while we're already building, so launch a + // job to build it right after this build + launchJob(new Job("rebuild") { + @Override + protected IStatus run(IProgressMonitor jobMonitor) { + try { + project.build(IncrementalProjectBuilder.CLEAN_BUILD, jobMonitor); + return Status.OK_STATUS; + } catch (CoreException e) { + return e.getStatus(); + } + } + }); + } + + // check that we have bin/res/ + IFolder binResFolder = androidOutput.getFolder(SdkConstants.FD_RESOURCES); + if (binResFolder.exists() == false) { + binResFolder.create(true /* force */, true /* local */, + new SubProgressMonitor(monitor, 10)); + project.refreshLocal(IResource.DEPTH_ONE, new SubProgressMonitor(monitor, 10)); + } + + // Check the preference to be sure we are supposed to refresh + // the folders. + if (AdtPrefs.getPrefs().getBuildForceResResfresh()) { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, Messages.Refreshing_Res); + + // refresh the res folder. + IFolder resFolder = project.getFolder(AdtConstants.WS_RESOURCES); + resFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor); + + // Also refresh the assets folder to make sure the ApkBuilder + // will now it's changed and will force a new resource packaging. + IFolder assetsFolder = project.getFolder(AdtConstants.WS_ASSETS); + assetsFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor); + } + + return null; + } +} |