Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion buildtools/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -2130,7 +2130,7 @@ func pythonCmd(c *cli.Context, projectType project.ProjectType) error {
workingDir, err := os.Getwd()
if err != nil {
log.Warn("Failed to get working directory, skipping build info collection: " + err.Error())
} else if err := buildinfo.GetPoetryBuildInfo(workingDir, buildConfiguration, deployerRepo); err != nil {
} else if err := buildinfo.GetPoetryBuildInfo(workingDir, buildConfiguration, deployerRepo, cmdName, poetryArgs); err != nil {
log.Warn("Failed to collect Poetry build info: " + err.Error())
} else {
buildNumber, err := buildConfiguration.GetBuildNumber()
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ require (

// replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260416054314-f941180fccc3

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

// replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260518123155-036d9195c4e9

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -400,8 +400,8 @@ github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP
github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4=
github.com/jfrog/archiver/v3 v3.6.3 h1:hkAmPjBw393tPmQ07JknLNWFNZjXdy2xFEnOW9wwOxI=
github.com/jfrog/archiver/v3 v3.6.3/go.mod h1:5V9l+Fte30Y4qe9dUOAd3yNTf8lmtVNuhKNrvI8PMhg=
github.com/jfrog/build-info-go v1.13.1-0.20260528065004-80409c046540 h1:yJjTgSfmsBx9Q6/iiJxXQ/m0KZfFjNx8nNzaRLCM7z4=
github.com/jfrog/build-info-go v1.13.1-0.20260528065004-80409c046540/go.mod h1:CYRUCvLKfyARjoJXLWAxce1qNUxTEtbRKAARkV42vpE=
github.com/jfrog/build-info-go v1.13.1-0.20260603044611-897a097a7031 h1:/OkgqPsrHP05VWj/o+nygOj1eMxtvq7So7iEtKIUO/o=
github.com/jfrog/build-info-go v1.13.1-0.20260603044611-897a097a7031/go.mod h1:CYRUCvLKfyARjoJXLWAxce1qNUxTEtbRKAARkV42vpE=
github.com/jfrog/froggit-go v1.22.0 h1:eeN5F8sOUo+h2cXkzArAu4nvSdjkDTAZtgqwrct70qg=
github.com/jfrog/froggit-go v1.22.0/go.mod h1:wRDryqyp3oe+eHgME2mpnEQmO8XBECIPagFwj0nHmdI=
github.com/jfrog/go-mockhttp v0.3.1 h1:/wac8v4GMZx62viZmv4wazB5GNKs+GxawuS1u3maJH8=
Expand Down
100 changes: 94 additions & 6 deletions utils/buildinfo/buildinfo.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package buildinfo

import (
"encoding/json"
"fmt"
"net/url"
"os"
Expand Down Expand Up @@ -44,7 +45,9 @@ func GetBuildInfoForPackageManager(pkgManager, workingDir string, buildConfigura

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

// GetPoetryBuildInfo collects build info for Poetry projects
func GetPoetryBuildInfo(workingDir string, buildConfiguration *buildUtils.BuildConfiguration, deployerRepo string) error {
// GetPoetryBuildInfo collects build info for Poetry projects.
//
// cmdName and args describe the poetry sub-command that just ran (e.g. "install",
// "--only", "main"). They are used to decide whether to query the active poetry
// venv for the ground-truth installed set — which is how we honor poetry's
// --only/--without/--with flags in build-info, mirroring the proven UV pattern.
// When cmdName is empty (legacy entry points), no ground-truth query is made
// and the existing lock-file-driven behaviour is preserved.
func GetPoetryBuildInfo(workingDir string, buildConfiguration *buildUtils.BuildConfiguration, deployerRepo, cmdName string, args []string) error {
log.Debug("Collecting Poetry build info from directory: " + workingDir)
log.Debug("Deployer repository: " + deployerRepo)

Expand Down Expand Up @@ -102,7 +112,17 @@ func GetPoetryBuildInfo(workingDir string, buildConfiguration *buildUtils.BuildC
}
log.Info(fmt.Sprintf("Using repository for artifacts: %s", artifactRepo))

err = collectPoetryBuildInfo(workingDir, buildName, buildNumber, serverDetails, repoConfig.TargetRepo(), artifactRepo, buildConfiguration)
// For venv-modifying commands (install/add/remove/update/sync), capture the
// real installed set from poetry so build-info reflects what was actually
// installed — honouring --only/--without/--with without any flag parsing.
// For other commands and on any error this stays nil → legacy behaviour.
var installed map[string]string
_ = args // reserved for future per-arg behaviour; ground truth comes from poetry itself
if poetryModifiesVenv(cmdName) {
installed = poetryInstalledPackages(workingDir)
}

err = collectPoetryBuildInfo(workingDir, buildName, buildNumber, serverDetails, repoConfig.TargetRepo(), artifactRepo, installed, buildConfiguration)
if err != nil {
log.Warn("Enhanced Poetry collection failed, falling back to standard method: " + err.Error())
err = saveBuildInfo(serverDetails, artifactRepo, "", buildConfiguration)
Expand Down Expand Up @@ -359,14 +379,19 @@ func CreateAqlQueryForSearch(repo, file string) string {
return fmt.Sprintf(itemsPart, repo, file)
}

// collectPoetryBuildInfo collects Poetry dependencies and artifacts for build info
func collectPoetryBuildInfo(workingDir, buildName, buildNumber string, serverDetails *config.ServerDetails, _ string, artifactRepo string, buildConfiguration *buildUtils.BuildConfiguration) error {
// collectPoetryBuildInfo collects Poetry dependencies and artifacts for build info.
//
// installedPackages, when non-nil, is the ground-truth set captured from
// `poetry run pip list`. Only lockfile entries present in this map are included
// in build-info, which is how --only/--without/--with are honoured.
func collectPoetryBuildInfo(workingDir, buildName, buildNumber string, serverDetails *config.ServerDetails, _ string, artifactRepo string, installedPackages map[string]string, buildConfiguration *buildUtils.BuildConfiguration) error {
log.Debug("Initializing Poetry dependency collection...")

// Create Poetry configuration
config := flexpack.PoetryConfig{
WorkingDirectory: workingDir,
IncludeDevDependencies: false, // Match standard behavior
InstalledPackages: installedPackages,
}

// Create Poetry instance
Expand Down Expand Up @@ -799,3 +824,66 @@ func extractRepoNameFromPypiURL(urlStr string) string {

return ""
}

// poetryModifiesVenv reports whether the named poetry sub-command actually
// installs/uninstalls packages into the venv. Only for these commands does
// querying the venv for the installed set make sense — for lock/build/publish
// etc. the venv state is unrelated to the operation.
func poetryModifiesVenv(cmdName string) bool {
switch cmdName {
case "install", "add", "remove", "update", "sync":
return true
}
return false
}

// poetryPipPackage is the shape of one entry in `pip list --format=json`.
type poetryPipPackage struct {
Name string `json:"name"`
Version string `json:"version"`
}

// poetryInstalledPackages runs `poetry run pip list --format=json` inside
// workingDir and returns the installed set as normalised name → version.
// Normalisation matches PEP 503 (lowercase, runs of [-_.] collapsed to "-"),
// the same normalisation applied to lockfile names by PoetryFlexPack.
// Returns nil on any error so the caller falls back to lock-file-driven
// resolution (no regression).
func poetryInstalledPackages(workingDir string) map[string]string {
cmd := exec.Command("poetry", "run", "pip", "list", "--format=json")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please try to use poetry show with all the flags from poetry install command being flushed into it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. poetry show doesn't accept all poetry install flags. The reproduction command from the bug report uses --no-root, and $ poetry show --only main --no-root --format=json
    The option "--no-root" does not exist
    Other install flags poetry show rejects: --no-directory, --sync, --dry-run, --all-extras, --all-groups, --only-root, --compile, -E.

  2. poetry show reads the lockfile, not the venv. Test on a venv with nothing installed:
    $ poetry show --only main --format=json
    [{"name": "numpy", "installed_status": "not-installed", "version": "1.26.1", ...}]
    It returns numpy even though numpy isn't installed. installed_status: "not-installed" is the giveaway poetry show is filtering the lockfile by group, not listing the venv. So it wouldn't reflect --no-root, partial-install recovery, or any non-flag-driven divergence.

cmd.Dir = workingDir
out, err := cmd.Output()
if err != nil {
log.Debug("Poetry build-info: 'poetry run pip list' failed, falling back to lock-file resolution: " + err.Error())
return nil
}
var list []poetryPipPackage
if err := json.Unmarshal(out, &list); err != nil {
log.Debug("Poetry build-info: failed to parse 'pip list' output: " + err.Error())
return nil
}
installed := make(map[string]string, len(list))
for _, p := range list {
installed[normalizePoetryPipName(p.Name)] = p.Version
}
return installed
}

func normalizePoetryPipName(name string) string {
lower := strings.ToLower(name)
var b strings.Builder
b.Grow(len(lower))
prevSep := false
for _, r := range lower {
if r == '-' || r == '_' || r == '.' {
if !prevSep && b.Len() > 0 {
b.WriteByte('-')
}
prevSep = true
continue
}
b.WriteRune(r)
prevSep = false
}
return strings.TrimSuffix(b.String(), "-")
}
Loading