Skip to content

Commit dfbca02

Browse files
authored
feat: auto-setup tool + docs update (v0.5.1)
Added cmd/setup — downloads correct wgpu-native v29 binary for current platform. Public API in setup/ package: Install(), FindLibrary(), Version. Implementation in internal/nativelib/ with tests (11 pass). Updated README: auto-setup section, gogpu/wgpu integration section. Updated CHANGELOG: v0.5.0 date fixed, v0.5.1 entry. deps: goffi v0.5.0 → v0.5.2
1 parent 36e2d05 commit dfbca02

11 files changed

Lines changed: 555 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,19 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## v0.5.0 (Unreleased)
8+
## v0.5.1 (2026-05-27)
9+
10+
### Added
11+
12+
- **Auto-setup tool**`go run github.com/go-webgpu/webgpu/cmd/setup@latest` downloads correct wgpu-native binary for your platform
13+
- **setup package** — public API: `setup.Install(dir)`, `setup.FindLibrary()`, `setup.Version`
14+
- **internal/nativelib** — platform detection, download, zip extraction with tests
15+
16+
### Changed
17+
18+
- **deps:** goffi v0.5.0 → v0.5.2
19+
20+
## v0.5.0 (2026-05-26)
921

1022
### Breaking Changes
1123
- **wgpu-native v29.0.0.0**: Migrated from v27.0.4.0 to v29.0.0.0 with stable webgpu-headers

