Skip to content

Commit dcd1b06

Browse files
authored
RTECO-1287 jf poetry install --only main captures all dependency (#3530)
1 parent d3c7b94 commit dcd1b06

6 files changed

Lines changed: 234 additions & 13 deletions

File tree

buildtools/cli.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2187,7 +2187,7 @@ func pythonCmd(c *cli.Context, projectType project.ProjectType) error {
21872187
workingDir, err := os.Getwd()
21882188
if err != nil {
21892189
log.Warn("Failed to get working directory, skipping build info collection: " + err.Error())
2190-
} else if err := buildinfo.GetPoetryBuildInfo(workingDir, buildConfiguration, deployerRepo); err != nil {
2190+
} else if err := buildinfo.GetPoetryBuildInfo(workingDir, buildConfiguration, deployerRepo, cmdName, poetryArgs); err != nil {
21912191
log.Warn("Failed to collect Poetry build info: " + err.Error())
21922192
} else {
21932193
buildNumber, err := buildConfiguration.GetBuildNumber()

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ require (
1818
github.com/buger/jsonparser v1.2.0
1919
github.com/gocarina/gocsv v0.0.0-20260523204920-c264028e67ea
2020
github.com/jfrog/archiver/v3 v3.6.3
21-
github.com/jfrog/build-info-go v1.13.1-0.20260528065004-80409c046540
21+
github.com/jfrog/build-info-go v1.13.1-0.20260609173330-a8a3ed3919af
2222
github.com/jfrog/gofrog v1.7.6
2323
github.com/jfrog/jfrog-cli-application v1.0.2-0.20260608074325-4de652aef752
2424
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260609101705-321f68d15a6d
@@ -248,7 +248,7 @@ require (
248248

249249
// replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260608132618-003a2af3b8a3
250250

251-
// replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.13.1-0.20260428071432-1e9d9a1991ad
251+
// replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.13.1-0.20260603044611-897a097a7031
252252

253253
// replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260604085947-7c110b77b4b4
254254

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -394,8 +394,8 @@ github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP
394394
github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4=
395395
github.com/jfrog/archiver/v3 v3.6.3 h1:hkAmPjBw393tPmQ07JknLNWFNZjXdy2xFEnOW9wwOxI=
396396
github.com/jfrog/archiver/v3 v3.6.3/go.mod h1:5V9l+Fte30Y4qe9dUOAd3yNTf8lmtVNuhKNrvI8PMhg=
397-
github.com/jfrog/build-info-go v1.13.1-0.20260528065004-80409c046540 h1:yJjTgSfmsBx9Q6/iiJxXQ/m0KZfFjNx8nNzaRLCM7z4=
398-
github.com/jfrog/build-info-go v1.13.1-0.20260528065004-80409c046540/go.mod h1:CYRUCvLKfyARjoJXLWAxce1qNUxTEtbRKAARkV42vpE=
397+
github.com/jfrog/build-info-go v1.13.1-0.20260609173330-a8a3ed3919af h1:ubWSqnwIip7pdr6PybXiynb3rUVwdNBYEGnb23JA0L4=
398+
github.com/jfrog/build-info-go v1.13.1-0.20260609173330-a8a3ed3919af/go.mod h1:CYRUCvLKfyARjoJXLWAxce1qNUxTEtbRKAARkV42vpE=
399399
github.com/jfrog/froggit-go v1.22.0 h1:eeN5F8sOUo+h2cXkzArAu4nvSdjkDTAZtgqwrct70qg=
400400
github.com/jfrog/froggit-go v1.22.0/go.mod h1:wRDryqyp3oe+eHgME2mpnEQmO8XBECIPagFwj0nHmdI=
401401
github.com/jfrog/go-mockhttp v0.3.1 h1:/wac8v4GMZx62viZmv4wazB5GNKs+GxawuS1u3maJH8=

poetry_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -711,8 +711,8 @@ func setPoetryHTTPBasicAuth(t *testing.T) func() {
711711
// `go test` process even when several FlexPack-mode tests invoke
712712
// buildJfBinaryAndAddToPath sequentially.
713713
var (
714-
jfBinaryOnce sync.Once
715-
jfBinaryDir string
714+
jfBinaryOnce sync.Once
715+
jfBinaryDir string
716716
jfBinaryBuildErr error
717717
)
718718

utils/buildinfo/buildinfo.go

Lines changed: 137 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package buildinfo
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"net/url"
67
"os"
@@ -44,7 +45,9 @@ func GetBuildInfoForPackageManager(pkgManager, workingDir string, buildConfigura
4445

4546
switch pkgManager {
4647
case "poetry":
47-
return GetPoetryBuildInfo(workingDir, buildConfiguration, "") // Empty deployer repo - will use from pyproject.toml
48+
// No cmd/args context available from this entry point — falls back to
49+
// the existing lock-file-driven behaviour (no installed-set filtering).
50+
return GetPoetryBuildInfo(workingDir, buildConfiguration, "", "", nil) // Empty deployer repo - will use from pyproject.toml
4851
case "mvn", "maven":
4952
// Maven FlexPack is handled directly in jfrog-cli-artifactory Maven command
5053
return GetBuildInfoForUploadedArtifacts("", buildConfiguration)
@@ -61,8 +64,12 @@ func GetBuildInfoForPackageManager(pkgManager, workingDir string, buildConfigura
6164
}
6265
}
6366

64-
// GetPoetryBuildInfo collects build info for Poetry projects
65-
func GetPoetryBuildInfo(workingDir string, buildConfiguration *buildUtils.BuildConfiguration, deployerRepo string) error {
67+
// GetPoetryBuildInfo collects build info for Poetry projects.
68+
//
69+
// cmdName and args describe the poetry sub-command that just ran (e.g. "install",
70+
// "--only", "main"). They are used to decide whether to query poetry for the
71+
// group-filtered installed set
72+
func GetPoetryBuildInfo(workingDir string, buildConfiguration *buildUtils.BuildConfiguration, deployerRepo, cmdName string, args []string) error {
6673
log.Debug("Collecting Poetry build info from directory: " + workingDir)
6774
log.Debug("Deployer repository: " + deployerRepo)
6875

@@ -102,7 +109,16 @@ func GetPoetryBuildInfo(workingDir string, buildConfiguration *buildUtils.BuildC
102109
}
103110
log.Info(fmt.Sprintf("Using repository for artifacts: %s", artifactRepo))
104111

105-
err = collectPoetryBuildInfo(workingDir, buildName, buildNumber, serverDetails, repoConfig.TargetRepo(), artifactRepo, buildConfiguration)
112+
// For venv-modifying commands (install/add/remove/update/sync), capture the
113+
// group-filtered set from `poetry show`, forwarding the same
114+
// --only/--with/--without flags that were passed to the poetry command so
115+
// build-info reflects exactly the groups that were installed.
116+
var installed map[string]string
117+
if poetryModifiesVenv(cmdName) {
118+
installed = poetryInstalledPackages(workingDir, args)
119+
}
120+
121+
err = collectPoetryBuildInfo(workingDir, buildName, buildNumber, serverDetails, repoConfig.TargetRepo(), artifactRepo, installed, buildConfiguration)
106122
if err != nil {
107123
log.Warn("Enhanced Poetry collection failed, falling back to standard method: " + err.Error())
108124
err = saveBuildInfo(serverDetails, artifactRepo, "", buildConfiguration)
@@ -359,14 +375,20 @@ func CreateAqlQueryForSearch(repo, file string) string {
359375
return fmt.Sprintf(itemsPart, repo, file)
360376
}
361377

362-
// collectPoetryBuildInfo collects Poetry dependencies and artifacts for build info
363-
func collectPoetryBuildInfo(workingDir, buildName, buildNumber string, serverDetails *config.ServerDetails, _ string, artifactRepo string, buildConfiguration *buildUtils.BuildConfiguration) error {
378+
// collectPoetryBuildInfo collects Poetry dependencies and artifacts for build info.
379+
//
380+
// installedPackages, when non-nil, is the group-filtered set captured from
381+
// `poetry show` (with the install command's --only/--with/--without flags
382+
// forwarded). Only lockfile entries present in this map are included in
383+
// build-info, which is how --only/--without/--with are honoured.
384+
func collectPoetryBuildInfo(workingDir, buildName, buildNumber string, serverDetails *config.ServerDetails, _ string, artifactRepo string, installedPackages map[string]string, buildConfiguration *buildUtils.BuildConfiguration) error {
364385
log.Debug("Initializing Poetry dependency collection...")
365386

366387
// Create Poetry configuration
367388
config := flexpack.PoetryConfig{
368389
WorkingDirectory: workingDir,
369390
IncludeDevDependencies: false, // Match standard behavior
391+
InstalledPackages: installedPackages,
370392
}
371393

372394
// Create Poetry instance
@@ -799,3 +821,112 @@ func extractRepoNameFromPypiURL(urlStr string) string {
799821

800822
return ""
801823
}
824+
825+
// poetryModifiesVenv reports whether the named poetry sub-command actually
826+
// installs/uninstalls packages into the venv. Only for these commands does
827+
// querying poetry for the group-filtered installed set make sense — for
828+
// lock/build/publish etc. the installed state is unrelated to the operation.
829+
func poetryModifiesVenv(cmdName string) bool {
830+
switch cmdName {
831+
case "install", "add", "remove", "update", "sync":
832+
return true
833+
}
834+
return false
835+
}
836+
837+
// poetryShowPackage is the shape of one entry in `poetry show --format json`.
838+
// poetry show emits more fields (installed_status, description, ...) but only
839+
// name and version are needed here.
840+
type poetryShowPackage struct {
841+
Name string `json:"name"`
842+
Version string `json:"version"`
843+
}
844+
845+
// poetryShowGroupArgs extracts the dependency-group filter flags
846+
// (--only/--with/--without and their values) from the poetry command args so
847+
// they can be forwarded verbatim to `poetry show`. Other install flags are not
848+
// valid for `poetry show` and are intentionally dropped.
849+
//
850+
// includeAll is true when the args request every group (e.g. --all-groups), in
851+
// which case no `poetry show` filtering is needed — the legacy include-all
852+
// behaviour already produces the correct result.
853+
func poetryShowGroupArgs(args []string) (groupArgs []string, includeAll bool) {
854+
groupFlags := map[string]bool{"--only": true, "--with": true, "--without": true}
855+
for i := 0; i < len(args); i++ {
856+
a := args[i]
857+
if a == "--all-groups" {
858+
return nil, true
859+
}
860+
// --flag=value form
861+
if eq := strings.IndexByte(a, '='); eq > 0 {
862+
if groupFlags[a[:eq]] {
863+
groupArgs = append(groupArgs, a)
864+
}
865+
continue
866+
}
867+
// --flag value form
868+
if groupFlags[a] {
869+
groupArgs = append(groupArgs, a)
870+
if i+1 < len(args) {
871+
groupArgs = append(groupArgs, args[i+1])
872+
i++
873+
}
874+
}
875+
}
876+
return groupArgs, false
877+
}
878+
879+
// poetryInstalledPackages runs `poetry show --format json` inside workingDir,
880+
// forwarding the dependency-group filter flags (--only/--with/--without) that
881+
// were passed to the poetry command. poetry show resolves the activated groups
882+
// via its own solver, so the result is exactly the set produced by the
883+
// group-filtered install — honouring --only/--without/--with.
884+
//
885+
// Returns the set as normalised name → version. Normalisation matches PEP 503
886+
// (lowercase, runs of [-_.] collapsed to "-"), the same normalisation applied
887+
// to lockfile names by PoetryFlexPack. Returns nil on any error so the caller
888+
// falls back to lock-file-driven resolution (no regression).
889+
func poetryInstalledPackages(workingDir string, args []string) map[string]string {
890+
groupArgs, includeAll := poetryShowGroupArgs(args)
891+
if includeAll {
892+
// Every group requested → legacy include-all behaviour is already correct.
893+
return nil
894+
}
895+
cmdArgs := append([]string{"show", "--format", "json"}, groupArgs...)
896+
cmd := exec.Command("poetry", cmdArgs...)
897+
cmd.Dir = workingDir
898+
out, err := cmd.Output()
899+
if err != nil {
900+
log.Debug("Poetry build-info: 'poetry show' failed, falling back to lock-file resolution: " + err.Error())
901+
return nil
902+
}
903+
var list []poetryShowPackage
904+
if err := json.Unmarshal(out, &list); err != nil {
905+
log.Debug("Poetry build-info: failed to parse 'poetry show' output: " + err.Error())
906+
return nil
907+
}
908+
installed := make(map[string]string, len(list))
909+
for _, p := range list {
910+
installed[normalizePoetryPipName(p.Name)] = p.Version
911+
}
912+
return installed
913+
}
914+
915+
func normalizePoetryPipName(name string) string {
916+
lower := strings.ToLower(name)
917+
var b strings.Builder
918+
b.Grow(len(lower))
919+
prevSep := false
920+
for _, r := range lower {
921+
if r == '-' || r == '_' || r == '.' {
922+
if !prevSep && b.Len() > 0 {
923+
b.WriteByte('-')
924+
}
925+
prevSep = true
926+
continue
927+
}
928+
b.WriteRune(r)
929+
prevSep = false
930+
}
931+
return strings.TrimSuffix(b.String(), "-")
932+
}

utils/buildinfo/buildinfo_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package buildinfo
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
// TestPoetryShowGroupArgs verifies that only the dependency-group filter flags
9+
// (--only/--with/--without) are forwarded to `poetry show`, that both the
10+
// "--flag value" and "--flag=value" forms are handled, and that --all-groups
11+
// short-circuits to include-all (legacy behaviour).
12+
func TestPoetryShowGroupArgs(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
args []string
16+
wantGroupArgs []string
17+
wantIncludeAll bool
18+
}{
19+
{
20+
name: "only main, space form",
21+
args: []string{"--only", "main", "--no-root"},
22+
wantGroupArgs: []string{"--only", "main"},
23+
},
24+
{
25+
name: "only main, equals form",
26+
args: []string{"--only=main", "--no-root"},
27+
wantGroupArgs: []string{"--only=main"},
28+
},
29+
{
30+
name: "without dev",
31+
args: []string{"--without", "dev"},
32+
wantGroupArgs: []string{"--without", "dev"},
33+
},
34+
{
35+
name: "with optional, multiple group flags",
36+
args: []string{"--with", "docs", "--without", "dev"},
37+
wantGroupArgs: []string{"--with", "docs", "--without", "dev"},
38+
},
39+
{
40+
name: "all-groups short-circuits to include-all",
41+
args: []string{"--all-groups"},
42+
wantGroupArgs: nil,
43+
wantIncludeAll: true,
44+
},
45+
{
46+
name: "no group flags drops everything else",
47+
args: []string{"--no-root", "--compile", "-E", "extra"},
48+
wantGroupArgs: nil,
49+
},
50+
{
51+
name: "trailing group flag without value is forwarded as-is",
52+
args: []string{"--only"},
53+
wantGroupArgs: []string{"--only"},
54+
},
55+
{
56+
name: "empty args",
57+
args: nil,
58+
wantGroupArgs: nil,
59+
},
60+
}
61+
62+
for _, tt := range tests {
63+
t.Run(tt.name, func(t *testing.T) {
64+
gotGroupArgs, gotIncludeAll := poetryShowGroupArgs(tt.args)
65+
if gotIncludeAll != tt.wantIncludeAll {
66+
t.Errorf("includeAll = %v, want %v", gotIncludeAll, tt.wantIncludeAll)
67+
}
68+
if !reflect.DeepEqual(gotGroupArgs, tt.wantGroupArgs) {
69+
t.Errorf("groupArgs = %#v, want %#v", gotGroupArgs, tt.wantGroupArgs)
70+
}
71+
})
72+
}
73+
}
74+
75+
// TestNormalizePoetryPipName verifies PEP 503 normalisation: lowercase with runs
76+
// of [-_.] collapsed to a single "-" and no leading/trailing separator.
77+
func TestNormalizePoetryPipName(t *testing.T) {
78+
tests := map[string]string{
79+
"Requests": "requests",
80+
"ruamel.yaml": "ruamel-yaml",
81+
"my__weird..name": "my-weird-name",
82+
"Already-Normalised": "already-normalised",
83+
"typing_extensions": "typing-extensions",
84+
}
85+
for in, want := range tests {
86+
if got := normalizePoetryPipName(in); got != want {
87+
t.Errorf("normalizePoetryPipName(%q) = %q, want %q", in, got, want)
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)