diff options
author | Xavier Ducrohet <xav@android.com> | 2010-05-10 17:29:51 -0700 |
---|---|---|
committer | Xavier Ducrohet <xav@android.com> | 2010-05-11 14:02:36 -0700 |
commit | 18362654ad11d0e27edbd043d5a24bdfd72915e1 (patch) | |
tree | f2db0f4110119503b29e19f72270ffb2e20e2ad6 /anttasks/src | |
parent | 2cc3f070f6c52ecaa93aeb3d97383919ff5f1ceb (diff) | |
download | sdk-18362654ad11d0e27edbd043d5a24bdfd72915e1.zip sdk-18362654ad11d0e27edbd043d5a24bdfd72915e1.tar.gz sdk-18362654ad11d0e27edbd043d5a24bdfd72915e1.tar.bz2 |
Add multi-apk export log.
The log is used to confirm that new export do not conflict
with previous build. The build file can also be used to set
per-apk minor version code.
Change-Id: Ic5ad98758aa327f6a5bc1d00e66cf5437ac098e4
Diffstat (limited to 'anttasks/src')
-rw-r--r-- | anttasks/src/com/android/ant/MultiApkExportTask.java | 368 |
1 files changed, 325 insertions, 43 deletions
diff --git a/anttasks/src/com/android/ant/MultiApkExportTask.java b/anttasks/src/com/android/ant/MultiApkExportTask.java index 2c161d5..50325a4 100644 --- a/anttasks/src/com/android/ant/MultiApkExportTask.java +++ b/anttasks/src/com/android/ant/MultiApkExportTask.java @@ -33,9 +33,12 @@ import org.apache.tools.ant.taskdefs.SubAnt; import org.apache.tools.ant.types.FileSet; import org.xml.sax.InputSource; +import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; import java.io.FilenameFilter; import java.io.IOException; import java.util.ArrayList; @@ -54,6 +57,11 @@ import javax.xml.xpath.XPathFactory; */ public class MultiApkExportTask extends Task { + private final static int MAX_MINOR = 100; + private final static int MAX_BUILDINFO = 100; + private final static int OFFSET_BUILD_INFO = MAX_MINOR; + private final static int OFFSET_VERSION_CODE = OFFSET_BUILD_INFO * MAX_BUILDINFO; + /** * Class representing one apk that needs to be generated. This contains * which project it must be created from, and which filters should be used. @@ -61,8 +69,16 @@ public class MultiApkExportTask extends Task { * This class is meant to be sortable in a way that allows generation of the buildInfo * value that goes in the composite versionCode. */ - private static class ExportData implements Comparable<ExportData> { + public static class ApkData implements Comparable<ApkData> { + + private final static int INDEX_OUTPUTNAME = 0; + private final static int INDEX_PROJECT = 1; + private final static int INDEX_MINOR = 2; + private final static int INDEX_MINSDK = 3; + private final static int INDEX_ABI = 4; + private final static int INDEX_MAX = 5; + String outputName; String relativePath; File project; int buildInfo; @@ -74,11 +90,11 @@ public class MultiApkExportTask extends Task { int glVersion; // screen size? - ExportData() { + public ApkData() { // do nothing. } - public ExportData(ExportData data) { + public ApkData(ApkData data) { relativePath = data.relativePath; project = data.project; buildInfo = data.buildInfo; @@ -88,7 +104,18 @@ public class MultiApkExportTask extends Task { glVersion = data.glVersion; } - public int compareTo(ExportData o) { + public String toString() { + StringBuilder sb = new StringBuilder(outputName); + sb.append(" / ").append(relativePath); + sb.append(" / ").append(buildInfo); + sb.append(" / ").append(minor); + sb.append(" / ").append(minSdkVersion); + sb.append(" / ").append(abi); + + return sb.toString(); + } + + public int compareTo(ApkData o) { int minSdkDiff = minSdkVersion - o.minSdkVersion; if (minSdkDiff != 0) { return minSdkDiff; @@ -116,6 +143,75 @@ public class MultiApkExportTask extends Task { return 0; } + + /** + * Writes the apk description in the given writer. a single line is used to write + * everything. + * @param writer + * @throws IOException + * + * @see {@link #read(String)} + */ + public void write(FileWriter writer) throws IOException { + for (int i = 0 ; i < ApkData.INDEX_MAX ; i++) { + write(i, writer); + } + } + + /** + * reads the apk description from a log line. + * @param data + * + * @see #write(FileWriter) + */ + public void read(String line) { + String[] dataStrs = line.split(","); + for (int i = 0 ; i < ApkData.INDEX_MAX ; i++) { + read(i, dataStrs); + } + } + + private void write(int index, FileWriter writer) throws IOException { + switch (index) { + case INDEX_OUTPUTNAME: + writeValue(writer, outputName); + break; + case INDEX_PROJECT: + writeValue(writer, relativePath); + break; + case INDEX_MINOR: + writeValue(writer, minor); + break; + case INDEX_MINSDK: + writeValue(writer, minSdkVersion); + break; + case INDEX_ABI: + writeValue(writer, abi != null ? abi : ""); + break; + } + } + + private void read(int index, String[] data) { + switch (index) { + case INDEX_OUTPUTNAME: + outputName = data[index]; + break; + case INDEX_PROJECT: + relativePath = data[index]; + break; + case INDEX_MINOR: + minor = Integer.parseInt(data[index]); + break; + case INDEX_MINSDK: + minSdkVersion = Integer.parseInt(data[index]); + break; + case INDEX_ABI: + if (index < data.length && data[index].length() > 0) { + abi = data[index]; + } + break; + } + } } private static enum Target { @@ -178,15 +274,47 @@ public class MultiApkExportTask extends Task { System.out.println("versionCode: " + version); // checks whether the projects can be signed. - String value = antProject.getProperty("key.store"); - String keyStore = value != null && value.length() > 0 ? value : null; - value = antProject.getProperty("key.alias"); - String keyAlias = value != null && value.length() > 0 ? value : null; - boolean canSign = keyStore != null && keyAlias != null; + boolean canSign = false; + String keyStore = null, keyAlias = null; + if (mTarget == Target.RELEASE) { + String value = antProject.getProperty("key.store"); + keyStore = value != null && value.length() > 0 ? value : null; + value = antProject.getProperty("key.alias"); + keyAlias = value != null && value.length() > 0 ? value : null; + canSign = keyStore != null && keyAlias != null; + } - ExportData[] projects = getProjects(antProject, appPackage); - HashSet<String> compiledProject = new HashSet<String>(); + // get the list of apk to export and their configuration. + ApkData[] apks = getProjects(antProject, appPackage); + + // look to see if there's an export log from a previous export + File log = getBuildLog(appPackage, versionCode); + if (mTarget == Target.RELEASE && log.isFile()) { + // load the log and compare to current export list. + // Any difference will force a new versionCode. + ApkData[] previousApks = getProjects(log); + + if (previousApks.length != apks.length) { + throw new BuildException(String.format( + "Project export is setup differently from previous export at versionCode %d.\n" + + "Any change in the multi-apk configuration require a increment of the versionCode.", + versionCode)); + } + for (int i = 0 ; i < previousApks.length ; i++) { + // update the minor value from what is in the log file. + apks[i].minor = previousApks[i].minor; + if (apks[i].compareTo(previousApks[i]) != 0) { + throw new BuildException(String.format( + "Project export is setup differently from previous export at versionCode %d.\n" + + "Any change in the multi-apk configuration require a increment of the versionCode.", + versionCode)); + } + } + } + + // some temp var used by the project loop + HashSet<String> compiledProject = new HashSet<String>(); XPathFactory xPathFactory = XPathFactory.newInstance(); File exportProjectOutput = new File(getValidatedProperty(antProject, "out.absolute.dir")); @@ -216,16 +344,16 @@ public class MultiApkExportTask extends Task { keyAliasPassword = getValidatedProperty(antProject, "key.alias.password"); } - for (ExportData projectData : projects) { + for (ApkData apk : apks) { // this output is prepended by "[android-export] " (17 chars), so we put 61 stars System.out.println("\n*************************************************************"); - System.out.println("Exporting project: " + projectData.relativePath); + System.out.println("Exporting project: " + apk.relativePath); SubAnt subAnt = new SubAnt(); subAnt.setTarget(mTarget.getTarget()); subAnt.setProject(antProject); - File subProjectFolder = new File(antProject.getBaseDir(), projectData.relativePath); + File subProjectFolder = new File(antProject.getBaseDir(), apk.relativePath); FileSet fileSet = new FileSet(); fileSet.setProject(antProject); @@ -240,19 +368,19 @@ public class MultiApkExportTask extends Task { // this project. // (projects can be export multiple time if some properties are set up to // generate more than one APK (for instance ABI split). - if (compiledProject.contains(projectData.relativePath) == false) { - compiledProject.add(projectData.relativePath); + if (compiledProject.contains(apk.relativePath) == false) { + compiledProject.add(apk.relativePath); } else { addProp(subAnt, "do.not.compile", "true"); } // set the version code, and filtering - String compositeVersionCode = getVersionCodeString(versionCode, projectData); + String compositeVersionCode = getVersionCodeString(versionCode, apk); addProp(subAnt, "version.code", compositeVersionCode); System.out.println("Composite versionCode: " + compositeVersionCode); - if (projectData.abi != null) { - addProp(subAnt, "filter.abi", projectData.abi); - System.out.println("ABI Filter: " + projectData.abi); + if (apk.abi != null) { + addProp(subAnt, "filter.abi", apk.abi); + System.out.println("ABI Filter: " + apk.abi); } // end of the output by this task. Everything that follows will be output @@ -279,7 +407,7 @@ public class MultiApkExportTask extends Task { // override the resource pack file. addProp(subAnt, "resource.package.file.name", - name + "-" + projectData.buildInfo + ".ap_"); + name + "-" + apk.buildInfo + ".ap_"); if (canSign) { // set the properties for the password. @@ -288,17 +416,18 @@ public class MultiApkExportTask extends Task { addProp(subAnt, "key.store.password", keyStorePassword); addProp(subAnt, "key.alias.password", keyAliasPassword); - // temporary file only get a filename change (still stored in the project // bin folder). addProp(subAnt, "out.unsigned.file.name", - name + "-" + projectData.buildInfo + "-unsigned.apk"); + name + "-" + apk.buildInfo + "-unsigned.apk"); addProp(subAnt, "out.unaligned.file", - name + "-" + projectData.buildInfo + "-unaligned.apk"); + name + "-" + apk.buildInfo + "-unaligned.apk"); // final file is stored locally. + apk.outputName = name + "-" + compositeVersionCode + "-release.apk"; addProp(subAnt, "out.release.file", new File(exportProjectOutput, - name + "-" + projectData.buildInfo + "-release.apk").getAbsolutePath()); + apk.outputName).getAbsolutePath()); + } else { // put some empty prop. This is to override possible ones defined in the // project. The reason is that if there's more than one project, we don't @@ -307,16 +436,18 @@ public class MultiApkExportTask extends Task { addProp(subAnt, "key.store", ""); addProp(subAnt, "key.alias", ""); // final file is the unsigned version. It gets stored locally. + apk.outputName = name + "-" + compositeVersionCode + "-unsigned.apk"; addProp(subAnt, "out.unsigned.file", new File(exportProjectOutput, - name + "-" + projectData.buildInfo + "-unsigned.apk").getAbsolutePath()); + apk.outputName).getAbsolutePath()); } } subAnt.execute(); } - // TODO: export build log. - + if (mTarget == Target.RELEASE) { + makeBuildLog(appPackage, versionCode, apks); + } } /** @@ -343,11 +474,11 @@ public class MultiApkExportTask extends Task { * @param antProject the Ant project. * @param appPackage the application package. Projects' manifest must match this. */ - private ExportData[] getProjects(Project antProject, String appPackage) { + private ApkData[] getProjects(Project antProject, String appPackage) { String projects = antProject.getProperty("projects"); String[] paths = projects.split("\\:"); - ArrayList<ExportData> datalist = new ArrayList<ExportData>(); + ArrayList<ApkData> datalist = new ArrayList<ApkData>(); for (String path : paths) { File projectFolder = new File(path); @@ -374,11 +505,11 @@ public class MultiApkExportTask extends Task { SdkConstants.FN_ANDROID_MANIFEST_XML)); } - ArrayList<ExportData> datalist2 = checkManifest(androidManifest, appPackage); + ArrayList<ApkData> datalist2 = checkManifest(androidManifest, appPackage); // if the method returns without throwing, this is a good project to // export. - for (ExportData data : datalist2) { + for (ApkData data : datalist2) { data.relativePath = path; data.project = projectFolder; } @@ -393,14 +524,22 @@ public class MultiApkExportTask extends Task { // sort the projects and assign buildInfo Collections.sort(datalist); int buildInfo = 0; - for (ExportData data : datalist) { + for (ApkData data : datalist) { data.buildInfo = buildInfo++; } - return datalist.toArray(new ExportData[datalist.size()]); + return datalist.toArray(new ApkData[datalist.size()]); } - private ArrayList<ExportData> checkManifest(FileWrapper androidManifest, String appPackage) { + /** + * Checks a manifest of the project for inclusion in the list of exported APK. + * If the manifest is correct, a list of apk to export is created and returned. + * @param androidManifest the manifest to check + * @param appPackage the package name of the application being exported, as read from + * export.properties. + * @return + */ + private ArrayList<ApkData> checkManifest(FileWrapper androidManifest, String appPackage) { try { String manifestPackage = AndroidManifest.getPackage(androidManifest); if (appPackage.equals(manifestPackage) == false) { @@ -421,8 +560,8 @@ public class MultiApkExportTask extends Task { "Codename in minSdkVersion is not supported by multi-apk export."); } - ArrayList<ExportData> dataList = new ArrayList<ExportData>(); - ExportData data = new ExportData(); + ArrayList<ApkData> dataList = new ArrayList<ApkData>(); + ApkData data = new ApkData(); dataList.add(data); data.minSdkVersion = minSdkVersion; @@ -441,10 +580,10 @@ public class MultiApkExportTask extends Task { if (apkSettings.isSplitByAbi()) { // need to find the available ABIs. List<String> abis = findAbis(projectPath); - ExportData current = data; + ApkData current = data; for (String abi : abis) { if (current == null) { - current = new ExportData(data); + current = new ApkData(data); dataList.add(current); } @@ -464,6 +603,11 @@ public class MultiApkExportTask extends Task { } } + /** + * Finds ABIs in a project folder. This is based on the presence of libs/<abi>/ folder. + * @param projectPath + * @return + */ private List<String> findAbis(String projectPath) { ArrayList<String> abiList = new ArrayList<String>(); File libs = new File(projectPath, SdkConstants.FD_NATIVE_LIBS); @@ -488,6 +632,12 @@ public class MultiApkExportTask extends Task { return abiList; } + /** + * Adds a property to a {@link SubAnt} task. + * @param task the task. + * @param name the name of the property. + * @param value the value of the property. + */ private void addProp(SubAnt task, String name, String value) { Property prop = new Property(); prop.setName(name); @@ -495,12 +645,144 @@ public class MultiApkExportTask extends Task { task.addProperty(prop); } - private String getVersionCodeString(int versionCode, ExportData projectData) { - int trueVersionCode = versionCode * 10000; - trueVersionCode += projectData.buildInfo * 100; - trueVersionCode += projectData.minor; + /** + * Computes and returns the composite version code + * @param versionCode the major version code. + * @param apkData the apk data. + * @return the composite versionCode to be used in the manifest. + */ + private String getVersionCodeString(int versionCode, ApkData apkData) { + int trueVersionCode = versionCode * OFFSET_VERSION_CODE; + trueVersionCode += apkData.buildInfo * OFFSET_BUILD_INFO; + trueVersionCode += apkData.minor; return Integer.toString(trueVersionCode); } + /** + * Returns the {@link File} for the build log. + * @param appPackage + * @param versionCode + * @return + */ + private File getBuildLog(String appPackage, int versionCode) { + return new File(appPackage + "." + versionCode + ".log"); + } + + /** + * Loads and returns a list of {@link ApkData} from a build log. + * @param log + * @return + */ + private ApkData[] getProjects(File log) { + ArrayList<ApkData> datalist = new ArrayList<ApkData>(); + + FileReader reader = null; + BufferedReader bufferedReader = null; + try { + reader = new FileReader(log); + bufferedReader = new BufferedReader(reader); + String line; + int lineIndex = 0; + int apkIndex = 0; + while ((line = bufferedReader.readLine()) != null) { + line = line.trim(); + if (line.length() == 0 || line.startsWith("#")) { + continue; + } + + switch (lineIndex) { + case 0: + // read package value + lineIndex++; + break; + case 1: + // read versionCode value + lineIndex++; + break; + default: + // read apk description + ApkData data = new ApkData(); + data.buildInfo = apkIndex++; + datalist.add(data); + data.read(line); + if (data.minor >= MAX_MINOR) { + throw new BuildException( + "Valid minor version code values are 0-" + (MAX_MINOR-1)); + } + break; + } + } + } catch (IOException e) { + throw new BuildException("Failed to read existing build log", e); + } finally { + try { + if (reader != null) { + reader.close(); + } + } catch (IOException e) { + throw new BuildException("Failed to read existing build log", e); + } + } + + return datalist.toArray(new ApkData[datalist.size()]); + } + + /** + * Writes the build log for a given list of {@link ApkData}. + * @param appPackage + * @param versionCode + * @param apks + */ + private void makeBuildLog(String appPackage, int versionCode, ApkData[] apks) { + File log = getBuildLog(appPackage, versionCode); + FileWriter writer = null; + try { + writer = new FileWriter(log); + + writer.append("# Multi-APK BUILD log.\n"); + writer.append("# Only edit manually to change minor versions.\n"); + + writeValue(writer, "package", appPackage); + writeValue(writer, "versionCode", versionCode); + + writer.append("# what follows is one line per generated apk with its description.\n"); + writer.append("# the format is CSV in the following order:\n"); + writer.append("# apkname,project,minor, minsdkversion, abi filter,\n"); + + for (ApkData apk : apks) { + apk.write(writer); + writer.append('\n'); + } + + writer.flush(); + } catch (IOException e) { + throw new BuildException("Failed to write build log", e); + } finally { + try { + if (writer != null) { + writer.close(); + } + } catch (IOException e) { + throw new BuildException("Failed to write build log", e); + } + } + } + + private static void writeValue(FileWriter writer, String value) throws IOException { + writer.append(value).append(','); + } + + private static void writeValue(FileWriter writer, int value) throws IOException { + writeValue(writer, Integer.toString(value)); + } + + private void writeValue(FileWriter writer, String name, String value) throws IOException { + writer.append(name).append('=').append(value).append('\n'); + } + + private void writeValue(FileWriter writer, String name, int value) throws IOException { + writeValue(writer, name, Integer.toString(value)); + } + } |