#!/usr/bin/env node /** * Builds app binaries for Mac, Windows, and Linux. */ const cp = require('child_process') const electronPackager = require('electron-packager') const fs = require('fs') const minimist = require('minimist') const os = require('os') const path = require('path') const rimraf = require('rimraf') const series = require('run-series') const zip = require('cross-zip') const config = require('../src/config') const pkg = require('../package.json') const BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION const BUILD_PATH = path.join(config.ROOT_PATH, 'build') const DIST_PATH = path.join(config.ROOT_PATH, 'dist') const NODE_MODULES_PATH = path.join(config.ROOT_PATH, 'node_modules') const argv = minimist(process.argv.slice(2), { boolean: [ 'sign' ], default: { package: 'all', sign: false }, string: [ 'package' ] }) function build () { console.log('Installing node_modules...') rimraf.sync(NODE_MODULES_PATH) cp.execSync('npm ci', { stdio: 'inherit' }) console.log('Nuking dist/ and build/...') rimraf.sync(DIST_PATH) rimraf.sync(BUILD_PATH) console.log('Build: Transpiling to ES5...') cp.execSync('npm run build', { NODE_ENV: 'production', stdio: 'inherit' }) console.log('Build: Transpiled to ES5.') const platform = argv._[0] if (platform === 'darwin') { buildDarwin(printDone) } else if (platform === 'win32') { buildWin32(printDone) } else if (platform === 'linux') { buildLinux(printDone) } else { buildDarwin(function (err) { printDone(err) buildWin32(function (err) { printDone(err) buildLinux(printDone) }) }) } } const all = { // The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata // property on Windows, and `NSHumanReadableCopyright` on Mac. appCopyright: config.APP_COPYRIGHT, // The release version of the application. Maps to the `ProductVersion` metadata // property on Windows, and `CFBundleShortVersionString` on Mac. appVersion: pkg.version, // Package the application's source code into an archive, using Electron's archive // format. Mitigates issues around long path names on Windows and slightly speeds up // require(). asar: { // A glob expression, that unpacks the files with matching names to the // "app.asar.unpacked" directory. unpack: 'WebTorrent*' }, // The build version of the application. Maps to the FileVersion metadata property on // Windows, and CFBundleVersion on Mac. Note: Windows requires the build version to // start with a number. We're using the version of the underlying WebTorrent library. buildVersion: require('webtorrent/package.json').version, // The application source directory. dir: config.ROOT_PATH, // Pattern which specifies which files to ignore when copying files to create the // package(s). ignore: /^\/src|^\/dist|\/(appveyor.yml|\.appveyor.yml|\.github|appdmg|AUTHORS|CONTRIBUTORS|bench|benchmark|benchmark\.js|bin|bower\.json|component\.json|coverage|doc|docs|docs\.mli|dragdrop\.min\.js|example|examples|example\.html|example\.js|externs|ipaddr\.min\.js|Makefile|min|minimist|perf|rusha|simplepeer\.min\.js|simplewebsocket\.min\.js|static\/screenshot\.png|test|tests|test\.js|tests\.js|webtorrent\.min\.js|\.[^/]*|.*\.md|.*\.markdown)$/, // The application name. name: config.APP_NAME, // The base directory where the finished package(s) are created. out: DIST_PATH, // Replace an already existing output directory. overwrite: true, // Runs `npm prune --production` which remove the packages specified in // "devDependencies" before starting to package the app. prune: true, // The Electron version that the app is built with (without the leading 'v') electronVersion: require('electron/package.json').version } const darwin = { // Build for Mac platform: 'darwin', // Build x64 binary only. arch: 'x64', // The bundle identifier to use in the application's plist (Mac only). appBundleId: 'io.webtorrent.webtorrent', // The application category type, as shown in the Finder via "View" -> "Arrange by // Application Category" when viewing the Applications directory (Mac only). appCategoryType: 'public.app-category.utilities', // The bundle identifier to use in the application helper's plist (Mac only). helperBundleId: 'io.webtorrent.webtorrent-helper', // Application icon. icon: config.APP_ICON + '.icns' } const win32 = { // Build for Windows. platform: 'win32', // Build x64 binary only. arch: 'x64', // Object hash of application metadata to embed into the executable (Windows only) win32metadata: { // Company that produced the file. CompanyName: config.APP_NAME, // Name of the program, displayed to users FileDescription: config.APP_NAME, // Original name of the file, not including a path. This information enables an // application to determine whether a file has been renamed by a user. The format of // the name depends on the file system for which the file was created. OriginalFilename: config.APP_NAME + '.exe', // Name of the product with which the file is distributed. ProductName: config.APP_NAME, // Internal name of the file, if one exists, for example, a module name if the file // is a dynamic-link library. If the file has no internal name, this string should be // the original filename, without extension. This string is required. InternalName: config.APP_NAME }, // Application icon. icon: config.APP_ICON + '.ico' } const linux = { // Build for Linux. platform: 'linux', // Build x64, armv7l, and arm64 binaries. arch: ['x64', 'armv7l', 'arm64'] // Note: Application icon for Linux is specified via the BrowserWindow `icon` option. } build() function buildDarwin (cb) { const plist = require('plist') console.log('Mac: Packaging electron...') electronPackager(Object.assign({}, all, darwin)).then(function (buildPath) { console.log('Mac: Packaged electron. ' + buildPath) const appPath = path.join(buildPath[0], config.APP_NAME + '.app') const contentsPath = path.join(appPath, 'Contents') const resourcesPath = path.join(contentsPath, 'Resources') const infoPlistPath = path.join(contentsPath, 'Info.plist') const infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8')) infoPlist.CFBundleDocumentTypes = [ { CFBundleTypeExtensions: ['torrent'], CFBundleTypeIconFile: path.basename(config.APP_FILE_ICON) + '.icns', CFBundleTypeName: 'BitTorrent Document', CFBundleTypeRole: 'Editor', LSHandlerRank: 'Owner', LSItemContentTypes: ['org.bittorrent.torrent'] }, { CFBundleTypeName: 'Any', CFBundleTypeOSTypes: ['****'], CFBundleTypeRole: 'Editor', LSHandlerRank: 'Owner', LSTypeIsPackage: false } ] infoPlist.CFBundleURLTypes = [ { CFBundleTypeRole: 'Editor', CFBundleURLIconFile: path.basename(config.APP_FILE_ICON) + '.icns', CFBundleURLName: 'BitTorrent Magnet URL', CFBundleURLSchemes: ['magnet'] }, { CFBundleTypeRole: 'Editor', CFBundleURLIconFile: path.basename(config.APP_FILE_ICON) + '.icns', CFBundleURLName: 'BitTorrent Stream-Magnet URL', CFBundleURLSchemes: ['stream-magnet'] } ] infoPlist.UTExportedTypeDeclarations = [ { UTTypeConformsTo: [ 'public.data', 'public.item', 'com.bittorrent.torrent' ], UTTypeDescription: 'BitTorrent Document', UTTypeIconFile: path.basename(config.APP_FILE_ICON) + '.icns', UTTypeIdentifier: 'org.bittorrent.torrent', UTTypeReferenceURL: 'http://www.bittorrent.org/beps/bep_0000.html', UTTypeTagSpecification: { 'com.apple.ostype': 'TORR', 'public.filename-extension': ['torrent'], 'public.mime-type': 'application/x-bittorrent' } } ] fs.writeFileSync(infoPlistPath, plist.build(infoPlist)) // Copy torrent file icon into app bundle cp.execSync(`cp ${config.APP_FILE_ICON + '.icns'} ${resourcesPath}`) if (process.platform === 'darwin') { if (argv.sign) { signApp(function (err) { if (err) return cb(err) pack(cb) }) } else { printWarning() pack(cb) } } else { printWarning() } function signApp (cb) { const sign = require('electron-osx-sign') const { notarize } = require('electron-notarize') /* * Sign the app with Apple Developer ID certificates. We sign the app for 2 reasons: * - So the auto-updater (Squirrrel.Mac) can check that app updates are signed by * the same author as the current version. * - So users will not a see a warning about the app coming from an "Unidentified * Developer" when they open it for the first time (Mac Gatekeeper). * * To sign an Mac app for distribution outside the App Store, the following are * required: * - Xcode * - Xcode Command Line Tools (xcode-select --install) * - Membership in the Apple Developer Program */ const signOpts = { verbose: true, app: appPath, platform: 'darwin', identity: 'Developer ID Application: Leenkx Team (5MAMC8G3L8)', hardenedRuntime: true, entitlements: path.join(config.ROOT_PATH, 'bin', 'darwin-entitlements.plist'), 'entitlements-inherit': path.join(config.ROOT_PATH, 'bin', 'darwin-entitlements.plist'), 'signature-flags': 'library' } const notarizeOpts = { appBundleId: darwin.appBundleId, appPath, appleId: 'feross@feross.org', appleIdPassword: '@keychain:AC_PASSWORD' } console.log('Mac: Signing app...') sign(signOpts, function (err) { if (err) return cb(err) console.log('Mac: Signed app.') console.log('Mac: Notarizing app...') notarize(notarizeOpts).then( function () { console.log('Mac: Notarized app.') cb(null) }, function (err) { cb(err) }) }) } function pack (cb) { packageZip() // always produce .zip file, used for automatic updates if (argv.package === 'dmg' || argv.package === 'all') { packageDmg(cb) } } function packageZip () { // Create .zip file (used by the auto-updater) console.log('Mac: Creating zip...') const inPath = path.join(buildPath[0], config.APP_NAME + '.app') const outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip') zip.zipSync(inPath, outPath) console.log('Mac: Created zip.') } function packageDmg (cb) { console.log('Mac: Creating dmg...') const appDmg = require('appdmg') const targetPath = path.join(DIST_PATH, BUILD_NAME + '.dmg') rimraf.sync(targetPath) // Create a .dmg (Mac disk image) file, for easy user installation. const dmgOpts = { basepath: config.ROOT_PATH, target: targetPath, specification: { title: config.APP_NAME, icon: config.APP_ICON + '.icns', background: path.join(config.STATIC_PATH, 'appdmg.png'), 'icon-size': 128, contents: [ { x: 122, y: 240, type: 'file', path: appPath }, { x: 380, y: 240, type: 'link', path: '/Applications' }, // Hide hidden icons out of view, for users who have hidden files shown. // https://github.com/LinusU/node-appdmg/issues/45#issuecomment-153924954 { x: 50, y: 500, type: 'position', path: '.background' }, { x: 100, y: 500, type: 'position', path: '.DS_Store' }, { x: 150, y: 500, type: 'position', path: '.Trashes' }, { x: 200, y: 500, type: 'position', path: '.VolumeIcon.icns' } ] } } const dmg = appDmg(dmgOpts) dmg.once('error', cb) dmg.on('progress', function (info) { if (info.type === 'step-begin') console.log(info.title + '...') }) dmg.once('finish', function (info) { console.log('Mac: Created dmg.') cb(null) }) } }).catch(function (err) { cb(err) }) } function buildWin32 (cb) { const installer = require('electron-winstaller') console.log('Windows: Packaging electron...') /* * Path to folder with the following files: * - Windows Authenticode private key and cert (authenticode.p12) * - Windows Authenticode password file (authenticode.txt) */ let CERT_PATH try { fs.accessSync('D:') CERT_PATH = 'D:' } catch (err) { CERT_PATH = path.join(os.homedir(), 'Desktop') } electronPackager(Object.assign({}, all, win32)).then(function (buildPath) { console.log('Windows: Packaged electron. ' + buildPath) let signWithParams if (process.platform === 'win32') { if (argv.sign) { const certificateFile = path.join(CERT_PATH, 'authenticode.p12') const certificatePassword = fs.readFileSync(path.join(CERT_PATH, 'authenticode.txt'), 'utf8') const timestampServer = 'http://timestamp.comodoca.com' signWithParams = `/a /f "${certificateFile}" /p "${certificatePassword}" /tr "${timestampServer}" /td sha256` } else { printWarning() } } else { printWarning() } const tasks = [] buildPath.forEach(function (filesPath) { if (argv.package === 'exe' || argv.package === 'all') { tasks.push((cb) => packageInstaller(filesPath, cb)) } if (argv.package === 'portable' || argv.package === 'all') { tasks.push((cb) => packagePortable(filesPath, cb)) } }) series(tasks, cb) function packageInstaller (filesPath, cb) { console.log('Windows: Creating installer...') installer.createWindowsInstaller({ appDirectory: filesPath, authors: config.APP_TEAM, description: config.APP_NAME, exe: config.APP_NAME + '.exe', iconUrl: config.GITHUB_URL_RAW + '/static/' + config.APP_NAME + '.ico', loadingGif: path.join(config.STATIC_PATH, 'loading.gif'), name: config.APP_NAME, noMsi: true, outputDirectory: DIST_PATH, productName: config.APP_NAME, // TODO: Re-enable Windows 64-bit delta updates when we confirm that they // work correctly in the presence of the "ia32" .nupkg files. I // (feross) noticed them listed in the 64-bit RELEASES file and // manually edited them out for the v0.17 release. Shipping only // full updates for now will work fine, with no ill-effects. // remoteReleases: config.GITHUB_URL, /** * If you hit a "GitHub API rate limit exceeded" error, set this token! */ // remoteToken: process.env.WEBTORRENT_GITHUB_API_TOKEN, setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe', setupIcon: config.APP_ICON + '.ico', signWithParams, title: config.APP_NAME, usePackageJson: false, version: pkg.version }) .then(function () { console.log('Windows: Created installer.') /** * Delete extraneous Squirrel files (i.e. *.nupkg delta files for older * versions of the app) */ fs.readdirSync(DIST_PATH) .filter((name) => name.endsWith('.nupkg') && !name.includes(pkg.version)) .forEach((filename) => { fs.unlinkSync(path.join(DIST_PATH, filename)) }) cb(null) }) .catch(cb) } function packagePortable (filesPath, cb) { console.log('Windows: Creating portable app...') const portablePath = path.join(filesPath, 'Portable Settings') fs.mkdirSync(portablePath, { recursive: true }) const downloadsPath = path.join(portablePath, 'Downloads') fs.mkdirSync(downloadsPath, { recursive: true }) const tempPath = path.join(portablePath, 'Temp') fs.mkdirSync(tempPath, { recursive: true }) const inPath = path.join(DIST_PATH, path.basename(filesPath)) const outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip') zip.zipSync(inPath, outPath) console.log('Windows: Created portable app.') cb(null) } }).catch(function (err) { cb(err) }) } function buildLinux (cb) { console.log('Linux: Packaging electron...') electronPackager(Object.assign({}, all, linux)).then(function (buildPath) { console.log('Linux: Packaged electron. ' + buildPath) const tasks = [] buildPath.forEach(function (filesPath) { const destArch = filesPath.split('-').pop() if (argv.package === 'deb' || argv.package === 'all') { tasks.push((cb) => packageDeb(filesPath, destArch, cb)) } if (argv.package === 'rpm' || argv.package === 'all') { tasks.push((cb) => packageRpm(filesPath, destArch, cb)) } if (argv.package === 'zip' || argv.package === 'all') { tasks.push((cb) => packageZip(filesPath, destArch, cb)) } }) series(tasks, cb) }).catch(function (err) { cb(err) }) function packageDeb (filesPath, destArch, cb) { // Linux convention for Debian based 'x64' is 'amd64' if (destArch === 'x64') { destArch = 'amd64' } // Create .deb file for Debian-based platforms console.log(`Linux: Creating ${destArch} deb...`) const installer = require('electron-installer-debian') const options = { src: filesPath + '/', dest: DIST_PATH, arch: destArch, bin: 'WebTorrent', icon: { '48x48': path.join(config.STATIC_PATH, 'linux/share/icons/hicolor/48x48/apps/webtorrent-desktop.png'), '256x256': path.join(config.STATIC_PATH, 'linux/share/icons/hicolor/256x256/apps/webtorrent-desktop.png') }, categories: ['Network', 'FileTransfer', 'P2P'], mimeType: ['application/x-bittorrent', 'x-scheme-handler/magnet', 'x-scheme-handler/stream-magnet'], desktopTemplate: path.join(config.STATIC_PATH, 'linux/webtorrent-desktop.ejs'), lintianOverrides: [ 'unstripped-binary-or-object', 'embedded-library', 'missing-dependency-on-libc', 'changelog-file-missing-in-native-package', 'description-synopsis-is-duplicated', 'setuid-binary', 'binary-without-manpage', 'shlib-with-executable-bit' ] } installer(options).then( () => { console.log(`Linux: Created ${destArch} deb.`) cb(null) }, (err) => cb(err) ) } function packageRpm (filesPath, destArch, cb) { // Linux convention for RedHat based 'x64' is 'x86_64' if (destArch === 'x64') { destArch = 'x86_64' } // Create .rpm file for RedHat-based platforms console.log(`Linux: Creating ${destArch} rpm...`) const installer = require('electron-installer-redhat') const options = { src: filesPath + '/', dest: DIST_PATH, arch: destArch, bin: 'WebTorrent', icon: { '48x48': path.join(config.STATIC_PATH, 'linux/share/icons/hicolor/48x48/apps/webtorrent-desktop.png'), '256x256': path.join(config.STATIC_PATH, 'linux/share/icons/hicolor/256x256/apps/webtorrent-desktop.png') }, categories: ['Network', 'FileTransfer', 'P2P'], mimeType: ['application/x-bittorrent', 'x-scheme-handler/magnet', 'x-scheme-handler/stream-magnet'], desktopTemplate: path.join(config.STATIC_PATH, 'linux/webtorrent-desktop.ejs') } installer(options).then( () => { console.log(`Linux: Created ${destArch} rpm.`) cb(null) }, (err) => cb(err) ) } function packageZip (filesPath, destArch, cb) { // Create .zip file for Linux console.log(`Linux: Creating ${destArch} zip...`) const inPath = path.join(DIST_PATH, path.basename(filesPath)) const outPath = path.join(DIST_PATH, `${BUILD_NAME}-linux-${destArch}.zip`) zip.zipSync(inPath, outPath) console.log(`Linux: Created ${destArch} zip.`) cb(null) } } function printDone (err) { if (err) console.error(err.message || err) } /* * Print a large warning when signing is disabled so we are less likely to accidentally * ship unsigned binaries to users. */ function printWarning () { console.log(fs.readFileSync(path.join(__dirname, 'warning.txt'), 'utf8')) }