Home | History | Annotate | Download | only in project
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.adt.internal.project;
     18 
     19 import static com.android.sdklib.internal.project.ProjectProperties.PROPERTY_SDK;
     20 
     21 import com.android.SdkConstants;
     22 import com.android.ide.eclipse.adt.AdtConstants;
     23 import com.android.ide.eclipse.adt.AdtPlugin;
     24 import com.android.ide.eclipse.adt.AndroidPrintStream;
     25 import com.android.ide.eclipse.adt.internal.build.BuildHelper;
     26 import com.android.ide.eclipse.adt.internal.build.DexException;
     27 import com.android.ide.eclipse.adt.internal.build.NativeLibInJarException;
     28 import com.android.ide.eclipse.adt.internal.build.ProguardExecException;
     29 import com.android.ide.eclipse.adt.internal.build.ProguardResultException;
     30 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
     31 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
     32 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     33 import com.android.ide.eclipse.adt.io.IFileWrapper;
     34 import com.android.sdklib.BuildToolInfo;
     35 import com.android.sdklib.build.ApkCreationException;
     36 import com.android.sdklib.build.DuplicateFileException;
     37 import com.android.sdklib.internal.project.ProjectProperties;
     38 import com.android.tools.lint.detector.api.LintUtils;
     39 import com.android.xml.AndroidManifest;
     40 
     41 import org.eclipse.core.resources.IFile;
     42 import org.eclipse.core.resources.IFolder;
     43 import org.eclipse.core.resources.IProject;
     44 import org.eclipse.core.resources.IResource;
     45 import org.eclipse.core.resources.IncrementalProjectBuilder;
     46 import org.eclipse.core.runtime.CoreException;
     47 import org.eclipse.core.runtime.IProgressMonitor;
     48 import org.eclipse.core.runtime.IStatus;
     49 import org.eclipse.core.runtime.Status;
     50 import org.eclipse.core.runtime.jobs.Job;
     51 import org.eclipse.jdt.core.IJavaProject;
     52 import org.eclipse.jdt.core.JavaCore;
     53 import org.eclipse.swt.SWT;
     54 import org.eclipse.swt.widgets.Display;
     55 import org.eclipse.swt.widgets.FileDialog;
     56 import org.eclipse.swt.widgets.Shell;
     57 
     58 import java.io.BufferedInputStream;
     59 import java.io.File;
     60 import java.io.FileInputStream;
     61 import java.io.FileOutputStream;
     62 import java.io.IOException;
     63 import java.io.OutputStream;
     64 import java.security.PrivateKey;
     65 import java.security.cert.X509Certificate;
     66 import java.util.ArrayList;
     67 import java.util.Collection;
     68 import java.util.Collections;
     69 import java.util.List;
     70 import java.util.jar.JarEntry;
     71 import java.util.jar.JarOutputStream;
     72 
     73 /**
     74  * Export helper to export release version of APKs.
     75  */
     76 public final class ExportHelper {
     77     private static final String HOME_PROPERTY = "user.home";                    //$NON-NLS-1$
     78     private static final String HOME_PROPERTY_REF = "${" + HOME_PROPERTY + '}'; //$NON-NLS-1$
     79     private static final String SDK_PROPERTY_REF = "${" + PROPERTY_SDK + '}';   //$NON-NLS-1$
     80     private final static String TEMP_PREFIX = "android_";                       //$NON-NLS-1$
     81 
     82     /**
     83      * Exports a release version of the application created by the given project.
     84      * @param project the project to export
     85      * @param outputFile the file to write
     86      * @param key the key to used for signing. Can be null.
     87      * @param certificate the certificate used for signing. Can be null.
     88      * @param monitor progress monitor
     89      * @throws CoreException if an error occurs
     90      */
     91     public static void exportReleaseApk(IProject project, File outputFile, PrivateKey key,
     92             X509Certificate certificate, IProgressMonitor monitor) throws CoreException {
     93 
     94         // the export, takes the output of the precompiler & Java builders so it's
     95         // important to call build in case the auto-build option of the workspace is disabled.
     96         // Also enable dependency building to make sure everything is up to date.
     97         // However do not package the APK since we're going to do it manually here, using a
     98         // different output location.
     99         ProjectHelper.compileInReleaseMode(project, monitor);
    100 
    101         // if either key or certificate is null, ensure the other is null.
    102         if (key == null) {
    103             certificate = null;
    104         } else if (certificate == null) {
    105             key = null;
    106         }
    107 
    108         try {
    109             // check if the manifest declares debuggable as true. While this is a release build,
    110             // debuggable in the manifest will override this and generate a debug build
    111             IResource manifestResource = project.findMember(SdkConstants.FN_ANDROID_MANIFEST_XML);
    112             if (manifestResource.getType() != IResource.FILE) {
    113                 throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    114                         String.format("%1$s missing.", SdkConstants.FN_ANDROID_MANIFEST_XML)));
    115             }
    116 
    117             IFileWrapper manifestFile = new IFileWrapper((IFile) manifestResource);
    118             boolean debugMode = AndroidManifest.getDebuggable(manifestFile);
    119 
    120             AndroidPrintStream fakeStream = new AndroidPrintStream(null, null, new OutputStream() {
    121                 @Override
    122                 public void write(int b) throws IOException {
    123                     // do nothing
    124                 }
    125             });
    126 
    127             ProjectState projectState = Sdk.getProjectState(project);
    128 
    129             // get the jumbo mode option
    130             String forceJumboStr = projectState.getProperty(AdtConstants.DEX_OPTIONS_FORCEJUMBO);
    131             Boolean jumbo = Boolean.valueOf(forceJumboStr);
    132 
    133             String dexMergerStr = projectState.getProperty(AdtConstants.DEX_OPTIONS_DISABLE_MERGER);
    134             Boolean dexMerger = Boolean.valueOf(dexMergerStr);
    135 
    136             BuildToolInfo buildToolInfo = getBuildTools(projectState);
    137 
    138             BuildHelper helper = new BuildHelper(
    139                     projectState,
    140                     buildToolInfo,
    141                     fakeStream, fakeStream,
    142                     jumbo.booleanValue(),
    143                     dexMerger.booleanValue(),
    144                     debugMode, false /*verbose*/,
    145                     null /*resourceMarker*/);
    146 
    147             // get the list of library projects
    148             List<IProject> libProjects = projectState.getFullLibraryProjects();
    149 
    150             // Step 1. Package the resources.
    151 
    152             // tmp file for the packaged resource file. To not disturb the incremental builders
    153             // output, all intermediary files are created in tmp files.
    154             File resourceFile = File.createTempFile(TEMP_PREFIX, SdkConstants.DOT_RES);
    155             resourceFile.deleteOnExit();
    156 
    157             // Make sure the PNG crunch cache is up to date
    158             helper.updateCrunchCache();
    159 
    160             // get the merged manifest
    161             IFolder androidOutputFolder = BaseProjectHelper.getAndroidOutputFolder(project);
    162             IFile mergedManifestFile = androidOutputFolder.getFile(
    163                     SdkConstants.FN_ANDROID_MANIFEST_XML);
    164 
    165 
    166             // package the resources.
    167             helper.packageResources(
    168                     mergedManifestFile,
    169                     libProjects,
    170                     null,   // res filter
    171                     0,      // versionCode
    172                     resourceFile.getParent(),
    173                     resourceFile.getName());
    174 
    175             // Step 2. Convert the byte code to Dalvik bytecode
    176 
    177             // tmp file for the packaged resource file.
    178             File dexFile = File.createTempFile(TEMP_PREFIX, SdkConstants.DOT_DEX);
    179             dexFile.deleteOnExit();
    180 
    181             ProjectState state = Sdk.getProjectState(project);
    182             String proguardConfig = state.getProperties().getProperty(
    183                     ProjectProperties.PROPERTY_PROGUARD_CONFIG);
    184 
    185             boolean runProguard = false;
    186             List<File> proguardConfigFiles = null;
    187             if (proguardConfig != null && proguardConfig.length() > 0) {
    188                 // Be tolerant with respect to file and path separators just like
    189                 // Ant is. Allow "/" in the property file to mean whatever the file
    190                 // separator character is:
    191                 if (File.separatorChar != '/' && proguardConfig.indexOf('/') != -1) {
    192                     proguardConfig = proguardConfig.replace('/', File.separatorChar);
    193                 }
    194 
    195                 Iterable<String> paths = LintUtils.splitPath(proguardConfig);
    196                 for (String path : paths) {
    197                     if (path.startsWith(SDK_PROPERTY_REF)) {
    198                         path = AdtPrefs.getPrefs().getOsSdkFolder() +
    199                                 path.substring(SDK_PROPERTY_REF.length());
    200                     } else if (path.startsWith(HOME_PROPERTY_REF)) {
    201                         path = System.getProperty(HOME_PROPERTY) +
    202                                 path.substring(HOME_PROPERTY_REF.length());
    203                     }
    204                     File proguardConfigFile = new File(path);
    205                     if (proguardConfigFile.isAbsolute() == false) {
    206                         proguardConfigFile = new File(project.getLocation().toFile(), path);
    207                     }
    208                     if (proguardConfigFile.isFile()) {
    209                         if (proguardConfigFiles == null) {
    210                             proguardConfigFiles = new ArrayList<File>();
    211                         }
    212                         proguardConfigFiles.add(proguardConfigFile);
    213                         runProguard = true;
    214                     } else {
    215                         throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    216                                 "Invalid proguard configuration file path " + proguardConfigFile
    217                                 + " does not exist or is not a regular file", null));
    218                     }
    219                 }
    220 
    221                 // get the proguard file output by aapt
    222                 if (proguardConfigFiles != null) {
    223                     IFile proguardFile = androidOutputFolder.getFile(AdtConstants.FN_AAPT_PROGUARD);
    224                     proguardConfigFiles.add(proguardFile.getLocation().toFile());
    225                 }
    226             }
    227 
    228             Collection<String> dxInput;
    229 
    230             if (runProguard) {
    231                 // get all the compiled code paths. This will contain both project output
    232                 // folder and jar files.
    233                 Collection<String> paths = helper.getCompiledCodePaths();
    234 
    235                 // create a jar file containing all the project output (as proguard cannot
    236                 // process folders of .class files).
    237                 File inputJar = File.createTempFile(TEMP_PREFIX, SdkConstants.DOT_JAR);
    238                 inputJar.deleteOnExit();
    239                 JarOutputStream jos = new JarOutputStream(new FileOutputStream(inputJar));
    240 
    241                 // a list of the other paths (jar files.)
    242                 List<String> jars = new ArrayList<String>();
    243 
    244                 for (String path : paths) {
    245                     File root = new File(path);
    246                     if (root.isDirectory()) {
    247                         addFileToJar(jos, root, root);
    248                     } else if (root.isFile()) {
    249                         jars.add(path);
    250                     }
    251                 }
    252                 jos.close();
    253 
    254                 // destination file for proguard
    255                 File obfuscatedJar = File.createTempFile(TEMP_PREFIX, SdkConstants.DOT_JAR);
    256                 obfuscatedJar.deleteOnExit();
    257 
    258                 // run proguard
    259                 helper.runProguard(proguardConfigFiles, inputJar, jars, obfuscatedJar,
    260                         new File(project.getLocation().toFile(), SdkConstants.FD_PROGUARD));
    261 
    262                 helper.setProguardOutput(obfuscatedJar.getAbsolutePath());
    263 
    264                 // dx input is proguard's output
    265                 dxInput = Collections.singletonList(obfuscatedJar.getAbsolutePath());
    266             } else {
    267                 // no proguard, simply get all the compiled code path: project output(s) +
    268                 // jar file(s)
    269                 dxInput = helper.getCompiledCodePaths();
    270             }
    271 
    272             IJavaProject javaProject = JavaCore.create(project);
    273 
    274             helper.executeDx(javaProject, dxInput, dexFile.getAbsolutePath());
    275 
    276             // Step 3. Final package
    277 
    278             helper.finalPackage(
    279                     resourceFile.getAbsolutePath(),
    280                     dexFile.getAbsolutePath(),
    281                     outputFile.getAbsolutePath(),
    282                     libProjects,
    283                     key,
    284                     certificate,
    285                     null); //resourceMarker
    286 
    287             // success!
    288         } catch (CoreException e) {
    289             throw e;
    290         } catch (ProguardResultException e) {
    291             String msg = String.format("Proguard returned with error code %d. See console",
    292                     e.getErrorCode());
    293             AdtPlugin.printErrorToConsole(project, msg);
    294             AdtPlugin.printErrorToConsole(project, (Object[]) e.getOutput());
    295             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    296                     msg, e));
    297         } catch (ProguardExecException e) {
    298             String msg = String.format("Failed to run proguard: %s", e.getMessage());
    299             AdtPlugin.printErrorToConsole(project, msg);
    300             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    301                     msg, e));
    302         } catch (DuplicateFileException e) {
    303             String msg = String.format(
    304                     "Found duplicate file for APK: %1$s\nOrigin 1: %2$s\nOrigin 2: %3$s",
    305                     e.getArchivePath(), e.getFile1(), e.getFile2());
    306             AdtPlugin.printErrorToConsole(project, msg);
    307             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    308                     e.getMessage(), e));
    309         } catch (NativeLibInJarException e) {
    310             String msg = e.getMessage();
    311 
    312             AdtPlugin.printErrorToConsole(project, msg);
    313             AdtPlugin.printErrorToConsole(project, (Object[]) e.getAdditionalInfo());
    314             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    315                     e.getMessage(), e));
    316         } catch (DexException e) {
    317             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    318                     e.getMessage(), e));
    319         } catch (ApkCreationException e) {
    320             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    321                     e.getMessage(), e));
    322         } catch (Exception e) {
    323             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    324                     "Failed to export application", e));
    325         } finally {
    326             // move back to a debug build.
    327             // By using a normal build, we'll simply rebuild the debug version, and let the
    328             // builder decide whether to build the full package or not.
    329             ProjectHelper.buildWithDeps(project, IncrementalProjectBuilder.FULL_BUILD, monitor);
    330             project.refreshLocal(IResource.DEPTH_INFINITE, monitor);
    331         }
    332     }
    333 
    334     public static BuildToolInfo getBuildTools(ProjectState projectState)
    335             throws CoreException {
    336         BuildToolInfo buildToolInfo = projectState.getBuildToolInfo();
    337         if (buildToolInfo == null) {
    338             buildToolInfo = Sdk.getCurrent().getLatestBuildTool();
    339         }
    340 
    341         if (buildToolInfo == null) {
    342             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    343                     "No Build Tools installed in the SDK."));
    344         }
    345         return buildToolInfo;
    346     }
    347 
    348     /**
    349      * Exports an unsigned release APK after prompting the user for a location.
    350      *
    351      * <strong>Must be called from the UI thread.</strong>
    352      *
    353      * @param project the project to export
    354      */
    355     public static void exportUnsignedReleaseApk(final IProject project) {
    356         Shell shell = Display.getCurrent().getActiveShell();
    357 
    358         // create a default file name for the apk.
    359         String fileName = project.getName() + SdkConstants.DOT_ANDROID_PACKAGE;
    360 
    361         // Pop up the file save window to get the file location
    362         FileDialog fileDialog = new FileDialog(shell, SWT.SAVE);
    363 
    364         fileDialog.setText("Export Project");
    365         fileDialog.setFileName(fileName);
    366 
    367         final String saveLocation = fileDialog.open();
    368         if (saveLocation != null) {
    369             new Job("Android Release Export") {
    370                 @Override
    371                 protected IStatus run(IProgressMonitor monitor) {
    372                     try {
    373                         exportReleaseApk(project,
    374                                 new File(saveLocation),
    375                                 null, //key
    376                                 null, //certificate
    377                                 monitor);
    378 
    379                         // this is unsigned export. Let's tell the developers to run zip align
    380                         AdtPlugin.displayWarning("Android IDE Plug-in", String.format(
    381                                 "An unsigned package of the application was saved at\n%1$s\n\n" +
    382                                 "Before publishing the application you will need to:\n" +
    383                                 "- Sign the application with your release key,\n" +
    384                                 "- run zipalign on the signed package. ZipAlign is located in <SDK>/tools/\n\n" +
    385                                 "Aligning applications allows Android to use application resources\n" +
    386                                 "more efficiently.", saveLocation));
    387 
    388                         return Status.OK_STATUS;
    389                     } catch (CoreException e) {
    390                         AdtPlugin.displayError("Android IDE Plug-in", String.format(
    391                                 "Error exporting application:\n\n%1$s", e.getMessage()));
    392                         return e.getStatus();
    393                     }
    394                 }
    395             }.schedule();
    396         }
    397     }
    398 
    399     /**
    400      * Adds a file to a jar file.
    401      * The <var>rootDirectory</var> dictates the path of the file inside the jar file. It must be
    402      * a parent of <var>file</var>.
    403      * @param jar the jar to add the file to
    404      * @param file the file to add
    405      * @param rootDirectory the rootDirectory.
    406      * @throws IOException
    407      */
    408     private static void addFileToJar(JarOutputStream jar, File file, File rootDirectory)
    409             throws IOException {
    410         if (file.isDirectory()) {
    411             if (file.getName().equals("META-INF") == false) {
    412                 for (File child: file.listFiles()) {
    413                     addFileToJar(jar, child, rootDirectory);
    414                 }
    415             }
    416         } else if (file.isFile()) {
    417             String rootPath = rootDirectory.getAbsolutePath();
    418             String path = file.getAbsolutePath();
    419             path = path.substring(rootPath.length()).replace("\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$
    420             if (path.charAt(0) == '/') {
    421                 path = path.substring(1);
    422             }
    423 
    424             JarEntry entry = new JarEntry(path);
    425             entry.setTime(file.lastModified());
    426             jar.putNextEntry(entry);
    427 
    428             // put the content of the file.
    429             byte[] buffer = new byte[1024];
    430             int count;
    431             BufferedInputStream bis = null;
    432             try {
    433                 bis = new BufferedInputStream(new FileInputStream(file));
    434                 while ((count = bis.read(buffer)) != -1) {
    435                     jar.write(buffer, 0, count);
    436                 }
    437             } finally {
    438                 if (bis != null) {
    439                     try {
    440                         bis.close();
    441                     } catch (IOException ignore) {
    442                     }
    443                 }
    444             }
    445             jar.closeEntry();
    446         }
    447     }
    448 }
    449