move out c/cpp/py

This commit is contained in:
Li Jie
2025-04-03 15:52:18 +08:00
parent 0a8a4eb6a6
commit ed366568b4
777 changed files with 4608 additions and 139122 deletions

922
internal/build/build.go Normal file
View File

@@ -0,0 +1,922 @@
/*
* Copyright (c) 2024 The GoPlus Authors (goplus.org). 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 (
"bytes"
"debug/macho"
"fmt"
"go/ast"
"go/build"
"go/constant"
"go/token"
"go/types"
"log"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strings"
"unsafe"
"golang.org/x/tools/go/ssa"
"github.com/goplus/llgo/cl"
"github.com/goplus/llgo/internal/env"
"github.com/goplus/llgo/internal/mockable"
"github.com/goplus/llgo/internal/packages"
"github.com/goplus/llgo/internal/typepatch"
"github.com/goplus/llgo/ssa/abi"
xenv "github.com/goplus/llgo/xtool/env"
"github.com/goplus/llgo/xtool/env/llvm"
llruntime "github.com/goplus/llgo/runtime"
llssa "github.com/goplus/llgo/ssa"
clangCheck "github.com/goplus/llgo/xtool/clang/check"
)
type Mode int
const (
ModeBuild Mode = iota
ModeInstall
ModeRun
ModeTest
ModeCmpTest
ModeGen
)
const (
debugBuild = packages.DebugPackagesLoad
)
type Config struct {
BinPath string
AppExt string // ".exe" on Windows, empty on Unix
OutFile string // only valid for ModeBuild when len(pkgs) == 1
RunArgs []string // only valid for ModeRun
Mode Mode
GenExpect bool // only valid for ModeCmpTest
}
func NewDefaultConf(mode Mode) *Config {
bin := os.Getenv("GOBIN")
if bin == "" {
gopath, err := envGOPATH()
if err != nil {
panic(fmt.Errorf("cannot get GOPATH: %v", err))
}
bin = filepath.Join(gopath, "bin")
}
if err := os.MkdirAll(bin, 0755); err != nil {
panic(fmt.Errorf("cannot create bin directory: %v", err))
}
conf := &Config{
BinPath: bin,
Mode: mode,
AppExt: DefaultAppExt(),
}
return conf
}
func envGOPATH() (string, error) {
if gopath := os.Getenv("GOPATH"); gopath != "" {
return gopath, nil
}
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, "go"), nil
}
func DefaultAppExt() string {
if runtime.GOOS == "windows" {
return ".exe"
}
return ""
}
// -----------------------------------------------------------------------------
const (
loadFiles = packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles
loadImports = loadFiles | packages.NeedImports
loadTypes = loadImports | packages.NeedTypes | packages.NeedTypesSizes
loadSyntax = loadTypes | packages.NeedSyntax | packages.NeedTypesInfo
)
func Do(args []string, conf *Config) ([]Package, error) {
flags, patterns, verbose := ParseArgs(args, buildFlags)
flags = append(flags, "-tags", "llgo")
cfg := &packages.Config{
Mode: loadSyntax | packages.NeedDeps | packages.NeedModule | packages.NeedExportFile,
BuildFlags: flags,
Fset: token.NewFileSet(),
Tests: conf.Mode == ModeTest,
}
if conf.Mode == ModeTest {
cfg.Mode |= packages.NeedForTest
}
if len(llruntime.OverlayFiles) > 0 {
cfg.Overlay = make(map[string][]byte)
for file, src := range llruntime.OverlayFiles {
overlay := unsafe.Slice(unsafe.StringData(src), len(src))
cfg.Overlay[filepath.Join(env.GOROOT(), "src", file)] = overlay
}
}
cl.EnableDebug(IsDbgEnabled())
cl.EnableDbgSyms(IsDbgSymsEnabled())
cl.EnableTrace(IsTraceEnabled())
llssa.Initialize(llssa.InitAll)
target := &llssa.Target{
GOOS: build.Default.GOOS,
GOARCH: build.Default.GOARCH,
}
prog := llssa.NewProgram(target)
sizes := func(sizes types.Sizes, compiler, arch string) types.Sizes {
if arch == "wasm" {
sizes = &types.StdSizes{4, 4}
}
return prog.TypeSizes(sizes)
}
dedup := packages.NewDeduper()
dedup.SetPreload(func(pkg *types.Package, files []*ast.File) {
if llruntime.SkipToBuild(pkg.Path()) {
return
}
cl.ParsePkgSyntax(prog, pkg, files)
})
if patterns == nil {
patterns = []string{"."}
}
initial, err := packages.LoadEx(dedup, sizes, cfg, patterns...)
check(err)
mode := conf.Mode
if len(initial) > 1 {
switch mode {
case ModeBuild:
if conf.OutFile != "" {
return nil, fmt.Errorf("cannot build multiple packages with -o")
}
case ModeRun:
return nil, fmt.Errorf("cannot run multiple packages")
case ModeTest:
newInitial := make([]*packages.Package, 0, len(initial))
for _, pkg := range initial {
if needLink(pkg, mode) {
newInitial = append(newInitial, pkg)
}
}
initial = newInitial
}
}
altPkgPaths := altPkgs(initial, llssa.PkgRuntime)
cfg.Dir = env.LLGoRuntimeDir()
altPkgs, err := packages.LoadEx(dedup, sizes, cfg, altPkgPaths...)
check(err)
noRt := 1
prog.SetRuntime(func() *types.Package {
noRt = 0
return altPkgs[0].Types
})
prog.SetPython(func() *types.Package {
return dedup.Check(llssa.PkgPython).Types
})
buildMode := ssaBuildMode
if IsDbgEnabled() {
buildMode |= ssa.GlobalDebug
}
if !IsOptimizeEnabled() {
buildMode |= ssa.NaiveForm
}
progSSA := ssa.NewProgram(initial[0].Fset, buildMode)
patches := make(cl.Patches, len(altPkgPaths))
altSSAPkgs(progSSA, patches, altPkgs[1:], verbose)
env := llvm.New("")
os.Setenv("PATH", env.BinDir()+":"+os.Getenv("PATH")) // TODO(xsw): check windows
output := conf.OutFile != ""
ctx := &context{env, cfg, progSSA, prog, dedup, patches, make(map[string]none), initial, mode, 0, output, make(map[*packages.Package]bool), make(map[*packages.Package]bool)}
pkgs, err := buildAllPkgs(ctx, initial, verbose)
check(err)
if mode == ModeGen {
for _, pkg := range pkgs {
if pkg.Package == initial[0] {
return []*aPackage{pkg}, nil
}
}
return nil, fmt.Errorf("initial package not found")
}
dpkg, err := buildAllPkgs(ctx, altPkgs[noRt:], verbose)
check(err)
var linkArgs []string
for _, pkg := range dpkg {
linkArgs = append(linkArgs, pkg.LinkArgs...)
}
for _, pkg := range initial {
if needLink(pkg, mode) {
linkMainPkg(ctx, pkg, pkgs, linkArgs, conf, mode, verbose)
}
}
return dpkg, nil
}
func needLink(pkg *packages.Package, mode Mode) bool {
if mode == ModeTest {
return strings.HasSuffix(pkg.ID, ".test")
}
return pkg.Name == "main"
}
func setNeedRuntimeOrPyInit(ctx *context, pkg *packages.Package, needRuntime, needPyInit bool) {
ctx.needRt[pkg] = needRuntime
ctx.needPyInit[pkg] = needPyInit
}
func isNeedRuntimeOrPyInit(ctx *context, pkg *packages.Package) (needRuntime, needPyInit bool) {
needRuntime = ctx.needRt[pkg]
needPyInit = ctx.needPyInit[pkg]
return
}
const (
ssaBuildMode = ssa.SanityCheckFunctions | ssa.InstantiateGenerics
)
type context struct {
env *llvm.Env
conf *packages.Config
progSSA *ssa.Program
prog llssa.Program
dedup packages.Deduper
patches cl.Patches
built map[string]none
initial []*packages.Package
mode Mode
nLibdir int
output bool
needRt map[*packages.Package]bool
needPyInit map[*packages.Package]bool
}
func buildAllPkgs(ctx *context, initial []*packages.Package, verbose bool) (pkgs []*aPackage, err error) {
pkgs, errPkgs := allPkgs(ctx, initial, verbose)
for _, errPkg := range errPkgs {
for _, err := range errPkg.Errors {
fmt.Fprintln(os.Stderr, err)
}
fmt.Fprintln(os.Stderr, "cannot build SSA for package", errPkg)
}
if len(errPkgs) > 0 {
return nil, fmt.Errorf("cannot build SSA for packages")
}
built := ctx.built
for _, aPkg := range pkgs {
pkg := aPkg.Package
if _, ok := built[pkg.ID]; ok {
pkg.ExportFile = ""
continue
}
built[pkg.ID] = none{}
switch kind, param := cl.PkgKindOf(pkg.Types); kind {
case cl.PkgDeclOnly:
// skip packages that only contain declarations
// and set no export file
pkg.ExportFile = ""
case cl.PkgLinkIR, cl.PkgLinkExtern, cl.PkgPyModule:
if len(pkg.GoFiles) > 0 {
cgoLdflags, err := buildPkg(ctx, aPkg, verbose)
if err != nil {
panic(err)
}
linkParts := concatPkgLinkFiles(ctx, pkg, verbose)
allParts := append(linkParts, cgoLdflags...)
if pkg.ExportFile != "" {
allParts = append(allParts, pkg.ExportFile)
}
aPkg.LinkArgs = allParts
} else {
// panic("todo")
// TODO(xsw): support packages out of llgo
pkg.ExportFile = ""
}
if kind == cl.PkgLinkExtern {
// need to be linked with external library
// format: ';' separated alternative link methods. e.g.
// link: $LLGO_LIB_PYTHON; $(pkg-config --libs python3-embed); -lpython3
altParts := strings.Split(param, ";")
expdArgs := make([]string, 0, len(altParts))
for _, param := range altParts {
param = strings.TrimSpace(param)
if strings.ContainsRune(param, '$') {
expdArgs = append(expdArgs, xenv.ExpandEnvToArgs(param)...)
ctx.nLibdir++
} else {
fields := strings.Fields(param)
expdArgs = append(expdArgs, fields...)
}
if len(expdArgs) > 0 {
break
}
}
if len(expdArgs) == 0 {
panic(fmt.Sprintf("'%s' cannot locate the external library", param))
}
pkgLinkArgs := make([]string, 0, 3)
if expdArgs[0][0] == '-' {
pkgLinkArgs = append(pkgLinkArgs, expdArgs...)
} else {
linkFile := expdArgs[0]
dir, lib := filepath.Split(linkFile)
pkgLinkArgs = append(pkgLinkArgs, "-l"+lib)
if dir != "" {
pkgLinkArgs = append(pkgLinkArgs, "-L"+dir)
ctx.nLibdir++
}
}
if err := clangCheck.CheckLinkArgs(pkgLinkArgs); err != nil {
panic(fmt.Sprintf("test link args '%s' failed\n\texpanded to: %v\n\tresolved to: %v\n\terror: %v", param, expdArgs, pkgLinkArgs, err))
}
aPkg.LinkArgs = append(aPkg.LinkArgs, pkgLinkArgs...)
}
default:
cgoLdflags, err := buildPkg(ctx, aPkg, verbose)
if err != nil {
panic(err)
}
if pkg.ExportFile != "" {
aPkg.LinkArgs = append(cgoLdflags, pkg.ExportFile)
}
aPkg.LinkArgs = append(aPkg.LinkArgs, concatPkgLinkFiles(ctx, pkg, verbose)...)
if aPkg.AltPkg != nil {
aPkg.LinkArgs = append(aPkg.LinkArgs, concatPkgLinkFiles(ctx, aPkg.AltPkg.Package, verbose)...)
}
setNeedRuntimeOrPyInit(ctx, pkg, aPkg.LPkg.NeedRuntime, aPkg.LPkg.NeedPyInit)
}
}
return
}
func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, linkArgs []string, conf *Config, mode Mode, verbose bool) {
pkgPath := pkg.PkgPath
name := path.Base(pkgPath)
app := conf.OutFile
if app == "" {
if mode == ModeBuild && len(ctx.initial) > 1 {
// For multiple packages in ModeBuild mode, use temporary file
tmpFile, err := os.CreateTemp("", name+"*"+conf.AppExt)
check(err)
app = tmpFile.Name()
tmpFile.Close()
} else {
app = filepath.Join(conf.BinPath, name+conf.AppExt)
}
}
args := make([]string, 0, len(pkg.Imports)+len(linkArgs)+16)
args = append(
args,
"-o", app,
"-Wl,--error-limit=0",
"-fuse-ld=lld",
"-Wno-override-module",
// "-O2", // FIXME: This will cause TestFinalizer in _test/bdwgc.go to fail on macOS.
)
switch runtime.GOOS {
case "darwin": // ld64.lld (macOS)
args = append(
args,
"-rpath", "@loader_path",
"-rpath", "@loader_path/../lib",
"-Xlinker", "-dead_strip",
)
case "windows": // lld-link (Windows)
// TODO: Add options for Windows.
default: // ld.lld (Unix), wasm-ld (WebAssembly)
args = append(
args,
"-rpath", "$ORIGIN",
"-rpath", "$ORIGIN/../lib",
"-fdata-sections",
"-ffunction-sections",
"-Xlinker", "--gc-sections",
"-lm",
"-latomic",
"-lpthread", // libpthread is built-in since glibc 2.34 (2021-08-01); we need to support earlier versions.
)
}
needRuntime := false
needPyInit := false
pkgsMap := make(map[*packages.Package]*aPackage, len(pkgs))
for _, v := range pkgs {
pkgsMap[v.Package] = v
}
packages.Visit([]*packages.Package{pkg}, nil, func(p *packages.Package) {
if p.ExportFile != "" { // skip packages that only contain declarations
aPkg := pkgsMap[p]
args = append(args, aPkg.LinkArgs...)
need1, need2 := isNeedRuntimeOrPyInit(ctx, p)
if !needRuntime {
needRuntime = need1
}
if !needPyInit {
needPyInit = need2
}
}
})
entryLLFile, err := genMainModuleFile(llssa.PkgRuntime, pkg.PkgPath, needRuntime, needPyInit)
if err != nil {
panic(err)
}
// defer os.Remove(entryLLFile)
args = append(args, entryLLFile)
var aPkg *aPackage
for _, v := range pkgs {
if v.Package == pkg { // found this package
aPkg = v
break
}
}
args = append(args, linkArgs...)
if ctx.output {
lpkg := aPkg.LPkg
os.WriteFile(pkg.ExportFile, []byte(lpkg.String()), 0644)
}
// add rpath and find libs
exargs := make([]string, 0, ctx.nLibdir<<1)
libs := make([]string, 0, ctx.nLibdir*3)
for _, arg := range args {
if strings.HasPrefix(arg, "-L") {
exargs = append(exargs, "-rpath", arg[2:])
} else if strings.HasPrefix(arg, "-l") {
libs = append(libs, arg[2:])
}
}
args = append(args, exargs...)
if IsDbgSymsEnabled() {
args = append(args, "-gdwarf-4")
}
// TODO(xsw): show work
if verbose {
fmt.Fprintln(os.Stderr, "clang", args)
}
err = ctx.env.Clang().Link(args...)
check(err)
if IsRpathChangeEnabled() && runtime.GOOS == "darwin" {
dylibDeps := make([]string, 0, len(libs))
for _, lib := range libs {
dylibDep := findDylibDep(app, lib)
if dylibDep != "" {
dylibDeps = append(dylibDeps, dylibDep)
}
}
err := ctx.env.InstallNameTool().ChangeToRpath(app, dylibDeps...)
check(err)
}
switch mode {
case ModeTest:
cmd := exec.Command(app, conf.RunArgs...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()
if s := cmd.ProcessState; s != nil {
fmt.Fprintf(os.Stderr, "%s: exit code %d\n", app, s.ExitCode())
}
case ModeRun:
cmd := exec.Command(app, conf.RunArgs...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()
if s := cmd.ProcessState; s != nil {
mockable.Exit(s.ExitCode())
}
case ModeCmpTest:
cmpTest(filepath.Dir(pkg.GoFiles[0]), pkgPath, app, conf.GenExpect, conf.RunArgs)
}
}
func genMainModuleFile(rtPkgPath, mainPkgPath string, needRuntime, needPyInit bool) (path string, err error) {
var (
pyInitDecl string
pyInit string
rtInitDecl string
rtInit string
)
if needRuntime {
rtInit = "call void @\"" + rtPkgPath + ".init\"()"
rtInitDecl = "declare void @\"" + rtPkgPath + ".init\"()"
}
if needPyInit {
pyInit = "call void @Py_Initialize()"
pyInitDecl = "declare void @Py_Initialize()"
}
mainCode := fmt.Sprintf(`; ModuleID = 'main'
source_filename = "main"
@__llgo_argc = global i32 0, align 4
@__llgo_argv = global ptr null, align 8
%s
%s
declare void @"%s.init"()
declare void @"%s.main"()
define weak void @runtime.init() {
ret void
}
; TODO(lijie): workaround for syscall patch
define weak void @"syscall.init"() {
ret void
}
define i32 @main(i32 %%0, ptr %%1) {
_llgo_0:
%s
store i32 %%0, ptr @__llgo_argc, align 4
store ptr %%1, ptr @__llgo_argv, align 8
%s
call void @runtime.init()
call void @"%s.init"()
call void @"%s.main"()
ret i32 0
}
`, pyInitDecl, rtInitDecl, mainPkgPath, mainPkgPath,
pyInit, rtInit, mainPkgPath, mainPkgPath)
f, err := os.CreateTemp("", "main*.ll")
if err != nil {
return "", err
}
_, err = f.Write([]byte(mainCode))
if err != nil {
return "", err
}
err = f.Close()
if err != nil {
return "", err
}
return f.Name(), nil
}
func buildPkg(ctx *context, aPkg *aPackage, verbose bool) (cgoLdflags []string, err error) {
pkg := aPkg.Package
pkgPath := pkg.PkgPath
if debugBuild || verbose {
fmt.Fprintln(os.Stderr, pkgPath)
}
if llruntime.SkipToBuild(pkgPath) {
pkg.ExportFile = ""
return
}
var syntax = pkg.Syntax
if altPkg := aPkg.AltPkg; altPkg != nil {
syntax = append(syntax, altPkg.Syntax...)
}
showDetail := verbose && pkgExists(ctx.initial, pkg)
if showDetail {
llssa.SetDebug(llssa.DbgFlagAll)
cl.SetDebug(cl.DbgFlagAll)
}
ret, externs, err := cl.NewPackageEx(ctx.prog, ctx.patches, aPkg.SSA, syntax)
if showDetail {
llssa.SetDebug(0)
cl.SetDebug(0)
}
check(err)
aPkg.LPkg = ret
cgoLdflags, err = buildCgo(ctx, aPkg, aPkg.Package.Syntax, externs, verbose)
if aPkg.AltPkg != nil {
altLdflags, e := buildCgo(ctx, aPkg, aPkg.AltPkg.Syntax, externs, verbose)
if e != nil {
return nil, fmt.Errorf("build cgo of %v failed: %v", pkgPath, e)
}
cgoLdflags = append(cgoLdflags, altLdflags...)
}
if pkg.ExportFile != "" {
pkg.ExportFile += ".ll"
os.WriteFile(pkg.ExportFile, []byte(ret.String()), 0644)
if debugBuild || verbose {
fmt.Fprintf(os.Stderr, "==> Export %s: %s\n", aPkg.PkgPath, pkg.ExportFile)
}
if IsCheckEnable() {
if err, msg := llcCheck(ctx.env, pkg.ExportFile); err != nil {
fmt.Fprintf(os.Stderr, "==> lcc %v: %v\n%v\n", pkg.PkgPath, pkg.ExportFile, msg)
}
}
}
return
}
func llcCheck(env *llvm.Env, exportFile string) (err error, msg string) {
bin := filepath.Join(env.BinDir(), "llc")
cmd := exec.Command(bin, "-filetype=null", exportFile)
var buf bytes.Buffer
cmd.Stderr = &buf
if err = cmd.Run(); err != nil {
msg = buf.String()
}
return
}
const (
altPkgPathPrefix = abi.PatchPathPrefix
)
func altPkgs(initial []*packages.Package, alts ...string) []string {
packages.Visit(initial, nil, func(p *packages.Package) {
if p.Types != nil && !p.IllTyped {
if llruntime.HasAltPkg(p.PkgPath) {
alts = append(alts, altPkgPathPrefix+p.PkgPath)
}
}
})
return alts
}
func altSSAPkgs(prog *ssa.Program, patches cl.Patches, alts []*packages.Package, verbose bool) {
packages.Visit(alts, nil, func(p *packages.Package) {
if typs := p.Types; typs != nil && !p.IllTyped {
if debugBuild || verbose {
log.Println("==> BuildSSA", p.ID)
}
pkgSSA := prog.CreatePackage(typs, p.Syntax, p.TypesInfo, true)
if strings.HasPrefix(p.ID, altPkgPathPrefix) {
path := p.ID[len(altPkgPathPrefix):]
patches[path] = cl.Patch{Alt: pkgSSA, Types: typepatch.Clone(typs)}
if debugBuild || verbose {
log.Println("==> Patching", path)
}
}
}
})
prog.Build()
}
type aPackage struct {
*packages.Package
SSA *ssa.Package
AltPkg *packages.Cached
LPkg llssa.Package
LinkArgs []string
}
type Package = *aPackage
func allPkgs(ctx *context, initial []*packages.Package, verbose bool) (all []*aPackage, errs []*packages.Package) {
prog := ctx.progSSA
built := ctx.built
packages.Visit(initial, nil, func(p *packages.Package) {
if p.Types != nil && !p.IllTyped {
pkgPath := p.PkgPath
if _, ok := built[pkgPath]; ok || strings.HasPrefix(pkgPath, altPkgPathPrefix) {
return
}
var altPkg *packages.Cached
var ssaPkg = createSSAPkg(prog, p, verbose)
if llruntime.HasAltPkg(pkgPath) {
if altPkg = ctx.dedup.Check(altPkgPathPrefix + pkgPath); altPkg == nil {
return
}
}
all = append(all, &aPackage{p, ssaPkg, altPkg, nil, nil})
} else {
errs = append(errs, p)
}
})
return
}
func createSSAPkg(prog *ssa.Program, p *packages.Package, verbose bool) *ssa.Package {
pkgSSA := prog.ImportedPackage(p.ID)
if pkgSSA == nil {
if debugBuild || verbose {
log.Println("==> BuildSSA", p.ID)
}
pkgSSA = prog.CreatePackage(p.Types, p.Syntax, p.TypesInfo, true)
pkgSSA.Build() // TODO(xsw): build concurrently
}
return pkgSSA
}
var (
// TODO(xsw): complete build flags
buildFlags = map[string]bool{
"-C": true, // -C dir: Change to dir before running the command
"-a": false, // -a: force rebuilding of packages that are already up-to-date
"-n": false, // -n: print the commands but do not run them
"-p": true, // -p n: the number of programs to run in parallel
"-race": false, // -race: enable data race detection
"-cover": false, // -cover: enable coverage analysis
"-covermode": true, // -covermode mode: set the mode for coverage analysis
"-v": false, // -v: print the names of packages as they are compiled
"-work": false, // -work: print the name of the temporary work directory and do not delete it when exiting
"-x": false, // -x: print the commands
"-tags": true, // -tags 'tag,list': a space-separated list of build tags to consider satisfied during the build
"-pkgdir": true, // -pkgdir dir: install and load all packages from dir instead of the usual locations
"-ldflags": true, // --ldflags 'flag list': arguments to pass on each go tool link invocation
}
)
const llgoDebug = "LLGO_DEBUG"
const llgoDbgSyms = "LLGO_DEBUG_SYMBOLS"
const llgoTrace = "LLGO_TRACE"
const llgoOptimize = "LLGO_OPTIMIZE"
const llgoCheck = "LLGO_CHECK"
const llgoRpathChange = "LLGO_RPATH_CHANGE"
func isEnvOn(env string, defVal bool) bool {
envVal := strings.ToLower(os.Getenv(env))
if envVal == "" {
return defVal
}
return envVal == "1" || envVal == "true" || envVal == "on"
}
func IsTraceEnabled() bool {
return isEnvOn(llgoTrace, false)
}
func IsDbgEnabled() bool {
return isEnvOn(llgoDebug, false) || isEnvOn(llgoDbgSyms, false)
}
func IsDbgSymsEnabled() bool {
return isEnvOn(llgoDbgSyms, false)
}
func IsOptimizeEnabled() bool {
return isEnvOn(llgoOptimize, true)
}
func IsCheckEnable() bool {
return isEnvOn(llgoCheck, false)
}
func IsRpathChangeEnabled() bool {
return isEnvOn(llgoRpathChange, false)
}
func ParseArgs(args []string, swflags map[string]bool) (flags, patterns []string, verbose bool) {
n := len(args)
for i := 0; i < n; i++ {
arg := args[i]
if strings.HasPrefix(arg, "-") {
checkFlag(arg, &i, &verbose, swflags)
} else {
patterns = append([]string{}, args[i:]...)
flags = append([]string{}, args[:i]...)
return
}
}
return
}
func SkipFlagArgs(args []string) int {
n := len(args)
for i := 0; i < n; i++ {
arg := args[i]
if strings.HasPrefix(arg, "-") {
checkFlag(arg, &i, nil, buildFlags)
} else {
return i
}
}
return -1
}
func checkFlag(arg string, i *int, verbose *bool, swflags map[string]bool) {
if pos := strings.IndexByte(arg, '='); pos > 0 {
if verbose != nil && arg == "-v=true" {
*verbose = true
}
} else if hasarg, ok := swflags[arg]; ok {
if hasarg {
*i++
} else if verbose != nil && arg == "-v" {
*verbose = true
}
} else {
panic("unknown flag: " + arg)
}
}
func concatPkgLinkFiles(ctx *context, pkg *packages.Package, verbose bool) (parts []string) {
llgoPkgLinkFiles(ctx, pkg, func(linkFile string) {
parts = append(parts, linkFile)
}, verbose)
return
}
// const LLGoFiles = "file1; file2; ..."
func llgoPkgLinkFiles(ctx *context, pkg *packages.Package, procFile func(linkFile string), verbose bool) {
if o := pkg.Types.Scope().Lookup("LLGoFiles"); o != nil {
val := o.(*types.Const).Val()
if val.Kind() == constant.String {
clFiles(ctx, constant.StringVal(val), pkg, procFile, verbose)
}
}
}
// files = "file1; file2; ..."
// files = "$(pkg-config --cflags xxx): file1; file2; ..."
func clFiles(ctx *context, files string, pkg *packages.Package, procFile func(linkFile string), verbose bool) {
dir := filepath.Dir(pkg.GoFiles[0])
expFile := pkg.ExportFile
args := make([]string, 0, 16)
if strings.HasPrefix(files, "$") { // has cflags
if pos := strings.IndexByte(files, ':'); pos > 0 {
cflags := xenv.ExpandEnvToArgs(files[:pos])
files = files[pos+1:]
args = append(args, cflags...)
}
}
for _, file := range strings.Split(files, ";") {
cFile := filepath.Join(dir, strings.TrimSpace(file))
clFile(ctx, args, cFile, expFile, procFile, verbose)
}
}
func clFile(ctx *context, args []string, cFile, expFile string, procFile func(linkFile string), verbose bool) {
llFile := expFile + filepath.Base(cFile) + ".ll"
args = append(args, "-emit-llvm", "-S", "-o", llFile, "-c", cFile)
if verbose {
fmt.Fprintln(os.Stderr, "clang", args)
}
err := ctx.env.Clang().Compile(args...)
check(err)
procFile(llFile)
}
func pkgExists(initial []*packages.Package, pkg *packages.Package) bool {
for _, v := range initial {
if v == pkg {
return true
}
}
return false
}
// findDylibDep finds the dylib dependency in the executable. It returns empty
// string if not found.
func findDylibDep(exe, lib string) string {
file, err := macho.Open(exe)
check(err)
defer file.Close()
for _, load := range file.Loads {
if dylib, ok := load.(*macho.Dylib); ok {
if strings.HasPrefix(filepath.Base(dylib.Name), fmt.Sprintf("lib%s.", lib)) {
return dylib.Name
}
}
}
return ""
}
type none struct{}
func check(err error) {
if err != nil {
panic(err)
}
}
// -----------------------------------------------------------------------------

View File

@@ -0,0 +1,66 @@
//go:build !llgo
// +build !llgo
package build
import (
"fmt"
"os"
"testing"
"github.com/goplus/llgo/internal/mockable"
)
func mockRun(args []string, cfg *Config) {
const maxAttempts = 3
var lastErr error
var lastPanic interface{}
for attempt := 0; attempt < maxAttempts; attempt++ {
mockable.EnableMock()
func() {
defer func() {
if r := recover(); r != nil {
if r != "exit" {
lastPanic = r
} else {
exitCode := mockable.ExitCode()
if (exitCode != 0) != false {
lastPanic = fmt.Errorf("got exit code %d", exitCode)
}
}
}
}()
file, _ := os.CreateTemp("", "llgo-*")
cfg.OutFile = file.Name()
file.Close()
defer os.Remove(cfg.OutFile)
_, err := Do(args, cfg)
if err == nil {
return // Success, return immediately from the inner function
}
lastErr = err
}()
if lastPanic == nil && lastErr == nil {
return // Success, return from mockRun
}
// Continue to next attempt if this one failed
}
// If we get here, all attempts failed
if lastPanic != nil {
panic(lastPanic)
}
panic(fmt.Errorf("all %d attempts failed, last error: %v", maxAttempts, lastErr))
}
func TestRun(t *testing.T) {
mockRun([]string{"-v", "../../cl/_testgo/print"}, &Config{Mode: ModeRun})
}
func _TestTest(t *testing.T) {
mockRun([]string{"-v", "../../_demo/runtest"}, &Config{Mode: ModeTest})
}
func TestCmpTest(t *testing.T) {
mockRun([]string{"-v", "../../_demo/runtest"}, &Config{Mode: ModeCmpTest})
}

413
internal/build/cgo.go Normal file
View File

@@ -0,0 +1,413 @@
/*
* Copyright (c) 2024 The GoPlus Authors (goplus.org). 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 (
"encoding/json"
"fmt"
"go/ast"
"go/token"
"go/types"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/goplus/llgo/internal/buildtags"
llssa "github.com/goplus/llgo/ssa"
"github.com/goplus/llgo/xtool/safesplit"
)
type cgoDecl struct {
tag string
cflags []string
ldflags []string
}
type cgoPreamble struct {
goFile string
src string
}
const (
cgoHeader = `
#include <stdlib.h>
static void* _Cmalloc(size_t size) {
return malloc(size);
}
`
)
func buildCgo(ctx *context, pkg *aPackage, files []*ast.File, externs []string, verbose bool) (cgoLdflags []string, err error) {
cfiles, preambles, cdecls, err := parseCgo_(pkg, files)
if err != nil {
return
}
tagUsed := make(map[string]bool)
for _, cdecl := range cdecls {
if cdecl.tag != "" {
tagUsed[cdecl.tag] = false
}
}
buildtags.CheckTags(ctx.conf.BuildFlags, tagUsed)
cflags := []string{}
ldflags := []string{}
for _, cdecl := range cdecls {
if cdecl.tag == "" || tagUsed[cdecl.tag] {
if len(cdecl.cflags) > 0 {
cflags = append(cflags, cdecl.cflags...)
}
if len(cdecl.ldflags) > 0 {
ldflags = append(ldflags, cdecl.ldflags...)
}
}
}
incDirs := make(map[string]none)
for _, preamble := range preambles {
dir, _ := filepath.Split(preamble.goFile)
if _, ok := incDirs[dir]; !ok {
incDirs[dir] = none{}
cflags = append(cflags, "-I"+dir)
}
}
for _, cfile := range cfiles {
clFile(ctx, cflags, cfile, pkg.ExportFile, func(linkFile string) {
cgoLdflags = append(cgoLdflags, linkFile)
}, verbose)
}
re := regexp.MustCompile(`^(_cgo_[^_]+_(Cfunc|Cmacro)_)(.*)$`)
cgoSymbols := make(map[string]string)
mallocFix := false
for _, symbolName := range externs {
lastPart := symbolName
lastDot := strings.LastIndex(symbolName, ".")
if lastDot != -1 {
lastPart = symbolName[lastDot+1:]
}
if strings.HasPrefix(lastPart, "__cgo_") {
// func ptr var: main.__cgo_func_name
cgoSymbols[symbolName] = lastPart
} else if m := re.FindStringSubmatch(symbolName); len(m) > 0 {
prefix := m[1] // _cgo_hash_(Cfunc|Cmacro)_
name := m[3] // remaining part
cgoSymbols[symbolName] = name
// fix missing _cgo_9113e32b6599_Cfunc__Cmalloc
if !mallocFix && m[2] == "Cfunc" {
mallocName := prefix + "_Cmalloc"
cgoSymbols[mallocName] = "_Cmalloc"
mallocFix = true
}
}
}
for _, preamble := range preambles {
tmpFile, err := os.CreateTemp("", "-cgo-*.c")
if err != nil {
return nil, fmt.Errorf("failed to create temp file: %v", err)
}
tmpName := tmpFile.Name()
defer os.Remove(tmpName)
code := cgoHeader + "\n\n" + preamble.src
externDecls, err := genExternDeclsByClang(pkg, code, cflags, cgoSymbols)
if err != nil {
return nil, fmt.Errorf("failed to generate extern decls: %v", err)
}
if err = os.WriteFile(tmpName, []byte(code+"\n\n"+externDecls), 0644); err != nil {
return nil, fmt.Errorf("failed to write temp file: %v", err)
}
clFile(ctx, cflags, tmpName, pkg.ExportFile, func(linkFile string) {
cgoLdflags = append(cgoLdflags, linkFile)
}, verbose)
}
for _, ldflag := range ldflags {
cgoLdflags = append(cgoLdflags, safesplit.SplitPkgConfigFlags(ldflag)...)
}
return
}
// clangASTNode represents a node in clang's AST
type clangASTNode struct {
Kind string `json:"kind"`
Name string `json:"name,omitempty"`
Inner []clangASTNode `json:"inner,omitempty"`
}
func genExternDeclsByClang(pkg *aPackage, src string, cflags []string, cgoSymbols map[string]string) (string, error) {
tmpSrc, err := os.CreateTemp("", "cgo-src-*.c")
if err != nil {
return "", fmt.Errorf("failed to create temp file: %v", err)
}
defer os.Remove(tmpSrc.Name())
if err := os.WriteFile(tmpSrc.Name(), []byte(src), 0644); err != nil {
return "", fmt.Errorf("failed to write temp file: %v", err)
}
symbolNames := make(map[string]bool)
if err := getFuncNames(tmpSrc.Name(), cflags, symbolNames); err != nil {
return "", fmt.Errorf("failed to get func names: %v", err)
}
macroNames := make(map[string]bool)
if err := getMacroNames(tmpSrc.Name(), cflags, macroNames); err != nil {
return "", fmt.Errorf("failed to get macro names: %v", err)
}
b := strings.Builder{}
var toRemove []string
for cgoName, symbolName := range cgoSymbols {
if strings.HasPrefix(symbolName, "__cgo_") {
gofuncName := strings.Replace(cgoName, ".__cgo_", ".", 1)
gofn := pkg.LPkg.FuncOf(gofuncName)
cgoVar := pkg.LPkg.VarOf(cgoName)
if gofn != nil {
cgoVar.ReplaceAllUsesWith(gofn.Expr)
} else {
cfuncName := symbolName[len("__cgo_"):]
cfn := pkg.LPkg.NewFunc(cfuncName, types.NewSignatureType(nil, nil, nil, nil, nil, false), llssa.InC)
cgoVar.ReplaceAllUsesWith(cfn.Expr)
}
toRemove = append(toRemove, cgoName)
} else {
usePtr := ""
if symbolNames[symbolName] {
usePtr = "*"
} else if !macroNames[symbolName] {
continue
}
/* template:
typeof(fputs)* _cgo_1574167f3838_Cfunc_fputs;
__attribute__((constructor))
static void _init__cgo_1574167f3838_Cfunc_fputs() {
_cgo_1574167f3838_Cfunc_fputs = fputs;
}*/
b.WriteString(fmt.Sprintf(`
typeof(%s)%s %s;
__attribute__((constructor))
static void _init_%s() {
%s = %s;
}
`,
symbolName, usePtr, cgoName,
cgoName,
cgoName, symbolName))
toRemove = append(toRemove, cgoName)
}
}
for _, funcName := range toRemove {
delete(cgoSymbols, funcName)
}
return b.String(), nil
}
func getMacroNames(file string, cflags []string, macroNames map[string]bool) error {
args := append([]string{"-dM", "-E"}, cflags...)
args = append(args, file)
cmd := exec.Command("clang", args...)
output, err := cmd.Output()
if err != nil {
return err
}
for _, line := range strings.Split(string(output), "\n") {
if strings.HasPrefix(line, "#define ") {
define := strings.TrimPrefix(line, "#define ")
parts := strings.SplitN(define, " ", 2)
if len(parts) > 1 {
macroNames[parts[0]] = true
}
}
}
return nil
}
func getFuncNames(file string, cflags []string, symbolNames map[string]bool) error {
args := append([]string{"-Xclang", "-ast-dump=json", "-fsyntax-only"}, cflags...)
args = append(args, file)
cmd := exec.Command("clang", args...)
cmd.Stderr = os.Stderr
output, err := cmd.Output()
if err != nil {
dump := "dump failed"
if tmpFile, err := os.CreateTemp("", "llgo-clang-ast-dump*.log"); err == nil {
dump = "dump saved to " + tmpFile.Name()
tmpFile.Write(output)
tmpFile.Close()
}
return fmt.Errorf("failed to run clang: %v, %s", err, dump)
}
var astRoot clangASTNode
if err := json.Unmarshal(output, &astRoot); err != nil {
dump := "dump failed"
if tmpFile, err := os.CreateTemp("", "llgo-clang-ast-dump*.log"); err == nil {
dump = "dump saved to " + tmpFile.Name()
tmpFile.Write(output)
tmpFile.Close()
}
return fmt.Errorf("failed to unmarshal AST: %v, %s", err, dump)
}
extractFuncNames(&astRoot, symbolNames)
return nil
}
func extractFuncNames(node *clangASTNode, funcNames map[string]bool) {
for _, inner := range node.Inner {
if inner.Kind == "FunctionDecl" && inner.Name != "" {
funcNames[inner.Name] = true
}
}
}
func parseCgo_(pkg *aPackage, files []*ast.File) (cfiles []string, preambles []cgoPreamble, cdecls []cgoDecl, err error) {
dirs := make(map[string]none)
for _, file := range files {
pos := pkg.Fset.Position(file.Name.NamePos)
dir, _ := filepath.Split(pos.Filename)
dirs[dir] = none{}
}
for dir := range dirs {
matches, err := filepath.Glob(filepath.Join(dir, "*.c"))
if err != nil {
continue
}
for _, match := range matches {
if strings.HasSuffix(match, "_test.c") {
continue
}
if fi, err := os.Stat(match); err == nil && !fi.IsDir() {
cfiles = append(cfiles, match)
}
}
}
for _, file := range files {
for _, decl := range file.Decls {
switch decl := decl.(type) {
case *ast.GenDecl:
if decl.Tok == token.IMPORT {
if doc := decl.Doc; doc != nil && len(decl.Specs) == 1 {
spec := decl.Specs[0].(*ast.ImportSpec)
if spec.Path.Value == "\"unsafe\"" {
pos := pkg.Fset.Position(doc.Pos())
preamble, flags, err := parseCgoPreamble(pos, doc.Text())
if err != nil {
panic(err)
}
preambles = append(preambles, preamble)
cdecls = append(cdecls, flags...)
}
}
}
}
}
}
return
}
func parseCgoPreamble(pos token.Position, text string) (preamble cgoPreamble, decls []cgoDecl, err error) {
b := strings.Builder{}
fline := pos.Line
fname := pos.Filename
b.WriteString(fmt.Sprintf("#line %d %q\n", fline, fname))
for _, line := range strings.Split(text, "\n") {
fline++
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "#cgo ") {
var cgoDecls []cgoDecl
cgoDecls, err = parseCgoDecl(line)
if err != nil {
return
}
decls = append(decls, cgoDecls...)
b.WriteString(fmt.Sprintf("#line %d %q\n", fline, fname))
} else {
b.WriteString(line)
b.WriteString("\n")
}
}
preamble = cgoPreamble{
goFile: pos.Filename,
src: b.String(),
}
return
}
// Parse cgo directive like:
// #cgo pkg-config: python3
// #cgo windows CFLAGS: -IC:/Python312/include
// #cgo windows LDFLAGS: -LC:/Python312/libs -lpython312
// #cgo CFLAGS: -I/usr/include/python3.12
// #cgo LDFLAGS: -L/usr/lib/python3.12/config-3.12-x86_64-linux-gnu -lpython3.12
func parseCgoDecl(line string) (cgoDecls []cgoDecl, err error) {
idx := strings.Index(line, ":")
if idx == -1 {
err = fmt.Errorf("invalid cgo format: %v", line)
return
}
decl := strings.TrimSpace(line[:idx])
arg := strings.TrimSpace(line[idx+1:])
// Split on first space to remove #cgo
parts := strings.SplitN(decl, " ", 2)
if len(parts) < 2 {
err = fmt.Errorf("invalid cgo directive: %v", line)
return
}
// Process remaining part
remaining := strings.TrimSpace(parts[1])
var tag, flag string
// Split on last space to get flag
if lastSpace := strings.LastIndex(remaining, " "); lastSpace != -1 {
tag = strings.TrimSpace(remaining[:lastSpace])
flag = strings.TrimSpace(remaining[lastSpace+1:])
} else {
flag = remaining
}
switch flag {
case "pkg-config":
ldflags, e := exec.Command("pkg-config", "--libs", arg).Output()
if e != nil {
err = fmt.Errorf("pkg-config: %v", e)
return
}
cflags, e := exec.Command("pkg-config", "--cflags", arg).Output()
if e != nil {
err = fmt.Errorf("pkg-config: %v", e)
return
}
cgoDecls = append(cgoDecls, cgoDecl{
tag: tag,
cflags: safesplit.SplitPkgConfigFlags(string(cflags)),
ldflags: safesplit.SplitPkgConfigFlags(string(ldflags)),
})
case "CFLAGS":
cgoDecls = append(cgoDecls, cgoDecl{
tag: tag,
cflags: safesplit.SplitPkgConfigFlags(arg),
})
case "LDFLAGS":
cgoDecls = append(cgoDecls, cgoDecl{
tag: tag,
ldflags: safesplit.SplitPkgConfigFlags(arg),
})
}
return
}

