Ctrl K
ComunidadGuías…

bldr/util/gocompiler/goscript.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package gocompiler

import (
	"context"
	"os"
	"os/exec"
	"path/filepath"
	"slices"
	"strings"
	"time"

	"github.com/pkg/errors"
	goscript_compiler "github.com/s4wave/goscript/compiler"
	bldr_buildbudget "github.com/s4wave/spacewave/bldr/util/buildbudget"
	"github.com/sirupsen/logrus"
)

// GoScriptStartupCacheEnvKeys returns env keys that affect GoScript artifact
// identity. The compiler cache root only changes reuse cost, not output bytes.
func GoScriptStartupCacheEnvKeys() []string {
	return nil
}

// GoScriptCompileOptions configures one goscript compile invocation.
type GoScriptCompileOptions struct {
	// WorkDir is the module directory used for package loading.
	WorkDir string
	// OutputPath receives the generated TypeScript package tree.
	OutputPath string
	// CacheRoot selects the optional compiler cache directory.
	CacheRoot string
	// Packages names the package patterns to compile.
	Packages []string
	// BuildFlags selects Go package build constraints.
	BuildFlags []string
	// Env supplies environment overrides for package loading.
	Env []string
	// OverrideDirs supplies GoScript source override directories.
	OverrideDirs []string
	// BindingRoots supplies dependency roots containing TypeScript bindings.
	BindingRoots []string
	// DeferredFunctions names exported function boundaries loaded on first invocation.
	DeferredFunctions []string
	// ProtobufTypeScriptBinding uses generated TypeScript protobuf bindings.
	ProtobufTypeScriptBinding bool
	// EliminateDeadCode leaves the code a program cannot reach out of the
	// output.
	EliminateDeadCode bool
}

// GoScriptCompilerCacheRootFromEnv resolves the compiler cache root for a Bldr
// build working path. The cache lives under the user cache directory unless
// the environment overrides it, so resetting a Bldr state root keeps it. An
// empty result disables the cache.
func GoScriptCompilerCacheRootFromEnv(buildPath string) (string, error) {
	rawRoot := strings.TrimSpace(os.Getenv(GoScriptCompilerCacheRootEnv))
	switch rawRoot {
	case "off":
		return "", nil
	case "":
		userCacheDir, err := os.UserCacheDir()
		if err != nil {
			return "", errors.Wrap(err, "resolve GoScript compiler cache root")
		}
		return filepath.Join(userCacheDir, "goscript"), nil
	}
	return ResolveGoScriptCompilerCacheRoot(buildPath, rawRoot)
}

// ResolveGoScriptCompilerCacheRoot resolves rawRoot for a Bldr build path.
// Absolute roots are used directly; relative roots live under the Bldr state
// root that owns the build path.
func ResolveGoScriptCompilerCacheRoot(buildPath, rawRoot string) (string, error) {
	// Return an empty root or use absolute roots directly.
	rawRoot = strings.TrimSpace(rawRoot)
	if rawRoot == "" {
		return "", nil
	}
	if filepath.IsAbs(rawRoot) {
		return filepath.Clean(rawRoot), nil
	}

	// Reject relative roots that escape the .bldr state root.
	cleanRoot := filepath.Clean(rawRoot)
	if cleanRoot == "." || cleanRoot == ".." || strings.HasPrefix(cleanRoot, ".."+string(filepath.Separator)) {
		return "", errors.Errorf("%s relative path escapes .bldr state root: %s", GoScriptCompilerCacheRootEnv, rawRoot)
	}
	stateRoot := goScriptBldrStateRootForBuildPath(buildPath)
	if stateRoot == "" {
		return "", errors.Errorf("%s relative path requires a .bldr build path: %s", GoScriptCompilerCacheRootEnv, buildPath)
	}
	return filepath.Join(stateRoot, cleanRoot), nil
}

func goScriptBldrStateRootForBuildPath(buildPath string) string {
	buildPath = strings.TrimSpace(buildPath)
	if buildPath == "" {
		return ""
	}
	for dir := filepath.Clean(buildPath); ; dir = filepath.Dir(dir) {
		if isGoScriptBldrStateRootName(filepath.Base(dir)) {
			return dir
		}
		parent := filepath.Dir(dir)
		if parent == dir {
			return ""
		}
	}
}

