detached 5004296

main.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 1

↑↓ moveenter openesc close