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

解决什么问题

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

技术方案

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

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

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

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

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

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

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

具体实现

配置模板

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

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

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

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

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

构建命令

 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。

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

const { execSync } = require("child_process");
function xxx(cmd){
    execSync(cmd);
}

处理构建结果

web构建

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

原生构建

  1. 备份代码

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

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

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

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

    // 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

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

原生打包

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

制作icon

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

    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项目,所以删除

    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,所以我们需要修改下:

    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
    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
    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
    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)”

    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

    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);
  • 包括多渠道打包、文件修改,都需要在这里修改

执行安卓打包

 execFileSync('./gradlew', [':' + configData.title + ':assembleRelease'], { cwd: androidDir });
  • cwd 表示当前路径切换到指定目录下执行命令。
  • 这一步可能花费时间较久,如果想看实时状态,可以换用swpan
    // 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并传入项目路径即可,参考提供的示例,可以直接使用批处理或终端,运行如下命令测试。

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

已知问题

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

以上,基本无难点,但各种细节较多,比如各类插件、正则使用等,需要花点时间推敲。如果遇到什么问题,可以关注我的微信公众号,如需该方案源码,可以回复AutoPack获取。
wx_gh

9赞

这个看起来也不错,我是直接在Jenkins里使用命令行+Node.js。

本质是一样的,这个只是提供了入口文件,剩下的,是想用自动化构建工具、后台触发还是打成exe等,甚至就行用批处理都可以

期待在 github 看到…文件传的容易搞丢,等段时间用的时候就找不到文件了

github 和 gitee 都有啊~

这个方案挺不错的

IOS构建不香吗? 一套代码 android 和 ios 都可以使用;

因为ios不是主要业务,不紧急

嗯,主要ios构建不用向android那样 需要Android的全家桶 :grinning: