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 +20 -0
- package/docs/BUILDING.md +42 -12
- package/package.json +5 -1
- package/scripts/build-libavif.js +27 -35
- package/scripts/check-prebuilt.js +372 -10
- package/src/process.js +17 -1
- package/vendor/darwin-arm64/avifgainmapresize +0 -0
- package/vendor/darwin-arm64/avifgainmaputil +0 -0
- package/vendor/darwin-x64/avifgainmapresize +0 -0
- package/vendor/darwin-x64/avifgainmaputil +0 -0
- package/vendor/linux-arm64/avifgainmapresize +0 -0
- package/vendor/linux-arm64/avifgainmaputil +0 -0
- package/vendor/linux-x64/avifgainmapresize +0 -0
- package/vendor/linux-x64/avifgainmaputil +0 -0
- package/vendor/win32-x64/avifgainmapresize.exe +0 -0
- package/vendor/win32-x64/avifgainmaputil.exe +0 -0
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-
|
|
10
|
-
- `linux-arm64` on `ubuntu-
|
|
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-
|
|
13
|
-
- `win32-x64` on `windows-
|
|
14
|
-
3.
|
|
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
|
-
|
|
16
|
+
手动运行 workflow 只会构筑和打包,不会发布。只有推送 `v*` tag 时,`publish npm` job 才会运行。
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
51
|
+
正式发布:
|
|
23
52
|
|
|
24
53
|
```sh
|
|
25
|
-
git tag v0.1.
|
|
26
|
-
git push origin v0.1.
|
|
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.
|
|
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"
|
package/scripts/build-libavif.js
CHANGED
|
@@ -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 =
|
|
153
|
+
const from = path.join(installDir, 'bin', name);
|
|
162
154
|
const to = path.join(vendorDir, name);
|
|
163
|
-
if (!from) {
|
|
164
|
-
throw new Error(`Expected
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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(
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|