blob: 2afac17005a7ce42836f78184d3adc9831332bb6 [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"
"path/filepath"
"strings"
"text/template"
"v.io/jiri/jiri"
"v.io/jiri/profiles"
"v.io/jiri/profiles/profilesmanager"
"v.io/jiri/profiles/profilesreader"
"v.io/x/lib/cmdline"
"v.io/x/lib/textutil"
)
const DefaultProfiles = "base,jiri"
// 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.",
}
}
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 --show-profiles-db
showProfilesDB bool
// The value of --available
available bool
// 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
)
// RegisterDBFilenameFlag registers the --profiles-db flag with the supplied FlagSet.
func RegisterDBFilenameFlag(flags *flag.FlagSet, manifest *string, defaultManifest string) {
root := jiri.FindRoot()
flags.StringVar(manifest, "profiles-db", filepath.Join(root, defaultManifest), "specify the profiles XML manifest filename.")
flags.Lookup("profiles-db").DefValue = filepath.Join("$JIRI_ROOT", defaultManifest)
}
// RegisterProfilesFlag registers the --profiles flag
func RegisterProfilesFlag(flags *flag.FlagSet, profiles *string) {
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, defaultDBFilename string) {
flags.Var(&fv.ProfilesMode, "skip-profiles", "if set, no profiles will be used")
RegisterDBFilenameFlag(flags, &fv.DBFilename, defaultDBFilename)
RegisterProfilesFlag(flags, &fv.Profiles)
fv.MergePolicies = profilesreader.JiriMergePolicies()
RegisterMergePoliciesFlag(flags, &fv.MergePolicies)
profiles.RegisterTargetAndEnvFlags(flags, &fv.Target)
}
func initializeReaderFlags(flags *flag.FlagSet, fv *ReaderFlagValues, defaultDBFilename string) {
envFlags.ReaderFlagValues = fv
listFlags.ReaderFlagValues = fv
RegisterReaderFlags(flags, fv, defaultDBFilename)
}
// 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, defaultDBFilename string) {
initializeReaderFlags(&parent.Flags, fv, defaultDBFilename)
RegisterReaderCommands(parent, defaultDBFilename)
}
// 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, defaultDBFilename string) {
registerListCommand(parent, defaultDBFilename)
registerEnvCommand(parent, defaultDBFilename)
}
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, defaultDBFilename string) {
parent.Children = append(parent.Children, cmdList)
if listFlags.ReaderFlagValues == nil {
listFlags.ReaderFlagValues = newReaderFlags()
RegisterReaderFlags(&cmdList.Flags, listFlags.ReaderFlagValues, defaultDBFilename)
}
cmdList.Flags.BoolVar(&listFlags.Verbose, "v", false, "print more detailed information")
cmdList.Flags.BoolVar(&listFlags.showProfilesDB, "show-profiles-db", false, "print out the profiles database file")
cmdList.Flags.BoolVar(&listFlags.available, "available", false, "print the list of available profiles")
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, defaultDBFilename string) {
parent.Children = append(parent.Children, cmdEnv)
if envFlags.ReaderFlagValues == nil {
envFlags.ReaderFlagValues = newReaderFlags()
RegisterReaderFlags(&cmdEnv.Flags, envFlags.ReaderFlagValues, defaultDBFilename)
}
cmdEnv.Flags.BoolVar(&envFlags.Verbose, "v", false, "print more detailed information")
}
func runList(jirix *jiri.X, args []string) error {
if listFlags.showProfilesDB {
data, err := jirix.NewSeq().ReadFile(listFlags.DBFilename)
if err != nil {
return err
}
fmt.Fprintln(jirix.Stdout(), string(data))
return nil
}
if listFlags.Verbose {
fmt.Fprintf(jirix.Stdout(), "Profiles Database Filename: %s\n", listFlags.DBFilename)
}
if listFlags.available {
if listFlags.Verbose {
fmt.Fprintf(jirix.Stdout(), "Available Profiles:\n")
for _, name := range profilesmanager.Managers() {
mgr := profilesmanager.LookupManager(name)
vi := mgr.VersionInfo()
fmt.Fprintf(jirix.Stdout(), "%s: versions: %s\n", name, vi)
}
} else {
fmt.Fprintf(jirix.Stdout(), "%s\n", strings.Join(profilesmanager.Managers(), ", "))
}
}
rd, err := profilesreader.NewReader(jirix, listFlags.ProfilesMode, listFlags.DBFilename)
if err != nil {
return err
}
profileNames := args
if len(args) == 0 {
if IsFlagSet(&cmdList.Flags, "profiles") {
profileNames = strings.Split(listFlags.Profiles, ",")
} else {
profileNames = rd.ProfileNames()
}
}
availableNames := []string{}
for _, name := range profileNames {
if rd.LookupProfile(name) != nil {
availableNames = append(availableNames, name)
}
}
if listFlags.Verbose {
fmt.Fprintf(jirix.Stdout(), "Installed Profiles: ")
fmt.Fprintf(jirix.Stdout(), "%s\n", strings.Join(rd.ProfileNames(), ", "))
for _, name := range availableNames {
profile := rd.LookupProfile(name)
fmt.Fprintf(jirix.Stdout(), "Profile: %s @ %s\n", profile.Name(), profile.Root())
for _, target := range profile.Targets() {
fmt.Fprintf(jirix.Stdout(), "\t%s\n", target.DebugString())
}
}
} else {
for _, name := range availableNames {
profile := rd.LookupProfile(name)
mgr := profilesmanager.LookupManager(name)
out := &bytes.Buffer{}
var targets profiles.Targets
if listFlags.Target.IsSet() {
targets = append(targets, rd.LookupProfileTarget(name, listFlags.Target))
} else {
targets = profile.Targets()
}
printHeader := len(availableNames) > 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, mgr, profile, target)
if err != nil {
return err
}
out.WriteString(r)
if printHeader {
out.WriteString("\n")
}
}
fmt.Fprint(jirix.Stdout(), out.String())
}
}
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
Target struct {
InstallationDir string
CommandLineEnv []string
Env []string
Command string
}
Profile struct {
Description string
Root string
DefaultVersion string
LatestVersion string
Versions []string
}
}
func infoUsage() string {
return `The following fields for use with --profile-info are available:
SchemaVersion - the version of the profiles implementation.
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.Description - description of the requested profile.
Profile.Root - the root directory of the requested profile.
Profile.Versions - the set of supported versions for this profile.
Profile.DefaultVersion - the default version of the requested profile.
Profile.LatestVersion - the latest version available for the requested 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.NewUTF8LineWriter(out, width)
fmt.Fprint(w, o)
w.Flush()
return out.String()
}
func fmtInfo(jirix *jiri.X, infoFmt string, rd *profilesreader.Reader, mgr profiles.Manager, profile *profiles.Profile, target *profiles.Target) (string, error) {
if len(infoFmt) > 0 {
// Populate an instance listInfo
info := &listInfo{}
name := ""
if mgr != nil {
// Format the description on its own, without any preceeding
// text so that the line breaks work correctly.
info.Profile.Description = "\n" + fmtOutput(jirix, mgr.Info()) + "\n"
vi := mgr.VersionInfo()
if supported := vi.Supported(); len(supported) > 0 {
info.Profile.Versions = supported
info.Profile.LatestVersion = supported[0]
}
info.Profile.DefaultVersion = vi.Default()
name = mgr.Name()
}
info.SchemaVersion = rd.SchemaVersion()
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, ","))
}
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)
}
// 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
}
return "", 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(), " ")
}