diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..81177dc --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,36 @@ +name: Check + +on: + push: + branches: [ "*" ] + pull_request: + branches: [ "*" ] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.23' + + - name: Install embedme + run: npm install -g embedme + + - name: Verify README.md embedded code + run: npx embedme --verify README.md + + - name: Check formatting + run: | + if [ -n "$(go fmt ./...)" ]; then + echo "Some files are not properly formatted. Please run 'go fmt ./...'" + exit 1 + fi diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 890f7e2..c716ec8 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -10,57 +10,20 @@ on: branches: [ "main" ] jobs: - check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Install embedme - run: npm install -g embedme - - - name: Verify README.md embedded code - run: npx embedme --verify README.md - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.23' - - - name: Check formatting - run: | - if [ -n "$(go fmt ./...)" ]; then - echo "Some files are not properly formatted. Please run 'go fmt ./...'" - exit 1 - fi - build: continue-on-error: true strategy: fail-fast: false matrix: - sys: - - {os: macos-latest, shell: bash} - - {os: ubuntu-24.04, shell: bash} - - {os: windows-latest, shell: bash} + os: + - macos-latest + - ubuntu-24.04 + - windows-latest defaults: run: - shell: ${{ matrix.sys.shell }} - runs-on: ${{matrix.sys.os}} + shell: bash + runs-on: ${{matrix.os}} steps: - # - uses: msys2/setup-msys2@v2 - # if: matrix.sys.os == 'windows-latest' - # with: - # update: true - # install: >- - # curl - # git - # pkg-config - - uses: actions/checkout@v4 - name: Set up Go @@ -72,15 +35,15 @@ jobs: with: python-version: '3.13' update-environment: true - + - name: Generate Python pkg-config for windows (patch) - if: matrix.sys.os == 'windows-latest' + if: matrix.os == 'windows-latest' run: | mkdir -p $PKG_CONFIG_PATH cp .github/assets/python3-embed.pc $PKG_CONFIG_PATH/ - + - name: Install tiny-pkg-config for windows (patch) - if: matrix.sys.os == 'windows-latest' + if: matrix.os == 'windows-latest' run: | set -x curl -L https://github.com/cpunion/tiny-pkg-config/releases/download/v0.2.0/tiny-pkg-config_Windows_x86_64.zip -o /tmp/tiny-pkg-config.zip @@ -96,16 +59,6 @@ jobs: - name: Test with coverage run: go test -coverprofile=coverage.txt -covermode=atomic ./... - - - name: Test gopy - run: | - set -x - gopy init $HOME/foo - cd $HOME/foo - gopy build -v . - export GP_INJECT_DEBUG=1 - gopy run -v . - gopy install -v . - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 diff --git a/.github/workflows/gopy.yml b/.github/workflows/gopy.yml new file mode 100644 index 0000000..5988d84 --- /dev/null +++ b/.github/workflows/gopy.yml @@ -0,0 +1,102 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Gopy + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + test-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.23 + + - name: Install gopy + run: go install ./cmd/gopy + + - name: Test init project + run: gopy init ../foo + + - name: Test build project + env: + GP_INJECT_DEBUG: "1" + run: | + Set-PSDebug -Trace 2 + cd ../foo + dir .deps/python/lib/pkgconfig + gopy build -o foo.exe . + gopy exec dir + $env:PATH=".deps/python;$env:PATH" + $env:PATH + ./foo.exe + + - name: Test run project + env: + GP_INJECT_DEBUG: "1" + run: | + cd ../foo + gopy run -v . + + - name: Test install project + run: | + cd ../foo + gopy install -v . + + test: + continue-on-error: true + strategy: + fail-fast: false + matrix: + os: + - macos-latest + - ubuntu-24.04 + defaults: + run: + shell: bash + runs-on: ${{matrix.os}} + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.23 + + - name: Install gopy + run: go install ./cmd/gopy + + - name: Test init project + run: gopy init $HOME/foo + + - name: Test build project + env: + GP_INJECT_DEBUG: "1" + run: | + set -x + cd $HOME/foo + gopy exec env + ls $HOME/foo/.deps/python/lib/pkgconfig + gopy build -o foo . + gopy exec ls -lh + ./foo + + - name: Test run project + env: + GP_INJECT_DEBUG: "1" + run: | + cd $HOME/foo + gopy run -v . + + - name: Test install project + run: | + cd $HOME/foo + gopy install -v . diff --git a/cmd/build.go b/cmd/build.go index 902a3a5..9f29cf5 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -25,7 +25,7 @@ var buildCmd = &cobra.Command{ }(), DisableFlagParsing: true, Run: func(cmd *cobra.Command, args []string) { - if err := rungo.RunGoCommand("build", args); err != nil { + if err := rungo.RunCommand("go", append([]string{"build"}, args...)); err != nil { fmt.Fprintf(os.Stderr, "Error: %s\n", err) os.Exit(1) } diff --git a/cmd/exec.go b/cmd/exec.go new file mode 100644 index 0000000..5e58a04 --- /dev/null +++ b/cmd/exec.go @@ -0,0 +1,34 @@ +/* +Copyright © 2024 NAME HERE +*/ +package cmd + +import ( + "fmt" + "os" + + "github.com/cpunion/go-python/cmd/internal/rungo" + "github.com/spf13/cobra" +) + +// execCmd represents the run command +var execCmd = &cobra.Command{ + Use: "exec [flags] [arguments...]", + Short: "Exec command with the Go and Python environment properly configured", + Long: "Exec executes a command with the Go and Python environment properly configured.", + DisableFlagParsing: true, + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + cmd.Help() + return + } + if err := rungo.RunCommand(args[0], args[1:]); err != nil { + fmt.Fprintf(os.Stderr, "Error: %s\n", err) + os.Exit(1) + } + }, +} + +func init() { + rootCmd.AddCommand(execCmd) +} diff --git a/cmd/install.go b/cmd/install.go index b7a8497..3c1a5e4 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -25,7 +25,7 @@ var installCmd = &cobra.Command{ }(), DisableFlagParsing: true, Run: func(cmd *cobra.Command, args []string) { - if err := rungo.RunGoCommand("install", args); err != nil { + if err := rungo.RunCommand("go", append([]string{"install"}, args...)); err != nil { fmt.Println("Error:", err) os.Exit(1) } diff --git a/cmd/internal/create/templates/main.go b/cmd/internal/create/templates/main.go index 21d7b2f..089e40f 100644 --- a/cmd/internal/create/templates/main.go +++ b/cmd/internal/create/templates/main.go @@ -7,4 +7,5 @@ import ( func main() { Initialize() defer Finalize() + println("Hello, World!") } diff --git a/cmd/internal/install/deps.go b/cmd/internal/install/deps.go index 36c3481..f4fc57c 100644 --- a/cmd/internal/install/deps.go +++ b/cmd/internal/install/deps.go @@ -36,19 +36,6 @@ func Dependencies(projectPath string, goVersion, tinyPkgConfigVersion, pyVersion return err } - if runtime.GOOS == "windows" { - pythonPath := env.GetPythonRoot(projectPath) - pkgConfigDir := env.GetPythonPkgConfigDir(projectPath) - if err := generatePkgConfig(pythonPath, pkgConfigDir); err != nil { - return err - } - } - - // Update pkg-config files - if err := updatePkgConfig(projectPath); err != nil { - return err - } - return nil } diff --git a/cmd/internal/install/python.go b/cmd/internal/install/python.go index 992658d..7644036 100644 --- a/cmd/internal/install/python.go +++ b/cmd/internal/install/python.go @@ -106,10 +106,6 @@ func getPythonURL(version, buildDate, arch, os string, freeThreaded, debug bool) // updateMacOSDylibs updates the install names of dylib files on macOS func updateMacOSDylibs(pythonDir string, verbose bool) error { - if runtime.GOOS != "darwin" { - return nil - } - libDir := filepath.Join(pythonDir, "lib") entries, err := os.ReadDir(libDir) if err != nil { @@ -159,14 +155,15 @@ func updateMacOSDylibs(pythonDir string, verbose bool) error { return nil } -// generatePkgConfig generates pkg-config files for Windows -func generatePkgConfig(pythonPath, pkgConfigDir string) error { +// genWinPyPkgConfig generates pkg-config files for Windows +func genWinPyPkgConfig(pythonRoot, pkgConfigDir string) error { + fmt.Printf("Generating pkg-config files in %s\n", pkgConfigDir) if err := os.MkdirAll(pkgConfigDir, 0755); err != nil { return fmt.Errorf("failed to create pkgconfig directory: %v", err) } // Get Python environment - pyEnv := env.NewPythonEnv(pythonPath) + pyEnv := env.NewPythonEnv(pythonRoot) pythonBin, err := pyEnv.Python() if err != nil { return fmt.Errorf("failed to get Python executable: %v", err) @@ -190,8 +187,8 @@ print(f'{version}\n{is_freethreaded}') return fmt.Errorf("unexpected Python info output format") } - version := info[0] - isFreethreaded := info[1] == "True" + version := strings.TrimSpace(info[0]) + isFreethreaded := strings.TrimSpace(info[1]) == "True" // Prepare version-specific library names versionNoPoints := strings.ReplaceAll(version, ".", "") @@ -203,28 +200,28 @@ print(f'{version}\n{is_freethreaded}') // Template for the pkg-config files embedTemplate := `prefix=${pcfiledir}/../.. exec_prefix=${prefix} -libdir=${exec_prefix}/lib +libdir=${exec_prefix} includedir=${prefix}/include Name: Python Description: Embed Python into an application Requires: Version: %s -Libs.private: +Libs.private: Libs: -L${libdir} -lpython%s%s Cflags: -I${includedir} ` normalTemplate := `prefix=${pcfiledir}/../.. exec_prefix=${prefix} -libdir=${exec_prefix}/lib +libdir=${exec_prefix} includedir=${prefix}/include Name: Python Description: Python library Requires: Version: %s -Libs.private: +Libs.private: Libs: -L${libdir} -lpython3%s Cflags: -I${includedir} ` @@ -265,7 +262,6 @@ Cflags: -I${includedir} } else { content = fmt.Sprintf(pair.template, version, libSuffix) } - if err := os.WriteFile(pcPath, []byte(content), 0644); err != nil { return fmt.Errorf("failed to write %s: %v", pair.name, err) } @@ -367,10 +363,10 @@ func updatePkgConfig(projectPath string) error { // installPythonEnv downloads and installs Python standalone build func installPythonEnv(projectPath string, version, buildDate string, freeThreaded, debug bool, verbose bool) error { fmt.Printf("Installing Python %s in %s\n", version, projectPath) - pythonDir := env.GetPythonRoot(projectPath) + pythonRoot := env.GetPythonRoot(projectPath) // Remove existing Python directory if it exists - if err := os.RemoveAll(pythonDir); err != nil { + if err := os.RemoveAll(pythonRoot); err != nil { return fmt.Errorf("error removing existing Python directory: %v", err) } @@ -380,17 +376,30 @@ func installPythonEnv(projectPath string, version, buildDate string, freeThreade return fmt.Errorf("unsupported platform") } - if err := downloadAndExtract("Python", version, url, pythonDir, "python/install", verbose); err != nil { + if err := downloadAndExtract("Python", version, url, pythonRoot, "python/install", verbose); err != nil { return fmt.Errorf("error downloading and extracting Python: %v", err) } // After extraction, update dylib install names on macOS - if err := updateMacOSDylibs(pythonDir, verbose); err != nil { - return fmt.Errorf("error updating dylib install names: %v", err) + if runtime.GOOS == "darwin" { + if err := updateMacOSDylibs(pythonRoot, verbose); err != nil { + return fmt.Errorf("error updating dylib install names: %v", err) + } + } + + if runtime.GOOS == "windows" { + pkgConfigDir := env.GetPythonPkgConfigDir(projectPath) + if err := genWinPyPkgConfig(pythonRoot, pkgConfigDir); err != nil { + return err + } + } + + if err := updatePkgConfig(projectPath); err != nil { + return fmt.Errorf("error updating pkg-config: %v", err) } // Create Python environment - pyEnv := env.NewPythonEnv(pythonDir) + pyEnv := env.NewPythonEnv(pythonRoot) if verbose { fmt.Println("Installing Python dependencies...") @@ -400,17 +409,12 @@ func installPythonEnv(projectPath string, version, buildDate string, freeThreade return fmt.Errorf("error upgrading pip, setuptools, whell") } - if err := updatePkgConfig(projectPath); err != nil { - return fmt.Errorf("error updating pkg-config: %v", err) - } - - pythonHome := env.GetPythonRoot(projectPath) pythonPath, err := pyEnv.GetPythonPath() if err != nil { return fmt.Errorf("failed to get Python path: %v", err) } // Write environment variables to env.txt - if err := env.WriteEnvFile(projectPath, pythonHome, pythonPath); err != nil { + if err := env.WriteEnvFile(projectPath, pythonRoot, pythonPath); err != nil { return fmt.Errorf("error writing environment file: %v", err) } diff --git a/cmd/internal/rungo/run.go b/cmd/internal/rungo/run.go index f0dba57..d30bf20 100644 --- a/cmd/internal/rungo/run.go +++ b/cmd/internal/rungo/run.go @@ -2,7 +2,6 @@ package rungo import ( "bytes" - "encoding/json" "fmt" "os" "os/exec" @@ -69,35 +68,33 @@ func GetPackageDir(pkgPath string) (string, error) { return absPath, nil } -// RunGoCommand executes a Go command with Python environment properly configured -func RunGoCommand(command string, args []string) error { - // Find the package argument - pkgIndex := FindPackageIndex(args) - - // TODO: don't depend on external go command - listArgs := []string{"list", "-find", "-json"} - - if pkgIndex != -1 { - pkgPath := args[pkgIndex] - listArgs = append(listArgs, pkgPath) +func FindProjectRoot(dir string) (string, error) { + env := env.NewPythonEnv(env.GetPythonRoot(dir)) + _, err := env.Python() + if err == nil { + return dir, nil } - cmd := exec.Command("go", listArgs...) - var out bytes.Buffer - cmd.Stdout = &out - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to get module info: %v", err) + parentDir := filepath.Dir(dir) + if parentDir == dir { + return "", fmt.Errorf("failed to find Gopy project") + } + return FindProjectRoot(parentDir) +} + +// RunGoCommand executes a Go command with Python environment properly configured +func RunCommand(command string, args []string) error { + wd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %v", err) } - var listInfo ListInfo - if err := json.NewDecoder(&out).Decode(&listInfo); err != nil { - return fmt.Errorf("failed to parse module info: %v", err) + projectRoot, err := FindProjectRoot(wd) + if err != nil { + return fmt.Errorf("should run this command in a Gopy project: %v", err) } - projectRoot := listInfo.Root env.SetBuildEnv(projectRoot) // Set up environment variables goEnv := []string{} - // Get PYTHONPATH and PYTHONHOME from env.txt var pythonPath, pythonHome string if additionalEnv, err := env.ReadEnv(projectRoot); err == nil { @@ -110,18 +107,19 @@ func RunGoCommand(command string, args []string) error { fmt.Fprintf(os.Stderr, "Warning: could not load environment variables: %v\n", err) } - // Process args to inject Python paths via ldflags - processedArgs := ProcessArgsWithLDFlags(args, projectRoot, pythonPath, pythonHome) + cmdArgs := args + if command == "go" { + goCmd := args[0] + args = args[1:] + // Process args to inject Python paths via ldflags + cmdArgs = append([]string{goCmd}, ProcessArgsWithLDFlags(args, projectRoot, pythonPath, pythonHome)...) + } - // Prepare go command with processed arguments - goArgs := append([]string{"go", command}, processedArgs...) - cmd = exec.Command(goArgs[0], goArgs[1:]...) + cmd := exec.Command(command, cmdArgs...) cmd.Env = append(goEnv, os.Environ()...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - if command == "run" { - cmd.Stdin = os.Stdin - } + cmd.Stdin = os.Stdin // Execute the command if err := cmd.Run(); err != nil { diff --git a/cmd/run.go b/cmd/run.go index b28895f..a32e6ab 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -25,7 +25,7 @@ var runCmd = &cobra.Command{ }(), DisableFlagParsing: true, Run: func(cmd *cobra.Command, args []string) { - if err := rungo.RunGoCommand("run", args); err != nil { + if err := rungo.RunCommand("go", append([]string{"run"}, args...)); err != nil { fmt.Fprintf(os.Stderr, "Error: %s\n", err) os.Exit(1) } diff --git a/inject.go b/inject.go index d5d3948..c9d9175 100644 --- a/inject.go +++ b/inject.go @@ -29,6 +29,7 @@ func init() { fmt.Fprintf(os.Stderr, "End of envs\n") } for key, value := range envs { + fmt.Fprintf(os.Stderr, "Injecting env: %s=%s\n", key, value) os.Setenv(key, value) } } diff --git a/internal/env/env.go b/internal/env/env.go index ab2e7b9..10f856f 100644 --- a/internal/env/env.go +++ b/internal/env/env.go @@ -37,6 +37,9 @@ func GetPythonRoot(projectPath string) string { // GetPythonBinDir returns the Python binary directory path relative to project path func GetPythonBinDir(projectPath string) string { + if runtime.GOOS == "windows" { + return filepath.Join(GetPythonRoot(projectPath)) + } return filepath.Join(GetPythonRoot(projectPath), "bin") } @@ -93,6 +96,7 @@ func SetBuildEnv(projectPath string) { } path := os.Getenv("PATH") path = GetGoBinDir(absPath) + pathSeparator() + path + path = GetPythonBinDir(absPath) + pathSeparator() + path if runtime.GOOS == "windows" { path = GetMingwRoot(absPath) + pathSeparator() + path path = GetTinyPkgConfigDir(absPath) + pathSeparator() + path @@ -101,6 +105,8 @@ func SetBuildEnv(projectPath string) { os.Setenv("GOPATH", GetGoPath(absPath)) os.Setenv("GOROOT", GetGoRoot(absPath)) os.Setenv("GOCACHE", GetGoCacheDir(absPath)) + os.Setenv("PKG_CONFIG_PATH", GetPythonPkgConfigDir(absPath)) + os.Setenv("CGO_ENABLED", "1") } func pathSeparator() string { @@ -110,13 +116,13 @@ func pathSeparator() string { return ":" } -// WriteEnvFile writes environment variables to .python/env.txt +// WriteEnvFile writes environment variables to .deps/env.txt func WriteEnvFile(projectPath, pythonHome, pythonPath string) error { // Prepare environment variables envVars := []string{ - fmt.Sprintf("PKG_CONFIG_PATH=%s", filepath.Join(pythonHome, "lib", "pkgconfig")), fmt.Sprintf("PYTHONPATH=%s", strings.TrimSpace(pythonPath)), fmt.Sprintf("PYTHONHOME=%s", pythonHome), + fmt.Sprintf("PATH=%s", GetPythonBinDir(projectPath)), } // Write to env.txt diff --git a/internal/env/env_test.go b/internal/env/env_test.go index 006fcb4..c89b87f 100644 --- a/internal/env/env_test.go +++ b/internal/env/env_test.go @@ -3,7 +3,6 @@ package env import ( "fmt" "os" - "path/filepath" "reflect" "runtime" "strings" @@ -106,7 +105,7 @@ func TestWriteEnvFile(t *testing.T) { // Verify the content contains expected environment variables envContent := string(content) expectedVars := []string{ - fmt.Sprintf("PKG_CONFIG_PATH=%s", filepath.Join(pythonDir, "lib", "pkgconfig")), + fmt.Sprintf("PATH=%s", GetPythonBinDir(projectDir)), fmt.Sprintf("PYTHONPATH=/mock/path1%s/mock/path2", pathSep), fmt.Sprintf("PYTHONHOME=%s", pythonDir), } diff --git a/internal/env/pyenv.go b/internal/env/pyenv.go index 09acc57..53dd7f4 100644 --- a/internal/env/pyenv.go +++ b/internal/env/pyenv.go @@ -83,5 +83,5 @@ func (e *PythonEnv) RunPythonWithOutput(writer io.Writer, args ...string) error } func (e *PythonEnv) GetPythonPath() (string, error) { - return e.RunPython("-c", `import sys; print(':'.join(sys.path))`) + return e.RunPython("-c", `import os,sys; print(os.pathsep.join(sys.path))`) }