1// Command wlbgen generates Go bindings from Wayland protocol XML.
2//
3// Usage:
4//
5// wlbgen -all [-root DIR]
6// wlbgen -side client|server -package NAME -out FILE [-import iface=path ...] protocol.xml
7//
8// With -all, wlbgen renders client/<pkg>/<pkg>_generated.go and
9// server/<pkg>/<pkg>_generated.go below DIR for every protocol listed in the
10// embedded spec manifest, and removes generated files of packages that left
11// the manifest.
12//
13// With -side, wlbgen generates a single file from a private protocol XML.
14// Interfaces defined in other protocols are resolved from the embedded
15// manifest (import path .../client/<owner> or .../server/<owner>); each
16// -import iface=path overrides that resolution for one interface. An
17// interface that cannot be resolved is an error.
18package main
19
20import (
21 "flag"
22 "fmt"
23 "io"
24 "os"
25 "path"
26 "path/filepath"
27 "sort"
28 "strings"
29
30 "github.com/bnema/go-wayland-bindings/internal/gen/client"
31 "github.com/bnema/go-wayland-bindings/internal/gen/server"
32 "github.com/bnema/go-wayland-bindings/spec"
33)
34
35const tool = "wlbgen"
36
37type importFlags map[string]string
38
39func (f importFlags) String() string { return fmt.Sprint(map[string]string(f)) }
40
41func (f importFlags) Set(v string) error {
42 iface, p, ok := strings.Cut(v, "=")
43 if !ok || iface == "" || p == "" {
44 return fmt.Errorf("want iface=importpath, got %q", v)
45 }
46 f[iface] = p
47 return nil
48}
49
50func main() {
51 if err := run(os.Args[1:], os.Stderr); err != nil {
52 fmt.Fprintln(os.Stderr, "wlbgen:", err)
53 os.Exit(1)
54 }
55}
56
57func run(args []string, stderr io.Writer) error {
58 fs := flag.NewFlagSet(tool, flag.ContinueOnError)
59 fs.SetOutput(stderr)
60 all := fs.Bool("all", false, "generate client and server bindings for every manifest protocol")
61 root := fs.String("root", ".", "repository root (with -all)")
62 side := fs.String("side", "", "`client` or `server` (single file mode)")
63 pkg := fs.String("package", "", "Go package name (single file mode)")
64 out := fs.String("out", "", "output file (single file mode)")
65 imports := importFlags{}
66 fs.Var(imports, "import", "`iface=importpath` override for a foreign interface (repeatable)")
67 if err := fs.Parse(args); err != nil {
68 return err
69 }
70 m, err := spec.Load()
71 if err != nil {
72 return err
73 }
74 if *all {
75 if fs.NArg() != 0 || *side != "" || *pkg != "" || *out != "" || len(imports) != 0 {
76 return fmt.Errorf("-all takes no other arguments except -root")
77 }
78 return generateAll(m, m.Open, *root)
79 }
80 if fs.NArg() != 1 {
81 return fmt.Errorf("want -all, or -side/-package/-out and one XML file")
82 }
83 if *pkg == "" || *out == "" {
84 return fmt.Errorf("-package and -out are required")
85 }
86 data, err := os.ReadFile(fs.Arg(0))
87 if err != nil {
88 return err
89 }
90 src, err := generateCustom(m, *side, *pkg, filepath.Base(fs.Arg(0)), data, imports)
91 if err != nil {
92 return err
93 }
94 return writeFile(*out, src)
95}
96
97func writeFile(name string, data []byte) error {
98 if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
99 return err
100 }
101 return os.WriteFile(name, data, 0o644)
102}
103
104func clientImport(owner string) string { return spec.ModulePath + "/client/" + owner }
105func serverImport(owner string) string { return spec.ModulePath + "/server/" + owner }
106
107func generatedPath(root, side, pkg string) string {
108 return filepath.Join(root, side, pkg, pkg+"_generated.go")
109}
110
111// generated is one rendered output file.
112type generated struct {
113 path string
114 data []byte
115}
116
117// generateAll writes every package of m below root and removes stale output.
118// Everything is rendered in memory first, so an error leaves the tree as it
119// was.
120func generateAll(m *spec.Manifest, open func(string) ([]byte, error), root string) error {
121 owners := m.Owners()
122 var files []generated
123 for _, e := range m.Protocols {
124 data, err := open(e.Package)
125 if err != nil {
126 return err
127 }
128 p, err := spec.Parse(data)
129 if err != nil {
130 return fmt.Errorf("%s: %w", e.Package, err)
131 }
132
133 s := client.NewScanner()
134 s.Tool = tool
135 spdx := ""
136 if e.License != "" && e.License != "MIT" {
137 spdx = e.License
138 }
139 s.SPDXLicense = spdx
140 s.CrossPackage = map[string]string{}
141 for iface, owner := range owners {
142 if owner != e.Package {
143 s.CrossPackage[iface] = clientImport(owner)
144 }
145 }
146 if err := s.LoadProtocol(p, path.Base(e.Path)); err != nil {
147 return err
148 }
149 src, err := s.Generate(e.Package)
150 if err != nil {
151 return fmt.Errorf("client/%s: %w", e.Package, err)
152 }
153 files = append(files, generated{generatedPath(root, "client", e.Package), src})
154
155 opts := server.Options{
156 Package: e.Package, Imports: map[string]string{}, Owners: map[string]string{}, Tool: tool,
157 Copyright: p.Copyright, SPDX: spdx,
158 }
159 for iface, owner := range owners {
160 if owner != e.Package {
161 opts.Owners[iface] = owner
162 }
163 }
164 for _, dep := range e.Deps {
165 if dep != e.Package {
166 opts.Imports[dep] = serverImport(dep)
167 }
168 }
169 src, err = server.Generate(p, opts)
170 if err != nil {
171 return fmt.Errorf("server/%s: %w", e.Package, err)
172 }
173 files = append(files, generated{generatedPath(root, "server", e.Package), src})
174 }
175 if err := removeStale(m, root); err != nil {
176 return err
177 }
178 for _, f := range files {
179 if err := writeFile(f.path, f.data); err != nil {
180 return err
181 }
182 }
183 return nil
184}
185
186// removeStale deletes <side>/<pkg>/<pkg>_generated.go for packages that are
187// not in m. If such a directory holds anything else, nothing is deleted and an
188// error lists the remaining files.
189func removeStale(m *spec.Manifest, root string) error {
190 known := map[string]bool{}
191 for _, e := range m.Protocols {
192 known[e.Package] = true
193 }
194 type stale struct{ dir, gen string }
195 var list []stale
196 var problems []string
197 for _, side := range []string{"client", "server"} {
198 entries, err := os.ReadDir(filepath.Join(root, side))
199 if os.IsNotExist(err) {
200 continue
201 }
202 if err != nil {
203 return err
204 }
205 for _, d := range entries {
206 if !d.IsDir() || known[d.Name()] {
207 continue
208 }
209 dir := filepath.Join(root, side, d.Name())
210 gen := filepath.Join(dir, d.Name()+"_generated.go")
211 if _, err := os.Stat(gen); err != nil {
212 continue // not generated output; leave alone
213 }
214 files, err := os.ReadDir(dir)
215 if err != nil {
216 return err
217 }
218 for _, f := range files {
219 if f.Name() != d.Name()+"_generated.go" {
220 problems = append(problems, filepath.Join(side, d.Name(), f.Name()))
221 }
222 }
223 list = append(list, stale{dir, gen})
224 }
225 }
226 if len(problems) > 0 {
227 sort.Strings(problems)
228 return fmt.Errorf("package removed from manifest but directory still holds other files: %s", strings.Join(problems, ", "))
229 }
230 for _, s := range list {
231 if err := os.Remove(s.gen); err != nil {
232 return err
233 }
234 if err := os.Remove(s.dir); err != nil {
235 return err
236 }
237 }
238 return nil
239}
240
241// foreignInterfaces returns, sorted, the interfaces referenced by object and
242// new_id arguments of p that p does not define.
243func foreignInterfaces(p *spec.Protocol) []string {
244 local := map[string]bool{}
245 for _, it := range p.Interfaces {
246 local[it.Name] = true
247 }
248 seen := map[string]bool{}
249 for _, it := range p.Interfaces {
250 for _, msgs := range [][]spec.Message{it.Requests, it.Events} {
251 for _, msg := range msgs {
252 for _, a := range msg.Args {
253 if (a.Type == "object" || a.Type == "new_id") && a.Interface != "" && !local[a.Interface] {
254 seen[a.Interface] = true
255 }
256 }
257 }
258 }
259 }
260 out := make([]string, 0, len(seen))
261 for n := range seen {
262 out = append(out, n)
263 }
264 sort.Strings(out)
265 return out
266}
267
268func generateCustom(m *spec.Manifest, side, pkg, source string, data []byte, overrides map[string]string) ([]byte, error) {
269 p, err := spec.Parse(data)
270 if err != nil {
271 return nil, fmt.Errorf("%s: %w", source, err)
272 }
273 foreign := foreignInterfaces(p)
274 owners := m.Owners()
275 switch side {
276 case "client":
277 s := client.NewScanner()
278 s.Tool = tool
279 s.CrossPackage = map[string]string{}
280 for _, iface := range foreign {
281 if p, ok := overrides[iface]; ok {
282 s.CrossPackage[iface] = p
283 } else if owner, ok := owners[iface]; ok {
284 s.CrossPackage[iface] = clientImport(owner)
285 }
286 // Unresolved interfaces are reported by the generator unless
287 // they are transport bootstrap types.
288 }
289 if err := s.LoadProtocol(p, source); err != nil {
290 return nil, err
291 }
292 return s.Generate(pkg)
293 case "server":
294 opts := server.Options{
295 Package: pkg, Imports: map[string]string{}, Owners: map[string]string{}, Tool: tool,
296 Copyright: p.Copyright,
297 }
298 for _, iface := range foreign {
299 var alias, importPath string
300 if p, ok := overrides[iface]; ok {
301 alias, importPath = importAlias(p), p
302 } else if owner, ok := owners[iface]; ok {
303 alias, importPath = owner, serverImport(owner)
304 } else {
305 return nil, fmt.Errorf("unresolved interface %q: not in the manifest; add -import %s=<importpath>", iface, iface)
306 }
307 if prev, ok := opts.Imports[alias]; ok && prev != importPath {
308 return nil, fmt.Errorf("import alias %q is used for both %q and %q", alias, prev, importPath)
309 }
310 opts.Imports[alias] = importPath
311 opts.Owners[iface] = alias
312 }
313 return server.Generate(p, opts)
314 default:
315 return nil, fmt.Errorf("-side must be client or server, got %q", side)
316 }
317}
318
319// importAlias derives a Go identifier from the last element of an import path.
320func importAlias(p string) string {
321 b := []byte(path.Base(p))
322 for i, c := range b {
323 if !(c == '_' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || i > 0 && c >= '0' && c <= '9') {
324 b[i] = '_'
325 }
326 }
327 return string(b)
328}