Refactor ProjectBuilder to use class instead of prototype

This commit is contained in:
Gearoid M 2018-06-29 09:16:57 +09:00
parent 8ee3a73dd1
commit 350d35fb24

View File

@ -34,273 +34,274 @@ const TEMPLATE =
'# This file is automatically generated.\n' + '# This file is automatically generated.\n' +
'# Do not modify this file -- ' + MARKER + '\n'; '# Do not modify this file -- ' + MARKER + '\n';
function ProjectBuilder (projectRoot) { class ProjectBuilder {
this.root = projectRoot || path.resolve(__dirname, '../../..'); constructor (projectRoot) {
this.binDirs = { this.root = projectRoot || path.resolve(__dirname, '../../..');
studio: path.join(this.root, 'app', 'build', 'outputs', 'apk'), this.binDirs = {
gradle: path.join(this.root, 'app', 'build', 'outputs', 'apk') studio: path.join(this.root, 'app', 'build', 'outputs', 'apk'),
}; gradle: path.join(this.root, 'app', 'build', 'outputs', 'apk')
} };
ProjectBuilder.prototype.getArgs = function (cmd, opts) {
if (cmd === 'release') {
cmd = 'cdvBuildRelease';
} else if (cmd === 'debug') {
cmd = 'cdvBuildDebug';
}
var args = [cmd, '-b', path.join(this.root, 'build.gradle')];
if (opts.arch) {
args.push('-PcdvBuildArch=' + opts.arch);
} }
// 10 seconds -> 6 seconds getArgs (cmd, opts) {
args.push('-Dorg.gradle.daemon=true'); if (cmd === 'release') {
// to allow dex in process cmd = 'cdvBuildRelease';
args.push('-Dorg.gradle.jvmargs=-Xmx2048m'); } else if (cmd === 'debug') {
// allow NDK to be used - required by Gradle 1.5 plugin cmd = 'cdvBuildDebug';
// args.push('-Pandroid.useDeprecatedNdk=true');
args.push.apply(args, opts.extraArgs);
// Shaves another 100ms, but produces a "try at own risk" warning. Not worth it (yet):
// args.push('-Dorg.gradle.parallel=true');
return args;
};
/*
* This returns a promise
*/
ProjectBuilder.prototype.runGradleWrapper = function (gradle_cmd) {
var gradlePath = path.join(this.root, 'gradlew');
var wrapperGradle = path.join(this.root, 'wrapper.gradle');
if (fs.existsSync(gradlePath)) {
// Literally do nothing, for some reason this works, while !fs.existsSync didn't on Windows
} else {
return spawn(gradle_cmd, ['-p', this.root, 'wrapper', '-b', wrapperGradle], { stdio: 'inherit' });
}
};
ProjectBuilder.prototype.readProjectProperties = function () {
function findAllUniq (data, r) {
var s = {};
var m;
while ((m = r.exec(data))) {
s[m[1]] = 1;
} }
return Object.keys(s); var args = [cmd, '-b', path.join(this.root, 'build.gradle')];
if (opts.arch) {
args.push('-PcdvBuildArch=' + opts.arch);
}
// 10 seconds -> 6 seconds
args.push('-Dorg.gradle.daemon=true');
// to allow dex in process
args.push('-Dorg.gradle.jvmargs=-Xmx2048m');
// allow NDK to be used - required by Gradle 1.5 plugin
// args.push('-Pandroid.useDeprecatedNdk=true');
args.push.apply(args, opts.extraArgs);
// Shaves another 100ms, but produces a "try at own risk" warning. Not worth it (yet):
// args.push('-Dorg.gradle.parallel=true');
return args;
} }
var data = fs.readFileSync(path.join(this.root, 'project.properties'), 'utf8'); /*
return { * This returns a promise
libs: findAllUniq(data, /^\s*android\.library\.reference\.\d+=(.*)(?:\s|$)/mg), */
gradleIncludes: findAllUniq(data, /^\s*cordova\.gradle\.include\.\d+=(.*)(?:\s|$)/mg), runGradleWrapper (gradle_cmd) {
systemLibs: findAllUniq(data, /^\s*cordova\.system\.library\.\d+=(.*)(?:\s|$)/mg) var gradlePath = path.join(this.root, 'gradlew');
}; var wrapperGradle = path.join(this.root, 'wrapper.gradle');
}; if (fs.existsSync(gradlePath)) {
// Literally do nothing, for some reason this works, while !fs.existsSync didn't on Windows
ProjectBuilder.prototype.extractRealProjectNameFromManifest = function () {
var manifestPath = path.join(this.root, 'app', 'src', 'main', 'AndroidManifest.xml');
var manifestData = fs.readFileSync(manifestPath, 'utf8');
var m = /<manifest[\s\S]*?package\s*=\s*"(.*?)"/i.exec(manifestData);
if (!m) {
throw new CordovaError('Could not find package name in ' + manifestPath);
}
var packageName = m[1];
var lastDotIndex = packageName.lastIndexOf('.');
return packageName.substring(lastDotIndex + 1);
};
// Makes the project buildable, minus the gradle wrapper.
ProjectBuilder.prototype.prepBuildFiles = function () {
// Update the version of build.gradle in each dependent library.
var pluginBuildGradle = path.join(this.root, 'cordova', 'lib', 'plugin-build.gradle');
var propertiesObj = this.readProjectProperties();
var subProjects = propertiesObj.libs;
// Check and copy the gradle file into the subproject
// Called by the loop before this function def
var checkAndCopy = function (subProject, root) {
var subProjectGradle = path.join(root, subProject, 'build.gradle');
// This is the future-proof way of checking if a file exists
// This must be synchronous to satisfy a Travis test
try {
fs.accessSync(subProjectGradle, fs.F_OK);
} catch (e) {
shell.cp('-f', pluginBuildGradle, subProjectGradle);
}
};
for (var i = 0; i < subProjects.length; ++i) {
if (subProjects[i] !== 'CordovaLib') {
checkAndCopy(subProjects[i], this.root);
}
}
var name = this.extractRealProjectNameFromManifest();
// Remove the proj.id/name- prefix from projects: https://issues.apache.org/jira/browse/CB-9149
var settingsGradlePaths = subProjects.map(function (p) {
var realDir = p.replace(/[/\\]/g, ':');
var libName = realDir.replace(name + '-', '');
var str = 'include ":' + libName + '"\n';
if (realDir.indexOf(name + '-') !== -1) {
str += 'project(":' + libName + '").projectDir = new File("' + p + '")\n';
}
return str;
});
fs.writeFileSync(path.join(this.root, 'settings.gradle'),
'// GENERATED FILE - DO NOT EDIT\n' +
'include ":"\n' + settingsGradlePaths.join(''));
// Update dependencies within build.gradle.
var buildGradle = fs.readFileSync(path.join(this.root, 'app', 'build.gradle'), 'utf8');
var depsList = '';
var root = this.root;
var insertExclude = function (p) {
var gradlePath = path.join(root, p, 'build.gradle');
var projectGradleFile = fs.readFileSync(gradlePath, 'utf-8');
if (projectGradleFile.indexOf('CordovaLib') !== -1) {
depsList += '{\n exclude module:("CordovaLib")\n }\n';
} else { } else {
depsList += '\n'; return spawn(gradle_cmd, ['-p', this.root, 'wrapper', '-b', wrapperGradle], { stdio: 'inherit' });
} }
}; }
subProjects.forEach(function (p) {
events.emit('log', 'Subproject Path: ' + p);
var libName = p.replace(/[/\\]/g, ':').replace(name + '-', '');
if (libName !== 'app') {
depsList += ' implementation(project(path: ":' + libName + '"))';
insertExclude(p);
}
});
// For why we do this mapping: https://issues.apache.org/jira/browse/CB-8390
var SYSTEM_LIBRARY_MAPPINGS = [
[/^\/?extras\/android\/support\/(.*)$/, 'com.android.support:support-$1:+'],
[/^\/?google\/google_play_services\/libproject\/google-play-services_lib\/?$/, 'com.google.android.gms:play-services:+']
];
propertiesObj.systemLibs.forEach(function (p) { readProjectProperties () {
var mavenRef; function findAllUniq (data, r) {
// It's already in gradle form if it has two ':'s var s = {};
if (/:.*:/.exec(p)) { var m;
mavenRef = p; while ((m = r.exec(data))) {
} else { s[m[1]] = 1;
for (var i = 0; i < SYSTEM_LIBRARY_MAPPINGS.length; ++i) {
var pair = SYSTEM_LIBRARY_MAPPINGS[i];
if (pair[0].exec(p)) {
mavenRef = p.replace(pair[0], pair[1]);
break;
}
} }
if (!mavenRef) { return Object.keys(s);
throw new CordovaError('Unsupported system library (does not work with gradle): ' + p); }
var data = fs.readFileSync(path.join(this.root, 'project.properties'), 'utf8');
return {
libs: findAllUniq(data, /^\s*android\.library\.reference\.\d+=(.*)(?:\s|$)/mg),
gradleIncludes: findAllUniq(data, /^\s*cordova\.gradle\.include\.\d+=(.*)(?:\s|$)/mg),
systemLibs: findAllUniq(data, /^\s*cordova\.system\.library\.\d+=(.*)(?:\s|$)/mg)
};
}
extractRealProjectNameFromManifest () {
var manifestPath = path.join(this.root, 'app', 'src', 'main', 'AndroidManifest.xml');
var manifestData = fs.readFileSync(manifestPath, 'utf8');
var m = /<manifest[\s\S]*?package\s*=\s*"(.*?)"/i.exec(manifestData);
if (!m) {
throw new CordovaError('Could not find package name in ' + manifestPath);
}
var packageName = m[1];
var lastDotIndex = packageName.lastIndexOf('.');
return packageName.substring(lastDotIndex + 1);
}
// Makes the project buildable, minus the gradle wrapper.
prepBuildFiles () {
// Update the version of build.gradle in each dependent library.
var pluginBuildGradle = path.join(this.root, 'cordova', 'lib', 'plugin-build.gradle');
var propertiesObj = this.readProjectProperties();
var subProjects = propertiesObj.libs;
// Check and copy the gradle file into the subproject
// Called by the loop before this function def
var checkAndCopy = function (subProject, root) {
var subProjectGradle = path.join(root, subProject, 'build.gradle');
// This is the future-proof way of checking if a file exists
// This must be synchronous to satisfy a Travis test
try {
fs.accessSync(subProjectGradle, fs.F_OK);
} catch (e) {
shell.cp('-f', pluginBuildGradle, subProjectGradle);
}
};
for (var i = 0; i < subProjects.length; ++i) {
if (subProjects[i] !== 'CordovaLib') {
checkAndCopy(subProjects[i], this.root);
} }
} }
depsList += ' compile "' + mavenRef + '"\n'; var name = this.extractRealProjectNameFromManifest();
}); // Remove the proj.id/name- prefix from projects: https://issues.apache.org/jira/browse/CB-9149
var settingsGradlePaths = subProjects.map(function (p) {
buildGradle = buildGradle.replace(/(SUB-PROJECT DEPENDENCIES START)[\s\S]*(\/\/ SUB-PROJECT DEPENDENCIES END)/, '$1\n' + depsList + ' $2'); var realDir = p.replace(/[/\\]/g, ':');
var includeList = ''; var libName = realDir.replace(name + '-', '');
var str = 'include ":' + libName + '"\n';
propertiesObj.gradleIncludes.forEach(function (includePath) { if (realDir.indexOf(name + '-') !== -1) {
includeList += 'apply from: "../' + includePath + '"\n'; str += 'project(":' + libName + '").projectDir = new File("' + p + '")\n';
});
buildGradle = buildGradle.replace(/(PLUGIN GRADLE EXTENSIONS START)[\s\S]*(\/\/ PLUGIN GRADLE EXTENSIONS END)/, '$1\n' + includeList + '$2');
// This needs to be stored in the app gradle, not the root grade
fs.writeFileSync(path.join(this.root, 'app', 'build.gradle'), buildGradle);
};
ProjectBuilder.prototype.prepEnv = function (opts) {
var self = this;
return check_reqs.check_gradle()
.then(function (gradlePath) {
return self.runGradleWrapper(gradlePath);
}).then(function () {
return self.prepBuildFiles();
}).then(function () {
// If the gradle distribution URL is set, make sure it points to version we want.
// If it's not set, do nothing, assuming that we're using a future version of gradle that we don't want to mess with.
// For some reason, using ^ and $ don't work. This does the job, though.
var distributionUrlRegex = /distributionUrl.*zip/;
var distributionUrl = process.env['CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL'] || 'https\\://services.gradle.org/distributions/gradle-4.4-all.zip';
var gradleWrapperPropertiesPath = path.join(self.root, 'gradle', 'wrapper', 'gradle-wrapper.properties');
shell.chmod('u+w', gradleWrapperPropertiesPath);
shell.sed('-i', distributionUrlRegex, 'distributionUrl=' + distributionUrl, gradleWrapperPropertiesPath);
var propertiesFile = opts.buildType + SIGNING_PROPERTIES;
var propertiesFilePath = path.join(self.root, propertiesFile);
if (opts.packageInfo) {
fs.writeFileSync(propertiesFilePath, TEMPLATE + opts.packageInfo.toProperties());
} else if (isAutoGenerated(propertiesFilePath)) {
shell.rm('-f', propertiesFilePath);
} }
return str;
}); });
};
/* fs.writeFileSync(path.join(this.root, 'settings.gradle'),
* Builds the project with gradle. '// GENERATED FILE - DO NOT EDIT\n' +
* Returns a promise. 'include ":"\n' + settingsGradlePaths.join(''));
*/
ProjectBuilder.prototype.build = function (opts) {
var wrapper = path.join(this.root, 'gradlew');
var args = this.getArgs(opts.buildType === 'debug' ? 'debug' : 'release', opts);
return spawn(wrapper, args, { stdio: 'pipe' }) // Update dependencies within build.gradle.
.progress(function (stdio) { var buildGradle = fs.readFileSync(path.join(this.root, 'app', 'build.gradle'), 'utf8');
if (stdio.stderr) { var depsList = '';
/* var root = this.root;
* Workaround for the issue with Java printing some unwanted information to var insertExclude = function (p) {
* stderr instead of stdout. var gradlePath = path.join(root, p, 'build.gradle');
* This function suppresses 'Picked up _JAVA_OPTIONS' message from being var projectGradleFile = fs.readFileSync(gradlePath, 'utf-8');
* printed to stderr. See https://issues.apache.org/jira/browse/CB-9971 for if (projectGradleFile.indexOf('CordovaLib') !== -1) {
* explanation. depsList += '{\n exclude module:("CordovaLib")\n }\n';
*/
var suppressThisLine = /^Picked up _JAVA_OPTIONS: /i.test(stdio.stderr.toString());
if (suppressThisLine) {
return;
}
process.stderr.write(stdio.stderr);
} else { } else {
process.stdout.write(stdio.stdout); depsList += '\n';
} }
}).catch(function (error) { };
if (error.toString().indexOf('failed to find target with hash string') >= 0) { subProjects.forEach(function (p) {
return check_reqs.check_android_target(error).then(function () { events.emit('log', 'Subproject Path: ' + p);
// If due to some odd reason - check_android_target succeeds var libName = p.replace(/[/\\]/g, ':').replace(name + '-', '');
// we should still fail here. if (libName !== 'app') {
return Q.reject(error); depsList += ' implementation(project(path: ":' + libName + '"))';
}); insertExclude(p);
} }
return Q.reject(error);
}); });
}; // For why we do this mapping: https://issues.apache.org/jira/browse/CB-8390
var SYSTEM_LIBRARY_MAPPINGS = [
[/^\/?extras\/android\/support\/(.*)$/, 'com.android.support:support-$1:+'],
[/^\/?google\/google_play_services\/libproject\/google-play-services_lib\/?$/, 'com.google.android.gms:play-services:+']
];
ProjectBuilder.prototype.clean = function (opts) { propertiesObj.systemLibs.forEach(function (p) {
var builder = this; var mavenRef;
var wrapper = path.join(this.root, 'gradlew'); // It's already in gradle form if it has two ':'s
var args = builder.getArgs('clean', opts); if (/:.*:/.exec(p)) {
return Q().then(function () { mavenRef = p;
return spawn(wrapper, args, { stdio: 'inherit' }); } else {
}) for (var i = 0; i < SYSTEM_LIBRARY_MAPPINGS.length; ++i) {
.then(function () { var pair = SYSTEM_LIBRARY_MAPPINGS[i];
shell.rm('-rf', path.join(builder.root, 'out')); if (pair[0].exec(p)) {
mavenRef = p.replace(pair[0], pair[1]);
break;
}
}
if (!mavenRef) {
throw new CordovaError('Unsupported system library (does not work with gradle): ' + p);
}
}
depsList += ' compile "' + mavenRef + '"\n';
});
['debug', 'release'].forEach(function (config) { buildGradle = buildGradle.replace(/(SUB-PROJECT DEPENDENCIES START)[\s\S]*(\/\/ SUB-PROJECT DEPENDENCIES END)/, '$1\n' + depsList + ' $2');
var propertiesFilePath = path.join(builder.root, config + SIGNING_PROPERTIES); var includeList = '';
if (isAutoGenerated(propertiesFilePath)) {
propertiesObj.gradleIncludes.forEach(function (includePath) {
includeList += 'apply from: "../' + includePath + '"\n';
});
buildGradle = buildGradle.replace(/(PLUGIN GRADLE EXTENSIONS START)[\s\S]*(\/\/ PLUGIN GRADLE EXTENSIONS END)/, '$1\n' + includeList + '$2');
// This needs to be stored in the app gradle, not the root grade
fs.writeFileSync(path.join(this.root, 'app', 'build.gradle'), buildGradle);
}
prepEnv (opts) {
var self = this;
return check_reqs.check_gradle()
.then(function (gradlePath) {
return self.runGradleWrapper(gradlePath);
}).then(function () {
return self.prepBuildFiles();
}).then(function () {
// If the gradle distribution URL is set, make sure it points to version we want.
// If it's not set, do nothing, assuming that we're using a future version of gradle that we don't want to mess with.
// For some reason, using ^ and $ don't work. This does the job, though.
var distributionUrlRegex = /distributionUrl.*zip/;
var distributionUrl = process.env['CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL'] || 'https\\://services.gradle.org/distributions/gradle-4.4-all.zip';
var gradleWrapperPropertiesPath = path.join(self.root, 'gradle', 'wrapper', 'gradle-wrapper.properties');
shell.chmod('u+w', gradleWrapperPropertiesPath);
shell.sed('-i', distributionUrlRegex, 'distributionUrl=' + distributionUrl, gradleWrapperPropertiesPath);
var propertiesFile = opts.buildType + SIGNING_PROPERTIES;
var propertiesFilePath = path.join(self.root, propertiesFile);
if (opts.packageInfo) {
fs.writeFileSync(propertiesFilePath, TEMPLATE + opts.packageInfo.toProperties());
} else if (isAutoGenerated(propertiesFilePath)) {
shell.rm('-f', propertiesFilePath); shell.rm('-f', propertiesFilePath);
} }
}); });
}); }
};
ProjectBuilder.prototype.findOutputApks = function (build_type, arch) { /*
var self = this; * Builds the project with gradle.
return Object.keys(this.binDirs).reduce(function (result, builderName) { * Returns a promise.
var binDir = self.binDirs[builderName]; */
return result.concat(findOutputApksHelper(binDir, build_type, builderName === 'ant' ? null : arch)); build (opts) {
}, []).sort(apkSorter); var wrapper = path.join(this.root, 'gradlew');
}; var args = this.getArgs(opts.buildType === 'debug' ? 'debug' : 'release', opts);
return spawn(wrapper, args, { stdio: 'pipe' })
.progress(function (stdio) {
if (stdio.stderr) {
/*
* Workaround for the issue with Java printing some unwanted information to
* stderr instead of stdout.
* This function suppresses 'Picked up _JAVA_OPTIONS' message from being
* printed to stderr. See https://issues.apache.org/jira/browse/CB-9971 for
* explanation.
*/
var suppressThisLine = /^Picked up _JAVA_OPTIONS: /i.test(stdio.stderr.toString());
if (suppressThisLine) {
return;
}
process.stderr.write(stdio.stderr);
} else {
process.stdout.write(stdio.stdout);
}
}).catch(function (error) {
if (error.toString().indexOf('failed to find target with hash string') >= 0) {
return check_reqs.check_android_target(error).then(function () {
// If due to some odd reason - check_android_target succeeds
// we should still fail here.
return Q.reject(error);
});
}
return Q.reject(error);
});
}
clean (opts) {
var builder = this;
var wrapper = path.join(this.root, 'gradlew');
var args = builder.getArgs('clean', opts);
return Q().then(function () {
return spawn(wrapper, args, { stdio: 'inherit' });
})
.then(function () {
shell.rm('-rf', path.join(builder.root, 'out'));
['debug', 'release'].forEach(function (config) {
var propertiesFilePath = path.join(builder.root, config + SIGNING_PROPERTIES);
if (isAutoGenerated(propertiesFilePath)) {
shell.rm('-f', propertiesFilePath);
}
});
});
}
findOutputApks (build_type, arch) {
var self = this;
return Object.keys(this.binDirs).reduce(function (result, builderName) {
var binDir = self.binDirs[builderName];
return result.concat(findOutputApksHelper(binDir, build_type, builderName === 'ant' ? null : arch));
}, []).sort(apkSorter);
}
}
module.exports = ProjectBuilder; module.exports = ProjectBuilder;