@lakutata/builder 3.1.3 → 3.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/README.zh-CN.md +5 -1
- package/dist/esm/index.js +3 -0
- package/dist/esm/lib/Archive.js +3 -2
- package/dist/esm/lib/Bundle.js +2 -1
- package/dist/esm/lib/Directories.js +18 -0
- package/dist/esm/lib/Executable.js +27 -6
- package/dist/esm/lib/Installer.js +26 -12
- package/dist/esm/lib/NativeRuntime.js +2 -0
- package/dist/esm/lib/linux/Linux.js +57 -8
- package/dist/esm/lib/mac/Mac.js +39 -11
- package/dist/esm/lib/nwjs/Addons.js +307 -0
- package/dist/esm/lib/nwjs/Package.js +149 -0
- package/dist/esm/lib/nwjs/Runtime.js +214 -0
- package/dist/esm/lib/nwjs/Stage.js +92 -0
- package/dist/esm/lib/win/Windows.js +63 -41
- package/dist/types/index.d.ts +4 -0
- package/dist/types/lib/Bundle.d.ts +5 -0
- package/dist/types/lib/Directories.d.ts +8 -0
- package/dist/types/lib/Executable.d.ts +7 -0
- package/dist/types/lib/Installer.d.ts +33 -4
- package/dist/types/lib/mac/Mac.d.ts +1 -1
- package/dist/types/lib/nwjs/Addons.d.ts +51 -0
- package/dist/types/lib/nwjs/Package.d.ts +52 -0
- package/dist/types/lib/nwjs/Runtime.d.ts +116 -0
- package/dist/types/lib/nwjs/Stage.d.ts +104 -0
- package/dist/types/lib/win/Windows.d.ts +28 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ The builder for lakutata applications. The `package bundle`, `package exe` and `
|
|
|
6
6
|
`@lakutata/cli` use it, and a project's scripts can call it directly:
|
|
7
7
|
|
|
8
8
|
```js
|
|
9
|
-
import {buildExecutable, buildInstaller, bundleProject} from '@lakutata/builder'
|
|
9
|
+
import {buildExecutable, buildInstaller, buildNwjsApp, bundleProject} from '@lakutata/builder'
|
|
10
10
|
|
|
11
11
|
//dist/<name>.cjs, run by node, with its native addons and its resources next to it
|
|
12
12
|
await bundleProject({project: process.cwd()})
|
|
@@ -16,6 +16,8 @@ await buildExecutable({project: process.cwd(), runtime: 'node', layout: 'directo
|
|
|
16
16
|
//dist/installer: the installers for the current platform (deb, rpm, apk, archlinux, tar.gz on Linux; pkg, tar.gz on
|
|
17
17
|
//macOS; nsis, msi, zip on Windows), which also register the application's service when it declares one
|
|
18
18
|
await buildInstaller({project: process.cwd()})
|
|
19
|
+
//An NW.js application (its version in lakutata.nwjs): dist/nwjs/<name>-<build>, the runtime of NW.js named after it
|
|
20
|
+
await buildNwjsApp({project: process.cwd()})
|
|
19
21
|
```
|
|
20
22
|
|
|
21
23
|
The builder works on the compiled application: build the project with `tsc` first, since `tsc` emits the decorator
|
|
@@ -29,6 +31,14 @@ result. At run time, `resourcesPath()` from lakutata locates the application's r
|
|
|
29
31
|
`lakutata package bundle`, `lakutata package exe` and `lakutata package installer` in the `@lakutata/cli`
|
|
30
32
|
documentation.
|
|
31
33
|
|
|
34
|
+
The NW.js applications run in the version of NW.js of their `lakutata.nwjs`. `nwjsReleases()` lists the releases of
|
|
35
|
+
NW.js, `downloadNwjs()` downloads a runtime into the cache (checked against the checksums of its release), and
|
|
36
|
+
`stageNwjsApp()` makes the application for NW.js: its bundle for the Node.js of that NW.js, its native addons made to
|
|
37
|
+
load in it (built again for its architecture, or with its headers when they use the V8 API; on Windows, given the
|
|
38
|
+
Node.js of NW.js), its resources and the manifest of NW.js. `buildNwjsApp()` puts it into the normal flavor of NW.js
|
|
39
|
+
named after the application, and `buildInstaller()` makes its installers with `"runtime": "nwjs"` (the installers of a
|
|
40
|
+
desktop application: dmg on macOS, nsis on Windows, deb on Linux by default).
|
|
41
|
+
|
|
32
42
|
The Linux installers are tested in containers (Docker): `npm run test:installer` builds them on Debian and Alpine, then
|
|
33
43
|
installs, upgrades and removes them on Debian and Fedora (systemd) and on Alpine (OpenRC). The macOS installers are
|
|
34
44
|
installed in a macOS virtual machine (`npm run test:installer:mac`, with tart), and the Windows installers on Windows,
|
package/README.zh-CN.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
lakutata 应用的打包工具。`@lakutata/cli` 的 `package bundle`、`package exe` 和 `package installer` 命令都靠它完成工作,项目的脚本也可以直接调用它:
|
|
6
6
|
|
|
7
7
|
```js
|
|
8
|
-
import {buildExecutable, buildInstaller, bundleProject} from '@lakutata/builder'
|
|
8
|
+
import {buildExecutable, buildInstaller, buildNwjsApp, bundleProject} from '@lakutata/builder'
|
|
9
9
|
|
|
10
10
|
//dist/<name>.cjs,由 node 运行,原生插件和资源放在它旁边
|
|
11
11
|
await bundleProject({project: process.cwd()})
|
|
@@ -15,10 +15,14 @@ await buildExecutable({project: process.cwd(), runtime: 'node', layout: 'directo
|
|
|
15
15
|
//dist/installer:当前平台的安装包(Linux 上是 deb、rpm、apk、archlinux、tar.gz;macOS 上是 pkg、tar.gz;
|
|
16
16
|
//Windows 上是 nsis、msi、zip),应用声明了服务时,安装包还会注册这个服务
|
|
17
17
|
await buildInstaller({project: process.cwd()})
|
|
18
|
+
//NW.js 应用(版本在 lakutata.nwjs 中):dist/nwjs/<name>-<build>,以应用命名的 NW.js 运行时
|
|
19
|
+
await buildNwjsApp({project: process.cwd()})
|
|
18
20
|
```
|
|
19
21
|
|
|
20
22
|
打包工具处理的是编译后的应用:请先用 `tsc` 构建项目,因为装饰器的元数据是由 `tsc` 生成的。单文件包是一个 CommonJS 文件,由 esbuild 生成。原生插件不能打进单文件包,所以它们连同运行时加载的内容会一起收集到一个 native 目录树中。打包器跟踪不到应用按路径读取的文件,这些文件也放在这个目录树中:单独运行的脚本(worker、worker 池、`fork()`、`require(path.join(__dirname, 'x.js'))`)会各自打包,数据文件和目录则直接复制。打包工具会找出由模块位置构造的路径,并改写它们;无法一起发布的内容会列在结果的 `warnings` 中。运行时,lakutata 的 `resourcesPath()` 负责找到应用的资源,这些资源在应用 `package.json` 的 `lakutata.resources` 中声明。安装包则由同一个文件中的 `lakutata.installer` 配置。详见 `@lakutata/cli` 文档中的 `lakutata package bundle`、`lakutata package exe` 和 `lakutata package installer`。
|
|
21
23
|
|
|
24
|
+
NW.js 应用运行在它 `lakutata.nwjs` 中指定的 NW.js 版本里。`nwjsReleases()` 列出 NW.js 的发布版本,`downloadNwjs()` 把一个运行时下载到缓存目录(按其发布版本的校验和核对),`stageNwjsApp()` 为 NW.js 准备应用:为这个 NW.js 的 Node.js 打包的代码、处理成能在其中加载的原生插件(为它的架构重新构建,使用 V8 API 的插件用它的头文件重新构建;在 Windows 上让插件加载 NW.js 的 Node.js)、资源和 NW.js 的清单。`buildNwjsApp()` 把应用放进以应用命名的 NW.js normal 版本中,`buildInstaller()` 在 `"runtime": "nwjs"` 时制作它的安装包(桌面应用的安装包:默认 macOS 上是 dmg,Windows 上是 nsis,Linux 上是 deb)。
|
|
25
|
+
|
|
22
26
|
Linux 安装包在容器(Docker)中测试:`npm run test:installer` 先在 Debian 和 Alpine 上制作安装包,再在 Debian、Fedora(systemd)和 Alpine(OpenRC)上安装、升级和卸载它们。macOS 安装包在 macOS 虚拟机中测试安装(`npm run test:installer:mac`,使用 tart),Windows 安装包则在 Windows 上以管理员身份测试安装(`LAKUTATA_WINDOWS_INSTALL=1 npm run test:installer:windows`)。
|
|
23
27
|
|
|
24
28
|
## 另见
|
package/dist/esm/index.js
CHANGED
|
@@ -11,3 +11,6 @@ export { INSTALLER_HOOKS, NATIVE_HOOKS } from './lib/Scripts.js';
|
|
|
11
11
|
export { detectLibc } from './lib/Platform.js';
|
|
12
12
|
export { WINDOWS_TARGETS } from './lib/win/Windows.js';
|
|
13
13
|
export { MAC_TARGETS } from './lib/mac/Mac.js';
|
|
14
|
+
export { downloadNwjs, nwjsPlatform, nwjsRelease, nwjsReleases } from './lib/nwjs/Runtime.js';
|
|
15
|
+
export { stageNwjsApp } from './lib/nwjs/Stage.js';
|
|
16
|
+
export { buildNwjsApp } from './lib/nwjs/Package.js';
|
package/dist/esm/lib/Archive.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { rm, stat } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { copyDirectory } from './Directories.js';
|
|
3
4
|
import { runCommand } from './Processes.js';
|
|
4
5
|
/**
|
|
5
6
|
* The tar.gz archive of the directory of the executable (its native addons and its resources next to it), named after
|
|
@@ -12,7 +13,7 @@ import { runCommand } from './Processes.js';
|
|
|
12
13
|
export async function archiveDirectory(plan, arch, work, outdir) {
|
|
13
14
|
const base = `${plan.name}-${plan.version}-${process.platform}-${arch}`;
|
|
14
15
|
await rm(path.join(work, base), { recursive: true, force: true });
|
|
15
|
-
await
|
|
16
|
+
await copyDirectory(plan.directory, path.join(work, base));
|
|
16
17
|
const file = path.join(outdir, `${base}.tar.gz`);
|
|
17
18
|
const archived = await runCommand('tar', ['-czf', file, base], work);
|
|
18
19
|
if (archived.code !== 0)
|
package/dist/esm/lib/Bundle.js
CHANGED
|
@@ -25,6 +25,7 @@ const IMPORT_META_URL = '__lakutataImportMetaUrl';
|
|
|
25
25
|
* arguments of the CommonJS modules: its file is the executable (as in the executables of Node.js), its require loads
|
|
26
26
|
* the modules of node and the ones of a node_modules next to the executable (the ones left out of the bundle)
|
|
27
27
|
*/
|
|
28
|
+
//node-compat: process.getBuiltinModule (Bun only)
|
|
28
29
|
const BUN_PRELUDE = 'var __filename=process.execPath,__dirname=process.getBuiltinModule(\'node:path\').dirname(__filename),require=typeof require===\'function\'?require:process.getBuiltinModule(\'node:module\').createRequire(__filename);';
|
|
29
30
|
/**
|
|
30
31
|
* The files of the resources of an application, declared by the "lakutata.resources" of its package.json: their paths in
|
|
@@ -111,7 +112,7 @@ export async function bundleProject(options) {
|
|
|
111
112
|
metafile: true,
|
|
112
113
|
bundle: true,
|
|
113
114
|
platform: 'node',
|
|
114
|
-
target: `node${process.versions.node.split('.')[0]}`,
|
|
115
|
+
target: `node${options.node?.replace(/^v/, '') ?? process.versions.node.split('.')[0]}`,
|
|
115
116
|
format: 'cjs',
|
|
116
117
|
minify: options.minify ?? true,
|
|
117
118
|
//The names of the classes are the ones of the objects of the container and of the errors
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { cp, mkdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { runCommand } from './Processes.js';
|
|
4
|
+
/**
|
|
5
|
+
* Copy a directory into another one, its links kept as they are: cp -a on macOS and Linux (the copy of Node.js turns
|
|
6
|
+
* the relative links into absolute ones before Node.js 18.17 and 20.2: its option verbatimSymlinks), the copy of Node.js
|
|
7
|
+
* on Windows (its directories have no links)
|
|
8
|
+
* @param source
|
|
9
|
+
* @param target
|
|
10
|
+
*/
|
|
11
|
+
export async function copyDirectory(source, target) {
|
|
12
|
+
if (process.platform === 'win32')
|
|
13
|
+
return await cp(source, target, { recursive: true });
|
|
14
|
+
await mkdir(target, { recursive: true });
|
|
15
|
+
const copied = await runCommand('cp', ['-a', `${source}/.`, target], path.dirname(target));
|
|
16
|
+
if (copied.code !== 0)
|
|
17
|
+
throw new Error(`${source} cannot be copied: ${copied.output.trim()}`);
|
|
18
|
+
}
|
|
@@ -76,14 +76,31 @@ async function writeIcon(work, icon) {
|
|
|
76
76
|
return file;
|
|
77
77
|
}
|
|
78
78
|
/**
|
|
79
|
-
*
|
|
79
|
+
* The node program embedded in an executable and its version: the one given, else the one running the tool. It must
|
|
80
|
+
* have the assets of the single executable applications (Node.js 20.12, 21.7 or later): node:sea and its getAsset.
|
|
81
|
+
* @param program
|
|
82
|
+
*/
|
|
83
|
+
async function nodeOfExecutable(program) {
|
|
84
|
+
const node = program ? path.resolve(program) : process.execPath;
|
|
85
|
+
//node-compat: node:sea (the probe, run by the node program, loads it in a try)
|
|
86
|
+
const probe = await runCommand(node, ['-e', "let assets=false;try{assets=typeof require('node:sea').getAsset==='function'}catch{}process.stdout.write(process.version+' '+assets)"], process.cwd()).catch((error) => ({ code: 1, output: error.message }));
|
|
87
|
+
const [version, assets] = probe.output.trim().split(' ');
|
|
88
|
+
if (probe.code !== 0 || !/^v\d+\.\d+\.\d+$/.test(version))
|
|
89
|
+
throw new Error(`${node} cannot be run as the node program of the executable: ${probe.output.trim()}`);
|
|
90
|
+
if (assets !== 'true')
|
|
91
|
+
throw new Error(`The executables of Node.js need the assets of its single executable applications (Node.js 20.12, 21.7 or later): ${node} is Node.js ${version}. Give a newer node program (nodeProgram, --node-program). Or make the executable with Bun (--runtime bun)`);
|
|
92
|
+
return { path: node, version: version };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Embed a bundle in a Node.js binary (the single executable applications of Node.js)
|
|
80
96
|
* @param bundle
|
|
81
97
|
* @param work
|
|
82
98
|
* @param file
|
|
83
99
|
* @param warnings
|
|
100
|
+
* @param node the node program embedded
|
|
84
101
|
* @param icon the icon of the application (ico, Windows)
|
|
85
102
|
*/
|
|
86
|
-
async function embedInNode(bundle, work, file, warnings, icon) {
|
|
103
|
+
async function embedInNode(bundle, work, file, warnings, node, icon) {
|
|
87
104
|
const assets = {};
|
|
88
105
|
if (bundle.native) {
|
|
89
106
|
//The files of the native packages, extracted by the executable at its first run
|
|
@@ -104,9 +121,9 @@ async function embedInNode(bundle, work, file, warnings, icon) {
|
|
|
104
121
|
useSnapshot: false,
|
|
105
122
|
assets: assets
|
|
106
123
|
}));
|
|
107
|
-
await must(
|
|
124
|
+
await must(node.path, ['--experimental-sea-config', 'sea-config.json'], work);
|
|
108
125
|
const { inject } = await import('postject');
|
|
109
|
-
await copyFile(
|
|
126
|
+
await copyFile(node.path, file);
|
|
110
127
|
//The icon of the application replaces the one of node.exe, before the bundle is embedded
|
|
111
128
|
if (icon)
|
|
112
129
|
await setExecutableIcon(file, icon);
|
|
@@ -119,7 +136,7 @@ async function embedInNode(bundle, work, file, warnings, icon) {
|
|
|
119
136
|
//The Authenticode signature of node.exe does not match the executable anymore
|
|
120
137
|
if (process.platform === 'win32')
|
|
121
138
|
warnings.push(`${path.basename(file)} has the signature of Node.js, which is not valid anymore: sign it (signtool) before distributing it`);
|
|
122
|
-
return `Node.js ${
|
|
139
|
+
return `Node.js ${node.version}`;
|
|
123
140
|
}
|
|
124
141
|
/**
|
|
125
142
|
* Compile a bundle into an executable of Bun, in bytecode: the files of the native tree are embedded beside it, the
|
|
@@ -189,6 +206,8 @@ export async function buildExecutable(options) {
|
|
|
189
206
|
const outdir = path.resolve(project, options.outdir ?? 'dist');
|
|
190
207
|
//Bun is found before the application is bundled
|
|
191
208
|
const bun = options.runtime === 'bun' ? await findBun(project) : undefined;
|
|
209
|
+
//The node program embedded, checked before the application is bundled
|
|
210
|
+
const node = bun ? undefined : await nodeOfExecutable(options.nodeProgram);
|
|
192
211
|
const work = await mkdtemp(path.join(os.tmpdir(), 'lakutata-executable-'));
|
|
193
212
|
try {
|
|
194
213
|
const directory = options.layout === 'directory';
|
|
@@ -200,6 +219,8 @@ export async function buildExecutable(options) {
|
|
|
200
219
|
await rm(target, { recursive: true, force: true });
|
|
201
220
|
await mkdir(target, { recursive: true });
|
|
202
221
|
const bundle = await bundleProject({
|
|
222
|
+
//The syntax of the node embedded
|
|
223
|
+
...node ? { node: node.version } : {},
|
|
203
224
|
...options,
|
|
204
225
|
outfile: path.join(work, 'main.cjs'),
|
|
205
226
|
native: directory ? 'directory' : 'embedded',
|
|
@@ -225,7 +246,7 @@ export async function buildExecutable(options) {
|
|
|
225
246
|
//The icon of the application: the one of the executables of Windows (the ones of macOS and Linux have none)
|
|
226
247
|
const application = applicationIcon(project, manifest);
|
|
227
248
|
const icon = application && process.platform === 'win32' ? icoFile(application.image) : undefined;
|
|
228
|
-
const runtime = bun ? await compileWithBun(bun, embedded, work, made, icon) : await embedInNode(embedded, work, made, warnings, icon);
|
|
249
|
+
const runtime = bun ? await compileWithBun(bun, embedded, work, made, icon) : await embedInNode(embedded, work, made, warnings, node, icon);
|
|
229
250
|
await copyFile(made, file);
|
|
230
251
|
await chmod(file, 0o755);
|
|
231
252
|
return {
|
|
@@ -3,6 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { programName } from './Bundle.js';
|
|
5
5
|
import { buildElectronInstaller } from './electron/Electron.js';
|
|
6
|
+
import { buildNwjsApp } from './nwjs/Package.js';
|
|
6
7
|
import { buildExecutable } from './Executable.js';
|
|
7
8
|
import { applicationIcon, icoFile } from './Icons.js';
|
|
8
9
|
import { detectLibc } from './Platform.js';
|
|
@@ -51,10 +52,12 @@ export async function buildInstaller(options) {
|
|
|
51
52
|
if (process.platform !== 'linux' && !mac && !windows)
|
|
52
53
|
throw new Error(`The installers of ${process.platform} are not supported: the ones of Linux are made on Linux, the ones of macOS on macOS, the ones of Windows on Windows`);
|
|
53
54
|
const libc = detectLibc();
|
|
55
|
+
//A desktop application (NW.js): its installers by default the ones of the desktop applications (as electron-builder)
|
|
56
|
+
const desktop = config.runtime === 'nwjs';
|
|
54
57
|
const targets = options.targets?.length ? options.targets
|
|
55
|
-
: mac ? config.mac?.targets ?? ['pkg', 'tar.gz']
|
|
56
|
-
: windows ? config.win?.targets ?? ['nsis', 'msi', 'zip']
|
|
57
|
-
: config.linux?.targets ?? (libc === 'glibc' ? ['deb', 'rpm', 'tar.gz'] : ['apk', 'tar.gz']);
|
|
58
|
+
: mac ? config.mac?.targets ?? (desktop ? ['dmg'] : ['pkg', 'tar.gz'])
|
|
59
|
+
: windows ? config.win?.targets ?? (desktop ? ['nsis'] : ['nsis', 'msi', 'zip'])
|
|
60
|
+
: config.linux?.targets ?? (libc === 'glibc' ? (desktop ? ['deb'] : ['deb', 'rpm', 'tar.gz']) : desktop ? ['tar.gz'] : ['apk', 'tar.gz']);
|
|
58
61
|
for (const target of targets) {
|
|
59
62
|
if (mac) {
|
|
60
63
|
if (!MAC_TARGETS.includes(target))
|
|
@@ -91,6 +94,8 @@ export async function buildInstaller(options) {
|
|
|
91
94
|
if (!/^[A-Za-z0-9][A-Za-z0-9.-]*$/.test(appId))
|
|
92
95
|
throw new Error(`The identifier of the application "${appId}" must be letters, digits, "." and "-" (lakutata.installer.appId)`);
|
|
93
96
|
const service = config.service === true ? {} : config.service || undefined;
|
|
97
|
+
if (desktop && service)
|
|
98
|
+
throw new Error('A desktop application (lakutata.installer.runtime nwjs) has no service (lakutata.installer.service)');
|
|
94
99
|
if (service?.name !== undefined && !/^[A-Za-z0-9_.@-]+$/.test(service.name))
|
|
95
100
|
throw new Error(`The name of the service "${service.name}" must be letters, digits, "_", ".", "@" and "-"`);
|
|
96
101
|
//A user of the system (Linux, macOS), an account of Windows (DOMAIN\user, NT SERVICE\name, a managed account ending in $)
|
|
@@ -108,35 +113,42 @@ export async function buildInstaller(options) {
|
|
|
108
113
|
icon = path.join(work, 'icon.ico');
|
|
109
114
|
await writeFile(icon, icoFile(application.image));
|
|
110
115
|
}
|
|
111
|
-
|
|
116
|
+
//The executable and what it loads; or the application of NW.js (its runtime, its executable named after it)
|
|
117
|
+
const nwjs = desktop ? await buildNwjsApp({ project: project, entry: options.entry, minify: options.minify, external: options.external, arch: options.arch, outdir: path.join(work, 'stage') }) : undefined;
|
|
118
|
+
const executable = nwjs ? undefined : await buildExecutable({
|
|
112
119
|
project: project,
|
|
113
120
|
entry: options.entry,
|
|
114
121
|
minify: options.minify,
|
|
115
122
|
external: options.external,
|
|
116
123
|
runtime: config.runtime,
|
|
124
|
+
nodeProgram: options.nodeProgram,
|
|
117
125
|
layout: 'directory',
|
|
118
126
|
outdir: path.join(work, 'stage')
|
|
119
127
|
});
|
|
120
|
-
warnings.push(...executable.warnings);
|
|
128
|
+
warnings.push(...executable?.warnings ?? nwjs.stage.bundle.warnings);
|
|
129
|
+
//The directory installed: the one of the bundle of macOS (/Applications/<product>.app), the one of the runtime of NW.js
|
|
130
|
+
const installed = nwjs ? (mac ? path.dirname(nwjs.directory) : nwjs.directory) : executable.directory;
|
|
131
|
+
const launcher = nwjs && path.basename(mac ? nwjs.directory : nwjs.executable);
|
|
121
132
|
const plan = {
|
|
122
133
|
name: name,
|
|
123
134
|
appId: appId,
|
|
124
135
|
productName: config.productName ?? (typeof manifest.productName === 'string' ? manifest.productName : name),
|
|
125
136
|
program: program,
|
|
126
137
|
version: version,
|
|
127
|
-
description:
|
|
138
|
+
//An empty description is none (the one of a project created without it): Windows Installer refuses it
|
|
139
|
+
description: config.description || manifest.description || name,
|
|
128
140
|
maintainer: maintainer,
|
|
129
141
|
homepage: config.homepage ?? (typeof manifest.homepage === 'string' ? manifest.homepage : undefined),
|
|
130
142
|
license: config.license ?? (typeof manifest.license === 'string' ? manifest.license : undefined),
|
|
131
143
|
vendor: config.vendor,
|
|
132
|
-
addToPath: config.addToPath ??
|
|
144
|
+
addToPath: config.addToPath ?? !desktop,
|
|
133
145
|
depends: config.linux?.depends ?? [],
|
|
134
146
|
recommends: config.linux?.recommends ?? [],
|
|
135
147
|
section: config.linux?.section,
|
|
136
148
|
priority: config.linux?.priority,
|
|
137
149
|
service: service && {
|
|
138
150
|
name: service.name ?? name,
|
|
139
|
-
description: service.description
|
|
151
|
+
description: service.description || config.description || manifest.description || name,
|
|
140
152
|
args: service.args ?? [],
|
|
141
153
|
env: { MODE: 'production', ...service.env },
|
|
142
154
|
//The users of the services of macOS are hidden by their names (_www), the ones of Windows are virtual
|
|
@@ -144,17 +156,19 @@ export async function buildInstaller(options) {
|
|
|
144
156
|
restart: service.restart ?? 'on-failure',
|
|
145
157
|
autoStart: service.autoStart ?? true
|
|
146
158
|
},
|
|
147
|
-
directory:
|
|
148
|
-
prefix: mac ? `/usr/local/lib/${name}` : windows ? `C:\\Program Files\\${name}` : `/opt/${name}`,
|
|
159
|
+
directory: installed,
|
|
160
|
+
prefix: mac ? (launcher ? `/Applications/${launcher}` : `/usr/local/lib/${name}`) : windows ? `C:\\Program Files\\${name}` : `/opt/${name}`,
|
|
149
161
|
scripts: scripts,
|
|
150
|
-
icon: icon
|
|
162
|
+
icon: icon,
|
|
163
|
+
arch: nwjs ? nwjs.runtime.platform.slice(nwjs.runtime.platform.indexOf('-') + 1) : process.arch,
|
|
164
|
+
...launcher ? { desktop: { executable: launcher, icon: application?.file } } : {}
|
|
151
165
|
};
|
|
152
166
|
const outdir = path.resolve(project, options.outdir ?? path.join('dist', 'installer'));
|
|
153
167
|
await mkdir(outdir, { recursive: true });
|
|
154
168
|
const files = mac ? await buildMacPackages(plan, targets, path.join(work, 'mac'), outdir)
|
|
155
169
|
: windows ? await buildWindowsPackages(plan, targets, path.join(work, 'win'), outdir)
|
|
156
170
|
: await buildLinuxPackages(plan, targets, path.join(work, 'linux'), outdir);
|
|
157
|
-
return { name: name, version: version, files: files, executable: executable, warnings: warnings };
|
|
171
|
+
return { name: name, version: version, files: files, ...executable ? { executable: executable } : {}, ...nwjs ? { nwjs: nwjs } : {}, warnings: warnings };
|
|
158
172
|
}
|
|
159
173
|
finally {
|
|
160
174
|
await rm(work, { recursive: true, force: true });
|
|
@@ -30,8 +30,10 @@ export function lakutataNativeRequire(nodeRequire, config) {
|
|
|
30
30
|
const fs = nodeRequire('node:fs');
|
|
31
31
|
const os = nodeRequire('node:os');
|
|
32
32
|
const { createRequire, isBuiltin } = nodeRequire('node:module');
|
|
33
|
+
//node-compat: node:sea (a type here; loaded below only where it exists)
|
|
33
34
|
let sea;
|
|
34
35
|
try {
|
|
36
|
+
//node-compat: node:sea
|
|
35
37
|
sea = nodeRequire('node:sea');
|
|
36
38
|
}
|
|
37
39
|
catch {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { copyFileSync, lstatSync, mkdirSync, readdirSync, readlinkSync } from 'node:fs';
|
|
2
|
-
import { stat, mkdir, writeFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { stat, mkdir, readFile, writeFile, rm } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { archiveDirectory } from '../Archive.js';
|
|
5
|
+
import { decodePng, writeLinuxIcons } from '../Icons.js';
|
|
5
6
|
import { runCommand } from '../Processes.js';
|
|
6
7
|
import { ensureTool } from '../Tools.js';
|
|
7
8
|
import { embeddedShell, INSTALLER_HOOKS, NATIVE_HOOKS } from '../Scripts.js';
|
|
@@ -18,11 +19,47 @@ const RUNTIME_DEPENDS = {
|
|
|
18
19
|
apk: ['libstdc++', 'libgcc'],
|
|
19
20
|
archlinux: ['gcc-libs']
|
|
20
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* The libraries of the system a desktop application of Chromium (NW.js) loads, by package: the deb ones of Electron, and
|
|
24
|
+
* the atomic operations the Node.js of NW.js loads (libnode.so needs libatomic.so.1, which only the compilers install)
|
|
25
|
+
*/
|
|
26
|
+
const DESKTOP_DEPENDS = {
|
|
27
|
+
deb: ['libgtk-3-0', 'libnotify4', 'libnss3', 'libxss1', 'libxtst6', 'xdg-utils', 'libatspi2.0-0', 'libuuid1', 'libsecret-1-0', 'libasound2', 'libgbm1', 'libatomic1'],
|
|
28
|
+
rpm: ['gtk3', 'libnotify', 'nss', 'libXScrnSaver', 'libXtst', 'xdg-utils', 'at-spi2-core', 'libuuid', 'libsecret', 'alsa-lib', 'mesa-libgbm', 'libatomic'],
|
|
29
|
+
apk: [],
|
|
30
|
+
archlinux: []
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The desktop entry of a desktop application, which the menus of the desktops list
|
|
34
|
+
* @param plan
|
|
35
|
+
* @param executable
|
|
36
|
+
*/
|
|
37
|
+
function desktopEntry(plan, executable) {
|
|
38
|
+
return [
|
|
39
|
+
'[Desktop Entry]',
|
|
40
|
+
'Type=Application',
|
|
41
|
+
`Name=${plan.productName}`,
|
|
42
|
+
`Comment=${plan.description.replace(/\s+/g, ' ')}`,
|
|
43
|
+
`Exec="${plan.prefix}/${executable}" %U`,
|
|
44
|
+
`Icon=${plan.name}`,
|
|
45
|
+
'Terminal=false',
|
|
46
|
+
'Categories=Utility;',
|
|
47
|
+
''
|
|
48
|
+
].join('\n');
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The lines refreshing the menus and the icons of the desktops after the installation and the removal of a desktop
|
|
52
|
+
* application, where their tools are installed
|
|
53
|
+
*/
|
|
54
|
+
const DESKTOP_REFRESH = [
|
|
55
|
+
'command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database -q /usr/share/applications || true',
|
|
56
|
+
'command -v gtk-update-icon-cache >/dev/null 2>&1 && gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor || true'
|
|
57
|
+
];
|
|
21
58
|
/**
|
|
22
59
|
* The architecture of the packages (the one of Go, which nFPM translates for each package)
|
|
23
60
|
*/
|
|
24
|
-
function packageArch() {
|
|
25
|
-
return { x64: 'amd64', arm64: 'arm64', ia32: '386', arm: 'arm7' }[
|
|
61
|
+
function packageArch(plan) {
|
|
62
|
+
return { x64: 'amd64', arm64: 'arm64', ia32: '386', arm: 'arm7' }[plan.arch] ?? plan.arch;
|
|
26
63
|
}
|
|
27
64
|
/**
|
|
28
65
|
* The characters of the globs, which nFPM reads in the sources of the contents (the chunks of Turbopack are named
|
|
@@ -78,7 +115,7 @@ function packageFile(plan, target) {
|
|
|
78
115
|
const arch = { x64: 'x86_64', arm64: 'aarch64' }[process.arch] ?? process.arch;
|
|
79
116
|
switch (target) {
|
|
80
117
|
case 'deb':
|
|
81
|
-
return `${plan.name}_${plan.version}_${packageArch()}.deb`;
|
|
118
|
+
return `${plan.name}_${plan.version}_${packageArch(plan)}.deb`;
|
|
82
119
|
case 'rpm':
|
|
83
120
|
return `${plan.name}-${plan.version}-1.${arch}.rpm`;
|
|
84
121
|
case 'apk':
|
|
@@ -276,7 +313,8 @@ function packageScripts(plan, target) {
|
|
|
276
313
|
//Not indented: the lines of the script are the ones of its file
|
|
277
314
|
...application ? [scriptAction(target, hook), 'if [ -n "$lakutata_action" ]; then', ...embeddedShell(application, plan.prefix, '"$lakutata_action"'), 'fi'] : [],
|
|
278
315
|
...native ? embeddedShell(native, plan.prefix, '"$@"') : [],
|
|
279
|
-
...service.after[hook] ?? []
|
|
316
|
+
...service.after[hook] ?? [],
|
|
317
|
+
...plan.desktop && (hook === 'postinstall' || hook === 'postremove' || hook === 'postupgrade') ? DESKTOP_REFRESH : []
|
|
280
318
|
];
|
|
281
319
|
if (lines.length)
|
|
282
320
|
scripts[hook] = ['#!/bin/sh', 'set -e', ...lines, ''].join('\n');
|
|
@@ -297,7 +335,7 @@ export async function buildLinuxPackages(plan, targets, work, outdir) {
|
|
|
297
335
|
const service = plan.service;
|
|
298
336
|
for (const target of targets) {
|
|
299
337
|
if (target === 'tar.gz') {
|
|
300
|
-
files.push(await archiveDirectory(plan, packageArch(), work, outdir));
|
|
338
|
+
files.push(await archiveDirectory(plan, packageArch(plan), work, outdir));
|
|
301
339
|
continue;
|
|
302
340
|
}
|
|
303
341
|
const directory = path.join(work, target);
|
|
@@ -305,6 +343,17 @@ export async function buildLinuxPackages(plan, targets, work, outdir) {
|
|
|
305
343
|
const contents = packageContents(plan.directory, plan.prefix, directory);
|
|
306
344
|
if (plan.addToPath)
|
|
307
345
|
contents.push({ src: `${plan.prefix}/${plan.program}`, dst: `/usr/bin/${plan.program}`, type: 'symlink' });
|
|
346
|
+
if (plan.desktop) {
|
|
347
|
+
//Its entry in the menus of the desktops, its icons in the theme hicolor
|
|
348
|
+
await writeFile(path.join(directory, 'desktop'), desktopEntry(plan, plan.desktop.executable));
|
|
349
|
+
contents.push({ src: path.join(directory, 'desktop'), dst: `/usr/share/applications/${plan.name}.desktop`, file_info: { mode: 0o644 } });
|
|
350
|
+
if (plan.desktop.icon) {
|
|
351
|
+
for (const icon of await writeLinuxIcons(decodePng(await readFile(plan.desktop.icon), plan.desktop.icon), path.join(directory, 'icons'))) {
|
|
352
|
+
const size = path.basename(icon, '.png');
|
|
353
|
+
contents.push({ src: icon, dst: `/usr/share/icons/hicolor/${size}/apps/${plan.name}.png`, file_info: { mode: 0o644 } });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
308
357
|
if (service) {
|
|
309
358
|
await writeFile(path.join(directory, 'environment'), environmentFile(service));
|
|
310
359
|
contents.push({ src: path.join(directory, 'environment'), dst: `/etc/${plan.name}/${plan.name}.env`, type: 'config|noreplace', file_info: { mode: 0o640 } });
|
|
@@ -331,7 +380,7 @@ export async function buildLinuxPackages(plan, targets, work, outdir) {
|
|
|
331
380
|
}
|
|
332
381
|
const config = {
|
|
333
382
|
name: plan.name,
|
|
334
|
-
arch: packageArch(),
|
|
383
|
+
arch: packageArch(plan),
|
|
335
384
|
platform: 'linux',
|
|
336
385
|
version: plan.version,
|
|
337
386
|
maintainer: plan.maintainer,
|
|
@@ -341,7 +390,7 @@ export async function buildLinuxPackages(plan, targets, work, outdir) {
|
|
|
341
390
|
...plan.license ? { license: plan.license } : {},
|
|
342
391
|
...plan.section ? { section: plan.section } : {},
|
|
343
392
|
...plan.priority ? { priority: plan.priority } : {},
|
|
344
|
-
depends: [...new Set([...RUNTIME_DEPENDS[target], ...plan.depends])],
|
|
393
|
+
depends: [...new Set([...RUNTIME_DEPENDS[target], ...plan.desktop ? DESKTOP_DEPENDS[target] : [], ...plan.depends])],
|
|
345
394
|
...plan.recommends.length ? { recommends: plan.recommends } : {},
|
|
346
395
|
contents: contents,
|
|
347
396
|
scripts: common,
|
package/dist/esm/lib/mac/Mac.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { chmod,
|
|
1
|
+
import { chmod, mkdir, readFile, rm, stat, symlink, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { archiveDirectory } from '../Archive.js';
|
|
4
|
+
import { copyDirectory } from '../Directories.js';
|
|
4
5
|
import { runCommand } from '../Processes.js';
|
|
5
6
|
import { embeddedShell } from '../Scripts.js';
|
|
6
7
|
/**
|
|
7
8
|
* The installers of macOS: the package of its installer, and the archive of the directory of the executable
|
|
8
9
|
*/
|
|
9
|
-
export const MAC_TARGETS = ['pkg', 'tar.gz'];
|
|
10
|
+
export const MAC_TARGETS = ['pkg', 'dmg', 'tar.gz'];
|
|
10
11
|
/**
|
|
11
12
|
* The architecture of the packages
|
|
12
13
|
*/
|
|
13
|
-
function packageArch() {
|
|
14
|
-
return
|
|
14
|
+
function packageArch(plan) {
|
|
15
|
+
return plan.arch;
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
17
18
|
* A text of a property list
|
|
@@ -103,7 +104,7 @@ function preInstall(plan) {
|
|
|
103
104
|
return [
|
|
104
105
|
'#!/bin/sh',
|
|
105
106
|
'set -e',
|
|
106
|
-
`if [ -e ${doubleQuoted(`${plan.prefix}/${plan.program}`)} ]; then lakutata_action=upgrade; else lakutata_action=install; fi`,
|
|
107
|
+
`if [ -e ${doubleQuoted(plan.desktop ? plan.prefix : `${plan.prefix}/${plan.program}`)} ]; then lakutata_action=upgrade; else lakutata_action=install; fi`,
|
|
107
108
|
`if [ -n "$INSTALLER_TEMP" ]; then echo "$lakutata_action" > ${ACTION_FILE}; fi`,
|
|
108
109
|
...hooks.preinstall ? embeddedShell(hooks.preinstall, plan.prefix, '"$lakutata_action"') : [],
|
|
109
110
|
''
|
|
@@ -223,6 +224,26 @@ function uninstall(plan) {
|
|
|
223
224
|
''
|
|
224
225
|
].join('\n');
|
|
225
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* The disk image of a desktop application (dmg): its bundle and a link to /Applications, where its user drags it
|
|
229
|
+
* @param plan
|
|
230
|
+
* @param work
|
|
231
|
+
* @param outdir
|
|
232
|
+
*/
|
|
233
|
+
async function diskImage(plan, work, outdir) {
|
|
234
|
+
if (!plan.desktop)
|
|
235
|
+
throw new Error('The disk images (dmg) are made for the desktop applications (NW.js): the other applications are installed by pkg');
|
|
236
|
+
const volume = path.join(work, 'dmg');
|
|
237
|
+
await rm(volume, { recursive: true, force: true });
|
|
238
|
+
await copyDirectory(path.join(plan.directory, plan.desktop.executable), path.join(volume, plan.desktop.executable));
|
|
239
|
+
await symlink('/Applications', path.join(volume, 'Applications'));
|
|
240
|
+
const file = path.join(outdir, `${plan.name}-${plan.version}-${packageArch(plan)}.dmg`);
|
|
241
|
+
await rm(file, { force: true });
|
|
242
|
+
const created = await runCommand('hdiutil', ['create', '-volname', plan.productName, '-srcfolder', volume, '-ov', '-format', 'UDZO', file], work);
|
|
243
|
+
if (created.code !== 0)
|
|
244
|
+
throw new Error(`The disk image cannot be made: ${created.output.trim()}`);
|
|
245
|
+
return { target: 'dmg', file: file, size: (await stat(file)).size };
|
|
246
|
+
}
|
|
226
247
|
/**
|
|
227
248
|
* Make the installers of macOS of an application: the package installed by its installer (pkgbuild, productbuild), the
|
|
228
249
|
* tar.gz archive of the directory of the executable
|
|
@@ -236,7 +257,11 @@ export async function buildMacPackages(plan, targets, work, outdir) {
|
|
|
236
257
|
const files = [];
|
|
237
258
|
for (const target of targets) {
|
|
238
259
|
if (target === 'tar.gz') {
|
|
239
|
-
files.push(await archiveDirectory(plan, packageArch(), work, outdir));
|
|
260
|
+
files.push(await archiveDirectory(plan, packageArch(plan), work, outdir));
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (target === 'dmg') {
|
|
264
|
+
files.push(await diskImage(plan, work, outdir));
|
|
240
265
|
continue;
|
|
241
266
|
}
|
|
242
267
|
//The files installed, from the root of the system
|
|
@@ -246,9 +271,11 @@ export async function buildMacPackages(plan, targets, work, outdir) {
|
|
|
246
271
|
await rm(scripts, { recursive: true, force: true });
|
|
247
272
|
const prefix = path.join(root, ...plan.prefix.split('/'));
|
|
248
273
|
await mkdir(path.dirname(prefix), { recursive: true });
|
|
249
|
-
|
|
250
|
-
await
|
|
251
|
-
if (plan.
|
|
274
|
+
//A desktop application: its bundle into /Applications (removed by its user, as the other applications)
|
|
275
|
+
await copyDirectory(plan.desktop ? path.join(plan.directory, plan.desktop.executable) : plan.directory, prefix);
|
|
276
|
+
if (!plan.desktop)
|
|
277
|
+
await writeFile(path.join(prefix, 'uninstall'), uninstall(plan), { mode: 0o755 });
|
|
278
|
+
if (plan.addToPath && !plan.desktop) {
|
|
252
279
|
await mkdir(path.join(root, 'usr', 'local', 'bin'), { recursive: true });
|
|
253
280
|
await symlink(path.posix.relative('/usr/local/bin', `${plan.prefix}/${plan.program}`), path.join(root, 'usr', 'local', 'bin', plan.program));
|
|
254
281
|
}
|
|
@@ -270,14 +297,15 @@ export async function buildMacPackages(plan, targets, work, outdir) {
|
|
|
270
297
|
const built = await runCommand('pkgbuild', ['--root', root, '--identifier', plan.appId, '--version', plan.version, '--install-location', '/', '--scripts', scripts, '--ownership', 'recommended', component], work);
|
|
271
298
|
if (built.code !== 0)
|
|
272
299
|
throw new Error(`The pkg package cannot be made: ${built.output.trim()}`);
|
|
273
|
-
const file = path.join(outdir, `${plan.name}-${plan.version}-${packageArch()}.pkg`);
|
|
300
|
+
const file = path.join(outdir, `${plan.name}-${plan.version}-${packageArch(plan)}.pkg`);
|
|
274
301
|
await rm(file, { force: true });
|
|
275
302
|
//Installed on the Macs of the architecture of the program only (productbuild gives both)
|
|
276
303
|
const distribution = path.join(work, 'distribution.xml');
|
|
277
304
|
const synthesized = await runCommand('productbuild', ['--synthesize', '--package', component, distribution], work);
|
|
278
305
|
if (synthesized.code !== 0)
|
|
279
306
|
throw new Error(`The pkg package cannot be made: ${synthesized.output.trim()}`);
|
|
280
|
-
|
|
307
|
+
//A desktop application of Intel runs on the Macs of Apple silicon too (Rosetta)
|
|
308
|
+
const architectures = `hostArchitectures="${packageArch(plan) === 'arm64' ? 'arm64' : plan.desktop ? 'x86_64,arm64' : 'x86_64'}"`;
|
|
281
309
|
const options = await readFile(distribution, 'utf8');
|
|
282
310
|
await writeFile(distribution, /hostArchitectures="[^"]*"/.test(options) ? options.replace(/hostArchitectures="[^"]*"/, architectures) : options.replace('<options ', `<options ${architectures} `));
|
|
283
311
|
const product = await runCommand('productbuild', ['--distribution', distribution, '--package-path', work, '--identifier', plan.appId, '--version', plan.version, file], work);
|