@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.
@@ -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 for NW.js: install it in the project (npm install --save-dev node-gyp)');
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, open, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
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 download(`${mirror()}/v${release.version}/${archive}`, file);
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 rename(path.join(temporary, name), target).catch((error) => {
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 });
@@ -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;