blob: 6fa89efaa166537df81c92a56ca5d52f80faf60d [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 commandline 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 commandline
import (
"bytes"
"flag"
"fmt"
"path/filepath"
"strings"
"text/template"
"v.io/jiri/profiles"
"v.io/jiri/project"
"v.io/jiri/tool"
"v.io/x/lib/cmdline"
"v.io/x/lib/textutil"
)
func init() {
tool.InitializeRunFlags(&CommandLineDriver.Flags)
}
// CommandLineDriver implements the command line for the 'profile'
// subcommand.
var CommandLineDriver = &cmdline.Command{
Name: "profile",
Short: "Manage profiles",
Long: helpMsg,
Children: []*cmdline.Command{
cmdInstall,
cmdList,
cmdEnv,
cmdUninstall,
cmdUpdate,
cmdCleanup,
},
}
// cmdInstall represents the "profile install" command.
var cmdInstall = &cmdline.Command{
Runner: cmdline.RunnerFunc(runInstall),
Name: "install",
Short: "Install the given profiles",
Long: "Install the given profiles.",
ArgsName: "<profiles>",
ArgsLong: "<profiles> is a list of profiles to install.",
}
// cmdList represents the "profile list" command.
var cmdList = &cmdline.Command{
Runner: cmdline.RunnerFunc(runList),
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.",
}
// cmdEnv represents the "profile env" command.
var cmdEnv = &cmdline.Command{
Runner: cmdline.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",
}
// cmdUpdate represents the "profile update" command.
var cmdUpdate = &cmdline.Command{
Runner: cmdline.RunnerFunc(runUpdate),
Name: "update",
Short: "Install the latest default version of the given profiles",
Long: "Install the latest default version of the given profiles.",
ArgsName: "<profiles>",
ArgsLong: "<profiles> is a list of profiles to update, if omitted all profiles are updated.",
}
// cmdCleanup represents the "profile Cleanup" command.
var cmdCleanup = &cmdline.Command{
Runner: cmdline.RunnerFunc(runCleanup),
Name: "cleanup",
Short: "Cleanup the locally installed profiles",
Long: "Cleanup the locally installed profiles. This is generally required when recovering from earlier bugs or when preparing for a subsequent change to the profiles implementation.",
ArgsName: "<profiles>",
ArgsLong: "<profiles> is a list of profiles to cleanup, if omitted all profiles are cleaned.",
}
// cmdUninstall represents the "profile uninstall" command.
var cmdUninstall = &cmdline.Command{
Runner: cmdline.RunnerFunc(runUninstall),
Name: "uninstall",
Short: "Uninstall the given profiles",
Long: "Uninstall the given profiles.",
ArgsName: "<profiles>",
ArgsLong: "<profiles> is a list of profiles to uninstall.",
}
var (
rootPath profiles.RelativePath
targetFlag profiles.Target
manifestFlag string
showManifestFlag bool
profilesFlag string
rootDir string
availableFlag bool
verboseFlag bool
allFlag bool
infoFlag string
mergePoliciesFlag profiles.MergePolicies
specificVersionsFlag bool
cleanupFlag bool
rmAllFlag bool
rewriteManifestFlag bool
)
func Main(name string) {
CommandLineDriver.Name = name
cmdline.Main(CommandLineDriver)
}
func Init(defaultManifestFilename string) {
targetFlag = profiles.DefaultTarget()
mergePoliciesFlag = profiles.JiriMergePolicies()
var err error
rootDir, err = project.JiriRoot()
if err != nil {
panic(err)
}
rootPath = profiles.NewRelativePath("JIRI_ROOT", rootDir).Join("profiles")
// Every sub-command accepts: --manifest
for _, fs := range []*flag.FlagSet{
&cmdInstall.Flags,
&cmdUpdate.Flags,
&cmdUninstall.Flags,
&cmdCleanup.Flags,
&cmdEnv.Flags,
&cmdList.Flags} {
profiles.RegisterManifestFlag(fs, &manifestFlag, defaultManifestFilename)
}
// install accepts: --target and, --env.
profiles.RegisterTargetAndEnvFlags(&cmdInstall.Flags, &targetFlag)
// uninstall, env and list accept: --target,
for _, fs := range []*flag.FlagSet{
&cmdUninstall.Flags,
&cmdEnv.Flags,
&cmdList.Flags} {
profiles.RegisterTargetFlag(fs, &targetFlag)
}
// env accepts --profiles and --merge-policies
profiles.RegisterProfilesFlag(&cmdEnv.Flags, &profilesFlag)
profiles.RegisterMergePoliciesFlag(&cmdEnv.Flags, &mergePoliciesFlag)
// uninstall, list, env and cleanup accept: --v
for _, fs := range []*flag.FlagSet{
&cmdUpdate.Flags,
&cmdList.Flags,
&cmdCleanup.Flags,
&cmdEnv.Flags} {
fs.BoolVar(&verboseFlag, "v", false, "print more detailed information")
}
// uninstall accept --all-targets but with different defaults.
cmdUninstall.Flags.BoolVar(&allFlag, "all-targets", false, "apply to all targets for the specified profile(s)")
// list accepts --show-profiles-manifest, --available, --dir, --default, --versions
cmdList.Flags.BoolVar(&showManifestFlag, "show-profiles-manifest", false, "print out the manifest file")
cmdList.Flags.BoolVar(&availableFlag, "available", false, "print the list of available profiles")
cmdList.Flags.StringVar(&infoFlag, "info", "", infoUsage())
for _, mgr := range profiles.Managers() {
profiles.LookupManager(mgr).AddFlags(&cmdInstall.Flags, profiles.Install)
profiles.LookupManager(mgr).AddFlags(&cmdUninstall.Flags, profiles.Uninstall)
}
// cleanup accepts the following flags:
cmdCleanup.Flags.BoolVar(&cleanupFlag, "gc", false, "uninstall profile targets that are older than the current default")
cmdCleanup.Flags.BoolVar(&specificVersionsFlag, "ensure-specific-versions-are-set", false, "ensure that profile targets have a specific version set")
cmdCleanup.Flags.BoolVar(&rmAllFlag, "rm-all", false, "remove profiles manifest and all profile generated output files.")
cmdCleanup.Flags.BoolVar(&rewriteManifestFlag, "rewrite-profiles-manifest", false, "rewrite the profiles manifest file to use the latest schema version")
}
func runList(env *cmdline.Env, args []string) error {
ctx := tool.NewContextFromEnv(env)
if showManifestFlag {
data, err := ctx.Run().ReadFile(manifestFlag)
if err != nil {
return err
}
fmt.Fprintln(ctx.Stdout(), string(data))
return nil
}
if verboseFlag {
fmt.Fprintf(ctx.Stdout(), "Manifest: %s\n", manifestFlag)
}
if availableFlag {
if verboseFlag {
fmt.Fprintf(ctx.Stdout(), "Available Profiles:\n")
for _, name := range profiles.Managers() {
mgr := profiles.LookupManager(name)
vi := mgr.VersionInfo()
fmt.Fprintf(ctx.Stdout(), "%s: versions: %s\n", name, vi)
}
} else {
fmt.Fprintf(ctx.Stdout(), "%s\n", strings.Join(profiles.Managers(), ", "))
}
}
if err := profiles.Read(ctx, manifestFlag); err != nil {
fmt.Fprintf(ctx.Stderr(), "Failed to read manifest: %v", err)
return err
}
profileNames := args
if len(args) == 0 {
profileNames = profiles.Profiles()
}
availableNames := []string{}
for _, name := range profileNames {
if profiles.LookupProfile(name) != nil {
availableNames = append(availableNames, name)
}
}
if verboseFlag {
fmt.Fprintf(ctx.Stdout(), "Installed Profiles: ")
fmt.Fprintf(ctx.Stdout(), "%s\n", strings.Join(profiles.Profiles(), ", "))
for _, name := range availableNames {
profile := profiles.LookupProfile(name)
fmt.Fprintf(ctx.Stdout(), "Profile: %s @ %s\n", profile.Name, profile.Root)
for _, target := range profile.Targets() {
fmt.Fprintf(ctx.Stdout(), "\t%s\n", target.DebugString())
}
}
} else {
for _, name := range availableNames {
profile := profiles.LookupProfile(name)
mgr := profiles.LookupManager(name)
out := &bytes.Buffer{}
var targets profiles.OrderedTargets
if targetFlag.IsSet() {
targets = append(targets, profiles.LookupProfileTarget(name, targetFlag))
} else {
targets = profile.Targets()
}
printHeader := len(availableNames) > 1 || len(targets) > 1 || len(infoFlag) == 0
for _, target := range targets {
if printHeader {
out.WriteString(fmtHeader(name, target))
out.WriteString(" ")
}
r, err := fmtInfo(ctx, mgr, profile, target)
if err != nil {
return err
}
out.WriteString(r)
if printHeader {
out.WriteString("\n")
}
}
fmt.Fprint(ctx.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(ctx *tool.Context, 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 handleRelativePath(root profiles.RelativePath, s string) string {
// Handle the transition from absolute to relative paths.
if filepath.IsAbs(s) {
return s
}
return root.RootJoin(s).Expand()
}
func fmtInfo(ctx *tool.Context, mgr profiles.Manager, profile *profiles.Profile, target *profiles.Target) (string, error) {
if len(infoFlag) > 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(ctx, 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 = profiles.SchemaVersion()
if target != nil {
info.Target.InstallationDir = handleRelativePath(rootPath, target.InstallationDir)
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 {
info.Profile.Root = handleRelativePath(rootPath, profile.Root)
}
// Use a template to print out any field in our instance of listInfo.
tmpl, err := template.New("list").Parse("{{ ." + infoFlag + "}}")
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(env *cmdline.Env, args []string) error {
if len(profilesFlag) == 0 {
return fmt.Errorf("no profiles were specified using --profiles")
}
ctx := tool.NewContextFromEnv(env)
ch, err := profiles.NewConfigHelper(ctx, profiles.UseProfiles, manifestFlag)
if err != nil {
return err
}
profileNames := strings.Split(profilesFlag, ",")
if err := ch.ValidateRequestedProfilesAndTarget(profileNames, targetFlag); err != nil {
return err
}
ch.MergeEnvFromProfiles(mergePoliciesFlag, targetFlag, profileNames...)
out := fmtVars(ch.ToMap(), args)
if len(out) > 0 {
fmt.Fprintln(ctx.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(), " ")
}
func initCommand(ctx *tool.Context, args []string) error {
if len(args) == 0 {
return fmt.Errorf("no profiles specified")
}
for _, n := range args {
if mgr := profiles.LookupManager(n); mgr == nil {
return fmt.Errorf("profile %v is not available, use \"list --available\" to see the list of available profiles", n)
}
}
if err := profiles.Read(ctx, manifestFlag); err != nil {
fmt.Fprintf(ctx.Stderr(), "Failed to read manifest: %v", err)
return err
}
return nil
}
func runUpdate(env *cmdline.Env, args []string) error {
ctx := tool.NewContextFromEnv(env)
if len(args) == 0 {
args = profiles.Managers()
}
if err := initCommand(ctx, args); err != nil {
return err
}
for _, n := range args {
mgr := profiles.LookupManager(n)
profile := profiles.LookupProfile(n)
if profile == nil {
continue
}
vi := mgr.VersionInfo()
for _, target := range profile.Targets() {
if vi.IsTargetOlderThanDefault(target.Version()) {
if verboseFlag {
fmt.Fprintf(ctx.Stdout(), "Updating %s %s from %q to %s\n", n, target, target.Version(), vi)
}
target.SetVersion(vi.Default())
err := mgr.Install(ctx, rootPath, *target)
logResult(ctx, "Update", mgr, *target, err)
if err != nil {
return err
}
} else {
if verboseFlag {
fmt.Fprintf(ctx.Stdout(), "%s %s at %q is up to date(%s)\n", n, target, target.Version(), vi)
}
}
}
}
return profiles.Write(ctx, manifestFlag)
}
func runGC(ctx *tool.Context, args []string) error {
for _, n := range args {
mgr := profiles.LookupManager(n)
vi := mgr.VersionInfo()
profile := profiles.LookupProfile(n)
for _, target := range profile.Targets() {
if vi.IsTargetOlderThanDefault(target.Version()) {
err := mgr.Uninstall(ctx, rootPath, *target)
logResult(ctx, "gc", mgr, *target, err)
if err != nil {
return err
}
}
}
}
return nil
}
func runEnsureVersionsAreSet(ctx *tool.Context, args []string) error {
for _, name := range args {
profile := profiles.LookupProfile(name)
mgr := profiles.LookupManager(name)
if mgr == nil {
fmt.Fprintf(ctx.Stderr(), "%s is not linked into this binary", name)
continue
}
for _, target := range profile.Targets() {
if len(target.Version()) == 0 {
prior := *target
version, err := mgr.VersionInfo().Select(target.Version())
if err != nil {
return err
}
target.SetVersion(version)
profiles.RemoveProfileTarget(name, prior)
if err := profiles.AddProfileTarget(name, *target); err != nil {
return err
}
if verboseFlag {
fmt.Fprintf(ctx.Stdout(), "%s %s had no version, now set to: %s\n", name, prior, target)
}
}
}
}
return nil
}
func runRmAll(ctx *tool.Context) error {
s := ctx.NewSeq()
if exists, err := s.FileExists(manifestFlag); err != nil || exists {
if err := s.Remove(manifestFlag).Done(); err != nil {
return err
}
}
rp := rootPath.Expand()
if exists, err := s.DirectoryExists(rp); err != nil || exists {
if err := s.Run("chmod", "-R", "u+w", rp).
RemoveAll(rp).Done(); err != nil {
return err
}
}
return nil
}
func runCleanup(env *cmdline.Env, args []string) error {
ctx := tool.NewContextFromEnv(env)
if err := profiles.Read(ctx, manifestFlag); err != nil {
fmt.Fprintf(ctx.Stderr(), "Failed to read manifest: %v", err)
return err
}
if len(args) == 0 {
args = profiles.Profiles()
}
dirty := false
if specificVersionsFlag {
if verboseFlag {
fmt.Fprintf(ctx.Stdout(), "Ensuring that all targets have a specific version set for %s\n", args)
}
if err := runEnsureVersionsAreSet(ctx, args); err != nil {
return fmt.Errorf("ensure-specific-versions-are-set: %v", err)
}
dirty = true
}
if cleanupFlag {
if verboseFlag {
fmt.Fprintf(ctx.Stdout(), "Removing targets older than the default version for %s\n", args)
}
if err := runGC(ctx, args); err != nil {
return fmt.Errorf("gc: %v", err)
}
dirty = true
}
if rmAllFlag {
if verboseFlag {
fmt.Fprintf(ctx.Stdout(), "Removing profile manifest and all profile output files\n")
}
if err := runRmAll(ctx); err != nil {
return err
}
// Don't write out the profiles manifest file again.
return nil
}
if rewriteManifestFlag {
dirty = true
}
if !dirty {
return fmt.Errorf("at least one option must be specified")
}
return profiles.Write(ctx, manifestFlag)
}
func logResult(ctx *tool.Context, action string, mgr profiles.Manager, target profiles.Target, err error) {
fmt.Fprintf(ctx.Stdout(), "%s: %s %s: ", action, mgr.Name(), target)
if err == nil {
fmt.Fprintf(ctx.Stdout(), "success\n")
} else {
fmt.Fprintf(ctx.Stdout(), "%v\n", err)
}
}
func applyCommand(names []string, env *cmdline.Env, ctx *tool.Context, target profiles.Target, fn func(profiles.Manager, *tool.Context, profiles.Target) error) error {
for _, n := range names {
mgr := profiles.LookupManager(n)
version, err := mgr.VersionInfo().Select(target.Version())
if err != nil {
return err
}
target.SetVersion(version)
if err := fn(mgr, ctx, target); err != nil {
return err
}
}
return nil
}
func runInstall(env *cmdline.Env, args []string) error {
ctx := tool.NewContextFromEnv(env)
if err := initCommand(ctx, args); err != nil {
return err
}
names := []string{}
if len(args) == 0 {
for _, name := range profiles.Managers() {
names = append(names, name)
}
}
targetFlag.UseCommandLineEnv()
for _, name := range args {
if p := profiles.LookupProfileTarget(name, targetFlag); p != nil {
fmt.Fprintf(ctx.Stdout(), "%v %v is already installed as %v\n", name, targetFlag, p)
continue
}
names = append(names, name)
}
if err := applyCommand(names, env, ctx, targetFlag,
func(mgr profiles.Manager, ctx *tool.Context, target profiles.Target) error {
err := mgr.Install(ctx, rootPath, target)
logResult(ctx, "Install:", mgr, target, err)
return err
}); err != nil {
return err
}
return profiles.Write(ctx, manifestFlag)
}
func runUninstall(env *cmdline.Env, args []string) error {
ctx := tool.NewContextFromEnv(env)
if err := initCommand(ctx, args); err != nil {
return err
}
if allFlag && targetFlag.IsSet() {
fmt.Fprintf(ctx.Stdout(), "ignore target (%v) when used in conjunction with --all-targets\n", targetFlag)
}
if allFlag {
for _, name := range args {
profile := profiles.LookupProfile(name)
mgr := profiles.LookupManager(name)
if profile == nil || mgr == nil {
continue
}
for _, target := range profile.Targets() {
if err := mgr.Uninstall(ctx, rootPath, *target); err != nil {
logResult(ctx, "Uninstall", mgr, *target, err)
return err
}
logResult(ctx, "Uninstall", mgr, *target, nil)
}
}
} else {
applyCommand(args, env, ctx, targetFlag,
func(mgr profiles.Manager, ctx *tool.Context, target profiles.Target) error {
err := mgr.Uninstall(ctx, rootPath, target)
logResult(ctx, "Uninstall", mgr, target, err)
return err
})
}
return profiles.Write(ctx, manifestFlag)
}