Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added _pydemo/callpy/callpy
Binary file not shown.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ require (
)

replace github.com/goplus/llgo/runtime => ./runtime

replace github.com/goplus/llgo/runtimeext/pyenvrt => ./runtimeext/pyenvrt
80 changes: 76 additions & 4 deletions internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
"github.com/goplus/llgo/internal/env"
"github.com/goplus/llgo/internal/mockable"
"github.com/goplus/llgo/internal/packages"
"github.com/goplus/llgo/internal/pyenv"
"github.com/goplus/llgo/internal/typepatch"
"github.com/goplus/llgo/ssa/abi"
"github.com/goplus/llgo/xtool/clang"
Expand Down Expand Up @@ -160,7 +161,6 @@ func Do(args []string, conf *Config) ([]Package, error) {
if err != nil {
return nil, fmt.Errorf("failed to setup crosscompile: %w", err)
}

// Update GOOS/GOARCH from export if target was used
if conf.Target != "" && export.GOOS != "" {
conf.Goos = export.GOOS
Expand Down Expand Up @@ -251,6 +251,14 @@ func Do(args []string, conf *Config) ([]Package, error) {
altPkgs, err := packages.LoadEx(dedup, sizes, cfg, altPkgPaths...)
check(err)

rootDir := filepath.Dir(env.LLGoRuntimeDir())
cfgExt := *cfg
cfgExt.Dir = rootDir
altExtPkgs, err := packages.LoadEx(dedup, sizes, &cfgExt, "github.com/goplus/llgo/runtimeext/pyenvrt")
check(err)

altPkgs = append(altPkgs, altExtPkgs...)

noRt := 1
prog.SetRuntime(func() *types.Package {
noRt = 0
Expand Down Expand Up @@ -286,6 +294,7 @@ func Do(args []string, conf *Config) ([]Package, error) {
isCheckLinkArgsEnabled: IsCheckLinkArgsEnabled(),
cTransformer: cabi.NewTransformer(prog, conf.AbiMode),
}

pkgs, err := buildAllPkgs(ctx, initial, verbose)
check(err)
if mode == ModeGen {
Expand All @@ -308,7 +317,6 @@ func Do(args []string, conf *Config) ([]Package, error) {

global, err := createGlobals(ctx, ctx.prog, pkgs)
check(err)

for _, pkg := range initial {
if needLink(pkg, mode) {
linkMainPkg(ctx, pkg, allPkgs, global, conf, mode, verbose)
Expand Down Expand Up @@ -449,6 +457,11 @@ func buildAllPkgs(ctx *context, initial []*packages.Package, verbose bool) (pkgs
expdArgs := make([]string, 0, len(altParts))
for _, param := range altParts {
param = strings.TrimSpace(param)
if param == "$LLGO_LIB_PYTHON" {
_ = pyenv.EnsureWithFetch("")
_ = pyenv.EnsureBuildEnv()
_ = pyenv.Verify()
}
if strings.ContainsRune(param, '$') {
expdArgs = append(expdArgs, xenv.ExpandEnvToArgs(param)...)
ctx.nLibdir++
Expand Down Expand Up @@ -476,6 +489,7 @@ func buildAllPkgs(ctx *context, initial []*packages.Package, verbose bool) (pkgs
ctx.nLibdir++
}
}

if ctx.isCheckLinkArgsEnabled {
if err := ctx.compiler().CheckLinkArgs(pkgLinkArgs, isWasmTarget(ctx.buildConf.Goos)); 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))
Expand Down Expand Up @@ -595,6 +609,49 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l
}
}
})

// Heuristic: if link args reference python libs, force python init
if !needPyInit {
for _, arg := range linkArgs {
if strings.Contains(arg, "python") {
needPyInit = true
break
}
}
}

// Ensure python runtime cache when python initialization is needed
if needPyInit {
// Prepare Python environment for build/link
_ = pyenv.Ensure()
_ = pyenv.EnsureBuildEnv()
_ = pyenv.Verify()
// On macOS, avoid absolute rpath: copy libpython near the executable and use @executable_path
if ctx.buildConf.Goos == "darwin" {
if pyHome := pyenv.PythonHome(); pyHome != "" {
// Place copied libs next to the binary: <appDir>/.llgo/lib
appDir := filepath.Dir(app)
appLibDir := filepath.Join(appDir, ".llgo", "lib")
_ = os.MkdirAll(appLibDir, 0755)
// Copy libpython*.dylib into appLibDir
if matches, _ := filepath.Glob(filepath.Join(pyHome, "lib", "libpython*.dylib")); len(matches) > 0 {
src := matches[0]
dst := filepath.Join(appLibDir, filepath.Base(src))
if data, err := os.ReadFile(src); err == nil {
_ = os.WriteFile(dst, data, 0644)
}
// Normalize copied lib's install name to @rpath/<basename>
_ = exec.Command("install_name_tool", "-id", "@rpath/"+filepath.Base(dst), dst).Run()
}
// Add relative rpath
relRPath := "-Wl,-rpath,@executable_path/.llgo/lib"
if !slices.Contains(linkArgs, relRPath) {
linkArgs = append(linkArgs, relRPath)
}
}
}
}

entryObjFile, err := genMainModuleFile(ctx, llssa.PkgRuntime, pkg, needRuntime, needPyInit)
check(err)
// defer os.Remove(entryLLFile)
Expand All @@ -609,6 +666,14 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l
err = linkObjFiles(ctx, app, objFiles, linkArgs, verbose)
check(err)

// After linking, ensure the executable references libpython via @rpath
if needPyInit && ctx.buildConf.Goos == "darwin" {
if dep := findDylibDep(app, "python3"); dep != "" && !strings.HasPrefix(dep, "@rpath") {
base := filepath.Base(dep)
_ = exec.Command("install_name_tool", "-change", dep, "@rpath/"+base, app).Run()
}
}

switch mode {
case ModeTest:
cmd := exec.Command(app, conf.RunArgs...)
Expand Down Expand Up @@ -687,17 +752,22 @@ func isWasmTarget(goos string) bool {

func genMainModuleFile(ctx *context, rtPkgPath string, pkg *packages.Package, needRuntime, needPyInit bool) (path string, err error) {
var (
pyEnvDecl string
pyEnvInit string
pyInitDecl string
pyInit string
rtInitDecl string
rtInit string
)
fmt.Print(rtPkgPath)
mainPkgPath := pkg.PkgPath
if needRuntime {
rtInit = "call void @\"" + rtPkgPath + ".init\"()"
rtInitDecl = "declare void @\"" + rtPkgPath + ".init\"()"
}
if needPyInit {
pyEnvDecl = "declare void @\"github.com/goplus/llgo/runtimeext/pyenvrt.init\"()"
pyEnvInit = "call void @\"github.com/goplus/llgo/runtimeext/pyenvrt.init\"()"
pyInit = "call void @Py_Initialize()"
pyInitDecl = "declare void @Py_Initialize()"
}
Expand Down Expand Up @@ -750,6 +820,7 @@ source_filename = "main"
%s
%s
%s
%s
declare void @"%s.init"()
declare void @"%s.main"()
define weak void @runtime.init() {
Expand All @@ -770,15 +841,16 @@ _llgo_0:
%s
%s
%s
%s
call void @runtime.init()
call void @"%s.init"()
call void @"%s.main"()
ret i32 0
}
`, declSizeT, stdioDecl,
pyInitDecl, rtInitDecl, mainPkgPath, mainPkgPath,
pyEnvDecl, pyInitDecl, rtInitDecl, mainPkgPath, mainPkgPath,
startDefine, mainDefine, stdioNobuf,
pyInit, rtInit, mainPkgPath, mainPkgPath)
pyEnvInit, pyInit, rtInit, mainPkgPath, mainPkgPath)

return exportObject(ctx, pkg.PkgPath+".main", pkg.ExportFile+"-main", []byte(mainCode))
}
Expand Down
127 changes: 127 additions & 0 deletions internal/pyenv/fetch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package pyenv

import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
)

func downloadAndExtract(url, dir string) (err error) {
if _, err = os.Stat(dir); err == nil {
os.RemoveAll(dir)
}
tempDir := dir + ".temp"
os.RemoveAll(tempDir)
if err = os.MkdirAll(tempDir, 0755); err != nil {
return fmt.Errorf("failed to create temporary directory: %w", err)
}

urlPath := strings.Split(url, "/")
filename := urlPath[len(urlPath)-1]
localFile := filepath.Join(tempDir, filename)
if err = downloadFile(url, localFile); err != nil {
return fmt.Errorf("failed to download file: %w", err)
}
defer os.Remove(localFile)

if strings.HasSuffix(filename, ".tar.gz") || strings.HasSuffix(filename, ".tgz") {
err = extractTarGz(localFile, tempDir)
} else {
return fmt.Errorf("unsupported archive format: %s", filename)
}
if err != nil {
return fmt.Errorf("failed to extract archive: %w", err)
}
if err = os.Rename(tempDir, dir); err != nil {
return fmt.Errorf("failed to rename directory: %w", err)
}
return nil
}

func downloadFile(url, filepath string) error {
out, err := os.Create(filepath)
if err != nil {
return err
}
defer out.Close()
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad status: %s", resp.Status)
}
_, err = io.Copy(out, resp.Body)
return err
}

func extractTarGz(tarGzFile, dest string) error {
file, err := os.Open(tarGzFile)
if err != nil {
return err
}
defer file.Close()
gzr, err := gzip.NewReader(file)
if err != nil {
return err
}
defer gzr.Close()
tr := tar.NewReader(gzr)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
target := filepath.Join(dest, header.Name)
if !strings.HasPrefix(target, filepath.Clean(dest)+string(os.PathSeparator)) {
return fmt.Errorf("%s: illegal file path", target)
}
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(target, 0755); err != nil {
return err
}
case tar.TypeReg:
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
return err
}
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
if err != nil {
return err
}
if _, err := io.Copy(f, tr); err != nil {
f.Close()
return err
}
f.Close()
case tar.TypeSymlink:
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
return err
}
_ = os.Remove(target)
if err := os.Symlink(header.Linkname, target); err != nil {
return err
}
case tar.TypeLink:
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
return err
}
_ = os.Remove(target)
linkTarget := filepath.Join(dest, header.Linkname)
if err := os.Link(linkTarget, target); err != nil {
return err
}

}
}
return nil
}
Loading
Loading