11package buildinfo
22
33import (
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+ }
0 commit comments