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 = projectState.getBuildToolInfo(); 137 if (buildToolInfo == null) { 138 buildToolInfo = Sdk.getCurrent().getLatestBuildTool(); 139 } 140 141 if (buildToolInfo == null) { 142 throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 143 "No Build Tools installed in the SDK.")); 144 } 145 146 BuildHelper helper = new BuildHelper(project, buildToolInfo, 147 fakeStream, fakeStream, 148 jumbo.booleanValue(), 149 dexMerger.booleanValue(), 150 debugMode, false /*verbose*/, 151 null /*resourceMarker*/); 152 153 // get the list of library projects 154 List<IProject> libProjects = projectState.getFullLibraryProjects(); 155 156 // Step 1. Package the resources. 157 158 // tmp file for the packaged resource file. To not disturb the incremental builders 159 // output, all intermediary files are created in tmp files. 160 File resourceFile = File.createTempFile(TEMP_PREFIX, SdkConstants.DOT_RES); 161 resourceFile.deleteOnExit(); 162 163 // Make sure the PNG crunch cache is up to date 164 helper.updateCrunchCache(); 165 166 // get the merged manifest 167 IFolder androidOutputFolder = BaseProjectHelper.getAndroidOutputFolder(project); 168 IFile mergedManifestFile = androidOutputFolder.getFile( 169 SdkConstants.FN_ANDROID_MANIFEST_XML); 170 171 172 // package the resources. 173 helper.packageResources( 174 mergedManifestFile, 175 libProjects, 176 null, // res filter 177 0, // versionCode 178 resourceFile.getParent(), 179 resourceFile.getName()); 180 181 // Step 2. Convert the byte code to Dalvik bytecode 182 183 // tmp file for the packaged resource file. 184 File dexFile = File.createTempFile(TEMP_PREFIX, SdkConstants.DOT_DEX); 185 dexFile.deleteOnExit(); 186 187 ProjectState state = Sdk.getProjectState(project); 188 String proguardConfig = state.getProperties().getProperty( 189 ProjectProperties.PROPERTY_PROGUARD_CONFIG); 190 191 boolean runProguard = false; 192 List<File> proguardConfigFiles = null; 193 if (proguardConfig != null && proguardConfig.length() > 0) { 194 // Be tolerant with respect to file and path separators just like 195 // Ant is. Allow "/" in the property file to mean whatever the file 196 // separator character is: 197 if (File.separatorChar != '/' && proguardConfig.indexOf('/') != -1) { 198 proguardConfig = proguardConfig.replace('/', File.separatorChar); 199 } 200 201 Iterable<String> paths = LintUtils.splitPath(proguardConfig); 202 for (String path : paths) { 203 if (path.startsWith(SDK_PROPERTY_REF)) { 204 path = AdtPrefs.getPrefs().getOsSdkFolder() + 205 path.substring(SDK_PROPERTY_REF.length()); 206 } else if (path.startsWith(HOME_PROPERTY_REF)) { 207 path = System.getProperty(HOME_PROPERTY) + 208 path.substring(HOME_PROPERTY_REF.length()); 209 } 210 File proguardConfigFile = new File(path); 211 if (proguardConfigFile.isAbsolute() == false) { 212 proguardConfigFile = new File(project.getLocation().toFile(), path); 213 } 214 if (proguardConfigFile.isFile()) { 215 if (proguardConfigFiles == null) { 216 proguardConfigFiles = new ArrayList<File>(); 217 } 218 proguardConfigFiles.add(proguardConfigFile); 219 runProguard = true; 220 } else { 221 throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 222 "Invalid proguard configuration file path " + proguardConfigFile 223 + " does not exist or is not a regular file", null)); 224 } 225 } 226 227 // get the proguard file output by aapt 228 if (proguardConfigFiles != null) { 229 IFile proguardFile = androidOutputFolder.getFile(AdtConstants.FN_AAPT_PROGUARD); 230 proguardConfigFiles.add(proguardFile.getLocation().toFile()); 231 } 232 } 233 234 Collection<String> dxInput; 235 236 if (runProguard) { 237 // get all the compiled code paths. This will contain both project output 238 // folder and jar files. 239 Collection<String> paths = helper.getCompiledCodePaths(); 240 241 // create a jar file containing all the project output (as proguard cannot 242 // process folders of .class files). 243 File inputJar = File.createTempFile(TEMP_PREFIX, SdkConstants.DOT_JAR); 244 inputJar.deleteOnExit(); 245 JarOutputStream jos = new JarOutputStream(new FileOutputStream(inputJar)); 246 247 // a list of the other paths (jar files.) 248 List<String> jars = new ArrayList<String>(); 249 250 for (String path : paths) { 251 File root = new File(path); 252 if (root.isDirectory()) { 253 addFileToJar(jos, root, root); 254 } else if (root.isFile()) { 255 jars.add(path); 256 } 257 } 258 jos.close(); 259 260 // destination file for proguard 261 File obfuscatedJar = File.createTempFile(TEMP_PREFIX, SdkConstants.DOT_JAR); 262 obfuscatedJar.deleteOnExit(); 263 264 // run proguard 265 helper.runProguard(proguardConfigFiles, inputJar, jars, obfuscatedJar, 266 new File(project.getLocation().toFile(), SdkConstants.FD_PROGUARD)); 267 268 helper.setProguardOutput(obfuscatedJar.getAbsolutePath()); 269 270 // dx input is proguard's output 271 dxInput = Collections.singletonList(obfuscatedJar.getAbsolutePath()); 272 } else { 273 // no proguard, simply get all the compiled code path: project output(s) + 274 // jar file(s) 275 dxInput = helper.getCompiledCodePaths(); 276 } 277 278 IJavaProject javaProject = JavaCore.create(project); 279 280 helper.executeDx(javaProject, dxInput, dexFile.getAbsolutePath()); 281 282 // Step 3. Final package 283 284 helper.finalPackage( 285 resourceFile.getAbsolutePath(), 286 dexFile.getAbsolutePath(), 287 outputFile.getAbsolutePath(), 288 libProjects, 289 key, 290 certificate, 291 null); //resourceMarker 292 293 // success! 294 } catch (CoreException e) { 295 throw e; 296 } catch (ProguardResultException e) { 297 String msg = String.format("Proguard returned with error code %d. See console", 298 e.getErrorCode()); 299 AdtPlugin.printErrorToConsole(project, msg); 300 AdtPlugin.printErrorToConsole(project, (Object[]) e.getOutput()); 301 throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 302 msg, e)); 303 } catch (ProguardExecException e) { 304 String msg = String.format("Failed to run proguard: %s", e.getMessage()); 305 AdtPlugin.printErrorToConsole(project, msg); 306 throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 307 msg, e)); 308 } catch (DuplicateFileException e) { 309 String msg = String.format( 310 "Found duplicate file for APK: %1$s\nOrigin 1: %2$s\nOrigin 2: %3$s", 311 e.getArchivePath(), e.getFile1(), e.getFile2()); 312 AdtPlugin.printErrorToConsole(project, msg); 313 throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 314 e.getMessage(), e)); 315 } catch (NativeLibInJarException e) { 316 String msg = e.getMessage(); 317 318 AdtPlugin.printErrorToConsole(project, msg); 319 AdtPlugin.printErrorToConsole(project, (Object[]) e.getAdditionalInfo()); 320 throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 321 e.getMessage(), e)); 322 } catch (DexException e) { 323 throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 324 e.getMessage(), e)); 325 } catch (ApkCreationException e) { 326 throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 327 e.getMessage(), e)); 328 } catch (Exception e) { 329 throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 330 "Failed to export application", e)); 331 } finally { 332 // move back to a debug build. 333 // By using a normal build, we'll simply rebuild the debug version, and let the 334 // builder decide whether to build the full package or not. 335 ProjectHelper.buildWithDeps(project, IncrementalProjectBuilder.FULL_BUILD, monitor); 336 project.refreshLocal(IResource.DEPTH_INFINITE, monitor); 337 } 338 } 339 340 /** 341 * Exports an unsigned release APK after prompting the user for a location. 342 * 343 * <strong>Must be called from the UI thread.</strong> 344 * 345 * @param project the project to export 346 */ 347 public static void exportUnsignedReleaseApk(final IProject project) { 348 Shell shell = Display.getCurrent().getActiveShell(); 349 350 // create a default file name for the apk. 351 String fileName = project.getName() + SdkConstants.DOT_ANDROID_PACKAGE; 352 353 // Pop up the file save window to get the file location 354 FileDialog fileDialog = new FileDialog(shell, SWT.SAVE); 355 356 fileDialog.setText("Export Project"); 357 fileDialog.setFileName(fileName); 358 359 final String saveLocation = fileDialog.open(); 360 if (saveLocation != null) { 361 new Job("Android Release Export") { 362 @Override 363 protected IStatus run(IProgressMonitor monitor) { 364 try { 365 exportReleaseApk(project, 366 new File(saveLocation), 367 null, //key 368 null, //certificate 369 monitor); 370 371 // this is unsigned export. Let's tell the developers to run zip align 372 AdtPlugin.displayWarning("Android IDE Plug-in", String.format( 373 "An unsigned package of the application was saved at\n%1$s\n\n" + 374 "Before publishing the application you will need to:\n" + 375 "- Sign the application with your release key,\n" + 376 "- run zipalign on the signed package. ZipAlign is located in <SDK>/tools/\n\n" + 377 "Aligning applications allows Android to use application resources\n" + 378 "more efficiently.", saveLocation)); 379 380 return Status.OK_STATUS; 381 } catch (CoreException e) { 382 AdtPlugin.displayError("Android IDE Plug-in", String.format( 383 "Error exporting application:\n\n%1$s", e.getMessage())); 384 return e.getStatus(); 385 } 386 } 387 }.schedule(); 388 } 389 } 390 391 /** 392 * Adds a file to a jar file. 393 * The <var>rootDirectory</var> dictates the path of the file inside the jar file. It must be 394 * a parent of <var>file</var>. 395 * @param jar the jar to add the file to 396 * @param file the file to add 397 * @param rootDirectory the rootDirectory. 398 * @throws IOException 399 */ 400 private static void addFileToJar(JarOutputStream jar, File file, File rootDirectory) 401 throws IOException { 402 if (file.isDirectory()) { 403 if (file.getName().equals("META-INF") == false) { 404 for (File child: file.listFiles()) { 405 addFileToJar(jar, child, rootDirectory); 406 } 407 } 408 } else if (file.isFile()) { 409 String rootPath = rootDirectory.getAbsolutePath(); 410 String path = file.getAbsolutePath(); 411 path = path.substring(rootPath.length()).replace("\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$ 412 if (path.charAt(0) == '/') { 413 path = path.substring(1); 414 } 415 416 JarEntry entry = new JarEntry(path); 417 entry.setTime(file.lastModified()); 418 jar.putNextEntry(entry); 419 420 // put the content of the file. 421 byte[] buffer = new byte[1024]; 422 int count; 423 BufferedInputStream bis = null; 424 try { 425 bis = new BufferedInputStream(new FileInputStream(file)); 426 while ((count = bis.read(buffer)) != -1) { 427 jar.write(buffer, 0, count); 428 } 429 } finally { 430 if (bis != null) { 431 try { 432 bis.close(); 433 } catch (IOException ignore) { 434 } 435 } 436 } 437 jar.closeEntry(); 438 } 439 } 440 } 441