From 55f65155f17a168980fa838ca061400df2459f18 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Thu, 30 Jul 2026 08:09:16 +0800 Subject: [PATCH 1/2] build: snapshot package linker state --- internal/build/build.go | 99 ++++++----- internal/build/collect.go | 19 +- internal/build/collect_test.go | 86 +++++++++- internal/build/fingerprint.go | 7 +- internal/build/funcinfo_table.go | 37 ++-- internal/build/package_build_test.go | 26 ++- internal/build/package_summary.go | 229 +++++++++++++++++++++++++ internal/build/package_summary_test.go | 93 ++++++++++ 8 files changed, 529 insertions(+), 67 deletions(-) create mode 100644 internal/build/package_summary.go create mode 100644 internal/build/package_summary_test.go diff --git a/internal/build/build.go b/internal/build/build.go index db46d50186..3e3ce4a6f6 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -1042,7 +1042,8 @@ func prePackageBuild(ctx *context, task *packageBuildTask, verbose bool) error { if task.isDeclOnly() { pkg.ExportFile = "" task.skip = true - return nil + aPkg.Summary = summarizePackage(aPkg) + return ctx.collectFingerprint(aPkg) } if task.isLinkOnly() && !task.hasSource() { pkg.ExportFile = "" @@ -1050,7 +1051,8 @@ func prePackageBuild(ctx *context, task *packageBuildTask, verbose bool) error { appendExternalLinkArgs(ctx, aPkg, task.kindParam) } task.skip = true - return nil + aPkg.Summary = summarizePackage(aPkg) + return ctx.collectFingerprint(aPkg) } if err := ctx.collectFingerprint(aPkg); err != nil { return err @@ -1091,6 +1093,7 @@ func finalizePackageBuild(ctx *context, task *packageBuildTask, verbose bool) (p if task.kind == cl.PkgLinkExtern { appendExternalLinkArgs(ctx, aPkg, task.kindParam) } + aPkg.Summary = summarizePackage(aPkg) if err := ctx.saveToCache(aPkg); err != nil && verbose { fmt.Fprintf(os.Stderr, "warning: failed to save cache for %s: %v\n", aPkg.PkgPath, err) } @@ -1341,39 +1344,47 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa aPkg = ctx.pkgByID[p.ID] } if p.ExportFile != "" && aPkg != nil { // skip packages that only contain declarations + if aPkg.Summary == nil && isRuntimePkg(aPkg.PkgPath) { + return + } linkedPkgs[p.ID] = true linkedOrder = append(linkedOrder, aPkg) } }) + linkedSummaries := make([]*PackageSummary, len(linkedOrder)) + for i, aPkg := range linkedOrder { + if aPkg.Summary == nil { + return fmt.Errorf("package %s has no linker summary", aPkg.PkgPath) + } + linkedSummaries[i] = aPkg.Summary + } // packages.Visit with a post callback yields dependencies before importers. // Reverse that order so static archives are linked after the objects that use them. for i := len(linkedOrder) - 1; i >= 0; i-- { - aPkg := linkedOrder[i] - p := aPkg.Package + summary := linkedSummaries[i] // Defer linking runtime packages unless we actually need the runtime. - if isRuntimePkg(p.PkgPath) { - rtLinkArgs = append(rtLinkArgs, aPkg.LinkArgs...) - if aPkg.ArchiveFile != "" { - rtLinkInputs = append(rtLinkInputs, aPkg.ArchiveFile) + if isRuntimePkg(summary.PkgPath) { + rtLinkArgs = append(rtLinkArgs, summary.LinkArgs...) + if summary.ArchiveFile != "" { + rtLinkInputs = append(rtLinkInputs, summary.ArchiveFile) } continue } // Only let non-runtime packages influence whether runtime is needed. - need1, need2 := aPkg.isNeedRuntimeOrPyInit() - needRuntime = needRuntime || need1 - needPyInit = needPyInit || need2 - needAbiInit |= aPkg.LPkg.NeedAbiInit - for k, _ := range aPkg.LPkg.MethodByIndex { - methodByIndex[k] = none{} + needRuntime = needRuntime || summary.NeedRuntime + needPyInit = needPyInit || summary.NeedPyInit + needAbiInit |= summary.NeedAbiInit + for _, method := range summary.MethodByIndex { + methodByIndex[method] = none{} } - for k, _ := range aPkg.LPkg.MethodByName { - methodByName[k] = none{} + for _, method := range summary.MethodByName { + methodByName[method] = none{} } - linkArgs = append(linkArgs, aPkg.LinkArgs...) - if aPkg.ArchiveFile != "" { - archiveInputs = append(archiveInputs, aPkg.ArchiveFile) + linkArgs = append(linkArgs, summary.LinkArgs...) + if summary.ArchiveFile != "" { + archiveInputs = append(archiveInputs, summary.ArchiveFile) } } @@ -1390,9 +1401,9 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa var pcLineInfo []pcLineRecord var funcInfoStubs []funcInfoStubRecord if ctx.buildConf.PCLNMode != PCLNNone { - funcInfo = prepareFuncInfoTableRecords(collectFuncInfo(linkedOrder), nil) - pcLineInfo = collectPCLineInfo(linkedOrder) - funcInfoStubs = collectFuncInfoStubRecords(linkedOrder, funcInfo) + funcInfo = prepareFuncInfoTableRecords(collectFuncInfoSummaries(linkedSummaries), nil) + pcLineInfo = collectPCLineInfoSummaries(linkedSummaries) + funcInfoStubs = collectFuncInfoStubRecordsSummaries(linkedSummaries, funcInfo) } entryPkg := genMainModule(ctx, llssa.PkgRuntime, pkg, &genConfig{ rtInit: needRuntime, @@ -1400,7 +1411,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa abiInit: needAbiInit, methodByIndex: methodByIndex, methodByName: methodByName, - abiSymbols: linkedModuleGlobals(linkedOrder), + abiSymbols: linkedPackageGlobals(linkedSummaries), funcInfo: funcInfo, pcLineInfo: pcLineInfo, funcInfoStubs: funcInfoStubs, @@ -1440,7 +1451,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa } } } - linkArgs = append(linkArgs, cSharedExportArgs(ctx, linkedOrder)...) + linkArgs = append(linkArgs, cSharedExportArgsSummaries(ctx, linkedSummaries)...) err = linkObjFiles(ctx, outputPath, linkInputs, linkArgs, verbose) if err != nil { @@ -1498,19 +1509,22 @@ func dceEntryRootCandidates(pkgs []Package, needRuntime bool) []string { } func linkedModuleGlobals(pkgs []Package) map[string]none { - if len(pkgs) == 0 { + return linkedPackageGlobals(summariesForPackages(pkgs)) +} + +func linkedPackageGlobals(summaries []*PackageSummary) map[string]none { + if len(summaries) == 0 { return nil } seen := make(map[string]none) - for _, pkg := range pkgs { - if pkg == nil || pkg.LPkg == nil { + for _, summary := range summaries { + if summary == nil { continue } - for g := pkg.LPkg.Module().FirstGlobal(); !g.IsNil(); g = gllvm.NextGlobal(g) { - if g.IsDeclaration() { - continue + for _, name := range summary.GlobalSymbols { + if name != "" { + seen[name] = none{} } - seen[g.Name()] = none{} } } return seen @@ -1589,22 +1603,26 @@ func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose // shared-library link roots. They live in package archives and otherwise remain // unreferenced, so the linker can omit both their object files and symbols. func cSharedExportArgs(ctx *context, pkgs []*aPackage) []string { + return cSharedExportArgsSummaries(ctx, summariesForPackages(pkgs)) +} + +func cSharedExportArgsSummaries(ctx *context, summaries []*PackageSummary) []string { if ctx == nil || ctx.buildConf == nil || ctx.buildConf.BuildMode != BuildModeCShared { return nil } exports := make(map[string]none) - for _, pkg := range pkgs { - if pkg == nil || pkg.LPkg == nil { + for _, summary := range summaries { + if summary == nil { continue } - for _, name := range pkg.LPkg.ExportFuncs() { + for _, name := range summary.CSharedExports { if name != "" { exports[name] = none{} } } - if ctx.mode == ModeTest && pkg.Package != nil && pkg.Name == "main" && strings.HasSuffix(pkg.PkgPath, ".test") { - exports[pkg.PkgPath+".init"] = none{} - exports[pkg.PkgPath+".main"] = none{} + if ctx.mode == ModeTest && summary.Name == "main" && strings.HasSuffix(summary.PkgPath, ".test") { + exports[summary.PkgPath+".init"] = none{} + exports[summary.PkgPath+".main"] = none{} } } names := make([]string, 0, len(exports)) @@ -2238,9 +2256,10 @@ func registerAltSSAPkgs(prog *ssa.Program, patches cl.Patches, alts []*packages. type aPackage struct { *packages.Package - SSA *ssa.Package - AltPkg *packages.Cached - LPkg llssa.Package + SSA *ssa.Package + AltPkg *packages.Cached + LPkg llssa.Package + Summary *PackageSummary NeedRt bool NeedPyInit bool diff --git a/internal/build/collect.go b/internal/build/collect.go index d8dcee7b1f..7d3c65166b 100644 --- a/internal/build/collect.go +++ b/internal/build/collect.go @@ -334,6 +334,9 @@ func (c *context) tryLoadFromCache(pkg *aPackage) bool { if c.packageCacheDisabled(pkg.ID) { return false } + if c.buildConf != nil && (c.buildConf.BuildMode == BuildModeCArchive || c.buildConf.BuildMode == BuildModeCShared) { + 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. @@ -377,6 +380,9 @@ func (c *context) tryLoadFromCache(pkg *aPackage) bool { if err != nil { return false } + if meta.Summary == nil { + return false + } // Use the .a archive directly for linking (no extraction needed) pkg.ArchiveFile = paths.Archive @@ -385,6 +391,7 @@ func (c *context) tryLoadFromCache(pkg *aPackage) bool { pkg.NeedPyInit = meta.NeedPyInit pkg.Meta = pkgMeta pkg.CacheHit = true + pkg.Summary = summaryFromMetadata(pkg, meta) return true } @@ -399,6 +406,7 @@ func parseManifestMetadata(content string) (*cacheArchiveMetadata, error) { meta.LinkArgs = append([]string(nil), data.Metadata.LinkArgs...) meta.NeedRt = data.Metadata.NeedRt meta.NeedPyInit = data.Metadata.NeedPyInit + meta.Summary = data.Metadata.Summary } return meta, nil } @@ -452,6 +460,7 @@ type cacheArchiveMetadata struct { LinkArgs []string NeedRt bool NeedPyInit bool + Summary *packageSummaryMetadata } // saveToCache saves a built package to cache. @@ -511,16 +520,16 @@ func (c *context) saveToCache(pkg *aPackage) error { return fmt.Errorf("decode manifest: %w", err) } + if pkg.Summary == nil { + pkg.Summary = summarizePackage(pkg) + } meta := &manifestMetadata{ LinkArgs: append([]string(nil), pkg.LinkArgs...), NeedRt: pkg.NeedRt, NeedPyInit: pkg.NeedPyInit, + Summary: pkg.Summary.metadata(), } - if len(meta.LinkArgs) == 0 && !meta.NeedRt && !meta.NeedPyInit { - data.Metadata = nil - } else { - data.Metadata = meta - } + data.Metadata = meta manifestWithMeta, err := buildManifestYAML(data) if err != nil { diff --git a/internal/build/collect_test.go b/internal/build/collect_test.go index cc9785518f..2abc1e3c13 100644 --- a/internal/build/collect_test.go +++ b/internal/build/collect_test.go @@ -22,6 +22,7 @@ import ( "os" "os/exec" "path/filepath" + "reflect" "runtime" "strings" "testing" @@ -1060,8 +1061,8 @@ func TestSaveToCache_Success(t *testing.T) { if data.Env.Goos != "darwin" { t.Errorf("manifest should contain original env content") } - if data.Metadata != nil { - t.Errorf("metadata should be empty when no link args/runtime flags") + if data.Metadata == nil || data.Metadata.Summary == nil { + t.Errorf("metadata should preserve an empty linker summary") } // Check archive exists @@ -1235,6 +1236,7 @@ func TestTryLoadFromCacheIgnoresMetaWhenPackageMetaDisabled(t *testing.T) { m := newManifestBuilder() m.env.Goos = "darwin" m.pkg.PkgPath = "example.com/nometa" + m.meta = &manifestMetadata{Summary: &packageSummaryMetadata{}} if err := writeManifest(paths.Manifest, m.Build()); err != nil { t.Fatal(err) } @@ -1250,6 +1252,86 @@ func TestTryLoadFromCacheIgnoresMetaWhenPackageMetaDisabled(t *testing.T) { } } +func TestTryLoadFromCacheRestoresPackageSummary(t *testing.T) { + td := t.TempDir() + oldFunc := cacheRootFunc + cacheRootFunc = func() string { return td } + defer func() { cacheRootFunc = oldFunc }() + + ctx := &context{ + conf: &packages.Config{}, + buildConf: &Config{Goos: "darwin", Goarch: "arm64"}, + crossCompile: crosscompile.Export{ + LLVMTarget: "arm64-apple-darwin", + }, + } + pkg := &aPackage{ + Package: &packages.Package{ + ID: "example.com/cached", + PkgPath: "example.com/cached", + Name: "cached", + }, + Fingerprint: "summary-test", + Manifest: func() string { + m := newManifestBuilder() + m.env.Goos = "darwin" + m.pkg.PkgPath = "example.com/cached" + return m.Build() + }(), + NeedRt: true, + NeedPyInit: true, + LinkArgs: []string{"-lcached"}, + Summary: &PackageSummary{ + ID: "example.com/cached", + PkgPath: "example.com/cached", + Name: "cached", + LinkArgs: []string{"-lcached"}, + NeedRuntime: true, + NeedPyInit: true, + NeedAbiInit: 3, + MethodByIndex: []int{1}, + MethodByName: []string{"Method"}, + GlobalSymbols: []string{"example.com/cached.global"}, + FuncInfo: []funcInfoRecord{{symbol: "example.com/cached.fn", name: "Fn", file: "p.go", line: 7}}, + PCLineInfo: []pcLineRecord{{id: 9, symbol: "example.com/cached.fn", file: "p.go", line: 8}}, + FuncInfoStubs: []string{closureStubPrefix + "example.com/cached.fn"}, + CSharedExports: []string{"Cached"}, + }, + } + want := *pkg.Summary + obj, err := os.CreateTemp(td, "cached-*.o") + if err != nil { + t.Fatal(err) + } + if _, err := obj.WriteString("object"); err != nil { + t.Fatal(err) + } + if err := obj.Close(); err != nil { + t.Fatal(err) + } + pkg.ObjFiles = []string{obj.Name()} + if err := ctx.saveToCache(pkg); err != nil { + t.Fatalf("saveToCache: %v", err) + } + + pkg.ObjFiles = nil + pkg.ArchiveFile = "" + pkg.LinkArgs = nil + pkg.NeedRt = false + pkg.NeedPyInit = false + pkg.Summary = nil + if !ctx.tryLoadFromCache(pkg) { + t.Fatal("tryLoadFromCache = false, want summary cache hit") + } + if pkg.Summary == nil { + t.Fatal("cache hit did not restore package summary") + } + want.ArchiveFile = pkg.ArchiveFile + if !reflect.DeepEqual(pkg.Summary, &want) { + t.Fatalf("restored summary = %#v, want %#v", pkg.Summary, &want) + } +} + func TestGetLLVMVersion(t *testing.T) { ctx := &context{ crossCompile: crosscompile.Export{}, diff --git a/internal/build/fingerprint.go b/internal/build/fingerprint.go index ecc55298ab..781ec180f6 100644 --- a/internal/build/fingerprint.go +++ b/internal/build/fingerprint.go @@ -37,9 +37,10 @@ type depEntry struct { // manifestMetadata stores metadata produced during build but not part of the fingerprint. type manifestMetadata struct { - LinkArgs []string `yaml:"link_args,omitempty"` - NeedRt bool `yaml:"need_rt,omitempty"` - NeedPyInit bool `yaml:"need_py_init,omitempty"` + LinkArgs []string `yaml:"link_args,omitempty"` + NeedRt bool `yaml:"need_rt,omitempty"` + NeedPyInit bool `yaml:"need_py_init,omitempty"` + Summary *packageSummaryMetadata `yaml:"summary,omitempty"` } // manifestData is the structured representation of manifest content. diff --git a/internal/build/funcinfo_table.go b/internal/build/funcinfo_table.go index b36aa29654..f86fd972a1 100644 --- a/internal/build/funcinfo_table.go +++ b/internal/build/funcinfo_table.go @@ -86,12 +86,16 @@ type funcInfoSymbolIndexRecord struct { } func collectFuncInfo(pkgs []Package) []funcInfoRecord { + return collectFuncInfoSummaries(summariesForPackages(pkgs)) +} + +func collectFuncInfoSummaries(summaries []*PackageSummary) []funcInfoRecord { seen := make(map[string]funcInfoRecord) - for _, pkg := range pkgs { - if pkg == nil || pkg.LPkg == nil { + for _, summary := range summaries { + if summary == nil { continue } - for _, rec := range readFuncInfo(pkg.LPkg.Module()) { + for _, rec := range summary.FuncInfo { if rec.symbol == "" { continue } @@ -114,13 +118,17 @@ func collectFuncInfo(pkgs []Package) []funcInfoRecord { } func collectPCLineInfo(pkgs []Package) []pcLineRecord { + return collectPCLineInfoSummaries(summariesForPackages(pkgs)) +} + +func collectPCLineInfoSummaries(summaries []*PackageSummary) []pcLineRecord { var out []pcLineRecord seen := make(map[uint64]none) - for _, pkg := range pkgs { - if pkg == nil || pkg.LPkg == nil { + for _, summary := range summaries { + if summary == nil { continue } - for _, rec := range readPCLineInfo(pkg.LPkg.Module()) { + for _, rec := range summary.PCLineInfo { if rec.id == 0 || rec.symbol == "" { continue } @@ -144,6 +152,10 @@ func collectPCLineInfo(pkgs []Package) []pcLineRecord { } func collectFuncInfoStubRecords(pkgs []Package, records []funcInfoRecord) []funcInfoStubRecord { + return collectFuncInfoStubRecordsSummaries(summariesForPackages(pkgs), records) +} + +func collectFuncInfoStubRecordsSummaries(summaries []*PackageSummary, records []funcInfoRecord) []funcInfoStubRecord { if len(records) == 0 { return nil } @@ -154,23 +166,16 @@ func collectFuncInfoStubRecords(pkgs []Package, records []funcInfoRecord) []func } } seen := make(map[string]funcInfoStubRecord) - for _, pkg := range pkgs { - if pkg == nil || pkg.LPkg == nil { + for _, summary := range summaries { + if summary == nil { continue } - fn := pkg.LPkg.Module().FirstFunction() - for !fn.IsNil() { - if fn.IsDeclaration() || fn.BasicBlocksCount() == 0 { - fn = llvm.NextFunction(fn) - continue - } - name := fn.Name() + for _, name := range summary.FuncInfoStubs { if target, ok := strings.CutPrefix(name, closureStubPrefix); ok { if idx := recordBySymbol[target]; idx != 0 { seen[name] = funcInfoStubRecord{symbol: name, funcIndex: idx} } } - fn = llvm.NextFunction(fn) } } if len(seen) == 0 { diff --git a/internal/build/package_build_test.go b/internal/build/package_build_test.go index 235e77e7cc..9ee12a983a 100644 --- a/internal/build/package_build_test.go +++ b/internal/build/package_build_test.go @@ -210,7 +210,11 @@ func TestPrePackageBuildSkipsDeclarationOnlyPackage(t *testing.T) { Types: types.Unsafe, ExportFile: "stale.a", }} - ctx := &context{built: make(map[string]none)} + ctx := &context{ + conf: &packages.Config{}, + buildConf: &Config{Goos: "linux", Goarch: "amd64", ForceRebuild: true}, + built: make(map[string]none), + } task := newPackageBuildTask(pkg) err := prePackageBuild(ctx, task, false) @@ -275,3 +279,23 @@ func TestBuildSSAPkgsEmptyAndNilEntries(t *testing.T) { pkg := prog.CreatePackage(types.NewPackage("example.com/ssa", "ssa"), nil, nil, true) buildSSAPkgs(ctx, []ssaBuildEntry{{pkg: pkg}, {pkg: pkg}}) } + +func TestPreflightFingerprintsSkippedPackage(t *testing.T) { + pkg := &aPackage{Package: &packages.Package{ + ID: "unsafe", + PkgPath: "unsafe", + Types: types.Unsafe, + }} + ctx := &context{ + conf: &packages.Config{}, + buildConf: &Config{Goos: "linux", Goarch: "amd64", ForceRebuild: true}, + built: make(map[string]none), + } + skip, err := preflightPackageBuild(ctx, newPackageBuildSpec(pkg), false) + if err != nil { + t.Fatal(err) + } + if !skip || pkg.Fingerprint == "" || pkg.Manifest == "" || pkg.Summary == nil { + t.Fatalf("skipped package was not fully prepared: skip=%v fingerprint=%q manifest=%q summary=%#v", skip, pkg.Fingerprint, pkg.Manifest, pkg.Summary) + } +} diff --git a/internal/build/package_summary.go b/internal/build/package_summary.go new file mode 100644 index 0000000000..76c1becca6 --- /dev/null +++ b/internal/build/package_summary.go @@ -0,0 +1,229 @@ +/* + * 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" + "strings" + + "github.com/xgo-dev/llvm" +) + +// PackageSummary is the immutable, LLVM-free output one package contributes +// to the final link. A backend can emit its object, capture this summary, and +// release its Program and LLVM context before whole-program linking starts. +// +// C archive/shared header declarations still consume LPkg and therefore stay +// on the serial compatibility path. +type PackageSummary struct { + ID string + PkgPath string + Name string + + LinkArgs []string + ArchiveFile string + NeedRuntime bool + NeedPyInit bool + + NeedAbiInit int + MethodByIndex []int + MethodByName []string + GlobalSymbols []string + + FuncInfo []funcInfoRecord + PCLineInfo []pcLineRecord + FuncInfoStubs []string + CSharedExports []string +} + +type packageSummaryMetadata struct { + NeedAbiInit int `yaml:"need_abi_init,omitempty"` + MethodByIndex []int `yaml:"method_by_index,omitempty"` + MethodByName []string `yaml:"method_by_name,omitempty"` + GlobalSymbols []string `yaml:"global_symbols,omitempty"` + + FuncInfo []funcInfoMetadata `yaml:"func_info,omitempty"` + PCLineInfo []pcLineMetadata `yaml:"pcline_info,omitempty"` + FuncInfoStubs []string `yaml:"func_info_stubs,omitempty"` + CSharedExports []string `yaml:"c_shared_exports,omitempty"` +} + +type funcInfoMetadata struct { + Symbol string `yaml:"symbol"` + Name string `yaml:"name,omitempty"` + File string `yaml:"file,omitempty"` + Line uint32 `yaml:"line,omitempty"` + Column uint32 `yaml:"column,omitempty"` +} + +type pcLineMetadata struct { + ID uint64 `yaml:"id"` + Symbol string `yaml:"symbol"` + File string `yaml:"file,omitempty"` + Line uint32 `yaml:"line,omitempty"` + Column uint32 `yaml:"column,omitempty"` +} + +func summarizePackage(pkg *aPackage) *PackageSummary { + if pkg == nil { + return nil + } + summary := &PackageSummary{ + LinkArgs: append([]string(nil), pkg.LinkArgs...), + ArchiveFile: pkg.ArchiveFile, + NeedRuntime: pkg.NeedRt, + NeedPyInit: pkg.NeedPyInit, + } + if pkg.Package != nil { + summary.ID = pkg.ID + summary.PkgPath = pkg.PkgPath + summary.Name = pkg.Name + } + if pkg.LPkg == nil { + return summary + } + + lpkg := pkg.LPkg + if summary.PkgPath == "" { + summary.PkgPath = lpkg.Path() + } + summary.NeedAbiInit = lpkg.NeedAbiInit + for method := range lpkg.MethodByIndex { + summary.MethodByIndex = append(summary.MethodByIndex, method) + } + sort.Ints(summary.MethodByIndex) + for method := range lpkg.MethodByName { + summary.MethodByName = append(summary.MethodByName, method) + } + sort.Strings(summary.MethodByName) + + mod := lpkg.Module() + for global := mod.FirstGlobal(); !global.IsNil(); global = llvm.NextGlobal(global) { + if !global.IsDeclaration() { + summary.GlobalSymbols = append(summary.GlobalSymbols, global.Name()) + } + } + sort.Strings(summary.GlobalSymbols) + summary.FuncInfo = readFuncInfo(mod) + summary.PCLineInfo = readPCLineInfo(mod) + for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { + if fn.IsDeclaration() || fn.BasicBlocksCount() == 0 { + continue + } + if _, ok := strings.CutPrefix(fn.Name(), closureStubPrefix); ok { + summary.FuncInfoStubs = append(summary.FuncInfoStubs, fn.Name()) + } + } + sort.Strings(summary.FuncInfoStubs) + for _, name := range lpkg.ExportFuncs() { + if name != "" { + summary.CSharedExports = append(summary.CSharedExports, name) + } + } + sort.Strings(summary.CSharedExports) + return summary +} + +func (s *PackageSummary) metadata() *packageSummaryMetadata { + if s == nil { + return nil + } + meta := &packageSummaryMetadata{ + NeedAbiInit: s.NeedAbiInit, + MethodByIndex: append([]int(nil), s.MethodByIndex...), + MethodByName: append([]string(nil), s.MethodByName...), + GlobalSymbols: append([]string(nil), s.GlobalSymbols...), + FuncInfoStubs: append([]string(nil), s.FuncInfoStubs...), + CSharedExports: append([]string(nil), s.CSharedExports...), + } + for _, rec := range s.FuncInfo { + meta.FuncInfo = append(meta.FuncInfo, funcInfoMetadata{ + Symbol: rec.symbol, + Name: rec.name, + File: rec.file, + Line: rec.line, + Column: rec.column, + }) + } + for _, rec := range s.PCLineInfo { + meta.PCLineInfo = append(meta.PCLineInfo, pcLineMetadata{ + ID: rec.id, + Symbol: rec.symbol, + File: rec.file, + Line: rec.line, + Column: rec.column, + }) + } + return meta +} + +func summaryFromMetadata(pkg *aPackage, meta *cacheArchiveMetadata) *PackageSummary { + if pkg == nil || pkg.Package == nil || meta == nil || meta.Summary == nil { + return nil + } + summary := &PackageSummary{ + ID: pkg.ID, + PkgPath: pkg.PkgPath, + Name: pkg.Name, + LinkArgs: append([]string(nil), pkg.LinkArgs...), + ArchiveFile: pkg.ArchiveFile, + NeedRuntime: pkg.NeedRt, + NeedPyInit: pkg.NeedPyInit, + NeedAbiInit: meta.Summary.NeedAbiInit, + MethodByIndex: append([]int(nil), meta.Summary.MethodByIndex...), + MethodByName: append([]string(nil), meta.Summary.MethodByName...), + GlobalSymbols: append([]string(nil), meta.Summary.GlobalSymbols...), + FuncInfoStubs: append([]string(nil), meta.Summary.FuncInfoStubs...), + CSharedExports: append([]string(nil), meta.Summary.CSharedExports...), + } + for _, rec := range meta.Summary.FuncInfo { + summary.FuncInfo = append(summary.FuncInfo, funcInfoRecord{ + symbol: rec.Symbol, + name: rec.Name, + file: rec.File, + line: rec.Line, + column: rec.Column, + }) + } + for _, rec := range meta.Summary.PCLineInfo { + summary.PCLineInfo = append(summary.PCLineInfo, pcLineRecord{ + id: rec.ID, + symbol: rec.Symbol, + file: rec.File, + line: rec.Line, + column: rec.Column, + }) + } + return summary +} + +func summariesForPackages(pkgs []Package) []*PackageSummary { + summaries := make([]*PackageSummary, 0, len(pkgs)) + for _, pkg := range pkgs { + if pkg == nil { + continue + } + summary := pkg.Summary + if summary == nil { + summary = summarizePackage(pkg) + } + if summary != nil { + summaries = append(summaries, summary) + } + } + return summaries +} diff --git a/internal/build/package_summary_test.go b/internal/build/package_summary_test.go new file mode 100644 index 0000000000..0e27ad6f56 --- /dev/null +++ b/internal/build/package_summary_test.go @@ -0,0 +1,93 @@ +/* + * 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 ( + "go/types" + "reflect" + "testing" + + "github.com/xgo-dev/llvm" + + "github.com/goplus/llgo/internal/packages" + llssa "github.com/goplus/llgo/ssa" +) + +func TestPackageSummaryCapturesLinkerFacts(t *testing.T) { + prog := llssa.NewProgram(nil) + defer prog.Dispose() + lpkg := prog.NewPackage("p", "example.com/p") + lpkg.NeedAbiInit = 3 + lpkg.RecordReflectMethodByIndex("example.com/p.Method", 4) + lpkg.RecordReflectMethodByIndex("example.com/p.Method", 1) + lpkg.RecordReflectMethodByName("example.com/p.MethodByName", "B") + lpkg.RecordReflectMethodByName("example.com/p.MethodByName", "A") + lpkg.SetExport("example.com/p.Export", "Export") + lpkg.EmitFuncInfo("example.com/p.live", "example.com/p.Live", "p.go", 17, 2) + lpkg.EmitPCLineInfo(42, "example.com/p.live", "p.go", 18, 3) + lpkg.NewFunc(closureStubPrefix+"example.com/p.live", llssa.NoArgsNoRet, llssa.InGo).MakeBody(1).Return() + + i32 := lpkg.Module().Context().Int32Type() + defined := llvm.AddGlobal(lpkg.Module(), i32, "example.com/p.defined") + defined.SetInitializer(llvm.ConstInt(i32, 1, false)) + llvm.AddGlobal(lpkg.Module(), i32, "example.com/p.declared") + + pkg := &aPackage{ + Package: &packages.Package{ + ID: "example.com/p", + PkgPath: "example.com/p", + Name: "p", + Types: types.NewPackage("example.com/p", "p"), + }, + LPkg: lpkg, + NeedRt: true, + NeedPyInit: true, + LinkArgs: []string{"-lp"}, + ArchiveFile: "p.a", + } + summary := summarizePackage(pkg) + if got, want := summary.MethodByIndex, []int{1, 4}; !reflect.DeepEqual(got, want) { + t.Fatalf("MethodByIndex = %v, want %v", got, want) + } + if got, want := summary.MethodByName, []string{"A", "B"}; !reflect.DeepEqual(got, want) { + t.Fatalf("MethodByName = %v, want %v", got, want) + } + if got, want := summary.GlobalSymbols, []string{"example.com/p.defined"}; !reflect.DeepEqual(got, want) { + t.Fatalf("GlobalSymbols = %v, want %v", got, want) + } + if got, want := summary.FuncInfoStubs, []string{closureStubPrefix + "example.com/p.live"}; !reflect.DeepEqual(got, want) { + t.Fatalf("FuncInfoStubs = %v, want %v", got, want) + } + if got := collectFuncInfoSummaries([]*PackageSummary{summary}); len(got) != 1 || got[0].symbol != "example.com/p.live" { + t.Fatalf("func info from summary = %+v, want live record", got) + } + if got := linkedPackageGlobals([]*PackageSummary{summary}); len(got) != 1 { + t.Fatalf("globals from summary = %#v, want one defined global", got) + } + + loadedPkg := &aPackage{ + Package: pkg.Package, + LinkArgs: []string{"-lp"}, + ArchiveFile: "p.a", + NeedRt: true, + NeedPyInit: true, + } + loaded := summaryFromMetadata(loadedPkg, &cacheArchiveMetadata{Summary: summary.metadata()}) + if !reflect.DeepEqual(loaded, summary) { + t.Fatalf("cache summary round trip = %#v, want %#v", loaded, summary) + } +} From ebd698853b7c3940554195135a8c9d2649ac1cc5 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Sat, 1 Aug 2026 08:07:20 +0800 Subject: [PATCH 2/2] test: cover package summary boundaries --- internal/build/collect_test.go | 36 ++++++++++++++++++++++++++ internal/build/package_build_test.go | 19 +++++++++----- internal/build/package_summary_test.go | 35 +++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 7 deletions(-) diff --git a/internal/build/collect_test.go b/internal/build/collect_test.go index 2abc1e3c13..7b0716af14 100644 --- a/internal/build/collect_test.go +++ b/internal/build/collect_test.go @@ -171,6 +171,42 @@ func TestDisabledPackageCacheSkipsLoadAndSave(t *testing.T) { } } +func TestTryLoadFromCacheRejectsMissingSummary(t *testing.T) { + t.Setenv(llgoBuildCache, "1") + td := t.TempDir() + oldFunc := cacheRootFunc + cacheRootFunc = func() string { return td } + defer func() { cacheRootFunc = oldFunc }() + + ctx := &context{ + buildConf: &Config{Goos: "linux", Goarch: "amd64"}, + crossCompile: crosscompile.Export{ + LLVMTarget: "x86_64-unknown-linux-gnu", + }, + } + pkg := &aPackage{Package: &packages.Package{ + ID: "example.com/no-summary", + PkgPath: "example.com/no-summary", + Name: "no-summary", + }, Fingerprint: "missing-summary"} + paths := ctx.ensureCacheManager().PackagePaths(ctx.targetTriple(), pkg.PkgPath, pkg.Fingerprint) + if err := ctx.cacheManager.EnsureDir(paths); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(paths.Archive, []byte("archive"), 0o644); err != nil { + t.Fatal(err) + } + manifest := newManifestBuilder() + manifest.env.Goos = "linux" + manifest.pkg.PkgPath = pkg.PkgPath + if err := writeManifest(paths.Manifest, manifest.Build()); err != nil { + t.Fatal(err) + } + if ctx.tryLoadFromCache(pkg) { + t.Fatal("tryLoadFromCache accepted manifest without package summary") + } +} + func TestCollectFingerprintDisablesCycles(t *testing.T) { pkg := &aPackage{Package: &packages.Package{ID: "example.com/cycle", PkgPath: "example.com/cycle"}} ctx := &context{fingerprinting: map[string]bool{pkg.ID: true}} diff --git a/internal/build/package_build_test.go b/internal/build/package_build_test.go index 9ee12a983a..9a5854dc7e 100644 --- a/internal/build/package_build_test.go +++ b/internal/build/package_build_test.go @@ -240,14 +240,18 @@ func TestPrePackageBuildSkipsExternalLinkOnlyPackage(t *testing.T) { Types: types.NewPackage("example.com/linkonly", "linkonly"), ExportFile: "stale.a", }} - ctx := &context{buildConf: &Config{}, built: make(map[string]none)} + ctx := &context{ + conf: &packages.Config{}, + buildConf: &Config{Goos: "linux", Goarch: "amd64", ForceRebuild: true}, + built: make(map[string]none), + } task := &packageBuildTask{pkg: pkg, kind: cl.PkgLinkExtern, kindParam: "-lexample"} err := prePackageBuild(ctx, task, false) if err != nil { t.Fatal(err) } - if !task.skip || pkg.ExportFile != "" { - t.Fatalf("external link-only pre = skip %v, export %q", task.skip, pkg.ExportFile) + if !task.skip || pkg.ExportFile != "" || pkg.Summary == nil { + t.Fatalf("external link-only pre = skip %v, export %q, summary %#v", task.skip, pkg.ExportFile, pkg.Summary) } if len(pkg.LinkArgs) != 1 || pkg.LinkArgs[0] != "-lexample" { t.Fatalf("external link args = %q, want [-lexample]", pkg.LinkArgs) @@ -280,7 +284,7 @@ func TestBuildSSAPkgsEmptyAndNilEntries(t *testing.T) { buildSSAPkgs(ctx, []ssaBuildEntry{{pkg: pkg}, {pkg: pkg}}) } -func TestPreflightFingerprintsSkippedPackage(t *testing.T) { +func TestPreFingerprintsSkippedPackage(t *testing.T) { pkg := &aPackage{Package: &packages.Package{ ID: "unsafe", PkgPath: "unsafe", @@ -291,11 +295,12 @@ func TestPreflightFingerprintsSkippedPackage(t *testing.T) { buildConf: &Config{Goos: "linux", Goarch: "amd64", ForceRebuild: true}, built: make(map[string]none), } - skip, err := preflightPackageBuild(ctx, newPackageBuildSpec(pkg), false) + task := newPackageBuildTask(pkg) + err := prePackageBuild(ctx, task, false) if err != nil { t.Fatal(err) } - if !skip || pkg.Fingerprint == "" || pkg.Manifest == "" || pkg.Summary == nil { - t.Fatalf("skipped package was not fully prepared: skip=%v fingerprint=%q manifest=%q summary=%#v", skip, pkg.Fingerprint, pkg.Manifest, pkg.Summary) + if !task.skip || pkg.Fingerprint == "" || pkg.Manifest == "" || pkg.Summary == nil { + t.Fatalf("skipped package was not fully prepared: skip=%v fingerprint=%q manifest=%q summary=%#v", task.skip, pkg.Fingerprint, pkg.Manifest, pkg.Summary) } } diff --git a/internal/build/package_summary_test.go b/internal/build/package_summary_test.go index 0e27ad6f56..3f65093777 100644 --- a/internal/build/package_summary_test.go +++ b/internal/build/package_summary_test.go @@ -91,3 +91,38 @@ func TestPackageSummaryCapturesLinkerFacts(t *testing.T) { t.Fatalf("cache summary round trip = %#v, want %#v", loaded, summary) } } + +func TestPackageSummaryEmptyInputs(t *testing.T) { + if got := summarizePackage(nil); got != nil { + t.Fatalf("summarizePackage(nil) = %#v", got) + } + var summary *PackageSummary + if got := summary.metadata(); got != nil { + t.Fatalf("nil summary metadata = %#v", got) + } + if got := summaryFromMetadata(nil, nil); got != nil { + t.Fatalf("summaryFromMetadata(nil, nil) = %#v", got) + } + if got := linkedPackageGlobals(nil); got != nil { + t.Fatalf("linkedPackageGlobals(nil) = %#v", got) + } + if got := linkedPackageGlobals([]*PackageSummary{nil}); len(got) != 0 { + t.Fatalf("linkedPackageGlobals([nil]) = %#v", got) + } + if got := collectFuncInfoSummaries([]*PackageSummary{nil, {FuncInfo: []funcInfoRecord{{}}}}); got != nil { + t.Fatalf("collectFuncInfoSummaries(empty) = %#v", got) + } + if got := collectPCLineInfoSummaries([]*PackageSummary{nil, {PCLineInfo: []pcLineRecord{{}}}}); len(got) != 0 { + t.Fatalf("collectPCLineInfoSummaries(empty) = %#v", got) + } + if got := collectFuncInfoStubRecordsSummaries([]*PackageSummary{nil}, nil); got != nil { + t.Fatalf("collectFuncInfoStubRecordsSummaries(nil) = %#v", got) + } + if got := collectFuncInfoStubRecordsSummaries([]*PackageSummary{nil}, []funcInfoRecord{{symbol: "target"}}); len(got) != 0 { + t.Fatalf("collectFuncInfoStubRecordsSummaries([nil]) = %#v", got) + } + sharedCtx := &context{buildConf: &Config{BuildMode: BuildModeCShared}} + if got := cSharedExportArgsSummaries(sharedCtx, []*PackageSummary{nil}); len(got) != 0 { + t.Fatalf("cSharedExportArgsSummaries([nil]) = %#v", got) + } +}