Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
323 changes: 217 additions & 106 deletions internal/build/build.go

Large diffs are not rendered by default.

62 changes: 44 additions & 18 deletions internal/build/collect.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"os/exec"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"

Expand All @@ -42,7 +41,12 @@ func (c *context) collectFingerprint(pkg *aPackage) error {
c.fingerprinting = make(map[string]bool)
}
if c.fingerprinting[pkg.ID] {
return fmt.Errorf("fingerprint cycle detected for %s", pkg.ID)
// Alternate packages can intentionally close a cycle in the runtime
// replacement graph after all packages have been built into SSA. A
// cycle cannot have a stable per-package cache key, so compile every
// member rather than returning an incorrect cache hit.
c.disablePackageCache(c.fingerprinting)
return nil
}
c.fingerprinting[pkg.ID] = true
defer delete(c.fingerprinting, pkg.ID)
Expand Down Expand Up @@ -70,6 +74,20 @@ func (c *context) collectFingerprint(pkg *aPackage) error {
return nil
}

func (c *context) disablePackageCache(pkgs map[string]bool) {
if c.cacheDisabled == nil {
c.cacheDisabled = make(map[string]none, len(pkgs))
}
for id := range pkgs {
c.cacheDisabled[id] = none{}
}
}

func (c *context) packageCacheDisabled(id string) bool {
_, disabled := c.cacheDisabled[id]
return disabled
}

