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:
- `--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

View File

@@ -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'))
}

View File

@@ -6,4 +6,6 @@ npm run update-authors
git diff --exit-code
rm -rf node_modules/
npm install
npm prune
npm dedupe
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 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

View File

@@ -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

View File

@@ -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"

View File

@@ -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')
})

View File

@@ -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')
}
}