func isGoScriptBldrStateRootName(name string) bool {
	return name == ".bldr" || strings.HasPrefix(name, ".bldr-")
}

// GoScriptBindingRoots returns dependency module roots containing protobuf
// TypeScript siblings. The main module is covered by GoScript's source root.
func GoScriptBindingRoots(ctx context.Context, workDir string, env ...string) ([]string, error) {
	// List all non-main module directories under the work dir.
	cmd := exec.CommandContext(ctx, "go", "list", "-mod=readonly", "-m", "-f", "{{if not .Main}}{{.Dir}}{{end}}", "all")
	cmd.Env = append(os.Environ(), GetDefaultEnv()...)
	cmd.Env = append(cmd.Env, env...)
	cmd.Dir = workDir
	out, err := cmd.CombinedOutput()
	if err != nil {
		var exitErr *exec.ExitError
		if errors.As(err, &exitErr) {
			return nil, errors.Errorf("go list modules failed: %s", strings.TrimSpace(string(out)))
		}
		return nil, err
	}

	// Keep module directories that contain protobuf TypeScript siblings.
	var roots []string
	seen := make(map[string]struct{})
	for moduleDir := range strings.SplitSeq(string(out), "\n") {
		moduleDir = strings.TrimSpace(moduleDir)
		if moduleDir == "" {
			continue
		}
		dir := filepath.Clean(moduleDir)
		if _, ok := seen[dir]; ok || !goScriptBindingRootHasProtobufTypeScript(dir) {
			continue
		}
		seen[dir] = struct{}{}
		roots = append(roots, dir)
	}

	// Return the roots in a stable order.
	slices.Sort(roots)
	return roots, nil
}

func goScriptBindingRootHasProtobufTypeScript(root string) bool {
	found := false
	_ = filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
		// Stop the walk on errors or once a sibling is found.
		if err != nil {
			return err
		}
		if found {
			return filepath.SkipAll
		}
		if entry.IsDir() {
			if path != root && (entry.Name() == "node_modules" || entry.Name() == "vendor") {
				return filepath.SkipDir
			}
			rel, relErr := filepath.Rel(root, path)
			if relErr != nil {
				return relErr
			}
			depth := 0
			if rel != "." {
				depth = strings.Count(rel, string(filepath.Separator)) + 1
			}
			if depth > 6 {
				return filepath.SkipDir
			}
			return nil
		}
		if strings.HasSuffix(entry.Name(), ".pb.ts") {
			found = true
		}
		return nil
	})
	return found
}

// GoListImportPath returns the import path for the package in workDir under the given build flags.
func GoListImportPath(ctx context.Context, workDir string, buildFlags []string, env ...string) (string, error) {
	// Assemble the go list arguments with validated build flags.
	args := []string{"list"}
	for _, flag := range buildFlags {
		flag = strings.TrimSpace(flag)
		if flag == "" {
			return "", errors.New("go list build flag cannot be empty")
		}
		args = append(args, flag)
	}
	args = append(args, "-f", "{{.ImportPath}}", ".")

	// Run go list in the work dir and extract the import path.
	cmd := exec.CommandContext(ctx, "go", args...)
	cmd.Env = append(os.Environ(), GetDefaultEnv()...)
	cmd.Env = append(cmd.Env, env...)
	cmd.Dir = workDir
	out, err := cmd.CombinedOutput()
	if err != nil {
		var exitErr *exec.ExitError
		if errors.As(err, &exitErr) {
			return "", errors.Errorf("go list import path failed: %s", strings.TrimSpace(string(out)))
		}
		return "", err
	}

	// Return the trimmed import path from the go list output.
	importPath := strings.TrimSpace(string(out))
	if importPath == "" {
		return "", errors.New("go list import path returned empty path")
	}
	return importPath, nil
}

