@webos-tools/cli
Version:
Command Line Interface for development webOS application and service
1,142 lines (1,045 loc) • 94.9 kB
JavaScript
/*
* 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