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 */ exportReleaseApk(IProject project, File outputFile, PrivateKey key, X509Certificate certificate, IProgressMonitor monitor)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 getBuildTools(ProjectState projectState)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 */ exportUnsignedReleaseApk(final IProject project)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 */ addFileToJar(JarOutputStream jar, File file, File rootDirectory)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