native-dawn 0.0.0-stage → 0.1.0

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/package.json CHANGED
@@ -1,6 +1,77 @@
1
1
  {
2
2
  "name": "native-dawn",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
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,77 @@
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
+ try {
73
+ await install()
74
+ } catch (error) {
75
+ console.error(`native-dawn: ${error.message}\nTo build from source instead, install the toolchain in README.md and set NATIVE_DAWN_BUILD_FROM_SOURCE=1.`)
76
+ process.exitCode = 1
77
+ }
@@ -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 }) }
@@ -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
+ }