README.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,34 @@ Pure Go WebGPU bindings using [goffi](https://github.com/go-webgpu/goffi) + [wgp
5050
go get github.com/go-webgpu/webgpu
5151
```
5252

53-
Download wgpu-native and place `wgpu_native.dll` (Windows) or `libwgpu_native.so` (Linux) in your project directory or system PATH.
53+
### wgpu-native Setup (auto)
5454

55-
To use a custom library location:
5655
```bash
57-
export WGPU_NATIVE_PATH=/path/to/libwgpu_native.so
56+
go run github.com/go-webgpu/webgpu/cmd/setup@latest
5857
```
5958

59+
This downloads the correct wgpu-native v29 binary for your platform (Windows/macOS/Linux, amd64/arm64) into `./lib/`.
60+
61+
### wgpu-native Setup (manual)
62+
63+
Download from [gfx-rs/wgpu-native releases](https://github.com/gfx-rs/wgpu-native/releases/tag/v29.0.0.0) and place in your project directory or system PATH.
64+
65+
Custom library location:
66+
```bash
67+
export WGPU_NATIVE_PATH=/path/to/libwgpu_native.so # Linux/macOS
68+
set WGPU_NATIVE_PATH=C:\path\to\wgpu_native.dll # Windows
69+
```
70+
71+
## gogpu/wgpu Integration
72+
73+
This library is the **Rust FFI backend** for [gogpu/wgpu](https://github.com/gogpu/wgpu) — the unified Go WebGPU package. Build with `-tags rust` to use wgpu-native instead of the Pure Go implementation:
74+
75+
```bash
76+
go build -tags rust ./myapp
77+
```
78+
79+
Same API, same types, same user code — build tag selects the implementation.
80+
6081
## Type System
6182

6283
WebGPU types from [gputypes](https://github.com/gogpu/gputypes) are re-exported as type aliases in the `wgpu` package. A single import is sufficient:

cmd/setup/main.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Command setup downloads and installs the wgpu-native library.
2+
//
3+
// Usage:
4+
//
5+
// go run github.com/go-webgpu/webgpu/cmd/setup@latest
6+
// go run github.com/go-webgpu/webgpu/cmd/setup@latest ./path/to/lib
7+
package main
8+
9+
import (
10+
"fmt"
11+
"os"
12+
13+
"github.com/go-webgpu/webgpu/setup"
14+
)
15+
16+
func main() {
17+
destDir := "lib"
18+
if len(os.Args) > 1 {
19+
destDir = os.Args[1]
20+
}
21+
22+
if _, err := setup.Install(destDir); err != nil {
23+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
24+
os.Exit(1)
25+
}
26+
}

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ module github.com/go-webgpu/webgpu
22

33
go 1.25.0
44

5-
require github.com/go-webgpu/goffi v0.5.0
5+
require github.com/go-webgpu/goffi v0.5.2
66

77
require golang.org/x/sys v0.42.0
88

9-
require github.com/gogpu/gputypes v0.3.0
9+
require github.com/gogpu/gputypes v0.5.0

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
github.com/go-webgpu/goffi v0.5.0 h1:EuvVRiRn9qAfCkYYXbHs9gz8NY+zv2/OA1N7gi56UVE=
2-
github.com/go-webgpu/goffi v0.5.0/go.mod h1:wfoxNsJkU+5RFbV1kNN1kunhc1lFHuJKK3zpgx08/uM=
3-
github.com/gogpu/gputypes v0.3.0 h1:gcwxsBrcoCX19GqqqiV55wLv2iFwaybiOluKCb0hVrs=
4-
github.com/gogpu/gputypes v0.3.0/go.mod h1:cnXrDMwTpWTvJLW1Vreop3PcT6a2YP/i3s91rPaOavw=
1+
github.com/go-webgpu/goffi v0.5.2 h1:Kq2llPlA6IhkkmAffkM5CtsgaXuBQC4mBI0BOREwV04=
2+
github.com/go-webgpu/goffi v0.5.2/go.mod h1:wfoxNsJkU+5RFbV1kNN1kunhc1lFHuJKK3zpgx08/uM=
3+
github.com/gogpu/gputypes v0.5.0 h1:i2ED/9w6m6yLxf8XJT69/NIMSNTLO2y5F1LqvugCKIE=
4+
github.com/gogpu/gputypes v0.5.0/go.mod h1:cnXrDMwTpWTvJLW1Vreop3PcT6a2YP/i3s91rPaOavw=
55
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
66
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=

internal/nativelib/download.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package nativelib
2+
3+
import (
4+
"archive/zip"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"os"
9+
"path/filepath"
10+
)
11+
12+
func Download(url string) (string, error) {
13+
resp, err := http.Get(url) //nolint:gosec // G107: URL constructed from constants
14+
if err != nil {
15+
return "", fmt.Errorf("download failed: %w", err)
16+
}
17+
defer resp.Body.Close()
18+
19+
if resp.StatusCode != http.StatusOK {
20+
return "", fmt.Errorf("download failed: HTTP %d from %s", resp.StatusCode, url)
21+
}
22+
23+
tmpFile, err := os.CreateTemp("", "wgpu-native-*.zip")
24+
if err != nil {
25+
return "", fmt.Errorf("create temp file: %w", err)
26+
}
27+
defer tmpFile.Close()
28+
29+
if _, err := io.Copy(tmpFile, resp.Body); err != nil {
30+
os.Remove(tmpFile.Name())
31+
return "", fmt.Errorf("download write: %w", err)
32+
}
33+
34+
return tmpFile.Name(), nil
35+
}
36+
37+
func ExtractLibrary(zipPath, destDir, libName string) (string, error) {
38+
r, err := zip.OpenReader(zipPath)
39+
if err != nil {
40+
return "", fmt.Errorf("open zip: %w", err)
41+
}
42+
defer r.Close()
43+
44+
for _, f := range r.File {
45+
name := filepath.Base(f.Name)
46+
if name != libName {
47+
continue
48+
}
49+
50+
src, err := f.Open()
51+
if err != nil {
52+
return "", fmt.Errorf("open %s in zip: %w", f.Name, err)
53+
}
54+
defer src.Close()
55+
56+
destPath := filepath.Join(destDir, libName)
57+
dst, err := os.Create(destPath)
58+
if err != nil {
59+
return "", fmt.Errorf("create %s: %w", destPath, err)
60+
}
61+
defer dst.Close()
62+
63+
if _, err := io.Copy(dst, src); err != nil {
64+
return "", fmt.Errorf("extract %s: %w", libName, err)
65+
}
66+
67+
return destPath, nil
68+
}
69+
70+
return "", fmt.Errorf("%s not found in archive", libName)
71+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package nativelib
2+
3+
import (
4+
"archive/zip"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
)
9+
10+
func TestExtractLibrary(t *testing.T) {
11+
zipPath := createTestZip(t, "test.dll", []byte("fake dll content"))
12+
destDir := t.TempDir()
13+
14+
path, err := ExtractLibrary(zipPath, destDir, "test.dll")
15+
if err != nil {
16+
t.Fatalf("ExtractLibrary() error: %v", err)
17+
}
18+
19+
if filepath.Base(path) != "test.dll" {
20+
t.Errorf("extracted path = %q, want test.dll basename", path)
21+
}
22+
23+
data, err := os.ReadFile(path)
24+
if err != nil {
25+
t.Fatalf("read extracted file: %v", err)
26+
}
27+
if string(data) != "fake dll content" {
28+
t.Errorf("content = %q, want %q", string(data), "fake dll content")
29+
}
30+
}
31+
32+
func TestExtractLibraryNestedPath(t *testing.T) {
33+
zipPath := createTestZipNested(t, "lib/wgpu_native.dll", []byte("nested content"))
34+
destDir := t.TempDir()
35+
36+
path, err := ExtractLibrary(zipPath, destDir, "wgpu_native.dll")
37+
if err != nil {
38+
t.Fatalf("ExtractLibrary() error: %v", err)
39+
}
40+
41+
data, err := os.ReadFile(path)
42+
if err != nil {
43+
t.Fatalf("read: %v", err)
44+
}
45+
if string(data) != "nested content" {
46+
t.Errorf("content = %q, want %q", string(data), "nested content")
47+
}
48+
}
49+
50+
func TestExtractLibraryNotFound(t *testing.T) {
51+
zipPath := createTestZip(t, "other.dll", []byte("data"))
52+
destDir := t.TempDir()
53+
54+
_, err := ExtractLibrary(zipPath, destDir, "missing.dll")
55+
if err == nil {
56+
t.Fatal("expected error for missing file in archive")
57+
}
58+
}
59+
60+
func TestExtractLibraryInvalidZip(t *testing.T) {
61+
tmpFile := filepath.Join(t.TempDir(), "bad.zip")
62+
os.WriteFile(tmpFile, []byte("not a zip"), 0o644)
63+
64+
_, err := ExtractLibrary(tmpFile, t.TempDir(), "test.dll")
65+
if err == nil {
66+
t.Fatal("expected error for invalid zip")
67+
}
68+
}
69+
70+
func createTestZip(t *testing.T, name string, content []byte) string {
71+
t.Helper()
72+
path := filepath.Join(t.TempDir(), "test.zip")
73+
f, err := os.Create(path)
74+
if err != nil {
75+
t.Fatal(err)
76+
}
77+
defer f.Close()
78+
79+
w := zip.NewWriter(f)
80+
entry, err := w.Create(name)
81+
if err != nil {
82+
t.Fatal(err)
83+
}
84+
entry.Write(content)
85+
w.Close()
86+
return path
87+
}
88+
89+
func createTestZipNested(t *testing.T, name string, content []byte) string {
90+
t.Helper()
91+
path := filepath.Join(t.TempDir(), "test.zip")
92+
f, err := os.Create(path)
93+
if err != nil {
94+
t.Fatal(err)
95+
}
96+
defer f.Close()
97+
98+
w := zip.NewWriter(f)
99+
entry, err := w.Create(name)
100+
if err != nil {
101+
t.Fatal(err)
102+
}
103+
entry.Write(content)
104+
w.Close()
105+
return path
106+
}

internal/nativelib/platform.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package nativelib
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"runtime"
8+
"strings"
9+
)
10+
11+
const (
12+
libWindows = "wgpu_native.dll"
13+
libDarwin = "libwgpu_native.dylib"
14+
libLinux = "libwgpu_native.so"
15+
)
16+
17+
type Platform struct {
18+
OS string
19+
Arch string
20+
LibName string
21+
}
22+
23+
func DetectPlatform() (*Platform, error) {
24+
p := &Platform{OS: runtime.GOOS, Arch: runtime.GOARCH}
25+
26+
switch p.OS {
27+
case "windows":
28+
p.LibName = libWindows
29+
case "darwin":
30+
p.LibName = libDarwin
31+
case "linux":
32+
p.LibName = libLinux
33+
default:
34+
return nil, fmt.Errorf("unsupported OS: %s", p.OS)
35+
}
36+
37+
if p.Arch != "amd64" && p.Arch != "arm64" {
38+
return nil, fmt.Errorf("unsupported architecture: %s", p.Arch)
39+
}
40+
41+
return p, nil
42+
}
43+
44+
func (p *Platform) archName() string {
45+
if p.Arch == "amd64" {
46+
return "x86_64"
47+
}
48+
return "aarch64"
49+
}
50+
51+
func (p *Platform) ZipName() string {
52+
arch := p.archName()
53+
switch p.OS {
54+
case "windows":
55+
return fmt.Sprintf("wgpu-%s-%s-msvc-release.zip", p.OS, arch)
56+
case "darwin":
57+
return fmt.Sprintf("wgpu-macos-%s-release.zip", arch)
58+
case "linux":
59+
return fmt.Sprintf("wgpu-%s-%s-release.zip", p.OS, arch)
60+
}
61+
return ""
62+
}
63+
64+
func (p *Platform) DownloadURL(version string) string {
65+
return fmt.Sprintf("https://github.com/gfx-rs/wgpu-native/releases/download/%s/%s", version, p.ZipName())
66+
}
67+
68+
func LibraryName() string {
69+
switch runtime.GOOS {
70+
case "windows":
71+
return libWindows
72+
case "darwin":
73+
return libDarwin
74+
default:
75+
return libLinux
76+
}
77+
}
78+
79+
func FindLibrary() string {
80+
if p := os.Getenv("WGPU_NATIVE_PATH"); p != "" {
81+
if _, err := os.Stat(p); err == nil {
82+
return p
83+
}
84+
}
85+
86+
libName := LibraryName()
87+
searchPaths := []string{
88+
filepath.Join(".", libName),
89+
filepath.Join("lib", libName),
90+
}
91+
92+
if exe, err := os.Executable(); err == nil {
93+
searchPaths = append(searchPaths, filepath.Join(filepath.Dir(exe), libName))
94+
}
95+
96+
for _, p := range searchPaths {
97+
if _, err := os.Stat(p); err == nil {
98+
abs, _ := filepath.Abs(p)
99+
return abs
100+
}
101+
}
102+
103+
for _, dir := range strings.Split(os.Getenv("PATH"), string(os.PathListSeparator)) {
104+
p := filepath.Join(dir, libName)
105+
if _, err := os.Stat(p); err == nil {
106+
return p
107+
}
108+
}
109+
110+
return ""
111+
}

0 commit comments

Comments
 (0)