Compare commits

...

3 Commits

Author SHA1 Message Date
Patricio Whittingslow cc959c5aab fix targets list 2026-02-25 12:49:07 -03:00
Patricio Whittingslow ef77ed3103 show number of different APIs in red 2026-02-25 12:29:37 -03:00
Patricio Whittingslow ca2694c5d9 add tgdoc tool 2026-02-25 12:21:09 -03:00
4 changed files with 912 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
package main
import (
"bufio"
"bytes"
"fmt"
"go/ast"
"go/build/constraint"
"go/doc"
"go/parser"
"go/token"
"os"
"path/filepath"
"strings"
)
// buildTagSet creates the full set of tags that should evaluate to true
// for a given target, matching TinyGo's Config.BuildTags() behavior.
func buildTagSet(t Target) map[string]bool {
tags := make(map[string]bool)
for _, tag := range t.BuildTags {
tags[tag] = true
}
if t.GOOS != "" {
tags[t.GOOS] = true
}
if t.GOARCH != "" {
tags[t.GOARCH] = true
}
tags["tinygo"] = true
tags["purego"] = true
tags["osusergo"] = true
tags["math_big_pure_go"] = true
if t.GC != "" {
tags["gc."+t.GC] = true
}
if t.Scheduler != "" {
tags["scheduler."+t.Scheduler] = true
}
if t.Serial != "" {
tags["serial."+t.Serial] = true
}
// Go version tags — TinyGo currently tracks Go 1.22.
for i := 1; i <= 22; i++ {
tags[fmt.Sprintf("go1.%d", i)] = true
}
return tags
}
// extractBuildConstraint reads the file header (before the package clause)
// looking for a //go:build constraint line.
func extractBuildConstraint(data []byte) (constraint.Expr, error) {
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
line := scanner.Text()
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "package ") {
break
}
if constraint.IsGoBuild(trimmed) {
return constraint.Parse(trimmed)
}
}
return nil, nil
}
// ExtractDocs filters .go files in pkgDir by the target's build tags,
// parses them, and returns a *doc.Package via go/doc.NewFromFiles.
func ExtractDocs(t Target, pkgDir string, allDecls bool) (*doc.Package, *token.FileSet, error) {
tags := buildTagSet(t)
entries, err := os.ReadDir(pkgDir)
if err != nil {
return nil, nil, err
}
fset := token.NewFileSet()
var files []*ast.File
for _, e := range entries {
name := e.Name()
if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
continue
}
path := filepath.Join(pkgDir, name)
data, err := os.ReadFile(path)
if err != nil {
return nil, nil, err
}
expr, err := extractBuildConstraint(data)
if err != nil {
continue // skip files with unparseable constraints
}
if expr != nil && !expr.Eval(func(tag string) bool { return tags[tag] }) {
continue
}
f, err := parser.ParseFile(fset, path, data, parser.ParseComments)
if err != nil {
continue // skip files with syntax errors
}
files = append(files, f)
}
if len(files) == 0 {
return nil, nil, fmt.Errorf("no matching files for target %s", t.Name)
}
var opts []any
if allDecls {
opts = append(opts, doc.AllDecls)
}
docPkg, err := doc.NewFromFiles(fset, files, "machine", opts...)
if err != nil {
return nil, nil, err
}
return docPkg, fset, nil
}
+172
View File
@@ -0,0 +1,172 @@
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"go/ast"
"go/doc"
"go/format"
"go/token"
"log"
"os"
"sort"
"strings"
)
// Result holds the extracted documentation for a single target.
type Result struct {
Target Target
Pkg *doc.Package
Fset *token.FileSet
}
// JSON-serializable API types.
type PackageAPI struct {
Target string `json:"target"`
Package string `json:"package"`
Doc string `json:"doc,omitempty"`
Constants []APIValue `json:"constants,omitempty"`
Variables []APIValue `json:"variables,omitempty"`
Types []APIType `json:"types,omitempty"`
Functions []APIFunc `json:"functions,omitempty"`
}
type APIValue struct {
Doc string `json:"doc,omitempty"`
Names []string `json:"names"`
Decl string `json:"decl"`
}
type APIType struct {
Doc string `json:"doc,omitempty"`
Name string `json:"name"`
Decl string `json:"decl"`
Constants []APIValue `json:"constants,omitempty"`
Variables []APIValue `json:"variables,omitempty"`
Functions []APIFunc `json:"functions,omitempty"`
Methods []APIFunc `json:"methods,omitempty"`
}
type APIFunc struct {
Doc string `json:"doc,omitempty"`
Name string `json:"name"`
Decl string `json:"decl"`
Recv string `json:"recv,omitempty"`
}
func formatNode(fset *token.FileSet, node ast.Node) string {
var buf bytes.Buffer
format.Node(&buf, fset, node)
return buf.String()
}
func convertValues(fset *token.FileSet, vals []*doc.Value) []APIValue {
out := make([]APIValue, 0, len(vals))
for _, v := range vals {
out = append(out, APIValue{
Doc: v.Doc,
Names: v.Names,
Decl: formatNode(fset, v.Decl),
})
}
return out
}
func convertFuncs(fset *token.FileSet, funcs []*doc.Func) []APIFunc {
out := make([]APIFunc, 0, len(funcs))
for _, f := range funcs {
out = append(out, APIFunc{
Doc: f.Doc,
Name: f.Name,
Decl: formatNode(fset, f.Decl),
Recv: f.Recv,
})
}
return out
}
func convertPackage(targetName string, pkg *doc.Package, fset *token.FileSet) *PackageAPI {
api := &PackageAPI{
Target: targetName,
Package: pkg.Name,
Doc: pkg.Doc,
Constants: convertValues(fset, pkg.Consts),
Variables: convertValues(fset, pkg.Vars),
Functions: convertFuncs(fset, pkg.Funcs),
}
for _, t := range pkg.Types {
api.Types = append(api.Types, APIType{
Doc: t.Doc,
Name: t.Name,
Decl: formatNode(fset, t.Decl),
Constants: convertValues(fset, t.Consts),
Variables: convertValues(fset, t.Vars),
Functions: convertFuncs(fset, t.Funcs),
Methods: convertFuncs(fset, t.Methods),
})
}
return api
}
func main() {
jsonOutput := flag.Bool("json", false, "output JSON to stdout")
httpAddr := flag.String("http", "", "serve HTTP documentation (e.g. :8080)")
allDecls := flag.Bool("all", false, "include unexported identifiers")
includeBase := flag.Bool("base", false, "include base/parent targets")
flag.Parse()
args := flag.Args()
if len(args) < 2 {
fmt.Fprintf(os.Stderr, "usage: tgdoc [flags] <targets-dir> <package-dir>\n")
fmt.Fprintf(os.Stderr, "\nexample: tgdoc -http :8080 ./targets ./src/machine\n")
fmt.Fprintf(os.Stderr, "\nFlags:\n")
flag.PrintDefaults()
os.Exit(1)
}
targetsDir := args[0]
pkgDir := args[1]
if *httpAddr != "" && !strings.Contains(*httpAddr, ":") {
fmt.Fprintf(os.Stderr, "error: -http value %q doesn't look like an address (expected e.g. :8080)\n", *httpAddr)
os.Exit(1)
}
targets, err := LoadTargets(targetsDir, *includeBase)
if err != nil {
log.Fatal(err)
}
var results []Result
for _, t := range targets {
pkg, fset, err := ExtractDocs(t, pkgDir, *allDecls)
if err != nil {
fmt.Fprintf(os.Stderr, "warning: %s: %v\n", t.Name, err)
continue
}
results = append(results, Result{t, pkg, fset})
}
sort.Slice(results, func(i, j int) bool { return results[i].Target.Name < results[j].Target.Name })
if *httpAddr != "" {
serve(*httpAddr, results)
return
}
// Default: JSON output.
_ = jsonOutput
apis := make([]*PackageAPI, 0, len(results))
for _, r := range results {
apis = append(apis, convertPackage(r.Target.Name, r.Pkg, r.Fset))
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(apis); err != nil {
log.Fatal(err)
}
}
+448
View File
@@ -0,0 +1,448 @@
package main
import (
"bytes"
"go/ast"
"go/doc"
"go/format"
"go/token"
"html/template"
"log"
"net/http"
"sort"
"strings"
)
// IdentIndex is the precomputed identifier index across all targets.
type IdentIndex struct {
Entries []*IdentEntry
ByKey map[string]*IdentEntry
Total int // total number of targets
}
// IdentEntry is one identifier (type, func, method, const, or var) across all targets.
type IdentEntry struct {
Name string
Kind string // "type", "func", "method", "const", "var"
Count int
Key string // URL key: "kind/name"
Groups []IdentGroup
}
// IdentGroup is a set of targets with identical declaration for an identifier.
type IdentGroup struct {
Decl string // representative formatted declaration
Targets []string // sorted target names
}
func buildIdentIndex(results []Result) *IdentIndex {
type occ struct {
target string
decl string // grouping key + display
}
type ikey struct {
name, kind string
}
idx := make(map[ikey][]occ)
addFunc := func(target string, fset *token.FileSet, f *doc.Func, prefix string) {
name := f.Name
if prefix != "" {
name = prefix + "." + f.Name
}
kind := "func"
if f.Recv != "" {
kind = "method"
}
decl := fmtNode(fset, f.Decl)
idx[ikey{name, kind}] = append(idx[ikey{name, kind}], occ{target, decl})
}
addValues := func(target string, fset *token.FileSet, vals []*doc.Value, kind string) {
for _, v := range vals {
for _, name := range v.Names {
typeKey := resolveValueType(fset, v.Decl, name)
idx[ikey{name, kind}] = append(idx[ikey{name, kind}], occ{target, typeKey})
}
}
}
for _, r := range results {
t := r.Target.Name
fset := r.Fset
pkg := r.Pkg
for _, ty := range pkg.Types {
decl := fmtNode(fset, ty.Decl)
idx[ikey{ty.Name, "type"}] = append(idx[ikey{ty.Name, "type"}], occ{t, decl})
for _, m := range ty.Methods {
addFunc(t, fset, m, ty.Name)
}
for _, f := range ty.Funcs {
addFunc(t, fset, f, "")
}
addValues(t, fset, ty.Consts, "const")
addValues(t, fset, ty.Vars, "var")
}
for _, f := range pkg.Funcs {
addFunc(t, fset, f, "")
}
addValues(t, fset, pkg.Consts, "const")
addValues(t, fset, pkg.Vars, "var")
}
index := &IdentIndex{
ByKey: make(map[string]*IdentEntry),
Total: len(results),
}
for key, occs := range idx {
groups := make(map[string][]string) // decl -> targets
for _, o := range occs {
groups[o.decl] = append(groups[o.decl], o.target)
}
var gs []IdentGroup
for decl, targets := range groups {
sort.Strings(targets)
gs = append(gs, IdentGroup{Decl: decl, Targets: targets})
}
sort.Slice(gs, func(i, j int) bool { return len(gs[i].Targets) > len(gs[j].Targets) })
urlKey := key.kind + "/" + key.name
entry := &IdentEntry{
Name: key.name,
Kind: key.kind,
Count: len(occs),
Key: urlKey,
Groups: gs,
}
index.Entries = append(index.Entries, entry)
index.ByKey[urlKey] = entry
}
sort.Slice(index.Entries, func(i, j int) bool {
if index.Entries[i].Count != index.Entries[j].Count {
return index.Entries[i].Count > index.Entries[j].Count
}
return index.Entries[i].Name < index.Entries[j].Name
})
return index
}
// resolveValueType returns the effective type string for a const/var name
// within a GenDecl, handling iota type inheritance for constants.
func resolveValueType(fset *token.FileSet, gd *ast.GenDecl, name string) string {
isConst := gd.Tok == token.CONST
var lastType ast.Expr
for _, spec := range gd.Specs {
vs := spec.(*ast.ValueSpec)
if vs.Type != nil {
lastType = vs.Type
}
for _, n := range vs.Names {
if n.Name != name {
continue
}
effType := vs.Type
if effType == nil && isConst {
effType = lastType
}
if effType != nil {
return fmtNode(fset, effType)
}
return "(untyped)"
}
}
return "(unknown)"
}
// fmtNode formats an AST node to Go source. Separate from formatNode in main.go
// to make it clear this is the server's version (same logic).
func fmtNode(fset *token.FileSet, node ast.Node) string {
var buf bytes.Buffer
format.Node(&buf, fset, node)
return buf.String()
}
// --- HTTP Server ---
func serve(addr string, results []Result) {
index := buildIdentIndex(results)
byTarget := make(map[string]Result, len(results))
for _, r := range results {
byTarget[r.Target.Name] = r
}
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
data := pageData{Index: index, Results: results}
pageTmpl.Execute(w, data)
})
mux.HandleFunc("/id/", func(w http.ResponseWriter, r *http.Request) {
key := strings.TrimPrefix(r.URL.Path, "/id/")
entry, ok := index.ByKey[key]
if !ok {
http.NotFound(w, r)
return
}
data := pageData{Index: index, Results: results, Selected: entry, SelectedKey: key}
pageTmpl.Execute(w, data)
})
mux.HandleFunc("/target/", func(w http.ResponseWriter, r *http.Request) {
name := strings.TrimPrefix(r.URL.Path, "/target/")
res, ok := byTarget[name]
if !ok {
http.NotFound(w, r)
return
}
api := convertPackage(res.Target.Name, res.Pkg, res.Fset)
targetTmpl.Execute(w, api)
})
log.Printf("serving documentation on http://localhost%s", addr)
log.Fatal(http.ListenAndServe(addr, mux))
}
type pageData struct {
Index *IdentIndex
Results []Result
Selected *IdentEntry
SelectedKey string
}
// --- Templates ---
var pageTmpl = template.Must(template.New("page").Funcs(template.FuncMap{
"kindBadge": func(kind string) string {
switch kind {
case "type":
return "T"
case "func":
return "F"
case "method":
return "M"
case "const":
return "C"
case "var":
return "V"
}
return "?"
},
"pre": func(s string) template.HTML {
return template.HTML("<pre>" + template.HTMLEscapeString(s) + "</pre>")
},
"kindClass": func(kind string) string { return "kind-" + kind },
}).Parse(`<!DOCTYPE html>
<html><head>
<title>tgdoc{{if .Selected}}{{.Selected.Name}}{{end}}</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, -apple-system, sans-serif; }
.layout { display: flex; height: 100vh; }
/* Sidebar */
.sidebar { width: 320px; min-width: 320px; border-right: 1px solid #ddd; display: flex; flex-direction: column; background: #fafafa; }
.sidebar-header { padding: 12px; border-bottom: 1px solid #ddd; }
.sidebar-header h2 { font-size: 1.1em; margin-bottom: 8px; }
.sidebar-header input { width: 100%; padding: 6px 10px; border: 1px solid #ccc; border-radius: 4px; font-size: 0.9em; }
.sidebar-header .filter-row { display: flex; gap: 4px; margin-top: 6px; flex-wrap: wrap; }
.sidebar-header .filter-btn { font-size: 0.7em; padding: 2px 6px; border: 1px solid #ccc; border-radius: 3px; background: white; cursor: pointer; }
.sidebar-header .filter-btn.active { background: #333; color: white; border-color: #333; }
.ident-list { overflow-y: auto; flex: 1; }
.ident-list a { display: flex; align-items: center; padding: 3px 12px; text-decoration: none; color: #333; font-size: 0.85em; border-left: 3px solid transparent; }
.ident-list a:hover { background: #f0f0f0; }
.ident-list a.active { background: #e8e8f4; border-left-color: #4444cc; }
.ident-list .badge { display: inline-block; width: 18px; height: 18px; line-height: 18px; text-align: center; border-radius: 3px; font-size: 0.65em; font-weight: bold; margin-right: 6px; color: white; flex-shrink: 0; }
.ident-list .count { margin-left: auto; color: #999; font-size: 0.8em; padding-left: 8px; flex-shrink: 0; }
.ident-list .ngroups { color: salmon; font-size: 0.8em; padding-left: 2px; flex-shrink: 0; }
.ident-list .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kind-type .badge { background: #2196F3; }
.kind-func .badge { background: #4CAF50; }
.kind-method .badge { background: #9C27B0; }
.kind-const .badge { background: #FF9800; }
.kind-var .badge { background: #795548; }
/* Main content */
main { flex: 1; overflow-y: auto; padding: 2em; }
main h1 { font-size: 1.5em; border-bottom: 2px solid #333; padding-bottom: 0.3em; margin-bottom: 0.5em; }
main .subtitle { color: #666; margin-bottom: 1.5em; }
.group { margin-bottom: 1.5em; border: 1px solid #e0e0e0; border-radius: 6px; overflow: hidden; }
.group-header { background: #f5f5f5; padding: 10px 14px; font-size: 0.9em; border-bottom: 1px solid #e0e0e0; }
.group-header strong { font-size: 1.05em; }
.group pre { margin: 0; padding: 14px; background: #fcfcfc; overflow-x: auto; font-size: 0.85em; border-bottom: 1px solid #eee; }
.group .targets { padding: 10px 14px; display: flex; flex-wrap: wrap; gap: 4px; }
.group .targets a { background: #e8e8e8; padding: 2px 8px; border-radius: 3px; font-size: 0.78em; text-decoration: none; color: #333; }
.group .targets a:hover { background: #d0d0d0; }
.welcome nav a { margin-right: 1em; }
</style>
</head><body>
<div class="layout">
<aside class="sidebar">
<div class="sidebar-header">
<h2><a href="/" style="text-decoration:none;color:inherit">tgdoc</a></h2>
<input type="text" id="filter" placeholder="Filter identifiers..." oninput="filterList()">
<div class="filter-row">
<button class="filter-btn active" data-kind="all" onclick="toggleKind(this)">All</button>
<button class="filter-btn active" data-kind="type" onclick="toggleKind(this)">T</button>
<button class="filter-btn active" data-kind="func" onclick="toggleKind(this)">F</button>
<button class="filter-btn active" data-kind="method" onclick="toggleKind(this)">M</button>
<button class="filter-btn active" data-kind="const" onclick="toggleKind(this)">C</button>
<button class="filter-btn active" data-kind="var" onclick="toggleKind(this)">V</button>
</div>
</div>
<div class="ident-list">
{{range .Index.Entries}}
<a href="/id/{{.Key}}" class="{{kindClass .Kind}}{{if eq .Key $.SelectedKey}} active{{end}}" data-name="{{.Name}}" data-kind="{{.Kind}}">
<span class="badge">{{kindBadge .Kind}}</span>
<span class="name">{{.Name}}</span>
<span class="count">{{.Count}}{{if gt (len .Groups) 1}}<span class="ngroups">/{{len .Groups}}</span>{{end}}</span>
</a>
{{end}}
</div>
</aside>
<main>
{{if .Selected}}
<h1><span class="badge" style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:0.6em;vertical-align:middle;color:white;background:{{if eq .Selected.Kind "type"}}#2196F3{{else if eq .Selected.Kind "func"}}#4CAF50{{else if eq .Selected.Kind "method"}}#9C27B0{{else if eq .Selected.Kind "const"}}#FF9800{{else}}#795548{{end}}">{{.Selected.Kind}}</span> {{.Selected.Name}}</h1>
<p class="subtitle">Present in {{.Selected.Count}} / {{.Index.Total}} targets
{{if eq (len .Selected.Groups) 1}}— identical across all targets{{else}}{{len .Selected.Groups}} distinct signatures{{end}}</p>
{{range .Selected.Groups}}
<div class="group">
<div class="group-header"><strong>{{len .Targets}} target{{if ne (len .Targets) 1}}s{{end}}</strong></div>
{{pre .Decl}}
<div class="targets">{{range .Targets}}<a href="/target/{{.}}">{{.}}</a> {{end}}</div>
</div>
{{end}}
{{else}}
<div class="welcome">
<h1>tgdoc</h1>
<p class="subtitle">{{len .Index.Entries}} identifiers across {{.Index.Total}} targets</p>
<p>Select an identifier from the sidebar to see its appearances across targets.</p>
<p style="margin-top:1em">Browse by target:</p>
<nav style="margin-top:0.5em;column-count:3">
{{range .Results}}<a href="/target/{{.Target.Name}}" style="display:block;padding:2px 0">{{.Target.Name}}</a>
{{end}}
</nav>
</div>
{{end}}
</main>
</div>
<script>
var activeKinds = new Set(["type","func","method","const","var"]);
function filterList() {
var q = document.getElementById("filter").value.toLowerCase();
document.querySelectorAll(".ident-list a").forEach(function(a) {
var name = a.dataset.name.toLowerCase();
var kind = a.dataset.kind;
var textMatch = !q || name.includes(q);
var kindMatch = activeKinds.has(kind);
a.style.display = (textMatch && kindMatch) ? "" : "none";
});
}
function toggleKind(btn) {
var kind = btn.dataset.kind;
if (kind === "all") {
var allActive = activeKinds.size === 5;
document.querySelectorAll(".filter-btn").forEach(function(b) {
if (allActive) { b.classList.remove("active"); activeKinds.clear(); }
else { b.classList.add("active"); activeKinds.add(b.dataset.kind); }
});
if (!allActive) activeKinds.delete("all");
} else {
btn.classList.toggle("active");
if (activeKinds.has(kind)) activeKinds.delete(kind); else activeKinds.add(kind);
var allBtn = document.querySelector('[data-kind="all"]');
if (activeKinds.size === 5) allBtn.classList.add("active"); else allBtn.classList.remove("active");
}
filterList();
}
</script>
</body></html>`))
var targetTmpl = template.Must(template.New("target").Funcs(template.FuncMap{
"pre": func(s string) template.HTML {
return template.HTML("<pre>" + template.HTMLEscapeString(s) + "</pre>")
},
}).Parse(`<!DOCTYPE html>
<html><head>
<title>{{.Target}} — tgdoc</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 900px; margin: 2em auto; padding: 0 1em; }
h1 { border-bottom: 2px solid #333; padding-bottom: 0.3em; }
h2 { margin-top: 2em; color: #444; }
h3 { margin-top: 1.5em; }
pre { background: #f5f5f5; padding: 0.8em; overflow-x: auto; font-size: 0.9em; border-radius: 4px; }
.doc { color: #555; margin-bottom: 0.5em; white-space: pre-wrap; }
.section { margin-left: 1em; }
a { text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head><body>
<p><a href="/">← identifier index</a></p>
<h1>{{.Target}}</h1>
<p>package {{.Package}}</p>
{{if .Doc}}<div class="doc">{{.Doc}}</div>{{end}}
{{if .Constants}}
<h2>Constants</h2>
{{range .Constants}}
{{if .Doc}}<div class="doc">{{.Doc}}</div>{{end}}
{{pre .Decl}}
{{end}}
{{end}}
{{if .Variables}}
<h2>Variables</h2>
{{range .Variables}}
{{if .Doc}}<div class="doc">{{.Doc}}</div>{{end}}
{{pre .Decl}}
{{end}}
{{end}}
{{if .Functions}}
<h2>Functions</h2>
{{range .Functions}}
<h3>func {{.Name}}</h3>
{{if .Doc}}<div class="doc">{{.Doc}}</div>{{end}}
{{pre .Decl}}
{{end}}
{{end}}
{{if .Types}}
<h2>Types</h2>
{{range .Types}}
<h3>type {{.Name}}</h3>
{{if .Doc}}<div class="doc">{{.Doc}}</div>{{end}}
{{pre .Decl}}
{{if .Constants}}<div class="section"><h4>Associated Constants</h4>
{{range .Constants}}{{pre .Decl}}{{end}}</div>{{end}}
{{if .Variables}}<div class="section"><h4>Associated Variables</h4>
{{range .Variables}}{{pre .Decl}}{{end}}</div>{{end}}
{{if .Functions}}<div class="section"><h4>Constructors</h4>
{{range .Functions}}
{{if .Doc}}<div class="doc">{{.Doc}}</div>{{end}}
{{pre .Decl}}
{{end}}</div>{{end}}
{{if .Methods}}<div class="section"><h4>Methods</h4>
{{range .Methods}}
{{if .Doc}}<div class="doc">{{.Doc}}</div>{{end}}
{{pre .Decl}}
{{end}}</div>{{end}}
{{end}}
{{end}}
</body></html>`))
+168
View File
@@ -0,0 +1,168 @@
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
// targetSpec is the minimal subset of TinyGo's target JSON we need.
type targetSpec struct {
Inherits []string `json:"inherits,omitempty"`
BuildTags []string `json:"build-tags,omitempty"`
GOOS string `json:"goos,omitempty"`
GOARCH string `json:"goarch,omitempty"`
GC string `json:"gc,omitempty"`
Scheduler string `json:"scheduler,omitempty"`
Serial string `json:"serial,omitempty"`
FlashMethod string `json:"flash-method,omitempty"`
FlashCommand string `json:"flash-command,omitempty"`
Emulator string `json:"emulator,omitempty"`
}
// Target is a resolved target with inheritance applied.
type Target struct {
Name string
BuildTags []string
GOOS string
GOARCH string
GC string
Scheduler string
Serial string
}
// override merges src properties into dst following TinyGo's semantics:
// strings override if non-empty, slices append with dedup.
func (dst *targetSpec) override(src *targetSpec) {
if src.GOOS != "" {
dst.GOOS = src.GOOS
}
if src.GOARCH != "" {
dst.GOARCH = src.GOARCH
}
if src.GC != "" {
dst.GC = src.GC
}
if src.Scheduler != "" {
dst.Scheduler = src.Scheduler
}
if src.Serial != "" {
dst.Serial = src.Serial
}
if src.FlashMethod != "" {
dst.FlashMethod = src.FlashMethod
}
if src.FlashCommand != "" {
dst.FlashCommand = src.FlashCommand
}
if src.Emulator != "" {
dst.Emulator = src.Emulator
}
dst.BuildTags = appendUnique(dst.BuildTags, src.BuildTags...)
}
func appendUnique(dst []string, src ...string) []string {
seen := make(map[string]bool, len(dst))
for _, s := range dst {
seen[s] = true
}
for _, s := range src {
if !seen[s] {
dst = append(dst, s)
seen[s] = true
}
}
return dst
}
func loadRawTargets(dir string) (map[string]*targetSpec, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
specs := make(map[string]*targetSpec)
for _, e := range entries {
if !e.Type().IsRegular() || !strings.HasSuffix(e.Name(), ".json") {
continue
}
name := strings.TrimSuffix(e.Name(), ".json")
data, err := os.ReadFile(filepath.Join(dir, e.Name()))
if err != nil {
return nil, err
}
var spec targetSpec
if err := json.Unmarshal(data, &spec); err != nil {
return nil, fmt.Errorf("parsing %s: %w", e.Name(), err)
}
specs[name] = &spec
}
return specs, nil
}
func resolveSpec(name string, raw map[string]*targetSpec, cache map[string]*targetSpec, resolving map[string]bool) (*targetSpec, error) {
if cached, ok := cache[name]; ok {
return cached, nil
}
if resolving[name] {
return nil, fmt.Errorf("circular inheritance: %s", name)
}
spec, ok := raw[name]
if !ok {
return nil, fmt.Errorf("unknown target: %s", name)
}
resolving[name] = true
defer delete(resolving, name)
result := &targetSpec{}
for _, parent := range spec.Inherits {
resolved, err := resolveSpec(parent, raw, cache, resolving)
if err != nil {
return nil, fmt.Errorf("resolving %s parent %s: %w", name, parent, err)
}
result.override(resolved)
}
result.override(spec)
cache[name] = result
return result, nil
}
// LoadTargets reads all target JSONs from dir, resolves inheritance,
// and returns the resolved targets. If includeBase is false, targets
// without flash/emulator configuration are excluded (base/parent targets).
func LoadTargets(dir string, includeBase bool) ([]Target, error) {
raw, err := loadRawTargets(dir)
if err != nil {
return nil, err
}
cache := make(map[string]*targetSpec)
resolving := make(map[string]bool)
var targets []Target
for name := range raw {
resolved, err := resolveSpec(name, raw, cache, resolving)
if err != nil {
return nil, err
}
if !includeBase && resolved.FlashMethod == "" && resolved.FlashCommand == "" && resolved.Emulator == "" {
continue
}
targets = append(targets, Target{
Name: name,
BuildTags: resolved.BuildTags,
GOOS: resolved.GOOS,
GOARCH: resolved.GOARCH,
GC: resolved.GC,
Scheduler: resolved.Scheduler,
Serial: resolved.Serial,
})
}
sort.Slice(targets, func(i, j int) bool { return targets[i].Name < targets[j].Name })
return targets, nil
}