mirror of
https://github.com/cocos/cocos-engine.git
synced 2026-09-06 07:38:54 +08:00
616 lines
21 KiB
TypeScript
616 lines
21 KiB
TypeScript
import * as ps from 'path';
|
|
import * as fs from 'fs-extra';
|
|
import { execSync, spawn } from 'child_process';
|
|
import * as os from 'os';
|
|
import { CocosParams } from './base/default';
|
|
const iconv = require('iconv-lite');
|
|
|
|
|
|
// 因为加密后有多个后缀
|
|
export const EXT_LIST = ['.js', '.ccc', '.ccd', '.jsg', '.jsc'];
|
|
|
|
export class cchelper {
|
|
|
|
static replaceEnvVariables(str: string): string {
|
|
return str.replace(/\$\{([^}]*)\}/g, (_, n) => process.env[n] === undefined ? _ : process.env[n]!)
|
|
.replace(/(~)/g, (_, n) => process.env.HOME!);
|
|
}
|
|
|
|
|
|
static fixPath(p: string): string {
|
|
p = this.replaceEnvVariables(p);
|
|
if (os.platform() === 'win32') {
|
|
// 目前未限制空格,无需报错
|
|
// if (p.indexOf(' ') >= 0) {
|
|
// console.error(`space found in path "${p}"`);
|
|
// }
|
|
return p.replace(/\\/g, '/').replace(/\/+/, '/');
|
|
}
|
|
return p;
|
|
}
|
|
|
|
static async delay(ms: number) {
|
|
return new Promise<void>((resolve, reject) => {
|
|
setTimeout(async () => {
|
|
resolve();
|
|
}, ms);
|
|
});
|
|
}
|
|
|
|
static join(p1: string, ...p2: string[]): string {
|
|
|
|
const l = p2.map((x) => this.replaceEnvVariables(x));
|
|
if (ps.isAbsolute(l[l.length - 1])) { return l[l.length - 1]; }
|
|
return ps.join(this.replaceEnvVariables(p1), ...p2);
|
|
}
|
|
|
|
static copyFileSync(srcRoot: string, srcFile: string, dstRoot: string, dstFile: string) {
|
|
|
|
// console.log(`copyFileSync args: ${JSON.stringify(arguments)}`);
|
|
srcRoot = this.replaceEnvVariables(srcRoot);
|
|
srcFile = this.replaceEnvVariables(srcFile);
|
|
dstRoot = this.replaceEnvVariables(dstRoot);
|
|
dstFile = this.replaceEnvVariables(dstFile);
|
|
const src = ps.isAbsolute(srcFile) ? srcFile : ps.join(srcRoot, srcFile);
|
|
const dst = ps.isAbsolute(dstFile) ? dstFile : ps.join(dstRoot, dstFile);
|
|
// console.error(`copyFileSync ${src} -> ${dst}`);
|
|
this.makeDirectoryRecursive(ps.dirname(dst));
|
|
fs.copyFileSync(src, dst);
|
|
}
|
|
|
|
static async copyFileAsync(src: string, dst: string) {
|
|
// console.log(`[async] copyFile ${src} -> ${dst}`);
|
|
this.makeDirectoryRecursive(ps.parse(dst).dir);
|
|
await fs.copyFile(src, dst);
|
|
}
|
|
|
|
static async copyRecursiveAsync(srcDir: string, dst: string) {
|
|
srcDir = this.replaceEnvVariables(srcDir);
|
|
dst = this.replaceEnvVariables(dst);
|
|
|
|
const tasks: Promise<any>[] = [];
|
|
const srcStat = await fs.stat(srcDir);
|
|
|
|
if (!srcStat) {
|
|
console.error(`failed to stat ${srcDir}`);
|
|
return;
|
|
}
|
|
if (srcStat.isDirectory()) {
|
|
this.makeDirectoryRecursive(dst);
|
|
const files = await fs.readdir(srcDir);
|
|
for (const f of files) {
|
|
if (f === '.' || f === '..') { continue; }
|
|
const fp = ps.join(srcDir, f);
|
|
const tsk = this.copyRecursiveAsync(fp, ps.join(dst, f));
|
|
tasks.push(tsk);
|
|
}
|
|
await Promise.all(tasks);
|
|
} else if (srcStat.isFile()) {
|
|
try {
|
|
await this.copyFileAsync(srcDir, dst);
|
|
} catch (e) {
|
|
await this.delay(10);
|
|
// console.log(`error: retry copying ${srcDir} -> to ${dst} ... ${e}`);
|
|
await this.copyFileAsync(srcDir, dst);
|
|
}
|
|
}
|
|
}
|
|
|
|
static prepareDirsForFiles(srcRoot: string, files: string[], dstDir: string) {
|
|
const tree: any = {};
|
|
for (const f of files) {
|
|
const parts = f.split('/');
|
|
let p = tree;
|
|
for (const i of parts) {
|
|
if (i in p) {
|
|
p = p[i];
|
|
} else {
|
|
p = p[i] = {};
|
|
}
|
|
}
|
|
}
|
|
|
|
const mkdirs = (srcDir: string, attrs: any, dstDir: string) => {
|
|
const srcStat = fs.statSync(srcDir);
|
|
if (!srcStat.isDirectory()) { return; }
|
|
if (!fs.existsSync(dstDir)) {
|
|
// console.log(`prepereDir ${dstDir}`);
|
|
fs.mkdirSync(dstDir);
|
|
}
|
|
for (const i in attrs) {
|
|
if (i !== '.' && i !== '..') {
|
|
mkdirs(ps.join(srcDir, i), attrs[i], ps.join(dstDir, i));
|
|
}
|
|
}
|
|
};
|
|
|
|
mkdirs(srcRoot, tree, dstDir);
|
|
|
|
}
|
|
|
|
static parallelCopyFiles(par: number, srcRoot: string, files: string[], dstDir: string) {
|
|
let runningTasks = 0;
|
|
dstDir = this.replaceEnvVariables(dstDir);
|
|
cchelper.prepareDirsForFiles(srcRoot, files, dstDir);
|
|
return new Promise<void>((resolve, reject) => {
|
|
const copyAsync = async (src: string, dst: string) => {
|
|
runningTasks += 1;
|
|
await this.copyRecursiveAsync(src, dst);
|
|
runningTasks -= 1;
|
|
scheduleCopy();
|
|
};
|
|
const scheduleCopy = () => {
|
|
if (files.length > 0 && runningTasks < par) {
|
|
const f = files.shift()!;
|
|
const srcFile = ps.join(srcRoot, f);
|
|
if (fs.existsSync(srcFile)) {
|
|
copyAsync(srcFile, ps.join(dstDir, f));
|
|
} else {
|
|
console.log(`warning: copyFile: ${srcFile} not exists!`);
|
|
}
|
|
}
|
|
if (files.length === 0 && runningTasks === 0) {
|
|
resolve();
|
|
}
|
|
};
|
|
for (let i = 0; i < par; i++) { scheduleCopy(); }
|
|
});
|
|
}
|
|
|
|
static makeDirectoryRecursive(dir: string) {
|
|
if (dir.length === 0) { return; }
|
|
const dirs: string[] = [];
|
|
let p = dir;
|
|
while (!fs.existsSync(p)) {
|
|
dirs.push(p);
|
|
p = ps.join(p, '..');
|
|
}
|
|
while (dirs.length > 0) {
|
|
fs.mkdirSync(dirs[dirs.length - 1]);
|
|
dirs.length = dirs.length - 1;
|
|
}
|
|
}
|
|
|
|
static async removeDirectoryRecursive(dir: string) {
|
|
const stat = await fs.stat(dir);
|
|
if (stat.isFile()) {
|
|
await fs.unlink(dir);
|
|
} else if (stat.isDirectory()) {
|
|
const list = await fs.readdir(dir);
|
|
const tasks: Promise<any>[] = [];
|
|
for (const f of list) {
|
|
if (f === '.' || f === '..') { continue; }
|
|
const fp = ps.join(dir, f);
|
|
tasks.push(this.removeDirectoryRecursive(fp));
|
|
}
|
|
await Promise.all(tasks);
|
|
await fs.rmdir(dir);
|
|
}
|
|
}
|
|
|
|
static async copyFilesWithConfig(cfg: { from: string, to: string, include?: string[], exclude?: string[] }, srcRoot: string, dstRoot: string) {
|
|
|
|
if (!fs.existsSync(srcRoot)) {
|
|
console.error(`copy file srcRoot ${srcRoot} is not exists!`);
|
|
return;
|
|
}
|
|
|
|
|
|
srcRoot = this.replaceEnvVariables(srcRoot);
|
|
dstRoot = this.replaceEnvVariables(dstRoot);
|
|
let from = this.replaceEnvVariables(cfg.from);
|
|
let to = this.replaceEnvVariables(cfg.to);
|
|
if (ps.isAbsolute(from)) {
|
|
srcRoot = from;
|
|
from = '.';
|
|
}
|
|
if (ps.isAbsolute(to)) {
|
|
dstRoot = to;
|
|
to = '.';
|
|
}
|
|
|
|
|
|
// console.log(`copy ${JSON.stringify(cfg)}, ${from} -> ${to} from ${srcRoot} -> ${dstRoot}`);
|
|
|
|
const buildPrefixTree = (list0: string[]) => {
|
|
const tree: any = {};
|
|
const list = list0.map((x) => Array.from(x));
|
|
while (list.length > 0) {
|
|
const t = list.shift()!;
|
|
let p = tree;
|
|
while (t.length > 0) {
|
|
const c = t.shift()!;
|
|
if (!(c in p)) {
|
|
p[c] = {};
|
|
}
|
|
p = p[c];
|
|
}
|
|
}
|
|
return tree;
|
|
};
|
|
|
|
const matchPrefixTree = (str: string, tree: any): boolean => {
|
|
if (tree === null) {
|
|
return false;
|
|
}
|
|
const arr = Array.from(str);
|
|
let i = 0;
|
|
let p = tree;
|
|
while (arr[i] in p) {
|
|
p = p[arr[i]];
|
|
i++;
|
|
}
|
|
return i === arr.length && Object.keys(p).length === 0;
|
|
};
|
|
|
|
const includePrefix = cfg.include ? buildPrefixTree(cfg.include) : null;
|
|
const excludePrefix = cfg.exclude ? buildPrefixTree(cfg.exclude) : null;
|
|
|
|
const cpRAsync = async (srcRoot: string, srcDir: string, dstRoot: string) => {
|
|
const currFullDir = ps.join(srcRoot, srcDir);
|
|
const stat = await fs.stat(currFullDir);
|
|
if (stat.isDirectory()) {
|
|
const files = await fs.readdir(currFullDir);
|
|
const subCopies: Promise<any>[] = [];
|
|
for (const f of files) {
|
|
if (f === '.' || f === '..') { continue; }
|
|
const pathInSrcRoot = ps.join(srcDir, f);
|
|
if (excludePrefix && matchPrefixTree(pathInSrcRoot, excludePrefix)) {
|
|
if (includePrefix && matchPrefixTree(pathInSrcRoot, includePrefix)) {
|
|
// include
|
|
} else {
|
|
console.log(` - skip copy ${srcRoot} ${pathInSrcRoot} to ${dstRoot}`);
|
|
continue;
|
|
}
|
|
}
|
|
subCopies.push(cpRAsync(srcRoot, pathInSrcRoot, dstRoot));
|
|
}
|
|
await Promise.all(subCopies);
|
|
} else if (stat.isFile()) {
|
|
// let dstFileAbs = ps.isAbsolute(srcDir) ? srcDir : ps.join(dstRoot, srcDir);
|
|
await this.copyFileAsync(currFullDir, ps.join(dstRoot, srcDir));
|
|
}
|
|
};
|
|
|
|
const copyFrom = this.replaceEnvVariables(ps.normalize(ps.join(srcRoot, from)));
|
|
const copyTo = this.replaceEnvVariables(ps.normalize(ps.join(dstRoot, to)));
|
|
await cpRAsync(srcRoot, from, copyTo);
|
|
}
|
|
|
|
static async replaceInFile(patterns: { reg: string | RegExp, text: string }[], filepath: string) {
|
|
filepath = this.replaceEnvVariables(filepath);
|
|
if (!fs.existsSync(filepath)) {
|
|
console.log(`While replace template content, file ${filepath}`);
|
|
return;
|
|
}
|
|
// console.log(`replace ${filepath} with ${JSON.stringify(patterns)}`);
|
|
const lines = (await fs.readFile(filepath)).toString('utf8').split('\n');
|
|
|
|
const newContent = lines.map((l) => {
|
|
patterns.forEach((p) => {
|
|
l = l.replace(new RegExp(p.reg), this.replaceEnvVariables(p.text));
|
|
});
|
|
return l;
|
|
}).join('\n');
|
|
|
|
await fs.writeFile(filepath, newContent);
|
|
}
|
|
|
|
|
|
static exactValueFromFile(regexp: RegExp, filename: string, idx: number): string | undefined {
|
|
if (!(fs.existsSync(filename))) {
|
|
console.error(`file ${filename} not exist!`);
|
|
return;
|
|
}
|
|
const lines = fs.readFileSync(filename).toString('utf-8').split('\n');
|
|
for (const l of lines) {
|
|
const r = l.match(regexp);
|
|
if (r) {
|
|
return r[idx];
|
|
}
|
|
}
|
|
}
|
|
|
|
static async runCmd(cmd: string, args: string[], slient: boolean, cwd?: string) {
|
|
return new Promise<void>((resolve, reject) => {
|
|
console.log(`[runCmd]: ${cmd} ${args.join(' ')}`);
|
|
const cp = spawn(cmd, args, {
|
|
shell: true,
|
|
env: process.env,
|
|
cwd: cwd! || process.cwd(),
|
|
});
|
|
if (!slient) {
|
|
cp.stdout.on(`data`, (chunk) => {
|
|
console.log(`[runCmd ${cmd}] ${chunk}`);
|
|
});
|
|
cp.stderr.on(`data`, (chunk) => {
|
|
console.log(`[runCmd ${cmd} - error] ${chunk}`);
|
|
});
|
|
}
|
|
cp.on('exit', (code, signal) => {
|
|
if (code !== 0 && !slient) {
|
|
reject(`failed to exec ${cmd} ${args.join(' ')}`);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
cp.on('error', (err: Error) => {
|
|
reject(err);
|
|
});
|
|
cp.on('close', (code, signal) => {
|
|
if (code !== 0 && !slient) {
|
|
reject(`failed to exec ${cmd} ${args.join(' ')}`);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
static existsSync(filePath: string): boolean {
|
|
const extName = ps.extname(filePath);
|
|
const filePathNotExt = ps.basename(filePath, extName);
|
|
filePath = ps.join(ps.dirname(filePath), filePathNotExt);
|
|
|
|
return !!EXT_LIST.find((ext) => {
|
|
return fs.existsSync(filePath + ext);
|
|
});
|
|
}
|
|
|
|
static checkJavaHome(): boolean {
|
|
if (!process.env.JAVA_HOME) {
|
|
console.log('warning: $JAVA_HOME is not set!');
|
|
}
|
|
const javaPath = cchelper.which('java');
|
|
if (!javaPath) {
|
|
console.error(`'java' is not found in PATH`);
|
|
} else {
|
|
try {
|
|
const version = execSync(`"${cchelper.fixPath(javaPath)}" -version`).toString();
|
|
if (/Java\(TM\)/.test(version)) {
|
|
return true;
|
|
} else {
|
|
console.error(`Oracle JDK is expected.`);
|
|
}
|
|
} catch (e) {
|
|
console.error(`Error checking java runtime...`);
|
|
console.error(e);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static accessSync(file: string, mode: number): boolean {
|
|
try {
|
|
fs.accessSync(file, mode);
|
|
return true;
|
|
} catch (e) { }
|
|
return false;
|
|
}
|
|
|
|
static which(executable: string): null | string {
|
|
// possible executable names
|
|
const execs = [executable];
|
|
const IS_WINDOWS = os.platform() === 'win32';
|
|
if (IS_WINDOWS) {
|
|
execs.push(executable + '.exe');
|
|
}
|
|
// seprate PATH environment variable
|
|
const pathList = IS_WINDOWS ? process.env.PATH?.split(';') : process.env.PATH?.split(':');
|
|
if (!pathList || pathList.length === 0) {
|
|
return null;
|
|
}
|
|
// search for executable in each PATH segment
|
|
for (const dir of pathList) {
|
|
for (const execName of execs) {
|
|
const testFile = ps.join(dir, execName);
|
|
if (fs.existsSync(testFile)) {
|
|
if (IS_WINDOWS || cchelper.accessSync(testFile, fs.constants.X_OK)) {
|
|
return testFile;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export const toolHelper = {
|
|
getXcodeMajorVerion():number {
|
|
try {
|
|
const output = execSync('xcrun xcodebuild -version').toString('utf8');
|
|
return Number.parseInt(output.match(/Xcode\s(\d+)\.\d+/)![1]);
|
|
} catch (e) {
|
|
console.error(e);
|
|
// fallback to default Xcode version
|
|
return 11;
|
|
}
|
|
},
|
|
|
|
async runCommand(cmd:string, args:string[], cb?:(code:number, stdout:string, stderr:string)=>void): Promise<boolean> {
|
|
return new Promise<boolean>((resolve, reject) => {
|
|
const cp = spawn(cmd, args);
|
|
const stdErr:Buffer[] = [];
|
|
const stdOut:Buffer[] = [];
|
|
cp.stderr.on('data', (d)=>stdErr.push(d));
|
|
cp.stdout.on('data', (d)=>stdOut.push(d));
|
|
cp.on('close', (code, signal)=>{
|
|
if(cb) {
|
|
cb(code as any, Buffer.concat(stdOut).toString('utf8'), Buffer.concat(stdErr).toString('utf8'));
|
|
}
|
|
resolve(code === 0);
|
|
});
|
|
});
|
|
},
|
|
|
|
runCmake(args: string[]) {
|
|
let cmakePath = Paths.cmakePath;
|
|
if (process.platform === 'win32' && cmakePath.indexOf(' ') > -1) {
|
|
cmakePath = `"${cmakePath}"`;
|
|
} else {
|
|
cmakePath = cmakePath.replace(/ /g, '\\ ');
|
|
}
|
|
// Delete environment variables start with `npm_`, which may cause compile error on windows
|
|
const newEnv: any = {};
|
|
Object.assign(newEnv, process.env);
|
|
Object.keys(newEnv).filter(x => x.toLowerCase().startsWith("npm_")).forEach(e => delete newEnv[e]);
|
|
|
|
return new Promise<void>((resolve, reject) => {
|
|
console.log(`run ${cmakePath} ${args.join(' ')}`);
|
|
const cp = spawn(cmakePath, args, {
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
env: newEnv,
|
|
shell: true,
|
|
});
|
|
cp.stdout.on('data', (data: any) => {
|
|
const msg = iconv.decode(data, 'gbk').toString();
|
|
if(/warning/i.test(msg)) {
|
|
console.log(`[cmake-warn] ${msg}`);
|
|
} else {
|
|
console.log(`[cmake] ${msg}`);
|
|
}
|
|
});
|
|
cp.stderr.on('data', (data: any) => {
|
|
const msg = iconv.decode(data, 'gbk').toString();
|
|
if(/CMake Warning/.test(msg) || /warning/i.test(msg)) {
|
|
console.log(`[cmake-warn] ${msg}`);
|
|
}else{
|
|
console.error(`[cmake-err] ${msg}`);
|
|
}
|
|
});
|
|
cp.on('close', (code: number, sig: any) => {
|
|
if (code !== 0) {
|
|
reject(new Error(`run cmake failed "cmake ${args.join(' ')}", code: ${code}, signal: ${sig}`));
|
|
return;
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
},
|
|
|
|
runXcodeBuild(args: string[]) {
|
|
// only runs on mac os, run with `xcodebuild` directly
|
|
// Delete environment variables start with `npm_`, which may cause compile error on windows
|
|
const newEnv: any = {};
|
|
Object.assign(newEnv, process.env);
|
|
Object.keys(newEnv).filter(x => x.toLowerCase().startsWith('npm_')).forEach(e => delete newEnv[e]);
|
|
|
|
return new Promise<void>((resolve, reject) => {
|
|
console.log(`run xcodebuild with ${args.join(' ')}`);
|
|
const xcodebuildPath = cchelper.which('xcodebuild');
|
|
if (!xcodebuildPath) {
|
|
console.error(`'xcodebuild' is not in the path`);
|
|
} else {
|
|
console.log(`run xcodebuild with ${args.join(' ')}`);
|
|
const cp = spawn(xcodebuildPath, args, {
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
env: newEnv,
|
|
shell: true,
|
|
});
|
|
cp.stdout.on('data', (data: any) => {
|
|
console.log(`[xcodebuild] ${iconv.decode(data, 'gbk').toString()}`);
|
|
});
|
|
cp.stderr.on('data', (data: any) => {
|
|
console.error(`[xcodebuild] ${iconv.decode(data, 'gbk').toString()}`);
|
|
});
|
|
cp.on('close', (code: number, sig: any) => {
|
|
if (code !== 0) {
|
|
reject(new Error(`run xcodebuild failed "xcodebuild ${args.join(' ')}", code: ${code}, signal: ${sig}`));
|
|
return;
|
|
}
|
|
resolve();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
export class Paths {
|
|
public static enginePath: string; // [engine]
|
|
public static nativeRoot: string; // [engine-native]
|
|
public static projectDir: string; // [project]
|
|
public static cmakePath: string;
|
|
/**
|
|
* ios/mac/windows/android
|
|
*/
|
|
private platform: string;
|
|
/**
|
|
* ios/mac/win64/win32/android
|
|
*/
|
|
private platformTemplateDirName: string;
|
|
/**
|
|
* build/[platform]
|
|
*/
|
|
public buildDir: string;
|
|
|
|
/**
|
|
* build/[platform]/data
|
|
*/
|
|
public buildAssetsDir: string;
|
|
|
|
constructor(params: CocosParams<Object>) {
|
|
Paths.enginePath = params.enginePath;
|
|
Paths.projectDir = params.projDir;
|
|
Paths.nativeRoot = params.nativeEnginePath;
|
|
Paths.cmakePath = params.cmakePath;
|
|
this.platform = params.platform;
|
|
this.buildDir = params.buildDir;
|
|
this.buildAssetsDir = params.buildAssetsDir;
|
|
if (params.platform === 'windows') {
|
|
this.platformTemplateDirName = params.platformParams.targetPlatform === "win32" ? "win32" : "win64";
|
|
} else {
|
|
this.platformTemplateDirName = params.platformName ? params.platformName : this.platform;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* [project]/native/engine/common
|
|
*/
|
|
get commonDirInPrj() {
|
|
return ps.join(Paths.projectDir, 'native', 'engine', 'common');
|
|
}
|
|
|
|
/**
|
|
* [engine]/templates/common
|
|
*/
|
|
get commonDirInCocos() {
|
|
return ps.join(this.nativeTemplateDirInCocos, 'common');
|
|
}
|
|
|
|
/**
|
|
* [project]/native/engine
|
|
*/
|
|
get nativeTemplateDirInPrj() {
|
|
return ps.join(Paths.projectDir, 'native', 'engine');
|
|
}
|
|
|
|
/**
|
|
* [engine]/templates
|
|
*/
|
|
get nativeTemplateDirInCocos() {
|
|
return ps.join(Paths.enginePath, 'templates');
|
|
}
|
|
|
|
/**
|
|
* [project]/native/engine/[platformTemplateDirName]
|
|
*/
|
|
get platformTemplateDirInPrj() {
|
|
return ps.join(this.nativeTemplateDirInPrj, this.platformTemplateDirName);
|
|
}
|
|
|
|
/**
|
|
* [engine]/templates/[platformTemplateDirName]
|
|
*/
|
|
get platformTemplateDirInCocos() {
|
|
return ps.join(this.nativeTemplateDirInCocos, this.platform);
|
|
}
|
|
|
|
/**
|
|
* build/[platform]/proj
|
|
*/
|
|
get nativePrjDir() {
|
|
return ps.join(this.buildDir, 'proj');
|
|
}
|
|
}
|