diff --git a/cmd/internal/install/python.go b/cmd/internal/install/python.go index 7b569e4..dabe183 100644 --- a/cmd/internal/install/python.go +++ b/cmd/internal/install/python.go @@ -165,25 +165,45 @@ func generatePkgConfig(pythonPath, pkgConfigDir string) error { return fmt.Errorf("failed to create pkgconfig directory: %v", err) } - // Get Python version from the environment + // 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 - cmd := exec.Command(pythonBin, "-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") + // 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 version: %v", err) + return fmt.Errorf("failed to get Python info: %v", err) } - version := strings.TrimSpace(string(output)) - // Template for the pkg-config file - pcTemplate := `prefix=${pcfiledir}/../.. + 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} +libdir=${exec_prefix}/lib includedir=${prefix}/include Name: Python @@ -191,39 +211,63 @@ Description: Embed Python into an application Requires: Version: %s Libs.private: -Libs: -L${libdir} -lpython313 +Libs: -L${libdir} -lpython%s%s Cflags: -I${includedir} ` - // TODO: need update libs - // Create the main pkg-config files - files := []struct { - name string - content string + 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.pc", version), - fmt.Sprintf(pcTemplate, version), - }, - { - fmt.Sprintf("python-%s-embed.pc", version), - fmt.Sprintf(pcTemplate, version), - }, - { - "python3.pc", - fmt.Sprintf(pcTemplate, version), - }, - { - "python3-embed.pc", - fmt.Sprintf(pcTemplate, version), - }, + {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 _, file := range files { - pcPath := filepath.Join(pkgConfigDir, file.name) - if err := os.WriteFile(pcPath, []byte(file.content), 0644); err != nil { - return fmt.Errorf("failed to write %s: %v", file.name, err) + 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) } } diff --git a/cmd/internal/install/python_test.go b/cmd/internal/install/python_test.go index f9a9b3f..8fdc665 100644 --- a/cmd/internal/install/python_test.go +++ b/cmd/internal/install/python_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strings" "testing" @@ -131,7 +132,11 @@ func TestGetCacheDir(t *testing.T) { t.Run("valid home directory", func(t *testing.T) { tmpDir := t.TempDir() - os.Setenv("HOME", tmpDir) + if runtime.GOOS == "windows" { + os.Setenv("USERPROFILE", tmpDir) + } else { + os.Setenv("HOME", tmpDir) + } got, err := getCacheDir() if err != nil { @@ -152,7 +157,11 @@ func TestGetCacheDir(t *testing.T) { t.Run("invalid home directory", func(t *testing.T) { // Set HOME to a non-existent directory - os.Setenv("HOME", "/nonexistent/path") + if runtime.GOOS == "windows" { + os.Setenv("USERPROFILE", "/nonexistent/path") + } else { + os.Setenv("HOME", "/nonexistent/path") + } _, err := getCacheDir() if err == nil { @@ -162,7 +171,7 @@ func TestGetCacheDir(t *testing.T) { } func TestUpdatePkgConfig(t *testing.T) { - t.Run("valid pkg-config files", func(t *testing.T) { + t.Run("freethreaded pkg-config files", func(t *testing.T) { // Create temporary directory structure tmpDir := t.TempDir() pkgConfigDir := env.GetPythonPkgConfigDir(tmpDir) @@ -170,10 +179,26 @@ func TestUpdatePkgConfig(t *testing.T) { t.Fatal(err) } - // Create test .pc files + // Create test .pc files with freethreaded content testFiles := map[string]string{ - "python-3.13t.pc": "prefix=/install\nlibdir=${prefix}/lib\n", - "python-3.13-embed.pc": "prefix=/install\nlibdir=${prefix}/lib\n", + "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 { @@ -189,19 +214,29 @@ func TestUpdatePkgConfig(t *testing.T) { } // Verify the generated files - expectedFiles := []string{ - "python-3.13t.pc", - "python-3.13.pc", - "python3t.pc", - "python3.pc", - "python-3.13-embed.pc", - "python3-embed.pc", + 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"}, } - for _, filename := range expectedFiles { + 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) { - t.Errorf("Expected file %s was not created", filename) + if expected.shouldExist { + t.Errorf("Expected file %s was not created", filename) + } continue } @@ -211,11 +246,98 @@ func TestUpdatePkgConfig(t *testing.T) { continue } - absPath, _ := filepath.Abs(filepath.Join(tmpDir, ".deps/python")) + // 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) + } } })