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