github.com/fastly/cli@v1.7.2-0.20240304164155-9d0f1d77c3bf/pkg/commands/install/root.go (about) 1 package install 2 3 import ( 4 "fmt" 5 "io" 6 "os" 7 "path/filepath" 8 9 "github.com/fastly/cli/pkg/argparser" 10 "github.com/fastly/cli/pkg/filesystem" 11 "github.com/fastly/cli/pkg/global" 12 "github.com/fastly/cli/pkg/text" 13 ) 14 15 // RootCommand is the parent command for all subcommands in this package. 16 // It should be installed under the primary root command. 17 type RootCommand struct { 18 argparser.Base 19 20 versionToInstall string 21 } 22 23 // NewRootCommand returns a new command registered in the parent. 24 func NewRootCommand(parent argparser.Registerer, g *global.Data) *RootCommand { 25 var c RootCommand 26 c.Globals = g 27 c.CmdClause = parent.Command("install", "Install the specified version of the CLI") 28 c.CmdClause.Arg("version", "CLI release version to install (e.g. 10.8.0)").Required().StringVar(&c.versionToInstall) 29 return &c 30 } 31 32 // Exec implements the command interface. 33 func (c *RootCommand) Exec(_ io.Reader, out io.Writer) error { 34 spinner, err := text.NewSpinner(out) 35 if err != nil { 36 return err 37 } 38 39 var downloadedBin string 40 err = spinner.Process(fmt.Sprintf("Fetching release %s", c.versionToInstall), func(_ *text.SpinnerWrapper) error { 41 downloadedBin, err = c.Globals.Versioners.CLI.DownloadVersion(c.versionToInstall) 42 if err != nil { 43 c.Globals.ErrLog.AddWithContext(err, map[string]any{ 44 "CLI version to install": c.versionToInstall, 45 }) 46 return fmt.Errorf("error downloading release version %s: %w", c.versionToInstall, err) 47 } 48 return nil 49 }) 50 if err != nil { 51 return err 52 } 53 defer os.RemoveAll(downloadedBin) 54 55 var currentBin string 56 err = spinner.Process("Replacing binary", func(_ *text.SpinnerWrapper) error { 57 execPath, err := os.Executable() 58 if err != nil { 59 c.Globals.ErrLog.Add(err) 60 return fmt.Errorf("error determining executable path: %w", err) 61 } 62 63 currentBin, err = filepath.Abs(execPath) 64 if err != nil { 65 c.Globals.ErrLog.AddWithContext(err, map[string]any{ 66 "Executable path": execPath, 67 }) 68 return fmt.Errorf("error determining absolute target path: %w", err) 69 } 70 71 // Windows does not permit replacing a running executable, however it will 72 // permit it if you first move the original executable. So we first move the 73 // running executable to a new location, then we move the executable that we 74 // downloaded to the same location as the original. 75 // I've also tested this approach on nix systems and it works fine. 76 // 77 // Reference: 78 // https://github.com/golang/go/issues/21997#issuecomment-331744930 79 80 backup := currentBin + ".bak" 81 if err := os.Rename(currentBin, backup); err != nil { 82 c.Globals.ErrLog.AddWithContext(err, map[string]any{ 83 "Executable (source)": downloadedBin, 84 "Executable (destination)": currentBin, 85 }) 86 return fmt.Errorf("error moving the current executable: %w", err) 87 } 88 89 if err = os.Remove(backup); err != nil { 90 c.Globals.ErrLog.Add(err) 91 } 92 93 // Move the downloaded binary to the same location as the current executable. 94 if err := os.Rename(downloadedBin, currentBin); err != nil { 95 c.Globals.ErrLog.AddWithContext(err, map[string]any{ 96 "Executable (source)": downloadedBin, 97 "Executable (destination)": currentBin, 98 }) 99 renameErr := err 100 101 // Failing that we'll try to io.Copy downloaded binary to the current binary. 102 if err := filesystem.CopyFile(downloadedBin, currentBin); err != nil { 103 c.Globals.ErrLog.AddWithContext(err, map[string]any{ 104 "Executable (source)": downloadedBin, 105 "Executable (destination)": currentBin, 106 }) 107 return fmt.Errorf("error 'copying' latest binary in place: %w (following an error 'moving': %w)", err, renameErr) 108 } 109 } 110 return nil 111 }) 112 if err != nil { 113 return err 114 } 115 116 text.Success(out, "\nInstalled version %s.", c.versionToInstall) 117 return nil 118 }