Upload via GUI (7543 Dateien)
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) Contributors to the Electron project
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
# @electron/universal
|
||||||
|
|
||||||
|
> Create universal macOS Electron applications
|
||||||
|
|
||||||
|
[](https://github.com/electron/universal/actions/workflows/test.yml)
|
||||||
|
[](https://npm.im/@electron/universal)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
This package takes an x64 app and an arm64 app and glues them together into a
|
||||||
|
[Universal macOS binary](https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary).
|
||||||
|
|
||||||
|
Note that parameters need to be **absolute** paths.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { makeUniversalApp } from '@electron/universal';
|
||||||
|
|
||||||
|
await makeUniversalApp({
|
||||||
|
x64AppPath: 'path/to/App_x64.app',
|
||||||
|
arm64AppPath: 'path/to/App_arm64.app',
|
||||||
|
outAppPath: 'path/to/App_universal.app',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced configuration
|
||||||
|
|
||||||
|
The basic usage patterns will work for most apps out of the box. Additional configuration
|
||||||
|
options are available for advanced usecases.
|
||||||
|
|
||||||
|
### Merging ASAR archives to reduce app size
|
||||||
|
|
||||||
|
**Added in [v1.2.0](https://github.com/electron/universal/commit/38ab1c3559e25382957d608e49e624dc72a4409c)**
|
||||||
|
|
||||||
|
If you are using ASAR archives to store your Electron app's JavaScript code, you can use the
|
||||||
|
`mergeASARs` option to merge your x64 and arm64 ASAR files to reduce the bundle size of
|
||||||
|
the output Universal app.
|
||||||
|
|
||||||
|
If some files are present in only the x64 app but not the arm64 version (or vice-versa),
|
||||||
|
you can exclude them from the merging process by specifying a `minimatch` pattern
|
||||||
|
in `singleArchFiles`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { makeUniversalApp } from '@electron/universal';
|
||||||
|
|
||||||
|
await makeUniversalApp({
|
||||||
|
x64AppPath: 'path/to/App_x64.app',
|
||||||
|
arm64AppPath: 'path/to/App_arm64.app',
|
||||||
|
outAppPath: 'path/to/App_universal.app',
|
||||||
|
mergeASARs: true,
|
||||||
|
singleArchFiles: 'node_modules/some-native-module/lib/binding/Release/**', // if you have files in your asar that are unique to x64 or arm64 apps
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
If `@electron/universal` detects an architecture-unique file that isn't covered by the
|
||||||
|
`singleArchFiles` rule, an error will be thrown.
|
||||||
|
|
||||||
|
### Skip lipo for certain binaries in your Universal app
|
||||||
|
|
||||||
|
**Added in [1.3.0](https://github.com/electron/universal/commit/01dfb8a9636965fe154192b07934670dd42509f3)**
|
||||||
|
|
||||||
|
If your Electron app contains binary resources that are already merged with the
|
||||||
|
`lipo` tool, providing a [`minimatch`] pattern to matching files in the `x64ArchFiles`
|
||||||
|
parameter will prevent `@electron/universal` from attempting to merge them a second time.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { makeUniversalApp } from '@electron/universal';
|
||||||
|
|
||||||
|
await makeUniversalApp({
|
||||||
|
x64AppPath: 'path/to/App_x64.app',
|
||||||
|
arm64AppPath: 'path/to/App_arm64.app',
|
||||||
|
outAppPath: 'path/to/App_universal.app',
|
||||||
|
mergeASARs: true,
|
||||||
|
x64ArchFiles: '*/electron-helper', // `electron-helper` is a binary merged using `lipo`
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
If `@electron/universal` detects a lipo'd file that isn't covered by the `x64ArchFiles` rule,
|
||||||
|
an error will be thrown.
|
||||||
|
|
||||||
|
### Including already codesigned app bundles into your Universal app
|
||||||
|
|
||||||
|
**Added in [v1.4.0](https://github.com/electron/universal/commit/b02ce7697fd2a3c2c79e1f6ab6bf7052125865cc)**
|
||||||
|
|
||||||
|
By default, the merging process will generate an `ElectronAsarIntegrity` key for
|
||||||
|
any `Info.plist` files in your Electron app.
|
||||||
|
|
||||||
|
If your Electron app bundles another `.app` that is already signed, you need to use
|
||||||
|
the `infoPlistsToIgnore` option to avoid modifying that app's plist.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { makeUniversalApp } from '@electron/universal';
|
||||||
|
|
||||||
|
await makeUniversalApp({
|
||||||
|
x64AppPath: 'path/to/App_x64.app',
|
||||||
|
arm64AppPath: 'path/to/App_arm64.app',
|
||||||
|
outAppPath: 'path/to/App_universal.app',
|
||||||
|
infoPlistsToIgnore: 'my-internal.app/Contents/Info.plist'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
#### The app is twice as big now, why?
|
||||||
|
|
||||||
|
A Universal app is just the x64 app and the arm64 app glued together into a single application.
|
||||||
|
It's twice as big because it contains two apps in one.
|
||||||
|
|
||||||
|
Merging your ASAR bundles can yield significant app size reductions depending on how large
|
||||||
|
your `app.asar` file is.
|
||||||
|
|
||||||
|
#### What about native modules?
|
||||||
|
|
||||||
|
Out of the box, you don't need to worry about building universal versions of your
|
||||||
|
native modules. As long as your x64 and arm64 apps work in isolation, the Universal
|
||||||
|
app will work as well.
|
||||||
|
|
||||||
|
Note that if you are using `mergeASARs`, you may need to add architecture-specific
|
||||||
|
binary resources to the `singleArchFiles` pattern.
|
||||||
|
See [Merging ASARs usage](#merging-asar-archives-to-reduce-app-size) for an example.
|
||||||
|
|
||||||
|
#### How do I build my app for Apple silicon in the first place?
|
||||||
|
|
||||||
|
Check out the [Electron Apple silicon blog post](https://www.electronjs.org/blog/apple-silicon).
|
||||||
|
|
||||||
|
[`minimatch`]: https://github.com/isaacs/minimatch?tab=readme-ov-file#features
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
export interface HeaderHash {
|
||||||
|
algorithm: 'SHA256';
|
||||||
|
hash: string;
|
||||||
|
}
|
||||||
|
export interface AsarIntegrity {
|
||||||
|
[key: string]: HeaderHash;
|
||||||
|
}
|
||||||
|
export declare function computeIntegrityData(contentsPath: string): Promise<AsarIntegrity>;
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import * as fs from 'fs-extra';
|
||||||
|
import path from 'path';
|
||||||
|
import { AppFileType, getAllAppFiles } from './file-utils';
|
||||||
|
import { generateAsarIntegrity } from './asar-utils';
|
||||||
|
export async function computeIntegrityData(contentsPath) {
|
||||||
|
const root = await fs.realpath(contentsPath);
|
||||||
|
const resourcesRelativePath = 'Resources';
|
||||||
|
const resourcesPath = path.resolve(root, resourcesRelativePath);
|
||||||
|
const resources = await getAllAppFiles(resourcesPath);
|
||||||
|
const resourceAsars = resources
|
||||||
|
.filter((file) => file.type === AppFileType.APP_CODE)
|
||||||
|
.reduce((prev, file) => (Object.assign(Object.assign({}, prev), { [path.join(resourcesRelativePath, file.relativePath)]: path.join(resourcesPath, file.relativePath) })), {});
|
||||||
|
// sort to produce constant result
|
||||||
|
const allAsars = Object.entries(resourceAsars).sort(([name1], [name2]) => name1.localeCompare(name2));
|
||||||
|
const hashes = await Promise.all(allAsars.map(async ([, from]) => generateAsarIntegrity(from)));
|
||||||
|
const asarIntegrity = {};
|
||||||
|
for (let i = 0; i < allAsars.length; i++) {
|
||||||
|
const [asar] = allAsars[i];
|
||||||
|
asarIntegrity[asar] = hashes[i];
|
||||||
|
}
|
||||||
|
return asarIntegrity;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=integrity.js.map
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"integrity.js","sourceRoot":"","sources":["../../src/integrity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAerD,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,YAAoB;IAC7D,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IAE7C,MAAM,qBAAqB,GAAG,WAAW,CAAC;IAC1C,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,qBAAqB,CAAC,CAAC;IAEhE,MAAM,SAAS,GAAG,MAAM,cAAc,CAAC,aAAa,CAAC,CAAC;IACtD,MAAM,aAAa,GAAG,SAAS;SAC5B,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,WAAW,CAAC,QAAQ,CAAC;SACpD,MAAM,CACL,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,iCACX,IAAI,KACP,CAAC,IAAI,CAAC,IAAI,CAAC,qBAAqB,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAC9D,aAAa,EACb,IAAI,CAAC,YAAY,CAClB,IACD,EACF,EAAE,CACH,CAAC;IAEJ,kCAAkC;IAClC,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,CACvE,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAC3B,CAAC;IACF,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAChG,MAAM,aAAa,GAAkB,EAAE,CAAC;IACxC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QACxC,MAAM,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC3B,aAAa,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;KACjC;IACD,OAAO,aAAa,CAAC;AACvB,CAAC"}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export declare const sha: (filePath: string) => Promise<any>;
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
import * as fs from 'fs-extra';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
import { pipeline } from 'stream/promises';
|
||||||
|
import { d } from './debug';
|
||||||
|
export const sha = async (filePath) => {
|
||||||
|
d('hashing', filePath);
|
||||||
|
const hash = crypto.createHash('sha256');
|
||||||
|
hash.setEncoding('hex');
|
||||||
|
await pipeline(fs.createReadStream(filePath), hash);
|
||||||
|
return hash.read();
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=sha.js.map
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"sha.js","sourceRoot":"","sources":["../../src/sha.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE3C,OAAO,EAAE,CAAC,EAAE,MAAM,SAAS,CAAC;AAE5B,MAAM,CAAC,MAAM,GAAG,GAAG,KAAK,EAAE,QAAgB,EAAE,EAAE;IAC5C,CAAC,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACvB,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IACzC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACxB,MAAM,QAAQ,CAAC,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC;IACpD,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC;AACrB,CAAC,CAAC"}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const electron_1 = require("electron");
|
||||||
|
const path_1 = __importDefault(require("path"));
|
||||||
|
if (process.arch === 'arm64') {
|
||||||
|
setPaths('arm64');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setPaths('x64');
|
||||||
|
}
|
||||||
|
function setPaths(platform) {
|
||||||
|
// This should return the full path, ending in something like
|
||||||
|
// Notion.app/Contents/Resources/app.asar
|
||||||
|
const appPath = electron_1.app.getAppPath();
|
||||||
|
const asarFile = `app-${platform}.asar`;
|
||||||
|
// Maybe we'll handle this in Electron one day
|
||||||
|
if (path_1.default.basename(appPath) === 'app.asar') {
|
||||||
|
const platformAppPath = path_1.default.join(path_1.default.dirname(appPath), asarFile);
|
||||||
|
// This is an undocumented API. It exists.
|
||||||
|
electron_1.app.setAppPath(platformAppPath);
|
||||||
|
}
|
||||||
|
process._archPath = require.resolve(`../${asarFile}`);
|
||||||
|
}
|
||||||
|
require(process._archPath);
|
||||||
|
//# sourceMappingURL=has-asar.js.map
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"has-asar.js","sourceRoot":"","sources":["has-asar.ts"],"names":[],"mappings":";;;;;AAAA,uCAA+B;AAC/B,gDAAwB;AAExB,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO,EAAE;IAC5B,QAAQ,CAAC,OAAO,CAAC,CAAC;CACnB;KAAM;IACL,QAAQ,CAAC,KAAK,CAAC,CAAC;CACjB;AAED,SAAS,QAAQ,CAAC,QAAgB;IAChC,6DAA6D;IAC7D,yCAAyC;IACzC,MAAM,OAAO,GAAG,cAAG,CAAC,UAAU,EAAE,CAAC;IACjC,MAAM,QAAQ,GAAG,OAAO,QAAQ,OAAO,CAAC;IAExC,8CAA8C;IAC9C,IAAI,cAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,UAAU,EAAE;QACzC,MAAM,eAAe,GAAG,cAAI,CAAC,IAAI,CAAC,cAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,CAAC;QAEnE,0CAA0C;QAC1C,cAAG,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC;KACjC;IAED,OAAO,CAAC,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,QAAQ,EAAE,CAAC,CAAC;AACxD,CAAC;AAED,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC"}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const electron_1 = require("electron");
|
||||||
|
const path_1 = __importDefault(require("path"));
|
||||||
|
if (process.arch === 'arm64') {
|
||||||
|
setPaths('arm64');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setPaths('x64');
|
||||||
|
}
|
||||||
|
function setPaths(platform) {
|
||||||
|
// This should return the full path, ending in something like
|
||||||
|
// Notion.app/Contents/Resources/app
|
||||||
|
const appPath = electron_1.app.getAppPath();
|
||||||
|
const appFolder = `app-${platform}`;
|
||||||
|
// Maybe we'll handle this in Electron one day
|
||||||
|
if (path_1.default.basename(appPath) === 'app') {
|
||||||
|
const platformAppPath = path_1.default.join(path_1.default.dirname(appPath), appFolder);
|
||||||
|
// This is an undocumented private API. It exists.
|
||||||
|
electron_1.app.setAppPath(platformAppPath);
|
||||||
|
}
|
||||||
|
process._archPath = require.resolve(`../${appFolder}`);
|
||||||
|
}
|
||||||
|
require(process._archPath);
|
||||||
|
//# sourceMappingURL=no-asar.js.map
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"no-asar.js","sourceRoot":"","sources":["no-asar.ts"],"names":[],"mappings":";;;;;AAAA,uCAA+B;AAC/B,gDAAwB;AAExB,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO,EAAE;IAC5B,QAAQ,CAAC,OAAO,CAAC,CAAC;CACnB;KAAM;IACL,QAAQ,CAAC,KAAK,CAAC,CAAC;CACjB;AAED,SAAS,QAAQ,CAAC,QAAgB;IAChC,6DAA6D;IAC7D,oCAAoC;IACpC,MAAM,OAAO,GAAG,cAAG,CAAC,UAAU,EAAE,CAAC;IACjC,MAAM,SAAS,GAAG,OAAO,QAAQ,EAAE,CAAC;IAEpC,8CAA8C;IAC9C,IAAI,cAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,KAAK,EAAE;QACpC,MAAM,eAAe,GAAG,cAAI,CAAC,IAAI,CAAC,cAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC,CAAC;QAEpE,kDAAkD;QAClD,cAAG,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC;KACjC;IAED,OAAO,CAAC,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,SAAS,EAAE,CAAC,CAAC;AACzD,CAAC;AAED,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC"}
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"name": "@electron/universal",
|
||||||
|
"version": "2.0.3",
|
||||||
|
"description": "Utility for creating Universal macOS applications from two x64 and arm64 Electron applications",
|
||||||
|
"main": "dist/cjs/index.js",
|
||||||
|
"module": "dist/esm/index.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"electron",
|
||||||
|
"apple silicon",
|
||||||
|
"universal"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/electron/universal.git"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.4"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/*",
|
||||||
|
"entry-asar/*",
|
||||||
|
"!entry-asar/**/*.ts",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"author": "Samuel Attard",
|
||||||
|
"publishConfig": {
|
||||||
|
"provenance": true
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && tsc -p tsconfig.entry-asar.json",
|
||||||
|
"build:docs": "npx typedoc",
|
||||||
|
"lint": "prettier --check \"{src,entry-asar,test}/**/*.ts\" \"*.ts\"",
|
||||||
|
"prettier:write": "prettier --write \"{src,entry-asar,test}/**/*.ts\" \"*.ts\"",
|
||||||
|
"prepublishOnly": "npm run build",
|
||||||
|
"test": "jest",
|
||||||
|
"prepare": "husky install"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@electron/get": "^3.0.0",
|
||||||
|
"@types/cross-zip": "^4.0.1",
|
||||||
|
"@types/debug": "^4.1.10",
|
||||||
|
"@types/fs-extra": "^11.0.3",
|
||||||
|
"@types/jest": "^29.5.7",
|
||||||
|
"@types/minimatch": "^5.1.2",
|
||||||
|
"@types/node": "^20.8.10",
|
||||||
|
"@types/plist": "^3.0.4",
|
||||||
|
"cross-zip": "^4.0.0",
|
||||||
|
"husky": "^8.0.3",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"lint-staged": "^15.2.10",
|
||||||
|
"prettier": "^3.0.3",
|
||||||
|
"ts-jest": "^29.1.1",
|
||||||
|
"typedoc": "~0.25.13",
|
||||||
|
"typescript": "^5.2.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@electron/asar": "^3.3.1",
|
||||||
|
"@malept/cross-spawn-promise": "^2.0.0",
|
||||||
|
"debug": "^4.3.1",
|
||||||
|
"dir-compare": "^4.2.0",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
|
"minimatch": "^9.0.3",
|
||||||
|
"plist": "^3.1.0"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.ts": [
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"jackspeak": "2.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
NAME
|
||||||
|
electron-windows-sign -- code signing for Electron apps on Windows.
|
||||||
|
|
||||||
|
SYNOPSIS
|
||||||
|
electron-windows-sign app [options ...]
|
||||||
|
|
||||||
|
DESCRIPTION
|
||||||
|
app
|
||||||
|
Path to the application to sign. It must be a directory.
|
||||||
|
|
||||||
|
certificate-file
|
||||||
|
Path to the certificate file (.pfx) to use for signing.
|
||||||
|
|
||||||
|
certificate-password
|
||||||
|
Password to use for the certificate.
|
||||||
|
|
||||||
|
sign-tool-path
|
||||||
|
Path to the signtool.exe binary. If not specified, the tool will attempt
|
||||||
|
use a built-in version.
|
||||||
|
|
||||||
|
timestamp-server
|
||||||
|
URL of the timestamp server to use. If not specified, the tool will
|
||||||
|
attempt to use a built-in server (http://timestamp.digicert.com)
|
||||||
|
|
||||||
|
description
|
||||||
|
Description to use for the signed files. Passed as /d to signtool.exe.
|
||||||
|
|
||||||
|
website
|
||||||
|
URL of the website to use for the signed files. Passed as /du to
|
||||||
|
signtool.exe.
|
||||||
|
|
||||||
|
sign-with-params
|
||||||
|
Additional parameters to pass to signtool.exe. This can be used to
|
||||||
|
specify additional certificates to use for cross-signing.
|
||||||
|
|
||||||
|
automatically-select-certificate
|
||||||
|
Automatically select the best certificate to use for signing. On by default.
|
||||||
|
|
||||||
|
help
|
||||||
|
Print this usage information.
|
||||||
|
|
||||||
|
debug
|
||||||
|
Print additional debug information.
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const args = require('minimist')(process.argv.slice(2), {
|
||||||
|
string: [
|
||||||
|
'certificate-file',
|
||||||
|
'certificate-password',
|
||||||
|
'sign-tool-path',
|
||||||
|
'timestamp-server',
|
||||||
|
'description',
|
||||||
|
'website',
|
||||||
|
'sign-with-params'
|
||||||
|
],
|
||||||
|
boolean: [
|
||||||
|
'help',
|
||||||
|
'debug'
|
||||||
|
],
|
||||||
|
default: {
|
||||||
|
'automatically-select-certificate': true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const usage = fs.readFileSync(path.join(__dirname, 'electron-windows-sign-usage.txt')).toString();
|
||||||
|
const sign = require('../').sign;
|
||||||
|
|
||||||
|
args.app = args._.shift();
|
||||||
|
|
||||||
|
if (!args.app || args.help) {
|
||||||
|
console.log(usage);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove excess arguments
|
||||||
|
delete args._;
|
||||||
|
delete args.help;
|
||||||
|
|
||||||
|
sign(args, function done(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Sign failed:');
|
||||||
|
if (err.message) console.error(err.message);
|
||||||
|
else if (err.stack) console.error(err.stack);
|
||||||
|
else console.log(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log('Application signed:', args.app);
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import { SignOptions } from './types';
|
||||||
|
/**
|
||||||
|
* Recursively goes through an entire directory and returns an array
|
||||||
|
* of full paths for files ot sign.
|
||||||
|
*
|
||||||
|
* - Portable executable files (.exe, .dll, .sys, .efi, .scr, .node)
|
||||||
|
* - Microsoft installers (.msi)
|
||||||
|
* - APPX/MSIX packages (.appx, .appxbundle, .msix, .msixbundle)
|
||||||
|
* - Catalog files (.cat)
|
||||||
|
* - Cabinet files (.cab)
|
||||||
|
* - Silverlight applications (.xap)
|
||||||
|
* - Scripts (.vbs, .wsf, .ps1)
|
||||||
|
* If configured:
|
||||||
|
* - JavaScript files (.js)
|
||||||
|
*/
|
||||||
|
export declare function getFilesToSign(options: SignOptions, dir?: string): Array<string>;
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.getFilesToSign = void 0;
|
||||||
|
const path_1 = __importDefault(require("path"));
|
||||||
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
||||||
|
const IS_PE_REGEX = /\.(exe|dll|sys|efi|scr|node)$/i;
|
||||||
|
const IS_MSI_REGEX = /\.msi$/i;
|
||||||
|
const IS_PACKAGE_REGEX = /\.(appx|appxbundle|msix|msixbundle)$/i;
|
||||||
|
const IS_CATCAB_REGEX = /\.(cat|cab)$/i;
|
||||||
|
const IS_SILVERLIGHT_REGEX = /\.xap$/i;
|
||||||
|
const IS_SCRIPT_REGEX = /\.(vbs|wsf|ps1)$/i;
|
||||||
|
const IS_JS_REGEX = /\.js$/i;
|
||||||
|
/**
|
||||||
|
* Recursively goes through an entire directory and returns an array
|
||||||
|
* of full paths for files ot sign.
|
||||||
|
*
|
||||||
|
* - Portable executable files (.exe, .dll, .sys, .efi, .scr, .node)
|
||||||
|
* - Microsoft installers (.msi)
|
||||||
|
* - APPX/MSIX packages (.appx, .appxbundle, .msix, .msixbundle)
|
||||||
|
* - Catalog files (.cat)
|
||||||
|
* - Cabinet files (.cab)
|
||||||
|
* - Silverlight applications (.xap)
|
||||||
|
* - Scripts (.vbs, .wsf, .ps1)
|
||||||
|
* If configured:
|
||||||
|
* - JavaScript files (.js)
|
||||||
|
*/
|
||||||
|
function getFilesToSign(options, dir) {
|
||||||
|
if (isSignOptionsForFiles(options)) {
|
||||||
|
return options.files;
|
||||||
|
}
|
||||||
|
dir = dir || options.appDirectory;
|
||||||
|
// Array of file paths to sign
|
||||||
|
const result = [];
|
||||||
|
// Iterate over the app directory, looking for files to sign
|
||||||
|
const files = fs_extra_1.default.readdirSync(dir);
|
||||||
|
const regexes = [
|
||||||
|
IS_PE_REGEX,
|
||||||
|
IS_MSI_REGEX,
|
||||||
|
IS_PACKAGE_REGEX,
|
||||||
|
IS_CATCAB_REGEX,
|
||||||
|
IS_SILVERLIGHT_REGEX,
|
||||||
|
IS_SCRIPT_REGEX
|
||||||
|
];
|
||||||
|
if (options.signJavaScript) {
|
||||||
|
regexes.push(IS_JS_REGEX);
|
||||||
|
}
|
||||||
|
for (const file of files) {
|
||||||
|
const fullPath = path_1.default.resolve(dir, file);
|
||||||
|
if (fs_extra_1.default.statSync(fullPath).isDirectory()) {
|
||||||
|
// If it's a directory, recurse
|
||||||
|
result.push(...getFilesToSign(options, fullPath));
|
||||||
|
}
|
||||||
|
else if (regexes.some((regex) => regex.test(file))) {
|
||||||
|
// If it's a match, add it to the list
|
||||||
|
result.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
exports.getFilesToSign = getFilesToSign;
|
||||||
|
function isSignOptionsForFiles(input) {
|
||||||
|
return !!input.files;
|
||||||
|
}
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
import { sign } from './sign';
|
||||||
|
import { createSeaSignTool, SeaOptions, InternalSeaOptions } from './sea';
|
||||||
|
import { HookFunction, OptionalHookOptions, OptionalSignToolOptions, SignOptions, SignToolOptions, SignOptionsForDirectory, SignOptionsForFiles } from './types';
|
||||||
|
export { sign, SignOptions, SignToolOptions, HookFunction, OptionalSignToolOptions, OptionalHookOptions, createSeaSignTool, SeaOptions, InternalSeaOptions, SignOptionsForDirectory, SignOptionsForFiles };
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.createSeaSignTool = exports.sign = void 0;
|
||||||
|
const sign_1 = require("./sign");
|
||||||
|
Object.defineProperty(exports, "sign", { enumerable: true, get: function () { return sign_1.sign; } });
|
||||||
|
const sea_1 = require("./sea");
|
||||||
|
Object.defineProperty(exports, "createSeaSignTool", { enumerable: true, get: function () { return sea_1.createSeaSignTool; } });
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
import { SignToolOptions } from './types';
|
||||||
|
/**
|
||||||
|
* Options for signing with a Node.js single executable application.
|
||||||
|
*
|
||||||
|
* @category Single executable applications
|
||||||
|
*/
|
||||||
|
export interface SeaOptions {
|
||||||
|
/**
|
||||||
|
* Full path to the Node.js single executable application. Needs to end with `.exe`.
|
||||||
|
*/
|
||||||
|
path: string;
|
||||||
|
/**
|
||||||
|
* A binary to use. Will use the current executable (process.execPath) by default.
|
||||||
|
*
|
||||||
|
* @defaultValue The Node.js {@link https://nodejs.org/api/process.html#processexecpath | `process.execPath`}
|
||||||
|
*/
|
||||||
|
bin?: string;
|
||||||
|
/**
|
||||||
|
* Options to pass to SignTool.
|
||||||
|
*/
|
||||||
|
windowsSign: SignToolOptions;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface represents {@link SeaOptions} with all optional properties
|
||||||
|
* inferred by `@electron/windows-sign` if not passed in by the user.
|
||||||
|
*
|
||||||
|
* @category Single executable applications
|
||||||
|
*/
|
||||||
|
export interface InternalSeaOptions extends Required<SeaOptions> {
|
||||||
|
/**
|
||||||
|
* Directory of the Node.js single executable application.
|
||||||
|
*/
|
||||||
|
dir: string;
|
||||||
|
/**
|
||||||
|
* File name of the Node.js single executable application.
|
||||||
|
*/
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* cross-dir uses new Error() stacks
|
||||||
|
* to figure out our directory in a way
|
||||||
|
* that's somewhat cross-compatible.
|
||||||
|
*
|
||||||
|
* We can't just use __dirname because it's
|
||||||
|
* undefined in ESM - and we can't use import.meta.url
|
||||||
|
* because TypeScript won't allow usage unless you're
|
||||||
|
* _only_ compiling for ESM.
|
||||||
|
*/
|
||||||
|
export declare const DIRNAME: string;
|
||||||
|
/**
|
||||||
|
* Uses Node's "Single Executable App" functionality
|
||||||
|
* to create a Node-driven signtool.exe that calls this
|
||||||
|
* module.
|
||||||
|
*
|
||||||
|
* This is useful with other tooling that _always_ calls
|
||||||
|
* a signtool.exe to sign. Some of those tools cannot be
|
||||||
|
* easily configured, but we _can_ override their signtool.exe.
|
||||||
|
*
|
||||||
|
* @category Single executable applications
|
||||||
|
*/
|
||||||
|
export declare function createSeaSignTool(options?: Partial<SeaOptions>): Promise<InternalSeaOptions>;
|
||||||
+220
@@ -0,0 +1,220 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.createSeaSignTool = exports.DIRNAME = void 0;
|
||||||
|
const cross_dirname_1 = require("cross-dirname");
|
||||||
|
const path_1 = __importDefault(require("path"));
|
||||||
|
const os_1 = __importDefault(require("os"));
|
||||||
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
||||||
|
const postject_1 = __importDefault(require("postject"));
|
||||||
|
const spawn_1 = require("./spawn");
|
||||||
|
const log_1 = require("./utils/log");
|
||||||
|
/**
|
||||||
|
* cross-dir uses new Error() stacks
|
||||||
|
* to figure out our directory in a way
|
||||||
|
* that's somewhat cross-compatible.
|
||||||
|
*
|
||||||
|
* We can't just use __dirname because it's
|
||||||
|
* undefined in ESM - and we can't use import.meta.url
|
||||||
|
* because TypeScript won't allow usage unless you're
|
||||||
|
* _only_ compiling for ESM.
|
||||||
|
*/
|
||||||
|
exports.DIRNAME = (0, cross_dirname_1.getDirname)();
|
||||||
|
const FILENAMES = {
|
||||||
|
SEA_CONFIG: 'sea-config.json',
|
||||||
|
SEA_MAIN: 'sea.js',
|
||||||
|
SEA_BLOB: 'sea.blob',
|
||||||
|
SEA_RECEIVER: 'receiver.mjs'
|
||||||
|
};
|
||||||
|
const SEA_MAIN_SCRIPT = `
|
||||||
|
const bin = "%PATH_TO_BIN%";
|
||||||
|
const script = "%PATH_TO_SCRIPT%";
|
||||||
|
const options = %WINDOWS_SIGN_OPTIONS%
|
||||||
|
|
||||||
|
const { spawnSync } = require('child_process');
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
console.log("@electron/windows-sign sea");
|
||||||
|
console.log({ bin, script });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const spawn = spawnSync(
|
||||||
|
bin,
|
||||||
|
[ script, JSON.stringify(options), JSON.stringify(process.argv.slice(1)) ],
|
||||||
|
{ stdio: ['inherit', 'inherit', 'pipe'] }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (spawn.status !== 0) {
|
||||||
|
throw new Error(\`Spawn failed with code: \${spawn.status}. Stderr: \${spawn.stderr}\`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Do not rethrow the error or write it to stdout/stderr. Then the process won't terminate.
|
||||||
|
// See: https://github.com/electron/windows-sign/pull/48
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
`;
|
||||||
|
const SEA_RECEIVER_SCRIPT = `
|
||||||
|
import { sign } from '@electron/windows-sign';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const logPath = path.join('electron-windows-sign.log');
|
||||||
|
const options = JSON.parse(process.argv[2]);
|
||||||
|
const signArgv = JSON.parse(process.argv[3]);
|
||||||
|
const files = signArgv.slice(-1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.appendFileSync(logPath, \`\\nCalled with: \${JSON.stringify(process.argv, null, 2)}\`);
|
||||||
|
|
||||||
|
sign({ ...options, files })
|
||||||
|
.then((result) => {
|
||||||
|
fs.appendFileSync(logPath, \`\\nSuccessfully signed with result: \${result}\`);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
fs.appendFileSync(logPath, \`\\nError from sign: \${error}\`);
|
||||||
|
throw new Error(error);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
fs.appendFileSync(logPath, \`\\Error invoking sign: \${error}\`);
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
/**
|
||||||
|
* Uses Node's "Single Executable App" functionality
|
||||||
|
* to create a Node-driven signtool.exe that calls this
|
||||||
|
* module.
|
||||||
|
*
|
||||||
|
* This is useful with other tooling that _always_ calls
|
||||||
|
* a signtool.exe to sign. Some of those tools cannot be
|
||||||
|
* easily configured, but we _can_ override their signtool.exe.
|
||||||
|
*
|
||||||
|
* @category Single executable applications
|
||||||
|
*/
|
||||||
|
async function createSeaSignTool(options = {}) {
|
||||||
|
checkCompatibility();
|
||||||
|
const requiredOptions = await getOptions(options);
|
||||||
|
await createFiles(requiredOptions);
|
||||||
|
await createBlob(requiredOptions);
|
||||||
|
await createBinary(requiredOptions);
|
||||||
|
await createSeaReceiver(requiredOptions);
|
||||||
|
await cleanup(requiredOptions);
|
||||||
|
return requiredOptions;
|
||||||
|
}
|
||||||
|
exports.createSeaSignTool = createSeaSignTool;
|
||||||
|
async function createSeaReceiver(options) {
|
||||||
|
const receiverPath = path_1.default.join(options.dir, FILENAMES.SEA_RECEIVER);
|
||||||
|
await fs_extra_1.default.ensureFile(receiverPath);
|
||||||
|
await fs_extra_1.default.writeFile(receiverPath, SEA_RECEIVER_SCRIPT);
|
||||||
|
}
|
||||||
|
async function createFiles(options) {
|
||||||
|
const { dir, bin } = options;
|
||||||
|
const receiverPath = path_1.default.join(options.dir, FILENAMES.SEA_RECEIVER);
|
||||||
|
// sea-config.json
|
||||||
|
await fs_extra_1.default.outputJSON(path_1.default.join(dir, FILENAMES.SEA_CONFIG), {
|
||||||
|
main: FILENAMES.SEA_MAIN,
|
||||||
|
output: FILENAMES.SEA_BLOB,
|
||||||
|
disableExperimentalSEAWarning: true
|
||||||
|
}, {
|
||||||
|
spaces: 2
|
||||||
|
});
|
||||||
|
// signtool.js
|
||||||
|
const binPath = bin || process.execPath;
|
||||||
|
const script = SEA_MAIN_SCRIPT
|
||||||
|
.replace('%PATH_TO_BIN%', escapeMaybe(binPath))
|
||||||
|
.replace('%PATH_TO_SCRIPT%', escapeMaybe(receiverPath))
|
||||||
|
.replace('%WINDOWS_SIGN_OPTIONS%', JSON.stringify(options.windowsSign));
|
||||||
|
await fs_extra_1.default.outputFile(path_1.default.join(dir, FILENAMES.SEA_MAIN), script);
|
||||||
|
}
|
||||||
|
async function createBlob(options) {
|
||||||
|
const args = ['--experimental-sea-config', 'sea-config.json'];
|
||||||
|
const bin = process.execPath;
|
||||||
|
const cwd = options.dir;
|
||||||
|
(0, log_1.log)(`Calling ${bin} with options:`, args);
|
||||||
|
const { stderr, stdout } = await (0, spawn_1.spawnPromise)(bin, args, {
|
||||||
|
cwd
|
||||||
|
});
|
||||||
|
(0, log_1.log)('stdout:', stdout);
|
||||||
|
(0, log_1.log)('stderr:', stderr);
|
||||||
|
}
|
||||||
|
async function createBinary(options) {
|
||||||
|
const { dir, filename } = options;
|
||||||
|
(0, log_1.log)(`Creating ${filename} in ${dir}`);
|
||||||
|
// Copy Node over
|
||||||
|
const seaPath = path_1.default.join(dir, filename);
|
||||||
|
await fs_extra_1.default.copyFile(process.execPath, seaPath);
|
||||||
|
// Remove the Node signature
|
||||||
|
const signtool = path_1.default.join(exports.DIRNAME, '../../vendor/signtool.exe');
|
||||||
|
await (0, spawn_1.spawnPromise)(signtool, [
|
||||||
|
'remove',
|
||||||
|
'/s',
|
||||||
|
seaPath
|
||||||
|
]);
|
||||||
|
// Inject the blob
|
||||||
|
const blob = await fs_extra_1.default.readFile(path_1.default.join(dir, FILENAMES.SEA_BLOB));
|
||||||
|
await postject_1.default.inject(seaPath, 'NODE_SEA_BLOB', blob, {
|
||||||
|
sentinelFuse: 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function cleanup(options) {
|
||||||
|
const { dir } = options;
|
||||||
|
const toRemove = [
|
||||||
|
FILENAMES.SEA_BLOB,
|
||||||
|
FILENAMES.SEA_MAIN,
|
||||||
|
FILENAMES.SEA_CONFIG
|
||||||
|
];
|
||||||
|
for (const file of toRemove) {
|
||||||
|
try {
|
||||||
|
await fs_extra_1.default.remove(path_1.default.join(dir, file));
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.warn(`Tried and failed to remove ${file}. Continuing.`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function getOptions(options) {
|
||||||
|
const cloned = { ...options };
|
||||||
|
if (!cloned.path) {
|
||||||
|
cloned.path = path_1.default.join(os_1.default.homedir(), '.electron', 'windows-sign', 'sea.exe');
|
||||||
|
await fs_extra_1.default.ensureFile(cloned.path);
|
||||||
|
}
|
||||||
|
if (!cloned.bin) {
|
||||||
|
cloned.bin = process.execPath;
|
||||||
|
}
|
||||||
|
if (!cloned.windowsSign) {
|
||||||
|
throw new Error('Did not find windowsSign options, which are required');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
path: cloned.path,
|
||||||
|
dir: path_1.default.dirname(cloned.path),
|
||||||
|
filename: path_1.default.basename(cloned.path),
|
||||||
|
bin: cloned.bin,
|
||||||
|
windowsSign: cloned.windowsSign
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Ensures that the current Node.js version supports SEA app generation and errors if not.
|
||||||
|
*/
|
||||||
|
function checkCompatibility() {
|
||||||
|
const version = process.versions.node;
|
||||||
|
const split = version.split('.');
|
||||||
|
const major = parseInt(split[0], 10);
|
||||||
|
if (major >= 20) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw new Error(`Your Node.js version (${process.version}) does not support Single Executable Applications. Please upgrade your version of Node.js.`);
|
||||||
|
}
|
||||||
|
/** Make sure that the input string has escaped backwards slashes
|
||||||
|
* - but never double-escaped backwards slashes.
|
||||||
|
*/
|
||||||
|
function escapeMaybe(input) {
|
||||||
|
const result = input.split(path_1.default.sep).join('\\\\');
|
||||||
|
if (result.includes('\\\\\\\\')) {
|
||||||
|
throw new Error(`Your passed input ${input} contains escaped slashes. Please do not escape them`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
import { InternalSignOptions } from './types';
|
||||||
|
/**
|
||||||
|
* Sign with a hook function, basically letting everyone
|
||||||
|
* write completely custom sign logic
|
||||||
|
*
|
||||||
|
* @param {InternalSignOptions} options
|
||||||
|
*/
|
||||||
|
export declare function signWithHook(options: InternalSignOptions): Promise<void>;
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.signWithHook = void 0;
|
||||||
|
const path_1 = __importDefault(require("path"));
|
||||||
|
const log_1 = require("./utils/log");
|
||||||
|
let hookFunction;
|
||||||
|
function getHookFunction(options) {
|
||||||
|
if (options.hookFunction) {
|
||||||
|
return options.hookFunction;
|
||||||
|
}
|
||||||
|
if (options.hookModulePath) {
|
||||||
|
const module = require(path_1.default.resolve(options.hookModulePath));
|
||||||
|
if (module.default) {
|
||||||
|
return module.default;
|
||||||
|
}
|
||||||
|
if (typeof module === 'function') {
|
||||||
|
return module;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hookFunction) {
|
||||||
|
throw new Error('No hook function found. Signing will not be possible. Please see the documentation for how to pass a hook function to @electron/windows-sign');
|
||||||
|
}
|
||||||
|
return hookFunction;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sign with a hook function, basically letting everyone
|
||||||
|
* write completely custom sign logic
|
||||||
|
*
|
||||||
|
* @param {InternalSignOptions} options
|
||||||
|
*/
|
||||||
|
async function signWithHook(options) {
|
||||||
|
hookFunction = getHookFunction(options);
|
||||||
|
for (const file of options.files) {
|
||||||
|
try {
|
||||||
|
await hookFunction(file);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
(0, log_1.log)(`Error signing ${file}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.signWithHook = signWithHook;
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
import { InternalSignOptions } from './types';
|
||||||
|
export declare function signWithSignTool(options: InternalSignOptions): Promise<void>;
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.signWithSignTool = void 0;
|
||||||
|
const path_1 = __importDefault(require("path"));
|
||||||
|
const log_1 = require("./utils/log");
|
||||||
|
const spawn_1 = require("./spawn");
|
||||||
|
const cross_dirname_1 = require("cross-dirname");
|
||||||
|
const DIRNAME = (0, cross_dirname_1.getDirname)();
|
||||||
|
function getSigntoolArgs(options) {
|
||||||
|
// See the following url for docs
|
||||||
|
// https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe
|
||||||
|
const { certificateFile, certificatePassword, hash, timestampServer } = options;
|
||||||
|
const args = ['sign'];
|
||||||
|
// Automatically select cert
|
||||||
|
if (options.automaticallySelectCertificate) {
|
||||||
|
args.push('/a');
|
||||||
|
}
|
||||||
|
// Dual-sign
|
||||||
|
if (options.appendSignature) {
|
||||||
|
args.push('/as');
|
||||||
|
}
|
||||||
|
// Timestamp
|
||||||
|
if (hash === "sha256" /* HASHES.sha256 */) {
|
||||||
|
args.push('/tr', timestampServer);
|
||||||
|
args.push('/td', hash);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
args.push('/t', timestampServer);
|
||||||
|
}
|
||||||
|
// Certificate file
|
||||||
|
if (certificateFile) {
|
||||||
|
args.push('/f', path_1.default.resolve(certificateFile));
|
||||||
|
}
|
||||||
|
// Certificate password
|
||||||
|
if (certificatePassword) {
|
||||||
|
args.push('/p', certificatePassword);
|
||||||
|
}
|
||||||
|
// Hash
|
||||||
|
args.push('/fd', hash);
|
||||||
|
// Description
|
||||||
|
if (options.description) {
|
||||||
|
args.push('/d', options.description);
|
||||||
|
}
|
||||||
|
// Website
|
||||||
|
if (options.website) {
|
||||||
|
args.push('/du', options.website);
|
||||||
|
}
|
||||||
|
// Debug
|
||||||
|
if (options.debug) {
|
||||||
|
args.push('/debug');
|
||||||
|
}
|
||||||
|
if (options.signWithParams) {
|
||||||
|
const extraArgs = [];
|
||||||
|
if (Array.isArray(options.signWithParams)) {
|
||||||
|
extraArgs.push(...options.signWithParams);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Split up at spaces and doublequotes
|
||||||
|
extraArgs.push(...options.signWithParams.match(/(?:[^\s"]+|"[^"]*")+/g));
|
||||||
|
}
|
||||||
|
(0, log_1.log)('Parsed signWithParams as:', extraArgs);
|
||||||
|
args.push(...extraArgs);
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
async function execute(options) {
|
||||||
|
const { signToolPath, files } = options;
|
||||||
|
const args = getSigntoolArgs(options);
|
||||||
|
(0, log_1.log)('Executing signtool with args', { args, files });
|
||||||
|
const { code, stderr, stdout } = await (0, spawn_1.spawnPromise)(signToolPath, [...args, ...files], {
|
||||||
|
env: process.env,
|
||||||
|
cwd: process.cwd()
|
||||||
|
});
|
||||||
|
if (code !== 0) {
|
||||||
|
throw new Error(`Signtool exited with code ${code}. Stderr: ${stderr}. Stdout: ${stdout}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function signWithSignTool(options) {
|
||||||
|
const certificatePassword = options.certificatePassword || process.env.WINDOWS_CERTIFICATE_PASSWORD;
|
||||||
|
const certificateFile = options.certificateFile || process.env.WINDOWS_CERTIFICATE_FILE;
|
||||||
|
const signWithParams = options.signWithParams || process.env.WINDOWS_SIGN_WITH_PARAMS;
|
||||||
|
const timestampServer = options.timestampServer || process.env.WINDOWS_TIMESTAMP_SERVER || 'http://timestamp.digicert.com';
|
||||||
|
const signToolPath = options.signToolPath || process.env.WINDOWS_SIGNTOOL_PATH || path_1.default.join(DIRNAME, '../../vendor/signtool.exe');
|
||||||
|
const description = options.description || process.env.WINDOWS_SIGN_DESCRIPTION;
|
||||||
|
const website = options.website || process.env.WINDOWS_SIGN_WEBSITE;
|
||||||
|
if (!certificateFile && !(signWithParams || signToolPath)) {
|
||||||
|
throw new Error('You must provide a certificateFile and a signToolPath or signing parameters');
|
||||||
|
}
|
||||||
|
if (!signToolPath && !signWithParams && !certificatePassword) {
|
||||||
|
throw new Error('You must provide a certificatePassword or signing parameters');
|
||||||
|
}
|
||||||
|
const internalOptions = {
|
||||||
|
appendSignature: false,
|
||||||
|
...options,
|
||||||
|
certificateFile,
|
||||||
|
certificatePassword,
|
||||||
|
signWithParams,
|
||||||
|
signToolPath,
|
||||||
|
description,
|
||||||
|
timestampServer,
|
||||||
|
website
|
||||||
|
};
|
||||||
|
const hashes = options.hashes == null || options.hashes.length === 0
|
||||||
|
? ["sha1" /* HASHES.sha1 */, "sha256" /* HASHES.sha256 */]
|
||||||
|
: options.hashes;
|
||||||
|
if (hashes.includes("sha1" /* HASHES.sha1 */)) {
|
||||||
|
await execute({ ...internalOptions, hash: "sha1" /* HASHES.sha1 */ });
|
||||||
|
// If we signed with SHA1, we need to append the SHA256 signature:
|
||||||
|
internalOptions.appendSignature = true;
|
||||||
|
}
|
||||||
|
if (hashes.includes("sha256" /* HASHES.sha256 */)) {
|
||||||
|
await execute({ ...internalOptions, hash: "sha256" /* HASHES.sha256 */ });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.signWithSignTool = signWithSignTool;
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
import { SignOptions } from './types';
|
||||||
|
/**
|
||||||
|
* This is the main function exported from this module. It'll
|
||||||
|
* look at your options, determine the best way to sign a file,
|
||||||
|
* and then return one of our internal functions to do the actual
|
||||||
|
* signing.
|
||||||
|
*
|
||||||
|
* @param options
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*
|
||||||
|
* @category Sign
|
||||||
|
*/
|
||||||
|
export declare function sign(options: SignOptions): Promise<void>;
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.sign = void 0;
|
||||||
|
const files_1 = require("./files");
|
||||||
|
const sign_with_hook_1 = require("./sign-with-hook");
|
||||||
|
const sign_with_signtool_1 = require("./sign-with-signtool");
|
||||||
|
const log_1 = require("./utils/log");
|
||||||
|
const parse_env_1 = require("./utils/parse-env");
|
||||||
|
/**
|
||||||
|
* This is the main function exported from this module. It'll
|
||||||
|
* look at your options, determine the best way to sign a file,
|
||||||
|
* and then return one of our internal functions to do the actual
|
||||||
|
* signing.
|
||||||
|
*
|
||||||
|
* @param options
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*
|
||||||
|
* @category Sign
|
||||||
|
*/
|
||||||
|
async function sign(options) {
|
||||||
|
const signJavaScript = options.signJavaScript || (0, parse_env_1.booleanFromEnv)('WINDOWS_SIGN_JAVASCRIPT');
|
||||||
|
const hookModulePath = options.hookModulePath || process.env.WINDOWS_SIGN_HOOK_MODULE_PATH;
|
||||||
|
if (options.debug) {
|
||||||
|
(0, log_1.enableDebugging)();
|
||||||
|
}
|
||||||
|
(0, log_1.log)('Called with options', { options });
|
||||||
|
const files = (0, files_1.getFilesToSign)(options);
|
||||||
|
const internalOptions = {
|
||||||
|
...options,
|
||||||
|
signJavaScript,
|
||||||
|
hookModulePath,
|
||||||
|
files
|
||||||
|
};
|
||||||
|
// If a hook is provides, sign with the hook
|
||||||
|
if (internalOptions.hookFunction || internalOptions.hookModulePath) {
|
||||||
|
(0, log_1.log)('Signing with hook');
|
||||||
|
return (0, sign_with_hook_1.signWithHook)(internalOptions);
|
||||||
|
}
|
||||||
|
// If we're going with the defaults, we're signing
|
||||||
|
// with signtool. Custom signing tools are also
|
||||||
|
// handled here.
|
||||||
|
(0, log_1.log)('Signing with signtool');
|
||||||
|
return (0, sign_with_signtool_1.signWithSignTool)(internalOptions);
|
||||||
|
}
|
||||||
|
exports.sign = sign;
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
/// <reference types="node" />
|
||||||
|
import { SpawnOptions } from 'child_process';
|
||||||
|
export interface SpawnPromiseResult {
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
code: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Spawn a process as a promise
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @param {Array<string>} args
|
||||||
|
* @param {SpawnOptions} [options]
|
||||||
|
* @returns {Promise<SpawnPromiseResult>}
|
||||||
|
*/
|
||||||
|
export declare function spawnPromise(name: string, args: Array<string>, options?: SpawnOptions): Promise<SpawnPromiseResult>;
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.spawnPromise = void 0;
|
||||||
|
const log_1 = require("./utils/log");
|
||||||
|
/**
|
||||||
|
* Spawn a process as a promise
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @param {Array<string>} args
|
||||||
|
* @param {SpawnOptions} [options]
|
||||||
|
* @returns {Promise<SpawnPromiseResult>}
|
||||||
|
*/
|
||||||
|
function spawnPromise(name, args, options) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
const fork = spawn(name, args, options);
|
||||||
|
(0, log_1.log)(`Spawning ${name} with ${args}`);
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
fork.stdout.on('data', (data) => {
|
||||||
|
(0, log_1.log)(`Spawn ${name} stdout: ${data}`);
|
||||||
|
stdout += data;
|
||||||
|
});
|
||||||
|
fork.stderr.on('data', (data) => {
|
||||||
|
(0, log_1.log)(`Spawn ${name} stderr: ${data}`);
|
||||||
|
stderr += data;
|
||||||
|
});
|
||||||
|
fork.on('close', (code) => {
|
||||||
|
(0, log_1.log)(`Spawn ${name}: Child process exited with code ${code}`);
|
||||||
|
resolve({ stdout, stderr, code });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.spawnPromise = spawnPromise;
|
||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
/**
|
||||||
|
* SHA-1 has been deprecated on Windows since 2016. We'll still dualsign.
|
||||||
|
* https://social.technet.microsoft.com/wiki/contents/articles/32288.windows-enforcement-of-sha1-certificates.aspx#Post-February_TwentySeventeen_Plan
|
||||||
|
*/
|
||||||
|
export declare const enum HASHES {
|
||||||
|
sha1 = "sha1",
|
||||||
|
sha256 = "sha256"
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Signing can be either by specifying a directory of files to sign.
|
||||||
|
*
|
||||||
|
* @category Sign
|
||||||
|
*/
|
||||||
|
export type SignOptions = SignOptionsForDirectory | SignOptionsForFiles;
|
||||||
|
/**
|
||||||
|
* Options for signing by passing a path to a directory to be codesigned.
|
||||||
|
*
|
||||||
|
* @category Sign
|
||||||
|
*/
|
||||||
|
export interface SignOptionsForDirectory extends SignToolOptions {
|
||||||
|
/**
|
||||||
|
* Path to the application directory. We will scan this
|
||||||
|
* directory for any `.dll`, `.exe`, `.msi`, or `.node` files and
|
||||||
|
* codesign them with `signtool.exe`.
|
||||||
|
*/
|
||||||
|
appDirectory: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Options for signing by passing an array of files to be codesigned.
|
||||||
|
*
|
||||||
|
* @category Sign
|
||||||
|
*/
|
||||||
|
export interface SignOptionsForFiles extends SignToolOptions {
|
||||||
|
/**
|
||||||
|
* Array of paths to files to be codesigned with `signtool.exe`.
|
||||||
|
*/
|
||||||
|
files: Array<string>;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @category Utility
|
||||||
|
*/
|
||||||
|
export interface SignToolOptions extends OptionalSignToolOptions, OptionalHookOptions {
|
||||||
|
}
|
||||||
|
export interface InternalSignOptions extends SignOptionsForFiles {
|
||||||
|
}
|
||||||
|
export interface InternalSignToolOptions extends OptionalSignToolOptions, OptionalHookOptions {
|
||||||
|
signToolPath: string;
|
||||||
|
timestampServer: string;
|
||||||
|
files: Array<string>;
|
||||||
|
hash: HASHES;
|
||||||
|
appendSignature?: boolean;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @category Utility
|
||||||
|
*/
|
||||||
|
export interface OptionalSignToolOptions {
|
||||||
|
/**
|
||||||
|
* Path to a `.pfx` code signing certificate.
|
||||||
|
* Will use `process.env.WINDOWS_CERTIFICATE_FILE` if this option is not provided.
|
||||||
|
*/
|
||||||
|
certificateFile?: string;
|
||||||
|
/**
|
||||||
|
* Password to {@link certificateFile}. If you don't provide this,
|
||||||
|
* you need to provide the {@link signWithParams} option.
|
||||||
|
* Will use `process.env.WINDOWS_CERTIFICATE_PASSWORD` if this option is not provided.
|
||||||
|
*/
|
||||||
|
certificatePassword?: string;
|
||||||
|
/**
|
||||||
|
* Path to a timestamp server.
|
||||||
|
* Will use `process.env.WINDOWS_TIMESTAMP_SERVER` if this option is not provided.
|
||||||
|
*
|
||||||
|
* @defaultValue http://timestamp.digicert.com
|
||||||
|
*/
|
||||||
|
timestampServer?: string;
|
||||||
|
/**
|
||||||
|
* Description of the signed content. Will be passed to `signtool.exe` as `/d`.
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
/**
|
||||||
|
* URL for the expanded description of the signed content. Will be passed to `signtool.exe` as `/du`.
|
||||||
|
*/
|
||||||
|
website?: string;
|
||||||
|
/**
|
||||||
|
* Path to the `signtool.exe` used to sign. Will use `vendor/signtool.exe` if not provided.
|
||||||
|
*/
|
||||||
|
signToolPath?: string;
|
||||||
|
/**
|
||||||
|
* Additional parameters to pass to `signtool.exe`.
|
||||||
|
*
|
||||||
|
* @see Microsoft's {@link https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe SignTool.exe documentation}
|
||||||
|
*/
|
||||||
|
signWithParams?: string | Array<string>;
|
||||||
|
/**
|
||||||
|
* Enables debug logging.
|
||||||
|
*
|
||||||
|
* @defaultValue false
|
||||||
|
*/
|
||||||
|
debug?: boolean;
|
||||||
|
/**
|
||||||
|
* Automatically selects the best signing certificate according to SignTool. Will be passed to `signtool.exe` as `/a`.
|
||||||
|
*
|
||||||
|
* @defaultValue true
|
||||||
|
*/
|
||||||
|
automaticallySelectCertificate?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether or not to sign JavaScript files.
|
||||||
|
*
|
||||||
|
* @defaultValue false
|
||||||
|
*/
|
||||||
|
signJavaScript?: boolean;
|
||||||
|
/**
|
||||||
|
* Hash algorithms to use for signing.
|
||||||
|
*/
|
||||||
|
hashes?: HASHES[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Custom function that is called sequentially for each file that needs to be signed.
|
||||||
|
*
|
||||||
|
* @param fileToSign Absolute path to the file to sign
|
||||||
|
*
|
||||||
|
* @category Utility
|
||||||
|
*/
|
||||||
|
export type HookFunction = (fileToSign: string) => void | Promise<void>;
|
||||||
|
/**
|
||||||
|
* @category Utility
|
||||||
|
*/
|
||||||
|
export interface OptionalHookOptions {
|
||||||
|
/**
|
||||||
|
* A hook function called for each file that needs to be signed.
|
||||||
|
* Use this for full control over your app's signing logic.
|
||||||
|
* `@electron/windows-sign` will not attempt to sign with SignTool if a custom hook is detected.
|
||||||
|
*/
|
||||||
|
hookFunction?: HookFunction;
|
||||||
|
/**
|
||||||
|
* A path to a JavaScript file, exporting a single function that will be called for each file that needs to be signed.
|
||||||
|
* Use this for full control over your app's signing logic.
|
||||||
|
* `@electron/windows-sign` will not attempt to sign with SignTool if a custom hook is detected.
|
||||||
|
*/
|
||||||
|
hookModulePath?: string;
|
||||||
|
}
|
||||||
|
export interface InternalHookOptions extends OptionalHookOptions {
|
||||||
|
files: Array<string>;
|
||||||
|
}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export declare function enableDebugging(): void;
|
||||||
|
export declare const log: import("debug").Debugger;
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.log = exports.enableDebugging = void 0;
|
||||||
|
const debug_1 = require("debug");
|
||||||
|
function enableDebugging() {
|
||||||
|
debug_1.debug.enable('electron-windows-sign');
|
||||||
|
}
|
||||||
|
exports.enableDebugging = enableDebugging;
|
||||||
|
exports.log = (0, debug_1.debug)('electron-windows-sign');
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Tries to parse an process.env string to a boolean.
|
||||||
|
* Will understand undefined as the default value
|
||||||
|
* Will understand "false", "False", "fAlse", or "0" as `false`
|
||||||
|
* Will understand everything else as true
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @param {string} name
|
||||||
|
* @return {*} {boolean}
|
||||||
|
*/
|
||||||
|
export declare function booleanFromEnv(name: string): boolean | undefined;
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.booleanFromEnv = void 0;
|
||||||
|
/**
|
||||||
|
* Tries to parse an process.env string to a boolean.
|
||||||
|
* Will understand undefined as the default value
|
||||||
|
* Will understand "false", "False", "fAlse", or "0" as `false`
|
||||||
|
* Will understand everything else as true
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @param {string} name
|
||||||
|
* @return {*} {boolean}
|
||||||
|
*/
|
||||||
|
function booleanFromEnv(name) {
|
||||||
|
const value = process.env[name];
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (value.toLowerCase() === 'false' || value === '0') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !!value;
|
||||||
|
}
|
||||||
|
exports.booleanFromEnv = booleanFromEnv;
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import { SignOptions } from './types';
|
||||||
|
/**
|
||||||
|
* Recursively goes through an entire directory and returns an array
|
||||||
|
* of full paths for files ot sign.
|
||||||
|
*
|
||||||
|
* - Portable executable files (.exe, .dll, .sys, .efi, .scr, .node)
|
||||||
|
* - Microsoft installers (.msi)
|
||||||
|
* - APPX/MSIX packages (.appx, .appxbundle, .msix, .msixbundle)
|
||||||
|
* - Catalog files (.cat)
|
||||||
|
* - Cabinet files (.cab)
|
||||||
|
* - Silverlight applications (.xap)
|
||||||
|
* - Scripts (.vbs, .wsf, .ps1)
|
||||||
|
* If configured:
|
||||||
|
* - JavaScript files (.js)
|
||||||
|
*/
|
||||||
|
export declare function getFilesToSign(options: SignOptions, dir?: string): Array<string>;
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
const IS_PE_REGEX = /\.(exe|dll|sys|efi|scr|node)$/i;
|
||||||
|
const IS_MSI_REGEX = /\.msi$/i;
|
||||||
|
const IS_PACKAGE_REGEX = /\.(appx|appxbundle|msix|msixbundle)$/i;
|
||||||
|
const IS_CATCAB_REGEX = /\.(cat|cab)$/i;
|
||||||
|
const IS_SILVERLIGHT_REGEX = /\.xap$/i;
|
||||||
|
const IS_SCRIPT_REGEX = /\.(vbs|wsf|ps1)$/i;
|
||||||
|
const IS_JS_REGEX = /\.js$/i;
|
||||||
|
/**
|
||||||
|
* Recursively goes through an entire directory and returns an array
|
||||||
|
* of full paths for files ot sign.
|
||||||
|
*
|
||||||
|
* - Portable executable files (.exe, .dll, .sys, .efi, .scr, .node)
|
||||||
|
* - Microsoft installers (.msi)
|
||||||
|
* - APPX/MSIX packages (.appx, .appxbundle, .msix, .msixbundle)
|
||||||
|
* - Catalog files (.cat)
|
||||||
|
* - Cabinet files (.cab)
|
||||||
|
* - Silverlight applications (.xap)
|
||||||
|
* - Scripts (.vbs, .wsf, .ps1)
|
||||||
|
* If configured:
|
||||||
|
* - JavaScript files (.js)
|
||||||
|
*/
|
||||||
|
export function getFilesToSign(options, dir) {
|
||||||
|
if (isSignOptionsForFiles(options)) {
|
||||||
|
return options.files;
|
||||||
|
}
|
||||||
|
dir = dir || options.appDirectory;
|
||||||
|
// Array of file paths to sign
|
||||||
|
const result = [];
|
||||||
|
// Iterate over the app directory, looking for files to sign
|
||||||
|
const files = fs.readdirSync(dir);
|
||||||
|
const regexes = [
|
||||||
|
IS_PE_REGEX,
|
||||||
|
IS_MSI_REGEX,
|
||||||
|
IS_PACKAGE_REGEX,
|
||||||
|
IS_CATCAB_REGEX,
|
||||||
|
IS_SILVERLIGHT_REGEX,
|
||||||
|
IS_SCRIPT_REGEX
|
||||||
|
];
|
||||||
|
if (options.signJavaScript) {
|
||||||
|
regexes.push(IS_JS_REGEX);
|
||||||
|
}
|
||||||
|
for (const file of files) {
|
||||||
|
const fullPath = path.resolve(dir, file);
|
||||||
|
if (fs.statSync(fullPath).isDirectory()) {
|
||||||
|
// If it's a directory, recurse
|
||||||
|
result.push(...getFilesToSign(options, fullPath));
|
||||||
|
}
|
||||||
|
else if (regexes.some((regex) => regex.test(file))) {
|
||||||
|
// If it's a match, add it to the list
|
||||||
|
result.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function isSignOptionsForFiles(input) {
|
||||||
|
return !!input.files;
|
||||||
|
}
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
import { sign } from './sign';
|
||||||
|
import { createSeaSignTool, SeaOptions, InternalSeaOptions } from './sea';
|
||||||
|
import { HookFunction, OptionalHookOptions, OptionalSignToolOptions, SignOptions, SignToolOptions, SignOptionsForDirectory, SignOptionsForFiles } from './types';
|
||||||
|
export { sign, SignOptions, SignToolOptions, HookFunction, OptionalSignToolOptions, OptionalHookOptions, createSeaSignTool, SeaOptions, InternalSeaOptions, SignOptionsForDirectory, SignOptionsForFiles };
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
import { sign } from './sign';
|
||||||
|
import { createSeaSignTool } from './sea';
|
||||||
|
export { sign, createSeaSignTool };
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
import { SignToolOptions } from './types';
|
||||||
|
/**
|
||||||
|
* Options for signing with a Node.js single executable application.
|
||||||
|
*
|
||||||
|
* @category Single executable applications
|
||||||
|
*/
|
||||||
|
export interface SeaOptions {
|
||||||
|
/**
|
||||||
|
* Full path to the Node.js single executable application. Needs to end with `.exe`.
|
||||||
|
*/
|
||||||
|
path: string;
|
||||||
|
/**
|
||||||
|
* A binary to use. Will use the current executable (process.execPath) by default.
|
||||||
|
*
|
||||||
|
* @defaultValue The Node.js {@link https://nodejs.org/api/process.html#processexecpath | `process.execPath`}
|
||||||
|
*/
|
||||||
|
bin?: string;
|
||||||
|
/**
|
||||||
|
* Options to pass to SignTool.
|
||||||
|
*/
|
||||||
|
windowsSign: SignToolOptions;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface represents {@link SeaOptions} with all optional properties
|
||||||
|
* inferred by `@electron/windows-sign` if not passed in by the user.
|
||||||
|
*
|
||||||
|
* @category Single executable applications
|
||||||
|
*/
|
||||||
|
export interface InternalSeaOptions extends Required<SeaOptions> {
|
||||||
|
/**
|
||||||
|
* Directory of the Node.js single executable application.
|
||||||
|
*/
|
||||||
|
dir: string;
|
||||||
|
/**
|
||||||
|
* File name of the Node.js single executable application.
|
||||||
|
*/
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* cross-dir uses new Error() stacks
|
||||||
|
* to figure out our directory in a way
|
||||||
|
* that's somewhat cross-compatible.
|
||||||
|
*
|
||||||
|
* We can't just use __dirname because it's
|
||||||
|
* undefined in ESM - and we can't use import.meta.url
|
||||||
|
* because TypeScript won't allow usage unless you're
|
||||||
|
* _only_ compiling for ESM.
|
||||||
|
*/
|
||||||
|
export declare const DIRNAME: string;
|
||||||
|
/**
|
||||||
|
* Uses Node's "Single Executable App" functionality
|
||||||
|
* to create a Node-driven signtool.exe that calls this
|
||||||
|
* module.
|
||||||
|
*
|
||||||
|
* This is useful with other tooling that _always_ calls
|
||||||
|
* a signtool.exe to sign. Some of those tools cannot be
|
||||||
|
* easily configured, but we _can_ override their signtool.exe.
|
||||||
|
*
|
||||||
|
* @category Single executable applications
|
||||||
|
*/
|
||||||
|
export declare function createSeaSignTool(options?: Partial<SeaOptions>): Promise<InternalSeaOptions>;
|
||||||
+213
@@ -0,0 +1,213 @@
|
|||||||
|
import { getDirname } from 'cross-dirname';
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import postject from 'postject';
|
||||||
|
import { spawnPromise } from './spawn';
|
||||||
|
import { log } from './utils/log';
|
||||||
|
/**
|
||||||
|
* cross-dir uses new Error() stacks
|
||||||
|
* to figure out our directory in a way
|
||||||
|
* that's somewhat cross-compatible.
|
||||||
|
*
|
||||||
|
* We can't just use __dirname because it's
|
||||||
|
* undefined in ESM - and we can't use import.meta.url
|
||||||
|
* because TypeScript won't allow usage unless you're
|
||||||
|
* _only_ compiling for ESM.
|
||||||
|
*/
|
||||||
|
export const DIRNAME = getDirname();
|
||||||
|
const FILENAMES = {
|
||||||
|
SEA_CONFIG: 'sea-config.json',
|
||||||
|
SEA_MAIN: 'sea.js',
|
||||||
|
SEA_BLOB: 'sea.blob',
|
||||||
|
SEA_RECEIVER: 'receiver.mjs'
|
||||||
|
};
|
||||||
|
const SEA_MAIN_SCRIPT = `
|
||||||
|
const bin = "%PATH_TO_BIN%";
|
||||||
|
const script = "%PATH_TO_SCRIPT%";
|
||||||
|
const options = %WINDOWS_SIGN_OPTIONS%
|
||||||
|
|
||||||
|
const { spawnSync } = require('child_process');
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
console.log("@electron/windows-sign sea");
|
||||||
|
console.log({ bin, script });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const spawn = spawnSync(
|
||||||
|
bin,
|
||||||
|
[ script, JSON.stringify(options), JSON.stringify(process.argv.slice(1)) ],
|
||||||
|
{ stdio: ['inherit', 'inherit', 'pipe'] }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (spawn.status !== 0) {
|
||||||
|
throw new Error(\`Spawn failed with code: \${spawn.status}. Stderr: \${spawn.stderr}\`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Do not rethrow the error or write it to stdout/stderr. Then the process won't terminate.
|
||||||
|
// See: https://github.com/electron/windows-sign/pull/48
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
`;
|
||||||
|
const SEA_RECEIVER_SCRIPT = `
|
||||||
|
import { sign } from '@electron/windows-sign';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const logPath = path.join('electron-windows-sign.log');
|
||||||
|
const options = JSON.parse(process.argv[2]);
|
||||||
|
const signArgv = JSON.parse(process.argv[3]);
|
||||||
|
const files = signArgv.slice(-1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.appendFileSync(logPath, \`\\nCalled with: \${JSON.stringify(process.argv, null, 2)}\`);
|
||||||
|
|
||||||
|
sign({ ...options, files })
|
||||||
|
.then((result) => {
|
||||||
|
fs.appendFileSync(logPath, \`\\nSuccessfully signed with result: \${result}\`);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
fs.appendFileSync(logPath, \`\\nError from sign: \${error}\`);
|
||||||
|
throw new Error(error);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
fs.appendFileSync(logPath, \`\\Error invoking sign: \${error}\`);
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
/**
|
||||||
|
* Uses Node's "Single Executable App" functionality
|
||||||
|
* to create a Node-driven signtool.exe that calls this
|
||||||
|
* module.
|
||||||
|
*
|
||||||
|
* This is useful with other tooling that _always_ calls
|
||||||
|
* a signtool.exe to sign. Some of those tools cannot be
|
||||||
|
* easily configured, but we _can_ override their signtool.exe.
|
||||||
|
*
|
||||||
|
* @category Single executable applications
|
||||||
|
*/
|
||||||
|
export async function createSeaSignTool(options = {}) {
|
||||||
|
checkCompatibility();
|
||||||
|
const requiredOptions = await getOptions(options);
|
||||||
|
await createFiles(requiredOptions);
|
||||||
|
await createBlob(requiredOptions);
|
||||||
|
await createBinary(requiredOptions);
|
||||||
|
await createSeaReceiver(requiredOptions);
|
||||||
|
await cleanup(requiredOptions);
|
||||||
|
return requiredOptions;
|
||||||
|
}
|
||||||
|
async function createSeaReceiver(options) {
|
||||||
|
const receiverPath = path.join(options.dir, FILENAMES.SEA_RECEIVER);
|
||||||
|
await fs.ensureFile(receiverPath);
|
||||||
|
await fs.writeFile(receiverPath, SEA_RECEIVER_SCRIPT);
|
||||||
|
}
|
||||||
|
async function createFiles(options) {
|
||||||
|
const { dir, bin } = options;
|
||||||
|
const receiverPath = path.join(options.dir, FILENAMES.SEA_RECEIVER);
|
||||||
|
// sea-config.json
|
||||||
|
await fs.outputJSON(path.join(dir, FILENAMES.SEA_CONFIG), {
|
||||||
|
main: FILENAMES.SEA_MAIN,
|
||||||
|
output: FILENAMES.SEA_BLOB,
|
||||||
|
disableExperimentalSEAWarning: true
|
||||||
|
}, {
|
||||||
|
spaces: 2
|
||||||
|
});
|
||||||
|
// signtool.js
|
||||||
|
const binPath = bin || process.execPath;
|
||||||
|
const script = SEA_MAIN_SCRIPT
|
||||||
|
.replace('%PATH_TO_BIN%', escapeMaybe(binPath))
|
||||||
|
.replace('%PATH_TO_SCRIPT%', escapeMaybe(receiverPath))
|
||||||
|
.replace('%WINDOWS_SIGN_OPTIONS%', JSON.stringify(options.windowsSign));
|
||||||
|
await fs.outputFile(path.join(dir, FILENAMES.SEA_MAIN), script);
|
||||||
|
}
|
||||||
|
async function createBlob(options) {
|
||||||
|
const args = ['--experimental-sea-config', 'sea-config.json'];
|
||||||
|
const bin = process.execPath;
|
||||||
|
const cwd = options.dir;
|
||||||
|
log(`Calling ${bin} with options:`, args);
|
||||||
|
const { stderr, stdout } = await spawnPromise(bin, args, {
|
||||||
|
cwd
|
||||||
|
});
|
||||||
|
log('stdout:', stdout);
|
||||||
|
log('stderr:', stderr);
|
||||||
|
}
|
||||||
|
async function createBinary(options) {
|
||||||
|
const { dir, filename } = options;
|
||||||
|
log(`Creating ${filename} in ${dir}`);
|
||||||
|
// Copy Node over
|
||||||
|
const seaPath = path.join(dir, filename);
|
||||||
|
await fs.copyFile(process.execPath, seaPath);
|
||||||
|
// Remove the Node signature
|
||||||
|
const signtool = path.join(DIRNAME, '../../vendor/signtool.exe');
|
||||||
|
await spawnPromise(signtool, [
|
||||||
|
'remove',
|
||||||
|
'/s',
|
||||||
|
seaPath
|
||||||
|
]);
|
||||||
|
// Inject the blob
|
||||||
|
const blob = await fs.readFile(path.join(dir, FILENAMES.SEA_BLOB));
|
||||||
|
await postject.inject(seaPath, 'NODE_SEA_BLOB', blob, {
|
||||||
|
sentinelFuse: 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function cleanup(options) {
|
||||||
|
const { dir } = options;
|
||||||
|
const toRemove = [
|
||||||
|
FILENAMES.SEA_BLOB,
|
||||||
|
FILENAMES.SEA_MAIN,
|
||||||
|
FILENAMES.SEA_CONFIG
|
||||||
|
];
|
||||||
|
for (const file of toRemove) {
|
||||||
|
try {
|
||||||
|
await fs.remove(path.join(dir, file));
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.warn(`Tried and failed to remove ${file}. Continuing.`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function getOptions(options) {
|
||||||
|
const cloned = { ...options };
|
||||||
|
if (!cloned.path) {
|
||||||
|
cloned.path = path.join(os.homedir(), '.electron', 'windows-sign', 'sea.exe');
|
||||||
|
await fs.ensureFile(cloned.path);
|
||||||
|
}
|
||||||
|
if (!cloned.bin) {
|
||||||
|
cloned.bin = process.execPath;
|
||||||
|
}
|
||||||
|
if (!cloned.windowsSign) {
|
||||||
|
throw new Error('Did not find windowsSign options, which are required');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
path: cloned.path,
|
||||||
|
dir: path.dirname(cloned.path),
|
||||||
|
filename: path.basename(cloned.path),
|
||||||
|
bin: cloned.bin,
|
||||||
|
windowsSign: cloned.windowsSign
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Ensures that the current Node.js version supports SEA app generation and errors if not.
|
||||||
|
*/
|
||||||
|
function checkCompatibility() {
|
||||||
|
const version = process.versions.node;
|
||||||
|
const split = version.split('.');
|
||||||
|
const major = parseInt(split[0], 10);
|
||||||
|
if (major >= 20) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw new Error(`Your Node.js version (${process.version}) does not support Single Executable Applications. Please upgrade your version of Node.js.`);
|
||||||
|
}
|
||||||
|
/** Make sure that the input string has escaped backwards slashes
|
||||||
|
* - but never double-escaped backwards slashes.
|
||||||
|
*/
|
||||||
|
function escapeMaybe(input) {
|
||||||
|
const result = input.split(path.sep).join('\\\\');
|
||||||
|
if (result.includes('\\\\\\\\')) {
|
||||||
|
throw new Error(`Your passed input ${input} contains escaped slashes. Please do not escape them`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
import { InternalSignOptions } from './types';
|
||||||
|
/**
|
||||||
|
* Sign with a hook function, basically letting everyone
|
||||||
|
* write completely custom sign logic
|
||||||
|
*
|
||||||
|
* @param {InternalSignOptions} options
|
||||||
|
*/
|
||||||
|
export declare function signWithHook(options: InternalSignOptions): Promise<void>;
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { log } from './utils/log';
|
||||||
|
let hookFunction;
|
||||||
|
function getHookFunction(options) {
|
||||||
|
if (options.hookFunction) {
|
||||||
|
return options.hookFunction;
|
||||||
|
}
|
||||||
|
if (options.hookModulePath) {
|
||||||
|
const module = require(path.resolve(options.hookModulePath));
|
||||||
|
if (module.default) {
|
||||||
|
return module.default;
|
||||||
|
}
|
||||||
|
if (typeof module === 'function') {
|
||||||
|
return module;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hookFunction) {
|
||||||
|
throw new Error('No hook function found. Signing will not be possible. Please see the documentation for how to pass a hook function to @electron/windows-sign');
|
||||||
|
}
|
||||||
|
return hookFunction;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sign with a hook function, basically letting everyone
|
||||||
|
* write completely custom sign logic
|
||||||
|
*
|
||||||
|
* @param {InternalSignOptions} options
|
||||||
|
*/
|
||||||
|
export async function signWithHook(options) {
|
||||||
|
hookFunction = getHookFunction(options);
|
||||||
|
for (const file of options.files) {
|
||||||
|
try {
|
||||||
|
await hookFunction(file);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
log(`Error signing ${file}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
import { InternalSignOptions } from './types';
|
||||||
|
export declare function signWithSignTool(options: InternalSignOptions): Promise<void>;
|
||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { log } from './utils/log';
|
||||||
|
import { spawnPromise } from './spawn';
|
||||||
|
import { getDirname } from 'cross-dirname';
|
||||||
|
const DIRNAME = getDirname();
|
||||||
|
function getSigntoolArgs(options) {
|
||||||
|
// See the following url for docs
|
||||||
|
// https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe
|
||||||
|
const { certificateFile, certificatePassword, hash, timestampServer } = options;
|
||||||
|
const args = ['sign'];
|
||||||
|
// Automatically select cert
|
||||||
|
if (options.automaticallySelectCertificate) {
|
||||||
|
args.push('/a');
|
||||||
|
}
|
||||||
|
// Dual-sign
|
||||||
|
if (options.appendSignature) {
|
||||||
|
args.push('/as');
|
||||||
|
}
|
||||||
|
// Timestamp
|
||||||
|
if (hash === "sha256" /* HASHES.sha256 */) {
|
||||||
|
args.push('/tr', timestampServer);
|
||||||
|
args.push('/td', hash);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
args.push('/t', timestampServer);
|
||||||
|
}
|
||||||
|
// Certificate file
|
||||||
|
if (certificateFile) {
|
||||||
|
args.push('/f', path.resolve(certificateFile));
|
||||||
|
}
|
||||||
|
// Certificate password
|
||||||
|
if (certificatePassword) {
|
||||||
|
args.push('/p', certificatePassword);
|
||||||
|
}
|
||||||
|
// Hash
|
||||||
|
args.push('/fd', hash);
|
||||||
|
// Description
|
||||||
|
if (options.description) {
|
||||||
|
args.push('/d', options.description);
|
||||||
|
}
|
||||||
|
// Website
|
||||||
|
if (options.website) {
|
||||||
|
args.push('/du', options.website);
|
||||||
|
}
|
||||||
|
// Debug
|
||||||
|
if (options.debug) {
|
||||||
|
args.push('/debug');
|
||||||
|
}
|
||||||
|
if (options.signWithParams) {
|
||||||
|
const extraArgs = [];
|
||||||
|
if (Array.isArray(options.signWithParams)) {
|
||||||
|
extraArgs.push(...options.signWithParams);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Split up at spaces and doublequotes
|
||||||
|
extraArgs.push(...options.signWithParams.match(/(?:[^\s"]+|"[^"]*")+/g));
|
||||||
|
}
|
||||||
|
log('Parsed signWithParams as:', extraArgs);
|
||||||
|
args.push(...extraArgs);
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
async function execute(options) {
|
||||||
|
const { signToolPath, files } = options;
|
||||||
|
const args = getSigntoolArgs(options);
|
||||||
|
log('Executing signtool with args', { args, files });
|
||||||
|
const { code, stderr, stdout } = await spawnPromise(signToolPath, [...args, ...files], {
|
||||||
|
env: process.env,
|
||||||
|
cwd: process.cwd()
|
||||||
|
});
|
||||||
|
if (code !== 0) {
|
||||||
|
throw new Error(`Signtool exited with code ${code}. Stderr: ${stderr}. Stdout: ${stdout}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function signWithSignTool(options) {
|
||||||
|
const certificatePassword = options.certificatePassword || process.env.WINDOWS_CERTIFICATE_PASSWORD;
|
||||||
|
const certificateFile = options.certificateFile || process.env.WINDOWS_CERTIFICATE_FILE;
|
||||||
|
const signWithParams = options.signWithParams || process.env.WINDOWS_SIGN_WITH_PARAMS;
|
||||||
|
const timestampServer = options.timestampServer || process.env.WINDOWS_TIMESTAMP_SERVER || 'http://timestamp.digicert.com';
|
||||||
|
const signToolPath = options.signToolPath || process.env.WINDOWS_SIGNTOOL_PATH || path.join(DIRNAME, '../../vendor/signtool.exe');
|
||||||
|
const description = options.description || process.env.WINDOWS_SIGN_DESCRIPTION;
|
||||||
|
const website = options.website || process.env.WINDOWS_SIGN_WEBSITE;
|
||||||
|
if (!certificateFile && !(signWithParams || signToolPath)) {
|
||||||
|
throw new Error('You must provide a certificateFile and a signToolPath or signing parameters');
|
||||||
|
}
|
||||||
|
if (!signToolPath && !signWithParams && !certificatePassword) {
|
||||||
|
throw new Error('You must provide a certificatePassword or signing parameters');
|
||||||
|
}
|
||||||
|
const internalOptions = {
|
||||||
|
appendSignature: false,
|
||||||
|
...options,
|
||||||
|
certificateFile,
|
||||||
|
certificatePassword,
|
||||||
|
signWithParams,
|
||||||
|
signToolPath,
|
||||||
|
description,
|
||||||
|
timestampServer,
|
||||||
|
website
|
||||||
|
};
|
||||||
|
const hashes = options.hashes == null || options.hashes.length === 0
|
||||||
|
? ["sha1" /* HASHES.sha1 */, "sha256" /* HASHES.sha256 */]
|
||||||
|
: options.hashes;
|
||||||
|
if (hashes.includes("sha1" /* HASHES.sha1 */)) {
|
||||||
|
await execute({ ...internalOptions, hash: "sha1" /* HASHES.sha1 */ });
|
||||||
|
// If we signed with SHA1, we need to append the SHA256 signature:
|
||||||
|
internalOptions.appendSignature = true;
|
||||||
|
}
|
||||||
|
if (hashes.includes("sha256" /* HASHES.sha256 */)) {
|
||||||
|
await execute({ ...internalOptions, hash: "sha256" /* HASHES.sha256 */ });
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
import { SignOptions } from './types';
|
||||||
|
/**
|
||||||
|
* This is the main function exported from this module. It'll
|
||||||
|
* look at your options, determine the best way to sign a file,
|
||||||
|
* and then return one of our internal functions to do the actual
|
||||||
|
* signing.
|
||||||
|
*
|
||||||
|
* @param options
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*
|
||||||
|
* @category Sign
|
||||||
|
*/
|
||||||
|
export declare function sign(options: SignOptions): Promise<void>;
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
import { getFilesToSign } from './files';
|
||||||
|
import { signWithHook } from './sign-with-hook';
|
||||||
|
import { signWithSignTool } from './sign-with-signtool';
|
||||||
|
import { enableDebugging, log } from './utils/log';
|
||||||
|
import { booleanFromEnv } from './utils/parse-env';
|
||||||
|
/**
|
||||||
|
* This is the main function exported from this module. It'll
|
||||||
|
* look at your options, determine the best way to sign a file,
|
||||||
|
* and then return one of our internal functions to do the actual
|
||||||
|
* signing.
|
||||||
|
*
|
||||||
|
* @param options
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*
|
||||||
|
* @category Sign
|
||||||
|
*/
|
||||||
|
export async function sign(options) {
|
||||||
|
const signJavaScript = options.signJavaScript || booleanFromEnv('WINDOWS_SIGN_JAVASCRIPT');
|
||||||
|
const hookModulePath = options.hookModulePath || process.env.WINDOWS_SIGN_HOOK_MODULE_PATH;
|
||||||
|
if (options.debug) {
|
||||||
|
enableDebugging();
|
||||||
|
}
|
||||||
|
log('Called with options', { options });
|
||||||
|
const files = getFilesToSign(options);
|
||||||
|
const internalOptions = {
|
||||||
|
...options,
|
||||||
|
signJavaScript,
|
||||||
|
hookModulePath,
|
||||||
|
files
|
||||||
|
};
|
||||||
|
// If a hook is provides, sign with the hook
|
||||||
|
if (internalOptions.hookFunction || internalOptions.hookModulePath) {
|
||||||
|
log('Signing with hook');
|
||||||
|
return signWithHook(internalOptions);
|
||||||
|
}
|
||||||
|
// If we're going with the defaults, we're signing
|
||||||
|
// with signtool. Custom signing tools are also
|
||||||
|
// handled here.
|
||||||
|
log('Signing with signtool');
|
||||||
|
return signWithSignTool(internalOptions);
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
/// <reference types="node" />
|
||||||
|
import { SpawnOptions } from 'child_process';
|
||||||
|
export interface SpawnPromiseResult {
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
code: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Spawn a process as a promise
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @param {Array<string>} args
|
||||||
|
* @param {SpawnOptions} [options]
|
||||||
|
* @returns {Promise<SpawnPromiseResult>}
|
||||||
|
*/
|
||||||
|
export declare function spawnPromise(name: string, args: Array<string>, options?: SpawnOptions): Promise<SpawnPromiseResult>;
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
import { log } from './utils/log';
|
||||||
|
/**
|
||||||
|
* Spawn a process as a promise
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @param {Array<string>} args
|
||||||
|
* @param {SpawnOptions} [options]
|
||||||
|
* @returns {Promise<SpawnPromiseResult>}
|
||||||
|
*/
|
||||||
|
export function spawnPromise(name, args, options) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
const fork = spawn(name, args, options);
|
||||||
|
log(`Spawning ${name} with ${args}`);
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
fork.stdout.on('data', (data) => {
|
||||||
|
log(`Spawn ${name} stdout: ${data}`);
|
||||||
|
stdout += data;
|
||||||
|
});
|
||||||
|
fork.stderr.on('data', (data) => {
|
||||||
|
log(`Spawn ${name} stderr: ${data}`);
|
||||||
|
stderr += data;
|
||||||
|
});
|
||||||
|
fork.on('close', (code) => {
|
||||||
|
log(`Spawn ${name}: Child process exited with code ${code}`);
|
||||||
|
resolve({ stdout, stderr, code });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
/**
|
||||||
|
* SHA-1 has been deprecated on Windows since 2016. We'll still dualsign.
|
||||||
|
* https://social.technet.microsoft.com/wiki/contents/articles/32288.windows-enforcement-of-sha1-certificates.aspx#Post-February_TwentySeventeen_Plan
|
||||||
|
*/
|
||||||
|
export declare const enum HASHES {
|
||||||
|
sha1 = "sha1",
|
||||||
|
sha256 = "sha256"
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Signing can be either by specifying a directory of files to sign.
|
||||||
|
*
|
||||||
|
* @category Sign
|
||||||
|
*/
|
||||||
|
export type SignOptions = SignOptionsForDirectory | SignOptionsForFiles;
|
||||||
|
/**
|
||||||
|
* Options for signing by passing a path to a directory to be codesigned.
|
||||||
|
*
|
||||||
|
* @category Sign
|
||||||
|
*/
|
||||||
|
export interface SignOptionsForDirectory extends SignToolOptions {
|
||||||
|
/**
|
||||||
|
* Path to the application directory. We will scan this
|
||||||
|
* directory for any `.dll`, `.exe`, `.msi`, or `.node` files and
|
||||||
|
* codesign them with `signtool.exe`.
|
||||||
|
*/
|
||||||
|
appDirectory: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Options for signing by passing an array of files to be codesigned.
|
||||||
|
*
|
||||||
|
* @category Sign
|
||||||
|
*/
|
||||||
|
export interface SignOptionsForFiles extends SignToolOptions {
|
||||||
|
/**
|
||||||
|
* Array of paths to files to be codesigned with `signtool.exe`.
|
||||||
|
*/
|
||||||
|
files: Array<string>;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @category Utility
|
||||||
|
*/
|
||||||
|
export interface SignToolOptions extends OptionalSignToolOptions, OptionalHookOptions {
|
||||||
|
}
|
||||||
|
export interface InternalSignOptions extends SignOptionsForFiles {
|
||||||
|
}
|
||||||
|
export interface InternalSignToolOptions extends OptionalSignToolOptions, OptionalHookOptions {
|
||||||
|
signToolPath: string;
|
||||||
|
timestampServer: string;
|
||||||
|
files: Array<string>;
|
||||||
|
hash: HASHES;
|
||||||
|
appendSignature?: boolean;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @category Utility
|
||||||
|
*/
|
||||||
|
export interface OptionalSignToolOptions {
|
||||||
|
/**
|
||||||
|
* Path to a `.pfx` code signing certificate.
|
||||||
|
* Will use `process.env.WINDOWS_CERTIFICATE_FILE` if this option is not provided.
|
||||||
|
*/
|
||||||
|
certificateFile?: string;
|
||||||
|
/**
|
||||||
|
* Password to {@link certificateFile}. If you don't provide this,
|
||||||
|
* you need to provide the {@link signWithParams} option.
|
||||||
|
* Will use `process.env.WINDOWS_CERTIFICATE_PASSWORD` if this option is not provided.
|
||||||
|
*/
|
||||||
|
certificatePassword?: string;
|
||||||
|
/**
|
||||||
|
* Path to a timestamp server.
|
||||||
|
* Will use `process.env.WINDOWS_TIMESTAMP_SERVER` if this option is not provided.
|
||||||
|
*
|
||||||
|
* @defaultValue http://timestamp.digicert.com
|
||||||
|
*/
|
||||||
|
timestampServer?: string;
|
||||||
|
/**
|
||||||
|
* Description of the signed content. Will be passed to `signtool.exe` as `/d`.
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
/**
|
||||||
|
* URL for the expanded description of the signed content. Will be passed to `signtool.exe` as `/du`.
|
||||||
|
*/
|
||||||
|
website?: string;
|
||||||
|
/**
|
||||||
|
* Path to the `signtool.exe` used to sign. Will use `vendor/signtool.exe` if not provided.
|
||||||
|
*/
|
||||||
|
signToolPath?: string;
|
||||||
|
/**
|
||||||
|
* Additional parameters to pass to `signtool.exe`.
|
||||||
|
*
|
||||||
|
* @see Microsoft's {@link https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe SignTool.exe documentation}
|
||||||
|
*/
|
||||||
|
signWithParams?: string | Array<string>;
|
||||||
|
/**
|
||||||
|
* Enables debug logging.
|
||||||
|
*
|
||||||
|
* @defaultValue false
|
||||||
|
*/
|
||||||
|
debug?: boolean;
|
||||||
|
/**
|
||||||
|
* Automatically selects the best signing certificate according to SignTool. Will be passed to `signtool.exe` as `/a`.
|
||||||
|
*
|
||||||
|
* @defaultValue true
|
||||||
|
*/
|
||||||
|
automaticallySelectCertificate?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether or not to sign JavaScript files.
|
||||||
|
*
|
||||||
|
* @defaultValue false
|
||||||
|
*/
|
||||||
|
signJavaScript?: boolean;
|
||||||
|
/**
|
||||||
|
* Hash algorithms to use for signing.
|
||||||
|
*/
|
||||||
|
hashes?: HASHES[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Custom function that is called sequentially for each file that needs to be signed.
|
||||||
|
*
|
||||||
|
* @param fileToSign Absolute path to the file to sign
|
||||||
|
*
|
||||||
|
* @category Utility
|
||||||
|
*/
|
||||||
|
export type HookFunction = (fileToSign: string) => void | Promise<void>;
|
||||||
|
/**
|
||||||
|
* @category Utility
|
||||||
|
*/
|
||||||
|
export interface OptionalHookOptions {
|
||||||
|
/**
|
||||||
|
* A hook function called for each file that needs to be signed.
|
||||||
|
* Use this for full control over your app's signing logic.
|
||||||
|
* `@electron/windows-sign` will not attempt to sign with SignTool if a custom hook is detected.
|
||||||
|
*/
|
||||||
|
hookFunction?: HookFunction;
|
||||||
|
/**
|
||||||
|
* A path to a JavaScript file, exporting a single function that will be called for each file that needs to be signed.
|
||||||
|
* Use this for full control over your app's signing logic.
|
||||||
|
* `@electron/windows-sign` will not attempt to sign with SignTool if a custom hook is detected.
|
||||||
|
*/
|
||||||
|
hookModulePath?: string;
|
||||||
|
}
|
||||||
|
export interface InternalHookOptions extends OptionalHookOptions {
|
||||||
|
files: Array<string>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user