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
34 changes: 30 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 @@ -595,6 +608,12 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l
}
}
})

// Ensure python runtime cache when python initialization is needed
if needPyInit {

}

entryObjFile, err := genMainModuleFile(ctx, llssa.PkgRuntime, pkg, needRuntime, needPyInit)
check(err)
// defer os.Remove(entryLLFile)
Expand Down Expand Up @@ -687,17 +706,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 +774,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 +795,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
}
128 changes: 128 additions & 0 deletions internal/pyenv/pybuild.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package pyenv

import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"

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

// EnsureBuildEnv ensures the Python build environment by:
// - ensuring the cache directory {LLGoCacheDir}/python_env exists (atomic creation)
// - applying PATH/PYTHONHOME/DYLD_LIBRARY_PATH/PKG_CONFIG_PATH
// Priority: use LLPYG_PYHOME if set; otherwise default to the cache path.
func EnsureBuildEnv() error {
if err := Ensure(); err != nil {
return err
}
pyHome := getPyHome(filepath.Join(env.LLGoCacheDir(), "python_env", "python"))
return applyEnv(pyHome)
}

// Verify runs a lightweight check to ensure a usable Python is available
// in current environment. It tries to execute: python -c "import sys; print('OK')".
func Verify() error {
exe, err := findPythonExec()
if err != nil {
return err
}
cmd := exec.Command(exe, "-c", "import sys; print('OK')")
cmd.Stdout, cmd.Stderr = nil, nil
return cmd.Run()
}

// PythonHome returns the path that should be used as PYTHONHOME,
// preferring LLPYG_PYHOME if set; otherwise defaulting to the llgo cache path.
func PythonHome() string {
return getPyHome(filepath.Join(env.LLGoCacheDir(), "python_env", "python"))
}

func findPythonExec() (string, error) {
if p, err := exec.LookPath("python"); err == nil {
return p, nil
}
if p, err := exec.LookPath("python3"); err == nil {
return p, nil
}
return "", exec.ErrNotFound
}

func getPyHome(defaultPath string) string {
if v := os.Getenv("LLPYG_PYHOME"); v != "" {
return v
}
return defaultPath
}

func applyEnv(pyHome string) error {
if pyHome == "" {
return nil
}
bin := filepath.Join(pyHome, "bin")
lib := filepath.Join(pyHome, "lib")

// PATH: prepend pyHome/bin if not present
path := os.Getenv("PATH")
parts := strings.Split(path, string(os.PathListSeparator))
hasBin := false
for _, p := range parts {
if p == bin {
hasBin = true
break
}
}
if !hasBin {
newPath := bin
if path != "" {
newPath += string(os.PathListSeparator) + path
}
if err := os.Setenv("PATH", newPath); err != nil {
return err
}
}

// PYTHONHOME
if err := os.Setenv("PYTHONHOME", pyHome); err != nil {
return err
}

// macOS: DYLD_LIBRARY_PATH append lib if missing
if runtime.GOOS == "darwin" {
dyld := os.Getenv("DYLD_LIBRARY_PATH")
if dyld == "" {
if err := os.Setenv("DYLD_LIBRARY_PATH", lib); err != nil {
return err
}
} else if !strings.Contains(dyld, lib) {
if err := os.Setenv("DYLD_LIBRARY_PATH", lib+string(os.PathListSeparator)+dyld); err != nil {
return err
}
}
}

// PKG_CONFIG_PATH: add pyHome/lib/pkgconfig
pkgcfg := filepath.Join(pyHome, "lib", "pkgconfig")
pcp := os.Getenv("PKG_CONFIG_PATH")
if pcp == "" {
_ = os.Setenv("PKG_CONFIG_PATH", pkgcfg)
} else {
parts := strings.Split(pcp, string(os.PathListSeparator))
found := false
for _, p := range parts {
if p == pkgcfg {
found = true
break
}
}
if !found {
_ = os.Setenv("PKG_CONFIG_PATH", pkgcfg+string(os.PathListSeparator)+pcp)
}
}

// Avoid interference from custom PYTHONPATH
_ = os.Unsetenv("PYTHONPATH")
return nil
}
Loading
Loading