@lakutata/builder 3.1.5 → 3.1.6
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 +13 -0
- package/README.zh-CN.md +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/lib/Installer.js +3 -3
- package/dist/esm/lib/NativeRuntime.js +25 -3
- package/dist/esm/lib/Processes.js +3 -2
- package/dist/esm/lib/Tools.js +98 -19
- package/dist/esm/lib/linux/Linux.js +3 -2
- package/dist/esm/lib/mobile/Addons.js +675 -0
- package/dist/esm/lib/mobile/Node.js +190 -0
- package/dist/esm/lib/mobile/Stage.js +180 -0
- package/dist/esm/lib/nwjs/Addons.js +7 -4
- package/dist/esm/lib/nwjs/Package.js +1 -1
- package/dist/esm/lib/nwjs/Runtime.js +5 -31
- package/dist/esm/lib/win/Windows.js +5 -4
- package/dist/types/index.d.ts +3 -0
- package/dist/types/lib/Installer.d.ts +4 -0
- package/dist/types/lib/Processes.d.ts +2 -1
- package/dist/types/lib/Tools.d.ts +47 -1
- package/dist/types/lib/linux/Linux.d.ts +2 -1
- package/dist/types/lib/mobile/Addons.d.ts +154 -0
- package/dist/types/lib/mobile/Node.d.ts +89 -0
- package/dist/types/lib/mobile/Stage.d.ts +111 -0
- package/dist/types/lib/nwjs/Addons.d.ts +18 -0
- package/dist/types/lib/nwjs/Package.d.ts +4 -0
- package/dist/types/lib/nwjs/Runtime.d.ts +4 -0
- package/dist/types/lib/win/Windows.d.ts +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { cp, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { runCommand } from '../Processes.js';
|
|
5
|
+
import { cacheDirectory, downloadFile, moveIntoCache } from '../Tools.js';
|
|
6
|
+
/**
|
|
7
|
+
* The version of Node.js of the mobile applications when their libnode does not tell it: the one of the release of
|
|
8
|
+
* lakutata/libnode they download (MOBILE_NODE_RELEASE)
|
|
9
|
+
*/
|
|
10
|
+
export const MOBILE_NODE_VERSION = '24.20.0';
|
|
11
|
+
/**
|
|
12
|
+
* The architectures of Android of a libnode built for them (the arch of Node.js names the native tree of the
|
|
13
|
+
* architecture, the target names the compilers of the NDK)
|
|
14
|
+
*/
|
|
15
|
+
export const ANDROID_ABIS = {
|
|
16
|
+
'arm64-v8a': { arch: 'arm64', target: 'aarch64-linux-android' },
|
|
17
|
+
'armeabi-v7a': { arch: 'arm', target: 'armv7a-linux-androideabi' },
|
|
18
|
+
'x86_64': { arch: 'x64', target: 'x86_64-linux-android' }
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The version of Node.js of a libnode: the one of its headers (include/node/node_version.h)
|
|
22
|
+
* @param directory the export of the libnode
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export function mobileNodeVersion(directory) {
|
|
26
|
+
const header = readFileSync(path.join(directory, 'include', 'node', 'node_version.h'), 'utf8');
|
|
27
|
+
const part = (name) => new RegExp(`#define NODE_${name}_VERSION (\\d+)`).exec(header)?.[1];
|
|
28
|
+
const [major, minor, patch] = [part('MAJOR'), part('MINOR'), part('PATCH')];
|
|
29
|
+
if (!major || !minor || !patch)
|
|
30
|
+
throw new Error(`${directory} holds no version of Node.js (include/node/node_version.h)`);
|
|
31
|
+
return `${major}.${minor}.${patch}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The architectures of Android a libnode is built for (libs/<abi>/libnode.so)
|
|
35
|
+
* @param directory the export of the libnode of Android
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
export function mobileNodeAbis(directory) {
|
|
39
|
+
return Object.keys(ANDROID_ABIS).filter((abi) => existsSync(path.join(directory, 'libs', abi, 'libnode.so')));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check the export of a libnode of a system: its headers, and its libraries (Android: libs/<abi>/libnode.so, HarmonyOS:
|
|
43
|
+
* libs/arm64-v8a/libnode.so) or its framework (iOS: NodeMobile.xcframework)
|
|
44
|
+
* @param platform
|
|
45
|
+
* @param directory
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export function checkMobileNode(platform, directory) {
|
|
49
|
+
const missing = (what) => new Error(`${directory} is not the libnode of ${platform} (${what} missing): download it (lakutata mobile node ${platform})`);
|
|
50
|
+
if (!existsSync(path.join(directory, 'include', 'node', 'node_api.h')))
|
|
51
|
+
throw missing('include/node/node_api.h');
|
|
52
|
+
if (platform === 'ios' && !existsSync(path.join(directory, 'NodeMobile.xcframework', 'Info.plist')))
|
|
53
|
+
throw missing('NodeMobile.xcframework');
|
|
54
|
+
if (platform === 'android' && !mobileNodeAbis(directory).length)
|
|
55
|
+
throw missing('libs/<abi>/libnode.so');
|
|
56
|
+
if (platform === 'harmony' && !existsSync(path.join(directory, 'libs', 'arm64-v8a', 'libnode.so')))
|
|
57
|
+
throw missing('libs/arm64-v8a/libnode.so');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The release of lakutata/libnode the mobile applications download their libnode from: its tag is v<release> (the
|
|
61
|
+
* version of Node.js, then -<revision> for a release of the same version built again)
|
|
62
|
+
*/
|
|
63
|
+
export const MOBILE_NODE_RELEASE = '24.20.0';
|
|
64
|
+
/**
|
|
65
|
+
* The architectures of HarmonyOS the mobile applications ship (the libs/arm64-v8a of their HAP)
|
|
66
|
+
*/
|
|
67
|
+
const HARMONY_ABIS = { 'arm64-v8a': { arch: 'arm64' } };
|
|
68
|
+
/**
|
|
69
|
+
* Where the releases of lakutata/libnode are downloaded from: LAKUTATA_LIBNODE_MIRROR (a mirror of them, its files laid
|
|
70
|
+
* out as the ones of GitHub: v<release>/<archive>), else the releases of the repository on GitHub
|
|
71
|
+
*/
|
|
72
|
+
function libnodeMirror() {
|
|
73
|
+
return (process.env.LAKUTATA_LIBNODE_MIRROR ?? 'https://github.com/lakutata/libnode/releases/download').replace(/\/+$/, '');
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* An archive of the release, downloaded once into the cache (~/.cache/lakutata/mobile/libnode, LAKUTATA_CACHE_DIR),
|
|
77
|
+
* checked with the SHA256SUMS of the release and extracted
|
|
78
|
+
* @param name the archive without its extension: libnode_<platform>_<arch>
|
|
79
|
+
* @param sums the sha256 of the archives of the release, by their names
|
|
80
|
+
* @param progress
|
|
81
|
+
* @returns the directory extracted: lib/libnode.so or NodeMobile.xcframework, include/node, LICENSE, libnode.json
|
|
82
|
+
*/
|
|
83
|
+
async function libnodeArchive(name, sums, progress) {
|
|
84
|
+
const directory = path.join(cacheDirectory(), 'mobile', 'libnode', MOBILE_NODE_RELEASE, name);
|
|
85
|
+
if (existsSync(path.join(directory, '.complete')))
|
|
86
|
+
return directory;
|
|
87
|
+
const archive = `${name}.tar.gz`;
|
|
88
|
+
const url = `${libnodeMirror()}/v${MOBILE_NODE_RELEASE}/${archive}`;
|
|
89
|
+
const sum = (await sums()).get(archive);
|
|
90
|
+
if (!sum)
|
|
91
|
+
throw new Error(`The release ${MOBILE_NODE_RELEASE} of libnode has no ${archive} in its SHA256SUMS`);
|
|
92
|
+
progress?.(`${archive} (libnode ${MOBILE_NODE_RELEASE})`);
|
|
93
|
+
//Extracted aside, then moved into the cache: the builds running together do not see it partly written
|
|
94
|
+
await mkdir(path.dirname(directory), { recursive: true });
|
|
95
|
+
const temporary = await mkdtemp(`${directory}.`);
|
|
96
|
+
try {
|
|
97
|
+
const file = path.join(temporary, archive);
|
|
98
|
+
const downloaded = await downloadFile(url, file, { progress: progress }).catch((error) => {
|
|
99
|
+
throw new Error(`${error.message}: a mirror of the releases of lakutata/libnode is given by LAKUTATA_LIBNODE_MIRROR, a libnode of your own by LAKUTATA_MOBILE_NODE_DIR`);
|
|
100
|
+
});
|
|
101
|
+
if (downloaded !== sum)
|
|
102
|
+
throw new Error(`The ${archive} downloaded is not the one of the release ${MOBILE_NODE_RELEASE} of libnode (sha256 ${downloaded}, ${sum} expected)`);
|
|
103
|
+
const extracted = await runCommand('tar', ['-xf', archive], temporary);
|
|
104
|
+
if (extracted.code !== 0)
|
|
105
|
+
throw new Error(`${archive} cannot be extracted: ${extracted.output.trim()}`);
|
|
106
|
+
await rm(file);
|
|
107
|
+
const root = path.join(temporary, name);
|
|
108
|
+
if (!existsSync(path.join(root, 'include', 'node', 'node_api.h')))
|
|
109
|
+
throw new Error(`${archive} has no ${name}/include/node`);
|
|
110
|
+
await writeFile(path.join(root, '.complete'), '');
|
|
111
|
+
await moveIntoCache(root, directory, () => existsSync(path.join(directory, '.complete')));
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
await rm(temporary, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
return directory;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Download the libnode of a mobile system from the releases of lakutata/libnode (LAKUTATA_LIBNODE_MIRROR: a mirror of
|
|
120
|
+
* them), once: its archives are checked with the SHA256SUMS of the release and kept in the cache
|
|
121
|
+
* (~/.cache/lakutata/mobile, LAKUTATA_CACHE_DIR). LAKUTATA_MOBILE_NODE_DIR gives exports of a libnode of your own instead
|
|
122
|
+
* (a directory per system: android, ios, harmony; built with the repository of lakutata/libnode)
|
|
123
|
+
* @param options
|
|
124
|
+
* @returns the directory of the export: its headers (include/node) and its libraries (libs/<abi>/libnode.so) or its
|
|
125
|
+
* framework (NodeMobile.xcframework)
|
|
126
|
+
* @example
|
|
127
|
+
* import {downloadMobileNode} from '@lakutata/builder'
|
|
128
|
+
*
|
|
129
|
+
* const directory = await downloadMobileNode({platform: 'android', abis: ['arm64-v8a'], progress: console.log})
|
|
130
|
+
* console.log(directory)
|
|
131
|
+
*/
|
|
132
|
+
export async function downloadMobileNode(options) {
|
|
133
|
+
const given = process.env.LAKUTATA_MOBILE_NODE_DIR;
|
|
134
|
+
if (given) {
|
|
135
|
+
const directory = path.resolve(given, options.platform);
|
|
136
|
+
checkMobileNode(options.platform, directory);
|
|
137
|
+
return directory;
|
|
138
|
+
}
|
|
139
|
+
//The archives by architecture: the ABI of their library in the export
|
|
140
|
+
const archives = options.platform === 'ios'
|
|
141
|
+
? [['libnode_ios_arm64', undefined]]
|
|
142
|
+
: options.platform === 'harmony'
|
|
143
|
+
? Object.entries(HARMONY_ABIS).map(([abi, { arch }]) => [`libnode_harmony_${arch}`, abi])
|
|
144
|
+
: [...new Set(options.abis ?? ['arm64-v8a', 'x86_64'])].map((abi) => {
|
|
145
|
+
if (!ANDROID_ABIS[abi])
|
|
146
|
+
throw new Error(`${abi} is not an architecture of Android (${Object.keys(ANDROID_ABIS).join(', ')})`);
|
|
147
|
+
return [`libnode_android_${ANDROID_ABIS[abi].arch}`, abi];
|
|
148
|
+
});
|
|
149
|
+
const target = path.join(cacheDirectory(), 'mobile', 'node', MOBILE_NODE_RELEASE, [options.platform, ...archives.map(([, abi]) => abi ?? '')].filter(Boolean).join('-'));
|
|
150
|
+
if (existsSync(path.join(target, '.complete')))
|
|
151
|
+
return target;
|
|
152
|
+
//The checksums of the release, read once and only when an archive is downloaded
|
|
153
|
+
let sums;
|
|
154
|
+
const checksums = () => sums ??= (async () => {
|
|
155
|
+
const response = await fetch(`${libnodeMirror()}/v${MOBILE_NODE_RELEASE}/SHA256SUMS`).catch((error) => {
|
|
156
|
+
throw new Error(`The checksums of the release ${MOBILE_NODE_RELEASE} of libnode cannot be downloaded (${error.message}): a mirror of the releases of lakutata/libnode is given by LAKUTATA_LIBNODE_MIRROR`);
|
|
157
|
+
});
|
|
158
|
+
if (!response.ok)
|
|
159
|
+
throw new Error(`The checksums of the release ${MOBILE_NODE_RELEASE} of libnode cannot be downloaded (${response.url}: ${response.status})`);
|
|
160
|
+
return new Map((await response.text()).split('\n').map((line) => /^([0-9a-f]{64})\s+\*?(\S+)$/.exec(line.trim())).filter((match) => !!match).map((match) => [match[2], match[1]]));
|
|
161
|
+
})();
|
|
162
|
+
const extracted = [];
|
|
163
|
+
for (const [name, abi] of archives)
|
|
164
|
+
extracted.push([await libnodeArchive(name, checksums, options.progress), abi]);
|
|
165
|
+
//The export: the headers and the description of the first archive, the library of each architecture
|
|
166
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
167
|
+
const temporary = await mkdtemp(`${target}.`);
|
|
168
|
+
try {
|
|
169
|
+
const [first] = extracted[0];
|
|
170
|
+
await cp(path.join(first, 'include'), path.join(temporary, 'include'), { recursive: true });
|
|
171
|
+
for (const file of ['LICENSE', 'libnode.json'])
|
|
172
|
+
if (existsSync(path.join(first, file)))
|
|
173
|
+
await cp(path.join(first, file), path.join(temporary, file));
|
|
174
|
+
for (const [directory, abi] of extracted) {
|
|
175
|
+
if (abi) {
|
|
176
|
+
await mkdir(path.join(temporary, 'libs', abi), { recursive: true });
|
|
177
|
+
await cp(path.join(directory, 'lib', 'libnode.so'), path.join(temporary, 'libs', abi, 'libnode.so'));
|
|
178
|
+
}
|
|
179
|
+
else
|
|
180
|
+
await cp(path.join(directory, 'NodeMobile.xcframework'), path.join(temporary, 'NodeMobile.xcframework'), { recursive: true, verbatimSymlinks: true });
|
|
181
|
+
}
|
|
182
|
+
checkMobileNode(options.platform, temporary);
|
|
183
|
+
await writeFile(path.join(temporary, '.complete'), '');
|
|
184
|
+
await moveIntoCache(temporary, target, () => existsSync(path.join(target, '.complete')));
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
await rm(temporary, { recursive: true, force: true });
|
|
188
|
+
}
|
|
189
|
+
return target;
|
|
190
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { lstat, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { bundleProject } from '../Bundle.js';
|
|
5
|
+
import { packNativeTree, writeNativeTree } from '../Native.js';
|
|
6
|
+
import { BUN_NATIVE_ASSETS } from '../NativeRuntime.js';
|
|
7
|
+
import { MOBILE_ADDONS_FILE, prepareAndroidAddons, prepareHarmonyAddons, prepareIosAddons } from './Addons.js';
|
|
8
|
+
import { checkMobileNode, MOBILE_NODE_VERSION, mobileNodeVersion } from './Node.js';
|
|
9
|
+
/**
|
|
10
|
+
* The package of a file of the native tree: the last one of the node_modules in its path
|
|
11
|
+
* @param file
|
|
12
|
+
*/
|
|
13
|
+
function packageOf(file) {
|
|
14
|
+
const parts = file.split('/');
|
|
15
|
+
const index = parts.lastIndexOf('node_modules');
|
|
16
|
+
if (index < 0)
|
|
17
|
+
return file;
|
|
18
|
+
return parts.slice(index + 1, parts[index + 1]?.startsWith('@') ? index + 3 : index + 2).join('/');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The files of the embedded native tree of HarmonyOS, next to the entry (names without a leading dot: the resources of
|
|
22
|
+
* the HAP leave those out)
|
|
23
|
+
*/
|
|
24
|
+
const NATIVE_DATA = 'native.gz';
|
|
25
|
+
const NATIVE_INDEX = 'native.json';
|
|
26
|
+
/**
|
|
27
|
+
* The entry of the Node.js project: the fetch() of Node.js put away without WebAssembly (V8 runs --jitless on
|
|
28
|
+
* HarmonyOS: its first call would throw an uncaught exception). The native tree embedded in the project (HarmonyOS) is
|
|
29
|
+
* given to the bundle, which extracts it at its first run into the directory LAKUTATA_NATIVE_DIR gives (the native
|
|
30
|
+
* module sets it). The addons of iOS and HarmonyOS are loaded from where the system keeps them (MOBILE_ADDONS_FILE: the
|
|
31
|
+
* frameworks of the application, the libraries of the HAP), in place of their placeholders in the tree. Then the
|
|
32
|
+
* application, bundled
|
|
33
|
+
*/
|
|
34
|
+
const ENTRY_SCRIPT = `//The entry of the application in the Node.js of the mobile application: fetch() put away without WebAssembly, the native tree embedded (HarmonyOS) given to the bundle, the native addons of iOS and HarmonyOS loaded from where the system keeps them, then the application, bundled
|
|
35
|
+
const fs = require('fs')
|
|
36
|
+
const path = require('path')
|
|
37
|
+
if (typeof WebAssembly === 'undefined') delete globalThis.fetch
|
|
38
|
+
const tree = path.join(__dirname, '${NATIVE_INDEX}')
|
|
39
|
+
if (fs.existsSync(tree)) globalThis[Symbol.for('${BUN_NATIVE_ASSETS}')] = {data: path.join(__dirname, '${NATIVE_DATA}'), index: tree}
|
|
40
|
+
const addons = path.join(__dirname, '${MOBILE_ADDONS_FILE}')
|
|
41
|
+
if (fs.existsSync(addons)) {
|
|
42
|
+
//The placeholders are keyed by their paths in the tree, wherever the tree is (next to the bundle, extracted)
|
|
43
|
+
const {directory, files} = JSON.parse(fs.readFileSync(addons, 'utf8'))
|
|
44
|
+
const base = path.resolve(fs.realpathSync(__dirname), directory)
|
|
45
|
+
const keys = Object.keys(files)
|
|
46
|
+
const dlopen = process.dlopen
|
|
47
|
+
process.dlopen = function (module, filename, ...rest) {
|
|
48
|
+
let real
|
|
49
|
+
try {
|
|
50
|
+
real = fs.realpathSync(filename).split(path.sep).join('/')
|
|
51
|
+
} catch {
|
|
52
|
+
real = undefined
|
|
53
|
+
}
|
|
54
|
+
const key = real && keys.find((key) => real.endsWith('/' + key))
|
|
55
|
+
return dlopen.call(this, module, key ? path.join(base, files[key]) : filename, ...rest)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
require('./app.cjs')
|
|
59
|
+
`;
|
|
60
|
+
/**
|
|
61
|
+
* The files of a native tree written in a directory, its links left out (they stay the links of the tree)
|
|
62
|
+
* @param directory
|
|
63
|
+
* @param base
|
|
64
|
+
*/
|
|
65
|
+
async function treeFiles(directory, base = directory) {
|
|
66
|
+
const files = [];
|
|
67
|
+
for (const entry of (await readdir(directory, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
68
|
+
const file = path.join(directory, entry.name);
|
|
69
|
+
if (entry.isSymbolicLink())
|
|
70
|
+
continue;
|
|
71
|
+
if (entry.isDirectory())
|
|
72
|
+
files.push(...await treeFiles(file, base));
|
|
73
|
+
else if (entry.isFile())
|
|
74
|
+
files.push({ path: path.relative(base, file).split(path.sep).join('/'), source: file, mode: (await lstat(file)).mode & 0o777 });
|
|
75
|
+
}
|
|
76
|
+
return files;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Make the Node.js project of a mobile application of React Native (Android, iOS, HarmonyOS), which the native module
|
|
80
|
+
* of the application runs in its libnode: the bundle of the application (CommonJS, for the version of Node.js of the
|
|
81
|
+
* libnode) and its resources (lakutata.resources), its manifest and its entry. The bundle holds the dependencies: the
|
|
82
|
+
* project is packed into the application as it is. The native addons are built for the system from the sources of their
|
|
83
|
+
* packages, against the headers and the libraries of the libnode: on Android, a native tree per architecture (node-gyp
|
|
84
|
+
* and the NDK of Android); on iOS, a framework per addon for each SDK (node-gyp and Xcode), in the directory of the
|
|
85
|
+
* frameworks the project of iOS embeds; on HarmonyOS, a library per addon (node-gyp and the native SDK of HarmonyOS),
|
|
86
|
+
* in the libraries of the HAP
|
|
87
|
+
* @param options
|
|
88
|
+
* @example
|
|
89
|
+
* import {stageMobileApp} from '@lakutata/builder'
|
|
90
|
+
*
|
|
91
|
+
* //In packages/app of a project of the mobile-react template, before the application of Android is built
|
|
92
|
+
* const stage = await stageMobileApp({
|
|
93
|
+
* platform: 'android',
|
|
94
|
+
* outdir: '../../android/app/src/main/assets/nodejs',
|
|
95
|
+
* node: '../ipc/native/node/out/android'
|
|
96
|
+
* })
|
|
97
|
+
* console.log(stage.directory, stage.node)
|
|
98
|
+
*/
|
|
99
|
+
export async function stageMobileApp(options) {
|
|
100
|
+
const project = path.resolve(options.project ?? process.cwd());
|
|
101
|
+
const manifestFile = path.join(project, 'package.json');
|
|
102
|
+
if (!existsSync(manifestFile))
|
|
103
|
+
throw new Error(`${project} has no package.json`);
|
|
104
|
+
const manifest = JSON.parse(await readFile(manifestFile, 'utf8'));
|
|
105
|
+
const directory = path.resolve(project, options.outdir);
|
|
106
|
+
//The directory is made again: never the project or a directory holding it
|
|
107
|
+
if (!path.relative(directory, project).startsWith('..'))
|
|
108
|
+
throw new Error(`The Node.js project of the mobile application is made in a directory of its own, not ${directory}`);
|
|
109
|
+
const node = options.node ? path.resolve(project, options.node) : undefined;
|
|
110
|
+
if (node)
|
|
111
|
+
checkMobileNode(options.platform, node);
|
|
112
|
+
const version = node ? mobileNodeVersion(node) : MOBILE_NODE_VERSION;
|
|
113
|
+
await rm(directory, { recursive: true, force: true });
|
|
114
|
+
await mkdir(directory, { recursive: true });
|
|
115
|
+
//The frameworks of the addons of iOS made before for the SDKs built: made again, or none (the ones of the other SDK
|
|
116
|
+
//kept: the development builds for the simulators only); the libraries of the addons of HarmonyOS made before
|
|
117
|
+
const frameworks = options.platform === 'ios' && options.frameworks ? path.resolve(project, options.frameworks) : undefined;
|
|
118
|
+
if (frameworks)
|
|
119
|
+
for (const sdk of options.sdks?.length ? options.sdks : ['iphoneos', 'iphonesimulator'])
|
|
120
|
+
await rm(path.join(frameworks, sdk), { recursive: true, force: true });
|
|
121
|
+
const libraries = options.platform === 'harmony' && options.libraries ? path.resolve(project, options.libraries) : undefined;
|
|
122
|
+
if (libraries && existsSync(libraries)) {
|
|
123
|
+
for (const name of await readdir(libraries))
|
|
124
|
+
if (/^libaddon-.*\.so$/.test(name))
|
|
125
|
+
await rm(path.join(libraries, name), { force: true });
|
|
126
|
+
}
|
|
127
|
+
//The resources of the HAP of HarmonyOS leave out the files whose names start with a dot (the .lakutata of the native
|
|
128
|
+
//tree): there the tree is embedded, packed into the project, extracted at run time
|
|
129
|
+
const harmony = options.platform === 'harmony';
|
|
130
|
+
const bundle = await bundleProject({
|
|
131
|
+
project: project,
|
|
132
|
+
entry: options.entry,
|
|
133
|
+
outfile: path.join(directory, 'app.cjs'),
|
|
134
|
+
node: version,
|
|
135
|
+
minify: options.minify ?? true,
|
|
136
|
+
external: ['@lakutata/netif', ...options.external ?? []],
|
|
137
|
+
native: harmony ? 'embedded' : 'directory',
|
|
138
|
+
directories: { native: 'app.native', resources: 'resources' }
|
|
139
|
+
});
|
|
140
|
+
//The tree its addons are built in: next to the bundle, or written there to be packed (HarmonyOS)
|
|
141
|
+
const tree = harmony && bundle.native ? path.join(directory, 'app.native') : bundle.nativeDirectory;
|
|
142
|
+
if (harmony && bundle.native)
|
|
143
|
+
await writeNativeTree(bundle.native, tree);
|
|
144
|
+
//The native tree holds the scripts of the workers too: its addons only are built
|
|
145
|
+
const files = bundle.native?.files ?? [];
|
|
146
|
+
const addonFiles = files.map((file) => file.path).filter((file) => file.endsWith('.node'));
|
|
147
|
+
let addons;
|
|
148
|
+
if (addonFiles.length) {
|
|
149
|
+
const packages = [...new Set(addonFiles.map(packageOf))].join(', ');
|
|
150
|
+
try {
|
|
151
|
+
if (!node)
|
|
152
|
+
throw new Error(`The native addons of the application are built against the libnode of ${options.platform}: give the directory of its export to the stage (node; ${packages})`);
|
|
153
|
+
if (options.platform === 'ios' && !frameworks)
|
|
154
|
+
throw new Error(`The native addons of the application are built for iOS into the frameworks the project of iOS embeds: give their directory (frameworks) to the stage (${packages})`);
|
|
155
|
+
if (options.platform === 'harmony' && !libraries)
|
|
156
|
+
throw new Error(`The native addons of the application are built for HarmonyOS into the libraries of the HAP: give their directory (libraries) to the stage (${packages})`);
|
|
157
|
+
if (options.platform === 'ios')
|
|
158
|
+
addons = await prepareIosAddons(project, directory, tree, files, frameworks, node, { sdks: options.sdks, progress: options.progress });
|
|
159
|
+
else if (options.platform === 'harmony')
|
|
160
|
+
addons = await prepareHarmonyAddons(project, directory, tree, files, libraries, node, { progress: options.progress });
|
|
161
|
+
else
|
|
162
|
+
addons = await prepareAndroidAddons(project, tree, files, node, { abis: options.abis, progress: options.progress });
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
await rm(directory, { recursive: true, force: true });
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (harmony && bundle.native) {
|
|
170
|
+
//Packed as the tree of an executable of Bun: the bundle extracts it as it is (its hash names the directory)
|
|
171
|
+
const links = bundle.native.links;
|
|
172
|
+
const packed = packNativeTree({ ...bundle.native, files: await treeFiles(tree), links: links });
|
|
173
|
+
await writeFile(path.join(directory, NATIVE_DATA), packed.data);
|
|
174
|
+
await writeFile(path.join(directory, NATIVE_INDEX), JSON.stringify(packed.index));
|
|
175
|
+
await rm(tree, { recursive: true, force: true });
|
|
176
|
+
}
|
|
177
|
+
await writeFile(path.join(directory, 'package.json'), `${JSON.stringify({ name: manifest.name ?? 'app', version: manifest.version ?? '0.0.0', private: true, main: 'main.js' }, null, 2)}\n`);
|
|
178
|
+
await writeFile(path.join(directory, 'main.js'), ENTRY_SCRIPT);
|
|
179
|
+
return { directory: directory, platform: options.platform, node: version, bundle: bundle, addons: addons };
|
|
180
|
+
}
|
|
@@ -127,8 +127,9 @@ const WINDOWS_32_NODE = '22.22.0';
|
|
|
127
127
|
/**
|
|
128
128
|
* node-gyp: the one of the project, of the builder, else the one of npm
|
|
129
129
|
* @param project
|
|
130
|
+
* @internal
|
|
130
131
|
*/
|
|
131
|
-
function nodeGyp(project) {
|
|
132
|
+
export function nodeGyp(project) {
|
|
132
133
|
for (const base of [path.join(project, 'package.json'), import.meta.url]) {
|
|
133
134
|
try {
|
|
134
135
|
return createRequire(base).resolve('node-gyp/bin/node-gyp.js');
|
|
@@ -142,7 +143,7 @@ function nodeGyp(project) {
|
|
|
142
143
|
const bundled = path.join(prefix, 'node_modules', 'npm', 'node_modules', 'node-gyp', 'bin', 'node-gyp.js');
|
|
143
144
|
if (existsSync(bundled))
|
|
144
145
|
return bundled;
|
|
145
|
-
throw new Error('node-gyp builds the addons
|
|
146
|
+
throw new Error('node-gyp builds the native addons again: install it in the project (npm install --save-dev node-gyp)');
|
|
146
147
|
}
|
|
147
148
|
/**
|
|
148
149
|
* The headers of a release of NW.js for node-gyp, in a directory of their own (--devdir) of the cache: downloaded once,
|
|
@@ -175,8 +176,9 @@ function mirror() {
|
|
|
175
176
|
/**
|
|
176
177
|
* The directory of the package of a file (the one holding its package.json)
|
|
177
178
|
* @param file
|
|
179
|
+
* @internal
|
|
178
180
|
*/
|
|
179
|
-
function packageOf(file) {
|
|
181
|
+
export function packageOf(file) {
|
|
180
182
|
for (let directory = path.dirname(file); directory !== path.dirname(directory); directory = path.dirname(directory)) {
|
|
181
183
|
if (existsSync(path.join(directory, 'package.json')))
|
|
182
184
|
return directory;
|
|
@@ -186,8 +188,9 @@ function packageOf(file) {
|
|
|
186
188
|
/**
|
|
187
189
|
* The .node files under a directory
|
|
188
190
|
* @param directory
|
|
191
|
+
* @internal
|
|
189
192
|
*/
|
|
190
|
-
async function nodeFiles(directory) {
|
|
193
|
+
export async function nodeFiles(directory) {
|
|
191
194
|
if (!existsSync(directory))
|
|
192
195
|
return [];
|
|
193
196
|
const found = [];
|
|
@@ -96,7 +96,7 @@ export async function buildNwjsApp(options) {
|
|
|
96
96
|
const work = await mkdtemp(path.join(os.tmpdir(), 'lakutata-nwjs-app-'));
|
|
97
97
|
try {
|
|
98
98
|
const stage = await stageNwjsApp({ ...options, project: project, outdir: path.join(work, 'app'), minify: options.minify ?? true });
|
|
99
|
-
const runtime = await downloadNwjs({ version: stage.release.version, flavor: 'normal', platform: options.platform, arch: options.arch });
|
|
99
|
+
const runtime = await downloadNwjs({ version: stage.release.version, flavor: 'normal', platform: options.platform, arch: options.arch, progress: options.progress });
|
|
100
100
|
const directory = path.join(path.resolve(project, options.outdir ?? path.join('dist', 'nwjs')), `${program}-${runtime.platform}`);
|
|
101
101
|
await rm(directory, { recursive: true, force: true });
|
|
102
102
|
await mkdir(directory, { recursive: true });
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
1
|
import { existsSync } from 'node:fs';
|
|
3
|
-
import { mkdir, mkdtemp,
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
3
|
import path from 'node:path';
|
|
5
4
|
import { runCommand } from '../Processes.js';
|
|
6
|
-
import { cacheDirectory } from '../Tools.js';
|
|
5
|
+
import { cacheDirectory, downloadFile, moveIntoCache } from '../Tools.js';
|
|
7
6
|
/**
|
|
8
7
|
* The oldest Node.js lakutata runs on: the releases of NW.js embedding an older one are refused
|
|
9
8
|
*/
|
|
@@ -128,28 +127,6 @@ async function checksums(version) {
|
|
|
128
127
|
}
|
|
129
128
|
return sums;
|
|
130
129
|
}
|
|
131
|
-
/**
|
|
132
|
-
* Download a file, its sha256 computed while it is written
|
|
133
|
-
* @param url
|
|
134
|
-
* @param file
|
|
135
|
-
*/
|
|
136
|
-
async function download(url, file) {
|
|
137
|
-
const response = await fetch(url);
|
|
138
|
-
if (!response.ok || !response.body)
|
|
139
|
-
throw new Error(`${url} cannot be downloaded (${response.status})`);
|
|
140
|
-
const hash = createHash('sha256');
|
|
141
|
-
const handle = await open(file, 'w');
|
|
142
|
-
try {
|
|
143
|
-
for await (const chunk of response.body) {
|
|
144
|
-
hash.update(chunk);
|
|
145
|
-
await handle.write(chunk);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
finally {
|
|
149
|
-
await handle.close();
|
|
150
|
-
}
|
|
151
|
-
return hash.digest('hex');
|
|
152
|
-
}
|
|
153
130
|
/**
|
|
154
131
|
* The executable of a runtime in its directory
|
|
155
132
|
* @param platform
|
|
@@ -185,12 +162,13 @@ export async function downloadNwjs(options) {
|
|
|
185
162
|
const sum = (await checksums(release.version)).get(archive);
|
|
186
163
|
if (!sum)
|
|
187
164
|
throw new Error(`NW.js ${release.version} has no ${archive} in its checksums`);
|
|
165
|
+
options.progress?.(`${archive} (NW.js ${release.version})`);
|
|
188
166
|
//Extracted aside, then renamed: the builds running together do not see it partly written
|
|
189
167
|
await mkdir(path.dirname(target), { recursive: true });
|
|
190
168
|
const temporary = await mkdtemp(`${target}.`);
|
|
191
169
|
try {
|
|
192
170
|
const file = path.join(temporary, archive);
|
|
193
|
-
const downloaded = await
|
|
171
|
+
const downloaded = await downloadFile(`${mirror()}/v${release.version}/${archive}`, file, { progress: options.progress });
|
|
194
172
|
if (downloaded !== sum)
|
|
195
173
|
throw new Error(`The ${archive} downloaded is not the one of NW.js ${release.version} (sha256 ${downloaded}, ${sum} expected)`);
|
|
196
174
|
//ditto keeps the links of the frameworks of macOS; the tar of Windows reads the zip archives
|
|
@@ -201,11 +179,7 @@ export async function downloadNwjs(options) {
|
|
|
201
179
|
throw new Error(`${archive} cannot be extracted: ${extracted.output.trim()}`);
|
|
202
180
|
if (!existsSync(path.join(temporary, name, executableOf(platform))))
|
|
203
181
|
throw new Error(`${archive} has no ${name}/${executableOf(platform).split(path.sep).join('/')}`);
|
|
204
|
-
await
|
|
205
|
-
//Extracted by another build
|
|
206
|
-
if (!existsSync(runtime.executable))
|
|
207
|
-
throw error;
|
|
208
|
-
});
|
|
182
|
+
await moveIntoCache(path.join(temporary, name), target, () => existsSync(runtime.executable));
|
|
209
183
|
}
|
|
210
184
|
finally {
|
|
211
185
|
await rm(temporary, { recursive: true, force: true });
|
|
@@ -470,8 +470,9 @@ async function must(command, args, cwd, what) {
|
|
|
470
470
|
* @param targets
|
|
471
471
|
* @param work
|
|
472
472
|
* @param outdir
|
|
473
|
+
* @param progress Called with what is being downloaded (WinSW, NSIS, WiX)
|
|
473
474
|
*/
|
|
474
|
-
export async function buildWindowsPackages(plan, targets, work, outdir) {
|
|
475
|
+
export async function buildWindowsPackages(plan, targets, work, outdir, progress) {
|
|
475
476
|
await mkdir(work, { recursive: true });
|
|
476
477
|
const files = [];
|
|
477
478
|
const service = plan.service;
|
|
@@ -481,7 +482,7 @@ export async function buildWindowsPackages(plan, targets, work, outdir) {
|
|
|
481
482
|
await mkdir(serviceDirectory, { recursive: true });
|
|
482
483
|
let environment;
|
|
483
484
|
if (service) {
|
|
484
|
-
await copyFile(await ensureTool('winsw'), path.join(serviceDirectory, `${installPaths(plan).wrapper}.exe`));
|
|
485
|
+
await copyFile(await ensureTool('winsw', progress), path.join(serviceDirectory, `${installPaths(plan).wrapper}.exe`));
|
|
485
486
|
await writeFile(path.join(serviceDirectory, `${installPaths(plan).wrapper}.xml`), serviceConfiguration(plan, service));
|
|
486
487
|
environment = path.join(serviceDirectory, `${plan.name}.env`);
|
|
487
488
|
await writeFile(environment, environmentFile(service));
|
|
@@ -528,14 +529,14 @@ export async function buildWindowsPackages(plan, targets, work, outdir) {
|
|
|
528
529
|
await rm(file, { force: true });
|
|
529
530
|
const script = path.join(work, 'installer.nsi');
|
|
530
531
|
await writeFile(script, `\ufeff${nsisScript(plan, stage, file, { hooks: hooks, include: include })}`);
|
|
531
|
-
await must(await ensureTool('nsis'), ['/V2', '/INPUTCHARSET', 'UTF8', script], work, 'The installer of NSIS');
|
|
532
|
+
await must(await ensureTool('nsis', progress), ['/V2', '/INPUTCHARSET', 'UTF8', script], work, 'The installer of NSIS');
|
|
532
533
|
files.push({ target: target, file: file, size: (await stat(file)).size });
|
|
533
534
|
continue;
|
|
534
535
|
}
|
|
535
536
|
//msi: compiled by candle, linked by light
|
|
536
537
|
const source = path.join(work, 'package.wxs');
|
|
537
538
|
await writeFile(source, wixSource(plan, plan.directory, serviceDirectory, environment));
|
|
538
|
-
const candle = await ensureTool('wix');
|
|
539
|
+
const candle = await ensureTool('wix', progress);
|
|
539
540
|
const light = path.join(path.dirname(candle), 'light.exe');
|
|
540
541
|
const file = path.join(outdir, `${plan.name}-${plan.version}-${packageArch(plan)}.msi`);
|
|
541
542
|
await rm(file, { force: true });
|
package/dist/types/index.d.ts
CHANGED
|
@@ -15,3 +15,6 @@ export { downloadNwjs, nwjsPlatform, nwjsRelease, nwjsReleases, type NwjsFlavor,
|
|
|
15
15
|
export { stageNwjsApp, type NwjsConfig, type NwjsStage, type NwjsStageOptions } from './lib/nwjs/Stage.js';
|
|
16
16
|
export type { NwjsAddons } from './lib/nwjs/Addons.js';
|
|
17
17
|
export { buildNwjsApp, type NwjsApp, type NwjsAppOptions } from './lib/nwjs/Package.js';
|
|
18
|
+
export { stageMobileApp, type MobileStage, type MobileStageOptions } from './lib/mobile/Stage.js';
|
|
19
|
+
export { downloadMobileNode, MOBILE_NODE_RELEASE, MOBILE_NODE_VERSION, type AndroidAbi, type MobileNodeDownloadOptions, type MobilePlatform } from './lib/mobile/Node.js';
|
|
20
|
+
export type { IosSdk, MobileAddons } from './lib/mobile/Addons.js';
|
|
@@ -179,6 +179,10 @@ export type InstallerOptions = {
|
|
|
179
179
|
* systems of Windows (Windows 7)
|
|
180
180
|
*/
|
|
181
181
|
arch?: string;
|
|
182
|
+
/**
|
|
183
|
+
* Called with what is being downloaded: the runtime of NW.js, the tools making the installers
|
|
184
|
+
*/
|
|
185
|
+
progress?: (line: string) => void;
|
|
182
186
|
};
|
|
183
187
|
export type InstallerResult = {
|
|
184
188
|
name: string;
|
|
@@ -13,9 +13,10 @@ export declare function spawnCommand(command: string, args: string[], options: S
|
|
|
13
13
|
* @param command
|
|
14
14
|
* @param args
|
|
15
15
|
* @param cwd
|
|
16
|
+
* @param env its environment, the one of the process by default
|
|
16
17
|
* @return the exit code and the output
|
|
17
18
|
*/
|
|
18
|
-
export declare function runCommand(command: string, args: string[], cwd: string): Promise<{
|
|
19
|
+
export declare function runCommand(command: string, args: string[], cwd: string, env?: NodeJS.ProcessEnv): Promise<{
|
|
19
20
|
code: number;
|
|
20
21
|
output: string;
|
|
21
22
|
}>;
|
|
@@ -22,11 +22,57 @@ declare const TOOLS: Record<'nfpm' | 'winsw' | 'nsis' | 'wix', Tool>;
|
|
|
22
22
|
* The directory of the cache of lakutata (the one of its tool): LAKUTATA_CACHE_DIR, else ~/.cache/lakutata
|
|
23
23
|
*/
|
|
24
24
|
export declare function cacheDirectory(): string;
|
|
25
|
+
/**
|
|
26
|
+
* The options of a download
|
|
27
|
+
*/
|
|
28
|
+
type DownloadOptions = {
|
|
29
|
+
/**
|
|
30
|
+
* Told when the file is downloaded again: a line naming it, read after "Downloading "
|
|
31
|
+
*/
|
|
32
|
+
progress?: (line: string) => void;
|
|
33
|
+
/**
|
|
34
|
+
* The headers of the requests
|
|
35
|
+
*/
|
|
36
|
+
headers?: Record<string, string>;
|
|
37
|
+
/**
|
|
38
|
+
* The while (milliseconds) a connection has to receive the minimum in, else it is left for another one: 30 s by
|
|
39
|
+
* default
|
|
40
|
+
*/
|
|
41
|
+
interval?: number;
|
|
42
|
+
/**
|
|
43
|
+
* The bytes a connection has to receive in each interval: 100 KB by default
|
|
44
|
+
*/
|
|
45
|
+
minimum?: number;
|
|
46
|
+
/**
|
|
47
|
+
* The connections tried: 3 by default
|
|
48
|
+
*/
|
|
49
|
+
attempts?: number;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Download a file, its sha256 computed while it is written. A connection receiving too little (one of the releases of
|
|
53
|
+
* GitHub can stay at some kilobytes a second while another one downloads the file at once) or broken is left for
|
|
54
|
+
* another one, which downloads the file again from its start
|
|
55
|
+
* @param url
|
|
56
|
+
* @param file
|
|
57
|
+
* @param options
|
|
58
|
+
*/
|
|
59
|
+
export declare function downloadFile(url: string, file: string, options?: DownloadOptions): Promise<string>;
|
|
60
|
+
/**
|
|
61
|
+
* Move a directory written aside into its place in the cache. The one already there is kept when it is complete (put
|
|
62
|
+
* there by another build running at the same time), and replaced when it is not: left by a build that was stopped, or
|
|
63
|
+
* emptied by a cleaning of the temporary files that removes the files and keeps the directories (macOS cleans its
|
|
64
|
+
* temporary directory so)
|
|
65
|
+
* @param temporary The directory written, removed by the caller.
|
|
66
|
+
* @param target Its place in the cache.
|
|
67
|
+
* @param complete Whether the directory in its place is complete.
|
|
68
|
+
*/
|
|
69
|
+
export declare function moveIntoCache(temporary: string, target: string, complete: () => boolean): Promise<void>;
|
|
25
70
|
/**
|
|
26
71
|
* A tool of the builder: the one given by its environment variable (LAKUTATA_NFPM, LAKUTATA_WINSW, LAKUTATA_NSIS:
|
|
27
72
|
* makensis.exe, LAKUTATA_WIX: candle.exe, next to light.exe), else the version of the builder, downloaded once into the
|
|
28
73
|
* cache and checked with its sha256
|
|
29
74
|
* @param name
|
|
75
|
+
* @param progress Called with what is being downloaded
|
|
30
76
|
*/
|
|
31
|
-
export declare function ensureTool(name: keyof typeof TOOLS): Promise<string>;
|
|
77
|
+
export declare function ensureTool(name: keyof typeof TOOLS, progress?: (line: string) => void): Promise<string>;
|
|
32
78
|
export {};
|
|
@@ -21,8 +21,9 @@ export declare function scriptAction(target: Exclude<LinuxTarget, 'tar.gz'>, hoo
|
|
|
21
21
|
* @param targets
|
|
22
22
|
* @param work
|
|
23
23
|
* @param outdir
|
|
24
|
+
* @param progress Called with what is being downloaded (nFPM)
|
|
24
25
|
*/
|
|
25
|
-
export declare function buildLinuxPackages(plan: InstallerPlan, targets: LinuxTarget[], work: string, outdir: string): Promise<{
|
|
26
|
+
export declare function buildLinuxPackages(plan: InstallerPlan, targets: LinuxTarget[], work: string, outdir: string, progress?: (line: string) => void): Promise<{
|
|
26
27
|
target: string;
|
|
27
28
|
file: string;
|
|
28
29
|
size: number;
|