一个针对CocosCreator自动化构建方案的实现

本文较长,废话描述较多,如不感兴趣,请直接翻到最后,获取代码地址。

解决什么问题

  1. 构建编译时无需开启各种编辑器
  2. 每个项目都可以通用
  3. 工作流程化
  4. 专注于游戏开发

技术方案

首先,其本质是命令行构建项目,使用CocosCreator命令构建脚本,使用gradlew构建安卓包等。

其次,所谓的自动化,是借助jenkins等自动化工具实现的,即触发式。

最后,流程化的意思是只有人员填写好配置即可。

因此,本方案采取的如下技术:

  1. 配置文件采用yaml:可读性强,相对json能添加注释。

  2. 使用nodejs处理:跨平台,npm库有非常多成熟的库可以拿来用,强大的“child_process”可以系统命令。

  3. 入口单一:可以自由选择jenkisn、命令行、服务器触发方式。

具体实现

配置模板

每个项目都根据配置进行构建打包,本方案将配置文件存放在settings文件夹下,推荐纳入版本管理

1
2
3
4
5
6
7
8
# 根据需要配置自己的平台
# 对各平台配置一个版本号,只有版本号大于上一个版本时才开始构建。
webCode: 1
appCode: 1

# 配置
title: 项目名称
# 其它各类配置

yaml可以借助yamljs库快速转成json

1
2
3
4
5
const yaml = require('yamljs');
function xxx(configPath){
let config = yaml.parse(fs.readFileSync(configPath).toString());
}

构建命令

