UNPKG

@webos-tools/cli

Version:

Command Line Interface for development webOS application and service

1,142 lines (1,045 loc) 94.9 kB
/* * Copyright (c) 2020-2024 LG Electronics Inc. * * SPDX-License-Identifier: Apache-2.0 */ const ar = require('ar-async'), async = require('async'), chardet = require('chardet'), CombinedStream = require('combined-stream'), crypto = require('crypto'), decompress = require('decompress'), decompressTargz = require('decompress-targz'), ElfParser = require('elfy').Parser, encoding = require('encoding'), fs = require('fs'), fstream = require('fstream'), Validator = require('jsonschema').Validator, mkdirp = require('mkdirp'), log = require('npmlog'), path = require('path'), rimraf = require('rimraf'), shelljs = require('shelljs'), stripbom = require('strip-bom'), temp = require('temp'), uglify = require('terser'), util = require('util'), // zlib = require('zlib'), // tarFilterPack = require('./tar-filter-pack'), errHndl = require('./base/error-handler'), tar = require('tar'), commonTools = require('./base/common-tools'); const appdata = commonTools.appdata; (function() { log.heading = 'packager'; log.level = 'warn'; const servicePkgMethod = 'id', defaultAssetsFields = { "main": true, "icon": true, "largeIcon": true, "bgImage": true, "splashBackground": true, "imageForRecents": true, "sysAssetsBasePath": true }, FILE_TYPE = { file: 'file', dir: 'dir', symlink: 'symlink' }, packager = {}; let objectCounter = 0; if (typeof module !== 'undefined' && module.exports) { module.exports = packager; } function Packager() { this.objectId = objectCounter++; this.verbose = false; this.silent = true; this.noclean = false; this.nativecmd = false; this.minify = true; this.excludeFiles = []; this.sign = null; this.certificate = null; this.appCount = 0; this.services = []; this.pkgServiceNames = []; this.rscCount = 0; this.resources = []; this.pkgResourceNames = []; this.pkgId; this.pkginfofile; this.remainPlainIPK = false; // To check app signing this.dataHash = ''; this.certificateHash = ''; this.controlSign = ''; } packager.Packager = Packager; Packager.prototype = { prepareOptions: function(options, next) { if (options && options.level) { log.level = options.level; if (['warn', 'error'].indexOf(options.level) !== -1) { this.silent = false; } } if (options && options.noclean === true) { this.noclean = true; } if (options && options.nativecmd === true) { this.nativecmd = true; } if (options && Object.prototype.hasOwnProperty.call(options, 'minify')) { this.minify = options.minify; } if (options && Object.prototype.hasOwnProperty.call(options, 'excludefiles')) { if (options.excludefiles instanceof Array) { this.excludeFiles = options.excludefiles.map(filePath => { if (filePath && filePath.length) filePath = filePath.replace(/(^\\|^)/i, "\\").replaceAll(/\\/gi, "\\\\"); return filePath; }); } else { this.excludeFiles.push(options.excludefiles); } this.excludeFiles.forEach(function(excl_file) { if (excl_file === "appinfo.json") { return next(errHndl.getErrMsg("NOT_EXCLUDE_APPINFO")); } }); } if (options && Object.prototype.hasOwnProperty.call(options, 'rom')) { this.rom = options.rom; } if (options && Object.prototype.hasOwnProperty.call(options, 'encrypt')) { this.encrypt = options.encrypt; } if (options && Object.prototype.hasOwnProperty.call(options, 'remainPlainIPK')) { this.remainPlainIPK = options.remainPlainIPK; } if (options && Object.prototype.hasOwnProperty.call(options, 'sign')) { if (!fs.existsSync(path.resolve(options.sign))) { return next(errHndl.getErrMsg("NOT_EXIST_PATH", options.sign)); } this.sign = options.sign; } if (options && Object.prototype.hasOwnProperty.call(options, 'certificate')) { if (!fs.existsSync(path.resolve(options.certificate))) { return next(errHndl.getErrMsg("NOT_EXIST_PATH", options.certificate)); } this.certificate = options.certificate; } log.verbose("package#Packager()", "Packager id:" + this.objectId); this.pkgVersion = options.pkgversion; if (options && Object.prototype.hasOwnProperty.call(options, 'pkgid')) { this.pkgId = options.pkgid; } if (options && Object.prototype.hasOwnProperty.call(options, 'pkginfofile')) { this.pkginfofile = options.pkginfofile; } if (this.pkgId && this.pkginfofile) { return next(errHndl.getErrMsg("NOT_USE_WITH_OPTIONS", "pkginfofile, pkgid")); } this.appCount = 0; }, checkInputDirectories: function(inDirs, options, next) { log.verbose("package#Packager#checkInputDirectories()", "input directory:", inDirs); this.prepareOptions(options, next); async.forEachSeries(inDirs, checkDirectory.bind(this, options), function(err) { if (err) { setImmediate(next, err); return; } setImmediate(next); }); return {app: this.appCount, resource: this.rscCount}; }, servicePackaging: function(inDirs, destination, options, middleCb, next) { log.info("package#Packager#servicePackaging()"); if (!Object.hasOwnProperty.call(options, 'pkgid') && !Object.hasOwnProperty.call(options, 'pkginfofile')) { return next(errHndl.getErrMsg("USE_PKGID_PKGINFO")); } async.series([ this.checkInputDirectories.bind(this, inDirs, options), setUmask.bind(this, 0), loadPkgInfo.bind(this), createTmpDir.bind(this), excludeIpkFileFromApp.bind(this), createPackageDir.bind(this), fillPackageDir.bind(this), findServiceDir.bind(this, this.services), loadServiceInfo.bind(this), checkServiceInfo.bind(this), createServiceDir.bind(this), copyService.bind(this), addServiceInPkgInfo.bind(this), copyData.bind(this, inDirs, options.force), loadPackageProperties.bind(this, inDirs), excludeFromApp.bind(this, middleCb), outputPackage.bind(this, destination), encryptPackage.bind(this), copyOutputToDst.bind(this, destination, middleCb), recoverUmask.bind(this), cleanupTmpDir.bind(this, middleCb) ], function(err) { if (err) { // TODO: call cleanupTmpDir() before returning setImmediate(next, err); return; } // TODO: probably some more checkings are needed setImmediate(next, null, {ipk: this.ipk, msg: "Success"}); }.bind(this)); }, resourcePackaging: function(inDirs, destination, options, middleCb, next) { log.info("package#Packager#resourcePackaging()"); this.dataCopyCount = 0; async.series([ this.checkInputDirectories.bind(this, inDirs, options), setUmask.bind(this, 0), loadPkgInfo.bind(this), createTmpDir.bind(this), excludeIpkFileFromApp.bind(this), createPackageDir.bind(this), fillPackageDir.bind(this), loadResourceInfo.bind(this), checkResourceInfo.bind(this), createResourceDir.bind(this), copyResource.bind(this), addResourceInPkgInfo.bind(this), copyData.bind(this, inDirs, options.force), loadPackageProperties.bind(this, inDirs), excludeFromApp.bind(this, middleCb), outputPackage.bind(this, destination), encryptPackage.bind(this), copyOutputToDst.bind(this, destination, middleCb), recoverUmask.bind(this), cleanupTmpDir.bind(this, middleCb) ], function(err) { if (err) { setImmediate(next, err); return; } setImmediate(next, null, {ipk: this.ipk, msg: "Success"}); }.bind(this)); }, generatePackage: function(inDirs, destination, options, middleCb, next) { log.info("package#Packager#generatePackage()", "from ", inDirs); // check whether app or service directories are copied or not this.dataCopyCount = 0; this.minifyDone = !this.minify; async.series([ this.checkInputDirectories.bind(this, inDirs, options), setUmask.bind(this, 0), loadPkgInfo.bind(this), loadAppInfo.bind(this), checkAppInfo.bind(this), createTmpDir.bind(this), createAppDir.bind(this), checkELFHeader.bind(this), fillAssetsField.bind(this), copyAssets.bind(this), copyApp.bind(this), excludeIpkFileFromApp.bind(this), createPackageDir.bind(this), fillPackageDir.bind(this), findServiceDir.bind(this, this.services), loadServiceInfo.bind(this), checkServiceInfo.bind(this), createServiceDir.bind(this), copyService.bind(this), addServiceInPkgInfo.bind(this), removeServiceFromAppDir.bind(this, middleCb), copyData.bind(this, inDirs, options.force), loadPackageProperties.bind(this, inDirs), excludeFromApp.bind(this, middleCb), outputPackage.bind(this, destination), encryptPackage.bind(this), copyOutputToDst.bind(this, destination, middleCb), recoverUmask.bind(this), cleanupTmpDir.bind(this, middleCb) ], function(err) { if (err) { // TODO: call cleanupTmpDir() before returning setImmediate(next, err); return; } // TODO: probably some more checkings are needed setImmediate(next, null, {ipk: this.ipk, msg: "Success"}); }.bind(this)); }, analyzeIPK: function(options, next) { log.info("package#Packager#analyzeIPK()"); let ipkConfigFile, ipkFile, ipkConfig, tmpDirPath; this.prepareOptions(options); async.series([ _setConfig, _unpackIpk, _unpackTar, _analyzeMetaFile, _removeTmpDir, ], function(err, results) { log.silly("package#analyzeIPK()", "err:", err, ", results:", results); if (err) { return next(err); } return next(null, {msg: results[3].trim()}); }); function _setConfig(next) { log.info("package#analyzeIPK()#_setConfig()"); if (options.info === 'true') { return next(errHndl.getErrMsg("EMPTY_VALUE", 'info')); } else if (options.infodetail === 'true') { return next(errHndl.getErrMsg("EMPTY_VALUE", 'info-detail')); } ipkFile = options.info ? options.info : options.infodetail; ipkConfigFile = path.resolve(__dirname, "../files/conf/ipk.json"); if (path.extname(ipkFile) !== ".ipk") { return next(errHndl.getErrMsg("SUPPORT_ONLY_IPK", ipkFile)); } if (!fs.existsSync(ipkFile)) { return next(errHndl.getErrMsg("NOT_EXIST_PATH", ipkFile)); } if (!fs.existsSync(ipkConfigFile)) { return next(errHndl.getErrMsg("NOT_EXIST_PATH", ipkConfigFile)); } ipkConfig = JSON.parse(fs.readFileSync(ipkConfigFile)); tmpDirPath = temp.path(ipkConfig.tmpPath); if (!fs.existsSync(tmpDirPath)) { fs.mkdirSync(tmpDirPath); } next(); } function _unpackIpk(next) { log.info("package#analyzeIPK()#_unpackIpk()"); const reader = new ar.ArReader(ipkFile); reader.on("entry", function(entry, next) { const name = entry.fileName(); entry.fileData() .pipe(fs.createWriteStream(path.resolve(tmpDirPath, name))) .on("finish", next); }); reader.on("error", function(err) { return next(err); }); reader.on("close", function() { log.verbose("package#analyzeIPK()#_unpackIpk()", "unpack ipk close"); next(); }); } function _unpackTar(next) { log.info("package#analyzeIPK()#_unpackTar()"); if (fs.existsSync(path.resolve(tmpDirPath, "control.tar.gz"))) { (async function() { await decompress(path.resolve(tmpDirPath, "control.tar.gz"), tmpDirPath, { plugins: [ decompressTargz() ] }); log.verbose("package#analyzeIPK()#_unpackTar()", "control.tar.gz decompressed"); })(); } else if (fs.existsSync(path.resolve(tmpDirPath, "control.tar.xz"))) { return next(errHndl.getErrMsg("NOT_SUPPORT_XZ")); } else { return next(errHndl.getErrMsg("NO_COMPONENT_FILE", ", control tar file")); } if (fs.existsSync(path.resolve(tmpDirPath, "data.tar.gz"))) { (async function() { await decompress(path.resolve(tmpDirPath, "data.tar.gz"), tmpDirPath, { plugins: [ decompressTargz() ] }); log.verbose("package#analyzeIPK()#_unpackTar()", "data.tar.gz decompressed"); next(); })(); } else if (fs.existsSync(path.resolve(tmpDirPath, "data.tar.xz"))) { return next(errHndl.getErrMsg("NOT_SUPPORT_XZ")); } else { return next(errHndl.getErrMsg("NO_COMPONENT_FILE", "data tar file")); } } function _analyzeMetaFile(next) { log.info("package#analyzeIPK()#_analyzeMetaFile()"); let result = "", targetFile, tmpTxt; ipkConfig.webOSMetaFiles.forEach(function(item) { const targetPath = path.resolve(tmpDirPath, ipkConfig[item].path); // Analyze control file if (item === "control") { targetFile = path.resolve(targetPath, ipkConfig[item].fileName); if (!fs.existsSync(targetFile)) { return next(errHndl.getErrMsg("NO_COMPONENT_FILE", "control file")); } if (options.infodetail) { result = "\n\n< " + ipkConfig[item].fileName + " >\n"; result += fs.readFileSync(targetFile).toString().trim(); } else { result = ipkConfig[item].heading + "\n"; tmpTxt = fs.readFileSync(targetFile).toString().trim(); ipkConfig[item].info.forEach(function(field) { if (tmpTxt.match(new RegExp(field + ": (.*)", "gi"))) { result += tmpTxt.match(new RegExp(field + ": (.*)", "gi"))[0] + "\n"; } }); } // Analyze appinfo.json, packageinfo.json, services.json, package.json files } else if (fs.existsSync(targetPath)) { const dirArr = fs.readdirSync(targetPath); let beforeDir = dirArr[0]; dirArr.forEach(function(dir) { if (ipkConfig[item].fileName) { ipkConfig[item].fileName.forEach(function(file) { targetFile = path.resolve(targetPath, dir, file); if (fs.existsSync(targetFile)) { if (options.infodetail) { result += "\n\n< " + file + " >\n"; result += fs.readFileSync(targetFile).toString().trim(); } else { if (ipkConfig[item].heading && beforeDir !== path.join(ipkConfig[item].path, dir)) { result += "\n" + ipkConfig[item].heading + "\n"; } tmpTxt = fs.readFileSync(targetFile).toString().trim(); const tmpJson = JSON.parse(tmpTxt); ipkConfig[item].info.forEach(function(field) { if (tmpJson[field]) { const fieldValue = tmpJson[field]; if (typeof fieldValue === "object") { if (ipkConfig[item][field]) { // Case of services.json, services_name field const subField = ipkConfig[item][field], subResult = []; fieldValue.forEach(function(subItem) { subResult.push(subItem[subField]); }); result += field + ": " + JSON.stringify(subResult) + "\n"; } else { // Case of packageinfo.json, services field result += field + ": " + JSON.stringify(fieldValue) + "\n"; } } else { result += field + ": " + fieldValue + "\n"; } } }); beforeDir = path.join(ipkConfig[item].path, dir); } } }); } }); } }); next(null, result); } function _removeTmpDir(next) { log.info("package#analyzeIPK()#_removeTmpDir()"); rimraf(tmpDirPath, function(err) { log.verbose("package#analyzeIPK()#_removeTmpDir()", "removed " + tmpDirPath); next(err); }); } } }; function Service() { this.srcDir = ""; this.dstDirs = []; this.valid = false; this.serviceInfo = ""; this.dirName = ""; } // Private functions function loadPkgInfo(next) { log.verbose("loadPkgInfo"); let data; if (this.pkginfofile) { log.silly("Use pkginfofile option"); if (fs.existsSync(this.pkginfofile)) { if ("packageinfo.json" !== path.basename(this.pkginfofile)) { return setImmediate(next, errHndl.getErrMsg("INVALID_FILE", "packageinfo.json")); } data = rewriteFileWoBOMAsUtf8(this.pkginfofile, true); try { log.verbose("PKGINFO >>" + data + "<<"); this.pkginfo = JSON.parse(data); if (!Object.prototype.hasOwnProperty.call(this.pkginfo, 'id')) { return setImmediate(next, errHndl.getErrMsg("REQUIRED_FIELD", "id")); } this.pkgId = this.pkginfo.id; this.pkgVersion = this.pkgVersion || this.pkginfo.version; setImmediate(next); } catch (err) { return setImmediate(next, errHndl.getErrMsg("INVALID_JSON_FORMAT", "packageinfo.json")); } } else { return setImmediate(next, errHndl.getErrMsg("NOT_EXIST_PATH", this.pkginfofile)); } } else if (this.pkgDir) { log.silly("Use packageinfo.json from PKG_DIR"); data = rewriteFileWoBOMAsUtf8(path.join(this.pkgDir, "packageinfo.json")); try { log.verbose("PKGINFO >>" + data + "<<"); this.pkginfo = JSON.parse(data); if (!Object.prototype.hasOwnProperty.call(this.pkginfo, 'id')) { return setImmediate(next, errHndl.getErrMsg("REQUIRED_FIELD", "id")); } this.pkgId = this.pkginfo.id; this.pkgVersion = this.pkgVersion || this.pkginfo.version; setImmediate(next); } catch (err) { return setImmediate(next, errHndl.getErrMsg("INVALID_JSON_FORMAT", "packageinfo.json")); } } else { return setImmediate(next); } } function loadAppInfo(next) { log.verbose("loadAppInfo"); if (this.appCount === 0) { return setImmediate(next); } const filepath = path.join(this.appDir, "appinfo.json"), data = rewriteFileWoBOMAsUtf8(filepath, true); try { this.appinfo = JSON.parse(data); log.silly("package#loadAppInfo()", "content of appinfo.json:", this.appinfo); if (!this.appinfo.version || this.appinfo.version === undefined) { this.appinfo.version = "1.0.0"; } this.pkgVersion = this.pkgVersion || this.appinfo.version; setImmediate(next); } catch(err) { setImmediate(next, err); } } function checkAppInfo(next) { if (this.appCount === 0) { return setImmediate(next); } // check enyo app if (this.pkgJSExist && this.appinfo.main && this.appinfo.main.match(/(\.html|\.htm)$/gi)) { const mainFile = path.join(this.appDir, this.appinfo.main); if (!fs.existsSync(mainFile)) { return setImmediate(next, errHndl.getErrMsg("NOT_EXIST_PATH", this.appinfo.main)); } const regex = new RegExp("(<script[^>]*src[ \t]*=[ \t]*['\"])[^'\"]*/enyo.js(['\"])"), data = fs.readFileSync(mainFile); if (data.toString().match(regex)) { // If enyo app, stop packaging. return setImmediate(next, errHndl.getErrMsg("NOT_SUPPORT_ENYO")); } } if (!this.appinfo.id || this.appinfo.id === undefined) { return setImmediate(next, errHndl.getErrMsg("REQUIRED_FIELD", "id")); } if (this.appinfo.id.length < 1 || !(/^[a-z0-9.+-]*$/.test(this.appinfo.id))) { log.error(errHndl.getErrMsg("INVALID_VALUE", "id", this.appinfo.id)); return setImmediate(next, errHndl.getErrMsg("INVALID_ID_RULE")); } if (this.pkgId && this.appinfo.id.indexOf(this.pkgId) !== 0) { log.error(errHndl.getErrMsg("INVALID_VALUE", "id", this.appinfo.id)); return setImmediate(next, errHndl.getErrMsg("INVALID_APPID", this.pkgId)); } if (!this.appinfo.version || this.appinfo.version === undefined) { return setImmediate(next, errHndl.getErrMsg("REQUIRED_FIELD", "version")); } if (this.appinfo.version.length < 1 || !(/^([1-9]\d{0,8}|\d)\.([1-9]\d{0,8}|\d)\.([1-9]\d{0,8}|\d)$/.test(this.appinfo.version))) { log.error(errHndl.getErrMsg("INVALID_VALUE", "version", this.appinfo.version)); return setImmediate(next, errHndl.getErrMsg("INVALID_VERSION_RULE")); } if (this.appinfo.type && this.appinfo.type.match(/clock/gi)) { return setImmediate(next); } const schemaFile = path.resolve(__dirname, "../files/schema/ApplicationDescription.schema"); async.waterfall([ fs.readFile.bind(this, schemaFile, "utf-8"), function getSchema(data, next) { try { const schema = JSON.parse(data); /* "required" keyword is redefined in draft 4. But current jsonschema lib support only draft 3. So this line changes "required" attribute according to the draft 3. */ const reqKeys = schema.required; if (reqKeys) { for (const key in schema.properties) { if (reqKeys.indexOf(key) !== -1) { schema.properties[key].required = true; } } } next(null, schema); } catch(err) { next(errHndl.getErrMsg("INVALID_JSON_FORMAT", "AppDescription schema")); } }, function checkValid(schema, next) { try { next(null, new Validator().validate(this.appinfo, schema)); } catch (err) { log.error(err); next(errHndl.getErrMsg("INVALID_JSON_FORMAT")); } }.bind(this) ], function(err, result) { if (err) { setImmediate(next, err); } else { if (result && result.errors.length > 0) { const errFile = "appinfo.json"; let errMsg = ""; for (const idx in result.errors) { let errMsgLine = result.errors[idx].property + " " + result.errors[idx].message; if (errMsgLine.indexOf("instance.") > -1) { errMsgLine = errMsgLine.substring("instance.".length); errMsg = errMsg.concat("\n"); errMsg = errMsg.concat(errMsgLine); } } errMsg = errHndl.getErrMsg("INVALID_FILE", errFile, errMsg); return setImmediate(next, errMsg); } else { log.verbose("package#checkAppInfo()", "APPINFO is valid"); } setImmediate(next); } }); } function fillAssetsField(next) { if (this.appCount === 0) { return setImmediate(next); } // make appinfo.assets to have default values so that they can be copied into the package this.appinfo.assets = this.appinfo.assets || []; for (const i in this.appinfo) { if (Object.prototype.hasOwnProperty.call(this.appinfo, i) && defaultAssetsFields[i]) { // no duplicated adding & value should not null string & file/dir should exist if ((this.appinfo.assets.indexOf(this.appinfo[i]) === -1) && this.appinfo[i]) { this.appinfo.assets.push(this.appinfo[i]); } } } // refer to appinfo.json files in localization directory. const appInfoPath = this.originAppDir, checkDir = path.join(this.originAppDir, "resources"), foundFilePath = [], resourcesAssets = []; try { const stat = fs.lstatSync(checkDir); if (!stat.isDirectory()) { return setImmediate(next, null); } } catch(err) { if (err.code === "ENOENT") { return setImmediate(next, null); } } async.series([ walkFolder.bind(null, checkDir, "appinfo.json", foundFilePath, 1), function(next) { async.forEach(foundFilePath, function(filePath, next) { rewriteFileWoBOMAsUtf8(filePath, true, function(err, data) { try { const appInfo = JSON.parse(data), dirPath = path.dirname(filePath); for (const i in appInfo) { if (Object.prototype.hasOwnProperty.call(appInfo, i) && defaultAssetsFields[i]) { if (appInfo[i]) { const itemPath = path.join(dirPath, appInfo[i]), relPath = path.relative(appInfoPath, itemPath); // no duplicated adding & value should not null string & file/dir should exist if ((resourcesAssets.indexOf(relPath) === -1)) { resourcesAssets.push(relPath); } } } } setImmediate(next, null); } catch(error) { setImmediate(next, errHndl.getErrMsg("INVALID_JSON_FORMAT", filePath)); } }); }, function(err) { setImmediate(next, err); }); }, function(next) { this.appinfo.assets = this.appinfo.assets.concat(resourcesAssets); setImmediate(next, null); }.bind(this) ], function(err) { setImmediate(next, err); }); } function createTmpDir(next) { this.tempDir = temp.path({prefix: 'com.palm.ares.hermes.bdOpenwebOS'}) + '.d'; log.verbose("package#createTmpDir()", "temp dir:", this.tempDir); mkdirp(this.tempDir, next); } function createAppDir(next) { if (this.appCount === 0) { return setImmediate(next); } try { this.applicationDir = path.resolve(this.tempDir, "data/usr/palm/applications", this.appinfo.id); log.info("package#createAppDir()", "application dir:" + this.applicationDir); mkdirp(this.applicationDir, next); } catch (err) { return setImmediate(next, err); } } function copySrcToDst(src, dst, next) { const fileList = [], self = this, requireMinify = !!((!!self.minify && !self.minifyDone)); src = path.normalize(path.resolve(src)); dst = path.normalize(path.resolve(dst)); async.series([ function(next) { const stat = fs.statSync(src); if (stat.isFile()) { _pushList(fileList, 'file', path.dirname(src), path.basename(src), true, null); setImmediate(next); } else { _getFileList(src, src, fileList, next); } }, _copySrcToDst.bind(null, fileList, dst, requireMinify) ], function(err) { next(err); }); function _pushList(list, type, basePath, relPath, isSubPath, indRelPath) { if (!FILE_TYPE[type]) { return; } list.push({ type: type, basePath: basePath, relPath: relPath, isSubPath: isSubPath, indRelPath: indRelPath }); } function _getFileList(dirPath, basePath, files, next) { // TODO: the following code should be more concise. // Handling symbolic links // if the path sym-link indicates is a sub-path of source directory, treat a sym-link as it is. // otherwise the files sym-link indicates should be copied async.waterfall([ fs.readdir.bind(null, dirPath), function(fileNames, next) { if (fileNames.length === 0) { _pushList(files, 'dir', basePath, path.relative(basePath, dirPath), true, null); return setImmediate(next); } async.forEachSeries(fileNames, function(fileName, next) { const filePath = path.join(dirPath, fileName), relPath = path.relative(basePath, filePath); async.waterfall([ fs.lstat.bind(null, filePath), function(lstat, next) { if (lstat.isSymbolicLink()) { let indicateFullPath; try { indicateFullPath = fs.realpathSync(filePath); } catch (err) { if (err.code === 'ENOENT') { log.warn("The file for symbolic link (" + filePath + ") is missing..."); return setImmediate(next); } return setImmediate(next, err); } const indicateRelPath = fs.readlinkSync(filePath); if (indicateFullPath.indexOf(basePath) !== -1) { _pushList(files, 'symlink', basePath, relPath, true, indicateRelPath); } else { const stat = fs.statSync(filePath); if (stat.isDirectory()) { return _getFileList(filePath, basePath, files, next); } else if (stat.isFile()) { _pushList(files, 'file', basePath, relPath, true, null); } } setImmediate(next); } else if (lstat.isDirectory()) { return _getFileList(filePath, basePath, files, next); } else if (lstat.isFile()) { _pushList(files, 'file', basePath, relPath, true, null); setImmediate(next); } else { setImmediate(next); } } ], next); // async.waterfall }, next); // async.forEach } ], function(err) { return setImmediate(next, err); }); // async.waterfall } function _copySrcToDst(files, dstPath, minify, next) { try { async.forEachSeries(files, function(file, next) { if (!FILE_TYPE[file.type]) { log.verbose("package#copySrcToDst()#_copySrcToDst()", "ignore 'unknown file type'("+file.type+")"); return; } if (!file.relPath) { log.verbose("package#copySrcToDst()#_copySrcToDst()", "ignore 'unknown path'"); return setImmediate(next); } if (file.type === FILE_TYPE.dir) { mkdirp.sync(path.join(dstPath, file.relPath)); return setImmediate(next); } const dstDirPath = path.dirname(path.join(dstPath, file.relPath)); if (!fs.existsSync(dstDirPath)) { mkdirp.sync(dstDirPath); } if (file.type === FILE_TYPE.symlink) { if (file.isSubPath && file.indRelPath) { const linkFile = path.join(dstPath, file.relPath); if (fs.existsSync(linkFile)) { if (fs.lstatSync(linkFile).isSymbolicLink()) { fs.unlinkSync(linkFile); } } fs.symlinkSync(file.indRelPath, linkFile, null); } } else { const sourceFile = path.join(file.basePath, file.relPath); if (fs.existsSync(sourceFile)) { if (minify && '.js' === path.extname(sourceFile) && file.relPath.indexOf('node_modules') === -1) { log.verbose("package#copySrcToDst()#_copySrcToDst()", "require minification # sourceFile:", sourceFile); try { const data = uglify.minify(fs.readFileSync(sourceFile,'utf8')); if (data.error) { throw data.error; } fs.writeFileSync(path.join(dstPath, file.relPath), data.code, 'utf8'); } catch (e) { log.verbose("package#copySrcToDst()#_copySrcToDst()", util.format('Failed to uglify code %s: %s', sourceFile, e.stack)); return setImmediate(next, errHndl.getErrMsg("FAILED_MINIFY", sourceFile)); } } else { shelljs.cp('-Rf', sourceFile, path.join(dstPath, file.relPath, '..')); } } else { log.verbose("package#copySrcToDst()#_copySrcToDst()", "ignore '" + file.relPath + "'"); } } setImmediate(next); }, function(err) { if (!err && minify) { self.minifyDone = true; } setImmediate(next, err); }); } catch(err) { setImmediate(next, err); } } } function checkELFHeader(next) { const self = this, ELF_HEADER_LEN = 64, buf = Buffer.alloc(ELF_HEADER_LEN), mainFile = path.resolve(path.join(this.appDir, this.appinfo.main)); if (!fs.existsSync(mainFile)) { return setImmediate(next, errHndl.getErrMsg("NOT_EXIST_PATH", mainFile)); } const fd = fs.openSync(mainFile, 'r'), stats = fs.fstatSync(fd), elfParser = new ElfParser(), _isELF = function(_buf) { if (_buf.slice(0, 4).toString() !== '\x7fELF') { return false; } else { return true; } }; if (stats.size < ELF_HEADER_LEN) { log.verbose("package#checkELFHeader()", "file size is smaller than ELF Header size"); return setImmediate(next); } fs.read(fd, buf, 0, ELF_HEADER_LEN, 0, function(err, bytesRead, _buf) { if (bytesRead < ELF_HEADER_LEN || err) { log.silly("package#checkELFHeader()", "err:", err, ", bytesRead:", bytesRead); log.silly("package#checkELFHeader()", "readBuf to parse ELF header is small or error occurred during reading file."); return setImmediate(next); } if (!_isELF(_buf)) { log.silly("package#checkELFHeader()", mainFile + " is not ELF format"); } else { log.silly("package#checkELFHeader()", mainFile + " is ELF format"); try { const elfHeader = elfParser.parseHeader(_buf); log.silly("package#checkELFHeader()", "elfHeader:", elfHeader); if (elfHeader.machine && elfHeader.machine.match(/86$/)) { // current emulator opkg is allowing only all, noarch and i586. // when it is used with --offline-root. self.architecture = 'i586'; } else if (elfHeader.machine && elfHeader.machine.match(/amd64$/)) { // change amd64 to x86_64 self.architecture = 'x86_64'; } else if (elfHeader.machine && elfHeader.machine.match(/AArch64$/)) { // change AArch64 to aarch64 self.architecture = 'aarch64'; } else { self.architecture = elfHeader.machine; } } catch(e) { log.verbose("package#checkELFHeader()", "exception:", e); } } log.verbose("package#checkELFHeader()", "machine:", self.architecture); fs.close(fd); setImmediate(next); }); } function copyApp(next) { if (this.appCount === 0) { return setImmediate(next); } this.dataCopyCount++; log.info("package#copyApp()", "copy " + this.appDir + " ==> " + this.applicationDir); copySrcToDst.call(this, this.appDir, this.applicationDir, next); } function copyAssets(next) { if (this.appCount === 0) { return setImmediate(next); } try { async.forEachSeries(this.appinfo.assets, _handleAssets.bind(this), next); } catch (err) { return setImmediate(next, err); } function _handleAssets(file, next) { if (path.resolve(this.originAppDir) === path.resolve(this.appDir)) { _checkAppinfo.call(this, file, next); } else { async.series([ _checkAppinfo.bind(this, file), _copyAssets.bind(this, file) ], next); } } function _checkAppinfo(file, next) { let source; if (path.resolve(file) === path.normalize(file)) { return next(errHndl.getErrMsg("NOT_RELATIVE_PATH_APPINFO", file)); } else { source = path.join(this.originAppDir, file); } if (path.resolve(source).indexOf(this.originAppDir) !== 0) { return next(errHndl.getErrMsg("NOT_RELATIVE_PATH_APPINFO", file)); } if (!fs.existsSync(source) && !this.rom) { const msg = errHndl.getErrMsg("NOT_EXIST_PATH", file); if (path.basename(source).indexOf('$') === 0) { // ignore property with starting $ prefix (dynamic property handling in the platform) return setImmediate(next); } else { return setImmediate(next, msg); } } setImmediate(next); } function _copyAssets(file, next) { log.verbose("package#copyAssets()#_copyAssets", "'" + file + "' will be located in app directory"); const source = path.join(this.originAppDir, file), destination = this.appDir; async.series([ function(next) { if (!fs.existsSync(destination)) { mkdirp(destination, next); } else { setImmediate(next); } } ], function(err) { if (err) { return setImmediate(next, err); } shelljs.cp('-Rf', source, destination); setImmediate(next); }); } } function excludeIpkFileFromApp(next) { // Exclude a pre-built .ipk file this.excludeFiles = this.excludeFiles.concat([ // eslint-disable-next-line no-useless-escape "[.]*[\\.]ipk", ".DS_Store", // ".vscode", // ".reloadignore", // ".launchparams.json" ]); setImmediate(next); } function _retrieve(list, regExp, dirPath, depth, next) { async.waterfall([ fs.readdir.bind(null, dirPath), function(fileNames, next) { async.forEach(fileNames, function(fileName, next) { const filePath = path.join(dirPath, fileName); async.waterfall([ fs.lstat.bind(null, filePath), function(stat, next) { let result = false; if (depth > 1 && regExp.test(filePath)) { result = true; list.push(filePath); } if (!result && stat.isDirectory()) { _retrieve(list, regExp, filePath, depth + 1, next); } else { setImmediate(next); } } ], next); }, next); } ], function(err) { setImmediate(next, err); }); } function excludeFromApp(middleCb, next) { let excludes; if (this.appCount === 0) { excludes = this.excludeFiles; } else { excludes = this.excludeFiles.concat(this.appinfo.exclude || []); } const regExpQueries = excludes.map(function(exclude) { return exclude.replace(/^\./g,"^\\.").replace(/^\*/g,"").replace(/$/g,"$"); }, this), strRegExp = regExpQueries.join("|"), regExp = new RegExp(strRegExp, "i"), excludeList = [], tempDirPrefix = path.resolve(this.tempDir, "data/usr/palm"); async.series([ _retrieve.bind(this, excludeList, regExp, tempDirPrefix, 0), function(next) { const meta_files = ["appinfo.json", "services.json", "packageinfo.json", "resourceinfo.json"], unexcludable = [], excluded = []; try { excludeList.forEach(function(file) { if (meta_files.includes(path.basename(file))) { unexcludable.push(path.basename(file)); } else { const sliced = file.slice(tempDirPrefix.length + 1); excluded.push(sliced.slice(sliced.indexOf(path.sep) + 1)); shelljs.rm('-rf', file); } }); if (unexcludable.length > 0) { middleCb("Cannot exclude: " + unexcludable); // FIXME } if (excluded.length > 0) { middleCb("Excluded: " + excluded); // FIXME } setImmediate(next); } catch(err) { setImmed