// collectEnvInputs collects environment-related inputs.
func (c *context) collectEnvInputs(m *manifestBuilder) {
m.env.Goos = c.buildConf.Goos
Expand Down Expand Up @@ -196,21 +214,7 @@ func (c *context) collectPackageInputs(m *manifestBuilder, pkg *aPackage) error

// collectDependencyInputs adds dependency fingerprints/versions into manifest.
func (c *context) collectDependencyInputs(m *manifestBuilder, pkg *aPackage) error {
if len(pkg.Imports) == 0 {
return nil
}

deps := make([]*packages.Package, 0, len(pkg.Imports))
for _, dep := range pkg.Imports {
if dep == nil || dep.ID == pkg.ID {
continue
}
deps = append(deps, dep)
}

sort.Slice(deps, func(i, j int) bool { return deps[i].ID < deps[j].ID })

for _, dep := range deps {
for _, dep := range effectiveDependencies(pkg) {
depEntry, err := c.dependencyFingerprint(dep)
if err != nil {
return err
Expand All @@ -221,6 +225,21 @@ func (c *context) collectDependencyInputs(m *manifestBuilder, pkg *aPackage) err
return nil
}

// initializePackageBuildState initializes the mutable state used by the
// package pipeline before any scheduler is introduced. This makes ownership
// explicit and avoids lazy first-use writes becoming data races later.
func (c *context) initializePackageBuildState() {
if c.sfilesCache == nil {
c.sfilesCache = make(map[string][]string)
}
if !cacheEnabled() {
return
}
c.cacheManager = newCacheManager()
c.llvmVersion = detectLLVMVersion(c)
c.llvmVersionReady = true
}

func (c *context) dependencyFingerprint(dep *packages.Package) (depEntry, error) {
entry := depEntry{ID: dep.ID}
if v := moduleVersion(dep.Module); v != "" {
Expand Down Expand Up @@ -264,10 +283,11 @@ func moduleVersion(mod *gopackages.Module) string {

// getLLVMVersion returns the cached LLVM version or detects it.
func (c *context) getLLVMVersion() string {
if c.llvmVersion != "" {
if c.llvmVersionReady {
return c.llvmVersion
}
c.llvmVersion = detectLLVMVersion(c)
c.llvmVersionReady = true
return c.llvmVersion
}

Expand Down Expand Up @@ -326,6 +346,9 @@ func (c *context) tryLoadFromCache(pkg *aPackage) bool {
if !cacheEnabled() {
return false
}
if c.packageCacheDisabled(pkg.ID) {
return false
}

// Main packages are intentionally not written to the build cache because
// each executable's entry module is linked against the current main archive.
Expand Down Expand Up @@ -451,6 +474,9 @@ func (c *context) saveToCache(pkg *aPackage) error {
if !cacheEnabled() {
return nil
}
if c.packageCacheDisabled(pkg.ID) {
return nil
}

if pkg.Fingerprint == "" || pkg.Manifest == "" {
return nil
Expand Down
40 changes: 40 additions & 0 deletions internal/build/collect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,46 @@ func TestCollectFingerprint(t *testing.T) {
}
}

func TestCollectFingerprintAltDependencyCycleDisablesCache(t *testing.T) {
runtimePkg := &packages.Package{ID: "runtime", PkgPath: "runtime"}
osPkg := &packages.Package{ID: "runtime/internal/clite/os", PkgPath: "runtime/internal/clite/os"}
syscallPkg := &packages.Package{ID: "runtime/internal/clite/syscall", PkgPath: "runtime/internal/clite/syscall"}
runtime := &aPackage{
Package: runtimePkg,
AltPkg: &packages.Cached{Package: &packages.Package{
ID: "runtime/alt",
Imports: map[string]*packages.Package{"os": osPkg},
}},
}
os := &aPackage{Package: osPkg}
syscall := &aPackage{Package: syscallPkg}
os.Imports = map[string]*packages.Package{"syscall": syscallPkg}
syscall.Imports = map[string]*packages.Package{"runtime": runtimePkg}
ctx := &context{
conf: &packages.Config{},
buildConf: &Config{Goos: "js", Goarch: "wasm"},
crossCompile: crosscompile.Export{
LLVMTarget: "wasm32-unknown-unknown",
},
pkgByID: map[string]Package{
runtimePkg.ID: runtime,
osPkg.ID: os,
syscallPkg.ID: syscall,
},
}
if err := ctx.collectFingerprint(runtime); err != nil {
t.Fatalf("collectFingerprint: %v", err)
}
for _, pkg := range []*aPackage{runtime, os, syscall} {
if !ctx.packageCacheDisabled(pkg.ID) {
t.Fatalf("cache for %s was not disabled after fingerprint cycle", pkg.ID)
}
if pkg.Fingerprint == "" || pkg.Manifest == "" {
t.Fatalf("fingerprint state for %s was not completed", pkg.ID)
}
}
}

func TestCollectFingerprintDeterminism(t *testing.T) {
td := t.TempDir()

Expand Down
52 changes: 52 additions & 0 deletions internal/build/dependencies.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package build

import (
"sort"

"github.com/goplus/llgo/internal/packages"
)

// effectiveDependencies returns the package graph that contributes code to an
// aPackage. An alternate package can add imports beyond the original package,
// so using only Package.Imports would allow stale cache entries after an alt
// dependency changes.
func effectiveDependencies(pkg *aPackage) []*packages.Package {
if pkg == nil || pkg.Package == nil {
return nil
}
deps := make(map[string]*packages.Package)
add := func(imports map[string]*packages.Package) {
for _, dep := range imports {
if dep == nil || dep.ID == pkg.ID || (pkg.AltPkg != nil && dep.ID == pkg.AltPkg.ID) {
continue
}
deps[dep.ID] = dep
}
}
add(pkg.Imports)
if pkg.AltPkg != nil {
add(pkg.AltPkg.Imports)
}
ret := make([]*packages.Package, 0, len(deps))
for _, dep := range deps {
ret = append(ret, dep)
}
sort.Slice(ret, func(i, j int) bool { return ret[i].ID < ret[j].ID })
return ret
}
46 changes: 46 additions & 0 deletions internal/build/dependencies_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package build

import (
"reflect"
"testing"

"github.com/goplus/llgo/internal/packages"
)

func TestEffectiveDependenciesIncludesAlternateImports(t *testing.T) {
base := &packages.Package{ID: "base"}
shared := &packages.Package{ID: "shared"}
altOnly := &packages.Package{ID: "alt-only"}
alt := &packages.Package{
ID: "alt",
Imports: map[string]*packages.Package{"shared": shared, "alt-only": altOnly},
}
pkg := &aPackage{
Package: &packages.Package{ID: "pkg", Imports: map[string]*packages.Package{"base": base, "shared": shared}},
AltPkg: &packages.Cached{Package: alt},
}
deps := effectiveDependencies(pkg)
got := make([]string, len(deps))
for i, dep := range deps {
got[i] = dep.ID
}
if want := []string{"alt-only", "base", "shared"}; !reflect.DeepEqual(got, want) {
t.Fatalf("effectiveDependencies = %v, want %v", got, want)
}
}
7 changes: 3 additions & 4 deletions internal/build/fingerprint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,9 @@ func TestManifestBuilder_EmptySections(t *testing.T) {
m := newManifestBuilder()
content := m.Build()

// Empty sections should not be written
expected := ``
if content != expected {
t.Errorf("unexpected empty manifest:\ngot:\n%s\nwant:\n%s", content, expected)
// Empty sections should not be written.
if content != "" {
t.Errorf("unexpected empty manifest:\ngot:\n%s", content)
}

// Should still produce a valid fingerprint
Expand Down
1 change: 1 addition & 0 deletions internal/build/module_hook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

func TestModuleHookReceivesMainPackageModule(t *testing.T) {
conf := NewDefaultConf(ModeGen)
conf.Parallel = 2

counts := make(map[string]int)
snapshots := make(map[string]string)
Expand Down
77 changes: 77 additions & 0 deletions internal/build/package_build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package build

import "github.com/goplus/llgo/cl"

// packageBuildSpec is the immutable scheduler input for one package. It keeps
// package classification out of the execution loop, so a later DAG scheduler
// can choose work without reinterpreting frontend metadata.
type packageBuildSpec struct {
pkg *aPackage
kind int
kindParam string
runtime bool
}

func newPackageBuildSpec(pkg *aPackage) packageBuildSpec {
kind, kindParam := cl.PkgKindOf(pkg.Types)
return packageBuildSpec{
pkg: pkg,
kind: kind,
kindParam: kindParam,
runtime: isRuntimePkg(pkg.PkgPath),
}
}

func (s packageBuildSpec) isDeclOnly() bool {
return s.kind == cl.PkgDeclOnly
}

func (s packageBuildSpec) isLinkOnly() bool {
return s.kind == cl.PkgLinkIR || s.kind == cl.PkgLinkExtern || s.kind == cl.PkgPyModule
}

func (s packageBuildSpec) hasSource() bool {
return len(s.pkg.GoFiles) > 0
}

func (s packageBuildSpec) needsRuntimeSignals() bool {
return !s.isLinkOnly() && !s.isDeclOnly()
}

// packageBuildResult carries the observable output of a serial package build.
// Subsequent scheduler PRs can pass this value between worker and finalization
// stages without exposing the mutable aPackage implementation details.
type packageBuildResult struct {
spec packageBuildSpec
cacheHit bool
archiveFile string
needRuntime bool
needPyInit bool
}

func packageBuildResultFor(spec packageBuildSpec) packageBuildResult {
pkg := spec.pkg
return packageBuildResult{
spec: spec,
cacheHit: pkg.CacheHit,
archiveFile: pkg.ArchiveFile,
needRuntime: pkg.NeedRt,
needPyInit: pkg.NeedPyInit,
}
}
Loading
Loading