1
const cmd = `${enginePath} --path ${projectDir} --build "title=${title};......"
  • build参数参考文档
  • 为了兼容不同的CocosCreator版本,所以引擎路径不要写死。

执行命令

nodejs可以通过child_process模块下的方法,执行批处理,该模块下大致可以分为三类:

  1. exec/execSync 执行批命令

  2. execFile/execFileSycn 执行批命令文件

  3. spawn/spawnSync 核心方法,,exec和execFile都是基于spawn封装的

sync则如其名,是对应方法的同步方法。

本方案是一种自动化的方案,理论上不应该关心中间状态,所以使用的是execSync和execFileSync,如果需要关心实时执行,则应该选择spwan。

所以,一般执行批命令为:

1
2
3
4
const { execSync } = require("child_process");
function xxx(cmd){
execSync(cmd);
}

处理构建结果

web构建

web版本构建结束后,我们可能要处理其适配问题,比如集成我的另外一个插件H5优化适配,那可能要设置一些参数,可以在这里进行。

原生构建

  1. 备份代码

原生打包,大多数都进行了混淆,那我们需要备份未混淆的代码。

CocosCreator本身是为我们备份了的,但是每个版本都会覆盖,所以我们需要另外备份下。

1
2
3
4
// 原始目录地址
const dstDir = path.join(buildDir, 'js backups (useful for debugging)');
const backupsDir = path.join(outputDir, title);
copyFileSync(dstDir, backupsDir);
  1. 生成热更包

同时,可能我们集成了热更文件,需要生成一个热更包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// manifest 形式
let manifest = {
packageUrl: packageUrl,
remoteManifestUrl: `${packageUrl} project.manifest`,
remoteVersionUrl: `${packageUrl} version.manifest`,
version: `${ver} `,
assets: {}
}
// 获取所有文件
let files = getAllFiles(hotUpdateDir);
files.forEach((filePath) => {
// 获取相对路径,作为key
const relative = encodeURI(path.relative(hotUpdateDir, filePath).replace(/\\/g, '/'));
// 获取size、md5和compressed
manifest.assets[relative] = {
size: fs.statSync(filePath),
md5: md5
}
}
// 写入文件
fs.writeFileSync(path.join(hotUpdateDir, 'project.manifest'), JSON.stringify(manifest, null, 0));
delete manifest.assets;
fs.writeFileSync(path.join(hotUpdateDir, 'version.manifest'), JSON.stringify(manifest, null, 4));

然后将其上传到对应的存储平台,比如本方案提供的上传到cos

1
2
const cmd = `coscmd upload - r ${hotUpdateDir} /hotUpdate/${configData.title} /${ver}`;
execSync(cmd);

原生打包

原生构建后,还需要打出apk、ipa包。

制作icon

参考另一篇文章Node.js的图片处理库images ,我们可以在settings文件夹中,放入一张logo.png, 我们通过images库处理下就可以了。

1
2
3
4
5
6
const images = require('images');
for(let i = 0; i < icons[i].length; i++){
images(path.join(configData.projectDir, 'settings', 'logo.png'))
.size(icons[i].width)
.save(path.join(iconPath, 'ic_launcher.png'));
}

根据这个原理,我还制作了一个icon快速生成的小工具ICON生成器

删除game和instantapp项目

因为我们只需要原生apk项目,所以删除

1
2
3
const sgPath = path.join(androidDir, 'settings.gradle');
let sgData = fs.readFileSync(sgPath).toString().replace(/\,[ ]*\'\:game\'[ ]*\,[ ]*\'\:instantapp\'/, "");
fs.writeFileSync(sgPath, sgData);
  • 如果不删除,则打包命令修改,或要接入game、instantapp代码

修改gralde.properties

因为CocosCreator的命令行打包,有个bug(已知2.3.3版本存在),即gralde.properties的sdk会变成-1,所以我们需要修改下:

1
2
3
4
5
6
7
8
9
10
const gpPath = path.join(androidDir, 'gradle.properties');
let gpData = fs.readFileSync(gpPath).toString();
gpData = gpData.replace(/PROP_COMPILE_SDK_VERSION=.*/, `PROP_COMPILE_SDK_VERSION=${configData.apiLevel}`);
gpData = gpData.replace(/PROP_TARGET_SDK_VERSION=.*/, `PROP_TARGET_SDK_VERSION=${configData.apiLevel}`);
gpData = gpData.replace(/PROP_APP_ABI=.*/, `PROP_APP_ABI=${JSON.stringify(configData.appABIs).replace('[', '').replace(']', '').replace(/\"/g, '').replace(/\,/g, ":")}`);
gpData = gpData.replace(/RELEASE_STORE_FILE=.*/, `RELEASE_STORE_FILE=${storeFile}`);
gpData = gpData.replace(/RELEASE_STORE_PASSWORD=.*/, `RELEASE_STORE_PASSWORD=${password}`);
gpData = gpData.replace(/RELEASE_KEY_ALIAS=.*/, `RELEASE_KEY_ALIAS=${alias}`);
gpData = gpData.replace(/RELEASE_KEY_PASSWORD=.*/, `RELEASE_KEY_PASSWORD=${keyPassword}`);
fs.writeFileSync(gpPath, gpData);

升级gradle

CocosCreator配置的gradle版本较低,如果需要接入高版本的sdk,我们还需要升级gradle,如下为升级到3.6.4的操作

  1. gradle-wrapper.properties
1
2
3
let gwpPath = path.join(androidDir, 'gradle', 'wrapper', 'gradle-wrapper.properties');
let gwpData = fs.readFileSync(gwpPath).toString().replace(/distributionUrl\=.*/, "distributionUrl=https\\://services.gradle.org/distributions/gradle-5.6.4-all.zip");
fs.writeFileSync(gwpPath, gwpData);
  1. 工程的build.gradle
1
2
3
const pbgPath = path.join(androidDir, 'build.gradle');
let data = fs.readFileSync(pbgPath).toString().replace(/gradle\:[0-9.]+/, "gradle:3.6.4");
fs.writeFileSync(pbgPath, data);
  1. 修改mk
1
2
3
const mkPath = path.join(androidDir, 'jni', 'CocosAndroid.mk');
let mkText = fs.readFileSync(mkPath).toString().replace('cocos2djs_shared', 'cocos2djs');
fs.writeFileSync(mkPath, mkText);
  1. 修改项目的build.gradle

本步骤主要是把复制语句的 “${outputDir}/xxx” 替换成 “outputDir.dir(xxx)”

1
2
3
4
5
6
7
8
9
10
11
const abgPath = path.join(androidDir, 'app', 'build.gradle');
let gradleData = fs.readFileSync(abgPath).toString();
let matcher = /\"\$\{outputDir\}\/[a-zA-Z-]+\"/g;
let matchs = gradleData.match(matcher);
if (null != matchs) {
for (let i = 0; i < matchs.length; i++) {
let newStr = `outputDir.dir("${matchs[i].substring(matchs[i].indexOf('/') + 1, matchs[i].length - 1)}")`;
gradleData = gradleData.replace(/\"\$\{outputDir\}\/[a-zA-Z-]+\"/, newStr);
}
}
fs.writeFileSync(abgPath, gradleData);

修改项目的build.gradle

1
2
3
4
5
6
7
8
9
const abgPath = path.join(androidDir, 'app', 'build.gradle');
let gradleData = fs.readFileSync(abgPath).toString();
let matcher1 = /applicationId[ ]+\"[a-zA-Z0-9_.]+\"/;
gradleData = gradleData.replace(matcher1, `applicationId "${configData.packageName}"`);
let matcher2 = /versionCode[ ]+[0-9]+/;
gradleData = gradleData.replace(matcher2, `versionCode ${configData.appCode}`);
let matcher3 = /versionName[ ]+\"[0-9.]+\"/;
gradleData = gradleData.replace(matcher3, `versionName "${configData.appVer}"`);
fs.writeFileSync(abgPath, gradleData);
  • 包括多渠道打包、文件修改,都需要在这里修改

执行安卓打包

1
execFileSync('./gradlew', [':' + configData.title + ':assembleRelease'], { cwd: androidDir });
  • cwd 表示当前路径切换到指定目录下执行命令。
  • 这一步可能花费时间较久,如果想看实时状态,可以换用swpan
1
2
3
4
5
6
7
8
9
10
11
12
13
// const gradlewSpawn = spawn('./gradlew', [':' + configData.title + ':assembleRelease'], { cwd: androidDir });
// gradlewSpawn.stdout.on('data', function (chunk) {
// console.log(chunk.toString());
// });
// gradlewSpawn.stderr.on('data', (data) => {
// console.log(data);
// });
// gradlewSpawn.on('close', function (code) {
// console.log('close code : ' + code);
// })
// gradlewSpawn.on('exit', (code) => {
// console.log('exit code : ' + code);
// });

运行

至此,我们整个技术就实现了,然后我们用自动化工具,调用app.js并传入项目路径即可,参考提供的示例,可以直接使用批处理或终端,运行如下命令测试。

1
node ./AutoPack/app.js -p ./PackTest

已知问题

  1. 受限于CocosCreator必须配合编辑器,目前不支持linxu平台。
  2. 构建IOS暂未实现。
  3. 构建安卓时,gradle不能和AndroidStudio共用(mac上是如此),需要自行配置全局目录,或者直接复制一份。

以上,基本无难点,但各种细节较多,比如各类插件、正则使用等,需要花点时间推敲。为减少各位操作步骤,公众号直接回复AutoPack即可获取完整代码,如果遇到什么问题,可以加我微信。