Builder.ts 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919
  1. import { dirname, basename, resolve } from 'path';
  2. import * as semver from 'semver';
  3. import { ensureDir, emptyDir, readFile, readJson, writeFile, copy, remove, rename, chmod, createReadStream, createWriteStream, readdir } from 'fs-extra';
  4. import * as Bluebird from 'bluebird';
  5. const debug = require('debug')('build:builder');
  6. const globby = require('globby');
  7. const rcedit = require('rcedit');
  8. const plist = require('plist');
  9. import { Downloader } from './Downloader';
  10. import { FFmpegDownloader } from './FFmpegDownloader';
  11. import { BuildConfig } from './config';
  12. import { NsisVersionInfo, DownloaderBase } from './common';
  13. import { NsisComposer, NsisDiffer, Nsis7Zipper, nsisBuild } from './nsis-gen';
  14. import { mergeOptions, findExecutable, findFFmpeg, findRuntimeRoot, findExcludableDependencies, tmpName, tmpFile, tmpDir, fixWindowsVersion, copyFileAsync, extractGeneric, compress } from './util';
  15. export interface IParseOutputPatternOptions {
  16. name: string;
  17. version: string;
  18. platform: string;
  19. arch: string;
  20. }
  21. export interface IBuilderOptions {
  22. win?: boolean;
  23. mac?: boolean;
  24. linux?: boolean;
  25. x86?: boolean;
  26. x64?: boolean;
  27. tasks?: string[];
  28. chromeApp?: boolean;
  29. mirror?: string;
  30. concurrent?: boolean;
  31. mute?: boolean;
  32. destination?: string;
  33. }
  34. export class Builder {
  35. public static DEFAULT_OPTIONS: IBuilderOptions = {
  36. win: false,
  37. mac: false,
  38. linux: false,
  39. x86: false,
  40. x64: false,
  41. tasks: [],
  42. chromeApp: false,
  43. mirror: Downloader.DEFAULT_OPTIONS.mirror,
  44. concurrent: false,
  45. mute: true,
  46. destination: DownloaderBase.DEFAULT_DESTINATION,
  47. };
  48. public options: IBuilderOptions;
  49. constructor(options: IBuilderOptions = {}, public dir: string) {
  50. this.options = mergeOptions(Builder.DEFAULT_OPTIONS, options);
  51. debug('in constructor', 'dir', dir);
  52. debug('in constructor', 'options', this.options);
  53. }
  54. public async build() {
  55. const tasks: string[][] = [];
  56. [ 'win', 'mac', 'linux' ].map((platform) => {
  57. [ 'x86', 'x64' ].map((arch) => {
  58. if((<any>this.options)[platform] && (<any>this.options)[arch]) {
  59. tasks.push([ platform, arch ]);
  60. }
  61. });
  62. });
  63. for(const task of this.options.tasks) {
  64. const [ platform, arch ] = task.split('-');
  65. if([ 'win', 'mac', 'linux' ].indexOf(platform) >= 0) {
  66. if([ 'x86', 'x64' ].indexOf(arch) >= 0) {
  67. tasks.push([ platform, arch ]);
  68. }
  69. }
  70. }
  71. if(!this.options.mute) {
  72. console.info('Starting building tasks...', {
  73. tasks,
  74. concurrent: this.options.concurrent,
  75. });
  76. }
  77. if(tasks.length == 0) {
  78. throw new Error('ERROR_NO_TASK');
  79. }
  80. if(this.options.concurrent) {
  81. await Bluebird.map(tasks, async ([ platform, arch ]) => {
  82. const options: any = {};
  83. options[platform] = true;
  84. options[arch] = true;
  85. options.mirror = this.options.mirror;
  86. options.concurrent = false;
  87. options.mute = true;
  88. const builder = new Builder(options, this.dir);
  89. const started = Date.now();
  90. if(!this.options.mute) {
  91. console.info(`Building for ${ platform }, ${ arch } starts...`);
  92. }
  93. await builder.build();
  94. if(!this.options.mute) {
  95. console.info(`Building for ${ platform }, ${ arch } ends within ${ this.getTimeDiff(started) }s.`);
  96. }
  97. });
  98. }
  99. else {
  100. const pkg: any = await readJson(resolve(this.dir, this.options.chromeApp ? 'manifest.json' : 'package.json'));
  101. const config = new BuildConfig(pkg);
  102. debug('in build', 'config', config);
  103. for(const [ platform, arch ] of tasks) {
  104. const started = Date.now();
  105. if(!this.options.mute) {
  106. console.info(`Building for ${ platform }, ${ arch } starts...`);
  107. }
  108. try {
  109. await this.buildTask(platform, arch, pkg, config);
  110. }
  111. catch(err) {
  112. console.warn(err);
  113. }
  114. if(!this.options.mute) {
  115. console.info(`Building for ${ platform }, ${ arch } ends within ${ this.getTimeDiff(started) }s.`);
  116. }
  117. }
  118. }
  119. }
  120. protected getTimeDiff(started: number) {
  121. return ((Date.now() - started) / 1000).toFixed(2);
  122. }
  123. protected async writeStrippedManifest(path: string, pkg: any, config: BuildConfig) {
  124. const json: any = {};
  125. for(const key in pkg) {
  126. if(pkg.hasOwnProperty(key) && config.strippedProperties.indexOf(key) === -1) {
  127. if (config.overriddenProperties && config.overriddenProperties.hasOwnProperty(key) ) {
  128. json[key] = config.overriddenProperties[key];
  129. } else {
  130. json[key] = pkg[key];
  131. }
  132. }
  133. }
  134. await writeFile(path, JSON.stringify(json));
  135. }
  136. protected parseOutputPattern(pattern: string, options: IParseOutputPatternOptions, pkg: any, config: BuildConfig) {
  137. return pattern.replace(/\$\{\s*(\w+)\s*\}/g, (match: string, key: string) => {
  138. switch(key.toLowerCase()) {
  139. case 'name':
  140. return options.name;
  141. case 'version':
  142. return options.version;
  143. case 'platform':
  144. return options.platform;
  145. case 'arch':
  146. return options.arch;
  147. default:
  148. throw new Error('ERROR_KEY_UNKNOWN');
  149. }
  150. });
  151. }
  152. protected combineExecutable(executable: string, nwFile: string) {
  153. return new Promise((resolve, reject) => {
  154. const nwStream = createReadStream(nwFile);
  155. const stream = createWriteStream(executable, {
  156. flags: 'a',
  157. });
  158. nwStream.on('error', reject);
  159. stream.on('error', reject);
  160. stream.on('finish', resolve);
  161. nwStream.pipe(stream);
  162. });
  163. }
  164. protected readPlist(path: string): Promise<any> {
  165. return readFile(path, {
  166. encoding: 'utf-8',
  167. })
  168. .then(data => plist.parse(data));
  169. }
  170. protected writePlist(path: string, p: any) {
  171. return writeFile(path, plist.build(p));
  172. }
  173. protected updateWinResources(targetDir: string, appRoot: string, pkg: any, config: BuildConfig) {
  174. const pathResolve = resolve;
  175. return new Promise((resolve, reject) => {
  176. const path = pathResolve(targetDir, 'nw.exe');
  177. const rc = {
  178. 'product-version': fixWindowsVersion(config.win.productVersion),
  179. 'file-version': fixWindowsVersion(config.win.fileVersion),
  180. 'version-string': {
  181. ProductName: config.win.productName,
  182. CompanyName: config.win.companyName,
  183. FileDescription: config.win.fileDescription,
  184. LegalCopyright: config.win.copyright,
  185. ...config.win.versionStrings,
  186. },
  187. 'icon': config.win.icon ? pathResolve(this.dir, config.win.icon) : undefined,
  188. };
  189. rcedit(path, rc, (err: Error) => err ? reject(err) : resolve());
  190. });
  191. }
  192. protected renameWinApp(targetDir: string, appRoot: string, pkg: any, config: BuildConfig) {
  193. const src = resolve(targetDir, 'nw.exe');
  194. const dest = resolve(targetDir, `${ config.win.productName }.exe`);
  195. return rename(src, dest);
  196. }
  197. protected async updatePlist(targetDir: string, appRoot: string, pkg: any, config: BuildConfig) {
  198. const path = resolve(targetDir, './nwjs.app/Contents/Info.plist');
  199. const plist = await this.readPlist(path);
  200. plist.CFBundleIdentifier = config.appId;
  201. plist.CFBundleName = config.mac.name;
  202. plist.CFBundleExecutable = config.mac.displayName;
  203. plist.CFBundleDisplayName = config.mac.displayName;
  204. plist.CFBundleVersion = config.mac.version;
  205. plist.CFBundleShortVersionString = config.mac.version;
  206. for(const key in config.mac.plistStrings) {
  207. if(config.mac.plistStrings.hasOwnProperty(key)) {
  208. plist[key] = config.mac.plistStrings[key];
  209. }
  210. }
  211. await this.writePlist(path, plist);
  212. }
  213. protected async updateHelperPlist(targetDir: string, appRoot: string, pkg: any, config: BuildConfig) {
  214. if (!this.canRenameMacHelperApp(pkg, config)) {
  215. return;
  216. }
  217. const helperPath = await this.findMacHelperApp(targetDir);
  218. const path = resolve(helperPath, 'Contents/Info.plist');
  219. const plist = await this.readPlist(path);
  220. const bin = pkg.product_string + ' Helper';
  221. plist.CFBundleIdentifier = config.appId + '.helper';
  222. plist.CFBundleDisplayName = bin;
  223. plist.CFBundleExecutable = bin;
  224. plist.CFBundleName = bin;
  225. await this.writePlist(path, plist);
  226. }
  227. protected async updateMacIcons(targetDir: string, appRoot: string, pkg: any, config: BuildConfig) {
  228. const copyIcon = async (iconPath: string, dest: string) => {
  229. if(!iconPath) {
  230. // use the default
  231. return;
  232. }
  233. await copy(resolve(this.dir, iconPath), dest);
  234. };
  235. await copyIcon(config.mac.icon, resolve(targetDir, './nwjs.app/Contents/Resources/app.icns'));
  236. await copyIcon(config.mac.documentIcon, resolve(targetDir, './nwjs.app/Contents/Resources/document.icns'));
  237. }
  238. protected async fixMacMeta(targetDir: string, appRoot: string, pkg: any, config: BuildConfig) {
  239. const files = await globby([ '**/InfoPlist.strings' ], {
  240. cwd: targetDir,
  241. });
  242. for(const file of files) {
  243. const path = resolve(targetDir, file);
  244. // Different versions has different encodings for `InforPlist.strings`.
  245. // We determine encoding by evaluating bytes of `CF` here.
  246. const data = await readFile(path);
  247. const encoding = data.indexOf(Buffer.from('43004600', 'hex')) >= 0
  248. ? 'ucs2' : 'utf-8';
  249. const strings = data.toString(encoding);
  250. const newStrings = strings.replace(/([A-Za-z]+)\s+=\s+"(.+?)";/g, (match: string, key: string, value: string) => {
  251. switch(key) {
  252. case 'CFBundleName':
  253. return `${ key } = "${ config.mac.name }";`;
  254. case 'CFBundleDisplayName':
  255. return `${ key } = "${ config.mac.displayName }";`;
  256. case 'CFBundleGetInfoString':
  257. return `${ key } = "${ config.mac.version }";`;
  258. case 'NSContactsUsageDescription':
  259. return `${ key } = "${ config.mac.description }";`;
  260. case 'NSHumanReadableCopyright':
  261. return `${ key } = "${ config.mac.copyright }";`;
  262. default:
  263. return `${ key } = "${ value }";`;
  264. }
  265. });
  266. await writeFile(path, Buffer.from(newStrings, encoding));
  267. }
  268. }
  269. protected async renameMacApp(targetDir: string, appRoot: string, pkg: any, config: BuildConfig) {
  270. const app = resolve(targetDir, 'nwjs.app');
  271. const bin = resolve(app, './Contents/MacOS/nwjs');
  272. let dest = bin.replace(/nwjs$/, config.mac.displayName);
  273. await rename(bin, dest);
  274. dest = app.replace(/nwjs\.app$/, `${config.mac.displayName}.app`);
  275. return rename(app, dest);
  276. }
  277. protected async renameMacHelperApp(targetDir: string, appRoot: string, pkg: any, config: BuildConfig) {
  278. if (!this.canRenameMacHelperApp(pkg, config)) {
  279. return;
  280. }
  281. const app = await this.findMacHelperApp(targetDir);
  282. const bin = resolve(app, './Contents/MacOS/nwjs Helper');
  283. let dest = bin.replace(/nwjs Helper$/, `${pkg.product_string} Helper`);
  284. await rename(bin, dest);
  285. dest = app.replace(/nwjs Helper\.app$/, `${pkg.product_string} Helper.app`);
  286. return rename(app, dest);
  287. }
  288. protected canRenameMacHelperApp(pkg: any, config: BuildConfig): boolean {
  289. if (semver.lt(config.nwVersion, '0.24.4')) {
  290. // this version doesn't support Helper app renaming.
  291. return false;
  292. }
  293. if (!pkg.product_string) {
  294. // we can't rename the Helper app as we don't have a new name.
  295. return false;
  296. }
  297. return true;
  298. }
  299. protected async findMacHelperApp(targetDir: string): Promise<string> {
  300. const path = resolve(targetDir, './nwjs.app/Contents/Versions');
  301. // what version are we actually dealing with?
  302. const versions = await readdir(path);
  303. if (!versions || versions.length !== 1) {
  304. throw new Error("Can't rename the Helper as we can't find it");
  305. }
  306. return resolve(path, versions[0], 'nwjs Helper.app');
  307. }
  308. protected async fixLinuxMode(targetDir: string, appRoot: string, pkg: any, config: BuildConfig) {
  309. const path = resolve(targetDir, 'nw');
  310. await chmod(path, 0o744);
  311. }
  312. protected renameLinuxApp(targetDir: string, appRoot: string, pkg: any, config: BuildConfig) {
  313. const src = resolve(targetDir, 'nw');
  314. const dest = resolve(targetDir, `${ pkg.name }`);
  315. return rename(src, dest);
  316. }
  317. protected async prepareWinBuild(targetDir: string, appRoot: string, pkg: any, config: BuildConfig) {
  318. await this.updateWinResources(targetDir, appRoot, pkg, config);
  319. }
  320. protected async prepareMacBuild(targetDir: string, appRoot: string, pkg: any, config: BuildConfig) {
  321. await this.updateHelperPlist(targetDir, appRoot, pkg, config);
  322. await this.updatePlist(targetDir, appRoot, pkg, config);
  323. await this.updateMacIcons(targetDir, appRoot, pkg, config);
  324. await this.fixMacMeta(targetDir, appRoot, pkg, config);
  325. }
  326. protected async prepareLinuxBuild(targetDir: string, appRoot: string, pkg: any, config: BuildConfig) {
  327. await this.fixLinuxMode(targetDir, appRoot, pkg, config);
  328. }
  329. protected async copyFiles(platform: string, targetDir: string, appRoot: string, pkg: any, config: BuildConfig) {
  330. const generalExcludes = [
  331. '**/node_modules/.bin',
  332. '**/node_modules/*/{ example, examples, test, tests }',
  333. '**/{ .DS_Store, .git, .hg, .svn, *.log }',
  334. ];
  335. const dependenciesExcludes = await findExcludableDependencies(this.dir, pkg)
  336. .then((excludable) => {
  337. return excludable.map(excludable => [ excludable, `${ excludable }/**/*` ]);
  338. })
  339. .then((excludes) => {
  340. return Array.prototype.concat.apply([], excludes);
  341. });
  342. debug('in copyFiles', 'dependenciesExcludes', dependenciesExcludes);
  343. const ignore = [
  344. ...config.excludes,
  345. ...generalExcludes,
  346. ...dependenciesExcludes,
  347. ...[ config.output, `${ config.output }/**/*` ]
  348. ];
  349. debug('in copyFiles', 'ignore', ignore);
  350. const files: string[] = await globby(config.files, {
  351. cwd: this.dir,
  352. // TODO: https://github.com/isaacs/node-glob#options, warn for cyclic links.
  353. follow: true,
  354. mark: true,
  355. ignore,
  356. });
  357. debug('in copyFiles', 'config.files', config.files);
  358. debug('in copyFiles', 'files', files);
  359. if(config.packed) {
  360. switch(platform) {
  361. case 'win32':
  362. case 'win':
  363. case 'linux':
  364. const nwFile = await tmpName({
  365. postfix: '.zip',
  366. });
  367. await compress(this.dir, files.filter((file) => !file.endsWith('/')), 'zip', nwFile);
  368. const { path: tempDir } = await tmpDir();
  369. await this.writeStrippedManifest(resolve(tempDir, 'package.json'), pkg, config);
  370. await compress(tempDir, [ './package.json' ], 'zip', nwFile);
  371. await remove(tempDir);
  372. const executable = await findExecutable(platform, targetDir);
  373. await this.combineExecutable(executable, nwFile);
  374. await remove(nwFile);
  375. break;
  376. case 'darwin':
  377. case 'osx':
  378. case 'mac':
  379. for(const file of files) {
  380. await copyFileAsync(resolve(this.dir, file), resolve(appRoot, file));
  381. }
  382. await this.writeStrippedManifest(resolve(appRoot, 'package.json'), pkg, config);
  383. break;
  384. default:
  385. throw new Error('ERROR_UNKNOWN_PLATFORM');
  386. }
  387. }
  388. else {
  389. for(const file of files) {
  390. await copyFileAsync(resolve(this.dir, file), resolve(appRoot, file));
  391. }
  392. await this.writeStrippedManifest(resolve(appRoot, 'package.json'), pkg, config);
  393. }
  394. }
  395. protected async integrateFFmpeg(platform: string, arch: string, targetDir: string, pkg: any, config: BuildConfig) {
  396. const downloader = new FFmpegDownloader({
  397. platform, arch,
  398. version: config.nwVersion,
  399. useCaches: true,
  400. showProgress: this.options.mute ? false : true,
  401. destination: this.options.destination,
  402. });
  403. if(!this.options.mute) {
  404. console.info('Fetching FFmpeg prebuilt...', {
  405. platform: downloader.options.platform,
  406. arch: downloader.options.arch,
  407. version: downloader.options.version,
  408. });
  409. }
  410. const ffmpegDir = await downloader.fetchAndExtract();
  411. const src = await findFFmpeg(platform, ffmpegDir);
  412. const dest = await findFFmpeg(platform, targetDir);
  413. await copy(src, dest);
  414. }
  415. protected async buildNsisDiffUpdater(platform: string, arch: string, versionInfo: NsisVersionInfo, fromVersion: string, toVersion: string, pkg: any, config: BuildConfig) {
  416. const diffNsis = resolve(this.dir, config.output, `${ pkg.name }-${ toVersion }-from-${ fromVersion }-${ platform }-${ arch }-Update.exe`);
  417. const fromDir = resolve(this.dir, config.output, (await versionInfo.getVersion(fromVersion)).source);
  418. const toDir = resolve(this.dir, config.output, (await versionInfo.getVersion(toVersion)).source);
  419. const data = await (new NsisDiffer(fromDir, toDir, {
  420. // Basic.
  421. appName: config.win.productName,
  422. companyName: config.win.companyName,
  423. description: config.win.fileDescription,
  424. version: fixWindowsVersion(config.win.productVersion),
  425. copyright: config.win.copyright,
  426. icon: config.nsis.icon ? resolve(this.dir, config.nsis.icon) : undefined,
  427. unIcon: config.nsis.unIcon ? resolve(this.dir, config.nsis.unIcon) : undefined,
  428. // Compression.
  429. compression: 'lzma',
  430. solid: true,
  431. languages: config.nsis.languages,
  432. installDirectory: config.nsis.installDirectory,
  433. // Output.
  434. output: diffNsis,
  435. })).make();
  436. const script = await tmpName();
  437. await writeFile(script, data);
  438. await nsisBuild(toDir, script, {
  439. mute: this.options.mute,
  440. });
  441. await remove(script);
  442. await versionInfo.addUpdater(toVersion, fromVersion, arch, diffNsis);
  443. }
  444. protected async buildDirTarget(platform: string, arch: string, runtimeDir: string, pkg: any, config: BuildConfig): Promise<string> {
  445. const targetDir = resolve(this.dir, config.output, this.parseOutputPattern(config.outputPattern, {
  446. name: pkg.name,
  447. version: pkg.version,
  448. platform, arch,
  449. }, pkg, config));
  450. const runtimeRoot = await findRuntimeRoot(platform, runtimeDir);
  451. const appRoot = resolve(targetDir, (() => {
  452. switch(platform) {
  453. case 'win32':
  454. case 'win':
  455. case 'linux':
  456. return './';
  457. case 'darwin':
  458. case 'osx':
  459. case 'mac':
  460. return './nwjs.app/Contents/Resources/app.nw/';
  461. default:
  462. throw new Error('ERROR_UNKNOWN_PLATFORM');
  463. }
  464. })());
  465. await emptyDir(targetDir);
  466. await copy(runtimeRoot, targetDir, {
  467. //dereference: true,
  468. });
  469. if(config.ffmpegIntegration) {
  470. await this.integrateFFmpeg(platform, arch, targetDir, pkg, config);
  471. }
  472. await ensureDir(appRoot);
  473. // Copy before refining might void the effort.
  474. switch(platform) {
  475. case 'win32':
  476. case 'win':
  477. await this.prepareWinBuild(targetDir, appRoot, pkg, config);
  478. await this.copyFiles(platform, targetDir, appRoot, pkg, config);
  479. await this.renameWinApp(targetDir, appRoot, pkg, config);
  480. break;
  481. case 'darwin':
  482. case 'osx':
  483. case 'mac':
  484. await this.prepareMacBuild(targetDir, appRoot, pkg, config);
  485. await this.copyFiles(platform, targetDir, appRoot, pkg, config);
  486. // rename Helper before main app rename.
  487. await this.renameMacHelperApp(targetDir, appRoot, pkg, config);
  488. await this.renameMacApp(targetDir, appRoot, pkg, config);
  489. break;
  490. case 'linux':
  491. await this.prepareLinuxBuild(targetDir, appRoot, pkg, config);
  492. await this.copyFiles(platform, targetDir, appRoot, pkg, config);
  493. await this.renameLinuxApp(targetDir, appRoot, pkg, config);
  494. break;
  495. default:
  496. throw new Error('ERROR_UNKNOWN_PLATFORM');
  497. }
  498. return targetDir;
  499. }
  500. protected async buildArchiveTarget(type: string, sourceDir: string) {
  501. const targetArchive = resolve(dirname(sourceDir), `${ basename(sourceDir) }.${ type }`);
  502. await remove(targetArchive);
  503. const files = await globby([ '**/*' ], {
  504. cwd: sourceDir,
  505. });
  506. await compress(sourceDir, files, type, targetArchive);
  507. return targetArchive;
  508. }
  509. protected async buildNsisTarget(platform: string, arch: string, sourceDir: string, pkg: any, config: BuildConfig) {
  510. if(platform != 'win') {
  511. if(!this.options.mute) {
  512. console.info(`Skip building nsis target for ${ platform }.`);
  513. }
  514. return;
  515. }
  516. const versionInfo = new NsisVersionInfo(resolve(this.dir, config.output, 'versions.nsis.json'));
  517. const targetNsis = resolve(dirname(sourceDir), `${ basename(sourceDir) }-Setup.exe`);
  518. const data = await (new NsisComposer({
  519. // Basic.
  520. appName: config.win.productName,
  521. companyName: config.win.companyName,
  522. description: config.win.fileDescription,
  523. version: fixWindowsVersion(config.win.productVersion),
  524. copyright: config.win.copyright,
  525. icon: config.nsis.icon ? resolve(this.dir, config.nsis.icon) : undefined,
  526. unIcon: config.nsis.unIcon ? resolve(this.dir, config.nsis.unIcon) : undefined,
  527. // Compression.
  528. compression: 'lzma',
  529. solid: true,
  530. languages: config.nsis.languages,
  531. installDirectory: config.nsis.installDirectory,
  532. // Output.
  533. output: targetNsis,
  534. })).make();
  535. const script = await tmpName();
  536. await writeFile(script, data);
  537. await nsisBuild(sourceDir, script, {
  538. mute: this.options.mute,
  539. });
  540. await remove(script);
  541. await versionInfo.addVersion(pkg.version, '', sourceDir);
  542. await versionInfo.addInstaller(pkg.version, arch, targetNsis);
  543. if(config.nsis.diffUpdaters) {
  544. for(const version of await versionInfo.getVersions()) {
  545. if(semver.gt(pkg.version, version)) {
  546. await this.buildNsisDiffUpdater(platform, arch, versionInfo, version, pkg.version, pkg, config);
  547. }
  548. }
  549. }
  550. await versionInfo.save();
  551. }
  552. protected async buildNsis7zTarget(platform: string, arch: string, sourceDir: string, pkg: any, config: BuildConfig) {
  553. if(platform != 'win') {
  554. if(!this.options.mute) {
  555. console.info(`Skip building nsis7z target for ${ platform }.`);
  556. }
  557. return;
  558. }
  559. const sourceArchive = await this.buildArchiveTarget('7z', sourceDir);
  560. const versionInfo = new NsisVersionInfo(resolve(this.dir, config.output, 'versions.nsis.json'));
  561. const targetNsis = resolve(dirname(sourceDir), `${ basename(sourceDir) }-Setup.exe`);
  562. const data = await (new Nsis7Zipper(sourceArchive, {
  563. // Basic.
  564. appName: config.win.productName,
  565. companyName: config.win.companyName,
  566. description: config.win.fileDescription,
  567. version: fixWindowsVersion(config.win.productVersion),
  568. copyright: config.win.copyright,
  569. icon: config.nsis.icon ? resolve(this.dir, config.nsis.icon) : undefined,
  570. unIcon: config.nsis.unIcon ? resolve(this.dir, config.nsis.unIcon) : undefined,
  571. // Compression.
  572. compression: 'lzma',
  573. solid: true,
  574. languages: config.nsis.languages,
  575. installDirectory: config.nsis.installDirectory,
  576. // Output.
  577. output: targetNsis,
  578. })).make();
  579. const script = await tmpName();
  580. await writeFile(script, data);
  581. await nsisBuild(sourceDir, script, {
  582. mute: this.options.mute,
  583. });
  584. await remove(script);
  585. await versionInfo.addVersion(pkg.version, '', sourceDir);
  586. await versionInfo.addInstaller(pkg.version, arch, targetNsis);
  587. if(config.nsis.diffUpdaters) {
  588. for(const version of await versionInfo.getVersions()) {
  589. if(semver.gt(pkg.version, version)) {
  590. await this.buildNsisDiffUpdater(platform, arch, versionInfo, version, pkg.version, pkg, config);
  591. }
  592. }
  593. }
  594. await versionInfo.save();
  595. }
  596. protected async buildTask(platform: string, arch: string, pkg: any, config: BuildConfig) {
  597. if(platform === 'mac' && arch === 'x86' && !config.nwVersion.includes('0.12.3')) {
  598. if(!this.options.mute) {
  599. console.info(`The NW.js binary for ${ platform }, ${ arch } isn't available for ${ config.nwVersion }, skipped.`);
  600. }
  601. throw new Error('ERROR_TASK_MAC_X86_SKIPPED');
  602. }
  603. const downloader = new Downloader({
  604. platform, arch,
  605. version: config.nwVersion,
  606. flavor: config.nwFlavor,
  607. mirror: this.options.mirror,
  608. useCaches: true,
  609. showProgress: this.options.mute ? false : true,
  610. destination: this.options.destination,
  611. });
  612. if(!this.options.mute) {
  613. console.info('Fetching NW.js binary...', {
  614. platform: downloader.options.platform,
  615. arch: downloader.options.arch,
  616. version: downloader.options.version,
  617. flavor: downloader.options.flavor,
  618. });
  619. }
  620. const runtimeDir = await downloader.fetchAndExtract();
  621. if(!this.options.mute) {
  622. console.info('Building targets...');
  623. }
  624. const started = Date.now();
  625. if(!this.options.mute) {
  626. console.info(`Building directory target starts...`);
  627. }
  628. const targetDir = await this.buildDirTarget(platform, arch, runtimeDir, pkg, config);
  629. if(!this.options.mute) {
  630. console.info(`Building directory target ends within ${ this.getTimeDiff(started) }s.`);
  631. }
  632. // TODO: Consider using `Bluebird.map` to enable concurrent target building.
  633. for(const target of config.targets) {
  634. const started = Date.now();
  635. switch(target) {
  636. case 'zip':
  637. case '7z':
  638. if(!this.options.mute) {
  639. console.info(`Building ${ target } archive target starts...`);
  640. }
  641. await this.buildArchiveTarget(target, targetDir);
  642. if(!this.options.mute) {
  643. console.info(`Building ${ target } archive target ends within ${ this.getTimeDiff(started) }s.`);
  644. }
  645. break;
  646. case 'nsis':
  647. if(!this.options.mute) {
  648. console.info(`Building nsis target starts...`);
  649. }
  650. await this.buildNsisTarget(platform, arch, targetDir, pkg, config);
  651. if(!this.options.mute) {
  652. console.info(`Building nsis target ends within ${ this.getTimeDiff(started) }s.`);
  653. }
  654. break;
  655. case 'nsis7z':
  656. if(!this.options.mute) {
  657. console.info(`Building nsis7z target starts...`);
  658. }
  659. await this.buildNsis7zTarget(platform, arch, targetDir, pkg, config);
  660. if(!this.options.mute) {
  661. console.info(`Building nsis7z target ends within ${ this.getTimeDiff(started) }s.`);
  662. }
  663. break;
  664. default:
  665. throw new Error('ERROR_UNKNOWN_TARGET');
  666. }
  667. }
  668. }
  669. }