aboutsummaryrefslogtreecommitdiffstats
path: root/anttasks/src/com/android/ant/SetupTask.java
blob: 8facf621c2de2645b04fb1d65b0b648dcc5d7cf0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.ant;

import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.ISdkLog;
import com.android.sdklib.SdkConstants;
import com.android.sdklib.SdkManager;
import com.android.sdklib.IAndroidTarget.IOptionalLibrary;
import com.android.sdklib.internal.project.ProjectProperties;
import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
import com.android.sdklib.io.FileWrapper;
import com.android.sdklib.xml.AndroidManifest;
import com.android.sdklib.xml.AndroidXPathFactory;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.ImportTask;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Path.PathElement;
import org.xml.sax.InputSource;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Properties;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;

/**
 * Setup/Import Ant task. This task accomplishes:
 * <ul>
 * <li>Gets the project target hash string from {@link ProjectProperties#PROPERTY_TARGET},
 * and resolves it to get the project's {@link IAndroidTarget}.</li>
 * <li>Sets up properties so that aapt can find the android.jar in the resolved target.</li>
 * <li>Sets up the boot classpath ref so that the <code>javac</code> task knows where to find
 * the libraries. This includes the default android.jar from the resolved target but also optional
 * libraries provided by the target (if any, when the target is an add-on).</li>
 * <li>Imports the build rules located in the resolved target so that the build actually does
 * something. This can be disabled with the attribute <var>import</var> set to <code>false</code>
 * </li></ul>
 *
 * This is used in build.xml/template.
 *
 */
public final class SetupTask extends ImportTask {
    /** current max version of the Ant rules that is supported */
    private final static int ANT_RULES_MAX_VERSION = 2;

    private final static String ANDROID_RULES = "android_rules.xml";
    // additional android rules for test project - depends on android_rules.xml
    private final static String ANDROID_TEST_RULES = "android_test_rules.xml";
    // additional android rules for test project - depends on android_rules.xml
    private final static String ANDROID_LIBRARY_RULES = "android_lib_rules.xml";
    // ant property with the path to the android.jar
    private final static String PROPERTY_ANDROID_JAR = "android.jar";
    // LEGACY - compatibility with 1.6 and before
    private final static String PROPERTY_ANDROID_JAR_LEGACY = "android-jar";
    // ant property with the path to the framework.jar
    private final static String PROPERTY_ANDROID_AIDL = "android.aidl";
    // LEGACY - compatibility with 1.6 and before
    private final static String PROPERTY_ANDROID_AIDL_LEGACY = "android-aidl";
    // ant property with the path to the aapt tool
    private final static String PROPERTY_AAPT = "aapt";
    // ant property with the path to the aidl tool
    private final static String PROPERTY_AIDL = "aidl";
    // ant property with the path to the dx tool
    private final static String PROPERTY_DX = "dx";
    // ref id to the <path> object containing all the boot classpaths.
    private final static String REF_CLASSPATH = "android.target.classpath";

    private boolean mDoImport = true;

