diff --git a/README.md b/README.md index 1ea5023f..0b116411 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,16 @@ Where `[platform]` is `darwin`, `linux`, `win32`, or `all` (default). The following optional arguments are available: -- `--package=[type]` - Package only one output file. `type` is `deb`, `dmg`, `zip`, or `all` (default) - `--sign` - Sign the application (OS X, Windows) +- `--package=[type]` - Package single output type. + - `deb` - Debian package + - `zip` - Linux zip file + - `dmg` - OS X disk image + - `exe` - Windows installer + - `portable` - Windows portable app + - `all` - All platforms (default) + +Note: Even with the `--package` option, the auto-update files (.nupkg for Windows, *-darwin.zip for OS X) will always be produced. #### Windows build notes diff --git a/bin/package.js b/bin/package.js index d45cd957..03578f90 100755 --- a/bin/package.js +++ b/bin/package.js @@ -8,8 +8,11 @@ var cp = require('child_process') var electronPackager = require('electron-packager') var fs = require('fs') var minimist = require('minimist') +var mkdirp = require('mkdirp') var path = require('path') var rimraf = require('rimraf') +var series = require('run-series') +var zip = require('cross-zip') var config = require('../config') var pkg = require('../package.json') @@ -25,6 +28,8 @@ var CERT_PATH = process.platform === 'win32' ? 'D:' : '/Volumes/Certs' +var DIST_PATH = path.join(config.ROOT_PATH, 'dist') + var argv = minimist(process.argv.slice(2), { boolean: [ 'sign' @@ -39,7 +44,7 @@ var argv = minimist(process.argv.slice(2), { }) function build () { - rimraf.sync(path.join(config.ROOT_PATH, 'dist')) + rimraf.sync(DIST_PATH) var platform = argv._[0] if (platform === 'darwin') { buildDarwin(printDone) @@ -48,10 +53,10 @@ function build () { } else if (platform === 'linux') { buildLinux(printDone) } else { - buildDarwin(function (err, buildPath) { - printDone(err, buildPath) - buildWin32(function (err, buildPath) { - printDone(err, buildPath) + buildDarwin(function (err) { + printDone(err) + buildWin32(function (err) { + printDone(err) buildLinux(printDone) }) }) @@ -94,7 +99,7 @@ var all = { name: config.APP_NAME, // The base directory where the finished package(s) are created. - out: path.join(config.ROOT_PATH, 'dist'), + out: DIST_PATH, // Replace an already existing output directory. overwrite: true, @@ -163,16 +168,6 @@ var linux = { // Note: Application icon for Linux is specified via the BrowserWindow `icon` option. } -/* - * Print a large warning when signing is disabled so we are less likely to accidentally - * ship unsigned binaries to users. - */ -process.on('exit', function () { - if (!argv.sign) { - printWarning() - } -}) - build() function buildDarwin (cb) { @@ -181,7 +176,7 @@ function buildDarwin (cb) { console.log('OS X: Packaging electron...') electronPackager(Object.assign({}, all, darwin), function (err, buildPath) { if (err) return cb(err) - console.log('OS X: Packaged electron.') + console.log('OS X: Packaged electron. ' + buildPath[0]) var appPath = path.join(buildPath[0], config.APP_NAME + '.app') var contentsPath = path.join(appPath, 'Contents') @@ -227,11 +222,11 @@ function buildDarwin (cb) { if (argv.sign) { signApp(function (err) { if (err) return cb(err) - pack() + pack(cb) }) } else { printWarning() - pack() + pack(cb) } } else { printWarning() @@ -267,27 +262,31 @@ function buildDarwin (cb) { }) } - function pack () { - if (argv.package === 'zip' || argv.package === 'all') { - packageZip() - } + function pack (cb) { + packageZip() // always produce .zip file, used for automatic updates + if (argv.package === 'dmg' || argv.package === 'all') { - packageDmg() + packageDmg(cb) } } function packageZip () { // Create .zip file (used by the auto-updater) - var zipPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '-darwin.zip') console.log('OS X: Creating zip...') - cp.execSync(`cd ${buildPath[0]} && zip -r -y ${zipPath} ${config.APP_NAME + '.app'}`) + + var inPath = path.join(buildPath[0], config.APP_NAME + '.app') + var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip') + zip(inPath, outPath) + console.log('OS X: Created zip.') } - function packageDmg () { + function packageDmg (cb) { + console.log('OS X: Creating dmg...') + var appDmg = require('appdmg') - var targetPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '.dmg') + var targetPath = path.join(DIST_PATH, BUILD_NAME + '.dmg') rimraf.sync(targetPath) // Create a .dmg (OS X disk image) file, for easy user installation. @@ -312,7 +311,6 @@ function buildDarwin (cb) { } } - console.log('OS X: Creating dmg...') var dmg = appDmg(dmgOpts) dmg.on('error', cb) dmg.on('progress', function (info) { @@ -320,7 +318,7 @@ function buildDarwin (cb) { }) dmg.on('finish', function (info) { console.log('OS X: Created dmg.') - cb(null, buildPath) + cb(null) }) } }) @@ -332,7 +330,7 @@ function buildWin32 (cb) { console.log('Windows: Packaging electron...') electronPackager(Object.assign({}, all, win32), function (err, buildPath) { if (err) return cb(err) - console.log('Windows: Packaged electron.') + console.log('Windows: Packaged electron. ' + buildPath[0]) var signWithParams if (process.platform === 'win32') { @@ -348,65 +346,90 @@ function buildWin32 (cb) { printWarning() } - console.log('Windows: Creating installer...') - installer.createWindowsInstaller({ - appDirectory: buildPath[0], - 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: path.join(config.ROOT_PATH, 'dist'), - productName: config.APP_NAME, - remoteReleases: config.GITHUB_URL, - setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe', - setupIcon: config.APP_ICON + '.ico', - signWithParams: signWithParams, - title: config.APP_NAME, - usePackageJson: false, - version: pkg.version - }).then(function () { - console.log('Windows: Created installer.') - cb(null, buildPath) - }).catch(cb) + var tasks = [] + if (argv.package === 'exe' || argv.package === 'all') { + tasks.push((cb) => packageInstaller(cb)) + } + if (argv.package === 'portable' || argv.package === 'all') { + tasks.push((cb) => packagePortable(cb)) + } + series(tasks, cb) + + function packageInstaller (cb) { + console.log('Windows: Creating installer...') + installer.createWindowsInstaller({ + appDirectory: buildPath[0], + 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, + remoteReleases: config.GITHUB_URL, + setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe', + setupIcon: config.APP_ICON + '.ico', + signWithParams: signWithParams, + title: config.APP_NAME, + usePackageJson: false, + version: pkg.version + }).then(function () { + console.log('Windows: Created installer.') + cb(null) + }).catch(cb) + } + + function packagePortable (cb) { + // Create Windows portable app + console.log('Windows: Creating portable app...') + + var portablePath = path.join(buildPath[0], 'Portable Settings') + mkdirp.sync(portablePath) + + var inPath = path.join(DIST_PATH, path.basename(buildPath[0])) + var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip') + zip(inPath, outPath) + + console.log('Windows: Created portable app.') + cb(null) + } }) } function buildLinux (cb) { - var distPath = path.join(config.ROOT_PATH, 'dist') - console.log('Linux: Packaging electron...') electronPackager(Object.assign({}, all, linux), function (err, buildPath) { if (err) return cb(err) - console.log('Linux: Packaged electron.') + console.log('Linux: Packaged electron. ' + buildPath[0]) - for (var i = 0; i < buildPath.length; i++) { - var filesPath = buildPath[i] + var tasks = [] + buildPath.forEach(function (filesPath) { var destArch = filesPath.split('-').pop() if (argv.package === 'deb' || argv.package === 'all') { - packageDeb(filesPath, destArch) + tasks.push((cb) => packageDeb(filesPath, destArch, cb)) } - if (argv.package === 'zip' || argv.package === 'all') { - packageZip(filesPath, destArch) + tasks.push((cb) => packageZip(filesPath, destArch, cb)) } - } + }) + series(tasks, cb) }) - function packageDeb (filesPath, destArch) { + function packageDeb (filesPath, destArch, cb) { // Create .deb file for Debian-based platforms + console.log(`Linux: Creating ${destArch} deb...`) + var deb = require('nobin-debian-installer')() var destPath = path.join('/opt', pkg.name) - console.log(`Linux: Creating ${destArch} deb...`) deb.pack({ package: pkg, info: { arch: destArch === 'x64' ? 'amd64' : 'i386', - targetDir: distPath, + targetDir: DIST_PATH, depends: 'libc6 (>= 2.4)', scripts: { postinst: path.join(config.STATIC_PATH, 'linux', 'postinst'), @@ -419,26 +442,33 @@ function buildLinux (cb) { expand: true, cwd: filesPath }], function (err) { - if (err) return console.error(err.message || err) + if (err) return cb(err) console.log(`Linux: Created ${destArch} deb.`) + cb(null) }) } - function packageZip (filesPath, destArch) { + function packageZip (filesPath, destArch, cb) { // Create .zip file for Linux - var zipPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '-linux-' + destArch + '.zip') - var appFolderName = path.basename(filesPath) console.log(`Linux: Creating ${destArch} zip...`) - cp.execSync(`cd ${distPath} && zip -r -y ${zipPath} ${appFolderName}`) + + var inPath = path.join(DIST_PATH, path.basename(filesPath)) + var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux-' + destArch + '.zip') + zip(inPath, outPath) + console.log(`Linux: Created ${destArch} zip.`) + cb(null) } } -function printDone (err, buildPath) { +function printDone (err) { if (err) console.error(err.message || err) - else console.log('Built ' + buildPath[0]) } +/* + * 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')) } diff --git a/bin/release-_pre.sh b/bin/release-_pre.sh index 1eb3981b..78e88c71 100755 --- a/bin/release-_pre.sh +++ b/bin/release-_pre.sh @@ -6,4 +6,6 @@ npm run update-authors git diff --exit-code rm -rf node_modules/ npm install +npm prune +npm dedupe npm test diff --git a/config.js b/config.js index 7cecb1fe..8ec82a1d 100644 --- a/config.js +++ b/config.js @@ -1,10 +1,13 @@ -var applicationConfigPath = require('application-config-path') +var appConfig = require('application-config')('WebTorrent') var path = require('path') +var pathExists = require('path-exists') var APP_NAME = 'WebTorrent' var APP_TEAM = 'The WebTorrent Project' var APP_VERSION = require('./package.json').version +var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings') + module.exports = { APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM, APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'), @@ -14,19 +17,20 @@ module.exports = { APP_VERSION: APP_VERSION, APP_WINDOW_TITLE: APP_NAME + ' (BETA)', + AUTO_UPDATE_CHECK_STARTUP_DELAY: 5 * 1000 /* 5 seconds */, AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update' + '?version=' + APP_VERSION + '&platform=' + process.platform, - AUTO_UPDATE_CHECK_STARTUP_DELAY: 5 * 1000 /* 5 seconds */, CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report', - CONFIG_PATH: applicationConfigPath(APP_NAME), - CONFIG_POSTER_PATH: path.join(applicationConfigPath(APP_NAME), 'Posters'), - CONFIG_TORRENT_PATH: path.join(applicationConfigPath(APP_NAME), 'Torrents'), + CONFIG_PATH: getConfigPath(), + CONFIG_POSTER_PATH: path.join(getConfigPath(), 'Posters'), + CONFIG_TORRENT_PATH: path.join(getConfigPath(), 'Torrents'), GITHUB_URL: 'https://github.com/feross/webtorrent-desktop', GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master', + IS_PORTABLE: isPortable(), IS_PRODUCTION: isProduction(), ROOT_PATH: __dirname, @@ -40,6 +44,18 @@ module.exports = { WINDOW_MIN_WIDTH: 425 } +function getConfigPath () { + if (isPortable()) { + return PORTABLE_PATH + } else { + return path.dirname(appConfig.filePath) + } +} + +function isPortable () { + return process.platform === 'win32' && isProduction() && pathExists(PORTABLE_PATH) +} + function isProduction () { if (!process.versions.electron) { return false diff --git a/main/index.js b/main/index.js index 2ad0c939..3d36c66f 100644 --- a/main/index.js +++ b/main/index.js @@ -37,6 +37,10 @@ if (!shouldQuit) { } function init () { + if (config.IS_PORTABLE) { + app.setPath('userData', config.CONFIG_PATH) + } + app.ipcReady = false // main window has finished loading and IPC is ready app.isQuitting = false diff --git a/package.json b/package.json index da1e9c64..295175a1 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,7 @@ }, "dependencies": { "airplay-js": "guerrerocarlos/node-airplay-js", - "application-config": "^0.2.0", - "application-config-path": "^0.1.0", + "application-config": "feross/node-application-config", "bitfield": "^1.0.2", "chromecasts": "^1.8.0", "concat-stream": "^1.5.1", @@ -43,6 +42,7 @@ "winreg": "^1.1.1" }, "devDependencies": { + "cross-zip": "^1.0.0", "electron-osx-sign": "^0.3.0", "electron-packager": "^6.0.2", "electron-winstaller": "feross/windows-installer#build", @@ -50,6 +50,7 @@ "minimist": "^1.2.0", "nobin-debian-installer": "^0.0.9", "plist": "^1.2.0", + "run-series": "^1.1.4", "standard": "^6.0.5" }, "homepage": "https://webtorrent.io", @@ -71,7 +72,7 @@ }, "scripts": { "clean": "node ./bin/clean.js", - "package": "npm install && npm prune && npm dedupe && node ./bin/package.js", + "package": "node ./bin/package.js", "start": "electron .", "test": "standard", "update-authors": "./bin/update-authors.sh" diff --git a/renderer/index.js b/renderer/index.js index a64d2e18..b607fbb6 100644 --- a/renderer/index.js +++ b/renderer/index.js @@ -1,6 +1,6 @@ console.time('init') -var cfg = require('application-config')('WebTorrent') +var appConfig = require('application-config')('WebTorrent') var concat = require('concat-stream') var dragDrop = require('drag-drop') var electron = require('electron') @@ -26,6 +26,8 @@ var util = require('./util') var {setDispatch} = require('./lib/dispatcher') setDispatch(dispatch) +appConfig.filePath = config.CONFIG_PATH + path.sep + 'config.json' + // Electron apps have two processes: a main process (node) runs first and starts // a renderer process (essentially a Chrome window). We're in the renderer process, // and this IPC channel receives from and sends messages to the main process @@ -425,9 +427,9 @@ function setupIpc () { // Load state.saved from the JSON state file function loadState (cb) { - cfg.read(function (err, data) { + appConfig.read(function (err, data) { if (err) console.error(err) - console.log('loaded state from ' + cfg.filePath) + console.log('loaded state from ' + appConfig.filePath) // populate defaults if they're not there state.saved = Object.assign({}, State.getDefaultSavedState(), data) @@ -457,7 +459,7 @@ function saveStateThrottled () { // Write state.saved to the JSON state file function saveState () { - console.log('saving state to ' + cfg.filePath) + console.log('saving state to ' + appConfig.filePath) // Clean up, so that we're not saving any pending state var copy = Object.assign({}, state.saved) @@ -479,7 +481,7 @@ function saveState () { return torrent }) - cfg.write(copy, function (err) { + appConfig.write(copy, function (err) { if (err) console.error(err) ipcRenderer.send('savedState') }) diff --git a/renderer/state.js b/renderer/state.js index f57c4910..010d091c 100644 --- a/renderer/state.js +++ b/renderer/state.js @@ -1,4 +1,5 @@ var electron = require('electron') +var path = require('path') var remote = electron.remote @@ -255,6 +256,8 @@ function getDefaultSavedState () { ] } ], - downloadPath: remote.app.getPath('downloads') + downloadPath: config.IS_PORTABLE + ? path.join(config.CONFIG_PATH, 'Downloads') + : remote.app.getPath('downloads') } }