Skip to content

Commit 57a1024

Browse files
committed
generate correct python pkg config (embed, freethreading)
1 parent 7c4d1df commit 57a1024

File tree

2 files changed

+216
-50
lines changed

2 files changed

+216
-50
lines changed

cmd/internal/install/python.go

Lines changed: 78 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -165,65 +165,109 @@ func generatePkgConfig(pythonPath, pkgConfigDir string) error {
165165
return fmt.Errorf("failed to create pkgconfig directory: %v", err)
166166
}
167167

168-
// Get Python version from the environment
168+
// Get Python environment
169169
pyEnv := env.NewPythonEnv(pythonPath)
170170
pythonBin, err := pyEnv.Python()
171171
if err != nil {
172172
return fmt.Errorf("failed to get Python executable: %v", err)
173173
}
174174

175-
// Get Python version
176-
cmd := exec.Command(pythonBin, "-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
175+
// Get Python version and check if freethreaded
176+
cmd := exec.Command(pythonBin, "-c", `
177+
import sys
178+
import sysconfig
179+
version = f'{sys.version_info.major}.{sys.version_info.minor}'
180+
is_freethreaded = hasattr(sys, "gettotalrefcount")
181+
print(f'{version}\n{is_freethreaded}')
182+
`)
177183
output, err := cmd.Output()
178184
if err != nil {
179-
return fmt.Errorf("failed to get Python version: %v", err)
185+
return fmt.Errorf("failed to get Python info: %v", err)
180186
}
181-
version := strings.TrimSpace(string(output))
182187

183-
// Template for the pkg-config file
184-
pcTemplate := `prefix=${pcfiledir}/../..
188+
info := strings.Split(strings.TrimSpace(string(output)), "\n")
189+
if len(info) != 2 {
190+
return fmt.Errorf("unexpected Python info output format")
191+
}
192+
193+
version := info[0]
194+
isFreethreaded := info[1] == "True"
195+
196+
// Prepare version-specific library names
197+
versionNoPoints := strings.ReplaceAll(version, ".", "")
198+
libSuffix := ""
199+
if isFreethreaded {
200+
libSuffix = "t"
201+
}
202+
203+
// Template for the pkg-config files
204+
embedTemplate := `prefix=${pcfiledir}/../..
185205
exec_prefix=${prefix}
186-
libdir=${exec_prefix}
206+
libdir=${exec_prefix}/lib
187207
includedir=${prefix}/include
188208
189209
Name: Python
190210
Description: Embed Python into an application
191211
Requires:
192212
Version: %s
193213
Libs.private:
194-
Libs: -L${libdir} -lpython313
214+
Libs: -L${libdir} -lpython%s%s
195215
Cflags: -I${includedir}
196216
`
197-
// TODO: need update libs
198217

199-
// Create the main pkg-config files
200-
files := []struct {
201-
name string
202-
content string
218+
normalTemplate := `prefix=${pcfiledir}/../..
219+
exec_prefix=${prefix}
220+
libdir=${exec_prefix}/lib
221+
includedir=${prefix}/include
222+
223+
Name: Python
224+
Description: Python library
225+
Requires:
226+
Version: %s
227+
Libs.private:
228+
Libs: -L${libdir} -lpython3%s
229+
Cflags: -I${includedir}
230+
`
231+
232+
// Generate file pairs
233+
filePairs := []struct {
234+
name string
235+
template string
236+
embed bool
203237
}{
204-
{
205-
fmt.Sprintf("python-%s.pc", version),
206-
fmt.Sprintf(pcTemplate, version),
207-
},
208-
{
209-
fmt.Sprintf("python-%s-embed.pc", version),
210-
fmt.Sprintf(pcTemplate, version),
211-
},
212-
{
213-
"python3.pc",
214-
fmt.Sprintf(pcTemplate, version),
215-
},
216-
{
217-
"python3-embed.pc",
218-
fmt.Sprintf(pcTemplate, version),
219-
},
238+
{fmt.Sprintf("python-%s%s.pc", version, libSuffix), normalTemplate, false},
239+
{fmt.Sprintf("python-%s%s-embed.pc", version, libSuffix), embedTemplate, true},
240+
{"python3" + libSuffix + ".pc", normalTemplate, false},
241+
{"python3" + libSuffix + "-embed.pc", embedTemplate, true},
242+
}
243+
244+
// If freethreaded, also generate non-t versions with the same content
245+
if isFreethreaded {
246+
additionalPairs := []struct {
247+
name string
248+
template string
249+
embed bool
250+
}{
251+
{fmt.Sprintf("python-%s.pc", version), normalTemplate, false},
252+
{fmt.Sprintf("python-%s-embed.pc", version), embedTemplate, true},
253+
{"python3.pc", normalTemplate, false},
254+
{"python3-embed.pc", embedTemplate, true},
255+
}
256+
filePairs = append(filePairs, additionalPairs...)
220257
}
221258

222259
// Write all pkg-config files
223-
for _, file := range files {
224-
pcPath := filepath.Join(pkgConfigDir, file.name)
225-
if err := os.WriteFile(pcPath, []byte(file.content), 0644); err != nil {
226-
return fmt.Errorf("failed to write %s: %v", file.name, err)
260+
for _, pair := range filePairs {
261+
pcPath := filepath.Join(pkgConfigDir, pair.name)
262+
var content string
263+
if pair.embed {
264+
content = fmt.Sprintf(pair.template, version, versionNoPoints, libSuffix)
265+
} else {
266+
content = fmt.Sprintf(pair.template, version, libSuffix)
267+
}
268+
269+
if err := os.WriteFile(pcPath, []byte(content), 0644); err != nil {
270+
return fmt.Errorf("failed to write %s: %v", pair.name, err)
227271
}
228272
}
229273

cmd/internal/install/python_test.go

Lines changed: 138 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"runtime"
78
"strings"
89
"testing"
910

@@ -131,7 +132,11 @@ func TestGetCacheDir(t *testing.T) {
131132

132133
t.Run("valid home directory", func(t *testing.T) {
133134
tmpDir := t.TempDir()
134-
os.Setenv("HOME", tmpDir)
135+
if runtime.GOOS == "windows" {
136+
os.Setenv("USERPROFILE", tmpDir)
137+
} else {
138+
os.Setenv("HOME", tmpDir)
139+
}
135140

136141
got, err := getCacheDir()
137142
if err != nil {
@@ -152,7 +157,11 @@ func TestGetCacheDir(t *testing.T) {
152157

153158
t.Run("invalid home directory", func(t *testing.T) {
154159
// Set HOME to a non-existent directory
155-
os.Setenv("HOME", "/nonexistent/path")
160+
if runtime.GOOS == "windows" {
161+
os.Setenv("USERPROFILE", "/nonexistent/path")
162+
} else {
163+
os.Setenv("HOME", "/nonexistent/path")
164+
}
156165

157166
_, err := getCacheDir()
158167
if err == nil {
@@ -162,18 +171,34 @@ func TestGetCacheDir(t *testing.T) {
162171
}
163172

164173
func TestUpdatePkgConfig(t *testing.T) {
165-
t.Run("valid pkg-config files", func(t *testing.T) {
174+
t.Run("freethreaded pkg-config files", func(t *testing.T) {
166175
// Create temporary directory structure
167176
tmpDir := t.TempDir()
168177
pkgConfigDir := env.GetPythonPkgConfigDir(tmpDir)
169178
if err := os.MkdirAll(pkgConfigDir, 0755); err != nil {
170179
t.Fatal(err)
171180
}
172181

173-
// Create test .pc files
182+
// Create test .pc files with freethreaded content
174183
testFiles := map[string]string{
175-
"python-3.13t.pc": "prefix=/install\nlibdir=${prefix}/lib\n",
176-
"python-3.13-embed.pc": "prefix=/install\nlibdir=${prefix}/lib\n",
184+
"python-3.13t.pc": `prefix=/install
185+
libdir=${prefix}/lib
186+
includedir=${prefix}/include
187+
188+
Name: Python
189+
Description: Python library
190+
Version: 3.13
191+
Libs: -L${libdir} -lpython3t
192+
Cflags: -I${includedir}`,
193+
"python-3.13t-embed.pc": `prefix=/install
194+
libdir=${prefix}/lib
195+
includedir=${prefix}/include
196+
197+
Name: Python
198+
Description: Embed Python into an application
199+
Version: 3.13
200+
Libs: -L${libdir} -lpython313t
201+
Cflags: -I${includedir}`,
177202
}
178203

179204
for filename, content := range testFiles {
@@ -189,19 +214,29 @@ func TestUpdatePkgConfig(t *testing.T) {
189214
}
190215

191216
// Verify the generated files
192-
expectedFiles := []string{
193-
"python-3.13t.pc",
194-
"python-3.13.pc",
195-
"python3t.pc",
196-
"python3.pc",
197-
"python-3.13-embed.pc",
198-
"python3-embed.pc",
217+
expectedFiles := map[string]struct {
218+
shouldExist bool
219+
libName string
220+
}{
221+
// Freethreaded versions
222+
"python-3.13t.pc": {true, "-lpython3t"},
223+
"python3t.pc": {true, "-lpython3t"},
224+
"python-3.13t-embed.pc": {true, "-lpython313t"},
225+
"python3t-embed.pc": {true, "-lpython313t"},
226+
// Non-t versions (same content as freethreaded)
227+
"python-3.13.pc": {true, "-lpython3t"},
228+
"python3.pc": {true, "-lpython3t"},
229+
"python-3.13-embed.pc": {true, "-lpython313t"},
230+
"python3-embed.pc": {true, "-lpython313t"},
199231
}
200232

201-
for _, filename := range expectedFiles {
233+
absPath, _ := filepath.Abs(filepath.Join(tmpDir, ".deps/python"))
234+
for filename, expected := range expectedFiles {
202235
path := filepath.Join(pkgConfigDir, filename)
203236
if _, err := os.Stat(path); os.IsNotExist(err) {
204-
t.Errorf("Expected file %s was not created", filename)
237+
if expected.shouldExist {
238+
t.Errorf("Expected file %s was not created", filename)
239+
}
205240
continue
206241
}
207242

@@ -211,11 +246,98 @@ func TestUpdatePkgConfig(t *testing.T) {
211246
continue
212247
}
213248

214-
absPath, _ := filepath.Abs(filepath.Join(tmpDir, ".deps/python"))
249+
// Check prefix
215250
expectedPrefix := fmt.Sprintf("prefix=%s", absPath)
216251
if !strings.Contains(string(content), expectedPrefix) {
217252
t.Errorf("File %s does not contain expected prefix %s", filename, expectedPrefix)
218253
}
254+
255+
// Check library name
256+
if !strings.Contains(string(content), expected.libName) {
257+
t.Errorf("File %s does not contain expected library name %s", filename, expected.libName)
258+
}
259+
}
260+
})
261+
262+
t.Run("non-freethreaded pkg-config files", func(t *testing.T) {
263+
// Create temporary directory structure
264+
tmpDir := t.TempDir()
265+
pkgConfigDir := env.GetPythonPkgConfigDir(tmpDir)
266+
if err := os.MkdirAll(pkgConfigDir, 0755); err != nil {
267+
t.Fatal(err)
268+
}
269+
270+
// Create test .pc files with non-freethreaded content
271+
testFiles := map[string]string{
272+
"python-3.13.pc": `prefix=/install
273+
libdir=${prefix}/lib
274+
includedir=${prefix}/include
275+
276+
Name: Python
277+
Description: Python library
278+
Version: 3.13
279+
Libs: -L${libdir} -lpython3
280+
Cflags: -I${includedir}`,
281+
"python-3.13-embed.pc": `prefix=/install
282+
libdir=${prefix}/lib
283+
includedir=${prefix}/include
284+
285+
Name: Python
286+
Description: Embed Python into an application
287+
Version: 3.13
288+
Libs: -L${libdir} -lpython313
289+
Cflags: -I${includedir}`,
290+
}
291+
292+
for filename, content := range testFiles {
293+
if err := os.WriteFile(filepath.Join(pkgConfigDir, filename), []byte(content), 0644); err != nil {
294+
t.Fatal(err)
295+
}
296+
}
297+
298+
// Test updating pkg-config files
299+
if err := updatePkgConfig(tmpDir); err != nil {
300+
t.Errorf("updatePkgConfig() error = %v, want nil", err)
301+
return
302+
}
303+
304+
// Verify the generated files
305+
expectedFiles := map[string]struct {
306+
shouldExist bool
307+
libName string
308+
}{
309+
"python-3.13.pc": {true, "-lpython3"},
310+
"python3.pc": {true, "-lpython3"},
311+
"python-3.13-embed.pc": {true, "-lpython313"},
312+
"python3-embed.pc": {true, "-lpython313"},
313+
}
314+
315+
absPath, _ := filepath.Abs(filepath.Join(tmpDir, ".deps/python"))
316+
for filename, expected := range expectedFiles {
317+
path := filepath.Join(pkgConfigDir, filename)
318+
if _, err := os.Stat(path); os.IsNotExist(err) {
319+
if expected.shouldExist {
320+
t.Errorf("Expected file %s was not created", filename)
321+
}
322+
continue
323+
}
324+
325+
content, err := os.ReadFile(path)
326+
if err != nil {
327+
t.Errorf("Failed to read file %s: %v", filename, err)
328+
continue
329+
}
330+
331+
// Check prefix
332+
expectedPrefix := fmt.Sprintf("prefix=%s", absPath)
333+
if !strings.Contains(string(content), expectedPrefix) {
334+
t.Errorf("File %s does not contain expected prefix %s", filename, expectedPrefix)
335+
}
336+
337+
// Check library name
338+
if !strings.Contains(string(content), expected.libName) {
339+
t.Errorf("File %s does not contain expected library name %s", filename, expected.libName)
340+
}
219341
}
220342
})
221343

0 commit comments

Comments
 (0)