move out c/cpp/py
This commit is contained in:
922
internal/build/build.go
Normal file
922
internal/build/build.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
66
internal/build/build_test.go
Normal file
66
internal/build/build_test.go
Normal 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
413
internal/build/cgo.go
Normal 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
85
internal/build/clean.go
Normal 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
111
internal/build/cmptest.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user