function NwBuilder(options) {
var defaults = {
files: null,
appName: false,
appVersion: false,
platforms: ['osx32', 'osx64', 'win32', 'win64'],
currentPlatform: detectCurrentPlatform(),
version: 'latest',
buildDir: './build',
cacheDir: './cache',
downloadUrl: 'https://dl.nwjs.io/',
manifestUrl: 'https://nwjs.io/versions.json',
flavor: 'sdk',
buildType: 'default',
forceDownload: false,
macCredits: false,
macIcns: false,
macZip: null,
zip: null,
zipOptions: null,
macPlist: false,
winIco: null,
argv: process.argv.slice(2)
};
// Intercept the platforms and check for the legacy platforms of 'osx' and 'win' and
// replace with 'osx32', 'osx64', and 'win32', 'win64' respectively.
if(typeof options.platforms != 'undefined'){
if(options.platforms.indexOf('osx') >= 0){
options.platforms.splice(options.platforms.indexOf('osx'), 1, 'osx32', 'osx64');
}
if(options.platforms.indexOf('win') >= 0){
options.platforms.splice(options.platforms.indexOf('win'), 1, 'win32', 'win64');
}
if(options.platforms.indexOf('linux') >= 0){
options.platforms.splice(options.platforms.indexOf('linux'), 1, 'linux32', 'linux64');
}
}
// Assign options
this.options = _.defaults(options, defaults);
// Some Option checking
if(!this.options.files) {
throw new Error('Please specify some files');
}
if (this.options.platforms.length === 0)
throw new Error('No platform to build!');
// verify all the platforms specifed by the user are supported
// this + previous check assures as we have only buildable platforms specified
this.options.platforms.forEach(function(platform) {
if (!(platform in platforms))
throw new Error('Unknown platform ' + platform);
});
this._platforms = _.cloneDeep(platforms);
// clear all unused platforms
for (var name in this._platforms) {
if (this.options.platforms.indexOf(name) === -1)
delete this._platforms[name];
}
}n/a
function EventEmitter() {
EventEmitter.init.call(this);
}n/a
checkCache = function (cachepath, files) {
var missing;
// if the version is >=0.12.3, then we don't which files we want from the archives, so just check that the folder
// exists and has at least 3 files in it.
if(files.length === 1 && files[0] === '*'){
return fs.existsSync(cachepath) && fs.readdirSync(cachepath).length >= 3;
}
files.forEach(function(file) {
if (missing) {
return;
}
if (!fs.existsSync(path.join(cachepath, file))) {
missing = true;
}
});
return !missing;
}...
NwBuilder.prototype.isPlatformCached = function (platformName, platform, version, flavor) {
this.setPlatformCacheDirectory(platformName, platform, version, flavor);
if (this.options.forceDownload) {
return false;
}
this.preparePlatformFiles(platformName, platform, version);
return Downloader.checkCache(platform.cache, platform.files);
};
// returns a Boolean; true if the desired platform is supported
NwBuilder.prototype.preparePlatformFiles = function(platformName, platform, version) {
// return if platform.files is already prepared
if(Object.keys(platform.files)[0] !== Object.keys(platforms[platformName].files)[0]){
return true;
...clearProgressbar = function () {
bar && bar.terminate();
bar = null;
}...
} else {
self.emit('log', 'Using cache for: ' + name);
}
});
return Promise.all(downloads)
.then(function(data) {
Downloader.clearProgressbar();
return data;
});
};
NwBuilder.prototype.buildGypModules = function () {
// @todo
// If we trigger a rebuild we have to copy
...downloadAndUnpack = function (cachepath, url) {
var extention = path.extname(url),
done = Promise.defer(),
self = this,
rq = request(url),
len,
stream;
function format(statusCode) {
return statusCode + ': ' + require('http').STATUS_CODES[statusCode];
}
rq.proxy = true;
rq.on('error', function(err) {
bar && bar.terminate();
done.reject(err);
});
rq.on('response', function (res) {
len = parseInt(res.headers['content-length'], 10);
if (res.statusCode !== 200) {
done.reject({
statusCode: res.statusCode,
msg: 'Recieved status code ' + format(res.statusCode)
});
} else if (len) {
if (!bar) {
bar = new progress(' downloading [:bar] :percent :etas', {
complete: '=',
incomplete: '-',
width: 20,
total: len
});
} else {
bar.total += len;
}
}
});
rq.on('data', function(chunk) {
len && bar && bar.tick(chunk.length);
});
if (extention === '.zip') {
stream = temp.createWriteStream();
stream.on('close', function() {
if(done.promise.isRejected()) return;
self.extractZip(stream.path, cachepath).then(self.stripRootFolder).then(function(files) {
done.resolve(files);
});
});
rq.pipe(stream);
}
if (extention === '.gz') {
rq.on('response', function(res) {
if(res.statusCode !== 200) return;
self.extractTar(res, cachepath).then(self.stripRootFolder).then(function(files) {
done.resolve(files);
});
});
}
return done.promise;
}...
}
fs.mkdirpSync(platform.cache);
self.emit('log', 'Create cache folder in ' + path.resolve(self.options.cacheDir, self._version.version + '
;-' + self._version.flavor));
if(!self.isPlatformCached(name, platform, self._version.version, self._version.flavor)) {
downloads.push(
Downloader.downloadAndUnpack(platform.cache, platform.url)
.catch(function(err){
if(err.statusCode === 404){
self.emit('log', 'ERROR: The version '+self._version.version+ ' (' + self._version
.flavor + ') does not have a corresponding build posted at ' + self.options.downloadUrl + '. Please choose a version
from that list.');
} else {
self.emit('log', err.msg);
}
...extractTar = function (tarstream, destination) {
var done = Promise.defer(),
gunzip = zlib.createGunzip(),
files = [];
tarstream
.pipe(gunzip)
.on('error', function(err){
done.reject(err);
})
.pipe(tar.extract(destination, {
umask: (isWin ? false : 0),
map: function(header) {
files.push({path: path.basename(header.name)});
return header;
}
}))
.on('finish', function() {
done.resolve({files:files, destination:destination});
});
return done.promise;
}...
rq.pipe(stream);
}
if (extention === '.gz') {
rq.on('response', function(res) {
if(res.statusCode !== 200) return;
self.extractTar(res, cachepath).then(self.stripRootFolder).then(function(
files) {
done.resolve(files);
});
});
}
return done.promise;
},
...extractZip = function (zipfile, destination) {
var files = [],
done = Promise.defer();
new DecompressZip(zipfile)
.on('error', function(err){
done.reject(err);
})
.on('extract', function(log) {
// Setup chmodSync to fix permissions
files.forEach(function(file) {
fs.chmodSync(path.join(destination, file.path), file.mode);
});
done.resolve({files:files, destination:destination});
})
.extract({
path: destination,
filter: function(entry) {
files.push({
path: entry.path,
mode: entry.mode.toString(8)
});
return true;
}
});
return done.promise;
}...
});
if (extention === '.zip') {
stream = temp.createWriteStream();
stream.on('close', function() {
if(done.promise.isRejected()) return;
self.extractZip(stream.path, cachepath).then(self.stripRootFolder).then(function
(files) {
done.resolve(files);
});
});
rq.pipe(stream);
}
...stripRootFolder = function (extracted){
var done = Promise.defer(),
files = extracted.files,
destination = extracted.destination,
rootFiles = fs.readdirSync(destination),
fromDir = path.join(destination, rootFiles.length === 1 ? rootFiles[0] : '');
// strip out root folder if it exists
if(rootFiles.length === 1 && fs.statSync(fromDir).isDirectory() ){
// strip folder from files
for (var i = 0; i < files.length; i++) {
var file = files[i];
file.path = path.relative(rootFiles[0], file.path);
if(file.path === '') {
files.splice(i, 1);
i--;
}
}
// move stripped folder to destination
ncp(fromDir, destination, function (err) {
if (err) done.reject();
else rimraf(fromDir, function(){
done.resolve(files);
});
});
} else {
done.resolve(files);
}
return done.promise;
}n/a
closerPathDepth = function (path1, path2) {
if (!path2) { return path1; }
var d1 = this.pathDepth(path1),
d2 = this.pathDepth(path2);
return d1 < d2 ? path1 : path2;
}...
return new Promise(function(resolve, reject) {
if(!matches.length) return reject('No files matching');
matches.forEach(function(file) {
var internalFileName = path.normalize(file);
if (internalFileName.match('package.json')) {
jsonfile = self.closerPathDepth(internalFileName, jsonfile);
package_path = path.normalize(jsonfile.split('package.json')[0] || './');
}
if(!fs.lstatSync(internalFileName).isDirectory()) {
srcFiles.push(internalFileName);
}
});
...copyFile = function (src, dest, _event, options) {
return new Promise(function(resolve, reject) {
options = options || {};
var stats = fs.lstatSync(src);
fs.copy(src, dest, options, function (err) {
if(err) return reject(err);
var retryCount = 0;
var existsCallback = function(exists){
if(exists){
fs.chmod(dest, stats.mode, function(err){
// ignore error
if (err) {
_event.emit('log', 'chmod ' + stats.mode + ' on ' + dest + ' failed after copying, ignoring');
}
resolve();
});
} else if (retryCount++ < 2) {
// This is antipattern!!!
// Callback should be called when the copy is finished!!!!
setTimeout(function(){
fs.exists(dest, existsCallback);
}, 1000);
} else {
reject(new Error("Copied file (" + dest + ") doesn't exist in destination after copying"));
}
};
fs.exists(dest, existsCallback);
});
});
}...
return !/nwjs\.app\/Contents\/Resources\/[^.]+\.lproj(\/|$)/.test(filepath);
};
}
// rename executable to app name
destFile = self.options.appName + path.extname(destFile);
}
copiedFiles.push(Utils.copyFile(file, path.join(platform.releasePath, destFile
), self, options));
platform.files.push(destFile);
});
return;
}
// legacy
...editPlist = function (plistInput, plistOutput, options) {
options = options || {};
// Make sure all required properties are set
[
'CFBundleName',
'CFBundleDisplayName',
'CFBundleVersion',
'CFBundleShortVersionString'
].forEach(function(prop) {
if(!options.hasOwnProperty(prop)) {
throw new Error('Missing macPlist property \'' + prop + '\'');
}
});
// Bundle identifier based on package name
if(options.CFBundleIdentifier === undefined) {
options.CFBundleIdentifier = 'com.nw-builder.' + options.CFBundleName.toLowerCase().replace(/[^a-z\-]/g,'');
}
// Read the input file
return readFile(plistInput, 'utf8')
// Parse it
.then(plist.parse)
// Then overwrite the properties with custom values
.then(function(info) {
// Keep backwards compatibility and handle aliases
Object.keys(options).forEach(function(key) {
var value = options[key];
switch(key) {
case 'mac_bundle_id':
info.CFBundleIdentifier = value;
break;
case 'mac_document_types':
info.CFBundleDocumentTypes = value.map(function(type) {
return {
CFBundleTypeName: type.name,
CFBundleTypeExtensions: type.extensions,
CFBundleTypeRole: type.role,
LSIsAppleDefaultForType: type.isDefault
};
});
break;
default:
info[key] = value;
}
});
// Remove some unwanted properties
if(!(options.hasOwnProperty('mac_document_types') || options.hasOwnProperty('CFBundleDocumentTypes'))) {
info.CFBundleDocumentTypes = [];
}
if(!options.hasOwnProperty('UTExportedTypeDeclarations'))
info.UTExportedTypeDeclarations = [];
// Write output file
return writeFile(plistOutput, plist.build(info));
});
}...
name: self.options.appName,
version: self.options.appVersion,
copyright: self._appPkg.copyright
},
self.options.macPlist
);
allDone.push(Utils.editPlist(PlistPath, PlistPath, plistOptions));
}
});
return Promise.all(allDone);
};
NwBuilder.prototype.handleWinApp = function () {
...generateZipFile = function (files, _event, platformSpecificManifest, zipOptions) {
var destStream = temp.createWriteStream(),
archive = archiver('zip', zipOptions || {});
return new Promise(function(resolve, reject) {
// Resolve on close
destStream.on('close', function () {
resolve(destStream.path);
});
// Reject on Error
archive.on('error', reject);
// Add the files
var filesBulk = [];
files.forEach(function(file){
if(file.dest === 'package.json' && platformSpecificManifest){
archive.append(platformSpecificManifest, {name: 'package.json'});
}
else
{
filesBulk.push({
src: file.src,
data: { name: path.basename(file.dest) },
expand: true,
flatten: true,
dest: path.dirname(file.dest)
});
}
});
archive.bulk(filesBulk);
// Some logs
archive.on('entry', function (file) {
_event.emit('log', 'Zipping ' + file.name);
});
// Pipe the stream
archive.pipe(destStream);
archive.finalize();
});
}...
return;
}
// create (or don't create) a ZIP for multiple platforms
new Promise(function(resolve, reject) {
if(numberOfPlatformsWithoutOverrides > 1){
Utils.generateZipFile(self._files, self, null, zipOptions).then(function (zip
) {
resolve(zip);
}, reject);
}
else {
resolve();
}
})
...getFileList = function (fileglob) {
var self = this,
jsonfile, destFiles = [],
srcFiles = [],
package_path,
matches = Glob(fileglob);
return new Promise(function(resolve, reject) {
if(!matches.length) return reject('No files matching');
matches.forEach(function(file) {
var internalFileName = path.normalize(file);
if (internalFileName.match('package.json')) {
jsonfile = self.closerPathDepth(internalFileName, jsonfile);
package_path = path.normalize(jsonfile.split('package.json')[0] || './');
}
if(!fs.lstatSync(internalFileName).isDirectory()) {
srcFiles.push(internalFileName);
}
});
if (!jsonfile) {
return reject('Could not find a package.json in your src folder');
}
srcFiles.forEach(function(file) {
destFiles.push({
src: file,
dest: file.replace(package_path, '')
});
});
resolve({
files: destFiles,
json: jsonfile
});
});
}...
return hasCallback ? true : done.promise;
};
NwBuilder.prototype.checkFiles = function () {
var self = this;
return Utils.getFileList(this.options.files).then(function (data) {
self._appPkg = data.json;
self._files = data.files;
return Utils.getPackageInfo(self._appPkg).then(function (appPkg) {
self._appPkg = appPkg;
...getPackageInfo = function (path) {
return new Promise(function(resolve, reject) {
fs.readFile(path, function (err, data) {
if (err) return reject(err);
try {
var appPkg = JSON.parse(data);
} catch(e) {
reject("Invalid package.json: " + e + "\nMake sure the file is encoded as utf-8");
return;
}
if (!appPkg.name || !appPkg.version) {
reject("Please make sure that your project's package.json includes a version and a name value");
} else {
resolve(appPkg);
}
});
});
}...
var self = this;
return Utils.getFileList(this.options.files).then(function (data) {
self._appPkg = data.json;
self._files = data.files;
return Utils.getPackageInfo(self._appPkg).then(function (appPkg) {
self._appPkg = appPkg;
if(!self.options.appName || !self.options.appVersion) {
self.options.appName = (self.options.appName ? self.options.appName : appPkg.name);
self.options.appVersion = (self.options.appVersion ? self.options.appVersion : appPkg.version);
}
});
...getPlistOptions = function (parsedParams, custom) {
var obj = {};
if(parsedParams.name) {
obj.CFBundleName = parsedParams.name;
obj.CFBundleDisplayName = parsedParams.name;
}
if(parsedParams.version) {
obj.CFBundleVersion = parsedParams.version;
obj.CFBundleShortVersionString = 'Version ' + parsedParams.version;
}
if(parsedParams.copyright) {
obj.NSHumanReadableCopyright = parsedParams.copyright;
}
return _.merge(obj, custom);
}...
var PlistPath = path.resolve(platform.releasePath, self.options.appName+'.app', 'Contents', 'Info.plist
');
// If the macPlist is a string we just copy the file
if(typeof self.options.macPlist === 'string') {
allDone.push(Utils.copyFile(self.options.macPlist, PlistPath, self));
} else {
// Setup the Plist
var plistOptions = Utils.getPlistOptions(
{
name: self.options.appName,
version: self.options.appVersion,
copyright: self._appPkg.copyright
},
self.options.macPlist
);
...mergeFiles = function (app, zipfile, chmod) {
// we need to pipe the app into the zipfile and chmod it
return new Promise(function(resolve, reject) {
var zipStream = fs.createReadStream(zipfile),
writeStream = fs.createWriteStream(app, {flags:'a'});
zipStream.on('error', reject);
writeStream.on('error', reject);
writeStream.on('finish', function () {
if(chmod) {
fs.chmodSync(app, chmod);
}
resolve();
});
zipStream.pipe(writeStream);
});
}...
path.resolve(self.getResourcesDirectoryPath(platform), 'app.nw'),
self
));
} else {
var executableToMergeWith = self._version.isLegacy ? _.first(platform.files) : self.getExecutableName(name);
// We cat the app.nw file into the .exe / nw
copyPromises.push(Utils.mergeFiles(
path.resolve(platform.releasePath, executableToMergeWith),
self.getZipFile(name),
platform.chmod
));
}
});
...pathDepth = function (absolutePath) {
return absolutePath.split(path.sep).length;
}...
json: jsonfile
});
});
},
closerPathDepth: function(path1, path2) {
if (!path2) { return path1; }
var d1 = this.pathDepth(path1),
d2 = this.pathDepth(path2);
return d1 < d2 ? path1 : path2;
},
pathDepth: function(absolutePath) {
return absolutePath.split(path.sep).length;
},
...getLatestVersion = function (downloadUrl, manifestUrl, flavor) {
return getManifest(manifestUrl)
.then(function(manifest) {
return {
desiredVersion: manifest.stable.replace('v', ''),
downloadUrl: downloadUrl,
manifestUrl: manifestUrl,
flavor: flavor
}
})
.then(this.getVersion);
}...
};
NwBuilder.prototype.resolveLatestVersion = function () {
var self = this;
if(self.options.version !== 'latest') return Promise.resolve();
return NwVersions.getLatestVersion(self.options.downloadUrl, self.options.manifestUrl
, self.options.flavor).then(function(latestVersion){
self.emit('log', 'Latest Version: v' + latestVersion.version);
self.options.version = latestVersion.version;
return latestVersion;
});
};
NwBuilder.prototype.checkVersion = function () {
...getVersion = function (args){
return (isLegacyVersion(args.desiredVersion) ? getLegacyVersions : getVersionsFromManifest)(args.downloadUrl, args.manifestUrl
)
.then(function(versions) {
var version = versions.findIndex(function(version){
return version.version === args.desiredVersion;
});
return version >= 0
? Promise.resolve(versions[version])
: Promise.reject('Version ' + args.desiredVersion + ' not found.');
});
}...
if(!semver.valid(version)){
return Promise.reject('The version ' + version + ' is not valid.');
}
var getVersionFromManifest = function(){
return NwVersions.getVersion({
desiredVersion: version,
downloadUrl: self.options.downloadUrl,
manifestUrl: self.options.manifestUrl,
flavor: flavor
});
};
var getVersion;
...getVersions = function (downloadUrl, manifestUrl, flavor){
return Promise.all([
getVersionsFromManifest(downloadUrl, manifestUrl, flavor),
getLegacyVersions(downloadUrl, flavor)
])
.then(function(versionLists){
return versionLists[0].concat(versionLists[1]);
});
}n/a