Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/build/collect.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func (c *context) collectEnvInputs(m *manifestBuilder) {
m.env.Goarch = c.buildConf.Goarch
m.env.LlvmTriple = c.crossCompile.LLVMTarget
m.env.LlgoVersion = env.Version()
m.env.LlgoCompilerHash = env.CompilerHash()
m.env.GoVersion = runtime.Version()
m.env.LlvmVersion = c.getLLVMVersion()

Expand Down
17 changes: 9 additions & 8 deletions internal/build/fingerprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,18 @@ func (m orderedStringMap) MarshalYAML() (interface{}, error) {

// envSection holds fixed environment fields and optional vars.
type envSection struct {
Goos string `yaml:"GOOS,omitempty"`
Goarch string `yaml:"GOARCH,omitempty"`
GoVersion string `yaml:"GO_VERSION,omitempty"`
LlgoVersion string `yaml:"LLGO_VERSION,omitempty"`
LlvmTriple string `yaml:"LLVM_TRIPLE,omitempty"`
LlvmVersion string `yaml:"LLVM_VERSION,omitempty"`
Vars orderedStringMap `yaml:"VARS,omitempty"`
Goos string `yaml:"GOOS,omitempty"`
Goarch string `yaml:"GOARCH,omitempty"`
GoVersion string `yaml:"GO_VERSION,omitempty"`
LlgoVersion string `yaml:"LLGO_VERSION,omitempty"`
LlgoCompilerHash string `yaml:"LLGO_COMPILER_HASH,omitempty"`
LlvmTriple string `yaml:"LLVM_TRIPLE,omitempty"`
LlvmVersion string `yaml:"LLVM_VERSION,omitempty"`
Vars orderedStringMap `yaml:"VARS,omitempty"`
}

func (s *envSection) empty() bool {
return s.Goos == "" && s.Goarch == "" && s.LlvmTriple == "" && s.LlgoVersion == "" && s.GoVersion == "" && s.LlvmVersion == "" && len(s.Vars) == 0
return s.Goos == "" && s.Goarch == "" && s.LlvmTriple == "" && s.LlgoVersion == "" && s.LlgoCompilerHash == "" && s.GoVersion == "" && s.LlvmVersion == "" && len(s.Vars) == 0
}

type commonSection struct {
Expand Down
2 changes: 2 additions & 0 deletions internal/env/utils_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !llgo

package env

import "testing"
Expand Down
80 changes: 79 additions & 1 deletion internal/env/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
package env

import (
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"os"
"runtime/debug"
"strings"
)
Expand All @@ -29,12 +35,40 @@ const (
// set by the linker.
var buildVersion string

// compilerHash caches the LLGo compiler fingerprint. Release builds may set it
// at link time, while development builds compute it from the running binary.
var compilerHash string

// init precomputes the compilerHash for development builds so cache entries can
// observe compiler updates without hashing the full executable contents.
func init() {
if err := ensureCompilerHash(); err != nil {
panic(err)
}
}

func ensureCompilerHash() error {
if Version() != devel || compilerHash != "" {
return nil
}
exe, err := executablePath()
if err != nil {
return fmt.Errorf("llgo: determine executable path: %w", err)
}
hash, err := compilerHashFromPathFunc(exe)
if err != nil {
return fmt.Errorf("llgo: compute compiler hash: %w", err)
}
compilerHash = hash
return nil
}

// Version returns the version of the running LLGo binary.
func Version() string {
if buildVersion != "" {
return buildVersion
}
info, ok := debug.ReadBuildInfo()
info, ok := readBuildInfo()
if ok && info.Main.Version != "" && !strings.HasSuffix(info.Main.Version, "+dirty") {
return info.Main.Version
}
Expand All @@ -44,3 +78,47 @@ func Version() string {
func Devel() bool {
return Version() == devel
}

// CompilerHash returns a fingerprint of the compiler binary. For release
// builds it returns an empty string. For development builds it hashes the
// executable's metadata (modification time + file size) so cache entries are
// invalidated automatically when the compiler changes.
func CompilerHash() string {
return compilerHash
}

// compilerHashFromPath generates a metadata-based fingerprint for the provided
// compiler binary. The hash uses the file's modification time and size rather
// than its full contents to provide fast cache invalidation in development
// workflows.
func compilerHashFromPath(path string) (string, error) {
info, err := os.Stat(path)
if err != nil {
return "", fmt.Errorf("llgo: stat executable: %w", err)
}
return hashMetadata(info.ModTime().UTC().UnixNano(), info.Size())
}

// hashMetadata encodes the file's modification timestamp and size as
// little-endian uint64 values and returns their SHA-256 digest. A negative size
// is rejected because it does not represent a valid file.
func hashMetadata(modTimeNano int64, size int64) (string, error) {
if size < 0 {
return "", fmt.Errorf("llgo: invalid executable size: %d", size)
}
h := sha256.New()
if err := writeUint64LE(h, uint64(modTimeNano)); err != nil {
return "", fmt.Errorf("llgo: hash metadata timestamp: %w", err)
}
if err := writeUint64LE(h, uint64(size)); err != nil {
return "", fmt.Errorf("llgo: hash metadata size: %w", err)
}
return hex.EncodeToString(h.Sum(nil)), nil
}

var (
writeUint64LE = func(w io.Writer, v uint64) error { return binary.Write(w, binary.LittleEndian, v) }
executablePath = os.Executable
compilerHashFromPathFunc = compilerHashFromPath
readBuildInfo = debug.ReadBuildInfo
)
Loading
Loading