85
internal/build/clean.go Normal file
View File

@@ -0,0 +1,85 @@
/*
* Copyright (c) 2024 The GoPlus Authors (goplus.org). 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 (
"fmt"
"os"
"path"
"path/filepath"
"github.com/goplus/llgo/internal/packages"
)
var (
// TODO(xsw): complete clean flags
cleanFlags = map[string]bool{
"-v": false, // -v: print the paths of packages as they are clean
}
)
func Clean(args []string, conf *Config) {
flags, patterns, verbose := ParseArgs(args, cleanFlags)
cfg := &packages.Config{
Mode: loadSyntax | packages.NeedExportFile,
BuildFlags: flags,
}
if patterns == nil {
patterns = []string{"."}
}
initial, err := packages.LoadEx(nil, nil, cfg, patterns...)
check(err)
cleanPkgs(initial, verbose)
for _, pkg := range initial {
if pkg.Name == "main" {
cleanMainPkg(pkg, conf, verbose)
}
}
}
func cleanMainPkg(pkg *packages.Package, conf *Config, verbose bool) {
pkgPath := pkg.PkgPath
name := path.Base(pkgPath)
fname := name + conf.AppExt
app := filepath.Join(conf.BinPath, fname)
removeFile(app, verbose)
if len(pkg.CompiledGoFiles) > 0 {
dir := filepath.Dir(pkg.CompiledGoFiles[0])
buildApp := filepath.Join(dir, fname)
removeFile(buildApp, verbose)
}
}
func cleanPkgs(initial []*packages.Package, verbose bool) {
packages.Visit(initial, nil, func(p *packages.Package) {
file := p.ExportFile + ".ll"
removeFile(file, verbose)
})
}
func removeFile(file string, verbose bool) {
if _, err := os.Stat(file); os.IsNotExist(err) {
return
}
if verbose {
fmt.Fprintln(os.Stderr, "Remove", file)
}
os.Remove(file)
}

111
internal/build/cmptest.go Normal file
View File

@@ -0,0 +1,111 @@
/*
* Copyright (c) 2024 The GoPlus Authors (goplus.org). 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 (
"bytes"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
)
func cmpTest(dir, pkgPath, llApp string, genExpect bool, runArgs []string) {
var llgoOut, llgoErr bytes.Buffer
var llgoRunErr = runApp(runArgs, dir, &llgoOut, &llgoErr, llApp)
llgoExpect := formatExpect(llgoOut.Bytes(), llgoErr.Bytes(), llgoRunErr)
llgoExpectFile := filepath.Join(dir, "llgo.expect")
if genExpect {
if _, err := os.Stat(llgoExpectFile); !errors.Is(err, os.ErrNotExist) {
fatal(fmt.Errorf("llgo.expect file already exists: %s", llgoExpectFile))
}
if err := os.WriteFile(llgoExpectFile, llgoExpect, 0644); err != nil {
fatal(err)
}
return
}
if b, err := os.ReadFile(llgoExpectFile); err == nil {
checkEqual("llgo.expect", llgoExpect, b)
return
} else if !errors.Is(err, os.ErrNotExist) {
fatal(err)
}
var goOut, goErr bytes.Buffer
var goRunErr = runApp(runArgs, dir, &goOut, &goErr, "go", "run", pkgPath)
checkEqual("output", llgoOut.Bytes(), goOut.Bytes())
checkEqual("stderr", llgoErr.Bytes(), goErr.Bytes())
checkEqualRunErr(llgoRunErr, goRunErr)
}
func formatExpect(stdout, stderr []byte, runErr error) []byte {
var exitCode int
if runErr != nil {
if ee, ok := runErr.(*exec.ExitError); ok {
exitCode = ee.ExitCode()
} else { // This should never happen, but just in case.
exitCode = 255
}
}
return []byte(fmt.Sprintf("#stdout\n%s\n#stderr\n%s\n#exit %d\n", stdout, stderr, exitCode))
}
func checkEqualRunErr(llgoRunErr, goRunErr error) {
if llgoRunErr == goRunErr {
return
}
fmt.Fprintln(os.Stderr, "=> Exit:", llgoRunErr)
fmt.Fprintln(os.Stderr, "\n=> Expected Exit:", goRunErr)
}
func checkEqual(prompt string, a, expected []byte) {
if bytes.Equal(a, expected) {
return
}
fmt.Fprintln(os.Stderr, "=> Result of", prompt)
os.Stderr.Write(a)
fmt.Fprintln(os.Stderr, "\n=> Expected", prompt)
os.Stderr.Write(expected)
fatal(errors.New("checkEqual: unexpected " + prompt))
}
func runApp(runArgs []string, dir string, stdout, stderr io.Writer, app string, args ...string) error {
if len(runArgs) > 0 {
if len(args) > 0 {
args = append(args, runArgs...)
} else {
args = runArgs
}
}
cmd := exec.Command(app, args...)
cmd.Dir = dir
cmd.Stdout = stdout
cmd.Stderr = stderr
return cmd.Run()
}
func fatal(err error) {
log.Panicln(err)
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright (c) 2024 The GoPlus Authors (goplus.org). 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 buildtags
import (
"fmt"
"go/build"
"io"
"io/fs"
"strings"
)
// checkTags checks which build tags are valid by creating virtual test files
// and using build.Context.MatchFile to verify them
func CheckTags(buildFlags []string, testTags map[string]bool) {
buildCtx := build.Default
buildCtx.BuildTags = parseBuildTags(buildFlags)
// Create virtual filesystem
vfs := &virtualFS{
files: make(map[string]virtualFile),
}
// Generate virtual files for each test tag
i := 0
fileToTag := make(map[string]string) // Map to track which file corresponds to which tag
for tag := range testTags {
fileName := fmt.Sprintf("a%02d.go", i)
content := fmt.Sprintf("// +build %s\n\npackage check\n", tag)
vfs.files[fileName] = virtualFile{
name: fileName,
content: content,
dir: ".",
}
fileToTag[fileName] = tag
i++
}
// Override OpenFile to return our virtual file contents
buildCtx.OpenFile = func(name string) (io.ReadCloser, error) {
if file, ok := vfs.files[name]; ok {
return io.NopCloser(strings.NewReader(file.content)), nil
}
return nil, fs.ErrNotExist
}
// Check each file against build context
for fileName, tag := range fileToTag {
match, err := buildCtx.MatchFile(".", fileName)
if err == nil && match {
testTags[tag] = true
}
}
}
// virtualFile represents a virtual build tag check file
type virtualFile struct {
name string
content string
dir string
}
// virtualFS implements a virtual filesystem for build tag checking
type virtualFS struct {
files map[string]virtualFile
}
func parseBuildTags(buildFlags []string) []string {
buildTags := make([]string, 0)
// Extract tags from buildFlags
for i := 0; i < len(buildFlags); i++ {
flag := buildFlags[i]
if flag == "-tags" && i+1 < len(buildFlags) {
// Handle "-tags xxx" format
tags := strings.FieldsFunc(buildFlags[i+1], func(r rune) bool {
return r == ',' || r == ' '
})
buildTags = append(buildTags, tags...)
i++ // Skip the next item since we've processed it
} else if strings.HasPrefix(flag, "-tags=") {
// Handle "-tags=xxx" format
value := strings.TrimPrefix(flag, "-tags=")
tags := strings.FieldsFunc(value, func(r rune) bool {
return r == ',' || r == ' '
})
buildTags = append(buildTags, tags...)
}
}
// Remove duplicates from tags
seen := make(map[string]bool)
uniqueBuildTags := make([]string, 0, len(buildTags))
for _, tag := range buildTags {
if !seen[tag] {
seen[tag] = true
uniqueBuildTags = append(uniqueBuildTags, tag)
}
}
return uniqueBuildTags
}

View File

@@ -0,0 +1,156 @@
//go:build !llgo
// +build !llgo
package buildtags
import (
"reflect"
"runtime"
"testing"
)
func TestCheckTags(t *testing.T) {
tests := []struct {
name string
buildFlags []string
testTags map[string]bool
want map[string]bool
}{
{
name: "mywindows tags",
buildFlags: []string{"-tags", "mywindows"},
testTags: map[string]bool{
"mywindows": false,
"!mywindows": false,
"mywindows,myamd64": false,
},
want: map[string]bool{
"mywindows": true,
"!mywindows": false,
"mywindows,myamd64": runtime.GOARCH == "myamd64",
},
},
{
name: "non-mywindows tags",
buildFlags: []string{"-tags", "mylinux"},
testTags: map[string]bool{
"mywindows": false,
"!mywindows": false,
"mylinux,myamd64": false,
"!mywindows,myamd64": false,
},
want: map[string]bool{
"mywindows": false,
"!mywindows": true,
"mylinux,myamd64": runtime.GOARCH == "myamd64",
"!mywindows,myamd64": runtime.GOARCH == "myamd64",
},
},
{
name: "multiple tags",
buildFlags: []string{"-tags", "mywindows,myamd64"},
testTags: map[string]bool{
"mywindows": false,
"myamd64": false,
"mywindows,myamd64": false,
"mylinux,myamd64": false,
},
want: map[string]bool{
"mywindows": true,
"myamd64": true,
"mywindows,myamd64": true,
"mylinux,myamd64": false,
},
},
{
name: "tags with equals format",
buildFlags: []string{"-tags=mywindows,myamd64"},
testTags: map[string]bool{
"mywindows": false,
"myamd64": false,
"mywindows,myamd64": false,
},
want: map[string]bool{
"mywindows": true,
"myamd64": true,
"mywindows,myamd64": true,
},
},
{
name: "complex tag combinations",
buildFlags: []string{"-tags", "mylinux,myamd64"},
testTags: map[string]bool{
"mywindows": false,
"!mywindows": false,
"mylinux": false,
"mylinux,myamd64": false,
"!mywindows,myamd64": false,
},
want: map[string]bool{
"mywindows": false,
"!mywindows": true,
"mylinux": true,
"mylinux,myamd64": true,
"!mywindows,myamd64": true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testTags := make(map[string]bool)
for k := range tt.testTags {
testTags[k] = false
}
CheckTags(tt.buildFlags, testTags)
if !reflect.DeepEqual(testTags, tt.want) {
t.Errorf("CheckTags() = %v, want %v", testTags, tt.want)
}
})
}
}
func TestParseBuildTags(t *testing.T) {
tests := []struct {
name string
buildFlags []string
want []string
}{
{
name: "space separated tags",
buildFlags: []string{"-tags", "mywindows myamd64"},
want: []string{"mywindows", "myamd64"},
},
{
name: "equals format",
buildFlags: []string{"-tags=mywindows,myamd64"},
want: []string{"mywindows", "myamd64"},
},
{
name: "multiple -tags flags",
buildFlags: []string{"-tags", "mywindows", "-tags", "myamd64"},
want: []string{"mywindows", "myamd64"},
},
{
name: "duplicate tags",
buildFlags: []string{"-tags", "mywindows myamd64", "-tags=mywindows"},
want: []string{"mywindows", "myamd64"},
},
{
name: "empty tags",
buildFlags: []string{},
want: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseBuildTags(tt.buildFlags)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("name: %v, parseBuildTags() = %v, want %v", tt.name, got, tt.want)
}
})
}
}

38
internal/env/build.go vendored Normal file
View File

@@ -0,0 +1,38 @@
/*
* Copyright (c) 2024 The GoPlus Authors (goplus.org). 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 env
import "runtime/debug"
// buildTime is the LLGo binary's build time. It should be set by the linker.
var buildTime string
// BuildTime returns the build time of the running LLGo binary.
func BuildTime() string {
if buildTime != "" {
return buildTime
}
info, ok := debug.ReadBuildInfo()
if ok {
for _, setting := range info.Settings {
if setting.Key == "vcs.time" {
return setting.Value
}
}
}
return ""
}

120
internal/env/env.go vendored Normal file
View File

@@ -0,0 +1,120 @@
package env
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)
const (
LLGoCompilerPkg = "github.com/goplus/llgo"
LLGoRuntimePkgName = "runtime"
LLGoRuntimePkg = LLGoCompilerPkg + "/" + LLGoRuntimePkgName
envFileName = "/internal/env/env.go"
)
func GOROOT() string {
root := os.Getenv("GOROOT")
if root != "" {
return root
}
cmd := exec.Command("go", "env", "GOROOT")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err == nil {
return strings.TrimSpace(out.String())
}
panic("cannot get GOROOT: " + err.Error())
}
func LLGoRuntimeDir() string {
root := LLGoROOT()
if root != "" {
return filepath.Join(root, LLGoRuntimePkgName)
}
return ""
}
func LLGoROOT() string {
llgoRootEnv := os.Getenv("LLGO_ROOT")
if llgoRootEnv != "" {
if root, ok := isLLGoRoot(llgoRootEnv); ok {
return root
}
fmt.Fprintf(os.Stderr, "WARNING: LLGO_ROOT is not a valid LLGO root: %s\n", llgoRootEnv)
}
// Get executable path
exe, err := os.Executable()
if err != nil {
return ""
}
// Resolve any symlinks
exe, err = filepath.EvalSymlinks(exe)
if err != nil {
return ""
}
// Check if parent directory is bin
dir := filepath.Dir(exe)
if filepath.Base(dir) == "bin" {
// Get parent directory of bin
root := filepath.Dir(dir)
if root, ok := isLLGoRoot(root); ok {
return root
}
}
if Devel() {
root, err := getRuntimePkgDirByCaller()
if err != nil {
return ""
}
if root, ok := isLLGoRoot(root); ok {
fmt.Fprintln(os.Stderr, "WARNING: Using LLGO root for devel: "+root)
return root
}
}
return ""
}
func isLLGoRoot(root string) (string, bool) {
if root == "" {
return "", false
}
root, err := filepath.Abs(root)
if err != nil {
return "", false
}
// Check for go.mod
data, err := os.ReadFile(filepath.Join(root, "go.mod"))
if err != nil {
return "", false
}
// Check module name
if !strings.Contains(string(data), "module "+LLGoCompilerPkg+"\n") {
return "", false
}
return root, true
}
func getRuntimePkgDirByCaller() (string, error) {
_, file, _, ok := runtime.Caller(0)
if !ok {
return "", fmt.Errorf("cannot get caller")
}
if !strings.HasSuffix(file, envFileName) {
return "", fmt.Errorf("wrong caller")
}
// check file exists
if _, err := os.Stat(file); os.IsNotExist(err) {
return "", fmt.Errorf("file %s not exists", file)
}
modPath := strings.TrimSuffix(file, envFileName)
if st, err := os.Stat(modPath); os.IsNotExist(err) || !st.IsDir() {
return "", fmt.Errorf("not llgo compiler root: %s", modPath)
}
return modPath, nil
}

183
internal/env/env_test.go vendored Normal file
View File

@@ -0,0 +1,183 @@
//go:build !llgo
// +build !llgo
package env
import (
"os"
"path/filepath"
"testing"
)
func TestGOROOT(t *testing.T) {
// Test with GOROOT environment variable set
t.Run("with GOROOT env", func(t *testing.T) {
origGoRoot := os.Getenv("GOROOT")
defer os.Setenv("GOROOT", origGoRoot)
expected := "/custom/goroot"
os.Setenv("GOROOT", expected)
if got := GOROOT(); got != expected {
t.Errorf("GOROOT() = %v, want %v", got, expected)
}
})
// Test without GOROOT environment variable
t.Run("without GOROOT env", func(t *testing.T) {
origGoRoot := os.Getenv("GOROOT")
defer os.Setenv("GOROOT", origGoRoot)
os.Setenv("GOROOT", "")
if got := GOROOT(); got == "" {
t.Error("GOROOT() should not return empty when using go env")
}
})
}
func TestLLGoRuntimeDir(t *testing.T) {
// Test with valid LLGO_ROOT
t.Run("with valid LLGO_ROOT", func(t *testing.T) {
origLLGoRoot := os.Getenv("LLGO_ROOT")
defer os.Setenv("LLGO_ROOT", origLLGoRoot)
tmpDir := t.TempDir()
goModContent := []byte("module github.com/goplus/llgo\n")
if err := os.WriteFile(filepath.Join(tmpDir, "go.mod"), goModContent, 0644); err != nil {
t.Fatal(err)
}
os.Setenv("LLGO_ROOT", tmpDir)
expected := filepath.Join(tmpDir, LLGoRuntimePkgName)
if got := LLGoRuntimeDir(); got != expected {
t.Errorf("LLGoRuntimeDir() = %v, want %v", got, expected)
}
})
// Test with invalid LLGO_ROOT
t.Run("with invalid LLGO_ROOT", func(t *testing.T) {
origLLGoRoot := os.Getenv("LLGO_ROOT")
defer os.Setenv("LLGO_ROOT", origLLGoRoot)
os.Setenv("LLGO_ROOT", "/nonexistent/path")
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
runtimeDir := filepath.Join(wd, "../../runtime")
if got := LLGoRuntimeDir(); got != runtimeDir {
t.Errorf("LLGoRuntimeDir() = %v, want %v", got, runtimeDir)
}
})
t.Run("devel runtime dir", func(t *testing.T) {
origLLGoRoot := os.Getenv("LLGO_ROOT")
defer os.Setenv("LLGO_ROOT", origLLGoRoot)
os.Setenv("LLGO_ROOT", "")
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
runtimeDir := filepath.Join(wd, "../../runtime")
if got := LLGoRuntimeDir(); got != runtimeDir {
t.Errorf("LLGoRuntimeDir() = %v, want %v", got, runtimeDir)
}
})
}
func TestLLGoROOT(t *testing.T) {
// Test with valid LLGO_ROOT environment variable
t.Run("with valid LLGO_ROOT env", func(t *testing.T) {
origLLGoRoot := os.Getenv("LLGO_ROOT")
defer os.Setenv("LLGO_ROOT", origLLGoRoot)
tmpDir := t.TempDir()
goModContent := []byte("module github.com/goplus/llgo\n")
if err := os.WriteFile(filepath.Join(tmpDir, "go.mod"), goModContent, 0644); err != nil {
t.Fatal(err)
}
os.Setenv("LLGO_ROOT", tmpDir)
if got := LLGoROOT(); got != tmpDir {
t.Errorf("LLGoROOT() = %v, want %v", got, tmpDir)
}
})
// Test with invalid LLGO_ROOT environment variable
t.Run("with invalid LLGO_ROOT env", func(t *testing.T) {
origLLGoRoot := os.Getenv("LLGO_ROOT")
defer os.Setenv("LLGO_ROOT", origLLGoRoot)
os.Setenv("LLGO_ROOT", "/nonexistent/path")
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
rootDir := filepath.Join(wd, "../..")
if got := LLGoROOT(); got != rootDir {
t.Errorf("LLGoROOT() = %v, want %v", got, rootDir)
}
})
// Test with empty LLGO_ROOT environment variable
t.Run("with empty LLGO_ROOT env", func(t *testing.T) {
origLLGoRoot := os.Getenv("LLGO_ROOT")
defer os.Setenv("LLGO_ROOT", origLLGoRoot)
os.Setenv("LLGO_ROOT", "")
// Result depends on executable path, just ensure it doesn't panic
LLGoROOT()
})
}
func TestIsLLGoRoot(t *testing.T) {
// Test with empty root
t.Run("empty root", func(t *testing.T) {
if root, ok := isLLGoRoot(""); ok {
t.Errorf("isLLGoRoot('') = %v, %v, want '', false", root, ok)
}
})
// Test with invalid path
t.Run("invalid path", func(t *testing.T) {
if root, ok := isLLGoRoot(string([]byte{0})); ok {
t.Errorf("isLLGoRoot(invalid) = %v, %v, want '', false", root, ok)
}
})
// Test with non-existent path
t.Run("non-existent path", func(t *testing.T) {
if root, ok := isLLGoRoot("/nonexistent/path"); ok {
t.Errorf("isLLGoRoot(nonexistent) = %v, %v, want '', false", root, ok)
}
})
// Test with valid path but invalid go.mod
t.Run("invalid go.mod", func(t *testing.T) {
tmpDir := t.TempDir()
runtimeDir := filepath.Join(tmpDir, "runtime")
os.MkdirAll(runtimeDir, 0755)
goModContent := []byte("module wrong/module/name\n")
if err := os.WriteFile(filepath.Join(runtimeDir, "go.mod"), goModContent, 0644); err != nil {
t.Fatal(err)
}
if root, ok := isLLGoRoot(tmpDir); ok {
t.Errorf("isLLGoRoot(invalid_mod) = %v, %v, want '', false", root, ok)
}
})
// Test with valid path and valid go.mod
t.Run("valid path and go.mod", func(t *testing.T) {
tmpDir := t.TempDir()
goModContent := []byte("module github.com/goplus/llgo\n")
if err := os.WriteFile(filepath.Join(tmpDir, "go.mod"), goModContent, 0644); err != nil {
t.Fatal(err)
}
absPath, _ := filepath.Abs(tmpDir)
if root, ok := isLLGoRoot(tmpDir); !ok || root != absPath {
t.Errorf("isLLGoRoot(valid) = %v, %v, want %v, true", root, ok, absPath)
}
})
}

48
internal/env/version.go vendored Normal file
View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) 2024 The GoPlus Authors (goplus.org). 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 env
import (
"runtime/debug"
"strings"
)
const (
devel = "(devel)"
)
// buildVersion is the LLGo tree's version string at build time. It should be
// set by the linker.
var buildVersion string
// Version returns the version of the running LLGo binary.
//
//export LLGoVersion
func Version() string {
if buildVersion != "" {
return buildVersion
}
info, ok := debug.ReadBuildInfo()
if ok && info.Main.Version != "" && !strings.HasSuffix(info.Main.Version, "+dirty") {
return info.Main.Version
}
return devel
}
func Devel() bool {
return Version() == devel
}

23
internal/llgen/llgen.go Normal file
View File

@@ -0,0 +1,23 @@
/*
* Copyright (c) 2024 The GoPlus Authors (goplus.org). 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 llgen
func check(err error) {
if err != nil {
panic(err)
}
}

115
internal/llgen/llgenf.go Normal file
View File

@@ -0,0 +1,115 @@
/*
* Copyright (c) 2024 The GoPlus Authors (goplus.org). 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 llgen
import (
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/goplus/llgo/internal/build"
)
func GenFrom(fileOrPkg string) string {
pkg, err := genFrom(fileOrPkg)
check(err)
return pkg.LPkg.String()
}
func genFrom(pkgPath string) (build.Package, error) {
oldDbg := os.Getenv("LLGO_DEBUG")
oldDbgSyms := os.Getenv("LLGO_DEBUG_SYMBOLS")
dbg := isDbgSymEnabled(filepath.Join(pkgPath, "flags.txt"))
if dbg {
os.Setenv("LLGO_DEBUG", "1")
os.Setenv("LLGO_DEBUG_SYMBOLS", "1")
}
defer func() {
os.Setenv("LLGO_DEBUG", oldDbg)
os.Setenv("LLGO_DEBUG_SYMBOLS", oldDbgSyms)
}()
conf := &build.Config{
Mode: build.ModeGen,
AppExt: build.DefaultAppExt(),
}
pkgs, err := build.Do([]string{pkgPath}, conf)
if err != nil {
return nil, err
}
return pkgs[0], nil
}
func DoFile(fileOrPkg, outFile string) {
ret := GenFrom(fileOrPkg)
err := os.WriteFile(outFile, []byte(ret), 0644)
check(err)
}
func isDbgSymEnabled(flagsFile string) bool {
data, err := os.ReadFile(flagsFile)
if err != nil {
return false
}
toks := strings.Split(strings.Join(strings.Split(string(data), "\n"), " "), " ")
for _, tok := range toks {
if tok == "-dbg" {
return true
}
}
return false
}
func SmartDoFile(pkgPath string) {
pkg, err := genFrom(pkgPath)
check(err)
const autgenFile = "llgo_autogen.ll"
dir, _ := filepath.Split(pkg.GoFiles[0])
absDir, _ := filepath.Abs(dir)
absDir = filepath.ToSlash(absDir)
fname := autgenFile
if inCompilerDir(absDir) {
fname = "out.ll"
}
outFile := dir + fname
b, err := os.ReadFile(outFile)
if err == nil && len(b) == 1 && b[0] == ';' {
return // skip to gen
}
if err = os.WriteFile(outFile, []byte(pkg.LPkg.String()), 0644); err != nil {
panic(err)
}
if false && fname == autgenFile {
genZip(absDir, "llgo_autogen.lla", autgenFile)
}
}
func genZip(dir string, outFile, inFile string) {
cmd := exec.Command("zip", outFile, inFile)
cmd.Dir = dir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()
}
func inCompilerDir(dir string) bool {
return strings.Contains(dir, "/cl/_test")
}

View File

@@ -0,0 +1,29 @@
package mockable
import (
"os"
)
var (
exitFunc = os.Exit
exitCode int
)
// EnableMock enables mocking of os.Exit
func EnableMock() {
exitCode = 0
exitFunc = func(code int) {
exitCode = code
panic("exit")
}
}
// Exit calls the current exit function
func Exit(code int) {
exitFunc(code)
}
// ExitCode returns the last exit code from a mocked Exit call
func ExitCode() int {
return exitCode
}

748
internal/packages/load.go Normal file
View File

@@ -0,0 +1,748 @@
/*
* Copyright (c) 2024 The GoPlus Authors (goplus.org). 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 packages
import (
"fmt"
"go/ast"
"go/scanner"
"go/types"
"log"
"os"
"runtime"
"strings"
"sync"
"unsafe"
"golang.org/x/tools/go/packages"
)
// A LoadMode controls the amount of detail to return when loading.
// The bits below can be combined to specify which fields should be
// filled in the result packages.
// The zero value is a special case, equivalent to combining
// the NeedName, NeedFiles, and NeedCompiledGoFiles bits.
// ID and Errors (if present) will always be filled.
// Load may return more information than requested.
type LoadMode = packages.LoadMode
const (
NeedName = packages.NeedName
NeedFiles = packages.NeedFiles
NeedSyntax = packages.NeedSyntax
NeedImports = packages.NeedImports
NeedDeps = packages.NeedDeps
NeedModule = packages.NeedModule
NeedExportFile = packages.NeedExportFile
NeedEmbedFiles = packages.NeedEmbedFiles
NeedEmbedPatterns = packages.NeedEmbedPatterns
NeedCompiledGoFiles = packages.NeedCompiledGoFiles
NeedTypes = packages.NeedTypes
NeedTypesSizes = packages.NeedTypesSizes
NeedTypesInfo = packages.NeedTypesInfo
NeedForTest = packages.NeedForTest
typecheckCgo = NeedModule - 1 // TODO(xsw): how to check
)
const (
DebugPackagesLoad = false
)
// A Config specifies details about how packages should be loaded.
// The zero value is a valid configuration.
// Calls to Load do not modify this struct.
type Config = packages.Config
// A Package describes a loaded Go package.
type Package = packages.Package
// loaderPackage augments Package with state used during the loading phase
type loaderPackage struct {
*Package
importErrors map[string]error // maps each bad import to its error
loadOnce sync.Once
color uint8 // for cycle detection
needsrc bool // load from source (Mode >= LoadTypes)
needtypes bool // type information is either requested or depended on
initial bool // package was matched by a pattern
goVersion int // minor version number of go command on PATH
}
// loader holds the working state of a single call to load.
type loader struct {
pkgs map[string]*loaderPackage
Config
sizes types.Sizes // TODO(xsw): ensure offset of sizes
parseCache map[string]unsafe.Pointer
parseCacheMu sync.Mutex
exportMu sync.Mutex // enforces mutual exclusion of exportdata operations
// Config.Mode contains the implied mode (see impliedLoadMode).
// Implied mode contains all the fields we need the data for.
// In requestedMode there are the actually requested fields.
// We'll zero them out before returning packages to the user.
// This makes it easier for us to get the conditions where
// we need certain modes right.
requestedMode LoadMode
}
type Cached struct {
*packages.Package
Types *types.Package
TypesInfo *types.Info
Syntax []*ast.File
}
type aDeduper struct {
cache sync.Map
setpath func(path string, name string) string
preload func(pkg *types.Package, syntax []*ast.File)
}
type Deduper = *aDeduper
func NewDeduper() Deduper {
return &aDeduper{}
}
func (p Deduper) SetPreload(fn func(pkg *types.Package, syntax []*ast.File)) {
p.preload = fn
}
func (p Deduper) SetPkgPath(fn func(path, name string) string) {
p.setpath = fn
}
func (p Deduper) Check(id string) *Cached {
if v, ok := p.cache.Load(id); ok {
return v.(*Cached)
}
return nil
}
func (p Deduper) set(id string, cp *Cached) {
if DebugPackagesLoad {
log.Println("==> Import", id)
}
p.cache.Store(id, cp)
}
//go:linkname defaultDriver golang.org/x/tools/go/packages.defaultDriver
func defaultDriver(cfg *Config, patterns ...string) (*packages.DriverResponse, bool, error)
//go:linkname newLoader golang.org/x/tools/go/packages.newLoader
func newLoader(cfg *Config) *loader
//go:linkname loadFromExportData golang.org/x/tools/go/packages.(*loader).loadFromExportData
func loadFromExportData(ld *loader, lpkg *loaderPackage) error
//go:linkname parseFiles golang.org/x/tools/go/packages.(*loader).parseFiles
func parseFiles(ld *loader, filenames []string) ([]*ast.File, []error)
//go:linkname typesinternalSetUsesCgo golang.org/x/tools/internal/typesinternal.SetUsesCgo
func typesinternalSetUsesCgo(conf *types.Config) bool
// An importFunc is an implementation of the single-method
// types.Importer interface based on a function value.
type importerFunc func(path string) (*types.Package, error)
func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) }
func loadPackageEx(dedup Deduper, ld *loader, lpkg *loaderPackage) {
if lpkg.PkgPath == "unsafe" {
// Fill in the blanks to avoid surprises.
lpkg.Types = types.Unsafe
lpkg.Fset = ld.Fset
lpkg.Syntax = []*ast.File{}
lpkg.TypesInfo = new(types.Info)
lpkg.TypesSizes = ld.sizes
return
}
if dedup != nil {
if cp := dedup.Check(lpkg.ID); cp != nil {
lpkg.Types = cp.Types
lpkg.Fset = ld.Fset
lpkg.TypesInfo = cp.TypesInfo
lpkg.Syntax = cp.Syntax
lpkg.TypesSizes = ld.sizes
return
}
defer func() {
if !lpkg.IllTyped && lpkg.needtypes && lpkg.needsrc {
dedup.set(lpkg.PkgPath, &Cached{
Package: lpkg.Package,
Types: lpkg.Types,
TypesInfo: lpkg.TypesInfo,
Syntax: lpkg.Syntax,
})
}
}()
if dedup.setpath != nil {
lpkg.PkgPath = dedup.setpath(lpkg.PkgPath, lpkg.Name)
}
}
// Call NewPackage directly with explicit name.
// This avoids skew between golist and go/types when the files'
// package declarations are inconsistent.
lpkg.Types = types.NewPackage(lpkg.PkgPath, lpkg.Name)
lpkg.Fset = ld.Fset
// Start shutting down if the context is done and do not load
// source or export data files.
// Packages that import this one will have ld.Context.Err() != nil.
// ld.Context.Err() will be returned later by refine.
if ld.Context.Err() != nil {
return
}
// Subtle: we populate all Types fields with an empty Package
// before loading export data so that export data processing
// never has to create a types.Package for an indirect dependency,
// which would then require that such created packages be explicitly
// inserted back into the Import graph as a final step after export data loading.
// (Hence this return is after the Types assignment.)
// The Diamond test exercises this case.
if !lpkg.needtypes && !lpkg.needsrc {
return
}
if !lpkg.needsrc {
if err := loadFromExportData(ld, lpkg); err != nil {
lpkg.Errors = append(lpkg.Errors, packages.Error{
Pos: "-",
Msg: err.Error(),
Kind: packages.UnknownError, // e.g. can't find/open/parse export data
})
}
return // not a source package, don't get syntax trees
}
appendError := func(err error) {
// Convert various error types into the one true Error.
var errs []packages.Error
switch err := err.(type) {
case packages.Error:
// from driver
errs = append(errs, err)
case *os.PathError:
// from parser
errs = append(errs, packages.Error{
Pos: err.Path + ":1",
Msg: err.Err.Error(),
Kind: packages.ParseError,
})
case scanner.ErrorList:
// from parser
for _, err := range err {
errs = append(errs, packages.Error{
Pos: err.Pos.String(),
Msg: err.Msg,
Kind: packages.ParseError,
})
}
case types.Error:
// from type checker
lpkg.TypeErrors = append(lpkg.TypeErrors, err)
errs = append(errs, packages.Error{
Pos: err.Fset.Position(err.Pos).String(),
Msg: err.Msg,
Kind: packages.TypeError,
})
default:
// unexpected impoverished error from parser?
errs = append(errs, packages.Error{
Pos: "-",
Msg: err.Error(),
Kind: packages.UnknownError,
})
// If you see this error message, please file a bug.
log.Printf("internal error: error %q (%T) without position", err, err)
}
lpkg.Errors = append(lpkg.Errors, errs...)
}
// If the go command on the PATH is newer than the runtime,
// then the go/{scanner,ast,parser,types} packages from the
// standard library may be unable to process the files
// selected by go list.
//
// There is currently no way to downgrade the effective
// version of the go command (see issue 52078), so we proceed
// with the newer go command but, in case of parse or type
// errors, we emit an additional diagnostic.
//
// See:
// - golang.org/issue/52078 (flag to set release tags)
// - golang.org/issue/50825 (gopls legacy version support)
// - golang.org/issue/55883 (go/packages confusing error)
//
// Should we assert a hard minimum of (currently) go1.16 here?
var runtimeVersion int
if _, err := fmt.Sscanf(runtime.Version(), "go1.%d", &runtimeVersion); err == nil && runtimeVersion < lpkg.goVersion {
defer func() {
if len(lpkg.Errors) > 0 {
appendError(packages.Error{
Pos: "-",
Msg: fmt.Sprintf("This application uses version go1.%d of the source-processing packages but runs version go1.%d of 'go list'. It may fail to process source files that rely on newer language features. If so, rebuild the application using a newer version of Go.", runtimeVersion, lpkg.goVersion),
Kind: packages.UnknownError,
})
}
}()
}
if ld.Config.Mode&NeedTypes != 0 && len(lpkg.CompiledGoFiles) == 0 && lpkg.ExportFile != "" {
// The config requested loading sources and types, but sources are missing.
// Add an error to the package and fall back to loading from export data.
appendError(packages.Error{
Pos: "-",
Msg: fmt.Sprintf("sources missing for package %s", lpkg.ID),
Kind: packages.ParseError,
})
_ = loadFromExportData(ld, lpkg) // ignore any secondary errors
return // can't get syntax trees for this package
}
files, errs := parseFiles(ld, lpkg.CompiledGoFiles)
for _, err := range errs {
appendError(err)
}
lpkg.Syntax = files
if ld.Config.Mode&NeedTypes == 0 {
return
}
// Start shutting down if the context is done and do not type check.
// Packages that import this one will have ld.Context.Err() != nil.
// ld.Context.Err() will be returned later by refine.
if ld.Context.Err() != nil {
return
}
lpkg.TypesInfo = &types.Info{
Types: make(map[ast.Expr]types.TypeAndValue),
Defs: make(map[*ast.Ident]types.Object),
Uses: make(map[*ast.Ident]types.Object),
Implicits: make(map[ast.Node]types.Object),
Instances: make(map[*ast.Ident]types.Instance),
Scopes: make(map[ast.Node]*types.Scope),
Selections: make(map[*ast.SelectorExpr]*types.Selection),
}
lpkg.TypesSizes = ld.sizes
importer := importerFunc(func(path string) (*types.Package, error) {
if path == "unsafe" {
return types.Unsafe, nil
}
// The imports map is keyed by import path.
ipkg := lpkg.Imports[path]
if ipkg == nil {
if err := lpkg.importErrors[path]; err != nil {
return nil, err
}
// There was skew between the metadata and the
// import declarations, likely due to an edit
// race, or because the ParseFile feature was
// used to supply alternative file contents.
return nil, fmt.Errorf("no metadata for %s", path)
}
if ipkg.Types != nil && ipkg.Types.Complete() {
return ipkg.Types, nil
}
log.Fatalf("internal error: package %q without types was imported from %q", path, lpkg)
panic("unreachable")
})
if dedup != nil && dedup.preload != nil {
dedup.preload(lpkg.Types, lpkg.Syntax)
}
// type-check
tc := &types.Config{
Importer: importer,
// Type-check bodies of functions only in initial packages.
// Example: for import graph A->B->C and initial packages {A,C},
// we can ignore function bodies in B.
IgnoreFuncBodies: ld.Mode&NeedDeps == 0 && !lpkg.initial,
Error: appendError,
Sizes: ld.sizes, // may be nil
}
if lpkg.Module != nil && lpkg.Module.GoVersion != "" {
tc.GoVersion = "go" + lpkg.Module.GoVersion
}
if (ld.Mode & typecheckCgo) != 0 {
if !typesinternalSetUsesCgo(tc) {
appendError(packages.Error{
Msg: "typecheckCgo requires Go 1.15+",
Kind: packages.ListError,
})
return
}
}
typErr := types.NewChecker(tc, ld.Fset, lpkg.Types, lpkg.TypesInfo).Files(lpkg.Syntax)
lpkg.importErrors = nil // no longer needed
// In go/types go1.21 and go1.22, Checker.Files failed fast with a
// a "too new" error, without calling tc.Error and without
// proceeding to type-check the package (#66525).
// We rely on the runtimeVersion error to give the suggested remedy.
if typErr != nil && len(lpkg.Errors) == 0 && len(lpkg.Syntax) > 0 {
if msg := typErr.Error(); strings.HasPrefix(msg, "package requires newer Go version") {
appendError(types.Error{
Fset: ld.Fset,
Pos: lpkg.Syntax[0].Package,
Msg: msg,
})
}
}
// If !Cgo, the type-checker uses FakeImportC mode, so
// it doesn't invoke the importer for import "C",
// nor report an error for the import,
// or for any undefined C.f reference.
// We must detect this explicitly and correctly
// mark the package as IllTyped (by reporting an error).
// TODO(adonovan): if these errors are annoying,
// we could just set IllTyped quietly.
if tc.FakeImportC {
outer:
for _, f := range lpkg.Syntax {
for _, imp := range f.Imports {
if imp.Path.Value == `"C"` {
err := types.Error{Fset: ld.Fset, Pos: imp.Pos(), Msg: `import "C" ignored`}
appendError(err)
break outer
}
}
}
}
// If types.Checker.Files had an error that was unreported,
// make sure to report the unknown error so the package is illTyped.
if typErr != nil && len(lpkg.Errors) == 0 {
appendError(typErr)
}
// Record accumulated errors.
illTyped := len(lpkg.Errors) > 0
if !illTyped {
for _, imp := range lpkg.Imports {
if imp.IllTyped {
illTyped = true
break
}
}
}
lpkg.IllTyped = illTyped
}
func loadRecursiveEx(dedup Deduper, ld *loader, lpkg *loaderPackage) {
lpkg.loadOnce.Do(func() {
// Load the direct dependencies, in parallel.
var wg sync.WaitGroup
for _, ipkg := range lpkg.Imports {
imp := ld.pkgs[ipkg.ID]
wg.Add(1)
go func(imp *loaderPackage) {
loadRecursiveEx(dedup, ld, imp)
wg.Done()
}(imp)
}
wg.Wait()
loadPackageEx(dedup, ld, lpkg)
})
}
func refineEx(dedup Deduper, ld *loader, response *packages.DriverResponse) ([]*Package, error) {
roots := response.Roots
rootMap := make(map[string]int, len(roots))
for i, root := range roots {
rootMap[root] = i
}
ld.pkgs = make(map[string]*loaderPackage)
// first pass, fixup and build the map and roots
var initial = make([]*loaderPackage, len(roots))
for _, pkg := range response.Packages {
rootIndex := -1
if i, found := rootMap[pkg.ID]; found {
rootIndex = i
}
// Overlays can invalidate export data.
// TODO(matloob): make this check fine-grained based on dependencies on overlaid files
exportDataInvalid := len(ld.Overlay) > 0 || pkg.ExportFile == "" && pkg.PkgPath != "unsafe"
// This package needs type information if the caller requested types and the package is
// either a root, or it's a non-root and the user requested dependencies ...
needtypes := (ld.Mode&NeedTypes|NeedTypesInfo != 0 && (rootIndex >= 0 || ld.Mode&NeedDeps != 0))
// This package needs source if the call requested source (or types info, which implies source)
// and the package is either a root, or itas a non- root and the user requested dependencies...
needsrc := ((ld.Mode&(NeedSyntax|NeedTypesInfo) != 0 && (rootIndex >= 0 || ld.Mode&NeedDeps != 0)) ||
// ... or if we need types and the exportData is invalid. We fall back to (incompletely)
// typechecking packages from source if they fail to compile.
(ld.Mode&(NeedTypes|NeedTypesInfo) != 0 && exportDataInvalid)) && pkg.PkgPath != "unsafe"
lpkg := &loaderPackage{
Package: pkg,
needtypes: needtypes,
needsrc: needsrc,
goVersion: response.GoVersion,
}
ld.pkgs[lpkg.ID] = lpkg
if rootIndex >= 0 {
initial[rootIndex] = lpkg
lpkg.initial = true
}
}
for i, root := range roots {
if initial[i] == nil {
return nil, fmt.Errorf("root package %v is missing", root)
}
}
if ld.Mode&NeedImports != 0 {
// Materialize the import graph.
const (
white = 0 // new
grey = 1 // in progress
black = 2 // complete
)
// visit traverses the import graph, depth-first,
// and materializes the graph as Packages.Imports.
//
// Valid imports are saved in the Packages.Import map.
// Invalid imports (cycles and missing nodes) are saved in the importErrors map.
// Thus, even in the presence of both kinds of errors,
// the Import graph remains a DAG.
//
// visit returns whether the package needs src or has a transitive
// dependency on a package that does. These are the only packages
// for which we load source code.
var stack []*loaderPackage
var visit func(lpkg *loaderPackage) bool
visit = func(lpkg *loaderPackage) bool {
switch lpkg.color {
case black:
return lpkg.needsrc
case grey:
panic("internal error: grey node")
}
lpkg.color = grey
stack = append(stack, lpkg) // push
stubs := lpkg.Imports // the structure form has only stubs with the ID in the Imports
lpkg.Imports = make(map[string]*Package, len(stubs))
for importPath, ipkg := range stubs {
var importErr error
imp := ld.pkgs[ipkg.ID]
if imp == nil {
// (includes package "C" when DisableCgo)
importErr = fmt.Errorf("missing package: %q", ipkg.ID)
} else if imp.color == grey {
importErr = fmt.Errorf("import cycle: %s", stack)
}
if importErr != nil {
if lpkg.importErrors == nil {
lpkg.importErrors = make(map[string]error)
}
lpkg.importErrors[importPath] = importErr
continue
}
if visit(imp) {
lpkg.needsrc = true
}
lpkg.Imports[importPath] = imp.Package
}
// Complete type information is required for the
// immediate dependencies of each source package.
if lpkg.needsrc && ld.Mode&NeedTypes != 0 {
for _, ipkg := range lpkg.Imports {
ld.pkgs[ipkg.ID].needtypes = true
}
}
// NeedTypeSizes causes TypeSizes to be set even
// on packages for which types aren't needed.
if ld.Mode&NeedTypesSizes != 0 {
lpkg.TypesSizes = ld.sizes
}
stack = stack[:len(stack)-1] // pop
lpkg.color = black
return lpkg.needsrc
}
// For each initial package, create its import DAG.
for _, lpkg := range initial {
visit(lpkg)
}
} else {
// !NeedImports: drop the stub (ID-only) import packages
// that we are not even going to try to resolve.
for _, lpkg := range initial {
lpkg.Imports = nil
}
}
// Load type data and syntax if needed, starting at
// the initial packages (roots of the import DAG).
if ld.Mode&NeedTypes != 0 || ld.Mode&NeedSyntax != 0 {
var wg sync.WaitGroup
for _, lpkg := range initial {
wg.Add(1)
go func(lpkg *loaderPackage) {
loadRecursiveEx(dedup, ld, lpkg)
wg.Done()
}(lpkg)
}
wg.Wait()
}
// If the context is done, return its error and
// throw out [likely] incomplete packages.
if err := ld.Context.Err(); err != nil {
return nil, err
}
result := make([]*Package, len(initial))
for i, lpkg := range initial {
result[i] = lpkg.Package
}
for i := range ld.pkgs {
// Clear all unrequested fields,
// to catch programs that use more than they request.
if ld.requestedMode&NeedName == 0 {
ld.pkgs[i].Name = ""
ld.pkgs[i].PkgPath = ""
}
if ld.requestedMode&NeedFiles == 0 {
ld.pkgs[i].GoFiles = nil
ld.pkgs[i].OtherFiles = nil
ld.pkgs[i].IgnoredFiles = nil
}
if ld.requestedMode&NeedEmbedFiles == 0 {
ld.pkgs[i].EmbedFiles = nil
}
if ld.requestedMode&NeedEmbedPatterns == 0 {
ld.pkgs[i].EmbedPatterns = nil
}
if ld.requestedMode&NeedCompiledGoFiles == 0 {
ld.pkgs[i].CompiledGoFiles = nil
}
if ld.requestedMode&NeedImports == 0 {
ld.pkgs[i].Imports = nil
}
if ld.requestedMode&NeedExportFile == 0 {
ld.pkgs[i].ExportFile = ""
}
if ld.requestedMode&NeedTypes == 0 {
ld.pkgs[i].Types = nil
ld.pkgs[i].Fset = nil
ld.pkgs[i].IllTyped = false
}
if ld.requestedMode&NeedSyntax == 0 {
ld.pkgs[i].Syntax = nil
}
if ld.requestedMode&NeedTypesInfo == 0 {
ld.pkgs[i].TypesInfo = nil
}
if ld.requestedMode&NeedTypesSizes == 0 {
ld.pkgs[i].TypesSizes = nil
}
if ld.requestedMode&NeedModule == 0 {
ld.pkgs[i].Module = nil
}
}
return result, nil
}
// LoadEx loads and returns the Go packages named by the given patterns.
//
// Config specifies loading options;
// nil behaves the same as an empty Config.
//
// If any of the patterns was invalid as defined by the
// underlying build system, Load returns an error.
// It may return an empty list of packages without an error,
// for instance for an empty expansion of a valid wildcard.
// Errors associated with a particular package are recorded in the
// corresponding Package's Errors list, and do not cause Load to
// return an error. Clients may need to handle such errors before
// proceeding with further analysis. The PrintErrors function is
// provided for convenient display of all errors.
func LoadEx(dedup Deduper, sizes func(sizes types.Sizes, compiler, arch string) types.Sizes, cfg *Config, patterns ...string) ([]*Package, error) {
ld := newLoader(cfg)
response, external, err := defaultDriver(&ld.Config, patterns...)
if err != nil {
return nil, err
}
ld.sizes = types.SizesFor(response.Compiler, response.Arch)
if ld.sizes == nil && ld.Config.Mode&(NeedTypes|NeedTypesSizes|NeedTypesInfo) != 0 {
// Type size information is needed but unavailable.
if external {
// An external driver may fail to populate the Compiler/GOARCH fields,
// especially since they are relatively new (see #63700).
// Provide a sensible fallback in this case.
ld.sizes = types.SizesFor("gc", runtime.GOARCH)
if ld.sizes == nil { // gccgo-only arch
ld.sizes = types.SizesFor("gc", "amd64")
}
} else {
// Go list should never fail to deliver accurate size information.
// Reject the whole Load since the error is the same for every package.
return nil, fmt.Errorf("can't determine type sizes for compiler %q on GOARCH %q",
response.Compiler, response.Arch)
}
}
if sizes != nil {
ld.sizes = sizes(ld.sizes, response.Compiler, response.Arch)
}
return refineEx(dedup, ld, response)
}
// Visit visits all the packages in the import graph whose roots are
// pkgs, calling the optional pre function the first time each package
// is encountered (preorder), and the optional post function after a
// package's dependencies have been visited (postorder).
// The boolean result of pre(pkg) determines whether
// the imports of package pkg are visited.
//
//go:linkname Visit golang.org/x/tools/go/packages.Visit
func Visit(pkgs []*Package, pre func(*Package) bool, post func(*Package))

109
internal/typepatch/patch.go Normal file
View File

@@ -0,0 +1,109 @@
/*
* Copyright (c) 2024 The GoPlus Authors (goplus.org). 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 typepatch
import (
"go/token"
"go/types"
"unsafe"
)
type typesPackage struct {
path string
name string
scope *types.Scope
imports []*types.Package
complete bool
fake bool // scope lookup errors are silently dropped if package is fake (internal use only)
cgo bool // uses of this package will be rewritten into uses of declarations from _cgo_gotypes.go
goVersion string // minimum Go version required for package (by Config.GoVersion, typically from go.mod)
}
type typesScope struct {
parent *types.Scope
children []*types.Scope
number int
elems map[string]types.Object // TODO(xsw): ensure offset of elems
pos, end token.Pos
comment string
isFunc bool
}
const (
tagPatched = 0x17
)
func IsPatched(pkg *types.Package) bool {
if pkg == nil {
return false
}
p := (*typesPackage)(unsafe.Pointer(pkg))
return *(*uint8)(unsafe.Pointer(&p.complete)) == tagPatched
}
func setPatched(pkg *types.Package) {
p := (*typesPackage)(unsafe.Pointer(pkg))
*(*uint8)(unsafe.Pointer(&p.complete)) = tagPatched
}
func setScope(pkg *types.Package, scope *types.Scope) {
p := (*typesPackage)(unsafe.Pointer(pkg))
p.scope = scope
}
func getElems(scope *types.Scope) map[string]types.Object {
s := (*typesScope)(unsafe.Pointer(scope))
return s.elems
}
func setElems(scope *types.Scope, elems map[string]types.Object) {
s := (*typesScope)(unsafe.Pointer(scope))
s.elems = elems
}
func Clone(alt *types.Package) *types.Package {
ret := *alt
return &ret
}
func Merge(alt, pkg *types.Package, skips map[string]struct{}, skipall bool) {
setPatched(pkg)
if skipall {
return
}
scope := *alt.Scope()
old := getElems(&scope)
elems := make(map[string]types.Object, len(old))
for name, o := range old {
elems[name] = o
}
setElems(&scope, elems)
setScope(alt, &scope)
for name, o := range getElems(pkg.Scope()) {
if _, ok := elems[name]; ok {
continue
}
if _, ok := skips[name]; ok {
continue
}
elems[name] = o
}
}