Merge branch 'gradle-build-3.x'

This introduces the gradle build system to Cordova-android, behind a flag currently
This commit is contained in:
Ian Clelland 2014-08-18 09:58:01 -04:00
commit 404ce8bc3e
13 changed files with 522 additions and 58 deletions

View File

@ -24,6 +24,7 @@ var shelljs = require('shelljs'),
Q = require('q'),
path = require('path'),
fs = require('fs'),
which = require('which'),
ROOT = path.join(__dirname, '..', '..');
var isWindows = process.platform == 'win32';
@ -55,6 +56,23 @@ module.exports.get_target = function() {
}
}
// Returns a promise.
module.exports.sdk_dir = function() {
var d = Q.defer();
which('android', function(err, path) {
if (err) {
d.reject(new Error('ERROR: Cannot find Android SDK. android command not found.'));
} else {
var toolsDir = path.substring(0, path.lastIndexOf('/'));
if (toolsDir.substring(toolsDir.length-6) != "/tools") {
d.reject(new Error('ERROR: Cannot find Android SDK. android command not found in tools dir.'));
}
d.resolve(toolsDir.substring(0, toolsDir.length-6));
}
});
return d.promise;
};
// Returns a promise. Called only by build and clean commands.
module.exports.check_ant = function() {
return tryCommand('ant -version', 'Failed to run "ant -version", make sure you have ant installed and added to your PATH.');

View File

@ -71,6 +71,7 @@ function copyJsAndLibrary(projectPath, shared, projectName) {
shell.mkdir('-p', nestedCordovaLibPath);
shell.cp('-f', path.join(ROOT, 'framework', 'AndroidManifest.xml'), nestedCordovaLibPath);
shell.cp('-f', path.join(ROOT, 'framework', 'project.properties'), nestedCordovaLibPath);
shell.cp('-f', path.join(ROOT, 'framework', 'build.gradle'), nestedCordovaLibPath);
shell.cp('-r', path.join(ROOT, 'framework', 'src'), nestedCordovaLibPath);
// Create an eclipse project file and set the name of it to something unique.
// Without this, you can't import multiple CordovaLib projects into the same workspace.
@ -89,7 +90,9 @@ function runAndroidUpdate(projectPath, target_api, shared) {
function copyAntRules(projectPath) {
var srcDir = path.join(ROOT, 'bin', 'templates', 'project');
shell.cp('-f', path.join(srcDir, 'custom_rules.xml'), projectPath);
if (fs.existsSync(path.join(srcDir, 'custom_rules.xml'))) {
shell.cp('-f', path.join(srcDir, 'custom_rules.xml'), projectPath);
}
}
function copyScripts(projectPath) {
@ -106,6 +109,13 @@ function copyScripts(projectPath) {
shell.cp(path.join(ROOT, 'bin', 'lib', 'android_sdk_version.js'), path.join(projectPath, 'cordova', 'lib', 'android_sdk_version.js'));
}
function copyGradleWrapper(sdkPath, projectPath) {
var wrapperDir = path.join(sdkPath, 'tools', 'templates','gradle','wrapper');
shell.cp(path.join(wrapperDir, 'gradlew'), projectPath);
shell.cp(path.join(wrapperDir, 'gradlew.bat'), projectPath);
shell.cp('-r', path.join(wrapperDir, 'gradle'), projectPath);
}
/**
* Test whether a package name is acceptable for use as an android project.
* Returns a promise, fulfilled if the package name is acceptable; rejected
@ -213,6 +223,16 @@ exports.createProject = function(project_path, package_name, project_name, proje
shell.cp('-r', path.join(project_template_dir, 'res'), project_path);
shell.cp('-r', path.join(ROOT, 'framework', 'res', 'xml'), path.join(project_path, 'res'));
shell.cp('-f', path.join(project_template_dir, 'build.gradle'), project_path);
shell.cp('-f', path.join(project_template_dir, 'libraries.gradle'), project_path);
shell.cp('-f', path.join(project_template_dir, 'settings.gradle'), project_path);
check_reqs.sdk_dir().then(function(dir) {
console.log("Copying Gradle wrapper from " + dir);
copyGradleWrapper(dir, project_path);
}).catch(function(err) {
console.log("Cannot find Android SDK. Not installing Gradle wrapper.");
});
// Manually create directories that would be empty within the template (since git doesn't track directories).
shell.mkdir(path.join(project_path, 'libs'));

23
bin/node_modules/which/LICENSE generated vendored Normal file
View File

@ -0,0 +1,23 @@
Copyright 2009, 2010, 2011 Isaac Z. Schlueter.
All rights reserved.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

5
bin/node_modules/which/README.md generated vendored Normal file
View File

@ -0,0 +1,5 @@
The "which" util from npm's guts.
Finds the first instance of a specified executable in the PATH
environment variable. Does not cache the results, so `hash -r` is not
needed when the PATH changes.

14
bin/node_modules/which/bin/which generated vendored Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env node
var which = require("../")
if (process.argv.length < 3) {
console.error("Usage: which <thing>")
process.exit(1)
}
which(process.argv[2], function (er, thing) {
if (er) {
console.error(er.message)
process.exit(er.errno || 127)
}
console.log(thing)
})

31
bin/node_modules/which/package.json generated vendored Normal file
View File

@ -0,0 +1,31 @@
{
"author": {
"name": "Isaac Z. Schlueter",
"email": "i@izs.me",
"url": "http://blog.izs.me"
},
"name": "which",
"description": "Like which(1) unix command. Find the first instance of an executable in the PATH.",
"version": "1.0.5",
"repository": {
"type": "git",
"url": "git://github.com/isaacs/node-which.git"
},
"main": "which.js",
"bin": {
"which": "./bin/which"
},
"engines": {
"node": "*"
},
"dependencies": {},
"devDependencies": {},
"readme": "The \"which\" util from npm's guts.\n\nFinds the first instance of a specified executable in the PATH\nenvironment variable. Does not cache the results, so `hash -r` is not\nneeded when the PATH changes.\n",
"readmeFilename": "README.md",
"bugs": {
"url": "https://github.com/isaacs/node-which/issues"
},
"homepage": "https://github.com/isaacs/node-which",
"_id": "which@1.0.5",
"_from": "which@"
}

104
bin/node_modules/which/which.js generated vendored Normal file
View File

@ -0,0 +1,104 @@
module.exports = which
which.sync = whichSync
var path = require("path")
, fs
, COLON = process.platform === "win32" ? ";" : ":"
, isExe
try {
fs = require("graceful-fs")
} catch (ex) {
fs = require("fs")
}
if (process.platform == "win32") {
// On windows, there is no good way to check that a file is executable
isExe = function isExe () { return true }
} else {
isExe = function isExe (mod, uid, gid) {
//console.error(mod, uid, gid);
//console.error("isExe?", (mod & 0111).toString(8))
var ret = (mod & 0001)
|| (mod & 0010) && process.getgid && gid === process.getgid()
|| (mod & 0100) && process.getuid && uid === process.getuid()
//console.error("isExe?", ret)
return ret
}
}
function which (cmd, cb) {
if (isAbsolute(cmd)) return cb(null, cmd)
var pathEnv = (process.env.PATH || "").split(COLON)
, pathExt = [""]
if (process.platform === "win32") {
pathEnv.push(process.cwd())
pathExt = (process.env.PATHEXT || ".EXE").split(COLON)
if (cmd.indexOf(".") !== -1) pathExt.unshift("")
}
//console.error("pathEnv", pathEnv)
;(function F (i, l) {
if (i === l) return cb(new Error("not found: "+cmd))
var p = path.resolve(pathEnv[i], cmd)
;(function E (ii, ll) {
if (ii === ll) return F(i + 1, l)
var ext = pathExt[ii]
//console.error(p + ext)
fs.stat(p + ext, function (er, stat) {
if (!er &&
stat &&
stat.isFile() &&
isExe(stat.mode, stat.uid, stat.gid)) {
//console.error("yes, exe!", p + ext)
return cb(null, p + ext)
}
return E(ii + 1, ll)
})
})(0, pathExt.length)
})(0, pathEnv.length)
}
function whichSync (cmd) {
if (isAbsolute(cmd)) return cmd
var pathEnv = (process.env.PATH || "").split(COLON)
, pathExt = [""]
if (process.platform === "win32") {
pathEnv.push(process.cwd())
pathExt = (process.env.PATHEXT || ".EXE").split(COLON)
if (cmd.indexOf(".") !== -1) pathExt.unshift("")
}
for (var i = 0, l = pathEnv.length; i < l; i ++) {
var p = path.join(pathEnv[i], cmd)
for (var j = 0, ll = pathExt.length; j < ll; j ++) {
var cur = p + pathExt[j]
var stat
try { stat = fs.statSync(cur) } catch (ex) {}
if (stat &&
stat.isFile() &&
isExe(stat.mode, stat.uid, stat.gid)) return cur
}
}
throw new Error("not found: "+cmd)
}
var isAbsolute = process.platform === "win32" ? absWin : absUnix
function absWin (p) {
if (absUnix(p)) return true
// pull off the device/UNC bit from a windows path.
// from node's lib/path.js
var splitDeviceRe =
/^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/][^\\\/]+)?([\\\/])?/
, result = splitDeviceRe.exec(p)
, device = result[1] || ''
, isUnc = device && device.charAt(1) !== ':'
, isAbsolute = !!result[2] || isUnc // UNC paths are always absolute
return isAbsolute
}
function absUnix (p) {
return p.charAt(0) === "/" || p === ""
}

View File

@ -24,12 +24,16 @@ var build = require('./lib/build'),
args = process.argv;
// Support basic help commands
if(args[2] == '--help' || args[2] == '/?' || args[2] == '-h' ||
args[2] == 'help' || args[2] == '-help' || args[2] == '/help') {
if(args[2] == '--help' ||
args[2] == '/?' ||
args[2] == '-h' ||
args[2] == 'help' ||
args[2] == '-help' ||
args[2] == '/help') {
build.help();
} else {
reqs.run().done(function() {
return build.run(args[2]);
return build.run.call(build.run, args.slice(2));
}, function(err) {
console.error(err);
process.exit(2);

View File

@ -27,39 +27,164 @@ var shell = require('shelljs'),
ROOT = path.join(__dirname, '..', '..');
var check_reqs = require('./check_reqs');
// Globals
var build_type,
build_method;
function find_files(directory, predicate) {
if (fs.existsSync(directory)) {
var candidates = fs.readdirSync(directory).filter(predicate).map(function(p) {
p = path.join(directory, p);
return { p: p, t: fs.statSync(p).mtime };
}).sort(function(a,b) {
return a.t > b.t ? -1 :
a.t < b.t ? 1 : 0;
}).map(function(p) { return p.p; });
return candidates;
} else {
console.error('ERROR : unable to find project ' + directory + ' directory, could not locate .apk');
process.exit(2);
}
}
function hasCustomRules() {
return fs.existsSync(path.join(ROOT, 'custom_rules.xml'));
}
module.exports.getAntArgs = function(cmd) {
var args = [cmd, '-f', path.join(ROOT, 'build.xml')];
// custom_rules.xml is required for incremental builds.
if (hasCustomRules()) {
args.push('-Dout.dir=ant-build', '-Dgen.absolute.dir=ant-gen');
module.exports.builders = {
ant: {
getArgs: function(cmd) {
var args = [cmd, '-f', path.join(ROOT, 'build.xml')];
// custom_rules.xml is required for incremental builds.
if (hasCustomRules()) {
args.push('-Dout.dir=ant-build', '-Dgen.absolute.dir=ant-gen');
}
try {
// Specify sdk dir in case local properties are missing
args.push('-Dsdk.dir='+path.join(which.sync('android'), '../..'));
} catch(e) {
// Can't find android; don't push arg: assume all is okay
}
return args;
},
/*
* Builds the project with ant.
* Returns a promise.
*/
build: function(build_type) {
var builder = this;
var args = builder.getArgs(build_type == "debug" ? 'debug' : 'release');
return Q().then(function() {
return spawn('ant', args);
}).then(function() {
return builder.getOutputFiles();
});
},
// Find the recently-generated output APK files
// Ant only generates one output file; return it.
getOutputFiles: function() {
var binDir;
if(hasCustomRules()) {
binDir = path.join(ROOT, 'ant-build');
} else {
binDir = path.join(ROOT, 'bin');
}
var candidates = find_files(binDir, function(candidate) { return path.extname(candidate) == '.apk'; });
if (candidates.length === 0) {
console.error('ERROR : No .apk found in ' + binDir + ' directory');
process.exit(2);
}
console.log('Using apk: ' + candidates[0]);
return [candidates[0]];
}
},
gradle: {
getArgs: function(cmd) {
var args = [cmd, '-b', path.join(ROOT, 'build.gradle')];
return args;
},
/*
* Builds the project with gradle.
* Returns a promise.
*/
build: function(build_type) {
var builder = this;
var wrapper = path.join(ROOT, 'gradlew');
var args = builder.getArgs('build');
return Q().then(function() {
return spawn(wrapper, args);
}).then(function() {
return builder.getOutputFiles(build_type);
});
},
// Find the recently-generated output APK files
// Gradle can generate multiple output files; return all of them.
getOutputFiles: function(build_type) {
var binDir = path.join(ROOT, 'build', 'apk');
var candidates = find_files(binDir, function(candidate) {
// Need to choose between release and debug .apk.
if (build_type === 'debug') {
return (path.extname(candidate) == '.apk' && candidate.indexOf('-debug-') >= 0);
}
if (build_type === 'release') {
return (path.extname(candidate) == '.apk' && candidate.indexOf('-release-') >= 0);
}
return path.extname(candidate) == '.apk';
});
return candidates;
}
}
return args;
};
/*
* Builds the project with ant.
* Builds the project with the specifed options
* Returns a promise.
*/
module.exports.run = function(build_type) {
//default build type
build_type = typeof build_type !== 'undefined' ? build_type : "--debug";
var args = module.exports.getAntArgs('debug');
switch(build_type) {
case '--debug' :
break;
case '--release' :
args[0] = 'release';
break;
case '--nobuild' :
console.log('Skipping build...');
return Q();
default :
return Q.reject('Build option \'' + build_type + '\' not recognized.');
module.exports.run = function(options) {
// Backwards-compatibility: Allow a single string argument
if (typeof options == "string") options = [options];
// Iterate through command line options
for (var i=0; options && (i < options.length); ++i) {
if (options[i].substring && options[i].substring(0,2) == "--") {
var option = options[i].substring(2);
switch(option) {
case 'debug':
case 'release':
if (build_type) {
return Q.reject('Multiple build types (' + build_type + ' and ' + option + ') specified.');
}
build_type = option;
break;
case 'ant':
case 'gradle':
if (build_method) {
return Q.reject('Multiple build methods (' + build_method + ' and ' + option + ') specified.');
}
build_method = option;
break;
case 'nobuild' :
console.log('Skipping build...');
return Q();
default :
return Q.reject('Build option \'' + options[i] + '\' not recognized.');
}
} else {
return Q.reject('Build option \'' + options[i] + '\' not recognized.');
}
}
// Defaults
build_type = build_type || "debug";
build_method = build_method || process.env.ANDROID_BUILD || "ant";
// Get the builder
var builder = module.exports.builders[build_method];
// Without our custom_rules.xml, we need to clean before building.
var ret;
if (!hasCustomRules()) {
@ -68,50 +193,48 @@ module.exports.run = function(build_type) {
} else {
ret = check_reqs.check_ant();
}
// Return a promise for the actual build
return ret.then(function() {
return spawn('ant', args);
return builder.build.call(builder, build_type);
}).then(function(apkFiles) {
var outputDir = path.join(ROOT, 'out');
try {
fs.mkdirSync(outputDir);
} catch (e) {
if (e.code != "EEXIST") {
throw e;
}
}
for (var i=0; i < apkFiles.length; ++i) {
shell.cp('-f', apkFiles[i], path.join(outputDir, path.basename(apkFiles[i])));
}
});
}
};
/*
* Gets the path to the apk file, if not such file exists then
* the script will error out. (should we error or just return undefined?)
* This is called by the run script to install the apk to the device
*/
module.exports.get_apk = function() {
var binDir = '';
if(!hasCustomRules()) {
binDir = path.join(ROOT, 'bin');
} else {
binDir = path.join(ROOT, 'ant-build');
}
if (fs.existsSync(binDir)) {
var candidates = fs.readdirSync(binDir).filter(function(p) {
// Need to choose between release and debug .apk.
return path.extname(p) == '.apk';
}).map(function(p) {
p = path.join(binDir, p);
return { p: p, t: fs.statSync(p).mtime };
}).sort(function(a,b) {
return a.t > b.t ? -1 :
a.t < b.t ? 1 : 0;
});
if (candidates.length === 0) {
console.error('ERROR : No .apk found in ' + binDir + ' directory');
process.exit(2);
}
console.log('Using apk: ' + candidates[0].p);
return candidates[0].p;
} else {
console.error('ERROR : unable to find project ' + binDir + ' directory, could not locate .apk');
module.exports.get_apk = function(build_type) {
var outputDir = path.join(ROOT, 'out');
var candidates = find_files(outputDir, function() { return true; });
if (candidates.length === 0) {
console.error('ERROR : No .apk found in ' + outputDir + ' directory');
process.exit(2);
}
}
console.log('Using apk: ' + candidates[0]);
return candidates[0];
};
module.exports.help = function() {
console.log('Usage: ' + path.relative(process.cwd(), path.join(ROOT, 'cordova', 'build')) + ' [build_type]');
console.log('Build Types : ');
console.log(' \'--debug\': Default build, will build project in using ant debug');
console.log(' \'--release\': will build project using ant release');
console.log(' \'--debug\': Default build, will build project in debug mode');
console.log(' \'--release\': will build project for release');
console.log(' \'--ant\': Default build, will build project with ant');
console.log(' \'--gradle\': will build project with gradle');
console.log(' \'--nobuild\': will skip build process (can be used with run command)');
process.exit(0);
}
};

View File

@ -0,0 +1,81 @@
import java.util.regex.Pattern
apply plugin: 'android'
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.10.+'
}
}
ext.multiarch=false
dependencies {
compile project(':CordovaLib')
}
apply from: 'libraries.gradle'
android {
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
resources.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
}
}
defaultConfig {
versionCode Integer.parseInt("" + getVersionCodeFromManifest() + "0")
}
compileSdkVersion 19
buildToolsVersion "19.0.0"
if (multiarch) {
productFlavors {
armv7 {
versionCode defaultConfig.versionCode + 2
ndk {
abiFilters "armeabi-v7a", ""
}
}
x86 {
versionCode defaultConfig.versionCode + 4
ndk {
abiFilters "x86", ""
}
}
all {
ndk {
abiFilters "all", ""
}
}
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
}
task wrapper(type: Wrapper) {
gradleVersion = '1.12'
}
def getVersionCodeFromManifest() {
def manifestFile = file(android.sourceSets.main.manifest.srcFile)
def pattern = Pattern.compile("versionCode=\"(\\d+)\"")
def matcher = pattern.matcher(manifestFile.getText())
matcher.find()
return Integer.parseInt(matcher.group(1))
}

View File

@ -0,0 +1,7 @@
dependencies {
// This file contains no plugins by default.
// To add a third-party library project, add a line to this file like
// compile project(':library_dir_name')
}
// If multiple ndk architectures are provided, uncomment this line
//ext.multiarch=true

View File

@ -0,0 +1 @@
include ':CordovaLib', ':'

33
framework/build.gradle Normal file
View File

@ -0,0 +1,33 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.10.+'
}
}
apply plugin: 'android-library'
android {
compileSdkVersion 19
buildToolsVersion "19.0.0"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
resources.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
}
}
}