    @Override
    public void execute() throws BuildException {
        Project antProject = getProject();

        // get the SDK location
        String sdkLocation = antProject.getProperty(ProjectProperties.PROPERTY_SDK);

        // check if it's valid and exists
        if (sdkLocation == null || sdkLocation.length() == 0) {
            // LEGACY support: project created with 1.6 or before may be using a different
            // property to declare the location of the SDK. At this point, we cannot
            // yet check which target is running so we check both always.
            sdkLocation = antProject.getProperty(ProjectProperties.PROPERTY_SDK_LEGACY);
            if (sdkLocation == null || sdkLocation.length() == 0) {
                throw new BuildException("SDK Location is not set.");
            }
        }

        File sdk = new File(sdkLocation);
        if (sdk.isDirectory() == false) {
            throw new BuildException(String.format("SDK Location '%s' is not valid.", sdkLocation));
        }

        // display SDK Tools revision
        int toolsRevison = getToolsRevision(sdk);
        if (toolsRevison != -1) {
            System.out.println("Android SDK Tools Revision " + toolsRevison);
        }

        // get the target property value
        String targetHashString = antProject.getProperty(ProjectProperties.PROPERTY_TARGET);

        boolean isTestProject = false;

        if (antProject.getProperty("tested.project.dir") != null) {
            isTestProject = true;
        }

        if (targetHashString == null) {
            throw new BuildException("Android Target is not set.");
        }

        // load up the sdk targets.
        final ArrayList<String> messages = new ArrayList<String>();
        SdkManager manager = SdkManager.createManager(sdkLocation, new ISdkLog() {
            public void error(Throwable t, String errorFormat, Object... args) {
                if (errorFormat != null) {
                    messages.add(String.format("Error: " + errorFormat, args));
                }
                if (t != null) {
                    messages.add("Error: " + t.getMessage());
                }
            }

            public void printf(String msgFormat, Object... args) {
                messages.add(String.format(msgFormat, args));
            }

            public void warning(String warningFormat, Object... args) {
                messages.add(String.format("Warning: " + warningFormat, args));
            }
        });

        if (manager == null) {
            // since we failed to parse the SDK, lets display the parsing output.
            for (String msg : messages) {
                System.out.println(msg);
            }
            throw new BuildException("Failed to parse SDK content.");
        }

        // resolve it
        IAndroidTarget androidTarget = manager.getTargetFromHashString(targetHashString);

        if (androidTarget == null) {
            throw new BuildException(String.format(
                    "Unable to resolve target '%s'", targetHashString));
        }

        // check that this version of the custom Ant task can build this target
        int antBuildVersion = androidTarget.getAntBuildRevision();
        if (antBuildVersion > ANT_RULES_MAX_VERSION) {
            throw new BuildException(String.format(
                    "The project target (%1$s) requires a more recent version of the tools. Please update.",
                    androidTarget.getName()));
        }

        // check if the project is a library
        boolean isLibrary = false;

        String libraryProp = antProject.getProperty(ProjectProperties.PROPERTY_LIBRARY);
        if (libraryProp != null) {
            isLibrary = Boolean.valueOf(libraryProp).booleanValue();
        }

        // look for referenced libraries.
        processReferencedLibraries(antProject, androidTarget);

        // display the project info
        System.out.println("Project Target: " + androidTarget.getName());
        if (isLibrary) {
            System.out.println("Type: Android Library");
        }
        if (androidTarget.isPlatform() == false) {
            System.out.println("Vendor: " + androidTarget.getVendor());
            System.out.println("Platform Version: " + androidTarget.getVersionName());
        }
        System.out.println("API level: " + androidTarget.getVersion().getApiString());

        // always check the manifest minSdkVersion.
        checkManifest(antProject, androidTarget.getVersion());

        // sets up the properties to find android.jar/framework.aidl/target tools
        String androidJar = androidTarget.getPath(IAndroidTarget.ANDROID_JAR);
        antProject.setProperty(PROPERTY_ANDROID_JAR, androidJar);

        String androidAidl = androidTarget.getPath(IAndroidTarget.ANDROID_AIDL);
        antProject.setProperty(PROPERTY_ANDROID_AIDL, androidAidl);

        antProject.setProperty(PROPERTY_AAPT, androidTarget.getPath(IAndroidTarget.AAPT));
        antProject.setProperty(PROPERTY_AIDL, androidTarget.getPath(IAndroidTarget.AIDL));
        antProject.setProperty(PROPERTY_DX, androidTarget.getPath(IAndroidTarget.DX));

        // sets up the boot classpath

        // create the Path object
        Path bootclasspath = new Path(antProject);

        // create a PathElement for the framework jar
        PathElement element = bootclasspath.createPathElement();
        element.setPath(androidJar);

        // create PathElement for each optional library.
        IOptionalLibrary[] libraries = androidTarget.getOptionalLibraries();
        if (libraries != null) {
            HashSet<String> visitedJars = new HashSet<String>();
            for (IOptionalLibrary library : libraries) {
                String jarPath = library.getJarPath();
                if (visitedJars.contains(jarPath) == false) {
                    visitedJars.add(jarPath);

                    element = bootclasspath.createPathElement();
                    element.setPath(library.getJarPath());
                }
            }
        }

        // finally sets the path in the project with a reference
        antProject.addReference(REF_CLASSPATH, bootclasspath);

        // find the file to import, and import it.
        String templateFolder = androidTarget.getPath(IAndroidTarget.TEMPLATES);

        // LEGACY support. android_rules.xml in older platforms expects properties with
        // older names. This sets those properties to make sure the rules will work.
        if (androidTarget.getVersion().getApiLevel() <= 4) { // 1.6 and earlier
            antProject.setProperty(PROPERTY_ANDROID_JAR_LEGACY, androidJar);
            antProject.setProperty(PROPERTY_ANDROID_AIDL_LEGACY, androidAidl);
            antProject.setProperty(ProjectProperties.PROPERTY_SDK_LEGACY, sdkLocation);
            String appPackage = antProject.getProperty(ProjectProperties.PROPERTY_APP_PACKAGE);
            if (appPackage != null && appPackage.length() > 0) {
                antProject.setProperty(ProjectProperties.PROPERTY_APP_PACKAGE_LEGACY, appPackage);
            }
        }

        // Now the import section. This is only executed if the task actually has to import a file.
        if (mDoImport) {
            // make sure the file exists.
            File templates = new File(templateFolder);

            if (templates.isDirectory() == false) {
                throw new BuildException(String.format("Template directory '%s' is missing.",
                        templateFolder));
            }

            String importedRulesFileName = isLibrary ? ANDROID_LIBRARY_RULES :
                    isTestProject ? ANDROID_TEST_RULES : ANDROID_RULES;

            // now check the rules file exists.
            File rules = new File(templateFolder, importedRulesFileName);

            if (rules.isFile() == false) {
                throw new BuildException(String.format("Build rules file '%s' is missing.",
                        templateFolder));
            }

            // set the file location to import
            setFile(rules.getAbsolutePath());

            // and import
            super.execute();
        }
    }