// ExecGoScriptCompile compiles Go packages to a GoScript TypeScript package
// tree in a child process of the current executable, so the gigabytes of
// syntax and type data the compiler holds return to the system when it exits.
// The child receives opts.Env in its environment.
func ExecGoScriptCompile(ctx context.Context, le *logrus.Entry, opts GoScriptCompileOptions) error {
	// Validate the options before waiting for the build budget.
	if _, _, err := newGoScriptConfig(opts); err != nil {
		return err
	}

	// Acquire a compile permit from the build budget.
	budget, err := bldr_buildbudget.Default()
	if err != nil {
		return err
	}
	waitStart := time.Now()
	permit, err := budget.Acquire(ctx, bldr_buildbudget.GoScriptCompileWeight)
	if err != nil {
		return err
	}
	defer permit.Release()

	// Run the compile in a worker process and log the duration.
	timeStart := time.Now()
	if err := runGoScriptWorker(ctx, le, opts); err != nil {
		return err
	}
	le.
		WithField("compiler", "goscript").
		WithField("dur", time.Since(timeStart).String()).
		WithField("budget-wait", timeStart.Sub(waitStart).String()).
		Info("compiled plugin TypeScript package tree")
	return nil
}

// compileGoScript runs the GoScript compiler over the packages of opts in
// this process.
func compileGoScript(ctx context.Context, le *logrus.Entry, opts GoScriptCompileOptions) error {
	// Build the configuration and compile the packages.
	conf, packages, err := newGoScriptConfig(opts)
	if err != nil {
		return err
	}
	comp, err := goscript_compiler.NewCompiler(conf, le, nil)
	if err != nil {
		return err
	}
	_, err = comp.CompilePackages(ctx, packages...)
	return err
}

// newGoScriptConfig validates opts and returns the compiler configuration and
// the packages to compile.
func newGoScriptConfig(opts GoScriptCompileOptions) (*goscript_compiler.Config, []string, error) {
	// Validate the required compile options.
	if strings.TrimSpace(opts.WorkDir) == "" {
		return nil, nil, errors.New("goscript work dir cannot be empty")
	}
	if strings.TrimSpace(opts.OutputPath) == "" {
		return nil, nil, errors.New("goscript output path cannot be empty")
	}
	if len(opts.Packages) == 0 {
		return nil, nil, errors.New("goscript packages cannot be empty")
	}
	for _, env := range opts.Env {
		if strings.TrimSpace(env) == "" {
			return nil, nil, errors.New("goscript env cannot be empty")
		}
	}

	// Build the compiler configuration from the options.
	conf := &goscript_compiler.Config{
		Dir:                       opts.WorkDir,
		DeferredFunctions:         slices.Clone(opts.DeferredFunctions),
		OutputPath:                opts.OutputPath,
		ProtobufTypeScriptBinding: opts.ProtobufTypeScriptBinding,
		EliminateDeadCode:         opts.EliminateDeadCode,
	}
	if opts.CacheRoot != "" {
		cacheRoot := strings.TrimSpace(opts.CacheRoot)
		if cacheRoot == "" {
			return nil, nil, errors.New("goscript cache root cannot be empty")
		}
		conf.CacheRoot = cacheRoot
	}
	packages := make([]string, 0, len(opts.Packages))
	for _, pkg := range opts.Packages {
		pkg = strings.TrimSpace(pkg)
		if pkg == "" {
			return nil, nil, errors.New("goscript package cannot be empty")
		}
		packages = append(packages, pkg)
	}
	for _, flag := range opts.BuildFlags {
		flag = strings.TrimSpace(flag)
		if flag == "" {
			return nil, nil, errors.New("goscript build flag cannot be empty")
		}
		conf.BuildFlags = append(conf.BuildFlags, flag)
	}
	for _, dir := range opts.OverrideDirs {
		dir = strings.TrimSpace(dir)
		if dir == "" {
			return nil, nil, errors.New("goscript override dir cannot be empty")
		}
		conf.OverrideDirs = append(conf.OverrideDirs, dir)
	}
	for _, root := range opts.BindingRoots {
		root = strings.TrimSpace(root)
		if root == "" {
			return nil, nil, errors.New("goscript binding root cannot be empty")
		}
		conf.AdditionalBindingRoots = append(conf.AdditionalBindingRoots, root)
	}
	return conf, packages, nil
}