blob: 0fa5180b6ae493e7f0774d7d317045a1c484cdca [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package profilescmdline provides a command line driver (for v.io/x/lib/cmdline)
// for implementing jiri 'profile' subcommands. The intent is to support
// project specific instances of such profiles for managing software
// dependencies.
package profilescmdline
import (
"bytes"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"text/template"
"v.io/jiri"
"v.io/jiri/profiles"
"v.io/jiri/profiles/profilesreader"
"v.io/x/lib/cmdline"
"v.io/x/lib/textutil"
)
// IsFlagSet returns true if the specified flag has been set on
// the command line.
func IsFlagSet(fs *flag.FlagSet, name string) bool {
found := false
fs.Visit(func(f *flag.Flag) {
if f.Name == name {
found = true
}
})
return found
}
// NOTE: we use functions to initialize the commands so that we
// can reinitialize them in tests. cmd_test.go contains a 'Reset' function
// that is only available to tests for doing so.
// NOTE: we can't set cmdList.Runner in the initialization loop since runList
// needs to access cmdList.Flags.
var (
// cmdList represents the "profile list" command.
cmdList *cmdline.Command
// cmdEnv represents the "profile env" command.
cmdEnv *cmdline.Command = newCmdEnv()
)
func init() {
cmdList = newCmdList()
cmdList.Runner = jiri.RunnerFunc(runList)
}
func newCmdList() *cmdline.Command {
return &cmdline.Command{
Name: "list",
Short: "List available or installed profiles",
Long: "List available or installed profiles.",
ArgsName: "[<profiles>]",
ArgsLong: `<profiles> is a list of profiles to list, defaulting to all
profiles if none are specifically requested. List can also be used
to test for the presence of a specific target for the requested profiles.
If the target is not installed, it will exit with an error.`,
}
}
func newCmdEnv() *cmdline.Command {
// cmdEnv represents the "profile env" command.
return &cmdline.Command{
Runner: jiri.RunnerFunc(runEnv),
Name: "env",
Short: "Display profile environment variables",
Long: `
List profile specific and target specific environment variables. If the
requested environment variable name ends in = then only the value will
be printed, otherwise both name and value are printed, i.e. GOPATH="foo" vs
just "foo".
If no environment variable names are requested then all will be printed
in <name>=<val> format.
`,
ArgsName: "[<environment variable names>]",
ArgsLong: "[<environment variable names>] is an optional list of environment variables to display",
}
}
// ReaderFlagValues contains the values of the command line flags accepted
// required to configure and use the profiles/Reader package.
type ReaderFlagValues struct {
// The value of --skip-profiles
ProfilesMode profilesreader.ProfilesMode
// The value of --profiles-db
DBFilename string
// The value of --profiles
Profiles string
// The value of --target and --env
Target profiles.Target
// The value of --merge-policies
MergePolicies profilesreader.MergePolicies
// The value of -v
Verbose bool
}
// listFlagValues contains the flag values expected by the list subcommand
type listFlagValues struct {
*ReaderFlagValues
// The value of --info
info string
}
// envFlagValues contains the flag values expected by the env subcommand
type envFlagValues struct {
*ReaderFlagValues
}
// All flag values are stored in listFlags and envFlags.
var (
listFlags listFlagValues
envFlags envFlagValues
)
// RegisterDBPathFlag registers the --profiles-db flag with the supplied FlagSet.
func RegisterDBPathFlag(flags *flag.FlagSet, manifest *string, defaultDBPath string) {
root := jiri.FindRoot()
flags.StringVar(manifest, "profiles-db", filepath.Join(root, defaultDBPath), "the path, relative to JIRI_ROOT, that contains the profiles database.")
flags.Lookup("profiles-db").DefValue = filepath.Join("$JIRI_ROOT", defaultDBPath)
}
// RegisterProfilesFlag registers the --profiles flag
func RegisterProfilesFlag(flags *flag.FlagSet, profiles *string) {
// TODO(cnicolaou): delete this when the new profiles are in use.
root := jiri.FindRoot()
fi, err := os.Stat(filepath.Join(root, jiri.ProfilesDBDir))
defaultProfiles := "base,jiri"
if err == nil && fi.IsDir() {
// TODO(cnicolaou): we need a better way of setting the default profiles,
// ideally via the profiles db, or some other config. Provide a command
// line tool for setting the default profiles.
defaultProfiles = "v23:base,jiri"
}
flags.StringVar(profiles, "profiles", defaultProfiles, "a comma separated list of profiles to use")
}
// RegisterMergePoliciesFlag registers the --merge-policies flag
func RegisterMergePoliciesFlag(flags *flag.FlagSet, policies *profilesreader.MergePolicies) {
flags.Var(policies, "merge-policies", "specify policies for merging environment variables")
}
// RegisterReaderFlags registers the 'reader' flags (see below)
// with the parent command. The values of the flags can be accessed via
// the supplied ReaderFlagValues struct.
// The reader flags are:
// --skip-profiles
// --profiles-db
// --profiles
// --merge-policies
// --target and --env
func RegisterReaderFlags(flags *flag.FlagSet, fv *ReaderFlagValues, defaultDBPath string) {
flags.Var(&fv.ProfilesMode, "skip-profiles", "if set, no profiles will be used")
RegisterDBPathFlag(flags, &fv.DBFilename, defaultDBPath)
RegisterProfilesFlag(flags, &fv.Profiles)
fv.MergePolicies = profilesreader.JiriMergePolicies()
RegisterMergePoliciesFlag(flags, &fv.MergePolicies)
profiles.RegisterTargetAndEnvFlags(flags, &fv.Target)
}
// RegisterReaderCommandsUsingParent registers the 'reader' flags
// (see RegisterReaderFlags) with the parent command and creates the
// list and env subcommands. The values of the flags can be accessed via
// the supplied ReaderFlagValues struct.
// RegisterReaderCommandsUsingParent results in a command line of the form:
// <parent> <reader-flags> [list|env] <list/env specific commands>
func RegisterReaderCommandsUsingParent(parent *cmdline.Command, fv *ReaderFlagValues, defaultDBPath string) {
envFlags.ReaderFlagValues = fv
listFlags.ReaderFlagValues = fv
RegisterReaderFlags(&parent.Flags, fv, defaultDBPath)
RegisterReaderCommands(parent, defaultDBPath)
}
// RegisterReaderCommands registers the list and env subcommands. The
// subcommands will host the 'reader' flags (see RegisterReaderFlags)
// resulting in a command line of the form:
// <parent> [list|env] <reader-flags> <list/env specific specific commands>
func RegisterReaderCommands(parent *cmdline.Command, defaultDBPath string) {
registerListCommand(parent, defaultDBPath)
registerEnvCommand(parent, defaultDBPath)
}
func newReaderFlags() *ReaderFlagValues {
return &ReaderFlagValues{MergePolicies: profilesreader.JiriMergePolicies()}
}
// registerListCommand the profiles list subcommand and returns it
// and a struct containing the values of the command line flags.
func registerListCommand(parent *cmdline.Command, defaultDBPath string) {
parent.Children = append(parent.Children, cmdList)
if listFlags.ReaderFlagValues == nil {
listFlags.ReaderFlagValues = newReaderFlags()
RegisterReaderFlags(&cmdList.Flags, listFlags.ReaderFlagValues, defaultDBPath)
}
cmdList.Flags.BoolVar(&listFlags.Verbose, "v", false, "print more detailed information")
cmdList.Flags.StringVar(&listFlags.info, "info", "", infoUsage())
}
// registerEnvCommand the profiles env subcommand and returns it and a
// struct containing the values of the command line flags.
func registerEnvCommand(parent *cmdline.Command, defaultDBPath string) {
parent.Children = append(parent.Children, cmdEnv)
if envFlags.ReaderFlagValues == nil {
envFlags.ReaderFlagValues = newReaderFlags()
RegisterReaderFlags(&cmdEnv.Flags, envFlags.ReaderFlagValues, defaultDBPath)
}
cmdEnv.Flags.BoolVar(&envFlags.Verbose, "v", false, "print more detailed information")
}
func matchingTargets(rd *profilesreader.Reader, profile *profiles.Profile) profiles.Targets {
var targets profiles.Targets
if IsFlagSet(cmdList.ParsedFlags, "target") {
if t := rd.LookupProfileTarget(profile.Name(), listFlags.Target); t != nil {
targets = profiles.Targets{t}
}
} else {
targets = profile.Targets()
}
targets.Sort()
return targets
}
func runList(jirix *jiri.X, args []string) error {
if listFlags.Verbose {
fmt.Fprintf(jirix.Stdout(), "Profiles Database Path: %s\n", listFlags.DBFilename)
}
rd, err := profilesreader.NewReader(jirix, listFlags.ProfilesMode, listFlags.DBFilename)
if err != nil {
return err
}
profileNames := []string{}
for _, a := range args {
if a != "" {
profileNames = append(profileNames, a)
}
}
if len(args) == 0 {
if IsFlagSet(cmdList.ParsedFlags, "profiles") {
profileNames = strings.Split(listFlags.Profiles, ",")
} else {
profileNames = rd.ProfileNames()
}
}
if listFlags.Verbose {
fmt.Fprintf(jirix.Stdout(), "Installed Profiles: ")
fmt.Fprintf(jirix.Stdout(), "%s\n", strings.Join(rd.ProfileNames(), ", "))
for _, name := range profileNames {
profile := rd.LookupProfile(name)
if profile == nil {
continue
}
fmt.Fprintf(jirix.Stdout(), "Profile: %s @ %s\n", profile.Name(), profile.Root())
for _, target := range matchingTargets(rd, profile) {
fmt.Fprintf(jirix.Stdout(), "\t%s\n", target.DebugString())
}
}
return nil
}
if listFlags.info == "" {
matchingNames := []string{}
for _, name := range profileNames {
profile := rd.LookupProfile(name)
if profile == nil {
continue
}
if len(matchingTargets(rd, profile)) > 0 {
matchingNames = append(matchingNames, name)
}
}
if len(matchingNames) > 0 {
fmt.Fprintln(jirix.Stdout(), strings.Join(matchingNames, ", "))
} else {
if IsFlagSet(cmdList.ParsedFlags, "target") {
return fmt.Errorf("no matching targets for %s", listFlags.Target)
}
}
return nil
}
// Handle --info
found := false
for _, name := range profileNames {
profile := rd.LookupProfile(name)
if profile == nil {
continue
}
targets := matchingTargets(rd, profile)
out := &bytes.Buffer{}
printHeader := len(profileNames) > 1 || len(targets) > 1 || len(listFlags.info) == 0
for _, target := range targets {
if printHeader {
out.WriteString(fmtHeader(name, target))
out.WriteString(" ")
}
r, err := fmtInfo(jirix, listFlags.info, rd, profile, target)
if err != nil {
return err
}
out.WriteString(r)
if printHeader {
out.WriteString("\n")
}
found = true
}
fmt.Fprint(jirix.Stdout(), out.String())
}
if !found && IsFlagSet(cmdList.ParsedFlags, "target") {
return fmt.Errorf("no matching targets for %s", listFlags.Target)
}
return nil
}
func fmtHeader(name string, target *profiles.Target) string {
if target == nil {
return name
}
return name + " " + target.String()
}
type listInfo struct {
SchemaVersion profiles.Version
DBPath string
Target struct {
InstallationDir string
CommandLineEnv []string
Env []string
Command string
}
Profile struct {
Root string
Name string
Installer string
DBPath string
}
}
func infoUsage() string {
return `The following fields for use with -info are available:
SchemaVersion - the version of the profiles implementation.
DBPath - the path for the profiles database.
Target.InstallationDir - the installation directory of the requested profile.
Target.CommandLineEnv - the environment variables specified via the command line when installing this profile target.
Target.Env - the environment variables computed by the profile installation process for this target.
Target.Command - a command that can be used to create this profile.
Note: if no --target is specified then the requested field will be displayed for all targets.
Profile.Root - the root directory of the requested profile.
Profile.Name - the qualified name of the profile.
Profile.Installer - the name of the profile installer.
Profile.DBPath - the path to the database file for this profile.
Note: if no profiles are specified then the requested field will be displayed for all profiles.`
}
func fmtOutput(jirix *jiri.X, o string) string {
_, width, err := textutil.TerminalSize()
if err != nil {
width = 80
}
if len(o) < width {
return o
}
out := &bytes.Buffer{}
w := textutil.NewUTF8WrapWriter(out, width)
fmt.Fprint(w, o)
w.Flush()
return out.String()
}
func fmtInfo(jirix *jiri.X, infoFmt string, rd *profilesreader.Reader, profile *profiles.Profile, target *profiles.Target) (string, error) {
// Populate an instance listInfo
info := &listInfo{}
name := profile.Name()
installer, _ := profiles.SplitProfileName(name)
info.SchemaVersion = rd.SchemaVersion()
info.DBPath = rd.Path()
if target != nil {
info.Target.InstallationDir = jiri.NewRelPath(target.InstallationDir).Abs(jirix)
info.Target.CommandLineEnv = target.CommandLineEnv().Vars
info.Target.Env = target.Env.Vars
clenv := ""
if len(info.Target.CommandLineEnv) > 0 {
clenv = fmt.Sprintf(" --env=\"%s\" ", strings.Join(info.Target.CommandLineEnv, ","))
}
if installer != "" {
info.Target.Command = fmt.Sprintf("jiri profile install --target=%s %s%s", target, clenv, name)
} else {
// TODO(cnicolaou): remove this when the transition is complete.
info.Target.Command = fmt.Sprintf("jiri v23-profile install --target=%s %s%s", target, clenv, name)
}
}
if profile != nil {
rp := jiri.NewRelPath(profile.Root())
info.Profile.Root = rp.Abs(jirix)
info.Profile.Name = name
info.Profile.Installer = installer
info.Profile.DBPath = info.DBPath
if installer != "" {
info.Profile.DBPath = filepath.Join(info.DBPath, installer)
}
}
// Use a template to print out any field in our instance of listInfo.
tmpl, err := template.New("list").Parse("{{ ." + infoFmt + "}}")
if err != nil {
return "", err
}
out := &bytes.Buffer{}
if err = tmpl.Execute(out, info); err != nil {
return "", fmt.Errorf("please specify a supported field:\n%s", infoUsage())
}
return out.String(), nil
}
func runEnv(jirix *jiri.X, args []string) error {
if len(envFlags.Profiles) == 0 {
return fmt.Errorf("no profiles were specified using --profiles")
}
rd, err := profilesreader.NewReader(jirix, envFlags.ProfilesMode, envFlags.DBFilename)
if err != nil {
return err
}
profileNames := strings.Split(envFlags.Profiles, ",")
if err := rd.ValidateRequestedProfilesAndTarget(profileNames, envFlags.Target); err != nil {
return err
}
rd.MergeEnvFromProfiles(envFlags.MergePolicies, envFlags.Target, profileNames...)
out := fmtVars(rd.ToMap(), args)
if len(out) > 0 {
fmt.Fprintln(jirix.Stdout(), out)
}
return nil
}
func expr(k, v string, trimmed bool) string {
if trimmed {
return v
}
return fmt.Sprintf("%s=%q ", k, v)
}
func fmtVars(vars map[string]string, args []string) string {
buf := bytes.Buffer{}
if len(args) == 0 {
for k, v := range vars {
buf.WriteString(fmt.Sprintf("%s=%q ", k, v))
}
} else {
for _, arg := range args {
name := strings.TrimSuffix(arg, "=")
trimmed := name != arg
for k, v := range vars {
if k == name {
buf.WriteString(expr(k, v, trimmed))
}
}
}
}
return strings.TrimSuffix(buf.String(), " ")
}