Skip to content

Commit

Permalink
generate correct python pkg config (embed, freethreading)
Browse files Browse the repository at this point in the history
  • Loading branch information
cpunion committed Nov 20, 2024
1 parent 7c4d1df commit 57a1024
Show file tree
Hide file tree
Showing 2 changed files with 216 additions and 50 deletions.
112 changes: 78 additions & 34 deletions cmd/internal/install/python.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,65 +165,109 @@ 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
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)
}
}

Expand Down
154 changes: 138 additions & 16 deletions cmd/internal/install/python_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing"

Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -162,18 +171,34 @@ 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)
if err := os.MkdirAll(pkgConfigDir, 0755); err != nil {
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 {
Expand All @@ -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
}

Expand All @@ -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)
}
}
})

Expand Down

0 comments on commit 57a1024

Please sign in to comment.