Skip to content

Commit ca7071c

Browse files
authored
Add JSON translation of webgpu.yml (#561)
This is helpful for automated tooling using a different toolchain than ours, so people don't have to deal with parsing YAML, but we can still write in YAML. Should simplify https://dawn-review.googlesource.com/c/dawn/+/264874 (and many other projects in the future).
1 parent 3e90f57 commit ca7071c

6 files changed

Lines changed: 7014 additions & 33 deletions

File tree

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
gen/** -linguist-detectable
2+
webgpu.json linguist-generated=true
3+
tests/extensions/extension.json linguist-generated=true

Makefile

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,57 @@
11
.PHONY: all all-help-message help check-schema fix gen gen-check doc
22

33
# default target if you just type `make`
4-
all: all-help-message fix gen doc
4+
all: all-help-message gen fix doc
55

66
# help message before starting `make all`
77
all-help-message: help
8-
@echo 'Running default targets: fix gen doc'
8+
@echo 'Running default targets: gen fix doc'
99

1010
help:
11-
@echo 'Targets are: all, help, check-schema, fix, gen, gen-check, doc'
11+
@echo 'Targets are: all, help, gen, fix, gen-check, doc'
1212

13-
check-schema: schema.json
14-
go run ./gen -schema schema.json -yaml webgpu.yml -header webgpu.h -yaml tests/extensions/extension.yml -header tests/extensions/webgpu_extension.h
13+
# Validate schema and regenerate JSON and header from YAML
14+
gen: schema.json webgpu.yml tests/extensions/extension.yml
15+
go run ./gen -schema schema.json \
16+
-yaml webgpu.yml \
17+
-out-json webgpu.json \
18+
-out-header webgpu.h \
19+
-yaml tests/extensions/extension.yml \
20+
-out-json tests/extensions/extension.json \
21+
-out-header tests/extensions/webgpu_extension.h
1522

16-
fix: check-schema webgpu.yml tests/extensions/extension.yml
23+
# Regenerate files (to validate schema) then autoformat YAML
24+
fix: gen
1725
go run ./fix -yaml webgpu.yml
1826
go run ./fix -yaml tests/extensions/extension.yml
1927

20-
gen: check-schema webgpu.yml tests/extensions/extension.yml
21-
28+
# Verify that generation and autoformat are up to date (used for pull request checks)
2229
gen-check: fix gen
23-
@git diff --quiet -- webgpu.h || { \
24-
echo "error: The re-generated header from yml doesn't match the checked-in header"; \
25-
git diff -- webgpu.h; \
26-
exit 1; \
30+
@git diff --quiet -- webgpu.h || { \
31+
echo "error: The re-generated header from yml doesn't match the checked-in version"; \
32+
git diff -- webgpu.h; \
33+
exit 1; \
34+
}
35+
@git diff --quiet -- webgpu.json || { \
36+
echo "error: The re-translated webgpu.json from webgpu.yml doesn't match the checked-in version"; \
37+
git diff -- webgpu.json; \
38+
exit 1; \
2739
}
2840
@git diff --quiet -- webgpu.yml || { \
2941
echo "error: Please re-run 'make fix' to format the yml"; \
3042
git diff -- webgpu.yml; \
3143
exit 1; \
3244
}
33-
@git diff --quiet -- tests/extensions/webgpu_extension.h || { \
34-
echo "error: The re-generated extensions header from yml doesn't match the checked-in header"; \
35-
git diff -- tests/extensions/webgpu_extension.h; \
45+
@git diff --quiet -- tests/extensions/webgpu_extension.h || { \
46+
echo "error: The re-generated extension header from yml doesn't match the checked-in version"; \
47+
git diff -- tests/extensions/webgpu_extension.h; \
3648
exit 1; \
3749
}
50+
@git diff --quiet -- tests/extensions/webgpu_extension.json || { \
51+
echo "error: The re-translated extension json from yml doesn't match the checked-in version"; \
52+
git diff -- tests/extensions/webgpu_extension.json; \
53+
exit 1; \
54+
}
3855
@git diff --quiet -- tests/extensions/extension.yml || { \
3956
echo "error: Please re-run 'make fix' to format the extension yml"; \
4057
git diff -- tests/extensions/extension.yml; \

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,23 @@ Implementations of this header include:
2727
- [wgpu-native](https://github.com/gfx-rs/wgpu-native), C bindings to [wgpu](https://github.com/gfx-rs/wgpu), the Rust WebGPU implementation used in Firefox.
2828
- **wgpu-native does not yet implement the stable version of this header. Contributions needed!**
2929

30-
## `webgpu.yml`
30+
## `webgpu.yml` and `webgpu.json`
3131

32-
`webgpu.yml` is the main machine-readable source of truth for the C API and its documentation (in [YAML](https://yaml.org/) format). It is used to generate the official `webgpu.h` header present in this repository to generate the official documentation, and may be used by any other third party to design tools and wrappers around WebGPU-Native, especially bindings into other languages.
32+
`webgpu.yml` and `webgpu.json` (same contents, different formats) are the main machine-readable source of truth for the C API and its documentation. This data is used to generate the official `webgpu.h` header present in this repository to generate the official documentation, and may be used by any other third party to design tools and wrappers around WebGPU-Native, especially bindings into other languages.
3333

3434
**If you are developing bindings of `webgpu.h` into another language and find that any additional high-level/semantic information would be useful in `webgpu.yml`, please contribute it!**
3535

3636
## Contributing to this project
3737

38-
**Important:** When submitting a change, one must modify both the `webgpu.yml` and `webgpu.h` files in a consistent way. One should first edit `webgpu.yml` (the source of truth), then run `make gen` to update `webgpu.h` and finally commit both changes together.
38+
**Important:** When submitting a change, one must modify `webgpu.yml`, `webgpu.json`, and `webgpu.h` files in a consistent way. One should first edit `webgpu.yml` (the source of truth), then run `make gen` to update `webgpu.json` and `webgpu.h`, and finally commit all changes together.
3939

4040
Here are some details about the structure of this repository.
4141

4242
### Main files
4343

4444
- `webgpu.h` is the one and only header file that defines the WebGPU C API. Only this needs to be integrated in a C project that links against a WebGPU implementation. (But be sure to use the headers from your implementation if you need any extensions, or if the implementation doesn't match this header exactly.)
4545

46-
- `webgpu.yml` - see above.
46+
- `webgpu.yml` and `webgpu.json` - see above.
4747

4848
- `schema.json` is the [JSON schema](https://json-schema.org/) that formally specifies the structure of `webgpu.yml`.
4949

gen/main.go

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"encoding/json"
45
"flag"
56
"os"
67
"path/filepath"
@@ -16,19 +17,21 @@ import (
1617
var tmpl string
1718

1819
var (
19-
schemaPath string
20-
headerPaths StringListFlag
21-
yamlPaths StringListFlag
22-
extPrefix bool
20+
schemaPath string
21+
yamlPaths StringListFlag
22+
outJsonPaths StringListFlag
23+
outHeaderPaths StringListFlag
24+
extPrefix bool
2325
)
2426

2527
func main() {
2628
flag.StringVar(&schemaPath, "schema", "", "path of the json schema")
2729
flag.Var(&yamlPaths, "yaml", "path of the yaml spec")
28-
flag.Var(&headerPaths, "header", "output path of the header")
29-
flag.BoolVar(&extPrefix, "extprefix", true, "append prefix to extension identifiers")
30+
flag.Var(&outJsonPaths, "out-json", "output path of the json version of the yaml")
31+
flag.Var(&outHeaderPaths, "out-header", "output path of the header")
32+
flag.BoolVar(&extPrefix, "ext-prefix", true, "append prefix to extension identifiers")
3033
flag.Parse()
31-
if schemaPath == "" || len(headerPaths) == 0 || len(yamlPaths) == 0 || len(headerPaths) != len(yamlPaths) {
34+
if schemaPath == "" || len(yamlPaths) == 0 || len(outHeaderPaths) != len(yamlPaths) || len(outJsonPaths) != len(yamlPaths) {
3235
flag.Usage()
3336
os.Exit(1)
3437
}
@@ -45,19 +48,23 @@ func main() {
4548

4649
// Generate the header files
4750
for i, yamlPath := range yamlPaths {
48-
headerPath := headerPaths[i]
49-
headerFileName := filepath.Base(headerPath)
50-
headerFileNameSplit := strings.Split(headerFileName, ".")
51-
if len(headerFileNameSplit) != 2 {
52-
panic("got invalid header file name: " + headerFileName)
51+
outHeaderPath := outHeaderPaths[i]
52+
outHeaderFileName := filepath.Base(outHeaderPath)
53+
outHeaderFileNameSplit := strings.Split(outHeaderFileName, ".")
54+
if len(outHeaderFileNameSplit) != 2 {
55+
panic("got invalid out-header file name: " + outHeaderFileName)
5356
}
5457

58+
outJsonPath := outJsonPaths[i]
59+
5560
src, err := os.ReadFile(yamlPath)
5661
if err != nil {
5762
panic(err)
5863
}
5964

60-
dst, err := os.Create(headerPath)
65+
ConvertToJSON(src, outJsonPath)
66+
67+
dst, err := os.Create(outHeaderPath)
6168
if err != nil {
6269
panic(err)
6370
}
@@ -75,7 +82,7 @@ func main() {
7582
}
7683
g := &Generator{
7784
Yml: &yml,
78-
HeaderName: headerFileNameSplit[0],
85+
HeaderName: outHeaderFileNameSplit[0],
7986
ExtPrefix: prefix,
8087
}
8188
if err := g.Gen(dst); err != nil {
@@ -168,3 +175,50 @@ func SortAndTransform(yml *Yml) {
168175
}
169176
}
170177
}
178+
179+
func ConvertToJSON(ymlString []byte, outJsonPath string) {
180+
var body interface{}
181+
if err := yaml.Unmarshal(ymlString, &body); err != nil {
182+
panic(err)
183+
}
184+
185+
switch b := body.(type) {
186+
case map[string]interface{}:
187+
// Insert an extra copy of the copyright that will get sorted at the
188+
// top, and auto-gen warning.
189+
b["__copyright"] = b["copyright"]
190+
b["_comment"] = "AUTO-GENERATED FILE! Edit webgpu.yml instead."
191+
default:
192+
panic("unexpected")
193+
}
194+
195+
body = convertToJSONHelper(body)
196+
197+
if outJson, err := json.MarshalIndent(body, "", " "); err != nil {
198+
panic(err)
199+
} else {
200+
outJson = append(outJson, '\n')
201+
if err := os.WriteFile(outJsonPath, outJson, 0644); err != nil {
202+
panic(err)
203+
}
204+
}
205+
}
206+
207+
// From https://stackoverflow.com/a/40737676
208+
// Note the original key order is not preserved as Go's map isn't ordered.
209+
// This is fine because JSON keys are unordered per spec.
210+
func convertToJSONHelper(i interface{}) interface{} {
211+
switch x := i.(type) {
212+
case map[interface{}]interface{}:
213+
m2 := map[string]interface{}{}
214+
for k, v := range x {
215+
m2[k.(string)] = convertToJSONHelper(v)
216+
}
217+
return m2
218+
case []interface{}:
219+
for i, v := range x {
220+
x[i] = convertToJSONHelper(v)
221+
}
222+
}
223+
return i
224+
}

0 commit comments

Comments
 (0)