| // 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" |
| "strings" |
| "text/template" |
| |
| "v.io/jiri/jiri" |
| "v.io/jiri/profiles" |
| "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: jiri.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: jiri.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: 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", |
| } |
| |
| // cmdUpdate represents the "profile update" command. |
| var cmdUpdate = &cmdline.Command{ |
| Runner: jiri.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: jiri.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: jiri.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.", |
| } |
| |
| // All installed profile data will be stored under profileRoot. |
| var profileRoot = jiri.NewRelPath("profiles") |
| |
| var ( |
| targetFlag profiles.Target |
| manifestFlag string |
| showManifestFlag bool |
| profilesFlag 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() |
| |
| // 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(jirix *jiri.X, args []string) error { |
| if showManifestFlag { |
| data, err := jirix.NewSeq().ReadFile(manifestFlag) |
| if err != nil { |
| return err |
| } |
| fmt.Fprintln(jirix.Stdout(), string(data)) |
| return nil |
| } |
| if verboseFlag { |
| fmt.Fprintf(jirix.Stdout(), "Manifest: %s\n", manifestFlag) |
| } |
| if availableFlag { |
| if verboseFlag { |
| fmt.Fprintf(jirix.Stdout(), "Available Profiles:\n") |
| for _, name := range profiles.Managers() { |
| mgr := profiles.LookupManager(name) |
| vi := mgr.VersionInfo() |
| fmt.Fprintf(jirix.Stdout(), "%s: versions: %s\n", name, vi) |
| } |
| } else { |
| fmt.Fprintf(jirix.Stdout(), "%s\n", strings.Join(profiles.Managers(), ", ")) |
| } |
| } |
| if err := profiles.Read(jirix, manifestFlag); err != nil { |
| fmt.Fprintf(jirix.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(jirix.Stdout(), "Installed Profiles: ") |
| fmt.Fprintf(jirix.Stdout(), "%s\n", strings.Join(profiles.Profiles(), ", ")) |
| for _, name := range availableNames { |
| profile := profiles.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 := 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(jirix, 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, 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(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 = profiles.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 { |
| info.Profile.Root = profileRoot.Abs(jirix) |
| } |
| |
| // 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(jirix *jiri.X, args []string) error { |
| if len(profilesFlag) == 0 { |
| return fmt.Errorf("no profiles were specified using --profiles") |
| } |
| ch, err := profiles.NewConfigHelper(jirix, 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(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(), " ") |
| } |
| |
| func initCommand(jirix *jiri.X, 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(jirix, manifestFlag); err != nil { |
| fmt.Fprintf(jirix.Stderr(), "Failed to read manifest: %v", err) |
| return err |
| } |
| return nil |
| } |
| |
| func runUpdate(jirix *jiri.X, args []string) error { |
| if len(args) == 0 { |
| args = profiles.Managers() |
| } |
| if err := initCommand(jirix, 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(jirix.Stdout(), "Updating %s %s from %q to %s\n", n, target, target.Version(), vi) |
| } |
| target.SetVersion(vi.Default()) |
| err := mgr.Install(jirix, profileRoot, *target) |
| logResult(jirix, "Update", mgr, *target, err) |
| if err != nil { |
| return err |
| } |
| } else { |
| if verboseFlag { |
| fmt.Fprintf(jirix.Stdout(), "%s %s at %q is up to date(%s)\n", n, target, target.Version(), vi) |
| } |
| } |
| |
| } |
| } |
| return profiles.Write(jirix, manifestFlag) |
| } |
| |
| func runGC(jirix *jiri.X, 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(jirix, profileRoot, *target) |
| logResult(jirix, "gc", mgr, *target, err) |
| if err != nil { |
| return err |
| } |
| } |
| } |
| } |
| return nil |
| } |
| |
| func runEnsureVersionsAreSet(jirix *jiri.X, args []string) error { |
| for _, name := range args { |
| profile := profiles.LookupProfile(name) |
| mgr := profiles.LookupManager(name) |
| if mgr == nil { |
| fmt.Fprintf(jirix.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(jirix.Stdout(), "%s %s had no version, now set to: %s\n", name, prior, target) |
| } |
| } |
| } |
| } |
| return nil |
| } |
| |
| func runRmAll(jirix *jiri.X) error { |
| s := jirix.NewSeq() |
| if exists, err := s.FileExists(manifestFlag); err != nil || exists { |
| if err := s.Remove(manifestFlag).Done(); err != nil { |
| return err |
| } |
| } |
| d := profileRoot.Abs(jirix) |
| if exists, err := s.DirectoryExists(d); err != nil || exists { |
| if err := s.Run("chmod", "-R", "u+w", d). |
| RemoveAll(d).Done(); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func runCleanup(jirix *jiri.X, args []string) error { |
| if err := profiles.Read(jirix, manifestFlag); err != nil { |
| fmt.Fprintf(jirix.Stderr(), "Failed to read manifest: %v", err) |
| return err |
| } |
| if len(args) == 0 { |
| args = profiles.Profiles() |
| } |
| dirty := false |
| if specificVersionsFlag { |
| if verboseFlag { |
| fmt.Fprintf(jirix.Stdout(), "Ensuring that all targets have a specific version set for %s\n", args) |
| } |
| if err := runEnsureVersionsAreSet(jirix, args); err != nil { |
| return fmt.Errorf("ensure-specific-versions-are-set: %v", err) |
| } |
| dirty = true |
| } |
| if cleanupFlag { |
| if verboseFlag { |
| fmt.Fprintf(jirix.Stdout(), "Removing targets older than the default version for %s\n", args) |
| } |
| if err := runGC(jirix, args); err != nil { |
| return fmt.Errorf("gc: %v", err) |
| } |
| dirty = true |
| } |
| if rmAllFlag { |
| if verboseFlag { |
| fmt.Fprintf(jirix.Stdout(), "Removing profile manifest and all profile output files\n") |
| } |
| if err := runRmAll(jirix); 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(jirix, manifestFlag) |
| } |
| |
| func logResult(jirix *jiri.X, action string, mgr profiles.Manager, target profiles.Target, err error) { |
| fmt.Fprintf(jirix.Stdout(), "%s: %s %s: ", action, mgr.Name(), target) |
| if err == nil { |
| fmt.Fprintf(jirix.Stdout(), "success\n") |
| } else { |
| fmt.Fprintf(jirix.Stdout(), "%v\n", err) |
| } |
| } |
| |
| func applyCommand(names []string, jirix *jiri.X, target profiles.Target, fn func(profiles.Manager, *jiri.X, profiles.Target) error) error { |
| for _, n := range names { |
| mgr := profiles.LookupManager(n) |
| profileTarget := target |
| version, err := mgr.VersionInfo().Select(profileTarget.Version()) |
| if err != nil { |
| return err |
| } |
| profileTarget.SetVersion(version) |
| if err := fn(mgr, jirix, profileTarget); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func runInstall(jirix *jiri.X, args []string) error { |
| if err := initCommand(jirix, 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(jirix.Stdout(), "%v %v is already installed as %v\n", name, targetFlag, p) |
| continue |
| } |
| names = append(names, name) |
| } |
| if err := applyCommand(names, jirix, targetFlag, |
| func(mgr profiles.Manager, jirix *jiri.X, target profiles.Target) error { |
| err := mgr.Install(jirix, profileRoot, target) |
| logResult(jirix, "Install:", mgr, target, err) |
| return err |
| }); err != nil { |
| return err |
| } |
| return profiles.Write(jirix, manifestFlag) |
| } |
| |
| func runUninstall(jirix *jiri.X, args []string) error { |
| if err := initCommand(jirix, args); err != nil { |
| return err |
| } |
| if allFlag && targetFlag.IsSet() { |
| fmt.Fprintf(jirix.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(jirix, profileRoot, *target); err != nil { |
| logResult(jirix, "Uninstall", mgr, *target, err) |
| return err |
| } |
| logResult(jirix, "Uninstall", mgr, *target, nil) |
| } |
| } |
| } else { |
| applyCommand(args, jirix, targetFlag, |
| func(mgr profiles.Manager, jirix *jiri.X, target profiles.Target) error { |
| err := mgr.Uninstall(jirix, profileRoot, target) |
| logResult(jirix, "Uninstall", mgr, target, err) |
| return err |
| }) |
| } |
| return profiles.Write(jirix, manifestFlag) |
| } |