Files
cocos-engine/scripts/native-pack-tool/source/base/default.ts

593 lines
20 KiB
TypeScript

import * as ps from 'path';
import * as fs from 'fs-extra';
import { cchelper, Paths } from "../utils";
import { CocosProjectTasks } from './cocosProjectTypes';
import { gzipSync } from 'zlib';
const globby = require('globby');
const xxtea = require('xxtea-node');
const PackageNewConfig = 'cocos-project-template.json';
export interface ICMakeConfig {
// 引擎模块
USE_AUDIO?: boolean;
USE_VIDEO?: boolean;
USE_WEBVIEW?: boolean;
// 任务调度系统配置,配置为布尔值的属性,会在生成时修改为 set(XXX ON) 的形式
USE_JOB_SYSTEM_TBB?: boolean;
USE_JOB_SYSTEM_TASKFLOW?: boolean;
// 是否勾选竖屏
USE_PORTRAIT?: boolean;
// 渲染后端
CC_USE_METAL?: boolean;
CC_USE_VUKAN?: boolean;
CC_USE_GLES3: boolean;
CC_USE_GLES2: boolean;
// 引擎路径
COCOS_X_PATH?: string;
// app名称
APP_NAME?: string;
// xxteakey
XXTEAKEY: string;
// // ios 和 mac 的bundle id设置
// MACOSX_BUNDLE_GUI_IDENTIFIER?: string;
// // ios 开发者
// DEVELOPMENT_TEAM?: string;
// TARGET_IOS_VERSION?: string;
// // mac
// TARGET_OSX_VERSION ?: string;
// // android
// CC_ENABLE_SWAPPY?: boolean;
// 其他属性
[propName: string]: any;
// 以服务器端模式运行
USE_SERVER_MODE: string;
}
export type InternaleNativePlatform = 'mac' | 'android' | 'windows' | 'ios' | 'ohos';
const ErrorCodeIncompatible = 15004;
export interface INativePlatformOptions {
extends?: InternaleNativePlatform, //传入继承的平台,将会继承已有平台注册的一些代码
overwrite?: InternaleNativePlatform, //传入继承但如果有同名的方法等会复写平台,将会继承已有平台注册的一些代码
create: () => Promise<boolean>;
genrate: () => Promise<boolean>;
make?: () => Promise<boolean>;
run?: () => Promise<boolean>;
init: (params: CocosParams<Object>) => void;
}
export abstract class NativePackTool {
// 传入的打包参数
params!: CocosParams<Object>;
// 收集初始化的一些路径信息
paths!: Paths;
// 存储调用 cmake 的命令行参数
cmakeArgs: string[] = [];
// 设置命令行调用时的环境参数
setEnv(key: string, value: any) {
process.env[key] = value;
}
init(params: CocosParams<Object>) {
this.params = new CocosParams(params);
this.paths = new Paths(params);
this.setEnv('NATIVE_DIR', this.paths.platformTemplateDirInPrj);
this.setEnv('COMMON_DIR', this.paths.commonDirInPrj);
this.setEnv('PROJECT_NAME', this.params.projectName);
}
protected parseVersion(content: string, key: string, def: number): number {
const regexp = new RegExp(`${key}=(.*)`);
const r = content.match(regexp);
if (!r) { return def; }
const v = Number.parseInt(r[1], 10);
return Number.isNaN(v) ? def : v;
}
protected async copyCommonTemplate() {
if (!fs.existsSync(ps.join(this.paths.commonDirInPrj, 'CMakeLists.txt'))) {
await fs.copy(this.paths.commonDirInCocos, this.paths.commonDirInPrj, { overwrite: false });
}
}
private get projEngineVersionPath() {
return ps.join(this.paths.commonDirInPrj, 'cocos-version.json');
}
private _debugInfo: any = null;
private get DebugInfos(): { [key: number]: string } {
if (!this._debugInfo) {
this._debugInfo = require(ps.join(Paths.enginePath, 'DebugInfos.json'));
if (!this._debugInfo) {
console.error(`Failed to load DebugInfos.json`);
}
}
return this._debugInfo;
}
private _versionParser: any = null;
private get versionParser() {
if (!this._versionParser) {
const scriptPath = ps.join(Paths.enginePath, 'native/cmake/scripts/plugin_support/plugin_cfg.js');
this._versionParser = require(scriptPath);
}
return this._versionParser;
}
/**
* Debug / Release
*/
protected get buildType(): string {
return this.params.debug ? "Debug" : "Release";
}
/**
* Read version number from cocos-version.json
*/
protected tryReadProjectTemplateVersion(): { version: string, skipCheck: boolean | undefined } | null {
const versionJsonPath = this.projEngineVersionPath;
if (!fs.existsSync(versionJsonPath)) {
console.log(`warning: ${versionJsonPath} not exists`);
return null;
}
try {
const content = fs.readJsonSync(versionJsonPath);
if (content.version === undefined) {
console.error(`Field 'version' missing in ${versionJsonPath}`);
return null;
}
return content;
} catch (e) {
console.error(`Failed to read json file ${versionJsonPath}`);
console.error(e);
}
return null;
}
/**
* Read package.json file in the root folder and return the version field.
*/
protected tryGetEngineVersion(): string | null {
const pkgJSON = ps.join(Paths.enginePath, 'package.json');
if (!fs.existsSync(pkgJSON)) {
console.error(`Failed to read file ${pkgJSON}`);
return null;
}
return fs.readJsonSync(pkgJSON).version || "3.6.0";
}
/**
* Version condition from compatibility-info.json for current platform.
*/
protected tryGetCompatibilityInfo(): string | null {
const compInfo = ps.join(Paths.enginePath, 'templates/compatibility-info.json');
if (!fs.existsSync(compInfo)) {
console.error(`${compInfo} does not exist`);
return null;
}
const json = fs.readJsonSync(compInfo);
if (!json.native) {
console.error(`${compInfo} does not contain "native" field`);
return null;
}
const native = json.native;
const defaultCfg = native.default;
if (!defaultCfg) {
console.error(`${compInfo} does not contain "native.default" field`);
return null;
}
const plt = this.params.platform;
if (!native[plt]) {
return defaultCfg;
}
return native[plt];
}
private commonDirAreIdentical(): boolean {
let commonSrc = this.paths.commonDirInCocos;
let commonDst = this.paths.commonDirInPrj;
const compFile = (src: string, dst: string): boolean => {
const linesSrc: string[] = fs.readFileSync(src).toString("utf8").split("\n").map((line) => line.trim());
const linesDst: string[] = fs.readFileSync(dst).toString("utf8").split("\n").map((line) => line.trim());
return linesSrc.length === linesDst.length && linesSrc.every((line, index) => line === linesDst[index]);
};
let compFiles = ["Classes/Game.h", "Classes/Game.cpp"];
for (let f of compFiles) {
const srcFile = ps.join(commonSrc, f);
const dstFile = ps.join(commonDst, f);
if (!fs.existsSync(dstFile)) {
return false;
}
if (!fs.existsSync(srcFile)) {
console.warn(`${f} not exists in ${commonSrc}`);
return false;
}
if (!compFile(srcFile, dstFile)) {
console.log(`File ${dstFile} differs from ${srcFile}`);
return false;
}
}
return true;
}
private skipVersionCheck = false;
/**
* The engine version used to generate the 'native/' folder should match the
* condition written in the 'compatibility-info.json' file.
*/
private validateTemplateVersion(): boolean {
console.log(`Checking template version...`);
const engineVersion = this.tryGetEngineVersion();
const projEngineVersionObj = this.tryReadProjectTemplateVersion();
if (projEngineVersionObj === null) {
if (this.commonDirAreIdentical()) {
console.log(`The files under common/Classes directory are identical with the ones in the template. Append version file to the project.`);
this.writeEngineVersion();
return true;
}
console.error(`Error code ${ErrorCodeIncompatible}, ${this.DebugInfos[ErrorCodeIncompatible]}`)
return false;
}
let versionRange = this.tryGetCompatibilityInfo();
const projEngineVersion = projEngineVersionObj?.version;
if (!versionRange) {
console.warn(`Ignore version range check`);
return true;
}
if (projEngineVersionObj.skipCheck === true) {
console.log(`Skip version range check by project`);
this.skipVersionCheck = true;
return true;
}
let cond = this.versionParser.parse(versionRange);
if (!cond) {
return true;
}
if (cond.match(projEngineVersion)) {
const newerThanEngineVersion = this.versionParser.parse(`>${engineVersion}`);
if (newerThanEngineVersion.match(projEngineVersion)) {
console.log(`warning: ${projEngineVersion} is newer than engine version ${engineVersion}`);
}
return true;
}
console.error(`'native/' folder was generated by ${projEngineVersion} which is incompatible with ${engineVersion}, condition: '${versionRange}'`);
console.error(`${this.DebugInfos[ErrorCodeIncompatible]}`)
return false;
}
/**
* Utility function to check if a file exists dst as in src.
*/
protected validateDirectory(src: string, dst: string, missingDirs: string[]) {
if (!fs.existsSync(dst)) {
missingDirs.push(dst);
return;
}
const st = fs.statSync(src);
if (!st.isDirectory()) {
return;
}
let list = fs.readdirSync(src);
for (let f of list) {
if (f.startsWith('.')) continue;
this.validateDirectory(ps.join(src, f), ps.join(dst, f), missingDirs);
}
}
/**
* Check files under `native/engine/platform` folder
*/
protected validatePlatformDirectory(missing: string[]): void {
console.log(`Validating platform source code directories...`);
const srcDir = ps.join(this.paths.nativeTemplateDirInCocos, this.params.platform);
const dstDir = this.paths.platformTemplateDirInPrj;
this.validateDirectory(srcDir, dstDir, missing)
}
/**
* Check if any file removed from the 'native/' folder
*/
private validateTemplateConsistency() {
console.log(`Validating template consistency...`);
let commonSrc = this.paths.commonDirInCocos;
let commonDst = this.paths.commonDirInPrj;
let missingDirs: string[] = [];
// validate common directory
this.validateDirectory(commonSrc, commonDst, missingDirs);
this.validatePlatformDirectory(missingDirs);
if (missingDirs.length > 0) {
console.log(`Following files are missing`);
for (let f of missingDirs) {
console.log(` ${f}`);
}
console.log(`Consider fix the problem or remove the directory`);
console.log(`To avoid this warning, set field \'skipCheck\' in cocos-version.json to true.`);
return false;
}
return true;
}
/**
* - Ensure the engine version used to generete 'native/' folder is compatible
* with the current engine version.
* - Check if any file under the 'native/' folder is removed.
*/
protected validateNativeDir() {
try {
if (this.validateTemplateVersion()) {
if (!this.skipVersionCheck && !this.validateTemplateConsistency()) {
console.log(`Failed to validate "native" directory`);
}
}
} catch (e) {
console.warn(`Failed to validate native directory`);
console.warn(e);
}
}
/**
* Write cocos-version.json into native/common/cocos-version.json
*/
protected writeEngineVersion() {
if (!fs.existsSync(this.projEngineVersionPath)) {
fs.writeJSON(this.projEngineVersionPath, {
version: this.tryGetEngineVersion(),
skipCheck: false,
});
}
}
protected async copyPlatformTemplate() {
if (!fs.existsSync(this.paths.platformTemplateDirInPrj)) {
// 拷贝 templates/平台/ 文件到 "native" 目录
await fs.copy(ps.join(this.paths.nativeTemplateDirInCocos, this.params.platform), this.paths.platformTemplateDirInPrj, { overwrite: false });
this.writeEngineVersion();
} else {
this.validateNativeDir();
}
}
protected async excuteTemplateTask(tasks: CocosProjectTasks) {
if (tasks.appendFile) {
await Promise.all(tasks.appendFile.map((task) => {
const dest = cchelper.replaceEnvVariables(task.to);
fs.ensureDirSync(ps.dirname(dest));
return fs.copy(ps.join(Paths.nativeRoot, task.from), dest);
}))
delete tasks.appendFile;
}
const replaceFilesDelay: { [key: string]: { reg: string, content: string }[] } = {};
if (tasks.projectReplaceProjectName) {
const cmd = tasks.projectReplaceProjectName;
cmd.files.forEach((file) => {
const fp = cchelper.join(this.paths.buildDir, file);
replaceFilesDelay[fp] = replaceFilesDelay[fp] || [];
replaceFilesDelay[fp].push({
reg: cmd.srcProjectName,
content: this.params.projectName,
});
});
delete tasks.projectReplaceProjectName;
}
if (tasks.projectReplacePackageName) {
const cmd = tasks.projectReplacePackageName;
const name = cmd.srcPackageName.replace(/\./g, '\\.');
cmd.files.forEach((file) => {
const fp = cchelper.join(this.paths.buildDir, file);
replaceFilesDelay[fp] = replaceFilesDelay[fp] || [];
replaceFilesDelay[fp].push({
reg: name,
content: this.params.platformParams.packageName!,
});
});
delete tasks.projectReplacePackageName;
}
for (const fullpath in replaceFilesDelay) {
const cfg = replaceFilesDelay[fullpath];
await cchelper.replaceInFile(cfg.map((x) => {
return { reg: x.reg, text: x.content };
}), fullpath);
}
if (Object.keys(tasks).length > 0) {
for (const f in tasks) {
console.error(`command "${f}" is not parsed in ${PackageNewConfig}`);
}
}
}
protected async generateCMakeConfig() {
// 添加一些 cmake 配置到 cfg.cmake
const file = ps.join(this.paths.nativePrjDir, 'cfg.cmake');
let content = '';
const config = this.params.cMakeConfig;
Object.keys(config).forEach((key: string) => {
// convert boolean to CMake option.
if (typeof config[key] === 'boolean') {
config[key] = `set(${key} ${config[key] ? 'ON' : 'OFF'})`;
}
});
Object.keys(config).forEach((key: string) => {
content += config[key] + '\n';
});
console.debug(`generateCMakeConfig, ${JSON.stringify(config)}`);
await fs.outputFile(file, content);
}
protected appendCmakeCommonArgs(args: string[]) {
args.push(`-DRES_DIR="${cchelper.fixPath(this.paths.buildDir)}"`);
args.push(`-DAPP_NAME="${this.params.projectName}"`);
args.push(`-DLAUNCH_TYPE="${this.buildType}"`);
if (this.params.platformParams?.skipUpdateXcodeProject) {
args.push(`-DCMAKE_SUPPRESS_REGENERATION=ON`);
}
}
/**
* 加密脚本,加密后,会修改 cmake 参数,因而需要再次执行 cmake 配置文件的生成
* @returns
*/
protected async encrypteScripts() {
if (!this.params.encrypted) {
return;
}
if (!this.params.xxteaKey) {
throw new Error('Encryption Key can not be empty');
}
console.debug('Start encrypte scripts...');
// native 加密步骤(1/3):生成完工程所有文件添加 cmake 配置
if (this.params.encrypted) {
this.params.cMakeConfig.XXTEAKEY = `set(XXTEAKEY "${this.params.xxteaKey}")`;
}
const backupPath = ps.join(this.paths.buildDir, 'script-backup');
fs.ensureDirSync(backupPath);
fs.emptyDirSync(backupPath);
const allBundleConfigs: string[] = await globby([
ps.join(this.paths.buildAssetsDir, 'assets/*/cc.config*.json'),
ps.join(this.paths.buildAssetsDir, 'remote/*/cc.config*.json'),
]);
for (const configPath of allBundleConfigs) {
const config = await fs.readJSON(configPath);
// native 加密步骤(2/3):加密的标志位,需要写入到 bundle 的 config.json 内运行时需要
const version = configPath.match(/\/cc.config(.*).json/)![1];
const scriptDest = ps.join(ps.dirname(configPath), `index${version}.js`);
let content: any = fs.readFileSync(scriptDest, 'utf8');
if (this.params.compressZip) {
content = gzipSync(content);
content = xxtea.encrypt(content, xxtea.toBytes(this.params.xxteaKey));
} else {
content = xxtea.encrypt(xxtea.toBytes(content), xxtea.toBytes(this.params.xxteaKey));
}
const newScriptDest = ps.join(ps.dirname(scriptDest), ps.basename(scriptDest, ps.extname(scriptDest)) + '.jsc');
fs.writeFileSync(newScriptDest, content);
config.encrypted = true;
fs.writeJSONSync(configPath, config);
fs.copySync(scriptDest, ps.join(backupPath, ps.relative(this.paths.buildAssetsDir, scriptDest)));
fs.removeSync(scriptDest);
}
await this.generateCMakeConfig();
console.debug('Encrypte scriptes success');
}
/**
* 解析、执行 cocos-template.json 模板任务
*/
protected async excuteCocosTemplateTask() {
const templatTaskMap: Record<string, CocosProjectTasks> = await fs.readJSON(ps.join(this.paths.nativeTemplateDirInCocos, PackageNewConfig));
for (const templatTask of Object.values(templatTaskMap)) {
await this.excuteTemplateTask(templatTask);
}
}
abstract create(): Promise<boolean>;
generate?(): Promise<boolean>;
make?(): Promise<boolean>;
run?(): Promise<boolean>;
}
// cocos.compile.json
export class CocosParams<T> {
platformParams!: T | any;
public debug: boolean;
public projectName: string;
public cmakePath: string;
public platform: string;
public platformName: string;
/**
* engine root
*/
public enginePath: string;
/**
* native engine root
*/
public nativeEnginePath: string;
/**
* project path
*/
public projDir: string;
/**
* build/[platform]
*/
public buildDir: string;
/**
* @zh 构建资源路径
* @en /build/[platform]/data
*/
public buildAssetsDir: string;
/**
* @zh 是否加密脚本
* @en is encrypted
*/
encrypted?: boolean;
/**
* @zh 是否压缩脚本
* @en is compress script
*/
compressZip?: boolean;
/**
* @zh 加密密钥
* @en encrypt Key
*/
xxteaKey?: string;
/**
* @zh 是否为模拟器
* @en is simulator
*/
simulator?: boolean;
public cMakeConfig: ICMakeConfig = {
CC_USE_GLES3: false,
CC_USE_GLES2: true,
USE_SERVER_MODE: 'set(USE_SERVER_MODE OFF)',
NET_MODE: 'set(NET_MODE 0)',
XXTEAKEY: '',
CC_ENABLE_SWAPPY: false,
}
constructor(params: CocosParams<Object>) {
this.buildAssetsDir = params.buildAssetsDir;
this.projectName = params.projectName;
this.debug = params.debug;
this.cmakePath = params.cmakePath;
this.platform = params.platform;
this.platformName = params.platformName;
this.enginePath = params.enginePath;
this.nativeEnginePath = params.nativeEnginePath;
this.projDir = params.projDir;
this.buildDir = params.buildDir;
this.xxteaKey = params.xxteaKey;
this.encrypted = params.encrypted;
Object.assign(this.cMakeConfig, params.cMakeConfig);
this.platformParams = params.platformParams;
}
}