2015-10-20 16:15:57 -07:00
#!/usr/bin/env node
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you 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.
*/
2017-06-13 11:42:20 -07:00
var shell = require ( 'shelljs' ) ;
var Q = require ( 'q' ) ;
var path = require ( 'path' ) ;
var fs = require ( 'fs' ) ;
var check _reqs = require ( './../templates/cordova/lib/check_reqs' ) ;
var ROOT = path . join ( _ _dirname , '..' , '..' ) ;
2015-10-20 16:15:57 -07:00
var CordovaError = require ( 'cordova-common' ) . CordovaError ;
var AndroidManifest = require ( '../templates/cordova/lib/AndroidManifest' ) ;
2017-07-11 12:25:12 -05:00
// Export all helper functions, and make sure internally within this module, we
// reference these methods via the `exports` object - this helps with testing
// (since we can then mock and control behaviour of all of these functions)
exports . validatePackageName = validatePackageName ;
exports . validateProjectName = validateProjectName ;
exports . setShellFatal = setShellFatal ;
exports . copyJsAndLibrary = copyJsAndLibrary ;
exports . copyScripts = copyScripts ;
exports . copyBuildRules = copyBuildRules ;
exports . writeProjectProperties = writeProjectProperties ;
exports . prepBuildFiles = prepBuildFiles ;
2017-06-13 11:42:20 -07:00
function setShellFatal ( value , func ) {
2015-10-20 16:15:57 -07:00
var oldVal = shell . config . fatal ;
shell . config . fatal = value ;
func ( ) ;
shell . config . fatal = oldVal ;
}
2017-06-13 11:42:20 -07:00
function getFrameworkDir ( projectPath , shared ) {
2015-10-20 16:15:57 -07:00
return shared ? path . join ( ROOT , 'framework' ) : path . join ( projectPath , 'CordovaLib' ) ;
}
2017-06-27 13:15:04 -07:00
function copyJsAndLibrary ( projectPath , shared , projectName , isLegacy ) {
2015-10-20 16:15:57 -07:00
var nestedCordovaLibPath = getFrameworkDir ( projectPath , false ) ;
var srcCordovaJsPath = path . join ( ROOT , 'bin' , 'templates' , 'project' , 'assets' , 'www' , 'cordova.js' ) ;
2017-04-21 16:27:26 -07:00
var app _path = path . join ( projectPath , 'app' , 'src' , 'main' ) ;
2017-06-27 13:15:04 -07:00
if ( isLegacy ) {
app _path = projectPath ;
}
2017-06-13 08:27:46 -07:00
2017-04-21 16:27:26 -07:00
shell . cp ( '-f' , srcCordovaJsPath , path . join ( app _path , 'assets' , 'www' , 'cordova.js' ) ) ;
2015-10-20 16:15:57 -07:00
// Copy the cordova.js file to platforms/<platform>/platform_www/
// The www dir is nuked on each prepare so we keep cordova.js in platform_www
shell . mkdir ( '-p' , path . join ( projectPath , 'platform_www' ) ) ;
shell . cp ( '-f' , srcCordovaJsPath , path . join ( projectPath , 'platform_www' ) ) ;
// Copy cordova-js-src directory into platform_www directory.
// We need these files to build cordova.js if using browserify method.
shell . cp ( '-rf' , path . join ( ROOT , 'cordova-js-src' ) , path . join ( projectPath , 'platform_www' ) ) ;
// Don't fail if there are no old jars.
2017-07-11 12:25:12 -05:00
exports . setShellFatal ( false , function ( ) {
2017-07-14 16:51:10 -07:00
shell . ls ( path . join ( app _path , 'libs' , 'cordova-*.jar' ) ) . forEach ( function ( oldJar ) {
2015-10-20 16:15:57 -07:00
console . log ( 'Deleting ' + oldJar ) ;
shell . rm ( '-f' , oldJar ) ;
} ) ;
var wasSymlink = true ;
try {
// Delete the symlink if it was one.
fs . unlinkSync ( nestedCordovaLibPath ) ;
} catch ( e ) {
wasSymlink = false ;
}
// Delete old library project if it existed.
if ( shared ) {
shell . rm ( '-rf' , nestedCordovaLibPath ) ;
} else if ( ! wasSymlink ) {
// Delete only the src, since Eclipse / Android Studio can't handle their project files being deleted.
shell . rm ( '-rf' , path . join ( nestedCordovaLibPath , 'src' ) ) ;
}
} ) ;
if ( shared ) {
var relativeFrameworkPath = path . relative ( projectPath , getFrameworkDir ( projectPath , true ) ) ;
fs . symlinkSync ( relativeFrameworkPath , nestedCordovaLibPath , 'dir' ) ;
} else {
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 ( '-f' , path . join ( ROOT , 'framework' , 'cordova.gradle' ) , nestedCordovaLibPath ) ;
shell . cp ( '-r' , path . join ( ROOT , 'framework' , 'src' ) , nestedCordovaLibPath ) ;
}
}
2017-06-13 11:42:20 -07:00
function extractSubProjectPaths ( data ) {
2015-10-20 16:15:57 -07:00
var ret = { } ;
var r = /^\s*android\.library\.reference\.\d+=(.*)(?:\s|$)/mg ;
var m ;
while ( ( m = r . exec ( data ) ) ) {
ret [ m [ 1 ] ] = 1 ;
}
return Object . keys ( ret ) ;
}
2017-06-13 11:42:20 -07:00
function writeProjectProperties ( projectPath , target _api ) {
2015-10-20 16:15:57 -07:00
var dstPath = path . join ( projectPath , 'project.properties' ) ;
var templatePath = path . join ( ROOT , 'bin' , 'templates' , 'project' , 'project.properties' ) ;
var srcPath = fs . existsSync ( dstPath ) ? dstPath : templatePath ;
var data = fs . readFileSync ( srcPath , 'utf8' ) ;
data = data . replace ( /^target=.*/m , 'target=' + target _api ) ;
var subProjects = extractSubProjectPaths ( data ) ;
2017-06-13 11:42:20 -07:00
subProjects = subProjects . filter ( function ( p ) {
2015-10-20 16:15:57 -07:00
return ! ( /^CordovaLib$/m . exec ( p ) ||
2017-06-13 11:42:20 -07:00
/[\\/]cordova-android[\\/]framework$/m . exec ( p ) ||
/^(\.\.[\\/])+framework$/m . exec ( p ) ) ;
2015-10-20 16:15:57 -07:00
} ) ;
subProjects . unshift ( 'CordovaLib' ) ;
data = data . replace ( /^\s*android\.library\.reference\.\d+=.*\n/mg , '' ) ;
if ( ! /\n$/ . exec ( data ) ) {
data += '\n' ;
}
for ( var i = 0 ; i < subProjects . length ; ++ i ) {
2017-06-13 11:42:20 -07:00
data += 'android.library.reference.' + ( i + 1 ) + '=' + subProjects [ i ] + '\n' ;
2015-10-20 16:15:57 -07:00
}
fs . writeFileSync ( dstPath , data ) ;
}
2017-03-30 13:38:18 -07:00
// This makes no sense, what if you're building with a different build system?
2018-09-06 11:06:18 +09:00
function prepBuildFiles ( projectPath ) {
2016-02-17 16:37:58 +03:00
var buildModule = require ( path . resolve ( projectPath , 'cordova/lib/builders/builders' ) ) ;
2018-09-06 11:06:18 +09:00
buildModule . getBuilder ( ) . prepBuildFiles ( ) ;
2015-10-20 16:15:57 -07:00
}
2017-06-27 13:15:04 -07:00
function copyBuildRules ( projectPath , isLegacy ) {
2015-10-20 16:15:57 -07:00
var srcDir = path . join ( ROOT , 'bin' , 'templates' , 'project' ) ;
2017-06-27 13:15:04 -07:00
if ( isLegacy ) {
// The project's build.gradle is identical to the earlier build.gradle, so it should still work
shell . cp ( '-f' , path . join ( srcDir , 'legacy' , 'build.gradle' ) , projectPath ) ;
shell . cp ( '-f' , path . join ( srcDir , 'wrapper.gradle' ) , projectPath ) ;
2017-06-13 08:27:46 -07:00
} else {
2017-06-27 13:15:04 -07:00
shell . cp ( '-f' , path . join ( srcDir , 'build.gradle' ) , projectPath ) ;
shell . cp ( '-f' , path . join ( srcDir , 'app' , 'build.gradle' ) , path . join ( projectPath , 'app' ) ) ;
shell . cp ( '-f' , path . join ( srcDir , 'wrapper.gradle' ) , projectPath ) ;
2017-06-13 08:27:46 -07:00
}
2015-10-20 16:15:57 -07:00
}
2018-12-19 10:33:16 +09:00
function copyScripts ( projectPath ) {
2017-03-15 11:58:16 -07:00
var bin = path . join ( ROOT , 'bin' ) ;
var srcScriptsDir = path . join ( bin , 'templates' , 'cordova' ) ;
2015-10-20 16:15:57 -07:00
var destScriptsDir = path . join ( projectPath , 'cordova' ) ;
// Delete old scripts directory if this is an update.
shell . rm ( '-rf' , destScriptsDir ) ;
// Copy in the new ones.
shell . cp ( '-r' , srcScriptsDir , projectPath ) ;
2018-12-19 10:33:16 +09:00
let nodeModulesDir = path . join ( ROOT , 'node_modules' ) ;
if ( fs . existsSync ( nodeModulesDir ) ) shell . cp ( '-r' , nodeModulesDir , destScriptsDir ) ;
2017-03-15 11:58:16 -07:00
shell . cp ( path . join ( bin , 'check_reqs*' ) , destScriptsDir ) ;
shell . cp ( path . join ( bin , 'android_sdk_version*' ) , destScriptsDir ) ;
var check _reqs = path . join ( destScriptsDir , 'check_reqs' ) ;
var android _sdk _version = path . join ( destScriptsDir , 'android_sdk_version' ) ;
// TODO: the two files being edited on-the-fly here are shared between
// platform and project-level commands. the below `sed` is updating the
// `require` path for the two libraries. if there's a better way to share
// modules across both the repo and generated projects, we should make sure
// to remove/update this.
shell . sed ( '-i' , /templates\/cordova\// , '' , android _sdk _version ) ;
shell . sed ( '-i' , /templates\/cordova\// , '' , check _reqs ) ;
2015-10-20 16:15:57 -07:00
}
/**
* Test whether a package name is acceptable for use as an android project.
* Returns a promise, fulfilled if the package name is acceptable; rejected
* otherwise.
*/
2017-06-13 11:42:20 -07:00
function validatePackageName ( package _name ) {
// Make the package conform to Java package types
// http://developer.android.com/guide/topics/manifest/manifest-element.html#package
// Enforce underscore limitation
2015-10-20 16:15:57 -07:00
var msg = 'Error validating package name. ' ;
2017-07-11 12:25:12 -05:00
2015-10-20 16:15:57 -07:00
if ( ! /^[a-zA-Z][a-zA-Z0-9_]+(\.[a-zA-Z][a-zA-Z0-9_]*)+$/ . test ( package _name ) ) {
2018-11-21 06:54:55 +01:00
return Q . reject ( new CordovaError ( msg + 'Must look like: `com.company.Name`. Currently is: `' + package _name + '`' ) ) ;
2015-10-20 16:15:57 -07:00
}
2017-06-13 11:42:20 -07:00
// Class is a reserved word
if ( /\b[Cc]lass\b/ . test ( package _name ) ) {
2015-10-20 16:15:57 -07:00
return Q . reject ( new CordovaError ( msg + '"class" is a reserved word' ) ) ;
}
return Q . resolve ( ) ;
}
/**
* Test whether a project name is acceptable for use as an android class.
* Returns a promise, fulfilled if the project name is acceptable; rejected
* otherwise.
*/
2017-06-13 11:42:20 -07:00
function validateProjectName ( project _name ) {
2015-10-20 16:15:57 -07:00
var msg = 'Error validating project name. ' ;
2017-06-13 11:42:20 -07:00
// Make sure there's something there
2015-10-20 16:15:57 -07:00
if ( project _name === '' ) {
return Q . reject ( new CordovaError ( msg + 'Project name cannot be empty' ) ) ;
}
2017-06-13 11:42:20 -07:00
// Enforce stupid name error
2015-10-20 16:15:57 -07:00
if ( project _name === 'CordovaActivity' ) {
return Q . reject ( new CordovaError ( msg + 'Project name cannot be CordovaActivity' ) ) ;
}
2017-06-13 11:42:20 -07:00
// Classes in Java don't begin with numbers
2015-10-20 16:15:57 -07:00
if ( /^[0-9]/ . test ( project _name ) ) {
return Q . reject ( new CordovaError ( msg + 'Project name must not begin with a number' ) ) ;
}
return Q . resolve ( ) ;
}
/**
* Creates an android application with the given options.
*
* @param {String} project_path Path to the new Cordova android project.
* @param {ConfigParser} config Instance of ConfigParser to retrieve basic
* project properties.
* @param {Object} [options={}] Various options
* @param {String} [options.activityName='MainActivity'] Name for the
* activity
* @param {Boolean} [options.link=false] Specifies whether javascript files
* and CordovaLib framework will be symlinked to created application.
* @param {String} [options.customTemplate] Path to project template
* (override)
* @param {EventEmitter} [events] An EventEmitter instance for logging
* events
*
* @return {Promise<String>} Directory where application has been created
*/
2017-06-13 11:42:20 -07:00
exports . create = function ( project _path , config , options , events ) {
2015-10-20 16:15:57 -07:00
options = options || { } ;
// Set default values for path, package and name
project _path = path . relative ( process . cwd ( ) , ( project _path || 'CordovaExample' ) ) ;
// Check if project already exists
2017-06-13 11:42:20 -07:00
if ( fs . existsSync ( project _path ) ) {
2015-10-20 16:15:57 -07:00
return Q . reject ( new CordovaError ( 'Project already exists! Delete and recreate' ) ) ;
}
2017-07-28 14:05:53 +03:00
var package _name = config . android _packageName ( ) || config . packageName ( ) || 'my.cordova.project' ;
2015-10-20 16:15:57 -07:00
var project _name = config . name ( ) ?
2017-06-13 11:42:20 -07:00
config . name ( ) . replace ( /[^\w.]/g , '_' ) : 'CordovaExample' ;
2015-10-20 16:15:57 -07:00
var safe _activity _name = config . android _activityName ( ) || options . activityName || 'MainActivity' ;
2017-06-13 11:42:20 -07:00
var target _api = check _reqs . get _target ( ) ;
2015-10-20 16:15:57 -07:00
2017-06-13 11:42:20 -07:00
// Make the package conform to Java package types
2017-07-11 12:25:12 -05:00
return exports . validatePackageName ( package _name )
2017-06-13 11:42:20 -07:00
. then ( function ( ) {
2017-07-11 12:25:12 -05:00
exports . validateProjectName ( project _name ) ;
2017-06-13 11:42:20 -07:00
} ) . then ( function ( ) {
2015-10-20 16:15:57 -07:00
// Log the given values for the project
2017-06-27 13:15:04 -07:00
events . emit ( 'log' , 'Creating Cordova project for the Android platform:' ) ;
events . emit ( 'log' , '\tPath: ' + project _path ) ;
events . emit ( 'log' , '\tPackage: ' + package _name ) ;
events . emit ( 'log' , '\tName: ' + project _name ) ;
events . emit ( 'log' , '\tActivity: ' + safe _activity _name ) ;
events . emit ( 'log' , '\tAndroid target: ' + target _api ) ;
events . emit ( 'verbose' , 'Copying android template project to ' + project _path ) ;
2017-07-11 12:25:12 -05:00
exports . setShellFatal ( true , function ( ) {
2017-06-27 13:15:04 -07:00
var project _template _dir = options . customTemplate || path . join ( ROOT , 'bin' , 'templates' , 'project' ) ;
var app _path = path . join ( project _path , 'app' , 'src' , 'main' ) ;
// copy project template
shell . mkdir ( '-p' , app _path ) ;
shell . cp ( '-r' , path . join ( project _template _dir , 'assets' ) , app _path ) ;
shell . cp ( '-r' , path . join ( project _template _dir , 'res' ) , app _path ) ;
shell . cp ( path . join ( project _template _dir , 'gitignore' ) , path . join ( project _path , '.gitignore' ) ) ;
// Manually create directories that would be empty within the template (since git doesn't track directories).
shell . mkdir ( path . join ( app _path , 'libs' ) ) ;
// copy cordova.js, cordova.jar
2017-07-11 12:25:12 -05:00
exports . copyJsAndLibrary ( project _path , options . link , safe _activity _name ) ;
2017-06-27 13:15:04 -07:00
// Set up ther Android Studio paths
var java _path = path . join ( app _path , 'java' ) ;
var assets _path = path . join ( app _path , 'assets' ) ;
var resource _path = path . join ( app _path , 'res' ) ;
shell . mkdir ( '-p' , java _path ) ;
shell . mkdir ( '-p' , assets _path ) ;
shell . mkdir ( '-p' , resource _path ) ;
// interpolate the activity name and package
var packagePath = package _name . replace ( /\./g , path . sep ) ;
var activity _dir = path . join ( java _path , packagePath ) ;
var activity _path = path . join ( activity _dir , safe _activity _name + '.java' ) ;
shell . mkdir ( '-p' , activity _dir ) ;
shell . cp ( '-f' , path . join ( project _template _dir , 'Activity.java' ) , activity _path ) ;
shell . sed ( '-i' , /__ACTIVITY__/ , safe _activity _name , activity _path ) ;
shell . sed ( '-i' , /__NAME__/ , project _name , path . join ( app _path , 'res' , 'values' , 'strings.xml' ) ) ;
shell . sed ( '-i' , /__ID__/ , package _name , activity _path ) ;
var manifest = new AndroidManifest ( path . join ( project _template _dir , 'AndroidManifest.xml' ) ) ;
manifest . setPackageId ( package _name )
. setTargetSdkVersion ( target _api . split ( '-' ) [ 1 ] )
. getActivity ( ) . setName ( safe _activity _name ) ;
var manifest _path = path . join ( app _path , 'AndroidManifest.xml' ) ;
manifest . write ( manifest _path ) ;
2018-12-19 10:33:16 +09:00
exports . copyScripts ( project _path ) ;
2017-07-11 12:25:12 -05:00
exports . copyBuildRules ( project _path ) ;
2017-06-27 13:15:04 -07:00
} ) ;
// Link it to local android install.
2017-07-11 12:25:12 -05:00
exports . writeProjectProperties ( project _path , target _api ) ;
2018-09-06 11:06:18 +09:00
exports . prepBuildFiles ( project _path ) ;
2017-06-27 13:15:04 -07:00
events . emit ( 'log' , generateDoneMessage ( 'create' , options . link ) ) ;
} ) . thenResolve ( project _path ) ;
2015-10-20 16:15:57 -07:00
} ;
2017-06-13 11:42:20 -07:00
function generateDoneMessage ( type , link ) {
2015-10-20 16:15:57 -07:00
var pkg = require ( '../../package' ) ;
2017-06-13 11:42:20 -07:00
var msg = 'Android project ' + ( type === 'update' ? 'updated ' : 'created ' ) + 'with ' + pkg . name + '@' + pkg . version ;
2015-10-20 16:15:57 -07:00
if ( link ) {
msg += ' and has a linked CordovaLib' ;
}
return msg ;
}
// Returns a promise.
2017-06-13 11:42:20 -07:00
exports . update = function ( projectPath , options , events ) {
2015-10-20 16:15:57 -07:00
2017-11-30 11:08:39 -08:00
var errorString =
'An in-place platform update is not supported. \n' +
'The `platforms` folder is always treated as a build artifact in the CLI workflow.\n' +
'To update your platform, you have to remove, then add your android platform again.\n' +
'Make sure you save your plugins beforehand using `cordova plugin save`, and save \n' + 'a copy of the platform first if you had manual changes in it.\n' +
'\tcordova plugin save\n' +
'\tcordova platform rm android\n' +
'\tcordova platform add android\n'
;
2017-11-30 10:40:07 -08:00
return Q . reject ( errorString ) ;
2015-10-20 16:15:57 -07:00
} ;