Ctrl K
ComunidadGuías…

bldr/dist-sync.go

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

Archivo original ↗Preguntar a KiBot
//go:build !js

package bldr

import (
	"context"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"slices"
	"strings"

	"github.com/aperturerobotics/util/exec"
	"github.com/pkg/errors"
	unixfs_sync "github.com/s4wave/spacewave/db/unixfs/sync"
	"github.com/sirupsen/logrus"
	"golang.org/x/mod/modfile"
)

// DistGoMod is the Go module path used for the checked-out Bldr dist sources.
const DistGoMod = "github.com/s4wave/spacewave/bldr-dist"

// distSyncOwnedPaths are the dist root entries that SyncDistSources writes
// itself rather than copying from the embedded dist sources.
var distSyncOwnedPaths = []string{"vendor", "node_modules", "go.mod", "go.sum", ".sync-hash"}

// DistSourceSyncConfig configures Bldr dist-source materialization.
type DistSourceSyncConfig struct {
	// RepoRoot is the project root containing the live go.mod and go.sum.
	RepoRoot string
	// DistRoot is the output directory used as BLDR_DIST_ROOT.
	DistRoot string
	// BldrVersion is the module version to require when BldrSrcPath is empty.
	BldrVersion string
	// BldrSum is the module checksum for BldrVersion.
	BldrSum string
	// BldrSrcPath is an optional replacement path for the source module.
	BldrSrcPath string
}

// SyncDistSources syncs embedded Bldr dist sources into DistRoot and
// materializes the vendor tree used by non-local @go/* TypeScript imports.
func SyncDistSources(ctx context.Context, le *logrus.Entry, conf DistSourceSyncConfig) error {
	// Reject a configuration missing the repository or dist root.
	if conf.RepoRoot == "" {
		return errors.New("repo root is required")
	}
	if conf.DistRoot == "" {
		return errors.New("dist root is required")
	}

	// Build the FS handle over the embedded dist sources.
	distSourcesHandle := BuildDistSourcesFSHandle(ctx, le)
	defer distSourcesHandle.Release()

	// Sync the embedded sources into the dist root, keeping the owned paths.
	if err := os.MkdirAll(conf.DistRoot, 0o755); err != nil {
		return err
	}
	if err := unixfs_sync.Sync(
		ctx,
		conf.DistRoot,
		distSourcesHandle,
		unixfs_sync.DeleteMode_DeleteMode_DURING,
		unixfs_sync.NewSkipPathPrefixes(distSyncOwnedPaths),
	); err != nil {
		return err
	}

	// Run each go mod step through a command wired to the debug log.
	runGoMod := func(cmd string) error {
		// Build the go mod command and wire its output to the debug log.
		le.Infof("bldr sources: running go mod %s", cmd)
		goVendorCmd := exec.NewCmd(ctx, "go", "mod", cmd)
		goVendorCmd.Dir = conf.DistRoot
		goModWriter := le.WriterLevel(logrus.DebugLevel)
		goVendorCmd.Stderr = goModWriter
		goVendorCmd.Stdout = goModWriter
		goVendorCmd.Env = os.Environ()

		// Run the command and report the run or writer-close error.
		runErr := goVendorCmd.Run()
		closeErr := goModWriter.Close()
		if runErr != nil {
			return runErr
		}
		return closeErr
	}

	// Parse the repository go.mod and retarget its module path for the dist copy.
	distGoModPath := filepath.Join(conf.DistRoot, "go.mod")
	sourceGoModPath := filepath.Join(conf.RepoRoot, "go.mod")
	sourceGoModData, err := os.ReadFile(sourceGoModPath)
	if err != nil {
		return errors.Wrapf(err, "read repo go.mod at %s", sourceGoModPath)
	}
	distModFile, err := modfile.Parse(sourceGoModPath, sourceGoModData, nil)
	if err != nil {
		return err
	}
	sourceModPath := distModFile.Module.Mod.Path
	distModFile.Module.Mod.Path = DistGoMod

	// Resolve relative replace targets against the repository root.
	if err := absolutizeRelativeReplaces(distModFile, conf.RepoRoot); err != nil {
		return err
	}

	// Declare the dist module and pin the source module by path or version.
	if err := distModFile.AddModuleStmt(DistGoMod); err != nil {
		return err
	}
	if conf.BldrSrcPath != "" {
		if err := distModFile.AddReplace(sourceModPath, "", conf.BldrSrcPath, ""); err != nil {
			return err
		}
	} else {
		if conf.BldrVersion == "" {
			return errors.New("bldr version is required when bldr source path is empty")
		}
		if err := distModFile.AddRequire(sourceModPath, conf.BldrVersion); err != nil {
			return err
		}
	}

	// Clean up and format the dist go.mod.
	distModFile.Cleanup()
	updatedDistGoMod, err := distModFile.Format()
	if err != nil {
		return err
	}

	// Read the repository go.sum to copy into the dist checkout.
	sourceGoSumPath := filepath.Join(conf.RepoRoot, "go.sum")
	sourceGoSumData, err := os.ReadFile(sourceGoSumPath)
	if err != nil {
		return errors.Wrapf(err, "read repo go.sum at %s", sourceGoSumPath)
	}

	// Tidy and vendor read the dist go.mod and go.sum and the imports of the
	// synced dist sources. The vendor tree is reused while all three match.
	hashStr, err := distSyncHash(conf.DistRoot, updatedDistGoMod, sourceGoSumData, conf.BldrSum)
	if err != nil {
		return err
	}
	syncHashPath := filepath.Join(conf.DistRoot, ".sync-hash")
	vendorDir := filepath.Join(conf.DistRoot, "vendor")

	// Skip tidy and vendor when the recorded hash and vendor tree are current.
	existingHash, hashReadErr := os.ReadFile(syncHashPath)
	_, vendorStatErr := os.Stat(vendorDir)
	if hashReadErr == nil && strings.TrimSpace(string(existingHash)) == hashStr && vendorStatErr == nil {
		le.Info("bldr sources: inputs unchanged, skipping tidy+vendor")
		le.Info("done checking out bldr sources")
		return nil
	}

	// #nosec G703 -- this fixed filename is written in the caller-owned dist checkout.
	if err := os.WriteFile(distGoModPath, updatedDistGoMod, 0o644); err != nil {
		return err
	}

	// Write the dist go.sum beside the dist go.mod.
	distGoSumPath := filepath.Join(conf.DistRoot, "go.sum")

	// #nosec G703 -- this fixed filename is written in the caller-owned dist checkout.
	if err := os.WriteFile(distGoSumPath, sourceGoSumData, 0o644); err != nil {
		return err
	}

	// Append the Bldr module checksums when vendoring a pinned version.
	if conf.BldrSum != "" {
		goModSum := sha256.Sum256(sourceGoModData)
		goModInner := hex.EncodeToString(goModSum[:]) + "  go.mod\n"
		goModInnerSum := sha256.Sum256([]byte(goModInner))
		goModSumHash := "h1:" + base64.StdEncoding.EncodeToString(goModInnerSum[:])

		goSumFile, err := os.OpenFile(distGoSumPath, os.O_APPEND|os.O_WRONLY, 0o644)
		if err != nil {
			return err
		}
		if _, err = goSumFile.WriteString(sourceModPath + " " + conf.BldrVersion + " " + conf.BldrSum + "\n"); err != nil {
			_ = goSumFile.Close()
			return err
		}
		if _, err = goSumFile.WriteString(sourceModPath + " " + conf.BldrVersion + "/go.mod " + goModSumHash + "\n"); err != nil {
			_ = goSumFile.Close()
			return err
		}
		if err = goSumFile.Close(); err != nil {
			return err
		}

		if err := runGoMod("download"); err != nil {
			return err
		}
	} else {
		if err := runGoMod("tidy"); err != nil {
			return err
		}
	}

	// Vendor the dependencies into the dist checkout.
	if err := runGoMod("vendor"); err != nil {
		return err
	}

	// #nosec G703 -- this fixed filename is written in the caller-owned dist checkout.
	if err := os.WriteFile(syncHashPath, []byte(hashStr), 0o644); err != nil {
		le.WithError(err).Debug("failed to write bldr sources sync hash")
	}
	le.Info("done checking out bldr sources")
	return nil
}

