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..dff0741 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-latest + - 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/README.md b/README.md index 09a39f5..c860af1 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # go-python: Write Python in Go - The most intuitive Python wrapper for Golang -[![Build Status](https://github.com/cpunion/go-python/actions/workflows/go.yml/badge.svg)](https://github.com/cpunion/go-python/actions/workflows/go.yml) -[![Coverage Status](https://codecov.io/github/cpunion/go-python/graph/badge.svg?token=DLVMvjAOFM)](https://codecov.io/github/cpunion/go-python) +[![Build Status](https://github.com/gotray/go-python/actions/workflows/go.yml/badge.svg)](https://github.com/gotray/go-python/actions/workflows/go.yml) +[![codecov](https://codecov.io/github/gotray/go-python/graph/badge.svg?token=TnaFHV1E3y)](https://codecov.io/github/gotray/go-python) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/cpunion/go-python) [![GitHub commits](https://badgen.net/github/commits/cpunion/go-python)](https://GitHub.com/Naereen/cpunion/go-python/commit/) -[![GitHub release](https://img.shields.io/github/v/tag/cpunion/go-python.svg?label=release)](https://github.com/cpunion/go-python/releases) -[![Go Report Card](https://goreportcard.com/badge/github.com/cpunion/go-python)](https://goreportcard.com/report/github.com/cpunion/go-python) -[![Go Reference](https://pkg.go.dev/badge/github.com/cpunion/go-python.svg)](https://pkg.go.dev/github.com/cpunion/go-python) +[![GitHub release](https://img.shields.io/github/v/tag/cpunion/go-python.svg?label=release)](https://github.com/gotray/go-python/releases) +[![Go Report Card](https://goreportcard.com/badge/github.com/gotray/go-python)](https://goreportcard.com/report/github.com/gotray/go-python) +[![Go Reference](https://pkg.go.dev/badge/github.com/gotray/go-python.svg)](https://pkg.go.dev/github.com/gotray/go-python) ## Goal @@ -57,7 +57,7 @@ See the [examples](demo). ```go package main -import . "github.com/cpunion/go-python" +import . "github.com/gotray/go-python" func main() { Initialize() @@ -75,7 +75,7 @@ func main() { ```go package main -import . "github.com/cpunion/go-python" +import . "github.com/gotray/go-python" type plt struct { Module @@ -113,7 +113,7 @@ package foo import ( "fmt" - . "github.com/cpunion/go-python" + . "github.com/gotray/go-python" ) type Point struct { @@ -165,8 +165,8 @@ package main import ( "fmt" - . "github.com/cpunion/go-python" - "github.com/cpunion/go-python/demo/module/foo" + . "github.com/gotray/go-python" + "github.com/gotray/go-python/demo/module/foo" ) func main() { @@ -235,7 +235,7 @@ import ( "fmt" "os" - . "github.com/cpunion/go-python" + . "github.com/gotray/go-python" ) /* diff --git a/cmd/add.go b/cmd/add.go deleted file mode 100644 index cf9e787..0000000 --- a/cmd/add.go +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright © 2024 NAME HERE -*/ -package cmd - -import ( - "fmt" - - "github.com/spf13/cobra" -) - -// addCmd represents the add command -var addCmd = &cobra.Command{ - Use: "add", - Short: "A brief description of your command", - Long: `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("add called") - }, -} - -func init() { - rootCmd.AddCommand(addCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // addCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // addCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") -} diff --git a/cmd/build.go b/cmd/build.go deleted file mode 100644 index 902a3a5..0000000 --- a/cmd/build.go +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright © 2024 NAME HERE -*/ -package cmd - -import ( - "fmt" - "os" - - "github.com/cpunion/go-python/cmd/internal/rungo" - "github.com/spf13/cobra" -) - -// buildCmd represents the build command -var buildCmd = &cobra.Command{ - Use: "build [flags] [package]", - Short: "Build a Go package with Python environment configured", - Long: func() string { - intro := "Build compiles a Go package with the Python environment properly configured.\n\n" - help, err := rungo.GetGoCommandHelp("build") - if err != nil { - return intro + "Failed to get go help: " + err.Error() - } - return intro + help - }(), - DisableFlagParsing: true, - Run: func(cmd *cobra.Command, args []string) { - if err := rungo.RunGoCommand("build", args); err != nil { - fmt.Fprintf(os.Stderr, "Error: %s\n", err) - os.Exit(1) - } - }, -} - -func init() { - rootCmd.AddCommand(buildCmd) -} diff --git a/cmd/gopy/gopy.go b/cmd/gopy/gopy.go deleted file mode 100644 index 1d8d9db..0000000 --- a/cmd/gopy/gopy.go +++ /dev/null @@ -1,10 +0,0 @@ -/* -Copyright © 2024 NAME HERE -*/ -package main - -import "github.com/cpunion/go-python/cmd" - -func main() { - cmd.Execute() -} diff --git a/cmd/init.go b/cmd/init.go deleted file mode 100644 index 5174e31..0000000 --- a/cmd/init.go +++ /dev/null @@ -1,127 +0,0 @@ -/* -Copyright © 2024 NAME HERE -*/ -package cmd - -import ( - "bufio" - "fmt" - "io" - "os" - "strings" - - "github.com/cpunion/go-python/cmd/internal/create" - "github.com/cpunion/go-python/cmd/internal/install" - "github.com/fatih/color" - "github.com/spf13/cobra" -) - -var bold = color.New(color.Bold).SprintFunc() - -// isDirEmpty checks if a directory is empty -func isDirEmpty(path string) (bool, error) { - f, err := os.Open(path) - if err != nil { - return false, err - } - defer f.Close() - - _, err = f.Readdirnames(1) - if err == io.EOF { - return true, nil - } - return false, err -} - -// promptYesNo asks user for confirmation -func promptYesNo(prompt string) bool { - reader := bufio.NewReader(os.Stdin) - fmt.Printf("%s [y/N]: ", prompt) - response, err := reader.ReadString('\n') - if err != nil { - return false - } - - response = strings.ToLower(strings.TrimSpace(response)) - return response == "y" || response == "yes" -} - -// initCmd represents the init command -var initCmd = &cobra.Command{ - Use: "init [path]", - Short: "Initialize a new go-python project", - Long: `Initialize a new go-python project in the specified directory. -If no path is provided, it will initialize in the current directory. - -Example: - gopy init - gopy init my-project - gopy init --debug my-project - gopy init -v my-project`, - Run: func(cmd *cobra.Command, args []string) { - // Get project path - projectPath := "." - if len(args) > 0 { - projectPath = args[0] - } - - // Get flags - debug, _ := cmd.Flags().GetBool("debug") - verbose, _ := cmd.Flags().GetBool("verbose") - goVersion, _ := cmd.Flags().GetString("go-version") - pyVersion, _ := cmd.Flags().GetString("python-version") - pyBuildDate, _ := cmd.Flags().GetString("python-build-date") - pyFreeThreaded, _ := cmd.Flags().GetBool("python-free-threaded") - tinyPkgConfigVersion, _ := cmd.Flags().GetString("tiny-pkg-config-version") - - // Check if directory exists - if _, err := os.Stat(projectPath); err == nil { - // Directory exists, check if it's empty - empty, err := isDirEmpty(projectPath) - if err != nil { - fmt.Printf("Error checking directory: %v\n", err) - return - } - - if !empty { - if !promptYesNo(fmt.Sprintf("Directory %s is not empty. Do you want to continue?", projectPath)) { - fmt.Println("Operation cancelled") - return - } - } - } else if !os.IsNotExist(err) { - fmt.Printf("Error checking directory: %v\n", err) - return - } - - // Create project using the create package - fmt.Printf("\n%s\n", bold("Creating project...")) - if err := create.Project(projectPath, verbose); err != nil { - fmt.Printf("Error creating project: %v\n", err) - return - } - - // Install dependencies - fmt.Printf("\n%s\n", bold("Installing dependencies...")) - if err := install.Dependencies(projectPath, goVersion, tinyPkgConfigVersion, pyVersion, pyBuildDate, pyFreeThreaded, debug, verbose); err != nil { - fmt.Printf("Error installing dependencies: %v\n", err) - return - } - - fmt.Printf("\n%s\n", bold("Successfully initialized go-python project in "+projectPath)) - fmt.Println("\nNext steps:") - fmt.Println("1. cd", projectPath) - fmt.Println("2. gopy run .") - }, -} - -func init() { - rootCmd.AddCommand(initCmd) - initCmd.Flags().Bool("debug", false, "Install debug version of Python (not available on Windows)") - initCmd.Flags().BoolP("verbose", "v", false, "Enable verbose output") - initCmd.Flags().String("tiny-pkg-config-version", "v0.2.0", "tiny-pkg-config version to install") - initCmd.Flags().String("go-version", "1.23.3", "Go version to install") - initCmd.Flags().String("python-version", "3.13.0", "Python version to install") - initCmd.Flags().String("python-build-date", "20241016", "Python build date") - initCmd.Flags().Bool("python-free-threaded", false, "Install free-threaded version of Python") -} diff --git a/cmd/install.go b/cmd/install.go deleted file mode 100644 index b7a8497..0000000 --- a/cmd/install.go +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright © 2024 NAME HERE -*/ -package cmd - -import ( - "fmt" - "os" - - "github.com/cpunion/go-python/cmd/internal/rungo" - "github.com/spf13/cobra" -) - -// installCmd represents the install command -var installCmd = &cobra.Command{ - Use: "install [flags] [packages]", - Short: "Install Go packages with Python environment configured", - Long: func() string { - intro := "Install compiles and installs Go packages with the Python environment properly configured.\n\n" - help, err := rungo.GetGoCommandHelp("install") - if err != nil { - return intro + "Failed to get go help: " + err.Error() - } - return intro + help - }(), - DisableFlagParsing: true, - Run: func(cmd *cobra.Command, args []string) { - if err := rungo.RunGoCommand("install", args); err != nil { - fmt.Println("Error:", err) - os.Exit(1) - } - }, -} - -func init() { - rootCmd.AddCommand(installCmd) -} diff --git a/cmd/internal/create/create.go b/cmd/internal/create/create.go deleted file mode 100644 index 6f6f34b..0000000 --- a/cmd/internal/create/create.go +++ /dev/null @@ -1,152 +0,0 @@ -package create - -import ( - "bufio" - "embed" - "fmt" - "io/fs" - "os" - "path/filepath" - "strings" - - "github.com/fatih/color" -) - -//go:embed templates/* -var templates embed.FS - -var ( - green = color.New(color.FgGreen).SprintFunc() - yellow = color.New(color.FgYellow).SprintFunc() -) - -// promptOverwrite asks user whether to overwrite a file -func promptOverwrite(path string) (bool, bool) { - reader := bufio.NewReader(os.Stdin) - for { - fmt.Printf("%s %s exists. Overwrite (Yes/No/All)? [y/n/a] ", yellow("conflict"), path) - response, err := reader.ReadString('\n') - if err != nil { - return false, false - } - - response = strings.ToLower(strings.TrimSpace(response)) - switch response { - case "y", "yes": - return true, false // overwrite this file only - case "n", "no": - return false, false // skip this file - case "a", "all": - return true, true // overwrite all files - } - } -} - -// Project initializes a new go-python project in the specified directory -func Project(projectPath string, verbose bool) error { - // Create project directory - if err := os.MkdirAll(projectPath, 0755); err != nil { - return fmt.Errorf("error creating directory: %v", err) - } - - overwriteAll := false - - // Walk through template files and copy them - err := fs.WalkDir(templates, "templates", func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - - // Skip the templates root directory - if path == "templates" { - return nil - } - - // Get relative path from templates directory - relPath, err := filepath.Rel("templates", path) - if err != nil { - return err - } - - // Create destination path - dstPath := filepath.Join(projectPath, relPath) - - // If it's a directory, create it - if d.IsDir() { - if err := os.MkdirAll(dstPath, 0755); err != nil { - return err - } - fmt.Printf("%s\t%s/\n", green("create"), relPath) - return nil - } - - // Check if file exists - _, err = os.Stat(dstPath) - fileExists := err == nil - - if fileExists && !overwriteAll { - overwrite, all := promptOverwrite(relPath) - if !overwrite { - fmt.Printf("%s\t%s\n", yellow("skip"), relPath) - return nil - } - overwriteAll = all - } - - // Read template file - content, err := templates.ReadFile(path) - if err != nil { - return err - } - - // Write file to destination - if err := os.WriteFile(dstPath, content, 0644); err != nil { - return err - } - - // Print status with color - if fileExists { - fmt.Printf("%s\t%s\n", yellow("overwrite"), relPath) - } else { - fmt.Printf("%s\t%s\n", green("create"), relPath) - } - - return nil - }) - - if err != nil { - return fmt.Errorf("error copying template files: %v", err) - } - - // Create go.mod file - goModPath := filepath.Join(projectPath, "go.mod") - goModExists := false - if _, err := os.Stat(goModPath); err == nil { - goModExists = true - if !overwriteAll { - overwrite, _ := promptOverwrite("go.mod") - if !overwrite { - fmt.Printf("%s\tgo.mod\n", yellow("skip")) - return nil - } - } - } - - goModContent := fmt.Sprintf(`module %s - -go 1.23 -`, filepath.Base(projectPath)) - - if err := os.WriteFile(goModPath, []byte(goModContent), 0644); err != nil { - return fmt.Errorf("error writing go.mod: %v", err) - } - - // Print go.mod status - if goModExists { - fmt.Printf("%s\tgo.mod\n", yellow("overwrite")) - } else { - fmt.Printf("%s\tgo.mod\n", green("create")) - } - - return nil -} diff --git a/cmd/internal/create/create_test.go b/cmd/internal/create/create_test.go deleted file mode 100644 index b56b948..0000000 --- a/cmd/internal/create/create_test.go +++ /dev/null @@ -1,161 +0,0 @@ -package create - -import ( - "os" - "path/filepath" - "testing" -) - -// setupTestDir creates a temporary directory for testing -func setupTestDir(t *testing.T) string { - t.Helper() - dir, err := os.MkdirTemp("", "gopy-test-*") - if err != nil { - t.Fatalf("failed to create temp dir: %v", err) - } - return dir -} - -// cleanupTestDir removes the temporary test directory -func cleanupTestDir(t *testing.T, dir string) { - t.Helper() - if err := os.RemoveAll(dir); err != nil { - t.Errorf("failed to cleanup test dir: %v", err) - } -} - -func TestProject(t *testing.T) { - tests := []struct { - name string - setup func(dir string) error - wantErr bool - }{ - { - name: "create new project in empty directory", - setup: func(dir string) error { - return nil - }, - wantErr: false, - }, - { - name: "create project with existing directory", - setup: func(dir string) error { - return os.MkdirAll(dir, 0755) - }, - wantErr: false, - }, - { - name: "create project with existing files", - setup: func(dir string) error { - if err := os.MkdirAll(dir, 0755); err != nil { - return err - } - // Create a go.mod file - return os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test\n"), 0644) - }, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create test directory - testDir := setupTestDir(t) - defer cleanupTestDir(t, testDir) - - // Setup test case - if err := tt.setup(testDir); err != nil { - t.Fatalf("test setup failed: %v", err) - } - - // Run Project function - err := Project(testDir, false) - if (err != nil) != tt.wantErr { - t.Errorf("Project() error = %v, wantErr %v", err, tt.wantErr) - return - } - - // Verify project structure - expectedFiles := []string{ - "go.mod", - "main.go", - ".gitignore", - } - - for _, file := range expectedFiles { - path := filepath.Join(testDir, file) - if _, err := os.Stat(path); os.IsNotExist(err) { - t.Errorf("expected file %s does not exist", file) - } - } - }) - } -} - -func TestPromptOverwrite(t *testing.T) { - tests := []struct { - name string - input string - wantOverwrite bool - wantOverwriteAll bool - }{ - { - name: "answer yes", - input: "y\n", - wantOverwrite: true, - wantOverwriteAll: false, - }, - { - name: "answer no", - input: "n\n", - wantOverwrite: false, - wantOverwriteAll: false, - }, - { - name: "answer all", - input: "a\n", - wantOverwrite: true, - wantOverwriteAll: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a temporary file to simulate stdin - tmpfile, err := os.CreateTemp("", "test-input") - if err != nil { - t.Fatal(err) - } - defer os.Remove(tmpfile.Name()) - - // Write test input to temp file - if _, err := tmpfile.Write([]byte(tt.input)); err != nil { - t.Fatal(err) - } - if err := tmpfile.Close(); err != nil { - t.Fatal(err) - } - - // Redirect stdin to our temp file - oldStdin := os.Stdin - f, err := os.Open(tmpfile.Name()) - if err != nil { - t.Fatal(err) - } - os.Stdin = f - defer func() { - os.Stdin = oldStdin - f.Close() - }() - - // Test promptOverwrite - gotOverwrite, gotOverwriteAll := promptOverwrite("test.txt") - if gotOverwrite != tt.wantOverwrite { - t.Errorf("promptOverwrite() overwrite = %v, want %v", gotOverwrite, tt.wantOverwrite) - } - if gotOverwriteAll != tt.wantOverwriteAll { - t.Errorf("promptOverwrite() overwriteAll = %v, want %v", gotOverwriteAll, tt.wantOverwriteAll) - } - }) - } -} diff --git a/cmd/internal/create/templates/.gitignore b/cmd/internal/create/templates/.gitignore deleted file mode 100644 index b47d072..0000000 --- a/cmd/internal/create/templates/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -__pycache__/ -*.pyc -.env -.deps/ diff --git a/cmd/internal/create/templates/main.go b/cmd/internal/create/templates/main.go deleted file mode 100644 index 21d7b2f..0000000 --- a/cmd/internal/create/templates/main.go +++ /dev/null @@ -1,10 +0,0 @@ -package main - -import ( - . "github.com/cpunion/go-python" -) - -func main() { - Initialize() - defer Finalize() -} diff --git a/cmd/internal/install/archive.go b/cmd/internal/install/archive.go deleted file mode 100644 index 0d732fb..0000000 --- a/cmd/internal/install/archive.go +++ /dev/null @@ -1,349 +0,0 @@ -package install - -import ( - "archive/tar" - "archive/zip" - "compress/gzip" - "crypto/sha1" - "encoding/hex" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "strings" - - "github.com/klauspost/compress/zstd" -) - -// getCacheDir returns the cache directory for downloaded files -func getCacheDir() (string, error) { - homeDir, err := os.UserHomeDir() - if err != nil { - return "", fmt.Errorf("failed to get user home directory: %v", err) - } - cacheDir := filepath.Join(homeDir, ".gopy", "cache") - if err := os.MkdirAll(cacheDir, 0755); err != nil { - return "", fmt.Errorf("failed to create cache directory: %v", err) - } - return cacheDir, nil -} - -// getFullExtension returns the full extension for a filename (e.g., ".tar.gz" for "file.tar.gz") -func getFullExtension(filename string) string { - // Handle common multi-level extensions - for _, ext := range []string{".tar.gz", ".tar.zst"} { - if strings.HasSuffix(filename, ext) { - return ext - } - } - return filepath.Ext(filename) -} - -// downloadFileWithCache downloads a file from url and returns the path to the cached file -func downloadFileWithCache(url string) (string, error) { - cacheDir, err := getCacheDir() - if err != nil { - return "", err - } - - // Use URL's last path segment as filename - urlPath := strings.Split(url, "/") - filename := urlPath[len(urlPath)-1] - - // Calculate SHA1 hash of the URL - hasher := sha1.New() - hasher.Write([]byte(url)) - urlHash := hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 characters of hash - - // Insert hash before the file extension, handling multi-level extensions - ext := getFullExtension(filename) - baseFilename := filename[:len(filename)-len(ext)] - cachedFilename := fmt.Sprintf("%s-%s%s", baseFilename, urlHash, ext) - cachedFile := filepath.Join(cacheDir, cachedFilename) - - // Check if file exists in cache - if _, err := os.Stat(cachedFile); err == nil { - fmt.Printf("Using cached file from %s\n", cachedFile) - return cachedFile, nil - } - - fmt.Printf("Downloading from %s\n", url) - - // Create temporary file - tmpFile, err := os.CreateTemp(cacheDir, "download-*") - if err != nil { - return "", fmt.Errorf("failed to create temporary file: %v", err) - } - tmpPath := tmpFile.Name() - defer os.Remove(tmpPath) - defer tmpFile.Close() - - // Download to temporary file - 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(tmpFile, resp.Body) - if err != nil { - return "", fmt.Errorf("failed to write file: %v", err) - } - - // Close the file before renaming - tmpFile.Close() - - // Rename temporary file to cached file - if err := os.Rename(tmpPath, cachedFile); err != nil { - return "", fmt.Errorf("failed to move file to cache: %v", err) - } - - return cachedFile, nil -} - -func downloadAndExtract(name, version, url, dir, trimPrefix string, verbose bool) error { - if verbose { - fmt.Printf("Downloading %s %s from %s\n", name, version, url) - } - - path, err := downloadFileWithCache(url) - if err != nil { - return fmt.Errorf("error downloading %s %s: %v", name, version, err) - } - - if verbose { - fmt.Printf("Extracting %s %s into %s...\n", name, version, dir) - } - - if err = os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("error creating directory %s: %v", dir, err) - } - - // Extract based on file extension - if strings.HasSuffix(path, ".zip") { - return extractZip(path, dir) - } else if strings.HasSuffix(path, ".tar.gz") { - return extractTarGz(path, dir) - } else if strings.HasSuffix(path, ".tar.zst") { - return extractTarZst(path, dir, trimPrefix, verbose) - } else { - return fmt.Errorf("unsupported file extension for %s %s", name, version) - } -} - -// extractZip extracts a zip file to the specified directory -func extractZip(zipFile, destDir string) error { - r, err := zip.OpenReader(zipFile) - if err != nil { - return err - } - defer r.Close() - - for _, f := range r.File { - // Skip the root "go" directory - if f.Name == "go/" || f.Name == "go" { - continue - } - - // Remove "go/" prefix from paths - destPath := filepath.Join(destDir, strings.TrimPrefix(f.Name, "go/")) - - if f.FileInfo().IsDir() { - os.MkdirAll(destPath, f.Mode()) - continue - } - - if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { - return err - } - - destFile, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) - if err != nil { - return err - } - - srcFile, err := f.Open() - if err != nil { - destFile.Close() - return err - } - - _, err = io.Copy(destFile, srcFile) - srcFile.Close() - destFile.Close() - if err != nil { - return err - } - } - return nil -} - -// extractTarGz extracts a tar.gz file to the specified directory -func extractTarGz(tarFile, destDir string) error { - file, err := os.Open(tarFile) - 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 - } - - // Skip the root "go" directory - if header.Name == "go/" || header.Name == "go" { - continue - } - - // Remove "go/" prefix from paths - destPath := filepath.Join(destDir, strings.TrimPrefix(header.Name, "go/")) - - switch header.Typeflag { - case tar.TypeDir: - if err := os.MkdirAll(destPath, os.FileMode(header.Mode)); err != nil { - return err - } - case tar.TypeReg: - if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { - return err - } - outFile, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode)) - if err != nil { - return err - } - if _, err := io.Copy(outFile, tr); err != nil { - outFile.Close() - return err - } - outFile.Close() - } - } - return nil -} - -// extractTarZst extracts a tar.zst file to a destination directory -func extractTarZst(src, dst, trimPrefix string, verbose bool) error { - if verbose { - fmt.Printf("Extracting from %s to %s\n", src, dst) - } - - // Open the zstd compressed file - file, err := os.Open(src) - if err != nil { - return fmt.Errorf("error opening file: %v", err) - } - defer file.Close() - - // Create zstd decoder - decoder, err := zstd.NewReader(file) - if err != nil { - return fmt.Errorf("error creating zstd decoder: %v", err) - } - defer decoder.Close() - - // Create tar reader from the decompressed stream - tr := tar.NewReader(decoder) - - for { - header, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - return err - } - - name := header.Name - - if trimPrefix != "" { - if !strings.HasPrefix(header.Name, trimPrefix) { - continue - } - - // Remove the trimPrefix prefix - name = strings.TrimPrefix(header.Name, trimPrefix) - if name == "" { - continue - } - } - - path := filepath.Join(dst, name) - if verbose { - fmt.Printf("Extracting: %s\n", path) - } - - switch header.Typeflag { - case tar.TypeDir: - if err := os.MkdirAll(path, os.FileMode(header.Mode)); err != nil { - return fmt.Errorf("error creating directory %s: %v", path, err) - } - case tar.TypeReg: - dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("error creating directory %s: %v", dir, err) - } - - file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) - if err != nil { - return fmt.Errorf("error creating file %s: %v", path, err) - } - - if _, err := io.Copy(file, tr); err != nil { - file.Close() - return fmt.Errorf("error writing to file %s: %v", path, err) - } - file.Close() - case tar.TypeSymlink: - dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("error creating directory %s: %v", dir, err) - } - - // Remove existing symlink if it exists - if err := os.RemoveAll(path); err != nil { - return fmt.Errorf("error removing existing symlink %s: %v", path, err) - } - - // Create new symlink - if err := os.Symlink(header.Linkname, path); err != nil { - return fmt.Errorf("error creating symlink %s -> %s: %v", path, header.Linkname, err) - } - case tar.TypeLink: - dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("error creating directory %s: %v", dir, err) - } - - // Remove existing file if it exists - if err := os.RemoveAll(path); err != nil { - return fmt.Errorf("error removing existing file %s: %v", path, err) - } - - // Create hard link relative to the destination directory - targetPath := filepath.Join(dst, strings.TrimPrefix(header.Linkname, "python/install/")) - if err := os.Link(targetPath, path); err != nil { - return fmt.Errorf("error creating hard link %s -> %s: %v", path, targetPath, err) - } - } - } - - return nil -} diff --git a/cmd/internal/install/deps.go b/cmd/internal/install/deps.go deleted file mode 100644 index 36c3481..0000000 --- a/cmd/internal/install/deps.go +++ /dev/null @@ -1,78 +0,0 @@ -package install - -import ( - "fmt" - "os" - "os/exec" - "runtime" - - "github.com/cpunion/go-python/internal/env" -) - -// Dependencies installs all required dependencies for the project -func Dependencies(projectPath string, goVersion, tinyPkgConfigVersion, pyVersion, pyBuildDate string, freeThreaded, debug bool, verbose bool) error { - if err := installTinyPkgConfig(projectPath, tinyPkgConfigVersion, verbose); err != nil { - return err - } - // Only install MSYS2 on Windows - if runtime.GOOS == "windows" { - if err := installMingw(projectPath, verbose); err != nil { - return err - } - } - - if err := installGo(projectPath, goVersion, verbose); err != nil { - return err - } - env.SetBuildEnv(projectPath) - - // Install Go dependencies - if err := installGoDeps(projectPath); err != nil { - return err - } - - // Install Python environment and dependencies - if err := installPythonEnv(projectPath, pyVersion, pyBuildDate, freeThreaded, debug, verbose); err != nil { - 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 -} - -// installGoDeps installs Go dependencies -func installGoDeps(projectPath string) error { - currentDir, err := os.Getwd() - if err != nil { - return fmt.Errorf("error getting current directory: %v", err) - } - - if err := os.Chdir(projectPath); err != nil { - return fmt.Errorf("error changing to project directory: %v", err) - } - defer func() { - _ = os.Chdir(currentDir) - }() - - fmt.Println("Installing Go dependencies...") - getCmd := exec.Command("go", "get", "-u", "github.com/cpunion/go-python") - getCmd.Stdout = os.Stdout - getCmd.Stderr = os.Stderr - if err := getCmd.Run(); err != nil { - return fmt.Errorf("error installing dependencies: %v", err) - } - - return nil -} diff --git a/cmd/internal/install/golang.go b/cmd/internal/install/golang.go deleted file mode 100644 index f13b23c..0000000 --- a/cmd/internal/install/golang.go +++ /dev/null @@ -1,58 +0,0 @@ -package install - -import ( - "fmt" - "runtime" - - "github.com/cpunion/go-python/internal/env" -) - -const ( - // Go download URL format - goDownloadURL = "https://go.dev/dl/go%s.%s-%s.%s" -) - -// getGoURL returns the appropriate Go download URL for the current platform -func getGoURL(version string) string { - var os, arch, ext string - - switch runtime.GOOS { - case "windows": - os = "windows" - ext = "zip" - case "darwin": - os = "darwin" - ext = "tar.gz" - case "linux": - os = "linux" - ext = "tar.gz" - default: - return "" - } - - switch runtime.GOARCH { - case "amd64": - arch = "amd64" - case "386": - arch = "386" - case "arm64": - arch = "arm64" - default: - return "" - } - - return fmt.Sprintf(goDownloadURL, version, os, arch, ext) -} - -// installGo downloads and installs Go in the project directory -func installGo(projectPath, version string, verbose bool) error { - goDir := env.GetGoDir(projectPath) - fmt.Printf("Installing Go %s in %s\n", version, goDir) - // Get download URL - url := getGoURL(version) - if url == "" { - return fmt.Errorf("unsupported platform") - } - - return downloadAndExtract("Go", version, url, goDir, "", verbose) -} diff --git a/cmd/internal/install/mingw.go b/cmd/internal/install/mingw.go deleted file mode 100644 index 0fd9961..0000000 --- a/cmd/internal/install/mingw.go +++ /dev/null @@ -1,18 +0,0 @@ -package install - -import ( - "fmt" - - "github.com/cpunion/go-python/internal/env" -) - -const ( - mingwVersion = "14.2.0" - mingwURL = "https://github.com/brechtsanders/winlibs_mingw/releases/download/14.2.0posix-19.1.1-12.0.0-ucrt-r2/winlibs-x86_64-posix-seh-gcc-14.2.0-llvm-19.1.1-mingw-w64ucrt-12.0.0-r2.zip" -) - -func installMingw(projectPath string, verbose bool) error { - root := env.GetMingwDir(projectPath) - fmt.Printf("Installing mingw in %v\n", root) - return downloadAndExtract("mingw", mingwVersion, mingwURL, root, "", verbose) -} diff --git a/cmd/internal/install/python.go b/cmd/internal/install/python.go deleted file mode 100644 index 992658d..0000000 --- a/cmd/internal/install/python.go +++ /dev/null @@ -1,418 +0,0 @@ -package install - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "regexp" - "runtime" - "strings" - - "github.com/cpunion/go-python/internal/env" -) - -const ( - baseURL = "https://github.com/indygreg/python-build-standalone/releases/download/%s" -) - -type pythonBuild struct { - arch string - os string - variant string - debug bool - shared bool - fullPack bool -} - -// getPythonURL returns the appropriate Python standalone URL for the current platform -func getPythonURL(version, buildDate, arch, os string, freeThreaded, debug bool) string { - // Map GOARCH to Python build architecture - archMap := map[string]string{ - "amd64": "x86_64", - "arm64": "aarch64", - "386": "i686", - } - - pythonArch, ok := archMap[arch] - if !ok { - return "" - } - - build := pythonBuild{ - arch: pythonArch, - fullPack: true, - debug: debug, - } - - switch os { - case "darwin": - build.os = "apple-darwin" - if freeThreaded { - build.variant = "freethreaded" - if build.debug { - build.variant += "+debug" - } else { - build.variant += "+pgo" - } - } else { - if build.debug { - build.variant = "debug" - } else { - build.variant = "pgo" - } - } - case "linux": - build.os = "unknown-linux-gnu" - if freeThreaded { - build.variant = "freethreaded" - if build.debug { - build.variant += "+debug" - } else { - build.variant += "+pgo" - } - } else { - if build.debug { - build.variant = "debug" - } else { - build.variant = "pgo" - } - } - case "windows": - build.os = "pc-windows-msvc" - build.shared = true - if freeThreaded { - build.variant = "freethreaded+pgo" - } else { - build.variant = "pgo" - } - default: - return "" - } - - // Construct filename - filename := fmt.Sprintf("cpython-%s+%s-%s-%s", version, buildDate, build.arch, build.os) - if build.shared { - filename += "-shared" - } - filename += "-" + build.variant - if build.fullPack { - filename += "-full" - } - filename += ".tar.zst" - - return fmt.Sprintf(baseURL, buildDate) + "/" + filename -} - -// 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 { - return fmt.Errorf("failed to read lib directory: %v", err) - } - - absLibDir, err := filepath.Abs(libDir) - if err != nil { - return fmt.Errorf("failed to get absolute path: %v", err) - } - - for _, entry := range entries { - if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".dylib") { - dylibPath := filepath.Join(libDir, entry.Name()) - if verbose { - fmt.Printf("Updating install name for: %s\n", dylibPath) - } - - // Get the current install name - cmd := exec.Command("otool", "-D", dylibPath) - output, err := cmd.Output() - if err != nil { - return fmt.Errorf("failed to get install name for %s: %v", dylibPath, err) - } - - // Parse the output to get the current install name - lines := strings.Split(string(output), "\n") - if len(lines) < 2 { - continue - } - currentName := strings.TrimSpace(lines[1]) - if currentName == "" { - continue - } - - // Calculate new install name using absolute path - newName := filepath.Join(absLibDir, filepath.Base(currentName)) - - fmt.Printf("Updating install name for %s to %s\n", dylibPath, newName) - // Update the install name - cmd = exec.Command("install_name_tool", "-id", newName, dylibPath) - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to update install name for %s: %v", dylibPath, err) - } - } - } - return nil -} - -// generatePkgConfig generates pkg-config files for Windows -func generatePkgConfig(pythonPath, pkgConfigDir string) error { - 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) - pythonBin, err := pyEnv.Python() - if err != nil { - return fmt.Errorf("failed to get Python executable: %v", err) - } - - // Get Python version and check if freethreaded - cmd := exec.Command(pythonBin, "-c", ` -import sys -import sysconfig -version = f'{sys.version_info.major}.{sys.version_info.minor}' -is_freethreaded = hasattr(sys, "gettotalrefcount") -print(f'{version}\n{is_freethreaded}') -`) - output, err := cmd.Output() - if err != nil { - return fmt.Errorf("failed to get Python info: %v", err) - } - - info := strings.Split(strings.TrimSpace(string(output)), "\n") - if len(info) != 2 { - return fmt.Errorf("unexpected Python info output format") - } - - version := info[0] - isFreethreaded := info[1] == "True" - - // Prepare version-specific library names - versionNoPoints := strings.ReplaceAll(version, ".", "") - libSuffix := "" - if isFreethreaded { - libSuffix = "t" - } - - // Template for the pkg-config files - embedTemplate := `prefix=${pcfiledir}/../.. -exec_prefix=${prefix} -libdir=${exec_prefix}/lib -includedir=${prefix}/include - -Name: Python -Description: Embed Python into an application -Requires: -Version: %s -Libs.private: -Libs: -L${libdir} -lpython%s%s -Cflags: -I${includedir} -` - - normalTemplate := `prefix=${pcfiledir}/../.. -exec_prefix=${prefix} -libdir=${exec_prefix}/lib -includedir=${prefix}/include - -Name: Python -Description: Python library -Requires: -Version: %s -Libs.private: -Libs: -L${libdir} -lpython3%s -Cflags: -I${includedir} -` - - // Generate file pairs - filePairs := []struct { - name string - template string - embed bool - }{ - {fmt.Sprintf("python-%s%s.pc", version, libSuffix), normalTemplate, false}, - {fmt.Sprintf("python-%s%s-embed.pc", version, libSuffix), embedTemplate, true}, - {"python3" + libSuffix + ".pc", normalTemplate, false}, - {"python3" + libSuffix + "-embed.pc", embedTemplate, true}, - } - - // If freethreaded, also generate non-t versions with the same content - if isFreethreaded { - additionalPairs := []struct { - name string - template string - embed bool - }{ - {fmt.Sprintf("python-%s.pc", version), normalTemplate, false}, - {fmt.Sprintf("python-%s-embed.pc", version), embedTemplate, true}, - {"python3.pc", normalTemplate, false}, - {"python3-embed.pc", embedTemplate, true}, - } - filePairs = append(filePairs, additionalPairs...) - } - - // Write all pkg-config files - for _, pair := range filePairs { - pcPath := filepath.Join(pkgConfigDir, pair.name) - var content string - if pair.embed { - content = fmt.Sprintf(pair.template, version, versionNoPoints, libSuffix) - } 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) - } - } - - return nil -} - -// updatePkgConfig updates the prefix in pkg-config files to use absolute path -func updatePkgConfig(projectPath string) error { - pythonPath := env.GetPythonRoot(projectPath) - pkgConfigDir := env.GetPythonPkgConfigDir(projectPath) - - entries, err := os.ReadDir(pkgConfigDir) - if err != nil { - return fmt.Errorf("failed to read pkgconfig directory: %v", err) - } - - absPath, err := filepath.Abs(pythonPath) - if err != nil { - return fmt.Errorf("failed to get absolute path: %v", err) - } - - // Helper function to write a .pc file with the correct prefix - writePC := func(path string, content []byte) error { - newContent := strings.ReplaceAll(string(content), "prefix=/install", "prefix="+absPath) - return os.WriteFile(path, []byte(newContent), 0644) - } - - // Regular expressions for matching file patterns - normalPattern := regexp.MustCompile(`^python-(\d+\.\d+)t?\.pc$`) - embedPattern := regexp.MustCompile(`^python-(\d+\.\d+)t?-embed\.pc$`) - - for _, entry := range entries { - if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".pc") { - pcFile := filepath.Join(pkgConfigDir, entry.Name()) - - // Read file content - content, err := os.ReadFile(pcFile) - if err != nil { - return fmt.Errorf("failed to read %s: %v", pcFile, err) - } - - // Update original file - if err := writePC(pcFile, content); err != nil { - return fmt.Errorf("failed to update %s: %v", pcFile, err) - } - - name := entry.Name() - // Create additional copies based on patterns - copies := make(map[string]bool) - - // Handle python-X.YZt.pc and python-X.YZ.pc patterns - if matches := normalPattern.FindStringSubmatch(name); matches != nil { - if strings.Contains(name, "t.pc") { - // python-3.13t.pc -> python3.pc and python3t.pc - copies["python3t.pc"] = true - copies["python3.pc"] = true - // Also create non-t version - noT := fmt.Sprintf("python-%s.pc", matches[1]) - if err := writePC(filepath.Join(pkgConfigDir, noT), content); err != nil { - return fmt.Errorf("failed to write %s: %v", noT, err) - } - } else { - // python-3.13.pc -> python3.pc - copies["python3.pc"] = true - } - } - - // Handle python-X.YZt-embed.pc and python-X.YZ-embed.pc patterns - if matches := embedPattern.FindStringSubmatch(name); matches != nil { - if strings.Contains(name, "t-embed.pc") { - // python-3.13t-embed.pc -> python3-embed.pc and python3t-embed.pc - copies["python3t-embed.pc"] = true - copies["python3-embed.pc"] = true - // Also create non-t version - noT := fmt.Sprintf("python-%s-embed.pc", matches[1]) - if err := writePC(filepath.Join(pkgConfigDir, noT), content); err != nil { - return fmt.Errorf("failed to write %s: %v", noT, err) - } - } else { - // python-3.13-embed.pc -> python3-embed.pc - copies["python3-embed.pc"] = true - } - } - - // Write all unique copies - for copyName := range copies { - copyPath := filepath.Join(pkgConfigDir, copyName) - if err := writePC(copyPath, content); err != nil { - return fmt.Errorf("failed to write %s: %v", copyPath, err) - } - } - } - } - return nil -} - -// 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) - - // Remove existing Python directory if it exists - if err := os.RemoveAll(pythonDir); err != nil { - return fmt.Errorf("error removing existing Python directory: %v", err) - } - - // Get Python URL - url := getPythonURL(version, buildDate, runtime.GOARCH, runtime.GOOS, freeThreaded, debug) - if url == "" { - return fmt.Errorf("unsupported platform") - } - - if err := downloadAndExtract("Python", version, url, pythonDir, "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) - } - - // Create Python environment - pyEnv := env.NewPythonEnv(pythonDir) - - if verbose { - fmt.Println("Installing Python dependencies...") - } - - if err := pyEnv.RunPip("install", "--upgrade", "pip", "setuptools", "wheel"); err != nil { - 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 { - return fmt.Errorf("error writing environment file: %v", err) - } - - return nil -} diff --git a/cmd/internal/install/python_test.go b/cmd/internal/install/python_test.go deleted file mode 100644 index 9002587..0000000 --- a/cmd/internal/install/python_test.go +++ /dev/null @@ -1,337 +0,0 @@ -package install - -import ( - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - "testing" - - "github.com/cpunion/go-python/internal/env" -) - -func TestGetPythonURL(t *testing.T) { - tests := []struct { - name string - arch string - os string - freeThreaded bool - - debug bool - want string - wantErr bool - }{ - { - name: "darwin-arm64-freethreaded-debug", - arch: "arm64", - os: "darwin", - freeThreaded: true, - debug: true, - want: "cpython-3.13.0+20241016-aarch64-apple-darwin-freethreaded+debug-full.tar.zst", - }, - { - name: "darwin-amd64-freethreaded-pgo", - arch: "amd64", - os: "darwin", - freeThreaded: true, - debug: false, - want: "cpython-3.13.0+20241016-x86_64-apple-darwin-freethreaded+pgo-full.tar.zst", - }, - { - name: "darwin-amd64-debug", - arch: "amd64", - os: "darwin", - freeThreaded: false, - debug: true, - want: "cpython-3.13.0+20241016-x86_64-apple-darwin-debug-full.tar.zst", - }, - { - name: "darwin-amd64-pgo", - arch: "amd64", - os: "darwin", - freeThreaded: false, - debug: false, - want: "cpython-3.13.0+20241016-x86_64-apple-darwin-pgo-full.tar.zst", - }, - { - name: "linux-amd64-freethreaded-debug", - arch: "amd64", - os: "linux", - freeThreaded: true, - debug: true, - want: "cpython-3.13.0+20241016-x86_64-unknown-linux-gnu-freethreaded+debug-full.tar.zst", - }, - { - name: "windows-amd64-freethreaded-pgo", - arch: "amd64", - os: "windows", - freeThreaded: true, - debug: false, - want: "cpython-3.13.0+20241016-x86_64-pc-windows-msvc-shared-freethreaded+pgo-full.tar.zst", - }, - { - name: "windows-386-freethreaded-pgo", - arch: "386", - os: "windows", - freeThreaded: true, - debug: false, - want: "cpython-3.13.0+20241016-i686-pc-windows-msvc-shared-freethreaded+pgo-full.tar.zst", - }, - { - name: "unsupported-arch", - arch: "mips", - os: "linux", - freeThreaded: false, - debug: false, - want: "", - wantErr: true, - }, - { - name: "unsupported-os", - arch: "amd64", - os: "freebsd", - freeThreaded: false, - debug: false, - want: "", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := getPythonURL("3.13.0", "20241016", tt.arch, tt.os, tt.freeThreaded, tt.debug) - - if tt.wantErr { - if got != "" { - t.Errorf("getPythonURL() = %v, want empty string for error case", got) - } - return - } - - if got == "" { - t.Errorf("getPythonURL() returned empty string, want %v", tt.want) - return - } - - // Extract filename from URL - parts := strings.Split(got, "/") - filename := parts[len(parts)-1] - - if filename != tt.want { - t.Errorf("getPythonURL() = %v, want %v", filename, tt.want) - } - }) - } -} - -func TestGetCacheDir(t *testing.T) { - // Save original home dir - origHome := os.Getenv("HOME") - defer os.Setenv("HOME", origHome) - - t.Run("valid home directory", func(t *testing.T) { - tmpDir := t.TempDir() - if runtime.GOOS == "windows" { - os.Setenv("USERPROFILE", tmpDir) - } else { - os.Setenv("HOME", tmpDir) - } - - got, err := getCacheDir() - if err != nil { - t.Errorf("getCacheDir() error = %v, want nil", err) - return - } - - want := filepath.Join(tmpDir, ".gopy", "cache") - if got != want { - t.Errorf("getCacheDir() = %v, want %v", got, want) - } - - // Verify directory was created - if _, err := os.Stat(got); os.IsNotExist(err) { - t.Errorf("getCacheDir() did not create cache directory") - } - }) -} - -func TestUpdatePkgConfig(t *testing.T) { - t.Run("freethreaded pkg-config files", func(t *testing.T) { - // Create temporary directory structure - tmpDir := t.TempDir() - pkgConfigDir := env.GetPythonPkgConfigDir(tmpDir) - if err := os.MkdirAll(pkgConfigDir, 0755); err != nil { - t.Fatal(err) - } - - // Create test .pc files with freethreaded content - testFiles := map[string]string{ - "python-3.13t.pc": `prefix=/install -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: Python -Description: Python library -Version: 3.13 -Libs: -L${libdir} -lpython3t -Cflags: -I${includedir}`, - "python-3.13t-embed.pc": `prefix=/install -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: Python -Description: Embed Python into an application -Version: 3.13 -Libs: -L${libdir} -lpython313t -Cflags: -I${includedir}`, - } - - for filename, content := range testFiles { - if err := os.WriteFile(filepath.Join(pkgConfigDir, filename), []byte(content), 0644); err != nil { - t.Fatal(err) - } - } - - // Test updating pkg-config files - if err := updatePkgConfig(tmpDir); err != nil { - t.Errorf("updatePkgConfig() error = %v, want nil", err) - return - } - - // Verify the generated files - expectedFiles := map[string]struct { - shouldExist bool - libName string - }{ - // Freethreaded versions - "python-3.13t.pc": {true, "-lpython3t"}, - "python3t.pc": {true, "-lpython3t"}, - "python-3.13t-embed.pc": {true, "-lpython313t"}, - "python3t-embed.pc": {true, "-lpython313t"}, - // Non-t versions (same content as freethreaded) - "python-3.13.pc": {true, "-lpython3t"}, - "python3.pc": {true, "-lpython3t"}, - "python-3.13-embed.pc": {true, "-lpython313t"}, - "python3-embed.pc": {true, "-lpython313t"}, - } - - absPath, _ := filepath.Abs(filepath.Join(tmpDir, ".deps/python")) - for filename, expected := range expectedFiles { - path := filepath.Join(pkgConfigDir, filename) - if _, err := os.Stat(path); os.IsNotExist(err) { - if expected.shouldExist { - t.Errorf("Expected file %s was not created", filename) - } - continue - } - - content, err := os.ReadFile(path) - if err != nil { - t.Errorf("Failed to read file %s: %v", filename, err) - continue - } - - // Check prefix - expectedPrefix := fmt.Sprintf("prefix=%s", absPath) - if !strings.Contains(string(content), expectedPrefix) { - t.Errorf("File %s does not contain expected prefix %s", filename, expectedPrefix) - } - - // Check library name - if !strings.Contains(string(content), expected.libName) { - t.Errorf("File %s does not contain expected library name %s", filename, expected.libName) - } - } - }) - - t.Run("non-freethreaded pkg-config files", func(t *testing.T) { - // Create temporary directory structure - tmpDir := t.TempDir() - pkgConfigDir := env.GetPythonPkgConfigDir(tmpDir) - if err := os.MkdirAll(pkgConfigDir, 0755); err != nil { - t.Fatal(err) - } - - // Create test .pc files with non-freethreaded content - testFiles := map[string]string{ - "python-3.13.pc": `prefix=/install -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: Python -Description: Python library -Version: 3.13 -Libs: -L${libdir} -lpython3 -Cflags: -I${includedir}`, - "python-3.13-embed.pc": `prefix=/install -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: Python -Description: Embed Python into an application -Version: 3.13 -Libs: -L${libdir} -lpython313 -Cflags: -I${includedir}`, - } - - for filename, content := range testFiles { - if err := os.WriteFile(filepath.Join(pkgConfigDir, filename), []byte(content), 0644); err != nil { - t.Fatal(err) - } - } - - // Test updating pkg-config files - if err := updatePkgConfig(tmpDir); err != nil { - t.Errorf("updatePkgConfig() error = %v, want nil", err) - return - } - - // Verify the generated files - expectedFiles := map[string]struct { - shouldExist bool - libName string - }{ - "python-3.13.pc": {true, "-lpython3"}, - "python3.pc": {true, "-lpython3"}, - "python-3.13-embed.pc": {true, "-lpython313"}, - "python3-embed.pc": {true, "-lpython313"}, - } - - absPath, _ := filepath.Abs(filepath.Join(tmpDir, ".deps/python")) - for filename, expected := range expectedFiles { - path := filepath.Join(pkgConfigDir, filename) - if _, err := os.Stat(path); os.IsNotExist(err) { - if expected.shouldExist { - t.Errorf("Expected file %s was not created", filename) - } - continue - } - - content, err := os.ReadFile(path) - if err != nil { - t.Errorf("Failed to read file %s: %v", filename, err) - continue - } - - // Check prefix - expectedPrefix := fmt.Sprintf("prefix=%s", absPath) - if !strings.Contains(string(content), expectedPrefix) { - t.Errorf("File %s does not contain expected prefix %s", filename, expectedPrefix) - } - - // Check library name - if !strings.Contains(string(content), expected.libName) { - t.Errorf("File %s does not contain expected library name %s", filename, expected.libName) - } - } - }) - - t.Run("missing pkgconfig directory", func(t *testing.T) { - tmpDir := t.TempDir() - err := updatePkgConfig(tmpDir) - if err == nil { - t.Error("updatePkgConfig() error = nil, want error for missing pkgconfig directory") - } - }) -} diff --git a/cmd/internal/install/tiny_pkg_config.go b/cmd/internal/install/tiny_pkg_config.go deleted file mode 100644 index d15748f..0000000 --- a/cmd/internal/install/tiny_pkg_config.go +++ /dev/null @@ -1,64 +0,0 @@ -package install - -import ( - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - - "github.com/cpunion/go-python/internal/env" -) - -const ( - tinyPkgDownloadURL = "https://github.com/cpunion/tiny-pkg-config/releases/download/%s/%s" -) - -func installTinyPkgConfig(projectPath, version string, verbose bool) error { - dir := env.GetTinyPkgConfigDir(projectPath) - // Determine OS and architecture - goos := runtime.GOOS - arch := runtime.GOARCH - - // Convert OS/arch to match release file naming - osName := strings.ToUpper(goos[:1]) + goos[1:] // "darwin" -> "Darwin", "linux" -> "Linux" - archName := arch - if arch == "amd64" { - archName = "x86_64" - } - - // Construct filename and URL - ext := ".tar.gz" - if osName == "Windows" { - ext = ".zip" - } - - filename := fmt.Sprintf("tiny-pkg-config_%s_%s%s", osName, archName, ext) - downloadURL := fmt.Sprintf(tinyPkgDownloadURL, version, filename) - - if err := downloadAndExtract("tiny-pkg-config", version, downloadURL, dir, "", verbose); err != nil { - return fmt.Errorf("download and extract tiny-pkg-config failed: %w", err) - } - - // After extraction, rename the executable - oldName := "tiny-pkg-config" - newName := "pkg-config" - if runtime.GOOS == "windows" { - oldName += ".exe" - newName += ".exe" - } - - oldPath := filepath.Join(dir, oldName) - newPath := filepath.Join(dir, newName) - - // Rename the file - if err := os.Rename(oldPath, newPath); err != nil { - return fmt.Errorf("failed to rename executable: %w", err) - } - - if verbose { - fmt.Printf("Renamed %s to %s\n", oldName, newName) - } - - return nil -} diff --git a/cmd/internal/log/log.go b/cmd/internal/log/log.go deleted file mode 100644 index a089bec..0000000 --- a/cmd/internal/log/log.go +++ /dev/null @@ -1,72 +0,0 @@ -package log - -import ( - "go.uber.org/zap" - "go.uber.org/zap/zapcore" -) - -var ( - logger *zap.Logger - sugar *zap.SugaredLogger -) - -// Init initializes the logger with the given verbosity level -func Init(verbose bool) { - config := zap.NewDevelopmentConfig() - if !verbose { - config.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel) - } else { - config.Level = zap.NewAtomicLevelAt(zapcore.DebugLevel) - } - - // Customize output format - config.EncoderConfig.TimeKey = "" // Remove timestamp - config.EncoderConfig.LevelKey = "" // Remove log level - config.EncoderConfig.CallerKey = "" // Remove caller - config.EncoderConfig.NameKey = "" // Remove logger name - config.EncoderConfig.StacktraceKey = "" // Remove stacktrace - config.DisableCaller = true - config.DisableStacktrace = true - - var err error - logger, err = config.Build() - if err != nil { - panic(err) - } - sugar = logger.Sugar() -} - -// Debug logs a debug message -func Debug(args ...interface{}) { - sugar.Debug(args...) -} - -// Debugf logs a formatted debug message -func Debugf(template string, args ...interface{}) { - sugar.Debugf(template, args...) -} - -// Info logs an info message -func Info(args ...interface{}) { - sugar.Info(args...) -} - -// Infof logs a formatted info message -func Infof(template string, args ...interface{}) { - sugar.Infof(template, args...) -} - -// Error logs an error message -func Error(args ...interface{}) { - sugar.Error(args...) -} - -// Errorf logs a formatted error message -func Errorf(template string, args ...interface{}) { - sugar.Errorf(template, args...) -} - -// Sync flushes any buffered log entries -func Sync() error { - return logger.Sync() -} diff --git a/cmd/internal/rungo/run.go b/cmd/internal/rungo/run.go deleted file mode 100644 index f0dba57..0000000 --- a/cmd/internal/rungo/run.go +++ /dev/null @@ -1,196 +0,0 @@ -package rungo - -import ( - "bytes" - "encoding/json" - "fmt" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - - "github.com/cpunion/go-python/internal/env" -) - -type ListInfo struct { - Dir string `json:"Dir"` - Root string `json:"Root"` -} - -// FindPackageIndex finds the package argument index by skipping flags and their values -func FindPackageIndex(args []string) int { - for i := 0; i < len(args); i++ { - arg := args[i] - if strings.HasPrefix(arg, "-") { - // Skip known flags that take values - switch arg { - case "-o", "-p", "-asmflags", "-buildmode", "-compiler", "-gccgoflags", "-gcflags", - "-installsuffix", "-ldflags", "-mod", "-modfile", "-pkgdir", "-tags", "-toolexec": - i++ // Skip the next argument as it's the flag's value - } - continue - } - return i - } - return -1 -} - -// GetPackageDir returns the directory containing the package -func GetPackageDir(pkgPath string) (string, error) { - // Get the absolute path - absPath, err := filepath.Abs(pkgPath) - if err != nil { - return "", fmt.Errorf("error resolving path: %v", err) - } - - // If it's not a directory, get its parent directory - fi, err := os.Stat(absPath) - if err != nil { - if os.IsNotExist(err) && pkgPath == "." { - // Special case: if "." doesn't exist, use current directory - dir, err := os.Getwd() - if err != nil { - return "", fmt.Errorf("error getting working directory: %v", err) - } - absPath = dir - fi, err = os.Stat(absPath) - if err != nil { - return "", fmt.Errorf("error checking path: %v", err) - } - } else { - return "", fmt.Errorf("error checking path: %v", err) - } - } - - if !fi.IsDir() { - return filepath.Dir(absPath), nil - } - 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) - } - 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) - } - var listInfo ListInfo - if err := json.NewDecoder(&out).Decode(&listInfo); err != nil { - return fmt.Errorf("failed to parse module info: %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 { - for key, value := range additionalEnv { - goEnv = append(goEnv, key+"="+value) - } - pythonPath = additionalEnv["PYTHONPATH"] - pythonHome = additionalEnv["PYTHONHOME"] - } else { - 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) - - // Prepare go command with processed arguments - goArgs := append([]string{"go", command}, processedArgs...) - cmd = exec.Command(goArgs[0], goArgs[1:]...) - cmd.Env = append(goEnv, os.Environ()...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if command == "run" { - cmd.Stdin = os.Stdin - } - - // Execute the command - if err := cmd.Run(); err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - os.Exit(exitErr.ExitCode()) - } - return fmt.Errorf("error executing command: %v", err) - } - - return nil -} - -// ProcessArgsWithLDFlags processes command line arguments to inject Python paths via ldflags -func ProcessArgsWithLDFlags(args []string, projectRoot, pythonPath, pythonHome string) []string { - result := make([]string, 0, len(args)) - - // Prepare the -X flags we want to add - ldflags := fmt.Sprintf("-X 'github.com/cpunion/go-python.ProjectRoot=%s'", projectRoot) - - // Prepare rpath flag if needed - pythonLibDir := env.GetPythonLibDir(projectRoot) - switch runtime.GOOS { - case "darwin", "linux": - ldflags += fmt.Sprintf(" -extldflags '-Wl,-rpath,%s'", pythonLibDir) - case "windows": - // Windows doesn't use rpath - default: - // Use Linux format for other Unix-like systems - ldflags += fmt.Sprintf(" -extldflags '-Wl,-rpath=%s'", pythonLibDir) - } - - for i := 0; i < len(args); i++ { - arg := args[i] - if strings.HasPrefix(arg, "-ldflags=") || arg == "-ldflags" { - // Get existing flags - var existingFlags string - if strings.HasPrefix(arg, "-ldflags=") { - existingFlags = strings.TrimPrefix(arg, "-ldflags=") - } else if i+1 < len(args) { - existingFlags = args[i+1] - i++ // Skip the next arg since we've consumed it - } - existingFlags = strings.TrimSpace(existingFlags) - if ldflags != "" { - ldflags += " " + existingFlags - } - } else { - result = append(result, arg) - } - } - return append([]string{"-ldflags", ldflags}, result...) -} - -// GetGoCommandHelp returns the formatted help text for the specified go command -func GetGoCommandHelp(command string) (string, error) { - cmd := exec.Command("go", "help", command) - var out bytes.Buffer - cmd.Stdout = &out - err := cmd.Run() - if err != nil { - return "", err - } - - intro := fmt.Sprintf(`The command arguments and flags are fully compatible with 'go %s'. - -Following is the help message from 'go %s': -------------------------------------------------------------------------------- - -`, command, command) - - return intro + out.String() + "\n-------------------------------------------------------------------------------", nil -} diff --git a/cmd/remove.go b/cmd/remove.go deleted file mode 100644 index cb641ce..0000000 --- a/cmd/remove.go +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright © 2024 NAME HERE -*/ -package cmd - -import ( - "fmt" - - "github.com/spf13/cobra" -) - -// removeCmd represents the remove command -var removeCmd = &cobra.Command{ - Use: "remove", - Short: "A brief description of your command", - Long: `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("remove called") - }, -} - -func init() { - rootCmd.AddCommand(removeCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // removeCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // removeCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") -} diff --git a/cmd/root.go b/cmd/root.go deleted file mode 100644 index 856482b..0000000 --- a/cmd/root.go +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright © 2024 NAME HERE -*/ -package cmd - -import ( - "os" - - "github.com/spf13/cobra" -) - -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "gopy", - Short: "A tool for building Go applications with Python integration", - Long: `gopy is a command line tool that helps you build, run and manage Go applications -that integrate with Python. - -It provides commands to: -- Initialize Python environment for your Go project -- Build Go applications with Python environment properly configured -- Run Go applications with Python runtime support -- Install Go packages with Python dependencies -- Add or remove Python packages to/from your project - -Use "gopy help [command]" for more information about a command.`, - // Uncomment the following line if your bare application - // has an action associated with it: - // Run: func(cmd *cobra.Command, args []string) { }, -} - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - err := rootCmd.Execute() - if err != nil { - os.Exit(1) - } -} - -func init() { - // Here you will define your flags and configuration settings. - // Cobra supports persistent flags, which, if defined here, - // will be global for your application. - - // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.go-python.yaml)") - - // Cobra also supports local flags, which will only run - // when this action is called directly. - rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") -} diff --git a/cmd/run.go b/cmd/run.go deleted file mode 100644 index b28895f..0000000 --- a/cmd/run.go +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright © 2024 NAME HERE -*/ -package cmd - -import ( - "fmt" - "os" - - "github.com/cpunion/go-python/cmd/internal/rungo" - "github.com/spf13/cobra" -) - -// runCmd represents the run command -var runCmd = &cobra.Command{ - Use: "run [flags] [package] [arguments...]", - Short: "Run a Go package with Python environment configured", - Long: func() string { - intro := "Run executes a Go package with the Python environment properly configured.\n\n" - help, err := rungo.GetGoCommandHelp("run") - if err != nil { - return intro + "Failed to get go help: " + err.Error() - } - return intro + help - }(), - DisableFlagParsing: true, - Run: func(cmd *cobra.Command, args []string) { - if err := rungo.RunGoCommand("run", args); err != nil { - fmt.Fprintf(os.Stderr, "Error: %s\n", err) - os.Exit(1) - } - }, -} - -func init() { - rootCmd.AddCommand(runCmd) -} diff --git a/demo/autoderef/autoderef.go b/demo/autoderef/autoderef.go index 4bdd41e..bea162c 100644 --- a/demo/autoderef/autoderef.go +++ b/demo/autoderef/autoderef.go @@ -4,8 +4,8 @@ import ( "fmt" "runtime" - . "github.com/cpunion/go-python" - pymath "github.com/cpunion/go-python/math" + . "github.com/gotray/go-python" + pymath "github.com/gotray/go-python/math" ) func main() { diff --git a/demo/gradio/gradio.go b/demo/gradio/gradio.go index dfeefe0..e6f3d20 100644 --- a/demo/gradio/gradio.go +++ b/demo/gradio/gradio.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - . "github.com/cpunion/go-python" + . "github.com/gotray/go-python" ) /* diff --git a/demo/module/foo/foo.go b/demo/module/foo/foo.go index cb89430..9f71060 100644 --- a/demo/module/foo/foo.go +++ b/demo/module/foo/foo.go @@ -3,7 +3,7 @@ package foo import ( "fmt" - . "github.com/cpunion/go-python" + . "github.com/gotray/go-python" ) type Point struct { diff --git a/demo/module/module.go b/demo/module/module.go index 6f41d61..5451178 100644 --- a/demo/module/module.go +++ b/demo/module/module.go @@ -3,8 +3,8 @@ package main import ( "fmt" - . "github.com/cpunion/go-python" - "github.com/cpunion/go-python/demo/module/foo" + . "github.com/gotray/go-python" + "github.com/gotray/go-python/demo/module/foo" ) func main() { diff --git a/demo/plot/plot.go b/demo/plot/plot.go index 13d6db1..c898b40 100644 --- a/demo/plot/plot.go +++ b/demo/plot/plot.go @@ -1,6 +1,6 @@ package main -import . "github.com/cpunion/go-python" +import . "github.com/gotray/go-python" func main() { Initialize() diff --git a/demo/plot2/plot2.go b/demo/plot2/plot2.go index 2412009..1c09c66 100644 --- a/demo/plot2/plot2.go +++ b/demo/plot2/plot2.go @@ -1,6 +1,6 @@ package main -import . "github.com/cpunion/go-python" +import . "github.com/gotray/go-python" type plt struct { Module diff --git a/go.mod b/go.mod index 0753726..9ebfe51 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/cpunion/go-python +module github.com/gotray/go-python go 1.21 diff --git a/inject.go b/inject.go deleted file mode 100644 index d5d3948..0000000 --- a/inject.go +++ /dev/null @@ -1,34 +0,0 @@ -package gp - -import ( - "fmt" - "os" - - "github.com/cpunion/go-python/internal/env" -) - -var ProjectRoot string - -func init() { - injectDebug := os.Getenv("GP_INJECT_DEBUG") - if ProjectRoot == "" { - if injectDebug != "" { - panic("ProjectRoot is not set, compile with -ldflags '-X github.com/cpunion/go-python.ProjectRoot=/path/to/project'") - } - return - } - envs, err := env.ReadEnv(ProjectRoot) - if err != nil { - panic(fmt.Sprintf("Failed to read env: %s", err)) - } - if injectDebug != "" { - fmt.Fprintf(os.Stderr, "Injecting envs for project: %s\n", ProjectRoot) - for key, value := range envs { - fmt.Fprintf(os.Stderr, " %s=%s\n", key, value) - } - fmt.Fprintf(os.Stderr, "End of envs\n") - } - for key, value := range envs { - os.Setenv(key, value) - } -} diff --git a/internal/env/env.go b/internal/env/env.go deleted file mode 100644 index ab2e7b9..0000000 --- a/internal/env/env.go +++ /dev/null @@ -1,179 +0,0 @@ -package env - -import ( - "fmt" - "os" - "path/filepath" - "runtime" - "strings" -) - -const ( - // depsDir is the directory for all dependencies - depsDir = ".deps" - // pyDir is the directory name for Python installation - pyDir = "python" - // goDir is the directory name for Go installation - goDir = "go" - // mingwDir is the directory name for Mingw installation - mingwDir = "mingw" - mingwRoot = mingwDir + "/mingw64" - - tinyPkgConfigDir = "tiny-pkg-config" -) - -func GetDepsDir(projectPath string) string { - return filepath.Join(projectPath, depsDir) -} - -func GetGoDir(projectPath string) string { - return filepath.Join(GetDepsDir(projectPath), goDir) -} - -// GetPythonRoot returns the Python installation root path relative to project path -func GetPythonRoot(projectPath string) string { - return filepath.Join(projectPath, depsDir, pyDir) -} - -// GetPythonBinDir returns the Python binary directory path relative to project path -func GetPythonBinDir(projectPath string) string { - return filepath.Join(GetPythonRoot(projectPath), "bin") -} - -// GetPythonLibDir returns the Python library directory path relative to project path -func GetPythonLibDir(projectPath string) string { - return filepath.Join(GetPythonRoot(projectPath), "lib") -} - -// GetPythonPkgConfigDir returns the pkg-config directory path relative to project path -func GetPythonPkgConfigDir(projectPath string) string { - return filepath.Join(GetPythonLibDir(projectPath), "pkgconfig") -} - -// GetGoRoot returns the Go installation root path relative to project path -func GetGoRoot(projectPath string) string { - return filepath.Join(projectPath, depsDir, goDir) -} - -// GetGoPath returns the Go path relative to project path -func GetGoPath(projectPath string) string { - return filepath.Join(GetGoRoot(projectPath), "packages") -} - -// GetGoBinDir returns the Go binary directory path relative to project path -func GetGoBinDir(projectPath string) string { - return filepath.Join(GetGoRoot(projectPath), "bin") -} - -// GetGoCacheDir returns the Go cache directory path relative to project path -func GetGoCacheDir(projectPath string) string { - return filepath.Join(GetGoRoot(projectPath), "go-build") -} - -func GetMingwDir(projectPath string) string { - return filepath.Join(projectPath, depsDir, mingwDir) -} - -func GetMingwRoot(projectPath string) string { - return filepath.Join(projectPath, depsDir, mingwRoot) -} - -func GetTinyPkgConfigDir(projectPath string) string { - return filepath.Join(projectPath, depsDir, tinyPkgConfigDir) -} - -func GetEnvConfigPath(projectPath string) string { - return filepath.Join(GetDepsDir(projectPath), "env.txt") -} - -func SetBuildEnv(projectPath string) { - absPath, err := filepath.Abs(projectPath) - if err != nil { - panic(err) - } - path := os.Getenv("PATH") - path = GetGoBinDir(absPath) + pathSeparator() + path - if runtime.GOOS == "windows" { - path = GetMingwRoot(absPath) + pathSeparator() + path - path = GetTinyPkgConfigDir(absPath) + pathSeparator() + path - } - os.Setenv("PATH", path) - os.Setenv("GOPATH", GetGoPath(absPath)) - os.Setenv("GOROOT", GetGoRoot(absPath)) - os.Setenv("GOCACHE", GetGoCacheDir(absPath)) -} - -func pathSeparator() string { - if runtime.GOOS == "windows" { - return ";" - } - return ":" -} - -// WriteEnvFile writes environment variables to .python/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), - } - - // Write to env.txt - envFile := GetEnvConfigPath(projectPath) - if err := os.WriteFile(envFile, []byte(strings.Join(envVars, "\n")), 0644); err != nil { - return fmt.Errorf("failed to write env file: %v", err) - } - - return nil -} - -// ReadEnvFile loads environment variables from .python/env.txt in the given directory -func ReadEnvFile(projectDir string) (map[string]string, error) { - envFile := GetEnvConfigPath(projectDir) - content, err := os.ReadFile(envFile) - if err != nil { - return nil, fmt.Errorf("failed to read env file %s: %v", envFile, err) - } - envs := map[string]string{} - for _, line := range strings.Split(strings.TrimSpace(string(content)), "\n") { - parts := strings.SplitN(line, "=", 2) - if len(parts) == 2 { - envs[parts[0]] = parts[1] - } - } - return envs, nil -} - -func GeneratePythonEnv(pythonHome, pythonPath string) map[string]string { - path := os.Getenv("PATH") - if runtime.GOOS == "windows" { - path = filepath.Join(pythonHome) + ";" + path - } else { - path = filepath.Join(pythonHome, "bin") + ":" + path - } - return map[string]string{ - "PYTHONHOME": pythonHome, - "PYTHONPATH": pythonPath, - "PATH": path, - } -} - -func ReadEnv(projectDir string) (map[string]string, error) { - envs, err := ReadEnvFile(projectDir) - if err != nil { - return nil, err - } - pythonHome, ok := envs["PYTHONHOME"] - if !ok { - return nil, fmt.Errorf("PYTHONHOME is not set in env.txt") - } - pythonPath, ok := envs["PYTHONPATH"] - if !ok { - return nil, fmt.Errorf("PYTHONPATH is not set in env.txt") - } - for k, v := range GeneratePythonEnv(pythonHome, pythonPath) { - envs[k] = v - } - return envs, nil -} diff --git a/internal/env/env_test.go b/internal/env/env_test.go deleted file mode 100644 index 006fcb4..0000000 --- a/internal/env/env_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package env - -import ( - "fmt" - "os" - "path/filepath" - "reflect" - "runtime" - "strings" - "testing" -) - -func TestLoadEnvFile(t *testing.T) { - t.Run("valid env file", func(t *testing.T) { - // Create temporary directory structure - projectDir := t.TempDir() - pythonDir := GetPythonRoot(projectDir) - if err := os.MkdirAll(pythonDir, 0755); err != nil { - t.Fatal(err) - } - - // Create test env.txt file - envContent := map[string]string{ - "PKG_CONFIG_PATH": "/test/lib/pkgconfig", - "PYTHONPATH": "/test/lib/python3.9", - "PYTHONHOME": "/test", - } - lines := []string{} - for key, value := range envContent { - lines = append(lines, fmt.Sprintf("%s=%s", key, value)) - } - envFile := GetEnvConfigPath(projectDir) - if err := os.WriteFile(envFile, []byte(strings.Join(lines, "\n")), 0644); err != nil { - t.Fatal(err) - } - - // Test loading the env file - got, err := ReadEnvFile(projectDir) - if err != nil { - t.Errorf("LoadEnvFile() error = %v, want nil", err) - return - } - - if !reflect.DeepEqual(got, envContent) { - t.Errorf("LoadEnvFile() = %v, want %v", got, envContent) - } - }) - - t.Run("missing env file", func(t *testing.T) { - tmpDir := t.TempDir() - _, err := ReadEnvFile(tmpDir) - if err == nil { - t.Error("LoadEnvFile() error = nil, want error for missing env file") - } - }) -} - -func TestWriteEnvFile(t *testing.T) { - if testing.Short() { - t.Skip("skipping test in short mode") - } - - t.Run("write env file", func(t *testing.T) { - // Create temporary directory structure - projectDir := t.TempDir() - pythonDir := GetPythonRoot(projectDir) - binDir := GetPythonBinDir(projectDir) - if err := os.MkdirAll(binDir, 0755); err != nil { - t.Fatal(err) - } - - // Create mock Python executable - var pythonPath string - if runtime.GOOS == "windows" { - pythonPath = "/mock/path1;/mock/path2" - } else { - pythonPath = "/mock/path1:/mock/path2" - } - - // Test writing env file - if err := WriteEnvFile(projectDir, pythonDir, pythonPath); err != nil { - t.Errorf("writeEnvFile() error = %v, want nil", err) - return - } - - // Verify the env file was created - envFile := GetEnvConfigPath(projectDir) - if _, err := os.Stat(envFile); os.IsNotExist(err) { - t.Error("writeEnvFile() did not create env.txt") - return - } - - // Read and verify content - content, err := os.ReadFile(envFile) - if err != nil { - t.Errorf("Failed to read env.txt: %v", err) - return - } - - // Get expected path separator - pathSep := ":" - if runtime.GOOS == "windows" { - pathSep = ";" - } - - // 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("PYTHONPATH=/mock/path1%s/mock/path2", pathSep), - fmt.Sprintf("PYTHONHOME=%s", pythonDir), - } - fmt.Printf("envContent:\n%v\n", envContent) - for _, v := range expectedVars { - if !strings.Contains(envContent, v) { - t.Errorf("env.txt missing expected variable %s", v) - } - } - }) -} diff --git a/internal/env/pyenv.go b/internal/env/pyenv.go deleted file mode 100644 index 09acc57..0000000 --- a/internal/env/pyenv.go +++ /dev/null @@ -1,87 +0,0 @@ -package env - -import ( - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "regexp" - "runtime" - "strings" -) - -// PythonEnv represents a Python environment -type PythonEnv struct { - Root string // Root directory of the Python installation -} - -// NewPythonEnv creates a new Python environment instance -func NewPythonEnv(pythonHome string) *PythonEnv { - return &PythonEnv{ - Root: pythonHome, - } -} - -// Python returns the path to the Python executable -func (e *PythonEnv) Python() (string, error) { - binDir := e.Root - if runtime.GOOS != "windows" { - binDir = filepath.Join(e.Root, "bin") - } - entries, err := os.ReadDir(binDir) - if err != nil { - return "", fmt.Errorf("failed to read bin directory: %v", err) - } - - // Single pattern to match all variants, prioritizing 't' versions - var pattern *regexp.Regexp - if runtime.GOOS == "windows" { - pattern = regexp.MustCompile(`^python3?[\d.]*t?(?:\.exe)?$`) - } else { - pattern = regexp.MustCompile(`^python3?[\d.]*t?$`) - } - - for _, entry := range entries { - if !entry.IsDir() && pattern.MatchString(entry.Name()) { - return filepath.Join(binDir, entry.Name()), nil - } - } - - return "", fmt.Errorf("python executable not found in %s", e.Root) -} - -// RunPip executes pip with the given arguments -func (e *PythonEnv) RunPip(args ...string) error { - return e.RunPythonWithOutput(nil, append([]string{"-m", "pip"}, args...)...) -} - -// RunPython executes python with the given arguments -func (e *PythonEnv) RunPython(args ...string) (string, error) { - var buf strings.Builder - err := e.RunPythonWithOutput(&buf, args...) - if err != nil { - return "", err - } - return strings.TrimSpace(buf.String()), nil -} - -func (e *PythonEnv) RunPythonWithOutput(writer io.Writer, args ...string) error { - pythonPath, err := e.Python() - if err != nil { - return err - } - - cmd := exec.Command(pythonPath, args...) - if writer != nil { - cmd.Stdout = io.MultiWriter(writer, os.Stdout) - } else { - cmd.Stdout = os.Stdout - } - cmd.Stderr = os.Stderr - return cmd.Run() -} - -func (e *PythonEnv) GetPythonPath() (string, error) { - return e.RunPython("-c", `import sys; print(':'.join(sys.path))`) -} diff --git a/math/math.go b/math/math.go index 98a170d..1f572bb 100644 --- a/math/math.go +++ b/math/math.go @@ -1,7 +1,7 @@ package math import ( - gp "github.com/cpunion/go-python" + gp "github.com/gotray/go-python" ) var math_ gp.Module diff --git a/math/math_test.go b/math/math_test.go index ee4bcfb..62c75cf 100644 --- a/math/math_test.go +++ b/math/math_test.go @@ -3,7 +3,7 @@ package math import ( "testing" - gp "github.com/cpunion/go-python" + gp "github.com/gotray/go-python" ) func TestSqrt(t *testing.T) {