VulpesOS/gaia/build/utils-node.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

626 lines
17 KiB
JavaScript
Executable file

'use strict';
/**
* This file contains all Node.js version of utils functions.
* Since we're stil in the progress to migrate to Node.js, the
* missing functions in this file should be in the 'utils-xpc.js'
*
* About documents:
* Users can find comments in the 'utils-xpc.js',
* which contains the complete functions used in 'utils.js'
*/
var utils = require('./utils');
var path = require('path');
var childProcess = require('child_process');
var fs = require('fs-extra');
var JSZip = require('jszip');
var Q = require('q');
var os = require('os');
var vm = require('vm');
var http = require('http');
var https = require('https');
var url = require('url');
var dive = require('diveSync');
var nodeUUID = require('node-uuid');
var jsdom = require('jsdom');
var esprima = require('esprima');
var procRunning = require('is-running');
var mime = require('mime');
var crypto = require('crypto');
// Our gecko will transfer .opus file to audio/ogg datauri type.
mime.define({'audio/ogg': ['opus']});
module.exports = {
Q: Q,
scriptParser: esprima.parse,
log: function() {
var args = Array.prototype.slice.call(arguments);
if (!args.length) {
console.log('\n');
return;
}
console.log('[' + args[0] + '] ' + args.slice(1).join(' '));
},
normalizePath: function(string) {
return path.normalize(string);
},
joinPath: function() {
return path.join.apply(path, arguments);
},
getOsType: function() {
var type = os.type();
return type === 'Windows_NT' ? 'WINNT' : type;
},
relativePath: function(from, to) {
return path.relative(from, to);
},
getFile: function() {
var self = module.exports;
var args = Array.prototype.slice.call(arguments);
var src = path.join.apply(path, args);
src = path.resolve(src);
var fileStat;
try {
fileStat = fs.statSync(src);
} catch (e) {
// In order to have compability with xpcshell interface,
// we just simply let fileStat undefined and don't need
// to do anything here.
}
return {
exists: function() {
return self.fileExists(src);
},
remove: function() {
fs.removeSync(src);
},
isDirectory: function() {
return !!fileStat && fileStat.isDirectory();
},
isFile: function() {
return !!fileStat && fileStat.isFile();
},
isHidden: function() {
return /^\./.test(path.basename(src));
},
path: src,
leafName: path.basename(src)
};
},
getFileContent: function(file, type) {
if (file.exists() && file.isFile()) {
return fs.readFileSync(file.path, { encoding: type || 'utf8' });
}
},
Commander: function(cmd) {
this.initPath = function() {};
this.run = function(args, callback, options) {
var cmds = args.join(' ');
// In *nix and OSX version commands are run via sh -c YOUR_COMMAND,
// but in Windows commands are run via cmd /C YOUR_COMMAND,
// so, we just let execSync module to handle the difference.
if (cmd === 'sh') {
cmds = cmds.replace(/^-c/, '');
} else {
cmds = cmd + ' ' + cmds;
}
console.log(cmds);
// XXX: Most cmds should run synchronously, we should use either promise
// pattern inside each script or find a sync module which doesn't
// require recompile again since TPBL doesn't support that.
return new Promise(function(resolve, reject) {
childProcess.exec(cmds, { maxBuffer: (4096 * 1024) },
function(err, stdout, stderr) {
if (err) {
options && options.stderr && options.stderr(stderr);
reject(stderr);
} else {
options && options.stdout && options.stdout(stdout);
callback && callback(stdout);
resolve(stdout);
}
});
});
};
this.runWithSubprocess = function(args, options) {
this.run(args, null, options);
};
},
readZipManifest: function(file) {
var zipPath = this.joinPath(file.path, 'application.zip');
if (!this.fileExists(zipPath)) {
return;
}
var manifest = new JSZip(fs.readFileSync(zipPath)).file('manifest.webapp');
if (!manifest) {
throw new Error('manifest.webapp not found in ' + zipPath);
}
return JSON.parse(manifest.asText());
},
killAppByPid: function(appName) {
childProcess.exec('adb shell b2g-ps', function(err, stdout) {
if (!err && stdout) {
var psMap = utils.psParser(stdout);
if (psMap[appName] && psMap[appName].PID) {
childProcess.exec('adb shell kill ' + psMap[appName].PID);
}
}
});
},
spawnProcess: function(module, appOptions) {
this.exitCode = null;
var args = [
'--harmony',
'-e', 'require("' + module + '").execute("' + JSON.stringify(appOptions)
.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '");'
];
var proc = childProcess.spawn('node', args);
proc.on('close', function(code) {
this.exitCode = code;
}.bind(this));
return proc;
},
processIsRunning: function(proc) {
return procRunning(proc.pid);
},
getProcessExitCode: function() {
return this.exitCode;
},
resolve: function(relativePath, gaiaDir) {
var absPath;
if (relativePath[0] === '/') {
absPath = relativePath;
} else {
absPath = path.join(gaiaDir, relativePath);
}
return this.getFile(absPath);
},
getFileAsDataURI: function(file) {
var data = this.getFileContent(file, 'base64');
return 'data:' + mime.lookup(file.path) + ';base64,' + data;
},
getJSON: function(file) {
var content = this.getFileContent(file);
try {
return JSON.parse(content);
} catch (error) {
console.log('Invalid JSON file : ' + file.path + '\n');
if (content) {
console.log('Content of JSON file:\n' + content + '\n');
}
throw error;
}
},
getDocument: function(content) {
return jsdom.jsdom(content);
},
getXML: function(file) {
return jsdom.jsdom(this.getFileContent(file));
},
getEnv: function(name) {
return process.env[name];
},
setEnv: function(name, value) {
process.env[name] = value;
},
getEnvPath: function() {
return process.env.PATH.split(path.delimiter);
},
getTempFolder: function(dirName) {
var tmpPath = path.join(os.tmpdir(), dirName);
fs.mkdirpSync(tmpPath);
return this.getFile(tmpPath);
},
ensureFolderExists: function(file) {
fs.mkdirpSync(file.path);
},
normalizeString: function(appname) {
return appname.replace(' ', '-').toLowerCase().replace(/\W/g, '');
},
ls: function(dir, recursive) {
var files = [];
if (!dir || !dir.exists()) {
return [];
}
dive(dir.path, { recursive: recursive }, function(err, filePath) {
if (err) {
// Skip error
return;
}
files.push(this.getFile(filePath));
}.bind(this));
return files;
},
download: function(fileUrl, dest, callback, errorCallback) {
var protocol = url.parse(fileUrl).protocol;
var request = (protocol === 'http:') ? http : https;
var file = fs.createWriteStream(dest);
request.get(fileUrl, function(response) {
response.pipe(file);
file.on('finish', function() {
file.close(callback);
});
}).on('error', function(err) {
fs.unlinkSync(dest);
if (errorCallback) {
errorCallback();
}
});
},
downloadJSON: function(fileUrl, callback) {
var protocol = url.parse(fileUrl).protocol;
var request = (protocol === 'http:') ? http : https;
request.get(fileUrl, function(res) {
var body = '';
res.on('data', function(chunk) {
body += chunk;
});
res.on('end', function() {
if (callback) {
callback(JSON.parse(body));
}
});
}).on('error', function(e) {
throw new Error('Download JSON with error: ' + e);
});
},
readJSONFromPath: function(path) {
return require(path);
},
concatenatedScripts: function(scriptsPaths, targetPath) {
var concatedScript = scriptsPaths.map(function(path) {
return this.getFileContent(this.getFile(path));
}, this).join('\n');
var targetFile = this.getFile(targetPath);
this.writeContent(targetFile, concatedScript);
},
getDistributionFileContent: function(name, defaultContent, distDir) {
if (distDir) {
let distributionFile = this.getFile(distDir, name + '.json');
if (distributionFile.exists()) {
return this.getFileContent(distributionFile);
}
}
return JSON.stringify(defaultContent, null, ' ');
},
getUUIDMapping: function(config) {
var UUIDMapping = this.UUIDMapping;
if (UUIDMapping) {
return UUIDMapping;
}
this.UUIDMapping = UUIDMapping = {};
// Try to retrieve it from $GAIA_DISTRIBUTION_DIR/uuid.json if exists.
try {
var uuidFile = this.getFile(config.GAIA_DISTRIBUTION_DIR, 'uuid.json');
if (uuidFile.exists()) {
console.log('uuid.json in GAIA_DISTRIBUTION_DIR found.');
UUIDMapping = JSON.parse(this.getFileContent(uuidFile));
}
} catch (e) {
// ignore exception if GAIA_DISTRIBUTION_DIR does not exist.
}
return UUIDMapping;
},
getWebapp: function(app, config) {
let appDir = this.getFile(app);
if (!appDir.exists()) {
throw new Error(' -*- build/utils.js: file not found (' + app + ')\n');
}
let manifestFile = appDir.path + '/manifest.webapp';
let updateFile = appDir.path + '/update.webapp';
// Ignore directories without manifest
if (!this.fileExists(manifestFile) && !this.fileExists(updateFile)) {
return;
}
let manifest = this.fileExists(manifestFile) ? manifestFile : updateFile;
let manifestJSON = this.getJSON(this.getFile(manifest));
// Use the folder name as the the domain name
let appDomain = appDir.leafName + '.' + config.GAIA_DOMAIN;
if (manifestJSON.origin) {
appDomain = this.getNewURI(manifestJSON.origin).host;
}
let self = this;
let webapp = {
appDirPath: appDir.path,
manifest: manifestJSON,
manifestFilePath: manifest,
url: config.GAIA_SCHEME + appDomain,
domain: appDomain,
sourceDirectoryFilePath: appDir.path,
sourceDirectoryName: appDir.leafName,
sourceAppDirectoryName: self.getFile(appDir.path, '..').leafName
};
// External webapps have a `metadata.json` file
let metaData = this.getFile(webapp.sourceDirectoryFilePath,
'metadata.json');
if (metaData.exists()) {
webapp.pckManifest = this.readZipManifest(appDir);
webapp.metaData = this.getJSON(metaData);
webapp.appStatus = utils.getAppStatus(
webapp.metaData.type ||
(webapp.pckManifest && webapp.pckManifest.type) ||
'web'
);
} else {
webapp.appStatus = utils.getAppStatus(webapp.manifest.type);
}
// Some webapps control their own build
webapp.buildDirectoryFilePath = this.joinPath(config.STAGE_DIR,
webapp.sourceDirectoryName);
webapp.buildManifestFilePath = this.joinPath(webapp.buildDirectoryFilePath,
'manifest.webapp');
// Generate the webapp folder name in the profile. Only if it's privileged
// and it has an origin in its manifest file it'll be able to specify a
// custom folder name. Otherwise, generate an UUID to use as folder name.
var webappTargetDirName;
if (this.isExternalApp(webapp)) {
var type = webapp.appStatus;
var isPackaged = false;
if (webapp.pckManifest) {
isPackaged = true;
if (webapp.metaData.origin) {
throw new Error('External webapp `' + webapp.sourceDirectoryName +
'` can not have origin in metadata because is packaged');
}
}
if (type === 2 && isPackaged && webapp.pckManifest.origin) {
webappTargetDirName = this.getNewURI(webapp.pckManifest.origin).host;
} else {
// uuid is used for webapp directory name, save it for further usage
let mapping = this.getUUIDMapping(config);
var uuid = mapping[webapp.sourceDirectoryName] ||
'{' + nodeUUID.v4() + '}';
mapping[webapp.sourceDirectoryName] = webappTargetDirName = uuid;
}
} else {
webappTargetDirName = webapp.domain;
}
webapp.profileDirectoryFilePath = this.joinPath(config.COREWEBAPPS_DIR,
'webapps',
webappTargetDirName);
return webapp;
},
getNewURI: function(uri) {
return {
host: /[a-z]+:\/\/[@]?([^\/:]+)/.exec(uri)[1],
prePath: /[a-z]+:\/\/[^\/]+/.exec(uri)[0],
resolve: function(to) {
return url.resolve(uri, to);
}
};
},
isExternalApp: function(webapp) {
if (webapp.metaData && webapp.metaData.external === undefined) {
throw new Error('"external" property in metadata.json is required ' +
'since Firefox OS 2.1, please add it into metadata.json and update ' +
'preload.py if you use this script to perload your apps. If you ' +
'created metadata.json for non-external apps, please set "external" ' +
'to false. metadata.json is in ' + webapp.sourceDirectoryFilePath);
}
if (!webapp.metaData || webapp.metaData.external === false) {
return false;
} else {
return true;
}
},
processEvents: function(exitResultFunc) {
// FIXME: A fake blocking function for porting utils-xpc
// This workaround will be removed once node.js migration is done
var exitResult = exitResultFunc();
if (exitResult.error) {
throw exitResult.error;
}
},
writeContent :function(file, content) {
fs.writeFileSync(file.path, content);
},
createZip: function() {
return new JSZip();
},
getCompression: function(type) {
switch(type) {
case 'none':
return 'STORE';
case 'best':
return 'DEFLATE';
}
},
addFileToZip: function(zip, pathInZip, file, compression) {
if (!file.exists()) {
return;
}
zip.file(pathInZip, fs.readFileSync(file.path), {
compression: compression || 'DEFLATE'
});
},
hasFileInZip: function(zip, pathInZip) {
return zip.file(pathInZip);
},
closeZip: function(zip, zipPath) {
fs.writeFileSync(zipPath, zip.generate({
type: 'nodebuffer',
platform: process.platform
}));
},
getLocaleBasedir: function(src) {
return os.type() === 'Windows_NT' ? src.replace('/', '\\', 'g') : src;
},
deleteFile: function(filePath) {
fs.removeSync(filePath);
},
fileExists: function(path) {
try {
fs.accessSync(path);
return true;
} catch(err) {
return false;
}
},
listFiles: function(path, type, recursive, exclude) {
var file = (typeof path === 'string' ? this.getFile(path) : path);
if (!file.isDirectory()) {
throw new Error('The path is not a directory.');
}
var files = this.ls(file, recursive === true, exclude);
var detectFunc = (type === 0 ? 'isFile' : 'isDirectory');
// To return simple JavaScript type, We need to put the file path to the
// array instead of nsIFile.
var results = [];
files.forEach(function(file) {
if (file[detectFunc]()) {
results.push(file.path);
}
});
return results;
},
mkdirs: function(path) {
fs.mkdirpSync(path);
},
dirname: function(filePath) {
return path.dirname(filePath);
},
basename: function(filePath) {
return path.basename(filePath);
},
existsInAppDirs: function(appDirs, appName) {
var apps = appDirs.split(' ');
var exists = apps.some(function (appPath) {
let appFile = this.getFile(appPath);
return (appName === appFile.leafName);
}, this);
return exists;
},
copyFileTo: function(filePath, toParent, name) {
var file = ((typeof filePath === 'string') ?
this.getFile(filePath) : filePath);
fs.copySync(file.path, path.join(toParent, name));
},
copyDirTo: function(filePath, toParent, name) {
var file = ((typeof filePath === 'string') ?
this.getFile(filePath) : filePath);
fs.copySync(file.path, path.join(toParent, name));
},
copyToStage: function(options) {
var appDir = this.getFile(options.APP_DIR);
this.copyDirTo(appDir, options.STAGE_DIR, appDir.leafName);
},
exit: function(code) {
process.exit(code);
},
scriptLoader: {
scripts: {},
load: function(filePath, exportObj, withoutCache) {
if (!withoutCache && this.scripts[filePath]) {
return;
}
var doc = jsdom.jsdom();
var win = doc.defaultView;
exportObj.Promise = Q;
global.addEventListener = win.addEventListener;
global.dispatchEvent = win.dispatchEvent;
for (let key in exportObj) {
global[key] = exportObj[key];
}
try {
var script = fs.readFileSync(filePath, { encoding: 'utf8' });
vm.runInThisContext(script, filePath);
this.scripts[filePath] = true;
} catch(error) {
delete this.scripts[filePath];
throw error;
}
}
},
createSandbox: function() {
return {};
},
runScriptInSandbox: function(filePath, sandbox) {
var script = fs.readFileSync(filePath, { encoding: 'utf8' });
return vm.runInNewContext(script, sandbox);
},
getHash: function(string) {
return crypto.createHash('sha1').update(string).digest('hex');
}
};