const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const setting = require('./build-setting'); const utils = require('./libs/utils'); const assetsObfuscator = require('./assets-obfuscator'); module.exports = (function (){ var _instance; function AssetsHotUpdate (params) { return { _ready: false, _src: null, _dest: null, _manifest: { packageUrl: 'http://localhost/hot-update/', remoteManifestUrl: 'http://localhost/hot-update/project.manifest', remoteVersionUrl: 'http://localhost/hot-update/version.manifest', version: '300001', assets: {}, searchPaths: [] }, init (actualPlatform) { this._ready = false; let assetsHotUpdate = `assetsHotUpdate`; let update = setting.getInstance().getItem(assetsHotUpdate); if (!update) { Editor.warn('缺少配置,跳过资源热更!'); return false; } if (!update.enable) { Editor.warn('未开启资源热更!'); return false; } let platform = update.platform || ''; // 平台配置 let supportList = platform.split(','); if (!supportList.includes(actualPlatform)) { Editor.warn(actualPlatform, '平台不支持资源热更'); return false; } let src = update.src; let dest = update.dest; let appVersion = update.appVersion; // 指定的代码版本号 let hxCode = update.hxCode; // 指定的混淆码 let resVersion = update.resVersion; // 指定的代码资源号 let remoteUrl = update.remoteUrl; if (!src || !dest || !remoteUrl) { Editor.error('资源热更配置不满足'); return false; } let cfg = setting.getInstance().getVersionConfig() || {}; // 如果没有指定代码版本号就使用 VersionConfig.js 的代码版本号 if (!resVersion || resVersion.length === 0) { appVersion = String(cfg.APP_VERSION) || ''; } // 如果没有指定混淆码就使用 VersionConfig.js 的混淆码 if (!hxCode || hxCode.length === 0) { hxCode = String(cfg.HX_CODE) || ''; } // 如果没有指定代码资源号就使用 VersionConfig.js 的代码资源号 if (!resVersion || resVersion.length === 0) { resVersion = String(cfg.RES_VERSION) || ''; } this.appVersion = appVersion; this.hxCode = hxCode; this.resVersion = resVersion; Editor.log('代码版本号: ' + appVersion + '; 混淆Code: ' + hxCode + '; 代码资源号: ' + resVersion); this._src = path.join(Editor.Project.path, src); this._dest = path.join(Editor.Project.path, dest); if (fs.existsSync(this._dest)) { utils.rmdirSync(this._dest); } fs.mkdirSync(this._dest); this._manifest.version = resVersion; this._manifest.packageUrl = remoteUrl; this._manifest.remoteManifestUrl = remoteUrl + '/project.manifest'; this._manifest.remoteVersionUrl = remoteUrl + '/version.manifest'; this._manifest.assets = {}; this._manifest.searchPaths = []; // 获取UUID let assetsdb = this.getAssetsdbPath(this._src); let projectManifestPath = this._src + '/project.manifest'; utils.accessPath(projectManifestPath, (err)=> { if (err && err.code === 'ENOENT') { let assetsdbProjectManifestPath = assetsdb + '/project.manifest'; utils.createProjectManifestAssets(assetsdbProjectManifestPath, remoteUrl, (err, results) => { if (err) { Editor.error(err); return false; } let result = results[0]; this._projectManifestUUID = result.uuid; }); } else { this._projectManifestUUID = Editor.assetdb._path2uuid[projectManifestPath]; } }); let versionManifestPath = this._src + '/version.manifest'; utils.accessPath(versionManifestPath, (err)=> { let assetsdbVersionManifestPath = assetsdb + '/version.manifest'; if (err && err.code === 'ENOENT') { utils.createVersionManifestAssets(assetsdbVersionManifestPath, remoteUrl, (err, results) => { if (err) { Editor.error(err); return false; } let result = results[0]; this._versionManifestUUID = result.uuid; }); } else { this._versionManifestUUID = Editor.assetdb._path2uuid[versionManifestPath]; } }); this._ignoreFileUuids = []; let ignores = update.ignores; if (ignores) { for (const aPath of ignores) { let fullpath = path.join(Editor.Project.path, aPath); let stat = fs.statSync(fullpath); if (stat.isDirectory()) { utils.findFileSync(fullpath, (filePath)=> { this.addIgnoreFile(filePath); }); } else if (stat.isFile()) { this.addIgnoreFile(fullpath); } } } this._ready = true; return true; }, isReady () { return this._ready; }, addIgnoreFile (filePath) { let index = filePath.lastIndexOf('.'); let ext = filePath.substr(index + 1); if (ext !== 'meta') { let uuid = Editor.assetdb._path2uuid[filePath]; this._ignoreFileUuids.push(uuid); } }, getAssetsdbPath (root) { let assetsdb = 'db://'; let assetsIdx = root.indexOf('assets'); if (assetsIdx !== -1) { assetsdb += root.substr(assetsIdx, root.length); } else { assetsdb += '/assets'; } return assetsdb; }, addHotUpdateSearchPaths (buildDest) { var root = path.normalize(buildDest); var url = path.join(root, 'main.js'); let data = fs.readFileSync(url, 'utf8'); if (data) { try { var newStr = ' // 编译助手追加代码 开始\n' + ' var hotUpdateSearchPaths = jsb.fileUtils.getWritablePath() + \'remote-assets\';\n' + ' var curVersionName = \'' + this.appVersion + '\';\n' + ' var curHxCode = \'' + this.hxCode + '\';\n' + ' var oldVersionName = cc.sys.localStorage.getItem(\'HOT_UPDATE_VERSION_NAME\');\n' + ' var oldHxCode = cc.sys.localStorage.getItem(\'HOT_UPDATE_HX_CODE\');\n' + ' if (curVersionName != oldVersionName || curHxCode != oldHxCode) {\n' + ' jsb.fileUtils.removeDirectory(hotUpdateSearchPaths);\n' + ' }\n' + ' var searchPaths = jsb.fileUtils.getSearchPaths();\n' + ' searchPaths.unshift(hotUpdateSearchPaths);\n' + ' jsb.fileUtils.setSearchPaths(searchPaths);\n' + ' // 编译助手追加代码 结束\n\n' + ' window.boot();'; newStr = data.replace(' window.boot();', newStr); fs.writeFileSync(url, newStr); Editor.log('热更搜索路径写入' + url + '成功'); } catch (error) { Editor.error('热更搜索路径成功写入' + url + '失败'); } } else { Editor.error('读取' + url + '失败'); } }, getAssetsBuiledUrl (root, uuid) { if (!root || !uuid) { return; } let url; let dirname = uuid.substr(0, 2); let dirPath = path.join(root, 'res/raw-assets', dirname); let files = fs.readdirSync(dirPath); for (const fileName of files) { if (fileName.includes(uuid)) { url = path.join(dirPath, fileName); break; } } return url; }, readBuildDest (root, dir, obj) { let stat = fs.statSync(dir); if (!stat.isDirectory()) { return; } let subpaths = fs.readdirSync(dir), subpath, size, md5, compressed, relative; for (let i = 0; i < subpaths.length; ++i) { if (subpaths[i][0] === '.') { continue; } subpath = path.join(dir, subpaths[i]); stat = fs.statSync(subpath); if (stat.isDirectory()) { this.readBuildDest(root, subpath, obj); } else if (stat.isFile()) { let hasIgnore = false; for (const uuid of this._ignoreFileUuids) { let include = subpath.includes(uuid); if (include) { hasIgnore = true; break; } } if (hasIgnore) { continue; } // Size in Bytes size = stat['size']; md5 = crypto.createHash('md5').update(fs.readFileSync(subpath)).digest('hex'); compressed = path.extname(subpath).toLowerCase() === '.zip'; relative = path.relative(root, subpath); relative = relative.replace(/\\/g, '/'); relative = encodeURI(relative); obj[relative] = { 'size': size, 'md5': md5 }; if (compressed) { obj[relative].compressed = true; } } } }, generateManifest (buildDest, callback) { let projectManifestUUID = this._projectManifestUUID; let versionManifestUUID = this._versionManifestUUID; if (assetsObfuscator.getInstance().isReady()) { let mima = assetsObfuscator.getInstance().getMima(); if (this._projectManifestUUID) { projectManifestUUID = assetsObfuscator.getInstance().getNewName(mima, this._projectManifestUUID); } if (this._versionManifestUUID) { Editor.log('this._versionManifestUUID', this._versionManifestUUID); versionManifestUUID = assetsObfuscator.getInstance().getNewName(mima, this._versionManifestUUID); Editor.log('versionManifestUUID', versionManifestUUID); } } let projectManifestURL = this.getAssetsBuiledUrl(buildDest, projectManifestUUID); if (!projectManifestURL) { Editor.error('生成project.manifest失败'); return; } let versionManifestURL = this.getAssetsBuiledUrl(buildDest, versionManifestUUID); if (!versionManifestURL) { Editor.error('生成version.manifest失败'); return; } let destProjectManifestURL = path.join(this._dest, 'project.manifest'); let destVersionManifestURL = path.join(this._dest, 'version.manifest'); this.readBuildDest(buildDest, path.join(buildDest, 'src'), this._manifest.assets); this.readBuildDest(buildDest, path.join(buildDest, 'res'), this._manifest.assets); try { fs.writeFileSync(projectManifestURL, JSON.stringify(this._manifest)); fs.writeFileSync(destProjectManifestURL, JSON.stringify(this._manifest)); Editor.log('写入project.manifest成功', projectManifestURL); let md5 = crypto.createHash('md5').update(fs.readFileSync(destProjectManifestURL)).digest('hex'); Editor.warn('project.manifest md5 = ', md5); delete this._manifest.assets; delete this._manifest.searchPaths; fs.writeFileSync(versionManifestURL, JSON.stringify(this._manifest)); fs.writeFileSync(destVersionManifestURL, JSON.stringify(this._manifest)); Editor.log('写入version.manifest成功', versionManifestURL); callback(); } catch (error) { callback(error); } }, copyAssets (buildDest) { // fs.copyFileSync(path.join(buildDest, 'main.js'), path.join(this._dest, 'main.js')); // fs.copyFileSync(path.join(buildDest, 'project.json'), path.join(this._dest, 'project.json')); // utils.copyFolder(path.join(buildDest, 'jsb-adapter'), path.join(this._dest, 'jsb-adapter')); utils.copyFolder(path.join(buildDest, 'src'), path.join(this._dest, 'src')); utils.copyFolder(path.join(buildDest, 'res'), path.join(this._dest, 'res')); // let debuggingPath = path.join(buildDest, 'js backups (useful for debugging)'); // if (fs.existsSync(debuggingPath)) { // utils.copyFolder(debuggingPath, path.join(this._dest, 'js backups (useful for debugging)')); // } } }; } return { getInstance (params) { if (!_instance){ _instance = AssetsHotUpdate(params); } return _instance; } }; })();