    /**
     * Sets the value of the "import" attribute.
     * @param value the value.
     */
    public void setImport(boolean value) {
        mDoImport = value;
    }

    /**
     * Checks the manifest <code>minSdkVersion</code> attribute.
     * @param antProject the ant project
     * @param androidVersion the version of the platform the project is compiling against.
     */
    private void checkManifest(Project antProject, AndroidVersion androidVersion) {
        try {
            File manifest = new File(antProject.getBaseDir(), SdkConstants.FN_ANDROID_MANIFEST_XML);

            XPath xPath = AndroidXPathFactory.newXPath();

            // check the package name.
            String value = xPath.evaluate(
                    "/"  + AndroidManifest.NODE_MANIFEST +
                    "/@" + AndroidManifest.ATTRIBUTE_PACKAGE,
                    new InputSource(new FileInputStream(manifest)));
            if (value != null) { // aapt will complain if it's missing.
                // only need to check that the package has 2 segments
                if (value.indexOf('.') == -1) {
                    throw new BuildException(String.format(
                            "Application package '%1$s' must have a minimum of 2 segments.",
                            value));
                }
            }

            // check the minSdkVersion value
            value = xPath.evaluate(
                    "/"  + AndroidManifest.NODE_MANIFEST +
                    "/"  + AndroidManifest.NODE_USES_SDK +
                    "/@" + AndroidXPathFactory.DEFAULT_NS_PREFIX + ":" +
                            AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION,
                    new InputSource(new FileInputStream(manifest)));

            if (androidVersion.isPreview()) {
                // in preview mode, the content of the minSdkVersion must match exactly the
                // platform codename.
                String codeName = androidVersion.getCodename();
                if (codeName.equals(value) == false) {
                    throw new BuildException(String.format(
                            "For '%1$s' SDK Preview, attribute minSdkVersion in AndroidManifest.xml must be '%1$s'",
                            codeName));
                }
            } else if (value.length() > 0) {
                // for normal platform, we'll only display warnings if the value is lower or higher
                // than the target api level.
                // First convert to an int.
                int minSdkValue = -1;
                try {
                    minSdkValue = Integer.parseInt(value);
                } catch (NumberFormatException e) {
                    // looks like it's not a number: error!
                    throw new BuildException(String.format(
                            "Attribute %1$s in AndroidManifest.xml must be an Integer!",
                            AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION));
                }

                int projectApiLevel = androidVersion.getApiLevel();
                if (minSdkValue < projectApiLevel) {
                    System.out.println(String.format(
                            "WARNING: Attribute %1$s in AndroidManifest.xml (%2$d) is lower than the project target API level (%3$d)",
                            AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION,
                            minSdkValue, projectApiLevel));
                } else if (minSdkValue > androidVersion.getApiLevel()) {
                    System.out.println(String.format(
                            "WARNING: Attribute %1$s in AndroidManifest.xml (%2$d) is higher than the project target API level (%3$d)",
                            AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION,
                            minSdkValue, projectApiLevel));
                }
            } else {
                // no minSdkVersion? display a warning
                System.out.println(
                        "WARNING: No minSdkVersion value set. Application will install on all Android versions.");
            }

        } catch (XPathExpressionException e) {
            throw new BuildException(e);
        } catch (FileNotFoundException e) {
            throw new BuildException(e);
        }
    }

