mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
157 lines
4.2 KiB
Go
157 lines
4.2 KiB
Go
package packagemanager
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"slices"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||
|
|
"golang.org/x/mod/semver"
|
||
|
|
)
|
||
|
|
|
||
|
|
type GoPackageManagerConfig struct {
|
||
|
|
CommandName string
|
||
|
|
|
||
|
|
// InstallCommands accept module@version args on the command line, used to
|
||
|
|
// extract pinned versions for cooldown reporting.
|
||
|
|
InstallCommands []string
|
||
|
|
|
||
|
|
// NonDownloadCommands never load packages and therefore never fetch
|
||
|
|
// modules. Deliberately minimal: fmt/clean/vet/fix/doc/mod-graph load
|
||
|
|
// packages and can download already-required modules on a cold cache even
|
||
|
|
// under -mod=readonly, so they are excluded on purpose and run with the
|
||
|
|
// proxy.
|
||
|
|
NonDownloadCommands []string
|
||
|
|
}
|
||
|
|
|
||
|
|
func DefaultGoPackageManagerConfig() GoPackageManagerConfig {
|
||
|
|
return GoPackageManagerConfig{
|
||
|
|
CommandName: "go",
|
||
|
|
InstallCommands: []string{"get", "install", "run"},
|
||
|
|
NonDownloadCommands: []string{"version", "env", "help"},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
type goPackageManager struct {
|
||
|
|
Config GoPackageManagerConfig
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewGoPackageManager(config GoPackageManagerConfig) (*goPackageManager, error) {
|
||
|
|
return &goPackageManager{Config: config}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
var _ PackageManager = &goPackageManager{}
|
||
|
|
|
||
|
|
func (g *goPackageManager) Name() string {
|
||
|
|
return g.Config.CommandName
|
||
|
|
}
|
||
|
|
|
||
|
|
func (g *goPackageManager) Ecosystem() packagev1.Ecosystem {
|
||
|
|
return packagev1.Ecosystem_ECOSYSTEM_GO
|
||
|
|
}
|
||
|
|
|
||
|
|
func (g *goPackageManager) ParseCommand(args []string) (*ParsedCommand, error) {
|
||
|
|
if len(args) > 0 && args[0] == g.Config.CommandName {
|
||
|
|
args = args[1:]
|
||
|
|
}
|
||
|
|
|
||
|
|
parsed := &ParsedCommand{Command: Command{Exe: g.Config.CommandName, Args: args}}
|
||
|
|
|
||
|
|
subcmd, rest := goFirstNonFlagArg(args)
|
||
|
|
if subcmd == "" {
|
||
|
|
return parsed, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
if slices.Contains(g.Config.NonDownloadCommands, subcmd) {
|
||
|
|
parsed.IsKnownNonDownloadCommand = true
|
||
|
|
return parsed, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
if slices.Contains(g.Config.InstallCommands, subcmd) {
|
||
|
|
parsed.InstallTargets = goRemoteModuleTargets(rest)
|
||
|
|
return parsed, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
if subcmd == "mod" {
|
||
|
|
modCmd, modRest := goFirstNonFlagArg(rest)
|
||
|
|
switch modCmd {
|
||
|
|
case "tidy":
|
||
|
|
parsed.IsManifestInstall = true
|
||
|
|
case "download":
|
||
|
|
parsed.InstallTargets = goRemoteModuleTargets(modRest)
|
||
|
|
if len(parsed.InstallTargets) == 0 {
|
||
|
|
parsed.IsManifestInstall = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return parsed, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func goFirstNonFlagArg(args []string) (string, []string) {
|
||
|
|
for i, arg := range args {
|
||
|
|
if strings.HasPrefix(arg, "-") {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
return arg, args[i+1:]
|
||
|
|
}
|
||
|
|
return "", nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// goRemoteModuleTargets extracts remote module targets (module[@version]) from
|
||
|
|
// command args, skipping flags, local paths and meta-patterns. Version queries
|
||
|
|
// (@latest, branch names, commit hashes) are passed through for go to resolve;
|
||
|
|
// only canonical semver counts as an explicit version for cooldown reporting.
|
||
|
|
func goRemoteModuleTargets(args []string) []*PackageInstallTarget {
|
||
|
|
var targets []*PackageInstallTarget
|
||
|
|
|
||
|
|
for _, arg := range args {
|
||
|
|
if strings.HasPrefix(arg, "-") || !isGoRemoteModuleTarget(arg) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
name, version := arg, ""
|
||
|
|
if at := strings.LastIndex(arg, "@"); at > 0 {
|
||
|
|
name, version = arg[:at], arg[at+1:]
|
||
|
|
}
|
||
|
|
|
||
|
|
targets = append(targets, &PackageInstallTarget{
|
||
|
|
PackageVersion: &packagev1.PackageVersion{
|
||
|
|
Package: &packagev1.Package{
|
||
|
|
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_GO,
|
||
|
|
Name: name,
|
||
|
|
},
|
||
|
|
Version: version,
|
||
|
|
},
|
||
|
|
IsExplicitVersion: semver.IsValid(version) && semver.Canonical(version) == version,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
return targets
|
||
|
|
}
|
||
|
|
|
||
|
|
// isGoRemoteModuleTarget discriminates a remote module target from a local
|
||
|
|
// path or meta-pattern: a remote target's first path segment is a domain
|
||
|
|
// (contains a dot), so `go install ./cmd/foo` and `go build ./...` yield no
|
||
|
|
// targets while `go get github.com/x/y@v1.2.3` does.
|
||
|
|
func isGoRemoteModuleTarget(target string) bool {
|
||
|
|
target = strings.TrimSpace(target)
|
||
|
|
if target == "" || target == "." || target == ".." {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
if strings.HasPrefix(target, "./") || strings.HasPrefix(target, "../") || strings.HasPrefix(target, "/") {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
if strings.Contains(target, `\`) {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
firstSegment, _, _ := strings.Cut(target, "/")
|
||
|
|
if firstSegment == "..." {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
return strings.Contains(firstSegment, ".")
|
||
|
|
}
|