@lakutata/builder 3.0.0-beta.6 → 3.1.0
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 +29 -20
- package/README.zh-CN.md +26 -0
- package/dist/esm/lib/Bundle.js +7 -2
- package/dist/esm/lib/Executable.js +25 -0
- package/dist/esm/lib/Installer.js +4 -0
- package/dist/esm/lib/Native.js +13 -3
- package/dist/esm/lib/NativeRuntime.js +8 -4
- package/dist/esm/lib/Scripts.js +11 -3
- package/dist/esm/lib/electron/Electron.js +5 -5
- package/dist/esm/lib/mac/Mac.js +35 -16
- package/dist/types/lib/Bundle.d.ts +5 -0
- package/dist/types/lib/Executable.d.ts +8 -0
- package/dist/types/lib/NativeRuntime.d.ts +6 -0
- package/dist/types/lib/Scripts.d.ts +1 -1
- package/package.json +25 -2
package/README.md
CHANGED
|
@@ -1,31 +1,40 @@
|
|
|
1
1
|
# @lakutata/builder
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
English | [中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
The builder for lakutata applications. The `package bundle`, `package exe` and `package installer` commands of
|
|
6
|
+
`@lakutata/cli` use it, and a project's scripts can call it directly:
|
|
5
7
|
|
|
6
8
|
```js
|
|
7
9
|
import {buildExecutable, buildInstaller, bundleProject} from '@lakutata/builder'
|
|
8
10
|
|
|
9
|
-
//dist/<name>.cjs, run by node, its native addons and its resources next to it
|
|
11
|
+
//dist/<name>.cjs, run by node, with its native addons and its resources next to it
|
|
10
12
|
await bundleProject({project: process.cwd()})
|
|
11
|
-
//
|
|
13
|
+
//An executable that runs without Node.js installed, with Node.js (the default) or Bun embedded. With layout
|
|
14
|
+
//'directory': dist/<name>/<name>, its native addons and resources next to it; with 'file' (the default): dist/<name>
|
|
12
15
|
await buildExecutable({project: process.cwd(), runtime: 'node', layout: 'directory'})
|
|
13
|
-
//dist/installer: the installers
|
|
14
|
-
//nsis, msi, zip on Windows),
|
|
16
|
+
//dist/installer: the installers for the current platform (deb, rpm, apk, archlinux, tar.gz on Linux; pkg, tar.gz on
|
|
17
|
+
//macOS; nsis, msi, zip on Windows), which also register the application's service when it declares one
|
|
15
18
|
await buildInstaller({project: process.cwd()})
|
|
16
19
|
```
|
|
17
20
|
|
|
18
|
-
The
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
`warnings` of the
|
|
24
|
-
|
|
25
|
-
`
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
(`
|
|
21
|
+
The builder works on the compiled application: build the project with `tsc` first, since `tsc` emits the decorator
|
|
22
|
+
metadata. The bundle is one CommonJS file, made by esbuild. Native addons cannot be bundled, so they are gathered, with
|
|
23
|
+
what they load at run time, into a native tree. The tree also holds the files that the application reads by path,
|
|
24
|
+
which the bundler does not follow: the scripts it runs on their own (workers, worker pools, `fork()`,
|
|
25
|
+
`require(path.join(__dirname, 'x.js'))`) are bundled, and its data files and directories are copied. The builder finds
|
|
26
|
+
the paths built from a module's location and rewrites them; whatever it cannot ship is listed in the `warnings` of the
|
|
27
|
+
result. At run time, `resourcesPath()` from lakutata locates the application's resources, declared by
|
|
28
|
+
`lakutata.resources` in its `package.json`. The installers are configured by `lakutata.installer` in the same file. See
|
|
29
|
+
`lakutata package bundle`, `lakutata package exe` and `lakutata package installer` in the `@lakutata/cli`
|
|
30
|
+
documentation.
|
|
31
|
+
|
|
32
|
+
The Linux installers are tested in containers (Docker): `npm run test:installer` builds them on Debian and Alpine, then
|
|
33
|
+
installs, upgrades and removes them on Debian and Fedora (systemd) and on Alpine (OpenRC). The macOS installers are
|
|
34
|
+
installed in a macOS virtual machine (`npm run test:installer:mac`, with tart), and the Windows installers on Windows,
|
|
35
|
+
as an administrator (`LAKUTATA_WINDOWS_INSTALL=1 npm run test:installer:windows`).
|
|
36
|
+
|
|
37
|
+
## See also
|
|
38
|
+
|
|
39
|
+
- The guides on the website: [Packaging](https://lakutata.com/guide/Packaging) and
|
|
40
|
+
[Installers](https://lakutata.com/guide/Installers).
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @lakutata/builder
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
lakutata 应用的打包工具。`@lakutata/cli` 的 `package bundle`、`package exe` 和 `package installer` 命令都靠它完成工作,项目的脚本也可以直接调用它:
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import {buildExecutable, buildInstaller, bundleProject} from '@lakutata/builder'
|
|
9
|
+
|
|
10
|
+
//dist/<name>.cjs,由 node 运行,原生插件和资源放在它旁边
|
|
11
|
+
await bundleProject({project: process.cwd()})
|
|
12
|
+
//不需要安装 Node.js 就能运行的可执行文件,内嵌 Node.js(默认)或 Bun。
|
|
13
|
+
//layout 为 'directory' 时:dist/<name>/<name>,原生插件和资源放在它旁边;为 'file'(默认)时:dist/<name>
|
|
14
|
+
await buildExecutable({project: process.cwd(), runtime: 'node', layout: 'directory'})
|
|
15
|
+
//dist/installer:当前平台的安装包(Linux 上是 deb、rpm、apk、archlinux、tar.gz;macOS 上是 pkg、tar.gz;
|
|
16
|
+
//Windows 上是 nsis、msi、zip),应用声明了服务时,安装包还会注册这个服务
|
|
17
|
+
await buildInstaller({project: process.cwd()})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
打包工具处理的是编译后的应用:请先用 `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
|
+
|
|
22
|
+
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
|
+
|
|
24
|
+
## 另见
|
|
25
|
+
|
|
26
|
+
- 网站上的指南:[打包](https://lakutata.com/zh/guide/Packaging)、[安装包](https://lakutata.com/zh/guide/Installers)。
|
package/dist/esm/lib/Bundle.js
CHANGED
|
@@ -180,17 +180,22 @@ export async function bundleProject(options) {
|
|
|
180
180
|
const embeddedFiles = [...native?.files ?? [], ...resources.files.map((file) => ({ ...file, path: `${EMBEDDED_RESOURCES}/${file.path}` }))];
|
|
181
181
|
native = { modules: native?.modules ?? [], files: embeddedFiles, links: native?.links ?? [], hash: hashNativeTree(embeddedFiles, native?.links ?? []) };
|
|
182
182
|
}
|
|
183
|
-
|
|
183
|
+
const executable = embedded || !!options.executable;
|
|
184
|
+
if (native && executable) {
|
|
184
185
|
//An executable runs the scripts of its tree it is started for (fork()) instead of the application
|
|
185
186
|
const hashbang = /^#![^\n]*\n/.exec(code)?.[0] ?? '';
|
|
186
187
|
code = `${hashbang}if (!require.lakutataScript()) {\n${code.slice(hashbang.length)}\n}\n`;
|
|
187
188
|
}
|
|
188
189
|
if (native) {
|
|
189
190
|
//The require loading the native packages is the first code, after the hashbang
|
|
190
|
-
code = prepend(code, nativeBanner({ name: name, hash: native.hash, directory: path.basename(nativeDirectory), modules: native.modules, embedded: embedded }));
|
|
191
|
+
code = prepend(code, nativeBanner({ name: name, hash: native.hash, directory: path.basename(nativeDirectory), modules: native.modules, embedded: embedded, executable: executable }));
|
|
191
192
|
if (!embedded)
|
|
192
193
|
await writeNativeTree(native, nativeDirectory);
|
|
193
194
|
}
|
|
195
|
+
else if (executable && options.runtime !== 'bun') {
|
|
196
|
+
//Without tree, an executable of Node.js still loads the modules left out of the bundle from next to it
|
|
197
|
+
code = prepend(code, nativeBanner({ name: name, hash: '', directory: '', modules: [], embedded: false, executable: true }));
|
|
198
|
+
}
|
|
194
199
|
if (!embedded) {
|
|
195
200
|
for (const file of resources.files) {
|
|
196
201
|
const target = path.join(resourcesDirectory, ...file.path.split('/'));
|
|
@@ -157,6 +157,26 @@ async function compileWithBun(bun, bundle, work, file, icon) {
|
|
|
157
157
|
], work);
|
|
158
158
|
return `Bun ${bun.version}`;
|
|
159
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* The packages of a native tree whose addons are built on the V8 API of Node.js (they export
|
|
162
|
+
* node_register_module_v<ABI>), not on Node-API (napi_register_module_v1): Bun may not load them (better-sqlite3)
|
|
163
|
+
* @param files The files of the native tree.
|
|
164
|
+
* @returns The names of the packages (or of the addons of the application itself), sorted.
|
|
165
|
+
*/
|
|
166
|
+
export function v8Addons(files) {
|
|
167
|
+
const names = new Set();
|
|
168
|
+
for (const file of files) {
|
|
169
|
+
if (!file.path.endsWith('.node'))
|
|
170
|
+
continue;
|
|
171
|
+
const content = file.content ?? readFileSync(file.source);
|
|
172
|
+
if (!content.includes('node_register_module_v') || content.includes('napi_register_module_v1'))
|
|
173
|
+
continue;
|
|
174
|
+
const at = file.path.lastIndexOf('node_modules/');
|
|
175
|
+
const parts = at < 0 ? [path.posix.basename(file.path)] : file.path.slice(at + 'node_modules/'.length).split('/');
|
|
176
|
+
names.add(parts[0].startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]);
|
|
177
|
+
}
|
|
178
|
+
return [...names].sort();
|
|
179
|
+
}
|
|
160
180
|
/**
|
|
161
181
|
* Make the executable of the application for the platform it is made on, which runs without Node.js installed: its bundle
|
|
162
182
|
* embedded in the Node.js binary running the tool (the single executable applications of Node.js), or compiled by Bun.
|
|
@@ -183,6 +203,7 @@ export async function buildExecutable(options) {
|
|
|
183
203
|
...options,
|
|
184
204
|
outfile: path.join(work, 'main.cjs'),
|
|
185
205
|
native: directory ? 'directory' : 'embedded',
|
|
206
|
+
executable: true,
|
|
186
207
|
directories: { native: path.join(target, 'native'), resources: path.join(target, 'resources') }
|
|
187
208
|
});
|
|
188
209
|
//The executables of Node.js do not load the native addons on musl (postject rewrites their symbols): the ones of Bun do
|
|
@@ -197,6 +218,10 @@ export async function buildExecutable(options) {
|
|
|
197
218
|
const made = path.join(work, 'executable', path.basename(file));
|
|
198
219
|
await mkdir(path.dirname(made), { recursive: true });
|
|
199
220
|
const warnings = [...bundle.warnings];
|
|
221
|
+
//Bun loads the addons of Node-API; an addon built on the V8 API of Node.js may fail when it is loaded
|
|
222
|
+
if (bun)
|
|
223
|
+
for (const name of v8Addons(bundle.native?.files ?? []))
|
|
224
|
+
warnings.push(`${name}: its addon is built on the V8 API of Node.js, not on Node-API, and Bun may not load it (the executable then fails when it loads the package): make the executable with Node.js (--runtime node)`);
|
|
200
225
|
//The icon of the application: the one of the executables of Windows (the ones of macOS and Linux have none)
|
|
201
226
|
const application = applicationIcon(project, manifest);
|
|
202
227
|
const icon = application && process.platform === 'win32' ? icoFile(application.image) : undefined;
|
|
@@ -93,6 +93,10 @@ export async function buildInstaller(options) {
|
|
|
93
93
|
const service = config.service === true ? {} : config.service || undefined;
|
|
94
94
|
if (service?.name !== undefined && !/^[A-Za-z0-9_.@-]+$/.test(service.name))
|
|
95
95
|
throw new Error(`The name of the service "${service.name}" must be letters, digits, "_", ".", "@" and "-"`);
|
|
96
|
+
//A user of the system (Linux, macOS), an account of Windows (DOMAIN\user, NT SERVICE\name, a managed account ending in $)
|
|
97
|
+
if (service?.user !== undefined && !(windows ? /^[A-Za-z0-9_][A-Za-z0-9_. @-]*(\\[A-Za-z0-9_][A-Za-z0-9_. @-]*)?\$?$/ : /^[A-Za-z_][A-Za-z0-9_.-]*$/).test(service.user)) {
|
|
98
|
+
throw new Error(`The user of the service "${service.user}" must be ${windows ? 'an account of Windows (LocalService, NetworkService, LocalSystem, DOMAIN\\user)' : 'a name of user: letters, digits, "_", "." and "-"'} (lakutata.installer.service.user)`);
|
|
99
|
+
}
|
|
96
100
|
const platform = mac ? 'mac' : windows ? 'win' : 'linux';
|
|
97
101
|
const scripts = resolveScripts(project, platform, config[platform], targets, warnings);
|
|
98
102
|
const application = applicationIcon(project, manifest);
|
package/dist/esm/lib/Native.js
CHANGED
|
@@ -29,9 +29,14 @@ const LIBRARY = /\.(node|dylib|dll|so)$|\.so\.\d+(\.\d+)*$/;
|
|
|
29
29
|
const RESOLVING = 'lakutataNativeResolving';
|
|
30
30
|
/**
|
|
31
31
|
* The files of the packages node never loads: their types, their source maps, their sources in TypeScript, their
|
|
32
|
-
* documentation
|
|
32
|
+
* documentation, the sources and the objects of their addons in C and C++ (better-sqlite3 holds the source of SQLite)
|
|
33
33
|
*/
|
|
34
|
-
const UNUSED = /\.(d\.[cm]?ts|[cm]?ts|map|md|markdown)$/i;
|
|
34
|
+
const UNUSED = /\.(d\.[cm]?ts|[cm]?ts|map|md|markdown|c|cc|cpp|cxx|h|hh|hpp|hxx|o|obj|a|lib|exp|pdb|ilk)$/i;
|
|
35
|
+
/**
|
|
36
|
+
* The files of the builds of the addons in build/ (node-gyp, CMake), which node never loads: their makefiles, their
|
|
37
|
+
* configurations, their objects and their dependency files; the addons and their libraries are kept
|
|
38
|
+
*/
|
|
39
|
+
const BUILD_FILES = /^build\/(?:.*\/)?(?:\.deps|obj|obj\.target|CMakeFiles)\/|^build\/(?:.*\/)?(?:Makefile|[^/]+\.mk|config\.gypi|gyp-(?:mac|win)-tool|CMakeCache\.txt|cmake_install\.cmake|build\.ninja|\.ninja_deps|\.ninja_log|compile_commands\.json|[^/]+\.(?:vcxproj|vcxproj\.filters|sln|tlog))$/;
|
|
35
40
|
/**
|
|
36
41
|
* The JavaScript files, which a package keeps when they are loaded (traced)
|
|
37
42
|
*/
|
|
@@ -502,7 +507,12 @@ export class NativeCollector {
|
|
|
502
507
|
* @param root
|
|
503
508
|
*/
|
|
504
509
|
packageFiles(root) {
|
|
505
|
-
|
|
510
|
+
//The builds of cargo (the Rust addons) are in target/
|
|
511
|
+
const cargo = existsSync(path.join(root, 'Cargo.toml'));
|
|
512
|
+
const files = packageFiles(root).filter((file) => {
|
|
513
|
+
const relative = path.relative(root, file).split(path.sep).join('/');
|
|
514
|
+
return !UNUSED.test(file) && !BUILD_FILES.test(relative) && !(cargo && relative.startsWith('target/'));
|
|
515
|
+
});
|
|
506
516
|
const loaded = this.loaded.get(root);
|
|
507
517
|
if (!loaded || this.whole.has(root) || this.package(root)?.libraries)
|
|
508
518
|
return files;
|
|
@@ -29,7 +29,7 @@ export function lakutataNativeRequire(nodeRequire, config) {
|
|
|
29
29
|
const path = nodeRequire('node:path');
|
|
30
30
|
const fs = nodeRequire('node:fs');
|
|
31
31
|
const os = nodeRequire('node:os');
|
|
32
|
-
const { createRequire } = nodeRequire('node:module');
|
|
32
|
+
const { createRequire, isBuiltin } = nodeRequire('node:module');
|
|
33
33
|
let sea;
|
|
34
34
|
try {
|
|
35
35
|
sea = nodeRequire('node:sea');
|
|
@@ -124,10 +124,14 @@ export function lakutataNativeRequire(nodeRequire, config) {
|
|
|
124
124
|
return modules.has(id.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]);
|
|
125
125
|
};
|
|
126
126
|
const tree = () => treeRequire ??= createRequire(path.join(locate(), 'index.js'));
|
|
127
|
+
//The modules left out of the bundle (--external), next to an executable of Node.js: the require of its single
|
|
128
|
+
//executable application loads the modules of node only (the one of a bundle run by node loads them)
|
|
129
|
+
const disk = sea?.isSea() ? createRequire(process.execPath) : undefined;
|
|
130
|
+
const outside = (id) => disk && !isBuiltin(id) ? disk : nodeRequire;
|
|
127
131
|
//The files loaded by their paths (the scripts of the tree): the require of an executable of Node.js loads the modules of
|
|
128
132
|
//node only
|
|
129
|
-
const require = ((id) => fromTree(id) ? tree()(id) : path.isAbsolute(id) ? createRequire(id)(id) :
|
|
130
|
-
require.resolve = ((id, options) => fromTree(id) ? tree().resolve(id, options) :
|
|
133
|
+
const require = ((id) => fromTree(id) ? tree()(id) : path.isAbsolute(id) ? createRequire(id)(id) : outside(id)(id));
|
|
134
|
+
require.resolve = ((id, options) => fromTree(id) ? tree().resolve(id, options) : outside(id).resolve(id, options));
|
|
131
135
|
require.cache = nodeRequire.cache;
|
|
132
136
|
require.main = nodeRequire.main;
|
|
133
137
|
require.extensions = nodeRequire.extensions;
|
|
@@ -144,7 +148,7 @@ export function lakutataNativeRequire(nodeRequire, config) {
|
|
|
144
148
|
//An executable started to run a script of its tree (fork(), spawn(process.execPath, [script]): process.execPath is the
|
|
145
149
|
//executable): it runs the script as node runs it, the application not started
|
|
146
150
|
require.lakutataScript = () => {
|
|
147
|
-
if (!config.embedded || config.root !== undefined)
|
|
151
|
+
if (!(config.embedded || config.executable) || config.root !== undefined)
|
|
148
152
|
return false;
|
|
149
153
|
const scripts = path.join(locate(), '.lakutata', 'files') + path.sep;
|
|
150
154
|
const at = [1, 2].find((index) => !!process.argv[index] && path.resolve(process.argv[index]).startsWith(scripts)) ?? -1;
|
package/dist/esm/lib/Scripts.js
CHANGED
|
@@ -11,6 +11,10 @@ export const INSTALLER_HOOKS = ['preinstall', 'postinstall', 'preremove', 'postr
|
|
|
11
11
|
* of dpkg, of rpm, of apk, of pacman); the upgrades of apk and pacman have their own ones
|
|
12
12
|
*/
|
|
13
13
|
export const NATIVE_HOOKS = ['preinstall', 'postinstall', 'preremove', 'postremove', 'preupgrade', 'postupgrade'];
|
|
14
|
+
/**
|
|
15
|
+
* The options of a platform which are not the ones of an installer
|
|
16
|
+
*/
|
|
17
|
+
const PLATFORM_OPTIONS = ['targets', 'scripts', 'depends', 'recommends', 'section', 'priority'];
|
|
14
18
|
/**
|
|
15
19
|
* The installers which run no script (archives, copied by their users)
|
|
16
20
|
*/
|
|
@@ -63,7 +67,7 @@ function installerHooks(project, scripts, where, windows) {
|
|
|
63
67
|
* The scripts of the installers made on a platform: the ones of the platform (lakutata.installer.<platform>.scripts),
|
|
64
68
|
* replaced by the ones of each installer (lakutata.installer.<platform>.<installer>.scripts), the native ones of the
|
|
65
69
|
* packages of Linux (nativeScripts), the file of NSIS included by its installer (win.nsis.include). The ones an
|
|
66
|
-
* installer does not run are told (warnings)
|
|
70
|
+
* installer does not run are told (warnings); the options of the installers not made are checked too
|
|
67
71
|
* @param project
|
|
68
72
|
* @param platform its name in the configuration: linux, mac or win
|
|
69
73
|
* @param config its configuration
|
|
@@ -78,7 +82,8 @@ export function resolveScripts(project, platform, config, targets, warnings, hoo
|
|
|
78
82
|
const common = installerHooks(project, config?.scripts, `${where}.scripts`, windows);
|
|
79
83
|
const resolved = {};
|
|
80
84
|
const unrun = [];
|
|
81
|
-
|
|
85
|
+
const configured = Object.keys(config ?? {}).filter((key) => !PLATFORM_OPTIONS.includes(key) && !targets.includes(key));
|
|
86
|
+
for (const target of [...targets, ...configured]) {
|
|
82
87
|
const options = config?.[target];
|
|
83
88
|
if (options !== undefined && (!options || typeof options !== 'object' || Array.isArray(options)))
|
|
84
89
|
throw new Error(`${where}.${target} must be an object`);
|
|
@@ -86,7 +91,7 @@ export function resolveScripts(project, platform, config, targets, warnings, hoo
|
|
|
86
91
|
const scripts = { hooks: { ...common, ...installerHooks(project, own.scripts, `${where}.${target}.scripts`, windows) }, native: {} };
|
|
87
92
|
if (own.nativeScripts !== undefined) {
|
|
88
93
|
if (!native || platform !== 'linux' || ARCHIVES.includes(target))
|
|
89
|
-
throw new Error(`${where}.${target}.nativeScripts: the native scripts are the ones of the packages of Linux lakutata makes (deb, rpm, apk, archlinux), not the ones of Electron`);
|
|
94
|
+
throw new Error(`${where}.${target}.nativeScripts: the native scripts are the ones of the packages of Linux lakutata makes (deb, rpm, apk, archlinux)${native ? '' : ', not the ones of Electron'}`);
|
|
90
95
|
if (!own.nativeScripts || typeof own.nativeScripts !== 'object' || Array.isArray(own.nativeScripts))
|
|
91
96
|
throw new Error(`${where}.${target}.nativeScripts must be {"<script>": "<file>"}`);
|
|
92
97
|
//The upgrades of deb and rpm run their scripts of the installation and of the removal
|
|
@@ -104,6 +109,9 @@ export function resolveScripts(project, platform, config, targets, warnings, hoo
|
|
|
104
109
|
throw new Error(`${where}.nsis.include (${own.include}) must be a file of NSIS (.nsh)`);
|
|
105
110
|
scripts.include = read(project, own.include, `${where}.nsis.include`);
|
|
106
111
|
}
|
|
112
|
+
//Checked only: not made
|
|
113
|
+
if (!targets.includes(target))
|
|
114
|
+
continue;
|
|
107
115
|
//The ones the installer does not run
|
|
108
116
|
const run = hooks(target);
|
|
109
117
|
const skipped = INSTALLER_HOOKS.filter((hook) => !!scripts.hooks[hook] && !run.includes(hook));
|
|
@@ -228,13 +228,16 @@ export async function buildElectronInstaller(project, manifest, config, options)
|
|
|
228
228
|
try {
|
|
229
229
|
//The application of electron-builder: its main process, its package.json; its native addons and its resources next to it
|
|
230
230
|
const application = path.join(work, 'app');
|
|
231
|
+
//The directory of the native addons and of the resources, copied into the resources of Electron: electron-builder
|
|
232
|
+
//leaves out the node_modules at the root of a directory it copies, the packages of the native tree
|
|
233
|
+
const extra = path.join(work, 'extra');
|
|
231
234
|
const bundle = await bundleProject({
|
|
232
235
|
project: project,
|
|
233
236
|
entry: options.entry,
|
|
234
237
|
minify: options.minify,
|
|
235
238
|
external: ['electron', ...options.external ?? []],
|
|
236
239
|
outfile: path.join(application, 'main.cjs'),
|
|
237
|
-
directories: { native: path.join(
|
|
240
|
+
directories: { native: path.join(extra, 'main.native'), resources: path.join(extra, 'main.resources') }
|
|
238
241
|
});
|
|
239
242
|
warnings.push(...bundle.warnings);
|
|
240
243
|
const maintainer = platform === 'linux' ? packageMaintainer(author, name, warnings) : author ?? name;
|
|
@@ -303,10 +306,7 @@ export async function buildElectronInstaller(project, manifest, config, options)
|
|
|
303
306
|
...existsSync(electronDist) ? { electronDist: electronDist } : {},
|
|
304
307
|
directories: { app: 'app', output: outdir },
|
|
305
308
|
files: ['main.cjs', 'package.json'],
|
|
306
|
-
extraResources: [
|
|
307
|
-
...bundle.nativeDirectory ? [{ from: bundle.nativeDirectory, to: path.basename(bundle.nativeDirectory) }] : [],
|
|
308
|
-
...bundle.resourcesDirectory ? [{ from: bundle.resourcesDirectory, to: path.basename(bundle.resourcesDirectory) }] : []
|
|
309
|
-
],
|
|
309
|
+
extraResources: bundle.nativeDirectory || bundle.resourcesDirectory ? [{ from: extra }] : [],
|
|
310
310
|
//The dependencies are bundled
|
|
311
311
|
npmRebuild: false,
|
|
312
312
|
...platform === 'linux' ? { linux: { maintainer: maintainer, ...config.vendor ? { vendor: config.vendor } : {} } } : {},
|
package/dist/esm/lib/mac/Mac.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmod, cp, mkdir, rm, stat, symlink, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { chmod, cp, 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
4
|
import { runCommand } from '../Processes.js';
|
|
@@ -66,7 +66,7 @@ ${service.user === 'root' ? '' : ` <key>UserName</key>
|
|
|
66
66
|
`} <key>WorkingDirectory</key>
|
|
67
67
|
<string>${xml(paths.data)}</string>
|
|
68
68
|
<key>RunAtLoad</key>
|
|
69
|
-
|
|
69
|
+
<true/>
|
|
70
70
|
<key>KeepAlive</key>
|
|
71
71
|
${keepAlive}
|
|
72
72
|
<key>ThrottleInterval</key>
|
|
@@ -94,8 +94,8 @@ function installerHooks(plan) {
|
|
|
94
94
|
*/
|
|
95
95
|
const ACTION_FILE = '"$INSTALLER_TEMP/lakutata-action"';
|
|
96
96
|
/**
|
|
97
|
-
* The script run before the installation (and the upgrades), when the application has scripts: what the
|
|
98
|
-
* does (an upgrade replaces the program installed), the preinstall script of the application
|
|
97
|
+
* The script run before the installation (and the upgrades), when the application has scripts or a service: what the
|
|
98
|
+
* installation does (an upgrade replaces the program installed), the preinstall script of the application
|
|
99
99
|
* @param plan
|
|
100
100
|
*/
|
|
101
101
|
function preInstall(plan) {
|
|
@@ -122,11 +122,9 @@ function postInstall(plan) {
|
|
|
122
122
|
'#!/bin/sh',
|
|
123
123
|
'set -e',
|
|
124
124
|
...service ? serviceInstallation(plan, service) : [],
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
] : [],
|
|
129
|
-
...service ? serviceStart(plan) : [],
|
|
125
|
+
`lakutata_action="$(cat ${ACTION_FILE} 2>/dev/null || echo install)"`,
|
|
126
|
+
...hooks.postinstall ? embeddedShell(hooks.postinstall, plan.prefix, '"$lakutata_action"') : [],
|
|
127
|
+
...service ? serviceStart(plan, service) : [],
|
|
130
128
|
''
|
|
131
129
|
].join('\n');
|
|
132
130
|
}
|
|
@@ -175,19 +173,32 @@ function serviceInstallation(plan, service) {
|
|
|
175
173
|
];
|
|
176
174
|
}
|
|
177
175
|
/**
|
|
178
|
-
* The service loaded again
|
|
176
|
+
* The service started: enabled, loaded again (started by RunAtLoad, the one of the version replaced stopped first). A
|
|
177
|
+
* service which does not start with the system (autoStart false) is disabled by its installation (launchd would load
|
|
178
|
+
* it when the system starts, and run it), and restarted by the upgrades when it runs, as systemd does
|
|
179
179
|
* @param plan
|
|
180
|
+
* @param service
|
|
180
181
|
*/
|
|
181
|
-
function serviceStart(plan) {
|
|
182
|
+
function serviceStart(plan, service) {
|
|
182
183
|
const paths = installPaths(plan);
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
//
|
|
184
|
+
const load = [
|
|
185
|
+
//bootout returns before the job of the version replaced is unloaded (the application ending gracefully,
|
|
186
|
+
//ExitTimeOut): bootstrap fails until it is
|
|
186
187
|
`launchctl bootout system/${plan.appId} 2>/dev/null || true`,
|
|
187
188
|
'waited=0',
|
|
188
189
|
`while launchctl print system/${plan.appId} >/dev/null 2>&1 && [ "$waited" -lt 70 ]; do sleep 0.5; waited=$((waited + 1)); done`,
|
|
189
190
|
`launchctl bootstrap system ${paths.daemon}`
|
|
190
191
|
];
|
|
192
|
+
if (service.autoStart)
|
|
193
|
+
return [`launchctl enable system/${plan.appId}`, ...load];
|
|
194
|
+
return [
|
|
195
|
+
`if launchctl print system/${plan.appId} >/dev/null 2>&1; then`,
|
|
196
|
+
...load.map((line) => ` ${line}`),
|
|
197
|
+
'elif [ "$lakutata_action" = install ]; then',
|
|
198
|
+
//Started by its administrator: sudo launchctl enable system/<appId>, then sudo launchctl bootstrap system <plist>
|
|
199
|
+
` launchctl disable system/${plan.appId}`,
|
|
200
|
+
'fi'
|
|
201
|
+
];
|
|
191
202
|
}
|
|
192
203
|
/**
|
|
193
204
|
* The script removing the application (the packages of macOS are not removed by the system): its service stopped, the
|
|
@@ -247,7 +258,7 @@ export async function buildMacPackages(plan, targets, work, outdir) {
|
|
|
247
258
|
await writeFile(path.join(root, 'Library', 'LaunchDaemons', `${plan.appId}.plist`), launchDaemon(plan, plan.service), { mode: 0o644 });
|
|
248
259
|
}
|
|
249
260
|
const hooks = installerHooks(plan);
|
|
250
|
-
if (hooks.preinstall || hooks.postinstall) {
|
|
261
|
+
if (hooks.preinstall || hooks.postinstall || plan.service) {
|
|
251
262
|
await writeFile(path.join(scripts, 'preinstall'), preInstall(plan));
|
|
252
263
|
await chmod(path.join(scripts, 'preinstall'), 0o755);
|
|
253
264
|
}
|
|
@@ -261,7 +272,15 @@ export async function buildMacPackages(plan, targets, work, outdir) {
|
|
|
261
272
|
throw new Error(`The pkg package cannot be made: ${built.output.trim()}`);
|
|
262
273
|
const file = path.join(outdir, `${plan.name}-${plan.version}-${packageArch()}.pkg`);
|
|
263
274
|
await rm(file, { force: true });
|
|
264
|
-
|
|
275
|
+
//Installed on the Macs of the architecture of the program only (productbuild gives both)
|
|
276
|
+
const distribution = path.join(work, 'distribution.xml');
|
|
277
|
+
const synthesized = await runCommand('productbuild', ['--synthesize', '--package', component, distribution], work);
|
|
278
|
+
if (synthesized.code !== 0)
|
|
279
|
+
throw new Error(`The pkg package cannot be made: ${synthesized.output.trim()}`);
|
|
280
|
+
const architectures = `hostArchitectures="${packageArch() === 'arm64' ? 'arm64' : 'x86_64'}"`;
|
|
281
|
+
const options = await readFile(distribution, 'utf8');
|
|
282
|
+
await writeFile(distribution, /hostArchitectures="[^"]*"/.test(options) ? options.replace(/hostArchitectures="[^"]*"/, architectures) : options.replace('<options ', `<options ${architectures} `));
|
|
283
|
+
const product = await runCommand('productbuild', ['--distribution', distribution, '--package-path', work, '--identifier', plan.appId, '--version', plan.version, file], work);
|
|
265
284
|
if (product.code !== 0)
|
|
266
285
|
throw new Error(`The pkg package cannot be made: ${product.output.trim()}`);
|
|
267
286
|
files.push({ target: target, file: file, size: (await stat(file)).size });
|
|
@@ -32,6 +32,11 @@ export type BundleOptions = {
|
|
|
32
32
|
* The runtime running the bundle: node (by default), or bun (the executables of Bun, which bundle it again)
|
|
33
33
|
*/
|
|
34
34
|
runtime?: 'node' | 'bun';
|
|
35
|
+
/**
|
|
36
|
+
* Whether the bundle is the one of an executable (buildExecutable): it runs the scripts of its tree it is started
|
|
37
|
+
* for, in both layouts, and on Node.js it loads the modules left out of it from next to the executable
|
|
38
|
+
*/
|
|
39
|
+
executable?: boolean;
|
|
35
40
|
/**
|
|
36
41
|
* The directories of the native tree and of the resources (when they are not embedded), next to the bundle by
|
|
37
42
|
* default (<name>.native and <name>.resources): the bundle loads them by their names from its directory
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type BundleOptions } from './Bundle.js';
|
|
2
|
+
import { type NativeFile } from './Native.js';
|
|
2
3
|
export type ExecutableOptions = Omit<BundleOptions, 'outfile' | 'native' | 'directories'> & {
|
|
3
4
|
/**
|
|
4
5
|
* The directory of the executable, dist by default
|
|
@@ -49,6 +50,13 @@ export type ExecutableResult = {
|
|
|
49
50
|
*/
|
|
50
51
|
warnings: string[];
|
|
51
52
|
};
|
|
53
|
+
/**
|
|
54
|
+
* The packages of a native tree whose addons are built on the V8 API of Node.js (they export
|
|
55
|
+
* node_register_module_v<ABI>), not on Node-API (napi_register_module_v1): Bun may not load them (better-sqlite3)
|
|
56
|
+
* @param files The files of the native tree.
|
|
57
|
+
* @returns The names of the packages (or of the addons of the application itself), sorted.
|
|
58
|
+
*/
|
|
59
|
+
export declare function v8Addons(files: NativeFile[]): string[];
|
|
52
60
|
/**
|
|
53
61
|
* Make the executable of the application for the platform it is made on, which runs without Node.js installed: its bundle
|
|
54
62
|
* embedded in the Node.js binary running the tool (the single executable applications of Node.js), or compiled by Bun.
|
|
@@ -29,6 +29,12 @@ export type NativeRuntimeConfig = {
|
|
|
29
29
|
* .lakutata/workers): the tree is not looked for (nor extracted)
|
|
30
30
|
*/
|
|
31
31
|
root?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Whether the bundle is the one of an executable, which its process.execPath runs (in both layouts): it runs the
|
|
34
|
+
* scripts of its tree it is started for (fork()), and an executable of Node.js loads the modules left out of the
|
|
35
|
+
* bundle (--external) from next to it
|
|
36
|
+
*/
|
|
37
|
+
executable?: boolean;
|
|
32
38
|
};
|
|
33
39
|
/**
|
|
34
40
|
* The global symbol of the packed applications giving the directory of their resources (resourcesPath of lakutata)
|
|
@@ -48,7 +48,7 @@ export type PlatformScripts = {
|
|
|
48
48
|
* The scripts of the installers made on a platform: the ones of the platform (lakutata.installer.<platform>.scripts),
|
|
49
49
|
* replaced by the ones of each installer (lakutata.installer.<platform>.<installer>.scripts), the native ones of the
|
|
50
50
|
* packages of Linux (nativeScripts), the file of NSIS included by its installer (win.nsis.include). The ones an
|
|
51
|
-
* installer does not run are told (warnings)
|
|
51
|
+
* installer does not run are told (warnings); the options of the installers not made are checked too
|
|
52
52
|
* @param project
|
|
53
53
|
* @param platform its name in the configuration: linux, mac or win
|
|
54
54
|
* @param config its configuration
|
package/package.json
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lakutata/builder",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "The builder of the applications of the lakutata framework: their bundles, their executables (Node.js, Bun) with their native addons and their resources.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"lakutata",
|
|
7
|
+
"bundler",
|
|
8
|
+
"esbuild",
|
|
9
|
+
"executable",
|
|
10
|
+
"single-executable-application",
|
|
11
|
+
"bun",
|
|
12
|
+
"native-addons",
|
|
13
|
+
"electron-builder",
|
|
14
|
+
"installer",
|
|
15
|
+
"typescript"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://lakutata.com/packages/builder",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/lakutata/lakutata/issues",
|
|
20
|
+
"email": "admin@lakutata.com"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/lakutata/lakutata.git",
|
|
25
|
+
"directory": "packages/builder"
|
|
26
|
+
},
|
|
27
|
+
"author": "alex.ma",
|
|
5
28
|
"type": "module",
|
|
6
29
|
"main": "./dist/esm/index.js",
|
|
7
30
|
"types": "./dist/types/index.d.ts",
|
|
@@ -37,7 +60,7 @@
|
|
|
37
60
|
}
|
|
38
61
|
},
|
|
39
62
|
"engines": {
|
|
40
|
-
"node": "
|
|
63
|
+
"node": "^22.13.0 || >=24.11.0"
|
|
41
64
|
},
|
|
42
65
|
"license": "MIT",
|
|
43
66
|
"files": [
|