Windows Portable App (#417)

* packager: call callbacks consistently

Before this, the callbacks would not being called, which would lead to
an incomplete build on non-OS X platforms when trying to build all for
all platforms.

* packager: Always produce OS X update file regardless of --package option

This makes it consistent with how the windows build always produces the
.nupkg autoupdate files

* packager: fix duplicate npm install

Move "npm prune && npm dedupe" into the release script. Remove an extra
"npm install"

* Make Windows portable app

When a folder named "Portable Settings" exists in same folder as
WebTorrent.exe, then use it instead of the default application config
path.

Closes #358

* packager: remove redundant signing warning

* cross platform zip function

* Set config file path to match config.CONFIG_PATH

* portable app: make electron settings portable

* portable: fix poster/torrent paths

* use cross-zip

* portable app: default download folder inside 'Portable Settings'
This commit is contained in:
Feross Aboukhadijeh
2016-04-16 04:18:21 -07:00
committed by DC
parent 85e49dea6d
commit 969c784df4
8 changed files with 154 additions and 88 deletions

View File

@@ -57,8 +57,16 @@ Where `[platform]` is `darwin`, `linux`, `win32`, or `all` (default).
The following optional arguments are available: 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) - `--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 #### Windows build notes

View File

@@ -8,8 +8,11 @@ var cp = require('child_process')
var electronPackager = require('electron-packager') var electronPackager = require('electron-packager')
var fs = require('fs') var fs = require('fs')
var minimist = require('minimist') var minimist = require('minimist')
var mkdirp = require('mkdirp')
var path = require('path') var path = require('path')
var rimraf = require('rimraf') var rimraf = require('rimraf')
var series = require('run-series')
var zip = require('cross-zip')
var config = require('../config') var config = require('../config')
var pkg = require('../package.json') var pkg = require('../package.json')
@@ -25,6 +28,8 @@ var CERT_PATH = process.platform === 'win32'
? 'D:' ? 'D:'
: '/Volumes/Certs' : '/Volumes/Certs'
var DIST_PATH = path.join(config.ROOT_PATH, 'dist')
var argv = minimist(process.argv.slice(2), { var argv = minimist(process.argv.slice(2), {
boolean: [ boolean: [
'sign' 'sign'
@@ -39,7 +44,7 @@ var argv = minimist(process.argv.slice(2), {
}) })
function build () { function build () {
rimraf.sync(path.join(config.ROOT_PATH, 'dist')) rimraf.sync(DIST_PATH)
var platform = argv._[0] var platform = argv._[0]
if (platform === 'darwin') { if (platform === 'darwin') {
buildDarwin(printDone) buildDarwin(printDone)
@@ -48,10 +53,10 @@ function build () {
} else if (platform === 'linux') { } else if (platform === 'linux') {
buildLinux(printDone) buildLinux(printDone)
} else { } else {
buildDarwin(function (err, buildPath) { buildDarwin(function (err) {
printDone(err, buildPath) printDone(err)
buildWin32(function (err, buildPath) { buildWin32(function (err) {
printDone(err, buildPath) printDone(err)
buildLinux(printDone) buildLinux(printDone)
}) })
}) })
@@ -94,7 +99,7 @@ var all = {
name: config.APP_NAME, name: config.APP_NAME,
// The base directory where the finished package(s) are created. // 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. // Replace an already existing output directory.
overwrite: true, overwrite: true,
@@ -163,16 +168,6 @@ var linux = {
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option. // 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() build()
function buildDarwin (cb) { function buildDarwin (cb) {
@@ -181,7 +176,7 @@ function buildDarwin (cb) {
console.log('OS X: Packaging electron...') console.log('OS X: Packaging electron...')
electronPackager(Object.assign({}, all, darwin), function (err, buildPath) { electronPackager(Object.assign({}, all, darwin), function (err, buildPath) {
if (err) return cb(err) 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 appPath = path.join(buildPath[0], config.APP_NAME + '.app')
var contentsPath = path.join(appPath, 'Contents') var contentsPath = path.join(appPath, 'Contents')
@@ -227,11 +222,11 @@ function buildDarwin (cb) {
if (argv.sign) { if (argv.sign) {
signApp(function (err) { signApp(function (err) {
if (err) return cb(err) if (err) return cb(err)
pack() pack(cb)
}) })
} else { } else {
printWarning() printWarning()
pack() pack(cb)
} }
} else { } else {
printWarning() printWarning()
@@ -267,27 +262,31 @@ function buildDarwin (cb) {
}) })
} }
function pack () { function pack (cb) {
if (argv.package === 'zip' || argv.package === 'all') { packageZip() // always produce .zip file, used for automatic updates
packageZip()
}
if (argv.package === 'dmg' || argv.package === 'all') { if (argv.package === 'dmg' || argv.package === 'all') {
packageDmg() packageDmg(cb)
} }
} }
function packageZip () { function packageZip () {
// Create .zip file (used by the auto-updater) // 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...') 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.') console.log('OS X: Created zip.')
} }
function packageDmg () { function packageDmg (cb) {
console.log('OS X: Creating dmg...')
var appDmg = require('appdmg') 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) rimraf.sync(targetPath)
// Create a .dmg (OS X disk image) file, for easy user installation. // 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) var dmg = appDmg(dmgOpts)
dmg.on('error', cb) dmg.on('error', cb)
dmg.on('progress', function (info) { dmg.on('progress', function (info) {
@@ -320,7 +318,7 @@ function buildDarwin (cb) {
}) })
dmg.on('finish', function (info) { dmg.on('finish', function (info) {
console.log('OS X: Created dmg.') console.log('OS X: Created dmg.')
cb(null, buildPath) cb(null)
}) })
} }
}) })
@@ -332,7 +330,7 @@ function buildWin32 (cb) {
console.log('Windows: Packaging electron...') console.log('Windows: Packaging electron...')
electronPackager(Object.assign({}, all, win32), function (err, buildPath) { electronPackager(Object.assign({}, all, win32), function (err, buildPath) {
if (err) return cb(err) if (err) return cb(err)
console.log('Windows: Packaged electron.') console.log('Windows: Packaged electron. ' + buildPath[0])
var signWithParams var signWithParams
if (process.platform === 'win32') { if (process.platform === 'win32') {
@@ -348,65 +346,90 @@ function buildWin32 (cb) {
printWarning() printWarning()
} }
console.log('Windows: Creating installer...') var tasks = []
installer.createWindowsInstaller({ if (argv.package === 'exe' || argv.package === 'all') {
appDirectory: buildPath[0], tasks.push((cb) => packageInstaller(cb))
authors: config.APP_TEAM, }
description: config.APP_NAME, if (argv.package === 'portable' || argv.package === 'all') {
exe: config.APP_NAME + '.exe', tasks.push((cb) => packagePortable(cb))
iconUrl: config.GITHUB_URL_RAW + '/static/' + config.APP_NAME + '.ico', }
loadingGif: path.join(config.STATIC_PATH, 'loading.gif'), series(tasks, cb)
name: config.APP_NAME,
noMsi: true, function packageInstaller (cb) {
outputDirectory: path.join(config.ROOT_PATH, 'dist'), console.log('Windows: Creating installer...')
productName: config.APP_NAME, installer.createWindowsInstaller({
remoteReleases: config.GITHUB_URL, appDirectory: buildPath[0],
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe', authors: config.APP_TEAM,
setupIcon: config.APP_ICON + '.ico', description: config.APP_NAME,
signWithParams: signWithParams, exe: config.APP_NAME + '.exe',
title: config.APP_NAME, iconUrl: config.GITHUB_URL_RAW + '/static/' + config.APP_NAME + '.ico',
usePackageJson: false, loadingGif: path.join(config.STATIC_PATH, 'loading.gif'),
version: pkg.version name: config.APP_NAME,
}).then(function () { noMsi: true,
console.log('Windows: Created installer.') outputDirectory: DIST_PATH,
cb(null, buildPath) productName: config.APP_NAME,
}).catch(cb) 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) { function buildLinux (cb) {
var distPath = path.join(config.ROOT_PATH, 'dist')
console.log('Linux: Packaging electron...') console.log('Linux: Packaging electron...')
electronPackager(Object.assign({}, all, linux), function (err, buildPath) { electronPackager(Object.assign({}, all, linux), function (err, buildPath) {
if (err) return cb(err) 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 tasks = []
var filesPath = buildPath[i] buildPath.forEach(function (filesPath) {
var destArch = filesPath.split('-').pop() var destArch = filesPath.split('-').pop()
if (argv.package === 'deb' || argv.package === 'all') { if (argv.package === 'deb' || argv.package === 'all') {
packageDeb(filesPath, destArch) tasks.push((cb) => packageDeb(filesPath, destArch, cb))
} }
if (argv.package === 'zip' || argv.package === 'all') { 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 // Create .deb file for Debian-based platforms
console.log(`Linux: Creating ${destArch} deb...`)
var deb = require('nobin-debian-installer')() var deb = require('nobin-debian-installer')()
var destPath = path.join('/opt', pkg.name) var destPath = path.join('/opt', pkg.name)
console.log(`Linux: Creating ${destArch} deb...`)
deb.pack({ deb.pack({
package: pkg, package: pkg,
info: { info: {
arch: destArch === 'x64' ? 'amd64' : 'i386', arch: destArch === 'x64' ? 'amd64' : 'i386',
targetDir: distPath, targetDir: DIST_PATH,
depends: 'libc6 (>= 2.4)', depends: 'libc6 (>= 2.4)',
scripts: { scripts: {
postinst: path.join(config.STATIC_PATH, 'linux', 'postinst'), postinst: path.join(config.STATIC_PATH, 'linux', 'postinst'),
@@ -419,26 +442,33 @@ function buildLinux (cb) {
expand: true, expand: true,
cwd: filesPath cwd: filesPath
}], function (err) { }], function (err) {
if (err) return console.error(err.message || err) if (err) return cb(err)
console.log(`Linux: Created ${destArch} deb.`) console.log(`Linux: Created ${destArch} deb.`)
cb(null)
}) })
} }
function packageZip (filesPath, destArch) { function packageZip (filesPath, destArch, cb) {
// Create .zip file for Linux // 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...`) 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.`) console.log(`Linux: Created ${destArch} zip.`)
cb(null)
} }
} }
function printDone (err, buildPath) { function printDone (err) {
if (err) console.error(err.message || 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 () { function printWarning () {
console.log(fs.readFileSync(path.join(__dirname, 'warning.txt'), 'utf8')) console.log(fs.readFileSync(path.join(__dirname, 'warning.txt'), 'utf8'))
} }

View File

@@ -6,4 +6,6 @@ npm run update-authors
git diff --exit-code git diff --exit-code
rm -rf node_modules/ rm -rf node_modules/
npm install npm install
npm prune
npm dedupe
npm test npm test

View File

@@ -1,10 +1,13 @@
var applicationConfigPath = require('application-config-path') var appConfig = require('application-config')('WebTorrent')
var path = require('path') var path = require('path')
var pathExists = require('path-exists')
var APP_NAME = 'WebTorrent' var APP_NAME = 'WebTorrent'
var APP_TEAM = 'The WebTorrent Project' var APP_TEAM = 'The WebTorrent Project'
var APP_VERSION = require('./package.json').version var APP_VERSION = require('./package.json').version
var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
module.exports = { module.exports = {
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM, APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'), APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
@@ -14,19 +17,20 @@ module.exports = {
APP_VERSION: APP_VERSION, APP_VERSION: APP_VERSION,
APP_WINDOW_TITLE: APP_NAME + ' (BETA)', APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
AUTO_UPDATE_CHECK_STARTUP_DELAY: 5 * 1000 /* 5 seconds */,
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update' + AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update' +
'?version=' + APP_VERSION + '&platform=' + process.platform, '?version=' + APP_VERSION + '&platform=' + process.platform,
AUTO_UPDATE_CHECK_STARTUP_DELAY: 5 * 1000 /* 5 seconds */,
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report', CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
CONFIG_PATH: applicationConfigPath(APP_NAME), CONFIG_PATH: getConfigPath(),
CONFIG_POSTER_PATH: path.join(applicationConfigPath(APP_NAME), 'Posters'), CONFIG_POSTER_PATH: path.join(getConfigPath(), 'Posters'),
CONFIG_TORRENT_PATH: path.join(applicationConfigPath(APP_NAME), 'Torrents'), CONFIG_TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop', GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master', GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
IS_PORTABLE: isPortable(),
IS_PRODUCTION: isProduction(), IS_PRODUCTION: isProduction(),
ROOT_PATH: __dirname, ROOT_PATH: __dirname,
@@ -40,6 +44,18 @@ module.exports = {
WINDOW_MIN_WIDTH: 425 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 () { function isProduction () {
if (!process.versions.electron) { if (!process.versions.electron) {
return false return false

View File

@@ -37,6 +37,10 @@ if (!shouldQuit) {
} }
function init () { 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.ipcReady = false // main window has finished loading and IPC is ready
app.isQuitting = false app.isQuitting = false

View File

@@ -15,8 +15,7 @@
}, },
"dependencies": { "dependencies": {
"airplay-js": "guerrerocarlos/node-airplay-js", "airplay-js": "guerrerocarlos/node-airplay-js",
"application-config": "^0.2.0", "application-config": "feross/node-application-config",
"application-config-path": "^0.1.0",
"bitfield": "^1.0.2", "bitfield": "^1.0.2",
"chromecasts": "^1.8.0", "chromecasts": "^1.8.0",
"concat-stream": "^1.5.1", "concat-stream": "^1.5.1",
@@ -43,6 +42,7 @@
"winreg": "^1.1.1" "winreg": "^1.1.1"
}, },
"devDependencies": { "devDependencies": {
"cross-zip": "^1.0.0",
"electron-osx-sign": "^0.3.0", "electron-osx-sign": "^0.3.0",
"electron-packager": "^6.0.2", "electron-packager": "^6.0.2",
"electron-winstaller": "feross/windows-installer#build", "electron-winstaller": "feross/windows-installer#build",
@@ -50,6 +50,7 @@
"minimist": "^1.2.0", "minimist": "^1.2.0",
"nobin-debian-installer": "^0.0.9", "nobin-debian-installer": "^0.0.9",
"plist": "^1.2.0", "plist": "^1.2.0",
"run-series": "^1.1.4",
"standard": "^6.0.5" "standard": "^6.0.5"
}, },
"homepage": "https://webtorrent.io", "homepage": "https://webtorrent.io",
@@ -71,7 +72,7 @@
}, },
"scripts": { "scripts": {
"clean": "node ./bin/clean.js", "clean": "node ./bin/clean.js",
"package": "npm install && npm prune && npm dedupe && node ./bin/package.js", "package": "node ./bin/package.js",
"start": "electron .", "start": "electron .",
"test": "standard", "test": "standard",
"update-authors": "./bin/update-authors.sh" "update-authors": "./bin/update-authors.sh"

View File

@@ -1,6 +1,6 @@
console.time('init') console.time('init')
var cfg = require('application-config')('WebTorrent') var appConfig = require('application-config')('WebTorrent')
var concat = require('concat-stream') var concat = require('concat-stream')
var dragDrop = require('drag-drop') var dragDrop = require('drag-drop')
var electron = require('electron') var electron = require('electron')
@@ -26,6 +26,8 @@ var util = require('./util')
var {setDispatch} = require('./lib/dispatcher') var {setDispatch} = require('./lib/dispatcher')
setDispatch(dispatch) setDispatch(dispatch)
appConfig.filePath = config.CONFIG_PATH + path.sep + 'config.json'
// Electron apps have two processes: a main process (node) runs first and starts // 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, // 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 // 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 // Load state.saved from the JSON state file
function loadState (cb) { function loadState (cb) {
cfg.read(function (err, data) { appConfig.read(function (err, data) {
if (err) console.error(err) 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 // populate defaults if they're not there
state.saved = Object.assign({}, State.getDefaultSavedState(), data) state.saved = Object.assign({}, State.getDefaultSavedState(), data)
@@ -457,7 +459,7 @@ function saveStateThrottled () {
// Write state.saved to the JSON state file // Write state.saved to the JSON state file
function saveState () { 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 // Clean up, so that we're not saving any pending state
var copy = Object.assign({}, state.saved) var copy = Object.assign({}, state.saved)
@@ -479,7 +481,7 @@ function saveState () {
return torrent return torrent
}) })
cfg.write(copy, function (err) { appConfig.write(copy, function (err) {
if (err) console.error(err) if (err) console.error(err)
ipcRenderer.send('savedState') ipcRenderer.send('savedState')
}) })

View File

@@ -1,4 +1,5 @@
var electron = require('electron') var electron = require('electron')
var path = require('path')
var remote = electron.remote 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')
} }
} }