Commit bd0f0111 authored by Ralph Morelli's avatar Ralph Morelli

Addresses Issue #259 -- implements annotations for component libraries (jars)

The main result of this change is that libraries are now conditionally loaded
into the packaged app depending on what libraries are used by the components
in the app. (Repl still loads all libraries for all components.)

Adds the new class components/annotations/UsesLibraries.java and revises
scripts/ComponentListGenerator.java and ComponentProcess.java, which are
used to process annotations.

Revised Compiler.java to dynamically construct the classpath and the
dxCommand line from the annotations.

Revised Twitter component to add an annotation:

  @UsesLibraries(libraries = "twitter4j.jar")

Tested on existing projects, such as HelloPurr to confirm that Twitter
library is not loaded into HelloPurr.apk.

10/14: This changes does not affect the Ya version number or the
    Twitter component version.

10/15: Fixed copyright, removed commented-out code, as per reviewer's
    suggestions.

Change-Id: I7acb5d8e2702d47b8f2bdc8f0746ef7d550963c2
parent edda4e85
......@@ -358,6 +358,7 @@
components_ComponentList builds:
- build/components/simple_components.txt
- build/components/simple_components_permissions.json
- build/components/simple_components_libraries.json
==================================================================== -->
<target name="components_ComponentList">
......
......@@ -122,13 +122,14 @@
<copy todir="${classes.files.dir}" flatten="true">
<fileset dir="${src.dir}/${buildserver.pkg}/resources" includes="*"/>
<fileset dir="${build.dir}/components"
includes="AndroidRuntime.jar,simple_components.txt,simple_components_permissions.json"/>
includes="AndroidRuntime.jar,simple_components.txt,simple_components_permissions.json,simple_components_libraries.json"/>
</copy>
<copy toFile="${classes.files.dir}/YailGenerator.jar" file="${local.build.dir}/YailGenerator.jar" />
<copy toFile="${classes.files.dir}/kawa.jar" file="${lib.dir}/kawa/kawa-1.11-modified.jar" />
<copy toFile="${classes.files.dir}/twitter4j.jar" file="${lib.dir}/twitter/twitter4j-2.0.10-SNAPSHOT.jar" />
<copy toFile="${classes.files.dir}/android.jar" file="${lib.dir}/android/2.2/android.jar" />
<copy toFile="${classes.files.dir}/dx.jar" file="${lib.dir}/android/tools/dx.jar" />
<property name="classes.tools.dir" location="${BuildServer-class.dir}/tools" />
<copy todir="${classes.tools.dir}">
<fileset dir="${lib.dir}/android/tools" includes="*/aapt" />
......
......@@ -28,6 +28,7 @@ import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
......@@ -69,6 +70,9 @@ public final class Compiler {
private static final String COMPONENT_PERMISSIONS =
RUNTIME_FILES_DIR + "simple_components_permissions.json";
private static final String COMPONENT_LIBRARIES =
RUNTIME_FILES_DIR + "simple_components_libraries.json";
/*
* Resource paths to yail runtime, runtime library files and sdk tools.
......@@ -86,10 +90,9 @@ public final class Compiler {
"/tools/linux/aapt";
private static final String KAWA_RUNTIME =
RUNTIME_FILES_DIR + "kawa.jar";
private static final String TWITTER_RUNTIME =
RUNTIME_FILES_DIR + "twitter4j.jar";
private static final String DX_JAR =
RUNTIME_FILES_DIR + "dx.jar";
@VisibleForTesting
static final String YAIL_RUNTIME =
RUNTIME_FILES_DIR + "runtime.scm";
......@@ -100,6 +103,9 @@ public final class Compiler {
private static final ConcurrentMap<String, Set<String>> componentPermissions =
new ConcurrentHashMap<String, Set<String>>();
private static final ConcurrentMap<String, Set<String>> componentLibraries =
new ConcurrentHashMap<String, Set<String>>();
/**
* Map used to hold the names and paths of resources that we've written out
* as temp files.
......@@ -128,6 +134,7 @@ public final class Compiler {
private final boolean isForRepl;
// Maximum ram that can be used by a child processes, in MB.
private final int childProcessRamMb;
private static Set<String> librariesNeeded; // Set of component libraries
/*
......@@ -156,6 +163,35 @@ public final class Compiler {
}
return permissions;
}
/*
* Generate the set of Android permissions needed by this project.
*/
@VisibleForTesting
Set<String> generateLibraryNames() {
// Before we can use componentLibraries, we have to call loadComponentLibraries().
try {
loadComponentLibraryNames();
} catch (IOException e) {
// This is fatal.
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "Libraries"));
return null;
} catch (JSONException e) {
// This is fatal, but shouldn't actually ever happen.
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "Libraries"));
return null;
}
Set<String> libraries = Sets.newHashSet();
for (String componentType : componentTypes) {
libraries.addAll(componentLibraries.get(componentType));
}
return libraries;
}
/*
* Creates an AndroidManifest.xml file needed for the Android application.
......@@ -286,11 +322,15 @@ public final class Compiler {
PrintStream out, PrintStream err, PrintStream userErrors,
boolean isForRepl, String keystoreFilePath, int childProcessRam) {
long start = System.currentTimeMillis();
// Create a new compiler instance for the compilation
Compiler compiler = new Compiler(project, componentTypes, out, err, userErrors, isForRepl,
childProcessRam);
// Get the names of component libraries for classpath and dx command line
librariesNeeded = compiler.generateLibraryNames();
// Create build directory.
File buildDir = createDirectory(project.getBuildDirectory());
......@@ -483,10 +523,10 @@ public final class Compiler {
int srcIndex = sourceFileName.indexOf("/../src/");
String sourceFileRelativePath = sourceFileName.substring(srcIndex + 8);
String classFileName = (classesDir.getAbsolutePath() + "/" + sourceFileRelativePath)
.replace(YoungAndroidConstants.YAIL_EXTENSION, ".class");
.replace(YoungAndroidConstants.YAIL_EXTENSION, ".class");
if (System.getProperty("os.name").startsWith("Windows")){
classFileName = classesDir.getAbsolutePath()
.replace(YoungAndroidConstants.YAIL_EXTENSION, ".class");
classFileName = classesDir.getAbsolutePath()
.replace(YoungAndroidConstants.YAIL_EXTENSION, ".class");
}
// Check whether user code exists by seeing if a left parenthesis exists at the beginning of
......@@ -515,11 +555,19 @@ public final class Compiler {
return false;
}
// Construct the class path including component libraries (jars)
String classpath =
getResource(KAWA_RUNTIME) + File.pathSeparator +
getResource(SIMPLE_ANDROID_RUNTIME_JAR) + File.pathSeparator +
getResource(TWITTER_RUNTIME) + File.pathSeparator +
getResource(ANDROID_RUNTIME);
getResource(KAWA_RUNTIME) + File.pathSeparator +
getResource(SIMPLE_ANDROID_RUNTIME_JAR) + File.pathSeparator;
// Add component library names to classpath
for (String library : librariesNeeded) {
classpath += getResource(RUNTIME_FILES_DIR + library) + File.pathSeparator;
}
classpath +=
getResource(ANDROID_RUNTIME);
String yailRuntime = getResource(YAIL_RUNTIME);
List<String> kawaCommandArgs = Lists.newArrayList();
int mx = childProcessRamMb - 200;
......@@ -554,7 +602,7 @@ public final class Compiler {
String kawaOutput = kawaOutputStream.toString();
out.print(kawaOutput);
String kawaCompileTimeMessage = "Kawa compile time: " +
((System.currentTimeMillis() - start) / 1000.0) + " seconds";
((System.currentTimeMillis() - start) / 1000.0) + " seconds";
out.println(kawaCompileTimeMessage);
LOG.info(kawaCompileTimeMessage);
......@@ -656,20 +704,28 @@ public final class Compiler {
private boolean runDx(File classesDir, String dexedClasses) {
int mx = childProcessRamMb - 200;
String[] dxCommandLine = {
System.getProperty("java.home") + "/bin/java",
"-mx" + mx + "M",
"-jar",
getResource(DX_JAR),
"--dex",
"--positions=lines",
"--output=" + dexedClasses,
classesDir.getAbsolutePath(),
getResource(SIMPLE_ANDROID_RUNTIME_JAR),
getResource(KAWA_RUNTIME),
getResource(TWITTER_RUNTIME),
};
long startDx = System.currentTimeMillis();
List<String> commandLineList = new ArrayList<String>();
commandLineList.add(System.getProperty("java.home") + "/bin/java");
commandLineList.add("-mx" + mx + "M");
commandLineList.add("-jar");
commandLineList.add(getResource(DX_JAR));
commandLineList.add("--dex");
commandLineList.add("--positions=lines");
commandLineList.add("--output=" + dexedClasses);
commandLineList.add(classesDir.getAbsolutePath());
commandLineList.add(getResource(SIMPLE_ANDROID_RUNTIME_JAR));
commandLineList.add(getResource(KAWA_RUNTIME));
// Add libraries to command line arguments
for (String library : librariesNeeded) {
commandLineList.add(getResource(RUNTIME_FILES_DIR + library));
}
// Convert command line to an array
String[] dxCommandLine = new String[commandLineList.size()];
commandLineList.toArray(dxCommandLine);
long startDx = System.currentTimeMillis();
// Using System.err and System.out on purpose. Don't want to polute build messages with
// tools output
boolean dxSuccess;
......@@ -689,7 +745,7 @@ public final class Compiler {
return true;
}
private boolean runAaptPackage(File manifestFile, File resDir, String tmpPackageName) {
// Need to make sure assets directory exists otherwise aapt will fail.
createDirectory(project.getAssetsDirectory());
......@@ -799,6 +855,39 @@ public final class Compiler {
}
}
}
/**
* Loads the names of library jars for each component and stores them in
* componentLibraries.
*
* @throws IOException
* @throws JSONException
*/
private static void loadComponentLibraryNames() throws IOException, JSONException {
synchronized (componentLibraries) {
if (componentLibraries.isEmpty()) {
String librariesJson = Resources.toString(
Compiler.class.getResource(COMPONENT_LIBRARIES), Charsets.UTF_8);
JSONArray componentsArray = new JSONArray(librariesJson);
int componentslength = componentsArray.length();
for (int componentsIndex = 0; componentsIndex < componentslength; componentsIndex++) {
JSONObject componentObject = componentsArray.getJSONObject(componentsIndex);
String name = componentObject.getString("name");
Set<String> librariesForThisComponent = Sets.newHashSet();
JSONArray librariesArray = componentObject.getJSONArray("libraries");
int librariesLength = librariesArray.length();
for (int librariesIndex = 0; librariesIndex < librariesLength; librariesIndex++) {
String libraryName = librariesArray.getString(librariesIndex);
librariesForThisComponent.add(libraryName);
}
componentLibraries.put(name, librariesForThisComponent);
}
}
}
}
/**
* Creates a new directory (if it doesn't exist already).
......
......@@ -309,11 +309,12 @@
</target>
<!-- =====================================================================
ComponentList: create simple_components.txt and
simple_components_permissions.json
ComponentList: create simple_components.txt,
simple_components_permissions.json,
and simple_components_libraries.json
===================================================================== -->
<target name="ComponentList"
description="Make simple_components.txt and simple_components_permissions.json."
description="Make simple_components.txt, simple_components_permissions.json, and simple_components_libraries.json."
depends="AnnotationProcessors,AndroidRuntime,HtmlEntities,CommonConstants">
<property name="ComponentList-class.dir" location="${class.dir}/ComponentList" />
<mkdir dir="${ComponentList-class.dir}" />
......@@ -326,6 +327,8 @@
todir="${public.build.dir}"/>
<copy file="${ComponentList-class.dir}/simple_components_permissions.json"
todir="${public.build.dir}"/>
<copy file="${ComponentList-class.dir}/simple_components_libraries.json"
todir="${public.build.dir}"/>
</target>
<!-- =====================================================================
......
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the MIT License https://raw.github.com/mit-cml/app-inventor/master/mitlicense.txt
package com.google.appinventor.components.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to indicate Android permissions required by components.
*
* @author ralph.morelli@trincoll.edu
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface UsesLibraries {
/**
* The names of the libraries separated by commas.
*
* @return the library name
* @see android.Manifest.permission
*/
String libraries() default "";
}
......@@ -13,6 +13,7 @@ import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.annotations.UsesPermissions;
import com.google.appinventor.components.annotations.UsesLibraries;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.common.PropertyTypeConstants;
import com.google.appinventor.components.common.YaVersion;
......@@ -73,6 +74,7 @@ import java.util.List;
iconName = "images/twitter.png")
@SimpleObject
@UsesPermissions(permissionNames = "android.permission.INTERNET")
@UsesLibraries(libraries = "twitter4j.jar")
public final class Twitter extends AndroidNonvisibleComponent
implements ActivityResultListener, Component {
private static final String ACCESS_TOKEN_TAG = "TwitterOauthAccessToken";
......
......@@ -22,6 +22,8 @@ public final class ComponentListGenerator extends ComponentProcessor {
private static final String COMPONENT_LIST_OUTPUT_FILE_NAME = "simple_components.txt";
private static final String COMPONENT_PERMISIONS_OUTPUT_FILE_NAME =
"simple_components_permissions.json";
private static final String COMPONENT_LIBRARIES_OUTPUT_FILE_NAME =
"simple_components_libraries.json";
@Override
protected void outputResults() throws IOException {
......@@ -29,6 +31,9 @@ public final class ComponentListGenerator extends ComponentProcessor {
StringBuilder componentList = new StringBuilder();
StringBuilder componentPermissions = new StringBuilder();
componentPermissions.append("[\n");
StringBuilder componentLibraries = new StringBuilder();
componentLibraries.append("[\n");
// Components are already sorted.
String listSeparator = "";
......@@ -41,10 +46,16 @@ public final class ComponentListGenerator extends ComponentProcessor {
componentPermissions.append(jsonSeparator);
outputComponentPermissions(component, componentPermissions);
componentLibraries.append(jsonSeparator);
outputComponentLibraries(component, componentLibraries);
jsonSeparator = ",\n";
}
componentPermissions.append("\n]");
componentLibraries.append("\n]");
FileObject src = createOutputFileObject(COMPONENT_LIST_OUTPUT_FILE_NAME);
Writer writer = src.openWriter();
......@@ -65,6 +76,17 @@ public final class ComponentListGenerator extends ComponentProcessor {
writer.close();
}
messager.printMessage(Diagnostic.Kind.NOTE, "Wrote file " + src.toUri());
src = createOutputFileObject(COMPONENT_LIBRARIES_OUTPUT_FILE_NAME);
writer = src.openWriter();
try {
writer.write(componentLibraries.toString());
writer.flush();
} finally {
writer.close();
}
messager.printMessage(Diagnostic.Kind.NOTE, "Wrote file " + src.toUri());
}
private static void outputComponentPermissions(ComponentInfo component, StringBuilder sb) {
......@@ -78,4 +100,16 @@ public final class ComponentListGenerator extends ComponentProcessor {
}
sb.append("]}");
}
private static void outputComponentLibraries(ComponentInfo component, StringBuilder sb) {
sb.append("{\"name\": \"");
sb.append(component.name);
sb.append("\", \"libraries\": [");
String separator = "";
for (String library : component.libraries) {
sb.append(separator).append("\"").append(library).append("\"");
separator = ", ";
}
sb.append("]}");
}
}
......@@ -2,6 +2,7 @@
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the MIT License https://raw.github.com/mit-cml/app-inventor/master/mitlicense.txt
package com.google.appinventor.components.scripts;
import com.google.appinventor.components.annotations.DesignerComponent;
......@@ -11,6 +12,7 @@ import com.google.appinventor.components.annotations.SimpleEvent;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.annotations.UsesLibraries;
import com.google.appinventor.components.annotations.UsesPermissions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
......@@ -416,6 +418,11 @@ public abstract class ComponentProcessor extends AbstractProcessor {
*/
protected final Set<String> permissions;
/**
* Libraries required by this component.
*/
protected final Set<String> libraries;
/**
* Properties of this component that are visible in the Designer.
* @see DesignerProperty
......@@ -469,6 +476,7 @@ public abstract class ComponentProcessor extends AbstractProcessor {
"Component");
displayName = getDisplayNameForComponentType(name);
permissions = Sets.newHashSet();
libraries = Sets.newHashSet();
designerProperties = Maps.newTreeMap();
properties = Maps.newTreeMap();
methods = Maps.newTreeMap();
......@@ -739,6 +747,14 @@ public abstract class ComponentProcessor extends AbstractProcessor {
}
}
// Gather library names.
UsesLibraries ulib = element.getAnnotation(UsesLibraries.class);
if (ulib != null) {
for (String library : ulib.libraries().split(",")) {
componentInfo.libraries.add(library.trim());
}
}
// Build up event information.
processEvents(componentInfo, element);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment