libavif-with-gainmap 0.1.0 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,6 +59,26 @@ await convertJpegGainMap('input.jpg', 'output.avif', {
59
59
 
60
60
  如果只给 `width` 或 `height`,另一边会按比例计算。`maxWidth` / `maxHeight` 只会缩小,不会放大。
61
61
 
62
+ ## 消费端测试项目
63
+
64
+ 仓库里有一个独立测试项目:[examples/consumer-test](examples/consumer-test)。
65
+
66
+ 把你自己的 JPEG gain map 图片放到:
67
+
68
+ ```text
69
+ examples/consumer-test/images/input.jpg
70
+ ```
71
+
72
+ 然后执行:
73
+
74
+ ```sh
75
+ cd examples/consumer-test
76
+ npm install
77
+ npm test
78
+ ```
79
+
80
+ 输出会写到 `examples/consumer-test/outputs/`。
81
+
62
82
  ## 原生工具覆盖
63
83
 
64
84
  默认使用 npm 包内的二进制。也可以用环境变量覆盖:
package/docs/BUILDING.md CHANGED
@@ -6,29 +6,58 @@
6
6
 
7
7
  1. 跑 JS 测试。
8
8
  2. 用 matrix 分别构筑五个平台的 libavif 工具:
9
- - `linux-x64` on `ubuntu-24.04`
10
- - `linux-arm64` on `ubuntu-24.04-arm`
9
+ - `linux-x64` on `ubuntu-22.04`
10
+ - `linux-arm64` on `ubuntu-22.04-arm`
11
11
  - `darwin-x64` on `macos-15-intel`
12
- - `darwin-arm64` on `macos-15`
13
- - `win32-x64` on `windows-2025`
14
- 3. 汇总 `vendor/<platform>-<arch>/` 后执行 `npm pack`,tag 触发时再 `npm publish`。
12
+ - `darwin-arm64` on `macos-14`
13
+ - `win32-x64` on `windows-2022`
14
+ 3. 对每个平台的二进制做依赖审计,汇总 `vendor/<platform>-<arch>/` 后执行 `npm pack`,tag 触发时再用 npm Trusted Publishing 发布。
15
15
 
16
- 发布到 npm 前,在仓库 secrets 里配置:
16
+ 手动运行 workflow 只会构筑和打包,不会发布。只有推送 `v*` tag 时,`publish npm` job 才会运行。
17
17
 
18
- ```text
19
- NPM_TOKEN=<npm automation token>
18
+ ## 原生二进制门禁
19
+
20
+ 发布包不是只检查文件是否存在。`npm run check-prebuilt` 会执行这些硬检查:
21
+
22
+ - Windows: 解析 PE import table,要求 `win32-x64` 架构正确,禁止 MinGW runtime DLL 和动态 MSVC runtime DLL。
23
+ - Linux: 解析 ELF header,要求 `x64` / `arm64` 架构正确;用 `readelf` 检查动态库,只允许 glibc 系统库和系统动态加载器;要求 GLIBC 版本不高于 `2.35`。
24
+ - macOS: 直接解析 Mach-O header 和 load commands,要求 `x64` / `arm64` 架构正确,禁止 Homebrew/非系统 dylib,要求最低 macOS 版本不高于 `12.0`。
25
+
26
+ 如果这些检查失败,workflow 必须失败,不能发布。
27
+
28
+ ## npm Trusted Publishing
29
+
30
+ 本仓库使用 npm Trusted Publishing/OIDC,不需要 `NPM_TOKEN`。
31
+
32
+ 在 npmjs.com 配置:
33
+
34
+ 1. 打开 package 的 Settings。
35
+ 2. 找到 Trusted publishing。
36
+ 3. 选择 GitHub Actions。
37
+ 4. 填写:
38
+ - Organization or user: `eeg1412`
39
+ - Repository: `libavif-with-gainmap`
40
+ - Workflow filename: `release.yml`
41
+ - Allowed actions: `npm publish`
42
+
43
+ npm 官方要求 Trusted Publishing 使用支持 OIDC 的云端 CI runner。这个 workflow 已经配置:
44
+
45
+ ```yaml
46
+ permissions:
47
+ contents: read
48
+ id-token: write
20
49
  ```
21
50
 
22
- 然后创建 tag:
51
+ 正式发布:
23
52
 
24
53
  ```sh
25
- git tag v0.1.0
26
- git push origin v0.1.0
54
+ git tag v0.1.5
55
+ git push origin v0.1.5
27
56
  ```
28
57
 
29
58
  ## 本地构筑当前平台
30
59
 
31
- 需要 `git`、`cmake`、`ninja`、`nasm`、Node.js 18+。
60
+ 需要 `git`、`cmake`、`ninja`、`nasm`、Node.js 18+。Windows 构筑必须能直接执行 `nasm -v`,否则 `aom` 的 CMake 配置会失败。
32
61
 
33
62
  ```sh
34
63
  npm test
@@ -52,6 +81,7 @@ vendor/<platform>-<arch>/
52
81
  - `LIBAVIF_INSTALL_DIR`: 自定义 CMake install 目录。
53
82
  - `LIBAVIF_CMAKE_ARGS`: 追加 CMake 参数。
54
83
  - `TARGET_PLATFORM_KEY`: 写入哪个 `vendor/<key>` 目录,默认当前平台。
84
+ - `MACOSX_DEPLOYMENT_TARGET`: macOS 最低部署版本,默认 `12.0`。
55
85
 
56
86
  示例:
57
87
 
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "libavif-with-gainmap",
3
- "version": "0.1.0",
3
+ "version": "0.1.5",
4
4
  "description": "Convert JPEG gain map images to AVIF gain map images using libavif.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "registry": "https://registry.npmjs.org"
10
+ },
7
11
  "repository": {
8
12
  "type": "git",
9
13
  "url": "git+https://github.com/eeg1412/libavif-with-gainmap.git"
@@ -20,6 +20,11 @@ const buildDir = process.env.LIBAVIF_BUILD_DIR || path.join(cacheDir, `build-${L
20
20
  const installDir = process.env.LIBAVIF_INSTALL_DIR || path.join(cacheDir, `install-${LIBAVIF_VERSION}`);
21
21
  const platformKey = process.env.TARGET_PLATFORM_KEY || getPlatformKey();
22
22
  const vendorDir = path.join(root, 'vendor', platformKey);
23
+ const macosDeploymentTarget = process.env.MACOSX_DEPLOYMENT_TARGET || '12.0';
24
+
25
+ function isMultiConfigGenerator(generator) {
26
+ return /visual studio|xcode|ninja multi-config/i.test(generator);
27
+ }
23
28
 
24
29
  function run(command, args, options = {}) {
25
30
  const result = spawnSync(command, args, {
@@ -93,6 +98,7 @@ function patchCMake() {
93
98
 
94
99
  function configureAndBuild() {
95
100
  const generator = process.env.CMAKE_GENERATOR || 'Ninja';
101
+ const generatorPlatform = process.env.CMAKE_GENERATOR_PLATFORM;
96
102
  const cmakeArgs = [
97
103
  '-S',
98
104
  sourceDir,
@@ -100,7 +106,6 @@ function configureAndBuild() {
100
106
  buildDir,
101
107
  '-G',
102
108
  generator,
103
- '-DCMAKE_BUILD_TYPE=Release',
104
109
  '-DBUILD_SHARED_LIBS=OFF',
105
110
  '-DAVIF_BUILD_APPS=ON',
106
111
  '-DAVIF_BUILD_TESTS=OFF',
@@ -115,8 +120,25 @@ function configureAndBuild() {
115
120
  ...splitExtraArgs(process.env.LIBAVIF_CMAKE_ARGS)
116
121
  ];
117
122
 
123
+ if (generatorPlatform) {
124
+ cmakeArgs.push('-A', generatorPlatform);
125
+ }
126
+ if (!isMultiConfigGenerator(generator)) {
127
+ cmakeArgs.push('-DCMAKE_BUILD_TYPE=Release');
128
+ }
129
+
118
130
  if (process.platform === 'win32') {
119
- cmakeArgs.push('-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded');
131
+ cmakeArgs.push('-DCMAKE_POLICY_DEFAULT_CMP0091=NEW', '-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded');
132
+ if (generator.toLowerCase().includes('ninja') && !generator.toLowerCase().includes('multi-config')) {
133
+ cmakeArgs.push('-DCMAKE_C_COMPILER=cl', '-DCMAKE_CXX_COMPILER=cl');
134
+ }
135
+ } else if (process.platform === 'linux') {
136
+ cmakeArgs.push(
137
+ '-DCMAKE_EXE_LINKER_FLAGS=-static-libgcc -static-libstdc++',
138
+ '-DCMAKE_SHARED_LINKER_FLAGS=-static-libgcc -static-libstdc++'
139
+ );
140
+ } else if (process.platform === 'darwin') {
141
+ cmakeArgs.push(`-DCMAKE_OSX_DEPLOYMENT_TARGET=${macosDeploymentTarget}`);
120
142
  }
121
143
 
122
144
  run('cmake', cmakeArgs);
@@ -124,44 +146,14 @@ function configureAndBuild() {
124
146
  run('cmake', ['--install', buildDir, '--config', 'Release']);
125
147
  }
126
148
 
127
- function findBuiltBinary(fileName) {
128
- const candidates = [
129
- path.join(installDir, 'bin', fileName),
130
- path.join(buildDir, fileName),
131
- path.join(buildDir, 'Release', fileName)
132
- ];
133
- for (const candidate of candidates) {
134
- if (fs.existsSync(candidate)) {
135
- return candidate;
136
- }
137
- }
138
-
139
- const stack = [buildDir];
140
- while (stack.length > 0) {
141
- const dir = stack.pop();
142
- if (!fs.existsSync(dir)) {
143
- continue;
144
- }
145
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
146
- const fullPath = path.join(dir, entry.name);
147
- if (entry.isDirectory()) {
148
- stack.push(fullPath);
149
- } else if (entry.isFile() && entry.name === fileName) {
150
- return fullPath;
151
- }
152
- }
153
- }
154
- return null;
155
- }
156
-
157
149
  function copyBinaries() {
158
150
  fs.mkdirSync(vendorDir, { recursive: true });
159
151
  for (const tool of [TOOL_GAINMAP_UTIL, TOOL_GAINMAP_RESIZE]) {
160
152
  const name = executableName(tool, platformKey);
161
- const from = findBuiltBinary(name);
153
+ const from = path.join(installDir, 'bin', name);
162
154
  const to = path.join(vendorDir, name);
163
- if (!from) {
164
- throw new Error(`Expected built binary was not found: ${name}`);
155
+ if (!fs.existsSync(from)) {
156
+ throw new Error(`Expected installed binary was not found: ${from}`);
165
157
  }
166
158
  fs.copyFileSync(from, to);
167
159
  if (!platformKey.startsWith('win32-')) {
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
+ const { spawnSync } = require('node:child_process');
5
6
  const {
6
7
  SUPPORTED_PLATFORM_KEYS,
7
8
  TOOL_NAMES,
@@ -13,19 +14,380 @@ const {
13
14
  const mode = process.argv.includes('--all') ? 'all' : 'current';
14
15
  const keys = mode === 'all' ? SUPPORTED_PLATFORM_KEYS : [process.env.TARGET_PLATFORM_KEY || getPlatformKey()];
15
16
  const missing = [];
17
+ const invalid = [];
18
+ const skipped = [];
16
19
 
17
- for (const key of keys) {
18
- for (const tool of TOOL_NAMES) {
19
- const file = path.join(packageRoot(), 'vendor', key, executableName(tool, key));
20
- if (!fs.existsSync(file)) {
21
- missing.push(file);
20
+ const MAX_LINUX_GLIBC = [2, 35]; // Ubuntu 22.04 baseline.
21
+ const MAX_MACOS_MIN_VERSION = [12, 0];
22
+ const MACHO_MAGIC_64_LE = 0xfeedfacf;
23
+ const MACHO_FAT_MAGIC_BE = 0xcafebabe;
24
+ const MACHO_FAT_MAGIC_64_BE = 0xcafebabf;
25
+ const MACHO_CPU_X86_64 = 0x01000007;
26
+ const MACHO_CPU_ARM64 = 0x0100000c;
27
+ const MACHO_LC_LOAD_DYLIB = 0x0c;
28
+ const MACHO_LC_LOAD_WEAK_DYLIB = 0x80000018;
29
+ const MACHO_LC_REEXPORT_DYLIB = 0x8000001f;
30
+ const MACHO_LC_LAZY_LOAD_DYLIB = 0x20;
31
+ const MACHO_LC_LOAD_UPWARD_DYLIB = 0x80000023;
32
+ const MACHO_LC_VERSION_MIN_MACOSX = 0x24;
33
+ const MACHO_LC_BUILD_VERSION = 0x32;
34
+ const MACHO_PLATFORM_MACOS = 1;
35
+
36
+ const FORBIDDEN_WINDOWS_RUNTIME_DLLS = new Set([
37
+ 'libgcc_s_seh-1.dll',
38
+ 'libgcc_s_dw2-1.dll',
39
+ 'libgcc_s_sjlj-1.dll',
40
+ 'libstdc++-6.dll',
41
+ 'libwinpthread-1.dll',
42
+ 'msvcp140.dll',
43
+ 'msvcp140_1.dll',
44
+ 'msvcp140_2.dll',
45
+ 'vcruntime140.dll',
46
+ 'vcruntime140_1.dll'
47
+ ]);
48
+
49
+ const ALLOWED_WINDOWS_DLLS = new Set([
50
+ 'advapi32.dll',
51
+ 'bcrypt.dll',
52
+ 'combase.dll',
53
+ 'crypt32.dll',
54
+ 'gdi32.dll',
55
+ 'imm32.dll',
56
+ 'kernel32.dll',
57
+ 'msvcrt.dll',
58
+ 'ntdll.dll',
59
+ 'ole32.dll',
60
+ 'oleaut32.dll',
61
+ 'rpcrt4.dll',
62
+ 'sechost.dll',
63
+ 'shell32.dll',
64
+ 'shlwapi.dll',
65
+ 'user32.dll',
66
+ 'version.dll',
67
+ 'winmm.dll',
68
+ 'ws2_32.dll'
69
+ ]);
70
+
71
+ const ALLOWED_LINUX_NEEDED = new Set([
72
+ 'libc.so.6',
73
+ 'libdl.so.2',
74
+ 'ld-linux-aarch64.so.1',
75
+ 'ld-linux-x86-64.so.2',
76
+ 'libm.so.6',
77
+ 'libpthread.so.0',
78
+ 'libresolv.so.2',
79
+ 'librt.so.1'
80
+ ]);
81
+
82
+ function run(command, args) {
83
+ const result = spawnSync(command, args, { encoding: 'utf8', shell: false });
84
+ if (result.error) {
85
+ throw result.error;
86
+ }
87
+ if (result.status !== 0) {
88
+ throw new Error(`${command} ${args.join(' ')} failed:\n${result.stderr || result.stdout}`);
89
+ }
90
+ return result.stdout;
91
+ }
92
+
93
+ function compareVersions(left, right) {
94
+ const length = Math.max(left.length, right.length);
95
+ for (let i = 0; i < length; i += 1) {
96
+ const a = left[i] || 0;
97
+ const b = right[i] || 0;
98
+ if (a !== b) {
99
+ return a - b;
100
+ }
101
+ }
102
+ return 0;
103
+ }
104
+
105
+ function parsePe(file) {
106
+ const buffer = fs.readFileSync(file);
107
+ const u16 = (offset) => buffer.readUInt16LE(offset);
108
+ const u32 = (offset) => buffer.readUInt32LE(offset);
109
+ const peOffset = u32(0x3c);
110
+ if (buffer.toString('ascii', peOffset, peOffset + 4) !== 'PE\0\0') {
111
+ throw new Error(`${file} is not a PE executable.`);
112
+ }
113
+
114
+ const coffOffset = peOffset + 4;
115
+ const machine = u16(coffOffset);
116
+ const sectionCount = u16(coffOffset + 2);
117
+ const optionalHeaderSize = u16(coffOffset + 16);
118
+ const optionalHeaderOffset = coffOffset + 20;
119
+ const optionalMagic = u16(optionalHeaderOffset);
120
+ const dataDirectoryOffset = optionalHeaderOffset + (optionalMagic === 0x20b ? 112 : 96);
121
+ const importTableRva = u32(dataDirectoryOffset + 8);
122
+ if (importTableRva === 0) {
123
+ return { imports: [], machine };
124
+ }
125
+
126
+ const sectionTableOffset = optionalHeaderOffset + optionalHeaderSize;
127
+ const sections = [];
128
+ for (let i = 0; i < sectionCount; i += 1) {
129
+ const offset = sectionTableOffset + i * 40;
130
+ sections.push({
131
+ rawOffset: u32(offset + 20),
132
+ rawSize: u32(offset + 16),
133
+ rva: u32(offset + 12),
134
+ virtualSize: u32(offset + 8)
135
+ });
136
+ }
137
+
138
+ function rvaToOffset(rva) {
139
+ for (const section of sections) {
140
+ const size = Math.max(section.rawSize, section.virtualSize);
141
+ if (rva >= section.rva && rva < section.rva + size) {
142
+ return section.rawOffset + (rva - section.rva);
143
+ }
144
+ }
145
+ throw new Error(`RVA not mapped in ${file}: 0x${rva.toString(16)}`);
146
+ }
147
+
148
+ function readCString(offset) {
149
+ let end = offset;
150
+ while (end < buffer.length && buffer[end] !== 0) {
151
+ end += 1;
152
+ }
153
+ return buffer.toString('ascii', offset, end);
154
+ }
155
+
156
+ const imports = [];
157
+ let descriptorOffset = rvaToOffset(importTableRva);
158
+ while (true) {
159
+ const originalFirstThunk = u32(descriptorOffset);
160
+ const nameRva = u32(descriptorOffset + 12);
161
+ const firstThunk = u32(descriptorOffset + 16);
162
+ if (!originalFirstThunk && !nameRva && !firstThunk) {
163
+ break;
164
+ }
165
+ imports.push(readCString(rvaToOffset(nameRva)).toLowerCase());
166
+ descriptorOffset += 20;
167
+ }
168
+ return { imports, machine };
169
+ }
170
+
171
+ function isAllowedWindowsImport(name) {
172
+ return (
173
+ ALLOWED_WINDOWS_DLLS.has(name) ||
174
+ name.startsWith('api-ms-win-') ||
175
+ name.startsWith('ext-ms-win-')
176
+ );
177
+ }
178
+
179
+ function checkWindows(file, key) {
180
+ const pe = parsePe(file);
181
+ if (key === 'win32-x64' && pe.machine !== 0x8664) {
182
+ invalid.push(`${file}: expected PE x64 machine 0x8664, got 0x${pe.machine.toString(16)}`);
183
+ }
184
+
185
+ const forbidden = pe.imports.filter((name) => FORBIDDEN_WINDOWS_RUNTIME_DLLS.has(name));
186
+ if (forbidden.length > 0) {
187
+ invalid.push(`${file}: imports forbidden runtime DLLs ${forbidden.join(', ')}`);
188
+ }
189
+
190
+ const unexpected = pe.imports.filter((name) => !isAllowedWindowsImport(name));
191
+ if (unexpected.length > 0) {
192
+ invalid.push(`${file}: imports unexpected DLLs ${unexpected.join(', ')}`);
193
+ }
194
+ }
195
+
196
+ function parseElfHeader(file) {
197
+ const buffer = fs.readFileSync(file);
198
+ if (buffer[0] !== 0x7f || buffer.toString('ascii', 1, 4) !== 'ELF') {
199
+ throw new Error(`${file} is not an ELF executable.`);
200
+ }
201
+ const elfClass = buffer[4];
202
+ const endian = buffer[5];
203
+ if (elfClass !== 2 || endian !== 1) {
204
+ throw new Error(`${file} must be 64-bit little-endian ELF.`);
205
+ }
206
+ return { machine: buffer.readUInt16LE(18) };
207
+ }
208
+
209
+ function readCString(buffer, offset, limit) {
210
+ let end = offset;
211
+ while (end < limit && buffer[end] !== 0) {
212
+ end += 1;
213
+ }
214
+ return buffer.toString('utf8', offset, end);
215
+ }
216
+
217
+ function decodeMachOVersion(value) {
218
+ return [(value >> 16) & 0xffff, (value >> 8) & 0xff, value & 0xff];
219
+ }
220
+
221
+ function parseMachO(file) {
222
+ const buffer = fs.readFileSync(file);
223
+ if (buffer.length < 32) {
224
+ throw new Error(`${file} is too small to be a Mach-O executable.`);
225
+ }
226
+
227
+ const magicLE = buffer.readUInt32LE(0);
228
+ const magicBE = buffer.readUInt32BE(0);
229
+ if (magicBE === MACHO_FAT_MAGIC_BE || magicBE === MACHO_FAT_MAGIC_64_BE) {
230
+ throw new Error(`${file} is a universal Mach-O binary; expected one platform-specific thin binary.`);
231
+ }
232
+ if (magicLE !== MACHO_MAGIC_64_LE) {
233
+ throw new Error(`${file} is not a 64-bit little-endian Mach-O executable.`);
234
+ }
235
+
236
+ const cpuType = buffer.readUInt32LE(4);
237
+ const commandCount = buffer.readUInt32LE(16);
238
+ const commandsSize = buffer.readUInt32LE(20);
239
+ const commandsStart = 32;
240
+ const commandsEnd = commandsStart + commandsSize;
241
+ if (commandsEnd > buffer.length) {
242
+ throw new Error(`${file} has invalid Mach-O load command bounds.`);
243
+ }
244
+
245
+ const dylibCommands = new Set([
246
+ MACHO_LC_LOAD_DYLIB,
247
+ MACHO_LC_LOAD_WEAK_DYLIB,
248
+ MACHO_LC_REEXPORT_DYLIB,
249
+ MACHO_LC_LAZY_LOAD_DYLIB,
250
+ MACHO_LC_LOAD_UPWARD_DYLIB
251
+ ]);
252
+ const dylibs = [];
253
+ const minVersions = [];
254
+ let commandOffset = commandsStart;
255
+
256
+ for (let i = 0; i < commandCount; i += 1) {
257
+ if (commandOffset + 8 > commandsEnd) {
258
+ throw new Error(`${file} has a truncated Mach-O load command.`);
22
259
  }
260
+ const command = buffer.readUInt32LE(commandOffset);
261
+ const commandSize = buffer.readUInt32LE(commandOffset + 4);
262
+ if (commandSize < 8 || commandOffset + commandSize > commandsEnd) {
263
+ throw new Error(`${file} has an invalid Mach-O load command size.`);
264
+ }
265
+
266
+ if (dylibCommands.has(command)) {
267
+ const nameOffset = buffer.readUInt32LE(commandOffset + 8);
268
+ if (nameOffset >= commandSize) {
269
+ throw new Error(`${file} has an invalid Mach-O dylib name offset.`);
270
+ }
271
+ dylibs.push(readCString(buffer, commandOffset + nameOffset, commandOffset + commandSize));
272
+ } else if (command === MACHO_LC_BUILD_VERSION) {
273
+ if (commandSize < 24) {
274
+ throw new Error(`${file} has an invalid LC_BUILD_VERSION command size.`);
275
+ }
276
+ const platform = buffer.readUInt32LE(commandOffset + 8);
277
+ if (platform === MACHO_PLATFORM_MACOS) {
278
+ minVersions.push(decodeMachOVersion(buffer.readUInt32LE(commandOffset + 12)));
279
+ }
280
+ } else if (command === MACHO_LC_VERSION_MIN_MACOSX) {
281
+ if (commandSize < 16) {
282
+ throw new Error(`${file} has an invalid LC_VERSION_MIN_MACOSX command size.`);
283
+ }
284
+ minVersions.push(decodeMachOVersion(buffer.readUInt32LE(commandOffset + 8)));
285
+ }
286
+
287
+ commandOffset += commandSize;
23
288
  }
289
+
290
+ return { cpuType, dylibs, minVersions };
24
291
  }
25
292
 
26
- if (missing.length > 0) {
27
- process.stderr.write(`Missing native binaries:\n${missing.map((file) => ` - ${file}`).join('\n')}\n`);
28
- process.exitCode = 1;
29
- } else {
30
- process.stdout.write(`Prebuilt binary check passed for ${keys.join(', ')}.\n`);
293
+ function checkLinux(file, key) {
294
+ const elf = parseElfHeader(file);
295
+ const expectedMachine = key === 'linux-x64' ? 62 : 183;
296
+ if (elf.machine !== expectedMachine) {
297
+ invalid.push(`${file}: expected ELF machine ${expectedMachine}, got ${elf.machine}`);
298
+ }
299
+
300
+ const dynamic = run('readelf', ['-d', file]);
301
+ const needed = [...dynamic.matchAll(/Shared library: \[(.+?)\]/g)].map((match) => match[1]);
302
+ const unexpected = needed.filter((name) => !ALLOWED_LINUX_NEEDED.has(name));
303
+ if (unexpected.length > 0) {
304
+ invalid.push(`${file}: links unexpected Linux shared libraries ${unexpected.join(', ')}`);
305
+ }
306
+
307
+ const versionInfo = run('readelf', ['--version-info', file]);
308
+ const glibcVersions = [...versionInfo.matchAll(/GLIBC_(\d+)\.(\d+)/g)].map((match) => [
309
+ Number(match[1]),
310
+ Number(match[2])
311
+ ]);
312
+ const tooNew = glibcVersions.filter((version) => compareVersions(version, MAX_LINUX_GLIBC) > 0);
313
+ if (tooNew.length > 0) {
314
+ const unique = [...new Set(tooNew.map((version) => version.join('.')))].sort();
315
+ invalid.push(`${file}: requires GLIBC newer than ${MAX_LINUX_GLIBC.join('.')}: ${unique.join(', ')}`);
316
+ }
31
317
  }
318
+
319
+ function checkMac(file, key) {
320
+ const macho = parseMachO(file);
321
+ const expectedCpu = key === 'darwin-x64' ? MACHO_CPU_X86_64 : MACHO_CPU_ARM64;
322
+ if (macho.cpuType !== expectedCpu) {
323
+ invalid.push(
324
+ `${file}: expected Mach-O CPU 0x${expectedCpu.toString(16)}, got 0x${macho.cpuType.toString(16)}`
325
+ );
326
+ }
327
+
328
+ const unexpected = macho.dylibs.filter(
329
+ (name) => !(name.startsWith('/usr/lib/') || name.startsWith('/System/Library/'))
330
+ );
331
+ if (unexpected.length > 0) {
332
+ invalid.push(`${file}: links non-system macOS libraries ${unexpected.join(', ')}`);
333
+ }
334
+
335
+ if (macho.minVersions.length === 0) {
336
+ invalid.push(`${file}: missing macOS minimum deployment target load command`);
337
+ }
338
+ const tooNew = macho.minVersions.filter((version) => compareVersions(version, MAX_MACOS_MIN_VERSION) > 0);
339
+ if (tooNew.length > 0) {
340
+ invalid.push(
341
+ `${file}: minimum macOS version exceeds ${MAX_MACOS_MIN_VERSION.join('.')}: ` +
342
+ tooNew.map((version) => version.join('.')).join(', ')
343
+ );
344
+ }
345
+ }
346
+
347
+ function inspectBinary(file, key) {
348
+ if (key.startsWith('win32-')) {
349
+ checkWindows(file, key);
350
+ } else if (key.startsWith('linux-')) {
351
+ if (process.platform !== 'linux') {
352
+ skipped.push(`${file}: Linux dependency inspection requires readelf on Linux.`);
353
+ return;
354
+ }
355
+ checkLinux(file, key);
356
+ } else if (key.startsWith('darwin-')) {
357
+ checkMac(file, key);
358
+ }
359
+ }
360
+
361
+ function main() {
362
+ for (const key of keys) {
363
+ for (const tool of TOOL_NAMES) {
364
+ const file = path.join(packageRoot(), 'vendor', key, executableName(tool, key));
365
+ if (!fs.existsSync(file)) {
366
+ missing.push(file);
367
+ continue;
368
+ }
369
+ inspectBinary(file, key);
370
+ }
371
+ }
372
+
373
+ if (missing.length > 0) {
374
+ process.stderr.write(`Missing native binaries:\n${missing.map((file) => ` - ${file}`).join('\n')}\n`);
375
+ process.exitCode = 1;
376
+ } else if (invalid.length > 0) {
377
+ process.stderr.write(`Invalid native binaries:\n${invalid.map((file) => ` - ${file}`).join('\n')}\n`);
378
+ process.exitCode = 1;
379
+ } else {
380
+ process.stdout.write(`Prebuilt binary check passed for ${keys.join(', ')}.\n`);
381
+ if (skipped.length > 0) {
382
+ process.stdout.write(`Skipped cross-OS inspections:\n${skipped.map((line) => ` - ${line}`).join('\n')}\n`);
383
+ }
384
+ }
385
+ }
386
+
387
+ if (require.main === module) {
388
+ main();
389
+ }
390
+
391
+ module.exports = {
392
+ parseMachO
393
+ };
package/src/process.js CHANGED
@@ -18,6 +18,22 @@ class AvifGainMapError extends Error {
18
18
  }
19
19
  }
20
20
 
21
+ const WINDOWS_STATUS_DLL_NOT_FOUND = 0xc0000135;
22
+
23
+ function isWindowsDllNotFound(exitCode) {
24
+ return exitCode === WINDOWS_STATUS_DLL_NOT_FOUND || exitCode === -1073741515;
25
+ }
26
+
27
+ function formatExitMessage(command, exitCode) {
28
+ let message = `${command} exited with code ${exitCode}.`;
29
+ if (process.platform === 'win32' && isWindowsDllNotFound(exitCode)) {
30
+ message +=
31
+ ' Windows status 0xC0000135 means the native executable could not start because a required DLL was not found. ' +
32
+ 'Upgrade libavif-with-gainmap or reinstall a package version whose Windows binaries are built with MSVC.';
33
+ }
34
+ return message;
35
+ }
36
+
21
37
  function runFile(command, args, options = {}) {
22
38
  return new Promise((resolve, reject) => {
23
39
  const child = spawn(command, args, {
@@ -63,7 +79,7 @@ function runFile(command, args, options = {}) {
63
79
  return;
64
80
  }
65
81
  reject(
66
- new AvifGainMapError(`${command} exited with code ${exitCode}.`, {
82
+ new AvifGainMapError(formatExitMessage(command, exitCode), {
67
83
  ...result
68
84
  })
69
85
  );
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file