176 lines
5.5 KiB
Go
176 lines
5.5 KiB
Go
package apigen
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"go/importer"
|
|
gotoken "go/token"
|
|
"go/types"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/mod/modfile"
|
|
)
|
|
|
|
// loadMappingTypes uses the Go compiler's export data, including the pending
|
|
// DTOs via an overlay. Module files and sum updates remain in a temporary dir.
|
|
// It compiles only explicitly selected packages and their dependencies; never
|
|
// runs project code. Build tags/GOOS follow the caller; GOWORK is disabled.
|
|
func loadMappingTypes(o Options, refs []string, generated, seeds map[string][]byte) (types.Importer, func(), error) {
|
|
// go list resolves its working directory (notably /var -> /private/var on
|
|
// macOS). Overlay keys must name that same physical path or stale DTOs win.
|
|
o.Dir = canonical(o.Dir)
|
|
temp, err := os.MkdirTemp("", "go-omni-mapping-*")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
cleanup := func() { _ = os.RemoveAll(temp) }
|
|
fail := func(err error) (types.Importer, func(), error) { cleanup(); return nil, nil, err }
|
|
raw, err := os.ReadFile(filepath.Join(o.Dir, "go.mod"))
|
|
if err != nil {
|
|
return fail(errors.New("mapping generation requires an existing Go module; scaffold first and resolve dependencies"))
|
|
}
|
|
mf, err := modfile.Parse("go.mod", raw, nil)
|
|
if err != nil {
|
|
return fail(err)
|
|
}
|
|
replacements := append([]*modfile.Replace(nil), mf.Replace...)
|
|
for _, r := range replacements {
|
|
if r.New.Version == "" && !filepath.IsAbs(r.New.Path) {
|
|
if err = mf.AddReplace(r.Old.Path, r.Old.Version, filepath.Join(o.Dir, r.New.Path), ""); err != nil {
|
|
return fail(err)
|
|
}
|
|
}
|
|
}
|
|
raw, err = mf.Format()
|
|
if err != nil {
|
|
return fail(err)
|
|
}
|
|
modPath := filepath.Join(temp, "go.mod")
|
|
if err = os.WriteFile(modPath, raw, 0600); err != nil {
|
|
return fail(err)
|
|
}
|
|
if sums, e := os.ReadFile(filepath.Join(o.Dir, "go.sum")); e == nil {
|
|
if err = os.WriteFile(filepath.Join(temp, "go.sum"), sums, 0600); err != nil {
|
|
return fail(err)
|
|
}
|
|
} else if !errors.Is(e, os.ErrNotExist) {
|
|
return fail(e)
|
|
}
|
|
overlay := map[string]string{}
|
|
// Hide stale owned Go files, which may otherwise redeclare a renamed DTO.
|
|
var previous manifest
|
|
if data, e := os.ReadFile(filepath.Join(o.Dir, manifestPath)); e == nil {
|
|
if err = json.Unmarshal(data, &previous); err != nil {
|
|
return fail(err)
|
|
}
|
|
for name := range previous.Generated {
|
|
if strings.HasSuffix(name, ".go") {
|
|
if !ownedPath(name) {
|
|
return fail(fmt.Errorf("invalid owned path %s", name))
|
|
}
|
|
if err = safePath(o.Dir, name); err != nil {
|
|
return fail(err)
|
|
}
|
|
if _, ok := generated[name]; !ok {
|
|
overlay[filepath.Join(o.Dir, filepath.FromSlash(name))] = ""
|
|
}
|
|
}
|
|
}
|
|
} else if !errors.Is(e, os.ErrNotExist) {
|
|
return fail(e)
|
|
}
|
|
for i, files := range []map[string][]byte{generated, seeds} {
|
|
names := make([]string, 0, len(files))
|
|
for name := range files {
|
|
if strings.HasSuffix(name, ".go") {
|
|
names = append(names, name)
|
|
}
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
if err = safePath(o.Dir, name); err != nil {
|
|
return fail(err)
|
|
}
|
|
path := filepath.Join(o.Dir, filepath.FromSlash(name))
|
|
if i == 1 {
|
|
if _, e := os.Stat(path); e == nil {
|
|
continue
|
|
} else if !errors.Is(e, os.ErrNotExist) {
|
|
return fail(e)
|
|
}
|
|
}
|
|
backing := filepath.Join(temp, fmt.Sprintf("overlay%d.go", len(overlay)))
|
|
if err = os.WriteFile(backing, files[name], 0600); err != nil {
|
|
return fail(err)
|
|
}
|
|
overlay[path] = backing
|
|
}
|
|
}
|
|
raw, err = json.Marshal(map[string]any{"Replace": overlay})
|
|
if err != nil {
|
|
return fail(err)
|
|
}
|
|
overlayPath := filepath.Join(temp, "overlay.json")
|
|
if err = os.WriteFile(overlayPath, raw, 0600); err != nil {
|
|
return fail(err)
|
|
}
|
|
args := []string{"list", "-deps", "-export", "-json", "-mod=readonly", "-modfile", modPath, "-overlay", overlayPath}
|
|
// fmt is used for fallible conversions even if neither struct imports it.
|
|
args = append(args, "fmt")
|
|
args = append(args, refs...)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(ctx, "go", args...)
|
|
cmd.Dir = o.Dir
|
|
cmd.Env = append(os.Environ(), "GOWORK=off")
|
|
cmd.WaitDelay = 2 * time.Second
|
|
stdout := &limitedCatalogBuffer{limit: 32 << 20}
|
|
stderr := &limitedCatalogBuffer{limit: 64 << 10}
|
|
cmd.Stdout = stdout
|
|
cmd.Stderr = stderr
|
|
if err = cmd.Run(); err != nil {
|
|
if ctx.Err() != nil {
|
|
return fail(fmt.Errorf("mapping type loading: %w", ctx.Err()))
|
|
}
|
|
// Go download errors may contain authenticated remote URLs. Keep arbitrary
|
|
// command output out of diagnostics; exact mapping errors follow type loading.
|
|
return fail(fmt.Errorf("mapping type loading failed (%v); run go list -deps -export for the configured source/target/converter packages; check dependencies, build tags and import cycles", err))
|
|
}
|
|
exports := map[string]string{}
|
|
dec := json.NewDecoder(bytes.NewReader(stdout.Bytes()))
|
|
for {
|
|
var p struct {
|
|
ImportPath, Export string
|
|
Error *struct{ Err string }
|
|
}
|
|
if err = dec.Decode(&p); err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
return fail(err)
|
|
}
|
|
if p.Error != nil {
|
|
return fail(fmt.Errorf("mapping package %s could not be compiled", p.ImportPath))
|
|
}
|
|
if p.Export != "" {
|
|
exports[p.ImportPath] = p.Export
|
|
}
|
|
}
|
|
imp := importer.ForCompiler(gotoken.NewFileSet(), "gc", func(path string) (io.ReadCloser, error) {
|
|
file, ok := exports[path]
|
|
if !ok {
|
|
return nil, fmt.Errorf("no export data for mapping dependency %s", path)
|
|
}
|
|
return os.Open(file)
|
|
})
|
|
return imp, cleanup, nil
|
|
}
|