diff --git a/README.md b/README.md index 0da2ced8ca..96dddc0732 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ config object: |-------------|---------------------|-------------|----------------------|----------|--------------| | `zip` | All | Zips your packaged application | None | Yes | `zip` on Darwin/Linux | | `squirrel` | Windows | Generates an installer and `.nupkg` files for Squirrel.Windows | [`electronWinstallerConfig`](https://github.com/electron/windows-installer#usage) | Yes | | +| `appx` | Windows | Generates a Windows Store package | [`windowsStoreConfig`](https://github.com/felixrieseberg/electron-windows-store#programmatic-usage) | No | | | `dmg` | Darwin | Generates a DMG file | [`electronInstallerDMG`](https://github.com/mongodb-js/electron-installer-dmg#api) | No | | | `deb` | Linux | Generates a Debian package | [`electronInstallerDebian`](https://github.com/unindented/electron-installer-debian#options) | Yes | [`fakeroot` and `dpkg`](https://github.com/unindented/electron-installer-debian#requirements) | | `rpm` | Linux | Generates an RPM package | [`electronInstallerRedhat`](https://github.com/unindented/electron-installer-redhat#options) | Yes | [`rpm`](https://github.com/unindented/electron-installer-redhatn#requirements) | diff --git a/package.json b/package.json index a251b046c4..7ab1364e5c 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "electron-installer-dmg": "^0.1.2", "electron-packager": "^8.4.0", "electron-sudo": "malept/electron-sudo#fix-linux-sudo-detection", + "electron-windows-store": "^0.6.3", "electron-winstaller": "^2.5.0", "fs-promise": "^1.0.0", "github": "^7.2.0", @@ -83,6 +84,7 @@ "resolve-package": "^1.0.1", "semver": "^5.3.0", "sudo-prompt": "^6.2.1", + "spawn-rx": "^2.0.7", "username": "^2.2.2", "yarn-or-npm": "^2.0.2", "zip-folder": "^1.0.0" diff --git a/res/default.pvk b/res/default.pvk new file mode 100644 index 0000000000..8f77c3a82e Binary files /dev/null and b/res/default.pvk differ diff --git a/src/init/init-npm.js b/src/init/init-npm.js index f9b06db33f..219d85a2f3 100644 --- a/src/init/init-npm.js +++ b/src/init/init-npm.js @@ -21,6 +21,7 @@ export default async (dir, lintStyle) => { const packageJSON = await readPackageJSON(path.resolve(__dirname, '../../tmpl')); packageJSON.productName = packageJSON.name = path.basename(dir).toLowerCase(); packageJSON.config.forge.electronWinstallerConfig.name = packageJSON.name.replace(/-/g, '_'); + packageJSON.config.forge.windowsStoreConfig.name = packageJSON.productName.replace(/-/g, ''); packageJSON.author = await username(); switch (lintStyle) { diff --git a/src/makers/win32/appx.js b/src/makers/win32/appx.js new file mode 100644 index 0000000000..1816cff55d --- /dev/null +++ b/src/makers/win32/appx.js @@ -0,0 +1,88 @@ +import windowsStore from 'electron-windows-store'; +import fs from 'fs'; +import path from 'path'; +import { spawnPromise, findActualExecutable } from 'spawn-rx'; + +import { ensureDirectory } from '../../util/ensure-output'; + +// NB: This is not a typo, we require AppXs to be built on 64-bit +// but if we're running in a 32-bit node.js process, we're going to +// be Wow64 redirected +const windowsSdkPath = process.arch === 'x64' ? + 'C:\\Program Files (x86)\\Windows Kits\\10\\bin\\x64' : + 'C:\\Program Files\\Windows Kits\\10\\bin\\x64'; + +function findSdkTool(exe) { + let sdkTool = path.join(windowsSdkPath, exe); + if (!fs.existsSync(sdkTool)) { + sdkTool = findActualExecutable(exe, []).cmd; + } + + if (!fs.existsSync(sdkTool)) { + throw new Error(`Can't find ${exe} in PATH, you probably need to install the Windows SDK`); + } + + return sdkTool; +} + +function spawnSdkTool(exe, params) { + return spawnPromise(findSdkTool(exe), params); +} + +export async function createDefaultCertificate(publisherName, outPath) { + const defaultPvk = path.resolve(__dirname, '..', '..', '..', 'res', 'default.pvk'); + const targetCert = path.join(outPath, 'default.cer'); + const targetPfx = path.join(outPath, 'default.pfx'); + + await spawnSdkTool( + 'makecert.exe', + ['-r', '-h', '0', '-n', `CN=${publisherName}`, '-eku', '1.3.6.1.5.5.7.3.3', '-pe', '-sv', defaultPvk, targetCert]); + + await spawnSdkTool('pvk2pfx.exe', ['-pvk', defaultPvk, '-spc', targetCert, '-pfx', targetPfx]); + + return targetPfx; +} + +export default async (dir, appName, targetArch, forgeConfig, packageJSON) => { // eslint-disable-line + const outPath = path.resolve(dir, `../make/appx/${targetArch}`); + await ensureDirectory(outPath); + + const opts = Object.assign({}, forgeConfig.windowsStoreConfig, { + inputDirectory: dir, + outputDirectory: outPath, + publisher: packageJSON.author, + flatten: false, + deploy: false, + packageVersion: `${packageJSON.version}.0`, + packageName: appName.replace(/-/g, ''), + packageDisplayName: appName, + packageDescription: packageJSON.description || appName, + packageExecutable: `app\\${appName}.exe`, + windowsKit: path.dirname(findSdkTool('makeappx.exe')), + }); + + if (!opts.devCert) { + opts.devCert = await createDefaultCertificate(opts.publisher, outPath); + } + + if (!opts.publisher.match(/^CN=/)) { + opts.publisher = `CN=${opts.publisher}`; + } + + if (opts.packageVersion.match(/-/)) { + if (opts.makeVersionWinStoreCompatible) { + const noBeta = opts.packageVersion.replace(/-.*/, ''); + opts.packageVersion = `${noBeta}.0`; + } else { + const err = "Windows Store version numbers don't support semver beta tags. To" + + 'automatically fix this, set makeVersionWinStoreCompatible to true or ' + + 'explicitly set packageVersion to a version of the format X.Y.Z.A'; + + throw new Error(err); + } + } + + delete opts.makeVersionWinStoreCompatible; + + await windowsStore(opts); +}; diff --git a/test/fixture/dummy_app/package.json b/test/fixture/dummy_app/package.json index 7841cf1668..c458a52ed1 100644 --- a/test/fixture/dummy_app/package.json +++ b/test/fixture/dummy_app/package.json @@ -13,14 +13,15 @@ "config": { "forge": { "make_targets": { - "win32": ["squirrel"], + "win32": ["squirrel", "appx"], "darwin": ["zip"], "linux": ["deb", "rpm"] }, "electronPackagerConfig": {}, "electronInstallerRedhat": {}, "electronInstallerDebian": {}, - "electronWinstallerConfig": { "windows": "magic" } + "electronWinstallerConfig": { "windows": "magic" }, + "windowsStoreConfig": { "packageName": "test" } } }, "devDependencies": { diff --git a/test/fixture/dummy_js_conf/forge.config.js b/test/fixture/dummy_js_conf/forge.config.js index 581c1e3bfe..d2c17999ad 100644 --- a/test/fixture/dummy_js_conf/forge.config.js +++ b/test/fixture/dummy_js_conf/forge.config.js @@ -1,6 +1,6 @@ module.exports = { make_targets: { - win32: ['squirrel'], + win32: ['squirrel', 'appx'], darwin: ['zip'], linux: ['deb', 'rpm'], mas: ['zip'], diff --git a/test/util_spec.js b/test/util_spec.js index 1082102c15..d47e4867d6 100644 --- a/test/util_spec.js +++ b/test/util_spec.js @@ -18,7 +18,7 @@ describe('resolve-dir', () => { const defaults = { make_targets: { - win32: ['squirrel'], + win32: ['squirrel', 'appx'], darwin: ['zip'], linux: ['deb', 'rpm'], mas: ['zip'], @@ -34,6 +34,7 @@ describe('forge-config', () => { it('should resolve the object in package.json with defaults if one exists', async () => { expect(await findConfig(path.resolve(__dirname, 'fixture/dummy_app'))).to.be.deep.equal(Object.assign({}, defaults, { electronWinstallerConfig: { windows: 'magic' }, + windowsStoreConfig: { packageName: 'test' }, })); }); diff --git a/tmpl/package.json b/tmpl/package.json index fa9514e188..c688ff9c52 100644 --- a/tmpl/package.json +++ b/tmpl/package.json @@ -24,8 +24,10 @@ "electronInstallerDebian": {}, "electronInstallerRedhat": {}, "github_repository": { - "owner": "", - "name": "" + "owner": "" + }, + "windowsStoreConfig": { + "packageName": "" } } }