// distSyncHash digests the inputs of tidy and vendor: the dist go.mod, the repo
// go.sum, the Bldr module checksum and every synced dist source file.
func distSyncHash(distRoot string, distGoMod, goSum []byte, bldrSum string) (string, error) {
	// Digest the fixed inputs: the dist go.mod, the repo go.sum and the Bldr sum.
	h := sha256.New()
	writeField := func(name string, data []byte) {
		_, _ = fmt.Fprintf(h, "%s %d\n", name, len(data))
		_, _ = h.Write(data)
	}
	writeField("go.mod", distGoMod)
	writeField("go.sum", goSum)
	writeField("bldr-sum", []byte(bldrSum))

	// Walk the dist tree and hash every regular file outside the owned paths.
	root, err := os.OpenRoot(distRoot)
	if err != nil {
		return "", err
	}
	defer root.Close()
	err = fs.WalkDir(root.FS(), ".", func(p string, d fs.DirEntry, err error) error {
		// Bail out on walk errors and skip the root directory entry.
		if err != nil {
			return err
		}
		if p == "." {
			return nil
		}

		// Skip the sync-owned metadata files at the dist root.
		if !strings.Contains(p, "/") && slices.Contains(distSyncOwnedPaths, p) {
			if d.IsDir() {
				return fs.SkipDir
			}
			return nil
		}

		// Hash each regular file's contents under its path.
		if !d.Type().IsRegular() {
			return nil
		}
		data, err := root.ReadFile(p)
		if err != nil {
			return err
		}
		writeField(p, data)
		return nil
	})
	if err != nil {
		return "", errors.Wrap(err, "hash dist sources")
	}
	return hex.EncodeToString(h.Sum(nil)), nil
}

func absolutizeRelativeReplaces(modFile *modfile.File, repoRoot string) error {
	absRepoRoot, err := filepath.Abs(repoRoot)
	if err != nil {
		return err
	}
	for _, replace := range modFile.Replace {
		if replace.New.Version != "" {
			continue
		}
		replacePath := replace.New.Path
		if filepath.IsAbs(replacePath) {
			continue
		}
		if !strings.HasPrefix(replacePath, ".") {
			continue
		}
		absReplacePath := filepath.Clean(filepath.Join(absRepoRoot, replacePath))
		replace.New.Path = absReplacePath
		if replace.Syntax != nil && len(replace.Syntax.Token) > 0 {
			replace.Syntax.Token[len(replace.Syntax.Token)-1] = absReplacePath
		}
	}
	return nil
}