    private void processReferencedLibraries(Project antProject, IAndroidTarget androidTarget) {
        // prepare several paths for future tasks
        Path sourcePath = new Path(antProject);
        Path resPath = new Path(antProject);
        Path libsPath = new Path(antProject);
        Path jarsPath = new Path(antProject);
        StringBuilder sb = new StringBuilder();

        FilenameFilter filter = new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.toLowerCase().endsWith(".jar");
            }
        };

        // get the build version for the current target. It'll be tested if there's at least
        // one library.
        int antBuildVersion = androidTarget.getAntBuildRevision();

        int index = 1;
        while (true) {
            String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index++);
            String rootPath = antProject.getProperty(propName);

            if (rootPath == null) {
                break;
            }

            if (antBuildVersion < SdkConstants.ANT_REV_LIBRARY) {
                throw new BuildException(String.format(
                        "The build system for this project target (%1$s) does not support libraries",
                        androidTarget.getFullName()));
            }

            // get the source path. default is src but can be overriden by the property
            // "source.dir" in build.properties.
            PathElement element = sourcePath.createPathElement();
            ProjectProperties prop = ProjectProperties.load(rootPath, PropertyType.BUILD);
            String sourceDir = SdkConstants.FD_SOURCES;
            if (prop != null) {
                String value = prop.getProperty(ProjectProperties.PROPERTY_BUILD_SOURCE_DIR);
                if (value != null) {
                    sourceDir = value;
                }
            }

            element.setPath(rootPath + "/" + sourceDir);

            // get the res path. Always $PROJECT/res
            element = resPath.createPathElement();
            element.setPath(rootPath + "/" + SdkConstants.FD_RESOURCES);

            // get the libs path. Always $PROJECT/libs
            element = libsPath.createPathElement();
            element.setPath(rootPath + "/" + SdkConstants.FD_NATIVE_LIBS);

            // get the jars from it too
            File libsFolder = new File(rootPath, SdkConstants.FD_NATIVE_LIBS);
            File[] jarFiles = libsFolder.listFiles(filter);
            for (File jarFile : jarFiles) {
                element = jarsPath.createPathElement();
                element.setPath(jarFile.getAbsolutePath());
            }

            // get the package from the manifest.
            FileWrapper manifest = new FileWrapper(rootPath, SdkConstants.FN_ANDROID_MANIFEST_XML);
            try {
                String value = AndroidManifest.getPackage(manifest);
                if (value != null) { // aapt will complain if it's missing.
                    sb.append(';');
                    sb.append(value);
                }
            } catch (Exception e) {
                throw new BuildException(e);
            }
        }

        // even with no libraries, always setup these so that various tasks in Ant don't complain
        // (the task themselves can handle a ref to an empty Path)
        antProject.addReference("android.libraries.src", sourcePath);
        antProject.addReference("android.libraries.jars", jarsPath);
        antProject.addReference("android.libraries.libs", libsPath);

        // the rest is done only if there's a library.
        if (sourcePath.list().length > 0) {
            antProject.addReference("android.libraries.res", resPath);
            antProject.setProperty("android.libraries.package", sb.toString());
        }
    }

    /**
     * Returns the revision of the tools for a given SDK.
     * @param sdkFile the {@link File} for the root folder of the SDK
     * @return the tools revision or -1 if not found.
     */
    private int getToolsRevision(File sdkFile) {
        Properties p = new Properties();
        try{
            // tools folder must exist, or this custom task wouldn't run!
            File toolsFolder= new File(sdkFile, SdkConstants.FD_TOOLS);
            File sourceProp = new File(toolsFolder, SdkConstants.FN_SOURCE_PROP);
            p.load(new FileInputStream(sourceProp));
            String value = p.getProperty("Pkg.Revision"); //$NON-NLS-1$
            if (value != null) {
                return Integer.parseInt(value);
            }
        } catch (FileNotFoundException e) {
            // couldn't find the file? return -1 below.
        } catch (IOException e) {
            // couldn't find the file? return -1 below.
        }

        return -1;
    }
}