native-dawn 0.0.0-stage → 0.1.1
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/CMakeLists.txt +90 -0
- package/LICENSE +21 -0
- package/NOTICE +9 -0
- package/README.md +255 -2
- package/bin/native-dawn.js +12 -0
- package/include/native_dawn/sdl2.h +88 -0
- package/include/native_dawn/window.h +102 -0
- package/index.d.ts +56 -0
- package/index.js +13 -0
- package/package.json +75 -4
- package/paths.d.ts +18 -0
- package/paths.js +22 -0
- package/scripts/build.mjs +187 -0
- package/scripts/check-release.mjs +13 -0
- package/scripts/common.mjs +47 -0
- package/scripts/fetch-dependencies.py +47 -0
- package/scripts/install.mjs +82 -0
- package/scripts/package-binary.mjs +14 -0
- package/scripts/test-android.mjs +43 -0
- package/scripts/test-c.mjs +29 -0
- package/scripts/test-package.mjs +34 -0
- package/scripts/test.mjs +11 -0
- package/src/node/extensions.cpp +285 -0
- package/src/node/extensions.h +6 -0
- package/upstream.json +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,77 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "native-dawn",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Prebuilt Dawn WebGPU for Node.js and native code: shared library, C headers, and Node-API bindings",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"import": "./index.js"
|
|
12
|
+
},
|
|
13
|
+
"./paths": {
|
|
14
|
+
"types": "./paths.d.ts",
|
|
15
|
+
"import": "./paths.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"native-dawn": "bin/native-dawn.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"index.js",
|
|
24
|
+
"index.d.ts",
|
|
25
|
+
"paths.js",
|
|
26
|
+
"paths.d.ts",
|
|
27
|
+
"bin",
|
|
28
|
+
"include",
|
|
29
|
+
"src",
|
|
30
|
+
"scripts/*.mjs",
|
|
31
|
+
"scripts/*.py",
|
|
32
|
+
"CMakeLists.txt",
|
|
33
|
+
"upstream.json",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"NOTICE",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=22"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"install": "node scripts/install.mjs",
|
|
43
|
+
"build": "node scripts/build.mjs",
|
|
44
|
+
"test": "node scripts/test.mjs",
|
|
45
|
+
"test:c": "node scripts/test-c.mjs",
|
|
46
|
+
"test:window": "node test/window.mjs",
|
|
47
|
+
"test:package": "node scripts/test-package.mjs",
|
|
48
|
+
"package:binary": "node scripts/package-binary.mjs",
|
|
49
|
+
"test:types": "tsc -p test/tsconfig.json",
|
|
50
|
+
"test:android": "node scripts/test-android.mjs"
|
|
51
|
+
},
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/monteslu/native-dawn.git"
|
|
55
|
+
},
|
|
56
|
+
"author": "Luis Montes",
|
|
57
|
+
"license": "MIT",
|
|
58
|
+
"keywords": [
|
|
59
|
+
"webgpu",
|
|
60
|
+
"dawn",
|
|
61
|
+
"wgsl",
|
|
62
|
+
"gpu",
|
|
63
|
+
"native",
|
|
64
|
+
"vulkan",
|
|
65
|
+
"metal",
|
|
66
|
+
"d3d12"
|
|
67
|
+
],
|
|
68
|
+
"dependencies": {
|
|
69
|
+
"@webgpu/types": "0.1.72",
|
|
70
|
+
"tar": "7.5.22"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@kmamal/sdl": "0.11.13",
|
|
74
|
+
"@types/node": "^22.0.0",
|
|
75
|
+
"typescript": "^5.9.3"
|
|
76
|
+
}
|
|
77
|
+
}
|
package/paths.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface NativeDawnPaths {
|
|
2
|
+
/** `${process.platform}-${process.arch}` */
|
|
3
|
+
readonly target: string
|
|
4
|
+
/** Install prefix: pass it as CMAKE_PREFIX_PATH to find_package(Dawn). */
|
|
5
|
+
readonly prefix: string
|
|
6
|
+
readonly include: string
|
|
7
|
+
readonly lib: string
|
|
8
|
+
readonly bin: string
|
|
9
|
+
readonly cmake: string
|
|
10
|
+
/** The shared library: libwebgpu_dawn.so, libwebgpu_dawn.dylib or webgpu_dawn.dll. */
|
|
11
|
+
readonly library: string
|
|
12
|
+
readonly addon: string
|
|
13
|
+
/** Dawn's machine-readable description of the WebGPU API. */
|
|
14
|
+
readonly dawnJson: string
|
|
15
|
+
readonly licenses: string
|
|
16
|
+
}
|
|
17
|
+
export declare const paths: NativeDawnPaths
|
|
18
|
+
export default paths
|
package/paths.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Locations inside the installed native build. Importing this module does not
|
|
2
|
+
// load the Node addon, so build scripts for C and C++ code can use it.
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
const target = `${process.platform}-${process.arch}`
|
|
7
|
+
const prefix = fileURLToPath(new URL(`./dist/${target}/`, import.meta.url))
|
|
8
|
+
const binaries = path.join(prefix, process.platform === 'win32' ? 'bin' : 'lib')
|
|
9
|
+
|
|
10
|
+
export const paths = Object.freeze({
|
|
11
|
+
target,
|
|
12
|
+
prefix,
|
|
13
|
+
include: path.join(prefix, 'include'),
|
|
14
|
+
lib: path.join(prefix, 'lib'),
|
|
15
|
+
bin: path.join(prefix, 'bin'),
|
|
16
|
+
cmake: path.join(prefix, 'lib', 'cmake'),
|
|
17
|
+
library: path.join(binaries, { win32: 'webgpu_dawn.dll', darwin: 'libwebgpu_dawn.dylib' }[process.platform] ?? 'libwebgpu_dawn.so'),
|
|
18
|
+
addon: path.join(binaries, 'dawn.node'),
|
|
19
|
+
dawnJson: path.join(prefix, 'share', 'native-dawn', 'dawn.json'),
|
|
20
|
+
licenses: path.join(prefix, 'share', 'native-dawn', 'licenses'),
|
|
21
|
+
})
|
|
22
|
+
export default paths
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import { spawnSync } from 'node:child_process'
|
|
5
|
+
import { root, run, upstream, target, targets, pkg, distDir, binDir, assertTarget, isAndroid, hasAddon, insertText } from './common.mjs'
|
|
6
|
+
|
|
7
|
+
assertTarget()
|
|
8
|
+
const dawn = path.join(root, '.cache/dawn')
|
|
9
|
+
|
|
10
|
+
// The NDK: ANDROID_NDK_HOME or ANDROID_NDK_ROOT, else the newest under the SDK.
|
|
11
|
+
function androidNdk() {
|
|
12
|
+
for (const name of ['ANDROID_NDK_HOME', 'ANDROID_NDK_ROOT', 'ANDROID_NDK_LATEST_HOME']) {
|
|
13
|
+
if (process.env[name] && fs.existsSync(process.env[name])) return process.env[name]
|
|
14
|
+
}
|
|
15
|
+
const sdk = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT
|
|
16
|
+
const dir = sdk && path.join(sdk, 'ndk')
|
|
17
|
+
const versions = dir && fs.existsSync(dir) ? fs.readdirSync(dir).sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) : []
|
|
18
|
+
if (!versions.length) throw new Error('Android builds need the NDK: set ANDROID_NDK_HOME')
|
|
19
|
+
return path.join(dir, versions.at(-1))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// A Go toolchain unpacked into .cache/go is used when none is on PATH.
|
|
23
|
+
const env = { ...process.env }
|
|
24
|
+
const localGo = path.join(root, '.cache/go/bin')
|
|
25
|
+
if (fs.existsSync(localGo)) env.PATH = `${localGo}${path.delimiter}${env.PATH}`
|
|
26
|
+
|
|
27
|
+
fs.mkdirSync(dawn, { recursive: true })
|
|
28
|
+
if (!fs.existsSync(path.join(dawn, '.git'))) run('git', ['init', '-q'], { cwd: dawn })
|
|
29
|
+
const rev = spawnSync('git', ['rev-parse', '--verify', 'HEAD'], { cwd: dawn, encoding: 'utf8' }).stdout?.trim()
|
|
30
|
+
if (rev !== upstream.dawn.revision) {
|
|
31
|
+
run('git', ['fetch', '--depth', '1', upstream.dawn.url, upstream.dawn.revision], { cwd: dawn })
|
|
32
|
+
run('git', ['checkout', '--detach', upstream.dawn.revision], { cwd: dawn })
|
|
33
|
+
}
|
|
34
|
+
run(process.platform === 'win32' ? 'python' : 'python3', [path.join(root, 'scripts/fetch-dependencies.py'), dawn])
|
|
35
|
+
|
|
36
|
+
// Small, checked hooks into Dawn's Node bindings. Everything else lives in src/node.
|
|
37
|
+
function insert(relative, needle, replacement) {
|
|
38
|
+
const file = path.join(dawn, relative)
|
|
39
|
+
const text = fs.readFileSync(file, 'utf8')
|
|
40
|
+
const patched = insertText(text, needle, replacement)
|
|
41
|
+
if (patched === null) throw new Error(`Dawn changed around a native-dawn hook in ${relative}; update scripts/build.mjs`)
|
|
42
|
+
if (patched !== text) fs.writeFileSync(file, patched)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
insert('src/dawn/node/Module.cpp', '#include "dawn/dawn_proc.h"', '#include "dawn/dawn_proc.h"\n#include "extensions.h"')
|
|
46
|
+
// The addon calls webgpu_dawn's exported functions directly, so Dawn's proc
|
|
47
|
+
// table (and the dawn_proc library behind it) is not used.
|
|
48
|
+
insert('src/dawn/node/Module.cpp', ' dawnProcSetProcs(&dawn::native::GetProcs());', ' // native-dawn: wgpu* calls go straight to the shared webgpu_dawn library.')
|
|
49
|
+
insert('src/dawn/node/Module.cpp', ' return exports;', ' native_dawn::Initialize(env, exports);\n return exports;')
|
|
50
|
+
insert('src/dawn/node/binding/GPUDevice.h', ' ~GPUDevice() override;', ' ~GPUDevice() override;\n wgpu::Device GetNativeDawnDevice() const { return device_; }')
|
|
51
|
+
// Lifetime fixes for Dawn's Node bindings.
|
|
52
|
+
//
|
|
53
|
+
// The polling runner shares the instance with GPU. Upstream it holds a raw
|
|
54
|
+
// pointer, while adapters, devices, buffers and queues keep the runner alive,
|
|
55
|
+
// so once the JavaScript GPU object is collected the next poll reads a freed
|
|
56
|
+
// instance and the process dies.
|
|
57
|
+
insert('src/dawn/node/binding/AsyncRunner.h', ' static std::shared_ptr<AsyncRunner> Create(dawn::native::Instance* instance);', ' static std::shared_ptr<AsyncRunner> Create(std::shared_ptr<dawn::native::Instance> instance);')
|
|
58
|
+
insert('src/dawn/node/binding/AsyncRunner.h', ' explicit AsyncRunner(dawn::native::Instance* instance);', ' explicit AsyncRunner(std::shared_ptr<dawn::native::Instance> instance);')
|
|
59
|
+
insert('src/dawn/node/binding/AsyncRunner.h', ' const dawn::native::Instance* const instance_;', ' // native-dawn: shared with GPU, so a poll never outlives the instance.\n const std::shared_ptr<dawn::native::Instance> instance_;')
|
|
60
|
+
insert('src/dawn/node/binding/AsyncRunner.cpp', 'std::shared_ptr<AsyncRunner> AsyncRunner::Create(dawn::native::Instance* instance) {\n auto runner = std::make_shared<AsyncRunner>(instance);', 'std::shared_ptr<AsyncRunner> AsyncRunner::Create(std::shared_ptr<dawn::native::Instance> instance) {\n auto runner = std::make_shared<AsyncRunner>(std::move(instance));')
|
|
61
|
+
insert('src/dawn/node/binding/AsyncRunner.cpp', 'AsyncRunner::AsyncRunner(dawn::native::Instance* instance) : instance_(instance) {}', 'AsyncRunner::AsyncRunner(std::shared_ptr<dawn::native::Instance> instance)\n : instance_(std::move(instance)) {}')
|
|
62
|
+
insert('src/dawn/node/binding/GPU.h', ' std::unique_ptr<dawn::native::Instance> instance_;', ' std::shared_ptr<dawn::native::Instance> instance_;')
|
|
63
|
+
insert('src/dawn/node/binding/GPU.cpp', ' instance_ = std::make_unique<dawn::native::Instance>(', ' instance_ = std::make_shared<dawn::native::Instance>(')
|
|
64
|
+
insert('src/dawn/node/binding/GPU.cpp', ' async_ = AsyncRunner::Create(instance_.get());', ' async_ = AsyncRunner::Create(instance_);')
|
|
65
|
+
// A live device is not pending work. Upstream counts the device.lost promise
|
|
66
|
+
// as a pending task, so every device keeps setImmediate polling for as long
|
|
67
|
+
// as it exists: a full CPU core while idle, and Node never exits on its own.
|
|
68
|
+
insert('src/dawn/node/binding/AsyncRunner.h', ' inline ~AsyncContext() { runner_->End(); }', ' inline ~AsyncContext() {\n if (!detached_) {\n runner_->End();\n }\n }\n\n // native-dawn: stop counting this context as pending work. For promises\n // that may never settle (device.lost), so they do not keep the event\n // loop polling for the life of the object.\n inline void Detach() {\n if (!detached_) {\n detached_ = true;\n runner_->End();\n }\n }')
|
|
69
|
+
insert('src/dawn/node/binding/AsyncRunner.h', ' std::shared_ptr<AsyncRunner> runner_;\n};', ' std::shared_ptr<AsyncRunner> runner_;\n bool detached_ = false;\n};')
|
|
70
|
+
insert('src/dawn/node/binding/GPUAdapter.cpp', ' auto device_lost_ctx = new DeviceLostContext(env, PROMISE_INFO, async_);', ' auto device_lost_ctx = new DeviceLostContext(env, PROMISE_INFO, async_);\n device_lost_ctx->Detach();')
|
|
71
|
+
// With no standing poll, destroy() asks for one so the device-lost callback
|
|
72
|
+
// queued by Dawn is delivered.
|
|
73
|
+
insert('src/dawn/node/binding/GPUDevice.cpp', '"device was destroyed"));\n }\n device_.Destroy();\n destroyed_ = true;', '"device was destroyed"));\n }\n device_.Destroy();\n destroyed_ = true;\n // native-dawn: deliver the queued device-lost callback.\n async_->Begin(env);\n async_->End();')
|
|
74
|
+
|
|
75
|
+
const build = path.join(root, 'build', target)
|
|
76
|
+
const args = ['-S', root, '-B', build, '-DCMAKE_BUILD_TYPE=Release', `-DNATIVE_DAWN_NODE=${hasAddon ? 'ON' : 'OFF'}`]
|
|
77
|
+
// Ninja when a compiler environment is already set up (always on Linux and
|
|
78
|
+
// macOS, and on Windows inside a developer prompt); Visual Studio otherwise.
|
|
79
|
+
if (process.platform !== 'win32' || process.env.VSCMD_VER) args.push('-G', 'Ninja')
|
|
80
|
+
else args.push('-A', process.arch === 'arm64' ? 'ARM64' : 'x64')
|
|
81
|
+
const ndk = isAndroid ? androidNdk() : null
|
|
82
|
+
if (isAndroid) {
|
|
83
|
+
args.push(
|
|
84
|
+
`-DCMAKE_TOOLCHAIN_FILE=${path.join(ndk, 'build/cmake/android.toolchain.cmake')}`,
|
|
85
|
+
'-DANDROID_ABI=arm64-v8a',
|
|
86
|
+
'-DANDROID_PLATFORM=android-26',
|
|
87
|
+
'-DANDROID_STL=c++_static',
|
|
88
|
+
)
|
|
89
|
+
} else if (target !== `${process.platform}-${process.arch}` || !targets.includes(target)) {
|
|
90
|
+
throw new Error(`Cross-compiling ${target} is not supported; build it on a ${target} machine`)
|
|
91
|
+
}
|
|
92
|
+
if (process.platform === 'darwin') args.push(`-DCMAKE_OSX_ARCHITECTURES=${process.arch === 'arm64' ? 'arm64' : 'x86_64'}`, '-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0')
|
|
93
|
+
if (process.env.NATIVE_DAWN_COMPILER_LAUNCHER) {
|
|
94
|
+
const launcher = process.env.NATIVE_DAWN_COMPILER_LAUNCHER
|
|
95
|
+
args.push(`-DCMAKE_C_COMPILER_LAUNCHER=${launcher}`, `-DCMAKE_CXX_COMPILER_LAUNCHER=${launcher}`)
|
|
96
|
+
// sccache cannot cache MSVC's /Zi program databases.
|
|
97
|
+
if (process.platform === 'win32') args.push('-DCMAKE_POLICY_DEFAULT_CMP0141=NEW', '-DCMAKE_MSVC_DEBUG_INFORMATION_FORMAT=Embedded')
|
|
98
|
+
}
|
|
99
|
+
run('cmake', args, { env })
|
|
100
|
+
const jobs = process.env.NATIVE_DAWN_BUILD_JOBS || String(Math.min(8, os.availableParallelism()))
|
|
101
|
+
// Dawn loads dxcompiler.dll at runtime; upstream only builds it as a dependency
|
|
102
|
+
// of the static dawn_native, so ask for it by name.
|
|
103
|
+
const buildTargets = ['webgpu_dawn', ...(hasAddon ? ['dawn_node'] : []), ...(target.startsWith('win32-') ? ['dxcompiler'] : [])]
|
|
104
|
+
run('cmake', ['--build', build, '--config', 'Release', '--target', ...buildTargets, '--parallel', jobs], { env })
|
|
105
|
+
|
|
106
|
+
fs.rmSync(distDir, { recursive: true, force: true })
|
|
107
|
+
run('cmake', ['--install', build, '--config', 'Release', '--prefix', distDir], { env })
|
|
108
|
+
|
|
109
|
+
// Installed static helper libraries and CMake files for them are not part of
|
|
110
|
+
// the SDK: consumers link webgpu_dawn only.
|
|
111
|
+
const libDir = path.join(distDir, 'lib')
|
|
112
|
+
for (const name of fs.readdirSync(libDir)) {
|
|
113
|
+
if (/\.(a)$/.test(name) || (target.startsWith('win32-') && name.endsWith('.lib') && name !== 'webgpu_dawn.lib')) {
|
|
114
|
+
fs.rmSync(path.join(libDir, name))
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// A file from the newest Windows 10/11 SDK, e.g. bin/<version>/arm64/dxil.dll.
|
|
119
|
+
function windowsSdkFile(arch, name) {
|
|
120
|
+
const roots = [process.env.WindowsSdkDir, path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Windows Kits', '10')].filter(Boolean)
|
|
121
|
+
for (const root of roots) {
|
|
122
|
+
const bin = path.join(root, 'bin')
|
|
123
|
+
if (!fs.existsSync(bin)) continue
|
|
124
|
+
const preferred = process.env.WindowsSDKVersion?.replace(/[\\/]+$/, '')
|
|
125
|
+
const versions = fs.readdirSync(bin).filter(v => /^10\./.test(v)).sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))
|
|
126
|
+
for (const version of preferred ? [preferred, ...versions] : versions) {
|
|
127
|
+
const file = path.join(bin, version, arch, name)
|
|
128
|
+
if (fs.existsSync(file)) return file
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
throw new Error(`No ${arch} ${name} in the Windows SDK`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function findFile(dir, name) {
|
|
135
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
136
|
+
const file = path.join(dir, entry.name)
|
|
137
|
+
if (entry.isFile() && entry.name === name) return file
|
|
138
|
+
if (entry.isDirectory()) { const found = findFile(file, name); if (found) return found }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (target.startsWith('win32-')) {
|
|
142
|
+
const arch = target === 'win32-arm64' ? 'arm64' : 'x64'
|
|
143
|
+
const dxcompiler = findFile(build, 'dxcompiler.dll')
|
|
144
|
+
if (!dxcompiler) throw new Error('Build did not produce dxcompiler.dll')
|
|
145
|
+
fs.copyFileSync(dxcompiler, path.join(distDir, 'bin', 'dxcompiler.dll'))
|
|
146
|
+
// dxil.dll (the DXIL validator/signer) comes from the Windows SDK. Dawn's own
|
|
147
|
+
// copy step always takes the x64 one, so pick the target's architecture here.
|
|
148
|
+
fs.copyFileSync(windowsSdkFile(arch, 'dxil.dll'), path.join(distDir, 'bin', 'dxil.dll'))
|
|
149
|
+
// A DLL for the wrong architecture fails to load at runtime with a misleading
|
|
150
|
+
// error, so check every shipped binary's PE machine type.
|
|
151
|
+
const machine = { x64: 0x8664, arm64: 0xaa64 }[arch]
|
|
152
|
+
for (const name of fs.readdirSync(path.join(distDir, 'bin')).filter(n => /\.(dll|node)$/.test(n))) {
|
|
153
|
+
const pe = fs.readFileSync(path.join(distDir, 'bin', name))
|
|
154
|
+
const actual = pe.readUInt16LE(pe.readUInt32LE(0x3c) + 4)
|
|
155
|
+
if (actual !== machine) throw new Error(`${name} is for machine 0x${actual.toString(16)}, not ${arch}`)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (isAndroid) {
|
|
159
|
+
const strip = path.join(ndk, 'toolchains/llvm/prebuilt', `${process.platform === 'darwin' ? 'darwin' : process.platform === 'win32' ? 'windows' : 'linux'}-x86_64`, 'bin', `llvm-strip${process.platform === 'win32' ? '.exe' : ''}`)
|
|
160
|
+
run(strip, ['--strip-unneeded', path.join(distDir, 'lib', 'libwebgpu_dawn.so')])
|
|
161
|
+
} else if (process.platform === 'linux') {
|
|
162
|
+
for (const name of ['dawn.node', 'libwebgpu_dawn.so']) run('strip', ['--strip-unneeded', path.join(distDir, binDir, name)])
|
|
163
|
+
}
|
|
164
|
+
if (!isAndroid && process.platform === 'darwin') {
|
|
165
|
+
for (const name of ['dawn.node', 'libwebgpu_dawn.dylib']) run('strip', ['-x', path.join(distDir, binDir, name)])
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Licenses for everything compiled into the binaries, without the sources.
|
|
169
|
+
const licenses = path.join(distDir, 'share/native-dawn/licenses')
|
|
170
|
+
fs.mkdirSync(licenses, { recursive: true })
|
|
171
|
+
fs.copyFileSync(path.join(dawn, 'LICENSE'), path.join(licenses, 'dawn-LICENSE'))
|
|
172
|
+
for (const entry of fs.readdirSync(path.join(dawn, 'third_party'), { withFileTypes: true })) {
|
|
173
|
+
if (!entry.isDirectory()) continue
|
|
174
|
+
for (const base of ['', 'src']) {
|
|
175
|
+
const dir = path.join(dawn, 'third_party', entry.name, base)
|
|
176
|
+
if (!fs.existsSync(dir)) continue
|
|
177
|
+
for (const name of fs.readdirSync(dir)) {
|
|
178
|
+
if (/^(LICENSE|COPYING|NOTICE|ThirdPartyNotices)(\.|$)/i.test(name) && fs.statSync(path.join(dir, name)).isFile()) {
|
|
179
|
+
fs.copyFileSync(path.join(dir, name), path.join(licenses, `${entry.name}-${name}`))
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
fs.copyFileSync(path.join(root, 'LICENSE'), path.join(licenses, 'native-dawn-LICENSE'))
|
|
185
|
+
fs.copyFileSync(path.join(root, 'NOTICE'), path.join(distDir, 'share/native-dawn/NOTICE'))
|
|
186
|
+
fs.writeFileSync(path.join(distDir, 'build.json'), JSON.stringify({ version: pkg.version, target, dawn: upstream.dawn.revision }, null, 2) + '\n')
|
|
187
|
+
console.log(`Built ${distDir}`)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { root, pkg, targets } from './common.mjs'
|
|
5
|
+
|
|
6
|
+
if (process.env.GITHUB_REF_NAME !== `v${pkg.version}`) throw new Error(`Tag ${process.env.GITHUB_REF_NAME} does not match package.json version ${pkg.version}`)
|
|
7
|
+
for (const target of targets) {
|
|
8
|
+
const name = `native-dawn-v${pkg.version}-${target}.tar.gz`
|
|
9
|
+
const archive = await fs.readFile(path.join(root, 'artifacts', name))
|
|
10
|
+
const checksum = await fs.readFile(path.join(root, 'artifacts', `${name}.sha256`), 'utf8')
|
|
11
|
+
if (createHash('sha256').update(archive).digest('hex') !== checksum.trim().split(/\s+/)[0]) throw new Error(`Checksum mismatch: ${name}`)
|
|
12
|
+
}
|
|
13
|
+
console.log(`Verified v${pkg.version}: ${targets.length} archives and checksums`)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { spawnSync } from 'node:child_process'
|
|
5
|
+
|
|
6
|
+
export const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
7
|
+
export const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json')))
|
|
8
|
+
export const upstream = JSON.parse(fs.readFileSync(path.join(root, 'upstream.json')))
|
|
9
|
+
export const targets = ['linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64', 'win32-x64', 'win32-arm64', 'android-arm64']
|
|
10
|
+
// Android is cross-compiled and ships the C SDK only; the rest also get the Node addon.
|
|
11
|
+
export const nodeTargets = targets.filter(t => !t.startsWith('android-'))
|
|
12
|
+
// NATIVE_DAWN_TARGET selects a cross-compiled target; the default is this machine.
|
|
13
|
+
export const target = process.env.NATIVE_DAWN_TARGET || `${process.platform}-${process.arch}`
|
|
14
|
+
export const isAndroid = target.startsWith('android-')
|
|
15
|
+
export const hasAddon = nodeTargets.includes(target)
|
|
16
|
+
export const assetName = `native-dawn-v${pkg.version}-${target}.tar.gz`
|
|
17
|
+
export const distDir = path.join(root, 'dist', target)
|
|
18
|
+
export const binDir = target.startsWith('win32-') ? 'bin' : 'lib'
|
|
19
|
+
|
|
20
|
+
export function run(command, args, options = {}) {
|
|
21
|
+
const result = spawnSync(command, args, { cwd: root, stdio: 'inherit', ...options })
|
|
22
|
+
if (result.error) throw result.error
|
|
23
|
+
if (result.status !== 0) throw new Error(`${command} exited with ${result.status ?? result.signal}`)
|
|
24
|
+
return result
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function assertTarget() {
|
|
28
|
+
if (!targets.includes(target)) throw new Error(`Unsupported platform ${target}; supported: ${targets.join(', ')}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function manifestMatches(manifest) {
|
|
32
|
+
return manifest.version === pkg.version && manifest.target === target && manifest.dawn === upstream.dawn.revision
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Applies one source hook: inserts replacement for a needle that must occur
|
|
36
|
+
// exactly once. Returns the new text, the same text if already applied, or
|
|
37
|
+
// null if the needle is missing or ambiguous. Dawn is checked out with CRLF on
|
|
38
|
+
// Windows (core.autocrlf), so multi-line hooks follow the file's line endings.
|
|
39
|
+
export function insertText(text, needle, replacement) {
|
|
40
|
+
if (text.includes('\r\n')) {
|
|
41
|
+
needle = needle.replaceAll('\n', '\r\n')
|
|
42
|
+
replacement = replacement.replaceAll('\n', '\r\n')
|
|
43
|
+
}
|
|
44
|
+
if (text.includes(replacement)) return text
|
|
45
|
+
if (text.split(needle).length !== 2) return null
|
|
46
|
+
return text.replace(needle, () => replacement)
|
|
47
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Use Dawn's DEPS pins without installing Chromium's depot_tools or toolchains."""
|
|
2
|
+
import importlib.util
|
|
3
|
+
import pathlib
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
root = pathlib.Path(sys.argv[1]).resolve()
|
|
7
|
+
spec = importlib.util.spec_from_file_location('dawn_fetch', root / 'tools/fetch_dawn_dependencies.py')
|
|
8
|
+
fetch = importlib.util.module_from_spec(spec)
|
|
9
|
+
spec.loader.exec_module(fetch)
|
|
10
|
+
deps = [
|
|
11
|
+
'third_party/abseil-cpp', 'third_party/jinja2', 'third_party/markupsafe',
|
|
12
|
+
'third_party/EGL-Registry/src', 'third_party/OpenGL-Registry/src',
|
|
13
|
+
'third_party/glslang/src', 'third_party/spirv-headers/src',
|
|
14
|
+
'third_party/spirv-tools/src', 'third_party/vulkan-headers/src',
|
|
15
|
+
'third_party/vulkan-utility-libraries/src', 'third_party/webgpu-headers/src',
|
|
16
|
+
'third_party/node-addon-api', 'third_party/node-api-headers', 'third_party/gpuweb',
|
|
17
|
+
]
|
|
18
|
+
if sys.platform == 'win32':
|
|
19
|
+
deps += ['third_party/directx-shader-compiler/src', 'third_party/directx-headers/src']
|
|
20
|
+
# Read the upstream dependency pins, but check every subprocess result.
|
|
21
|
+
import subprocess
|
|
22
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
23
|
+
scope = {}
|
|
24
|
+
exec((root / 'DEPS').read_text(), {'Var': fetch.Var, 'Str': str}, scope)
|
|
25
|
+
def download(dep):
|
|
26
|
+
entry = scope['deps'].get(dep)
|
|
27
|
+
if not entry:
|
|
28
|
+
raise RuntimeError(f'Dawn no longer declares the required dependency {dep}')
|
|
29
|
+
url, pin = entry['url'].format(**scope['vars']).rsplit('@', 1)
|
|
30
|
+
folder = root / dep
|
|
31
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
def git(*args, capture=False):
|
|
33
|
+
return subprocess.run(['git', '-C', str(folder), *args], check=True,
|
|
34
|
+
text=True, capture_output=capture)
|
|
35
|
+
if not (folder / '.git').exists():
|
|
36
|
+
git('init', '-q')
|
|
37
|
+
current = subprocess.run(['git', '-C', str(folder), 'rev-parse', 'HEAD'], capture_output=True, text=True)
|
|
38
|
+
if current.returncode == 0 and current.stdout.strip() == pin:
|
|
39
|
+
return
|
|
40
|
+
print(f'Fetching {dep} at {pin}', flush=True)
|
|
41
|
+
git('fetch', '--depth', '1', url, pin)
|
|
42
|
+
git('checkout', '--detach', pin)
|
|
43
|
+
actual = subprocess.check_output(['git', '-C', str(root / dep), 'rev-parse', 'HEAD'], text=True).strip()
|
|
44
|
+
if actual != pin:
|
|
45
|
+
raise RuntimeError(f'{dep}: expected {pin}, got {actual}')
|
|
46
|
+
with ThreadPoolExecutor(max_workers=4) as pool:
|
|
47
|
+
list(pool.map(download, deps))
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import { createHash } from 'node:crypto'
|
|
5
|
+
import { createRequire } from 'node:module'
|
|
6
|
+
import { x as extract } from 'tar'
|
|
7
|
+
import { root, target, pkg, assetName, distDir, binDir, nodeTargets, hasAddon, manifestMatches, run } from './common.mjs'
|
|
8
|
+
|
|
9
|
+
const addon = dir => path.join(dir, binDir, 'dawn.node')
|
|
10
|
+
|
|
11
|
+
export async function install() {
|
|
12
|
+
if (process.env.NATIVE_DAWN_SKIP_INSTALL === '1') return
|
|
13
|
+
if (!hasAddon) throw new Error(`No Node build for ${target}; supported: ${nodeTargets.join(', ')}. Android SDK archives are on the GitHub releases.`)
|
|
14
|
+
if (process.env.NATIVE_DAWN_BUILD_FROM_SOURCE === '1') {
|
|
15
|
+
run(process.execPath, [path.join(root, 'scripts/build.mjs')])
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
if (!process.env.NATIVE_DAWN_BINARY) {
|
|
19
|
+
try {
|
|
20
|
+
if (manifestMatches(JSON.parse(await fs.readFile(path.join(distDir, 'build.json'))))) {
|
|
21
|
+
createRequire(import.meta.url)(addon(distDir))
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
} catch { /* No usable local build; install the release archive. */ }
|
|
25
|
+
}
|
|
26
|
+
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), 'native-dawn-install-'))
|
|
27
|
+
try {
|
|
28
|
+
let archive, checksum
|
|
29
|
+
if (process.env.NATIVE_DAWN_BINARY) {
|
|
30
|
+
const file = path.resolve(process.env.NATIVE_DAWN_BINARY)
|
|
31
|
+
archive = await fs.readFile(file)
|
|
32
|
+
checksum = await fs.readFile(`${file}.sha256`, 'utf8')
|
|
33
|
+
} else {
|
|
34
|
+
const base = `https://github.com/monteslu/native-dawn/releases/download/v${pkg.version}`
|
|
35
|
+
const responses = await Promise.all([
|
|
36
|
+
fetch(`${base}/${assetName}`, { signal: AbortSignal.timeout(300000) }),
|
|
37
|
+
fetch(`${base}/${assetName}.sha256`, { signal: AbortSignal.timeout(60000) }),
|
|
38
|
+
])
|
|
39
|
+
for (const response of responses) if (!response.ok) throw new Error(`Release download failed: HTTP ${response.status} ${response.url}`)
|
|
40
|
+
archive = Buffer.from(await responses[0].arrayBuffer())
|
|
41
|
+
checksum = await responses[1].text()
|
|
42
|
+
}
|
|
43
|
+
const expected = checksum.trim().split(/\s+/)[0]
|
|
44
|
+
const actual = createHash('sha256').update(archive).digest('hex')
|
|
45
|
+
if (!/^[a-f0-9]{64}$/.test(expected) || actual !== expected) throw new Error('Native archive checksum mismatch')
|
|
46
|
+
const file = path.join(temporary, 'binary.tar.gz')
|
|
47
|
+
const staging = path.join(temporary, 'unpacked')
|
|
48
|
+
await fs.mkdir(staging)
|
|
49
|
+
await fs.writeFile(file, archive)
|
|
50
|
+
await extract({
|
|
51
|
+
file, cwd: staging, strict: true, preservePaths: false,
|
|
52
|
+
filter: (name, entry) => {
|
|
53
|
+
if (path.isAbsolute(name) || name.split(/[\\/]/).includes('..') || !['File', 'Directory', 'SymbolicLink'].includes(entry.type)) throw new Error(`Invalid native archive entry: ${name}`)
|
|
54
|
+
if (entry.type === 'SymbolicLink' && (path.isAbsolute(entry.linkpath) || entry.linkpath.split(/[\\/]/).includes('..'))) throw new Error(`Invalid native archive link: ${name}`)
|
|
55
|
+
return true
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
if (!manifestMatches(JSON.parse(await fs.readFile(path.join(staging, 'build.json'))))) {
|
|
59
|
+
throw new Error('Native archive does not match this package version, platform, or Dawn revision')
|
|
60
|
+
}
|
|
61
|
+
await fs.access(addon(staging))
|
|
62
|
+
await fs.rm(distDir, { recursive: true, force: true })
|
|
63
|
+
await fs.mkdir(path.dirname(distDir), { recursive: true })
|
|
64
|
+
await fs.cp(staging, distDir, { recursive: true, verbatimSymlinks: true })
|
|
65
|
+
createRequire(import.meta.url)(addon(distDir))
|
|
66
|
+
console.log(`Installed native-dawn for ${target}`)
|
|
67
|
+
} finally {
|
|
68
|
+
await fs.rm(temporary, { recursive: true, force: true })
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A failed install leaves native-dawn without a native build but never fails
|
|
73
|
+
// the surrounding npm install: packages that depend on it (webgpu-node, and
|
|
74
|
+
// through it wasmcart) must still install on platforms native-dawn does not
|
|
75
|
+
// cover, and only error when WebGPU is actually used. A bad archive is never
|
|
76
|
+
// installed either way. NATIVE_DAWN_STRICT_INSTALL=1 makes failures fatal.
|
|
77
|
+
try {
|
|
78
|
+
await install()
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.warn(`native-dawn: no native build installed: ${error.message}\nWebGPU will not be available on this machine; importing native-dawn will throw. To build from source, install the toolchain in README.md and set NATIVE_DAWN_BUILD_FROM_SOURCE=1.`)
|
|
81
|
+
if (process.env.NATIVE_DAWN_STRICT_INSTALL === '1') process.exitCode = 1
|
|
82
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { c as tar } from 'tar'
|
|
5
|
+
import { root, assetName, distDir, binDir, hasAddon, manifestMatches } from './common.mjs'
|
|
6
|
+
|
|
7
|
+
if (!manifestMatches(JSON.parse(await fs.readFile(path.join(distDir, 'build.json'))))) throw new Error('Refusing to package a stale native build')
|
|
8
|
+
await fs.access(path.join(distDir, binDir, hasAddon ? 'dawn.node' : 'libwebgpu_dawn.so'))
|
|
9
|
+
await fs.mkdir(path.join(root, 'artifacts'), { recursive: true })
|
|
10
|
+
const file = path.join(root, 'artifacts', assetName)
|
|
11
|
+
await tar({ file, cwd: distDir, gzip: { level: 9 }, portable: true }, await fs.readdir(distDir))
|
|
12
|
+
const hash = createHash('sha256').update(await fs.readFile(file)).digest('hex')
|
|
13
|
+
await fs.writeFile(`${file}.sha256`, `${hash} ${assetName}\n`)
|
|
14
|
+
console.log(file)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Builds test/c with the NDK against an Android prefix, pushes it to the
|
|
2
|
+
// device or emulator adb is connected to, and runs it there.
|
|
3
|
+
// NATIVE_DAWN_TARGET=android-arm64 node scripts/test-android.mjs [prefix]
|
|
4
|
+
// On an x86_64 emulator, the arm64 binary runs through Android's ARM translation.
|
|
5
|
+
// NATIVE_DAWN_TEST_* variables are passed through to the device.
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
import os from 'node:os'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { spawnSync } from 'node:child_process'
|
|
10
|
+
import { root, run, target, distDir, isAndroid } from './common.mjs'
|
|
11
|
+
|
|
12
|
+
if (!isAndroid) throw new Error('Set NATIVE_DAWN_TARGET=android-arm64')
|
|
13
|
+
const prefix = path.resolve(process.argv[2] || distDir)
|
|
14
|
+
const sdk = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT
|
|
15
|
+
function ndk() {
|
|
16
|
+
for (const name of ['ANDROID_NDK_HOME', 'ANDROID_NDK_ROOT', 'ANDROID_NDK_LATEST_HOME']) {
|
|
17
|
+
if (process.env[name] && fs.existsSync(process.env[name])) return process.env[name]
|
|
18
|
+
}
|
|
19
|
+
const dir = path.join(sdk, 'ndk')
|
|
20
|
+
return path.join(dir, fs.readdirSync(dir).sort((a, b) => a.localeCompare(b, undefined, { numeric: true })).at(-1))
|
|
21
|
+
}
|
|
22
|
+
const adb = sdk && fs.existsSync(path.join(sdk, 'platform-tools/adb')) ? path.join(sdk, 'platform-tools/adb') : 'adb'
|
|
23
|
+
|
|
24
|
+
const build = fs.mkdtempSync(path.join(os.tmpdir(), 'native-dawn-android-'))
|
|
25
|
+
const remote = '/data/local/tmp/native-dawn'
|
|
26
|
+
try {
|
|
27
|
+
run('cmake', ['-S', path.join(root, 'test/c'), '-B', build, '-G', 'Ninja', '-DCMAKE_BUILD_TYPE=Release',
|
|
28
|
+
`-DCMAKE_TOOLCHAIN_FILE=${path.join(ndk(), 'build/cmake/android.toolchain.cmake')}`,
|
|
29
|
+
'-DANDROID_ABI=arm64-v8a', '-DANDROID_PLATFORM=android-26',
|
|
30
|
+
`-DDawn_DIR=${path.join(prefix, 'lib/cmake/Dawn')}`, `-DCMAKE_FIND_ROOT_PATH=${prefix}`])
|
|
31
|
+
run('cmake', ['--build', build])
|
|
32
|
+
run(adb, ['shell', `rm -rf ${remote} && mkdir -p ${remote}`])
|
|
33
|
+
run(adb, ['push', path.join(build, 'compute'), path.join(prefix, 'lib/libwebgpu_dawn.so'), `${remote}/`])
|
|
34
|
+
const vars = Object.entries(process.env).filter(([k]) => k.startsWith('NATIVE_DAWN_TEST_')).map(([k, v]) => `${k}=${v}`).join(' ')
|
|
35
|
+
const result = spawnSync(adb, ['shell', `cd ${remote} && chmod 755 compute && ${vars} LD_LIBRARY_PATH=${remote} ./compute; echo "exit=$?"`], { encoding: 'utf8' })
|
|
36
|
+
process.stdout.write(result.stdout)
|
|
37
|
+
process.stderr.write(result.stderr)
|
|
38
|
+
if (!/exit=0\s*$/.test(result.stdout)) throw new Error(`compute failed on the device (${target})`)
|
|
39
|
+
console.log(`Android ${target}: C test passed on the device`)
|
|
40
|
+
} finally {
|
|
41
|
+
spawnSync(adb, ['shell', `rm -rf ${remote}`])
|
|
42
|
+
fs.rmSync(build, { recursive: true, force: true })
|
|
43
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Builds test/c against an installed prefix with find_package(Dawn) and runs
|
|
2
|
+
// it, the way a C or C++ project outside Node would consume native-dawn.
|
|
3
|
+
// node scripts/test-c.mjs [prefix]
|
|
4
|
+
// NATIVE_DAWN_TEST_SDL2=1 also builds and runs the SDL2 window test.
|
|
5
|
+
import fs from 'node:fs'
|
|
6
|
+
import os from 'node:os'
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
import { root, run, distDir } from './common.mjs'
|
|
9
|
+
|
|
10
|
+
const prefix = path.resolve(process.argv[2] || distDir)
|
|
11
|
+
const sdl2 = process.env.NATIVE_DAWN_TEST_SDL2 === '1'
|
|
12
|
+
const build = fs.mkdtempSync(path.join(os.tmpdir(), 'native-dawn-c-'))
|
|
13
|
+
try {
|
|
14
|
+
const args = ['-S', path.join(root, 'test/c'), '-B', build, '-DCMAKE_BUILD_TYPE=Release', `-DCMAKE_PREFIX_PATH=${prefix}`, `-DNATIVE_DAWN_TEST_SDL2=${sdl2 ? 'ON' : 'OFF'}`]
|
|
15
|
+
if (process.platform !== 'win32' || process.env.VSCMD_VER) args.push('-G', 'Ninja')
|
|
16
|
+
run('cmake', args)
|
|
17
|
+
run('cmake', ['--build', build, '--config', 'Release'])
|
|
18
|
+
const env = { ...process.env }
|
|
19
|
+
if (process.platform === 'win32') env.PATH = `${path.join(prefix, 'bin')}${path.delimiter}${env.PATH}`
|
|
20
|
+
const exe = name => {
|
|
21
|
+
const file = name + (process.platform === 'win32' ? '.exe' : '')
|
|
22
|
+
for (const dir of [build, path.join(build, 'Release')]) if (fs.existsSync(path.join(dir, file))) return path.join(dir, file)
|
|
23
|
+
throw new Error(`${file} was not built`)
|
|
24
|
+
}
|
|
25
|
+
run(exe('compute'), [], { env })
|
|
26
|
+
if (sdl2) run(exe('sdl2_window'), [], { env })
|
|
27
|
+
} finally {
|
|
28
|
+
fs.rmSync(build, { recursive: true, force: true })
|
|
29
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Packs the npm tarball, installs it with the release archive into a scratch
|
|
2
|
+
// project, and checks it from Node and from a C program built against it.
|
|
3
|
+
import fs from 'node:fs/promises'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { spawnSync } from 'node:child_process'
|
|
7
|
+
import { root, assetName, run } from './common.mjs'
|
|
8
|
+
|
|
9
|
+
run(process.execPath, ['scripts/package-binary.mjs'])
|
|
10
|
+
const npm = process.env.npm_execpath
|
|
11
|
+
if (!npm) throw new Error('Run this through npm run test:package')
|
|
12
|
+
function npmRun(args, options = {}) {
|
|
13
|
+
const result = spawnSync(process.execPath, [npm, ...args], { cwd: root, stdio: 'inherit', ...options })
|
|
14
|
+
if (result.error) throw result.error
|
|
15
|
+
if (result.status !== 0) throw new Error(`npm ${args[0]} failed (${result.status})`)
|
|
16
|
+
return result
|
|
17
|
+
}
|
|
18
|
+
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), 'native-dawn-consumer-'))
|
|
19
|
+
try {
|
|
20
|
+
const packed = npmRun(['pack', '--json', '--pack-destination', temporary], { stdio: 'pipe', encoding: 'utf8' })
|
|
21
|
+
const [metadata] = JSON.parse(packed.stdout)
|
|
22
|
+
const leaked = metadata.files.filter(f => /^(\.cache|node_modules|build|dist|artifacts|test)\//.test(f.path))
|
|
23
|
+
if (leaked.length) throw new Error(`npm package includes ${leaked.map(f => f.path).join(', ')}`)
|
|
24
|
+
await fs.writeFile(path.join(temporary, 'package.json'), '{"name":"native-dawn-consumer","private":true,"type":"module"}\n')
|
|
25
|
+
npmRun(['install', '--omit=dev', '--no-audit', '--no-fund', path.join(temporary, metadata.filename)], {
|
|
26
|
+
cwd: temporary,
|
|
27
|
+
env: { ...process.env, NATIVE_DAWN_BINARY: path.join(root, 'artifacts', assetName), NATIVE_DAWN_SKIP_INSTALL: '0', NATIVE_DAWN_BUILD_FROM_SOURCE: '0' },
|
|
28
|
+
})
|
|
29
|
+
await fs.copyFile(path.join(root, 'test/consumer.mjs'), path.join(temporary, 'consumer.mjs'))
|
|
30
|
+
run(process.execPath, ['consumer.mjs'], { cwd: temporary })
|
|
31
|
+
const prefix = spawnSync(process.execPath, [path.join(temporary, 'node_modules/native-dawn/bin/native-dawn.js'), 'prefix'], { encoding: 'utf8' }).stdout.trim()
|
|
32
|
+
run(process.execPath, ['scripts/test-c.mjs', prefix])
|
|
33
|
+
console.log('Packed package passed from Node and from C')
|
|
34
|
+
} finally { await fs.rm(temporary, { recursive: true, force: true }) }
|
package/scripts/test.mjs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import { root } from './common.mjs'
|
|
5
|
+
|
|
6
|
+
// One process per file keeps each file's GPU devices and addon state separate.
|
|
7
|
+
for (const file of fs.readdirSync(path.join(root, 'test')).filter(name => name.endsWith('.test.mjs')).sort()) {
|
|
8
|
+
const result = spawnSync(process.execPath, [path.join(root, 'test', file)], { cwd: root, stdio: 'inherit', timeout: 180000 })
|
|
9
|
+
if (result.error) throw result.error
|
|
10
|
+
if (result.status !== 0) process.exit(result.status || 1)
|
|
11
|
+
}
|