Skip to content

Commit 433d9e6

Browse files
authored
feat(internal/pip): implement support for local pip project installations (#6126)
This pull request implements support for installing local Python package paths via pip. It is particularly useful for configuring the java installer package to fetch and build local pip projects such as synthtool directly from sdk-platform-java/hermetic_build/library_generation without spawning external remote download calls. Key Changes: 1. Adds the LocalPath configuration schema field inside config.go and doc/config-schema.md. 2. Implements LocalPath folder validations and absolute path resolutions inside internal/pip/pip.go. 3. Integrates local_path configuration inside internal/librarian/java/librarian.yaml. 4. Updates pip_test.go and install_test.go to pre-create sandboxed paths and assert correct pip arguments. For #5154
1 parent dc6418b commit 433d9e6

6 files changed

Lines changed: 52 additions & 5 deletions

File tree

doc/config-schema.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ This document describes the schema for the librarian.yaml.
7878
| `name` | string | Is the pip package name. |
7979
| `version` | string | Is the version to install. |
8080
| `package` | string | Is the pip install specifier (e.g., "pkg@git+https://..."). |
81+
| `local_path` | string | Is the path to a local Python package to install. |
8182

8283
## GoTool Configuration
8384

internal/config/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ type PipTool struct {
170170

171171
// Package is the pip install specifier (e.g., "pkg@git+https://...").
172172
Package string `yaml:"package,omitempty"`
173+
174+
// LocalPath is the path to a local Python package to install.
175+
LocalPath string `yaml:"local_path,omitempty"`
173176
}
174177

175178
// GoTool defines a tool to install via go.

internal/librarian/java/install_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,13 @@ import (
2626

2727
func TestInstall(t *testing.T) {
2828
tmpDir := t.TempDir()
29+
t.Chdir(tmpDir)
2930
tempHome := t.TempDir()
3031
t.Setenv("HOME", tempHome)
32+
localPkgDir := filepath.Join(tmpDir, "sdk-platform-java", "hermetic_build", "library_generation")
33+
if err := os.MkdirAll(localPkgDir, 0755); err != nil {
34+
t.Fatal(err)
35+
}
3136
m2Repo := filepath.Join(tempHome, ".m2", "repository")
3237
gjfDir := filepath.Join(m2Repo, "com", "google", "googlejavaformat", "google-java-format", "1.25.2")
3338
if err := os.MkdirAll(gjfDir, 0755); err != nil {
@@ -53,7 +58,7 @@ func TestInstall(t *testing.T) {
5358
{
5459
name: "pip",
5560
logFilename: "pip_invocations.log",
56-
wantArgs: "pip install PyYAML==6.0.2 jinja2==3.1.6",
61+
wantArgs: "pip install PyYAML==6.0.2 jinja2==3.1.6 " + localPkgDir,
5762
},
5863
{
5964
name: "mvn",

internal/librarian/java/librarian.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,5 @@ tools:
3535
version: "6.0.2"
3636
- name: jinja2
3737
version: "3.1.6"
38+
- name: synthtool
39+
local_path: sdk-platform-java/hermetic_build/library_generation

internal/pip/pip.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,36 @@ import (
1919
"context"
2020
"errors"
2121
"fmt"
22+
"os"
23+
"path/filepath"
2224

2325
"github.com/googleapis/librarian/internal/command"
2426
"github.com/googleapis/librarian/internal/config"
2527
)
2628

27-
// ErrInstall indicates a failure to install pip packages.
28-
var ErrInstall = errors.New("failed to install python packages")
29+
var (
30+
// ErrInstall indicates a failure to install pip packages.
31+
ErrInstall = errors.New("failed to install python packages")
32+
33+
// ErrLocalPathNotFound indicates that the specified local package path does not exist.
34+
ErrLocalPathNotFound = errors.New("local pip package path not found")
35+
)
2936

3037
// Install installs a list of pip tools into the environment.
3138
func Install(ctx context.Context, tools []*config.PipTool) error {
3239
var installTargets []string
3340
for _, tool := range tools {
41+
if tool.LocalPath != "" {
42+
absPath, err := filepath.Abs(tool.LocalPath)
43+
if err != nil {
44+
return fmt.Errorf("failed to resolve absolute path for %s: %w", tool.LocalPath, err)
45+
}
46+
if _, err := os.Stat(absPath); err != nil {
47+
return fmt.Errorf("%w: %w", ErrLocalPathNotFound, err)
48+
}
49+
installTargets = append(installTargets, absPath)
50+
continue
51+
}
3452
if tool.Package != "" {
3553
installTargets = append(installTargets, tool.Package)
3654
continue

internal/pip/pip_test.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import (
2929
func TestInstall(t *testing.T) {
3030
tmpDir := t.TempDir()
3131
stubLogPath := filepath.Join(tmpDir, "pip_invocations.log")
32-
stubContent := fmt.Sprintf(`#!/bin/bash
32+
stubContent := fmt.Sprintf(`#!/bin/sh
3333
echo "pip $@" >> %q
3434
`, stubLogPath)
3535
stubDir := filepath.Join(tmpDir, "bin")
@@ -41,6 +41,10 @@ echo "pip $@" >> %q
4141
t.Fatal(err)
4242
}
4343
t.Setenv("PATH", stubDir)
44+
localPkgPath := filepath.Join(tmpDir, "mylocalpkg")
45+
if err := os.MkdirAll(localPkgPath, 0755); err != nil {
46+
t.Fatal(err)
47+
}
4448
for _, test := range []struct {
4549
name string
4650
tools []*config.PipTool
@@ -68,6 +72,13 @@ echo "pip $@" >> %q
6872
},
6973
wantArgs: "install requests",
7074
},
75+
{
76+
name: "install local package path",
77+
tools: []*config.PipTool{
78+
{Name: "synthtool", LocalPath: localPkgPath},
79+
},
80+
wantArgs: "install " + localPkgPath,
81+
},
7182
} {
7283
t.Run(test.name, func(t *testing.T) {
7384
_ = os.Remove(stubLogPath)
@@ -95,7 +106,7 @@ func TestInstall_Error(t *testing.T) {
95106
t.Fatal(err)
96107
}
97108
stubPath := filepath.Join(stubDir, "pip")
98-
if err := os.WriteFile(stubPath, []byte("#!/bin/bash\nexit 1\n"), 0755); err != nil {
109+
if err := os.WriteFile(stubPath, []byte("#!/bin/sh\nexit 1\n"), 0755); err != nil {
99110
t.Fatal(err)
100111
}
101112
for _, test := range []struct {
@@ -114,6 +125,13 @@ func TestInstall_Error(t *testing.T) {
114125
},
115126
wantErr: ErrInstall,
116127
},
128+
{
129+
name: "local path not found",
130+
tools: []*config.PipTool{
131+
{Name: "failpkg", LocalPath: filepath.Join(tmpDir, "nonexistentpkg")},
132+
},
133+
wantErr: ErrLocalPathNotFound,
134+
},
117135
} {
118136
t.Run(test.name, func(t *testing.T) {
119137
if test.setup != nil {

0 commit comments

Comments
 (0)