@lakutata/builder 3.1.3 → 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 +24 -1
- package/README.zh-CN.md +7 -1
- package/dist/esm/index.js +5 -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 +28 -14
- package/dist/esm/lib/NativeRuntime.js +27 -3
- package/dist/esm/lib/Processes.js +3 -2
- package/dist/esm/lib/Tools.js +98 -19
- package/dist/esm/lib/linux/Linux.js +60 -10
- package/dist/esm/lib/mac/Mac.js +39 -11
- 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 +310 -0
- package/dist/esm/lib/nwjs/Package.js +149 -0
- package/dist/esm/lib/nwjs/Runtime.js +188 -0
- package/dist/esm/lib/nwjs/Stage.js +92 -0
- package/dist/esm/lib/win/Windows.js +68 -45
- package/dist/types/index.d.ts +7 -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 +37 -4
- 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/mac/Mac.d.ts +1 -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 +69 -0
- package/dist/types/lib/nwjs/Package.d.ts +56 -0
- package/dist/types/lib/nwjs/Runtime.d.ts +120 -0
- package/dist/types/lib/nwjs/Stage.d.ts +104 -0
- package/dist/types/lib/win/Windows.d.ts +30 -1
- package/package.json +2 -2
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { constants, existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { cp, mkdir, mkdtemp, readdir, readFile, rm, stat, symlink, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { runCommand } from '../Processes.js';
|
|
8
|
+
import { binaryArchitectures, nodeFiles, nodeGyp, packageOf } from '../nwjs/Addons.js';
|
|
9
|
+
import { compareVersions } from '../nwjs/Runtime.js';
|
|
10
|
+
import { cacheDirectory, moveIntoCache } from '../Tools.js';
|
|
11
|
+
import { ANDROID_ABIS, mobileNodeAbis, mobileNodeVersion } from './Node.js';
|
|
12
|
+
/**
|
|
13
|
+
* The version of Android the addons are built for: the minSdkVersion of the projects of React Native 0.86
|
|
14
|
+
*/
|
|
15
|
+
const ANDROID_API = 24;
|
|
16
|
+
/**
|
|
17
|
+
* The SDKs of iOS the native addons are built for: the architectures built and the slice of the framework of the
|
|
18
|
+
* Node.js of the application (NodeMobile.xcframework) they link
|
|
19
|
+
*/
|
|
20
|
+
const IOS_SDKS = {
|
|
21
|
+
iphoneos: { archs: 'arm64', slice: 'ios-arm64', platform: 'iPhoneOS' },
|
|
22
|
+
iphonesimulator: { archs: 'arm64', slice: 'ios-arm64-simulator', platform: 'iPhoneSimulator' }
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The version of iOS the addons are built for: the minimum of React Native 0.86, the deployment target of libnode
|
|
26
|
+
*/
|
|
27
|
+
const IOS_DEPLOYMENT_TARGET = '15.1';
|
|
28
|
+
/**
|
|
29
|
+
* Where the libraries of the HAP are on HarmonyOS (the libs/arm64-v8a of its module), which the addons are loaded from
|
|
30
|
+
*/
|
|
31
|
+
const HARMONY_LIBRARIES = '/data/storage/el1/bundle/libs/arm64';
|
|
32
|
+
/**
|
|
33
|
+
* The file of a Node.js project mapping the placeholders of its addons (their paths in the native tree) to the libraries
|
|
34
|
+
* the system loads instead (the frameworks of the application on iOS, the libraries of the HAP on HarmonyOS):
|
|
35
|
+
* {directory, files}, the directory relative to the project or absolute
|
|
36
|
+
*/
|
|
37
|
+
export const MOBILE_ADDONS_FILE = 'addons.json';
|
|
38
|
+
/**
|
|
39
|
+
* The packages of the addons of the native tree: built again from their sources, a package is the unit
|
|
40
|
+
* @param files the files of the tree
|
|
41
|
+
*/
|
|
42
|
+
function addonPackages(files) {
|
|
43
|
+
const packages = new Map();
|
|
44
|
+
for (const file of files.filter((item) => item.path.endsWith('.node'))) {
|
|
45
|
+
const source = packageOf(file.source);
|
|
46
|
+
if (!source)
|
|
47
|
+
throw new Error(`${file.path} is in no package: the native addons are built for iOS and Android from the sources of their packages`);
|
|
48
|
+
const relative = path.relative(source, file.source).split(path.sep).join('/');
|
|
49
|
+
let item = packages.get(source);
|
|
50
|
+
if (!item) {
|
|
51
|
+
const manifest = JSON.parse(readFileSync(path.join(source, 'package.json'), 'utf8'));
|
|
52
|
+
item = { name: manifest.name ?? path.basename(source), source: source, target: '', files: [] };
|
|
53
|
+
packages.set(source, item);
|
|
54
|
+
}
|
|
55
|
+
if (file.path.endsWith(`/${relative}`))
|
|
56
|
+
item.target = file.path.slice(0, file.path.length - relative.length - 1);
|
|
57
|
+
item.files.push(file.path);
|
|
58
|
+
}
|
|
59
|
+
return [...packages.values()];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The NDK of Android: ANDROID_NDK_HOME, ANDROID_NDK_ROOT, else the latest one of the SDK (ANDROID_HOME, ANDROID_SDK_ROOT,
|
|
63
|
+
* else the directory Android Studio installs it into)
|
|
64
|
+
* @returns its directory, the directory of its compilers and its revision
|
|
65
|
+
*/
|
|
66
|
+
function androidNdk() {
|
|
67
|
+
let ndk = [process.env.ANDROID_NDK_HOME, process.env.ANDROID_NDK_ROOT].find((directory) => !!directory && existsSync(path.join(directory, 'source.properties')));
|
|
68
|
+
if (!ndk) {
|
|
69
|
+
const sdk = process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT ?? path.join(os.homedir(), ...process.platform === 'darwin' ? ['Library', 'Android', 'sdk'] : ['Android', 'Sdk']);
|
|
70
|
+
const root = path.join(sdk, 'ndk');
|
|
71
|
+
const latest = existsSync(root) ? readdirSync(root).filter((version) => existsSync(path.join(root, version, 'source.properties'))).sort(compareVersions).at(-1) : undefined;
|
|
72
|
+
if (latest)
|
|
73
|
+
ndk = path.join(root, latest);
|
|
74
|
+
}
|
|
75
|
+
if (!ndk)
|
|
76
|
+
throw new Error('The NDK of Android builds the native addons for Android: install it (the SDK Manager of Android Studio, or sdkmanager "ndk;27.0.12077973"), or give its directory (ANDROID_NDK_HOME)');
|
|
77
|
+
const bin = path.join(ndk, 'toolchains', 'llvm', 'prebuilt', `${process.platform === 'darwin' ? 'darwin' : 'linux'}-x86_64`, 'bin');
|
|
78
|
+
if (!existsSync(bin))
|
|
79
|
+
throw new Error(`The NDK ${ndk} has no compilers for this system (${bin})`);
|
|
80
|
+
const revision = /Pkg\.Revision\s*=\s*(\S+)/.exec(readFileSync(path.join(ndk, 'source.properties'), 'utf8'))?.[1] ?? 'unknown';
|
|
81
|
+
return { root: ndk, bin: bin, revision: revision };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The format of the builds kept in the cache: changed when what a build makes changes
|
|
85
|
+
*/
|
|
86
|
+
const CACHE_FORMAT = 4;
|
|
87
|
+
/**
|
|
88
|
+
* The files of a package, by their paths, their sizes and their times (not their contents: a package of sources has
|
|
89
|
+
* thousands of files), its build, its modules and the files of git left out
|
|
90
|
+
* @param directory
|
|
91
|
+
* @param base
|
|
92
|
+
*/
|
|
93
|
+
async function fingerprint(directory, base = directory) {
|
|
94
|
+
const lines = [];
|
|
95
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
96
|
+
const file = path.join(directory, entry.name);
|
|
97
|
+
const relative = path.relative(base, file).split(path.sep).join('/');
|
|
98
|
+
if (['build', 'node_modules', '.git'].includes(relative))
|
|
99
|
+
continue;
|
|
100
|
+
if (entry.isDirectory()) {
|
|
101
|
+
lines.push(...await fingerprint(file, base));
|
|
102
|
+
}
|
|
103
|
+
else if (entry.isFile()) {
|
|
104
|
+
const stats = await stat(file);
|
|
105
|
+
lines.push(`${relative}\0${stats.size}\0${stats.mtimeMs}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return lines;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* What a libnode is to the addons built against it: its version and its configuration
|
|
112
|
+
* @param node the export of the libnode
|
|
113
|
+
*/
|
|
114
|
+
function nodeIdentity(node) {
|
|
115
|
+
const config = path.join(node, 'include', 'node', 'config.gypi');
|
|
116
|
+
return `${mobileNodeVersion(node)} ${existsSync(config) ? createHash('sha256').update(readFileSync(config)).digest('hex').slice(0, 16) : ''}`;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The addons of a package built for a target, kept in the cache (~/.cache/lakutata/mobile/addons, LAKUTATA_CACHE_DIR)
|
|
120
|
+
* by the files of the package, the target, the toolchain and the libnode: built once, then taken from the cache (the
|
|
121
|
+
* development stages the application at each change)
|
|
122
|
+
* @param item
|
|
123
|
+
* @param target the ABI, the SDK of iOS, or harmony
|
|
124
|
+
* @param toolchain the version of the NDK, of Xcode, or of the native SDK of HarmonyOS
|
|
125
|
+
* @param node the export of the libnode built against
|
|
126
|
+
* @param build
|
|
127
|
+
* @returns the addons
|
|
128
|
+
*/
|
|
129
|
+
async function cachedBuild(item, target, toolchain, node, build) {
|
|
130
|
+
const hash = createHash('sha256').update(JSON.stringify({ format: CACHE_FORMAT, node: nodeIdentity(node), name: item.name, target: target, toolchain: toolchain }));
|
|
131
|
+
for (const line of (await fingerprint(item.source)).sort())
|
|
132
|
+
hash.update(`${line}\n`);
|
|
133
|
+
const directory = path.join(cacheDirectory(), 'mobile', 'addons', hash.digest('hex').slice(0, 32));
|
|
134
|
+
const addons = async () => (await readdir(directory)).filter((name) => name.endsWith('.node')).sort().map((name) => path.join(directory, name));
|
|
135
|
+
if (existsSync(path.join(directory, '.complete')))
|
|
136
|
+
return addons();
|
|
137
|
+
const built = await build();
|
|
138
|
+
//Written aside, then renamed: the builds running together do not see it partly written
|
|
139
|
+
await mkdir(path.dirname(directory), { recursive: true });
|
|
140
|
+
const temporary = await mkdtemp(`${directory}.`);
|
|
141
|
+
try {
|
|
142
|
+
for (const addon of built)
|
|
143
|
+
await cp(addon, path.join(temporary, path.basename(addon)));
|
|
144
|
+
await writeFile(path.join(temporary, '.complete'), '');
|
|
145
|
+
await moveIntoCache(temporary, directory, () => existsSync(path.join(directory, '.complete')));
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
await rm(temporary, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
return addons();
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The modules the build of a package loads (node-addon-api), found where the package is
|
|
154
|
+
* @param source
|
|
155
|
+
*/
|
|
156
|
+
function buildModules(source) {
|
|
157
|
+
return [...createRequire(path.join(source, 'package.json')).resolve.paths('x') ?? [], process.env.NODE_PATH].filter(Boolean).join(path.delimiter);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Build the addons of a package for an architecture of Android with node-gyp (a copy of the package), cross-compiled
|
|
161
|
+
* by the NDK against the headers and the library of libnode
|
|
162
|
+
* @param project
|
|
163
|
+
* @param item
|
|
164
|
+
* @param abi
|
|
165
|
+
* @param node the export of the libnode of Android
|
|
166
|
+
* @param ndk the NDK
|
|
167
|
+
* @param work a directory of the build
|
|
168
|
+
* @returns the addons built (in build/Release of the copy)
|
|
169
|
+
*/
|
|
170
|
+
async function buildAndroid(project, item, abi, node, ndk, work) {
|
|
171
|
+
const bin = ndk.bin;
|
|
172
|
+
const copy = path.join(work, path.basename(item.source));
|
|
173
|
+
await cp(item.source, copy, { recursive: true, filter: (file) => path.relative(item.source, file) !== 'build' });
|
|
174
|
+
const compiler = path.join(bin, `${ANDROID_ABIS[abi].target}${ANDROID_API}-clang`);
|
|
175
|
+
const env = {
|
|
176
|
+
...process.env,
|
|
177
|
+
NODE_PATH: buildModules(item.source),
|
|
178
|
+
GYP_CROSSCOMPILE: '1',
|
|
179
|
+
CC: compiler,
|
|
180
|
+
CXX: `${compiler}++`,
|
|
181
|
+
LINK: `${compiler}++`,
|
|
182
|
+
AR: path.join(bin, 'llvm-ar'),
|
|
183
|
+
CC_target: compiler,
|
|
184
|
+
CXX_target: `${compiler}++`,
|
|
185
|
+
LINK_target: `${compiler}++`,
|
|
186
|
+
AR_target: path.join(bin, 'llvm-ar'),
|
|
187
|
+
CC_host: process.env.CC_host ?? 'cc',
|
|
188
|
+
CXX_host: process.env.CXX_host ?? 'c++',
|
|
189
|
+
LINK_host: process.env.LINK_host ?? 'c++',
|
|
190
|
+
AR_host: process.env.AR_host ?? 'ar',
|
|
191
|
+
//The addons load the Node.js of the application (libnode.so); the pages of 16 KB of Android 15
|
|
192
|
+
LDFLAGS: `-L${path.join(node, 'libs', abi)} -lnode -Wl,-z,max-page-size=16384`
|
|
193
|
+
};
|
|
194
|
+
//The common.gypi of Node.js 24 reads the NDK (its cpufeatures) when OS is android
|
|
195
|
+
const args = [nodeGyp(project), 'rebuild', `--arch=${ANDROID_ABIS[abi].arch}`, `--nodedir=${await addonNodeHeaders(node, work)}`, '--jobs=max', '--', '-f', 'make-android', '-DOS=android', `-Dandroid_ndk_path=${ndk.root}`];
|
|
196
|
+
const built = await runCommand(process.execPath, args, copy, env);
|
|
197
|
+
if (built.code !== 0)
|
|
198
|
+
throw new Error(`The addons of ${item.name} cannot be built for Android (${abi}): ${built.output.trim().split('\n').slice(-8).join('\n')}`);
|
|
199
|
+
const addons = await nodeFiles(path.join(copy, 'build', 'Release'));
|
|
200
|
+
if (!addons.length)
|
|
201
|
+
throw new Error(`The build of ${item.name} for Android (${abi}) made no addon (build/Release)`);
|
|
202
|
+
for (const addon of addons) {
|
|
203
|
+
if (!binaryArchitectures(await readFile(addon)).includes(ANDROID_ABIS[abi].arch))
|
|
204
|
+
throw new Error(`${path.basename(addon)} of ${item.name} is not built for ${abi}`);
|
|
205
|
+
}
|
|
206
|
+
return addons;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Give an addon package of a native tree the addons of an architecture: each addon of the package takes the one built of
|
|
210
|
+
* its name (in build/Release, or where node-pre-gyp puts it), the others (the addons of other systems) are removed
|
|
211
|
+
* @param tree
|
|
212
|
+
* @param item
|
|
213
|
+
* @param built
|
|
214
|
+
* @param kept the addons of the tree kept as they are (the prebuilt ones of the architecture)
|
|
215
|
+
*/
|
|
216
|
+
async function giveAddons(tree, item, built, kept) {
|
|
217
|
+
const names = new Map(built.map((file) => [path.basename(file), file]));
|
|
218
|
+
for (const file of item.files) {
|
|
219
|
+
if (kept(file))
|
|
220
|
+
continue;
|
|
221
|
+
const target = path.join(tree, ...file.split('/'));
|
|
222
|
+
const addon = names.get(path.basename(file));
|
|
223
|
+
if (addon)
|
|
224
|
+
await cp(addon, target);
|
|
225
|
+
else
|
|
226
|
+
await rm(target, { force: true });
|
|
227
|
+
}
|
|
228
|
+
//The ones of build/Release, which the loaders look for first
|
|
229
|
+
if (item.target) {
|
|
230
|
+
for (const [name, addon] of names) {
|
|
231
|
+
const target = path.join(tree, ...item.target.split('/'), 'build', 'Release', name);
|
|
232
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
233
|
+
await cp(addon, target);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Build the native addons of a mobile application of Android: a native tree per architecture (<directory>-<arch>, the
|
|
239
|
+
* one of process.arch loaded by the bundle), each package of addons built for it from its sources (node-gyp, the NDK,
|
|
240
|
+
* the headers and the library of libnode), or keeping its prebuilt addons of the architecture
|
|
241
|
+
* (prebuilds/android-<arch>, node-gyp-build). The tree of the build is removed once they are made
|
|
242
|
+
* @param project
|
|
243
|
+
* @param directory the native tree
|
|
244
|
+
* @param files the files of the tree
|
|
245
|
+
* @param node the export of the libnode of Android
|
|
246
|
+
* @param options
|
|
247
|
+
* @internal
|
|
248
|
+
*/
|
|
249
|
+
export async function prepareAndroidAddons(project, directory, files, node, options = {}) {
|
|
250
|
+
if (process.platform === 'win32')
|
|
251
|
+
throw new Error('The native addons are built for Android on macOS and on Linux (node-gyp, make and the NDK)');
|
|
252
|
+
const built = mobileNodeAbis(node);
|
|
253
|
+
const abis = options.abis?.length ? options.abis : built;
|
|
254
|
+
for (const abi of abis)
|
|
255
|
+
if (!built.includes(abi))
|
|
256
|
+
throw new Error(`The libnode of ${node} is not built for the architecture ${abi} of Android (${built.join(', ') || 'none'})`);
|
|
257
|
+
const packages = addonPackages(files);
|
|
258
|
+
const result = { built: [], prebuilt: [], frameworks: [], libraries: [] };
|
|
259
|
+
const work = await mkdtemp(path.join(os.tmpdir(), 'lakutata-mobile-addons-'));
|
|
260
|
+
const trees = [];
|
|
261
|
+
try {
|
|
262
|
+
//The NDK, once a package is built
|
|
263
|
+
let ndk;
|
|
264
|
+
for (const abi of abis) {
|
|
265
|
+
const arch = ANDROID_ABIS[abi].arch;
|
|
266
|
+
const tree = `${directory}-${arch}`;
|
|
267
|
+
await rm(tree, { recursive: true, force: true });
|
|
268
|
+
await cp(directory, tree, { recursive: true, verbatimSymlinks: true, mode: constants.COPYFILE_FICLONE });
|
|
269
|
+
trees.push(tree);
|
|
270
|
+
for (const item of packages) {
|
|
271
|
+
//The prebuilt addons of the architecture (node-gyp-build loads them), when the package keeps its addons in its tree
|
|
272
|
+
const prebuilds = `${item.target}/prebuilds/android-${arch}/`;
|
|
273
|
+
if (item.target && existsSync(path.join(item.source, 'prebuilds', `android-${arch}`))) {
|
|
274
|
+
await giveAddons(tree, { ...item, target: '' }, [], (file) => file.startsWith(prebuilds));
|
|
275
|
+
result.prebuilt.push(`${item.name} (${abi})`);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (!existsSync(path.join(item.source, 'binding.gyp')))
|
|
279
|
+
throw new Error(`${item.name} has no sources to build its addons for Android (binding.gyp), and no prebuilt addons of Android (prebuilds/android-${arch})`);
|
|
280
|
+
ndk ??= androidNdk();
|
|
281
|
+
const toolchain = ndk;
|
|
282
|
+
const addons = await cachedBuild(item, abi, `ndk ${toolchain.revision}`, node, async () => {
|
|
283
|
+
options.progress?.(`The addons of ${item.name} built for Android (${abi})`);
|
|
284
|
+
const build = path.join(work, `${result.built.length}`);
|
|
285
|
+
await mkdir(build, { recursive: true });
|
|
286
|
+
return buildAndroid(project, item, abi, node, toolchain, build);
|
|
287
|
+
});
|
|
288
|
+
await giveAddons(tree, item, addons, () => false);
|
|
289
|
+
result.built.push(`${item.name} (${abi})`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
await rm(directory, { recursive: true, force: true });
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
//Nothing half made
|
|
296
|
+
for (const tree of trees)
|
|
297
|
+
await rm(tree, { recursive: true, force: true });
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
await rm(work, { recursive: true, force: true });
|
|
302
|
+
}
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* The name of the framework of an addon: its package and its file, in the characters of the names and of the
|
|
307
|
+
* identifiers of the bundles (letters, digits and hyphens)
|
|
308
|
+
* @param name the name of its package
|
|
309
|
+
* @param file its file
|
|
310
|
+
* @internal
|
|
311
|
+
*/
|
|
312
|
+
export function frameworkName(name, file) {
|
|
313
|
+
return `${name}-${path.basename(file, '.node')}`.replace(/^@/, '').replace(/[^A-Za-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* The Info.plist of the framework of an addon: the keys the frameworks embedded in an application of the App Store
|
|
317
|
+
* have
|
|
318
|
+
* @param name
|
|
319
|
+
* @param version the version of its package
|
|
320
|
+
* @param sdk
|
|
321
|
+
* @internal
|
|
322
|
+
*/
|
|
323
|
+
export function frameworkInfo(name, version, sdk) {
|
|
324
|
+
const short = /^\d+(\.\d+){0,2}/.exec(version)?.[0] ?? '1.0.0';
|
|
325
|
+
const entries = [
|
|
326
|
+
['CFBundleDevelopmentRegion', 'en'],
|
|
327
|
+
['CFBundleExecutable', name],
|
|
328
|
+
['CFBundleIdentifier', `dev.lakutata.addon.${name}`],
|
|
329
|
+
['CFBundleInfoDictionaryVersion', '6.0'],
|
|
330
|
+
['CFBundleName', name],
|
|
331
|
+
['CFBundlePackageType', 'FMWK'],
|
|
332
|
+
['CFBundleShortVersionString', short],
|
|
333
|
+
['CFBundleVersion', short],
|
|
334
|
+
['MinimumOSVersion', IOS_DEPLOYMENT_TARGET]
|
|
335
|
+
];
|
|
336
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
337
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
338
|
+
<plist version="1.0">
|
|
339
|
+
<dict>
|
|
340
|
+
${entries.map(([key, value]) => `\t<key>${key}</key>\n\t<string>${value}</string>`).join('\n')}
|
|
341
|
+
\t<key>CFBundleSupportedPlatforms</key>
|
|
342
|
+
\t<array>
|
|
343
|
+
\t\t<string>${IOS_SDKS[sdk].platform}</string>
|
|
344
|
+
\t</array>
|
|
345
|
+
</dict>
|
|
346
|
+
</plist>
|
|
347
|
+
`;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* The calls of the cleanup hooks of node::ObjectWrap in its header, in its constructor and its destructor. Node.js 24.19
|
|
351
|
+
* backported them without what lets the destructor remove its hook when no Environment is current, which is how the
|
|
352
|
+
* garbage collector calls it: the destructor of an object it frees aborts the process (Assertion failed: (env) !=
|
|
353
|
+
* nullptr, in RemoveEnvironmentCleanupHook; https://github.com/nodejs/node/issues/65446)
|
|
354
|
+
*/
|
|
355
|
+
const OBJECT_WRAP_HOOKS = /^[ \t]*(?:Add|Remove)CleanupHook\(\);[ \t]*\r?\n/gm;
|
|
356
|
+
/**
|
|
357
|
+
* The headers of a libnode as node-gyp builds the addons against them, linked but for these:
|
|
358
|
+
* - node_object_wrap.h without the calls of the cleanup hooks of node::ObjectWrap (OBJECT_WRAP_HOOKS), as in the headers
|
|
359
|
+
* of Node.js 24.18: an addon built on node::ObjectWrap (better-sqlite3, the addons of NAN) would abort the application
|
|
360
|
+
* whenever the garbage collector frees one of its objects. A header without them is copied as it is;
|
|
361
|
+
* - for iOS, config.gypi with OS "mac": the libnode of iOS is configured with OS "ios", for which the common.gypi of
|
|
362
|
+
* Node.js turns the exceptions and the RTTI of C++ off in the projects of Xcode as it does for macOS, while the addons
|
|
363
|
+
* (and node-addon-api) give their settings of Xcode (the exceptions, the RTTI, the standard of C++) to OS "mac" only.
|
|
364
|
+
* Built as for macOS, an addon keeps them; the SDK, the architectures and the deployment target of iOS are given to
|
|
365
|
+
* xcodebuild
|
|
366
|
+
* @param node the export of the libnode
|
|
367
|
+
* @param work a directory of the build
|
|
368
|
+
* @param os "mac" for iOS: the OS given to config.gypi
|
|
369
|
+
* @returns the directory of the headers (the nodedir of node-gyp)
|
|
370
|
+
*/
|
|
371
|
+
export async function addonNodeHeaders(node, work, os) {
|
|
372
|
+
const source = path.join(node, 'include', 'node');
|
|
373
|
+
const directory = path.join(work, 'node-headers');
|
|
374
|
+
const include = path.join(directory, 'include', 'node');
|
|
375
|
+
await mkdir(include, { recursive: true });
|
|
376
|
+
for (const entry of await readdir(source)) {
|
|
377
|
+
const target = path.join(include, entry);
|
|
378
|
+
if (existsSync(target))
|
|
379
|
+
continue;
|
|
380
|
+
if (entry === 'node_object_wrap.h') {
|
|
381
|
+
await writeFile(target, (await readFile(path.join(source, entry), 'utf8')).replace(OBJECT_WRAP_HOOKS, ''));
|
|
382
|
+
}
|
|
383
|
+
else if (entry === 'config.gypi' && os) {
|
|
384
|
+
await writeFile(target, (await readFile(path.join(source, entry), 'utf8')).replace(/(["']OS["']\s*:\s*)(["'])ios\2/, `$1$2${os}$2`));
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
await symlink(path.join(source, entry), target);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return directory;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Build the addons of a package for a SDK of iOS (a copy of the package): node-gyp makes its project of Xcode, built by
|
|
394
|
+
* xcodebuild into dynamic libraries linking the framework of the Node.js of the application (NodeMobile)
|
|
395
|
+
* @param project
|
|
396
|
+
* @param item
|
|
397
|
+
* @param sdk
|
|
398
|
+
* @param node the export of the libnode of iOS
|
|
399
|
+
* @param work a directory of the build
|
|
400
|
+
* @returns the addons built
|
|
401
|
+
*/
|
|
402
|
+
async function buildIos(project, item, sdk, node, work) {
|
|
403
|
+
const copy = path.join(work, path.basename(item.source));
|
|
404
|
+
await cp(item.source, copy, { recursive: true, filter: (file) => path.relative(item.source, file) !== 'build' });
|
|
405
|
+
const env = { ...process.env, NODE_PATH: buildModules(item.source) };
|
|
406
|
+
const configured = await runCommand(process.execPath, [nodeGyp(project), 'configure', `--nodedir=${await addonNodeHeaders(node, work, 'mac')}`, '--', '-f', 'xcode'], copy, env);
|
|
407
|
+
if (configured.code !== 0)
|
|
408
|
+
throw new Error(`The addons of ${item.name} cannot be built for iOS (node-gyp configure): ${configured.output.trim().split('\n').slice(-8).join('\n')}`);
|
|
409
|
+
//The addons made dynamic libraries (in frameworks) rather than bundles: their targets only, not the static libraries
|
|
410
|
+
//they link (a setting of xcodebuild applies to every target)
|
|
411
|
+
const pbxproj = path.join(copy, 'build', 'binding.xcodeproj', 'project.pbxproj');
|
|
412
|
+
await writeFile(pbxproj, (await readFile(pbxproj, 'utf8')).replaceAll('MACH_O_TYPE = mh_bundle;', 'MACH_O_TYPE = mh_dylib;'));
|
|
413
|
+
const output = path.join(work, 'products');
|
|
414
|
+
const framework = path.join(node, 'NodeMobile.xcframework', IOS_SDKS[sdk].slice);
|
|
415
|
+
const settings = [
|
|
416
|
+
`ARCHS=${IOS_SDKS[sdk].archs}`, 'ONLY_ACTIVE_ARCH=NO', `SDKROOT=${sdk}`, `SUPPORTED_PLATFORMS=${sdk}`,
|
|
417
|
+
`IPHONEOS_DEPLOYMENT_TARGET=${IOS_DEPLOYMENT_TARGET}`, 'MACOSX_DEPLOYMENT_TARGET=',
|
|
418
|
+
//Linking the Node.js of the application, not left to look it up (-undefined dynamic_lookup)
|
|
419
|
+
`OTHER_LDFLAGS=-F${framework} -framework NodeMobile`,
|
|
420
|
+
'CODE_SIGNING_ALLOWED=NO', `CONFIGURATION_BUILD_DIR=${output}`,
|
|
421
|
+
//One root for the projects of the package: gyp makes a project of each of its gyp files, whose
|
|
422
|
+
//SHARED_INTERMEDIATE_DIR is in the root of that project, so the files an action of one writes there (the SQLite
|
|
423
|
+
//that better-sqlite3 copies) are looked for in the root of another, and the headers of the SDK are found instead
|
|
424
|
+
`SYMROOT=${path.join(work, 'symroot')}`
|
|
425
|
+
];
|
|
426
|
+
const built = await runCommand('xcodebuild', ['-project', 'binding.xcodeproj', '-configuration', 'Release', '-alltargets', '-sdk', sdk, ...settings, 'build'], path.join(copy, 'build'), env);
|
|
427
|
+
if (built.code !== 0)
|
|
428
|
+
throw new Error(`The addons of ${item.name} cannot be built for iOS (${sdk}): ${built.output.trim().split('\n').filter((line) => /error/i.test(line)).slice(-8).join('\n') || built.output.trim().split('\n').slice(-8).join('\n')}`);
|
|
429
|
+
const addons = (await nodeFiles(output)).filter((file) => path.dirname(file) === output);
|
|
430
|
+
if (!addons.length)
|
|
431
|
+
throw new Error(`The build of ${item.name} for iOS (${sdk}) made no addon`);
|
|
432
|
+
return addons;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Make an addon built for iOS a framework: its library named after it (its install name in the frameworks of the
|
|
436
|
+
* application), its Info.plist, signed ad hoc (the build of the application signs it with its identity)
|
|
437
|
+
* @param addon
|
|
438
|
+
* @param directory where the framework is made
|
|
439
|
+
* @param name
|
|
440
|
+
* @param version the version of its package
|
|
441
|
+
* @param sdk
|
|
442
|
+
*/
|
|
443
|
+
async function makeFramework(addon, directory, name, version, sdk) {
|
|
444
|
+
const framework = path.join(directory, `${name}.framework`);
|
|
445
|
+
await rm(framework, { recursive: true, force: true });
|
|
446
|
+
await mkdir(framework, { recursive: true });
|
|
447
|
+
await cp(addon, path.join(framework, name));
|
|
448
|
+
await writeFile(path.join(framework, 'Info.plist'), frameworkInfo(name, version, sdk));
|
|
449
|
+
for (const [command, args] of [['install_name_tool', ['-id', `@rpath/${name}.framework/${name}`, path.join(framework, name)]], ['codesign', ['--force', '--sign', '-', framework]]]) {
|
|
450
|
+
const result = await runCommand(command, args, directory);
|
|
451
|
+
if (result.code !== 0)
|
|
452
|
+
throw new Error(`The framework ${name} cannot be made (${command}): ${result.output.trim()}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Build the native addons of a mobile application of iOS: each package of addons built for the SDKs from its sources
|
|
457
|
+
* (node-gyp, xcodebuild, linking the framework of the Node.js of the application), each addon made a framework, which
|
|
458
|
+
* the project of iOS embeds in the application; its files in the native tree replaced by placeholders (no library of
|
|
459
|
+
* the App Store outside of its frameworks), which the entry of the project loads from the frameworks of the application
|
|
460
|
+
* (MOBILE_ADDONS_FILE: the project is in the application, next to its frameworks). The frameworks of the SDKs made
|
|
461
|
+
* before are removed
|
|
462
|
+
* @param project
|
|
463
|
+
* @param stage the Node.js project
|
|
464
|
+
* @param directory its native tree
|
|
465
|
+
* @param files the files of the tree
|
|
466
|
+
* @param frameworks the directory of the frameworks, a directory per SDK (the NodeAddons of the project of iOS)
|
|
467
|
+
* @param node the export of the libnode of iOS
|
|
468
|
+
* @param options
|
|
469
|
+
* @internal
|
|
470
|
+
*/
|
|
471
|
+
export async function prepareIosAddons(project, stage, directory, files, frameworks, node, options = {}) {
|
|
472
|
+
if (process.platform !== 'darwin')
|
|
473
|
+
throw new Error('The native addons are built for iOS on macOS (Xcode)');
|
|
474
|
+
const sdks = options.sdks?.length ? options.sdks : Object.keys(IOS_SDKS);
|
|
475
|
+
const packages = addonPackages(files);
|
|
476
|
+
const result = { built: [], prebuilt: [], frameworks: [], libraries: [] };
|
|
477
|
+
const work = await mkdtemp(path.join(os.tmpdir(), 'lakutata-mobile-addons-'));
|
|
478
|
+
try {
|
|
479
|
+
for (const item of packages) {
|
|
480
|
+
if (!existsSync(path.join(item.source, 'binding.gyp')))
|
|
481
|
+
throw new Error(`${item.name} has no sources to build its addons for iOS (binding.gyp)`);
|
|
482
|
+
}
|
|
483
|
+
//The version of Xcode, which builds them
|
|
484
|
+
const xcode = await runCommand('xcodebuild', ['-version'], work);
|
|
485
|
+
if (xcode.code !== 0)
|
|
486
|
+
throw new Error(`Xcode builds the native addons for iOS: ${xcode.output.trim()}`);
|
|
487
|
+
const mapping = {};
|
|
488
|
+
for (const item of packages) {
|
|
489
|
+
const version = JSON.parse(await readFile(path.join(item.source, 'package.json'), 'utf8')).version ?? '1.0.0';
|
|
490
|
+
const names = new Set();
|
|
491
|
+
for (const sdk of sdks) {
|
|
492
|
+
const addons = await cachedBuild(item, sdk, xcode.output.trim(), node, async () => {
|
|
493
|
+
options.progress?.(`The addons of ${item.name} built for iOS (${sdk})`);
|
|
494
|
+
const build = path.join(work, `${result.built.length}`);
|
|
495
|
+
await mkdir(build, { recursive: true });
|
|
496
|
+
return buildIos(project, item, sdk, node, build);
|
|
497
|
+
});
|
|
498
|
+
for (const addon of addons) {
|
|
499
|
+
const name = frameworkName(item.name, addon);
|
|
500
|
+
await makeFramework(addon, path.join(frameworks, sdk), name, version, sdk);
|
|
501
|
+
result.frameworks.push(`${sdk}/${name}.framework`);
|
|
502
|
+
names.add(path.basename(addon));
|
|
503
|
+
}
|
|
504
|
+
result.built.push(`${item.name} (${sdk})`);
|
|
505
|
+
}
|
|
506
|
+
//The files of the addons in the tree: placeholders of the frameworks, the others (of other systems) removed
|
|
507
|
+
const placeholders = item.files.filter((file) => names.has(path.basename(file)));
|
|
508
|
+
if (item.target)
|
|
509
|
+
for (const name of names)
|
|
510
|
+
placeholders.push(`${item.target}/build/Release/${name}`);
|
|
511
|
+
for (const file of item.files)
|
|
512
|
+
if (!names.has(path.basename(file)))
|
|
513
|
+
await rm(path.join(directory, ...file.split('/')), { force: true });
|
|
514
|
+
for (const file of new Set(placeholders)) {
|
|
515
|
+
const target = path.join(directory, ...file.split('/'));
|
|
516
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
517
|
+
await writeFile(target, '');
|
|
518
|
+
const name = frameworkName(item.name, file);
|
|
519
|
+
mapping[path.relative(directory, target).split(path.sep).join('/')] = `${name}.framework/${name}`;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
await writeFile(path.join(stage, MOBILE_ADDONS_FILE), `${JSON.stringify({ directory: '../Frameworks', files: mapping }, null, 2)}\n`);
|
|
523
|
+
}
|
|
524
|
+
finally {
|
|
525
|
+
await rm(work, { recursive: true, force: true });
|
|
526
|
+
}
|
|
527
|
+
return result;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* The native SDK of HarmonyOS (its LLVM and its sysroot): OHOS_NDK_HOME, the native SDK of DEVECO_SDK_HOME, else the
|
|
531
|
+
* one of DevEco Studio (macOS) or of its command line tools (~/command-line-tools)
|
|
532
|
+
* @returns the directory of its compilers and its version
|
|
533
|
+
*/
|
|
534
|
+
function harmonyNdk() {
|
|
535
|
+
const candidates = [
|
|
536
|
+
process.env.OHOS_NDK_HOME,
|
|
537
|
+
process.env.DEVECO_SDK_HOME ? path.join(process.env.DEVECO_SDK_HOME, 'default', 'openharmony', 'native') : undefined,
|
|
538
|
+
'/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native',
|
|
539
|
+
path.join(os.homedir(), 'command-line-tools', 'sdk', 'default', 'openharmony', 'native')
|
|
540
|
+
];
|
|
541
|
+
const native = candidates.find((directory) => !!directory && existsSync(path.join(directory, 'llvm', 'bin', 'aarch64-unknown-linux-ohos-clang')));
|
|
542
|
+
if (!native)
|
|
543
|
+
throw new Error('The native SDK of HarmonyOS builds the native addons for HarmonyOS: install DevEco Studio (or its command line tools), or give the directory of the native SDK (OHOS_NDK_HOME)');
|
|
544
|
+
let version = 'unknown';
|
|
545
|
+
try {
|
|
546
|
+
version = JSON.parse(readFileSync(path.join(native, 'oh-uni-package.json'), 'utf8')).version ?? version;
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
//A native SDK without its manifest: its builds are cached as unknown
|
|
550
|
+
}
|
|
551
|
+
return { bin: path.join(native, 'llvm', 'bin'), version: version };
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* The name of the library of an addon in the HAP: libaddon-<package>-<file>.so, in letters, digits, hyphens and
|
|
555
|
+
* underscores (the libraries made before are the libaddon-*.so of the directory)
|
|
556
|
+
* @param name the name of its package
|
|
557
|
+
* @param file its file
|
|
558
|
+
* @internal
|
|
559
|
+
*/
|
|
560
|
+
export function harmonyLibraryName(name, file) {
|
|
561
|
+
return `libaddon-${`${name}-${path.basename(file, '.node')}`.replace(/^@/, '').replace(/[^A-Za-z0-9_]+/g, '-').replace(/^-+|-+$/g, '')}.so`;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Build the addons of a package for HarmonyOS with node-gyp (a copy of the package), cross-compiled by the LLVM of the
|
|
565
|
+
* native SDK of HarmonyOS against the headers and the library of libnode (the addons link libnode.so, which binds them
|
|
566
|
+
* to the Node-API of the Node.js of the application)
|
|
567
|
+
* @param project
|
|
568
|
+
* @param item
|
|
569
|
+
* @param node the export of the libnode of HarmonyOS
|
|
570
|
+
* @param bin the compilers of the native SDK
|
|
571
|
+
* @param work a directory of the build
|
|
572
|
+
* @returns the addons built (in build/Release of the copy)
|
|
573
|
+
*/
|
|
574
|
+
async function buildHarmony(project, item, node, bin, work) {
|
|
575
|
+
const copy = path.join(work, path.basename(item.source));
|
|
576
|
+
await cp(item.source, copy, { recursive: true, filter: (file) => path.relative(item.source, file) !== 'build' });
|
|
577
|
+
const compiler = path.join(bin, 'aarch64-unknown-linux-ohos-clang');
|
|
578
|
+
const env = {
|
|
579
|
+
...process.env,
|
|
580
|
+
NODE_PATH: buildModules(item.source),
|
|
581
|
+
GYP_CROSSCOMPILE: '1',
|
|
582
|
+
CC: compiler,
|
|
583
|
+
CXX: `${compiler}++`,
|
|
584
|
+
LINK: `${compiler}++`,
|
|
585
|
+
AR: path.join(bin, 'llvm-ar'),
|
|
586
|
+
CC_target: compiler,
|
|
587
|
+
CXX_target: `${compiler}++`,
|
|
588
|
+
LINK_target: `${compiler}++`,
|
|
589
|
+
AR_target: path.join(bin, 'llvm-ar'),
|
|
590
|
+
CC_host: process.env.CC_host ?? 'cc',
|
|
591
|
+
CXX_host: process.env.CXX_host ?? 'c++',
|
|
592
|
+
LINK_host: process.env.LINK_host ?? 'c++',
|
|
593
|
+
AR_host: process.env.AR_host ?? 'ar',
|
|
594
|
+
LDFLAGS: `-L${path.join(node, 'libs', 'arm64-v8a')} -lnode`
|
|
595
|
+
};
|
|
596
|
+
const args = [nodeGyp(project), 'rebuild', '--arch=arm64', `--nodedir=${await addonNodeHeaders(node, work)}`, '--jobs=max', '--', '-f', 'make-linux', '-DOS=openharmony'];
|
|
597
|
+
const built = await runCommand(process.execPath, args, copy, env);
|
|
598
|
+
if (built.code !== 0)
|
|
599
|
+
throw new Error(`The addons of ${item.name} cannot be built for HarmonyOS: ${built.output.trim().split('\n').slice(-8).join('\n')}`);
|
|
600
|
+
const addons = await nodeFiles(path.join(copy, 'build', 'Release'));
|
|
601
|
+
if (!addons.length)
|
|
602
|
+
throw new Error(`The build of ${item.name} for HarmonyOS made no addon (build/Release)`);
|
|
603
|
+
for (const addon of addons) {
|
|
604
|
+
if (!binaryArchitectures(await readFile(addon)).includes('arm64'))
|
|
605
|
+
throw new Error(`${path.basename(addon)} of ${item.name} is not built for HarmonyOS (arm64)`);
|
|
606
|
+
}
|
|
607
|
+
return addons;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Build the native addons of a mobile application of HarmonyOS: each package of addons built from its sources (node-gyp,
|
|
611
|
+
* the LLVM of the native SDK, linking libnode.so), each addon made a library of the HAP (libaddon-<package>-<file>.so in
|
|
612
|
+
* its libs/arm64-v8a: the addons cannot be loaded from the resources of the HAP); its files in the native tree replaced
|
|
613
|
+
* by placeholders, which the entry of the project loads from the libraries of the HAP (MOBILE_ADDONS_FILE)
|
|
614
|
+
* @param project
|
|
615
|
+
* @param stage the Node.js project
|
|
616
|
+
* @param directory its native tree
|
|
617
|
+
* @param files the files of the tree
|
|
618
|
+
* @param libraries the directory of the libraries of the HAP (the libs/arm64-v8a of its module)
|
|
619
|
+
* @param node the export of the libnode of HarmonyOS
|
|
620
|
+
* @param options
|
|
621
|
+
* @internal
|
|
622
|
+
*/
|
|
623
|
+
export async function prepareHarmonyAddons(project, stage, directory, files, libraries, node, options = {}) {
|
|
624
|
+
if (process.platform === 'win32')
|
|
625
|
+
throw new Error('The native addons are built for HarmonyOS on macOS and on Linux (node-gyp, make and the native SDK)');
|
|
626
|
+
const packages = addonPackages(files);
|
|
627
|
+
const result = { built: [], prebuilt: [], frameworks: [], libraries: [] };
|
|
628
|
+
for (const item of packages) {
|
|
629
|
+
if (!existsSync(path.join(item.source, 'binding.gyp')))
|
|
630
|
+
throw new Error(`${item.name} has no sources to build its addons for HarmonyOS (binding.gyp)`);
|
|
631
|
+
}
|
|
632
|
+
const ndk = harmonyNdk();
|
|
633
|
+
const work = await mkdtemp(path.join(os.tmpdir(), 'lakutata-mobile-addons-'));
|
|
634
|
+
try {
|
|
635
|
+
await mkdir(libraries, { recursive: true });
|
|
636
|
+
const mapping = {};
|
|
637
|
+
for (const item of packages) {
|
|
638
|
+
const addons = await cachedBuild(item, 'harmony', `ohos ${ndk.version}`, node, async () => {
|
|
639
|
+
options.progress?.(`The addons of ${item.name} built for HarmonyOS`);
|
|
640
|
+
const build = path.join(work, `${result.built.length}`);
|
|
641
|
+
await mkdir(build, { recursive: true });
|
|
642
|
+
return buildHarmony(project, item, node, ndk.bin, build);
|
|
643
|
+
});
|
|
644
|
+
const names = new Set();
|
|
645
|
+
for (const addon of addons) {
|
|
646
|
+
const library = harmonyLibraryName(item.name, addon);
|
|
647
|
+
if (result.libraries.includes(library))
|
|
648
|
+
throw new Error(`Two addons are made the library ${library} of HarmonyOS: rename one of their files`);
|
|
649
|
+
await cp(addon, path.join(libraries, library));
|
|
650
|
+
result.libraries.push(library);
|
|
651
|
+
names.add(path.basename(addon));
|
|
652
|
+
}
|
|
653
|
+
result.built.push(`${item.name} (harmony)`);
|
|
654
|
+
//The files of the addons in the tree: placeholders of the libraries, the others (of other systems) removed
|
|
655
|
+
const placeholders = item.files.filter((file) => names.has(path.basename(file)));
|
|
656
|
+
if (item.target)
|
|
657
|
+
for (const name of names)
|
|
658
|
+
placeholders.push(`${item.target}/build/Release/${name}`);
|
|
659
|
+
for (const file of item.files)
|
|
660
|
+
if (!names.has(path.basename(file)))
|
|
661
|
+
await rm(path.join(directory, ...file.split('/')), { force: true });
|
|
662
|
+
for (const file of new Set(placeholders)) {
|
|
663
|
+
const target = path.join(directory, ...file.split('/'));
|
|
664
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
665
|
+
await writeFile(target, '');
|
|
666
|
+
mapping[path.relative(directory, target).split(path.sep).join('/')] = harmonyLibraryName(item.name, file);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
await writeFile(path.join(stage, MOBILE_ADDONS_FILE), `${JSON.stringify({ directory: HARMONY_LIBRARIES, files: mapping }, null, 2)}\n`);
|
|
670
|
+
}
|
|
671
|
+
finally {
|
|
672
|
+
await rm(work, { recursive: true, force: true });
|
|
673
|
+
}
|
|
674
|
+
return result;
|
|
675
|
+
}
|