diff --git a/cmd/qpm/internal/cmd/aquifer.go b/cmd/qpm/internal/cmd/aquifer.go index 1447c6d..9355985 100644 --- a/cmd/qpm/internal/cmd/aquifer.go +++ b/cmd/qpm/internal/cmd/aquifer.go @@ -5,6 +5,7 @@ import ( "os" "time" + "github.com/anoriqq/qpm" "github.com/anoriqq/qpm/cmd/qpm/internal/config" "github.com/anoriqq/qpm/cmd/qpm/internal/git" "github.com/spf13/cobra" @@ -20,6 +21,8 @@ var aquiferCmd = &cobra.Command{ } func init() { + var aquiferPath string + aquiferPullCmd := &cobra.Command{ Use: "pull", Short: "Get aquifer form remote repository", @@ -34,8 +37,14 @@ func init() { return err } - oldDir := fmt.Sprintf("%s.old_%s", c.AquiferPath, time.Now().Format("20060102150405")) - if err := os.Rename(c.AquiferPath, oldDir); err != nil && !os.IsNotExist(err) { + if aquiferPath != "" { + c.AquiferPath = aquiferPath + } + + aquiferPath := os.ExpandEnv(c.AquiferPath) + + oldDir := fmt.Sprintf("%s.old_%s", aquiferPath, time.Now().Format("20060102150405")) + if err := os.Rename(aquiferPath, oldDir); err != nil && !os.IsNotExist(err) { return err } @@ -44,7 +53,34 @@ func init() { return err } - if err = cl.Clone(c.AquiferPath, c.AquiferRemote.String()); err != nil { + if err = cl.Clone(aquiferPath, c.AquiferRemote.String()); err != nil { + return err + } + + return nil + }, + } + + aquiferValidateCmd := &cobra.Command{ + Use: "validate", + Short: "validate specific stratum of aquifer", + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + path, err := config.InitConfigFile() + if err != nil { + return err + } + + c, err := config.ReadConfig(path) + if err != nil { + return err + } + + if aquiferPath != "" { + c.AquiferPath = aquiferPath + } + + if _, err := qpm.ReadStratum(c, args[0]); err != nil { return err } @@ -52,5 +88,7 @@ func init() { }, } + aquiferCmd.PersistentFlags().StringVarP(&aquiferPath, "aquifer", "a", "", "Aquifer directory path") aquiferCmd.AddCommand(aquiferPullCmd) + aquiferCmd.AddCommand(aquiferValidateCmd) } diff --git a/cmd/qpm/internal/cmd/config.go b/cmd/qpm/internal/cmd/config.go index 4f5696c..0a21a97 100644 --- a/cmd/qpm/internal/cmd/config.go +++ b/cmd/qpm/internal/cmd/config.go @@ -77,7 +77,7 @@ func init() { }, } - configCmd.Flags().BoolVarP(&isInit, "init", "i", false, "Interalcive inisialization") + configCmd.Flags().BoolVarP(&isInit, "init", "i", false, "Interactive initialisation") configCmd.Flags().BoolVarP(&isClear, "clear", "", false, "Clear config file") rootCmd.AddCommand(configCmd) } @@ -136,7 +136,7 @@ func SurveyGitHubUsername(current *string) (string, error) { } func SurveyGitHubToken(current *string) (string, error) { - msg := "Please enter GitHub access token. If nothing is enterd, the current config will be taken over." + msg := "Please enter GitHub access token. If nothing is entered, the current config will be taken over." v, err := survey.AskOnePassword(msg) if err != nil { diff --git a/cmd/qpm/internal/cmd/install.go b/cmd/qpm/internal/cmd/install.go index e3d023d..fe50103 100644 --- a/cmd/qpm/internal/cmd/install.go +++ b/cmd/qpm/internal/cmd/install.go @@ -1,8 +1,13 @@ package cmd import ( + "bufio" + "fmt" + "os" + "github.com/anoriqq/qpm" "github.com/anoriqq/qpm/cmd/qpm/internal/config" + "github.com/anoriqq/qpm/cmd/qpm/internal/survey" "github.com/spf13/cobra" ) @@ -11,7 +16,7 @@ func init() { installCmd := &cobra.Command{ Use: "install", - Short: "Install specifc package", + Short: "Install specific package", Example: ` # Install foo package qpm install foo`, Args: cobra.RangeArgs(1, 2), @@ -35,10 +40,36 @@ func init() { return err } - return qpm.Execute(c, s, qpm.Install) + if alreadyInstalled, err := qpm.IsAlreadyInstalled(s); err != nil { + return err + } else { + if alreadyInstalled { + if v, err := SurveyForceInstall(s.Name); err != nil { + return err + } else { + if !v { + fmt.Println("install canceled") + return nil + } + } + } + } + + return qpm.Execute(c, s, qpm.Install, bufio.NewWriter(os.Stdout), bufio.NewWriter(os.Stderr)) }, } installCmd.PersistentFlags().StringVarP(&aquiferPath, "aquifer", "a", "", "Aquifer directory path") rootCmd.AddCommand(installCmd) } + +func SurveyForceInstall(name string) (bool, error) { + msg := name + " is already installed. Do you want to force installation?" + + v, err := survey.AskOneConfirm(msg, false) + if err != nil { + return false, err + } + + return v, nil +} diff --git a/cmd/qpm/internal/cmd/uninstall.go b/cmd/qpm/internal/cmd/uninstall.go index 0fba880..771fc6a 100644 --- a/cmd/qpm/internal/cmd/uninstall.go +++ b/cmd/qpm/internal/cmd/uninstall.go @@ -1,6 +1,9 @@ package cmd import ( + "bufio" + "os" + "github.com/anoriqq/qpm" "github.com/anoriqq/qpm/cmd/qpm/internal/config" "github.com/spf13/cobra" @@ -11,7 +14,7 @@ func init() { uninstallCmd := &cobra.Command{ Use: "uninstall", - Short: "Unnstall specifc package", + Short: "Uninstall specific package", Example: ` # Uninstall foo package qpm uninstall foo`, Args: cobra.RangeArgs(1, 2), @@ -35,7 +38,7 @@ func init() { return err } - return qpm.Execute(c, s, qpm.Uninstall) + return qpm.Execute(c, s, qpm.Uninstall, bufio.NewWriter(os.Stdout), bufio.NewWriter(os.Stderr)) }, } diff --git a/cmd/qpm/internal/git/git.go b/cmd/qpm/internal/git/git.go index 1da3927..9fed47f 100644 --- a/cmd/qpm/internal/git/git.go +++ b/cmd/qpm/internal/git/git.go @@ -5,7 +5,7 @@ import ( "net/http" "github.com/go-git/go-git/v5" - ghttp "github.com/go-git/go-git/v5/plumbing/transport/http" + gHTTP "github.com/go-git/go-git/v5/plumbing/transport/http" ) type client struct { @@ -17,7 +17,7 @@ type client struct { func (c *client) Clone(path, url string) error { o := &git.CloneOptions{ URL: url, - Auth: &ghttp.BasicAuth{ + Auth: &gHTTP.BasicAuth{ Username: c.username, Password: c.accessToken, }, diff --git a/go.mod b/go.mod index d06e505..ea1f0a4 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( github.com/stretchr/testify v1.8.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect golang.org/x/crypto v0.9.0 // indirect + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/net v0.10.0 // indirect golang.org/x/sys v0.8.0 // indirect golang.org/x/term v0.8.0 // indirect diff --git a/go.sum b/go.sum index 89a99e6..d6de0a9 100644 --- a/go.sum +++ b/go.sum @@ -113,6 +113,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= diff --git a/qpm.go b/qpm.go index 357df93..e9f8325 100644 --- a/qpm.go +++ b/qpm.go @@ -1,7 +1,6 @@ package qpm import ( - "bufio" "context" "fmt" "io" @@ -14,10 +13,11 @@ import ( "github.com/goccy/go-yaml" "github.com/pkg/errors" + "golang.org/x/exp/slices" ) func Version() string { - return "v2.0.0" + return "v3.0.0" } type Config struct { @@ -84,34 +84,38 @@ type ( steps []step } osToJob map[OS]job - stratum map[Action]osToJob + plan map[Action]osToJob + stratum struct { + Plan plan + Name string + } ) // ReadStratum AquiferPathにあるStratumのうち、指定されたStratumを取得する。 func ReadStratum(c Config, name string) (stratum, error) { path := os.ExpandEnv(c.AquiferPath) if _, err := os.Stat(path); os.IsNotExist(err) { - return nil, fmt.Errorf("aquifer not found in %s", path) + return stratum{}, fmt.Errorf("aquifer not found in %s", path) } stratumPath, err := filepath.Abs(fmt.Sprintf("%s/%s.yml", path, name)) if err != nil { - return nil, err + return stratum{}, err } if _, err := os.Stat(stratumPath); os.IsNotExist(err) { - return nil, fmt.Errorf("stratum not found path=%s", stratumPath) + return stratum{}, fmt.Errorf("stratum not found path=%s", stratumPath) } f, err := os.Open(stratumPath) if err != nil { - return nil, err + return stratum{}, err } defer f.Close() b, err := io.ReadAll(f) if err != nil { - return nil, err + return stratum{}, err } var ay map[string][]struct { @@ -121,25 +125,33 @@ func ReadStratum(c Config, name string) (stratum, error) { } if err := yaml.Unmarshal(b, &ay); err != nil { fmt.Println(yaml.FormatError(err, true, true)) - return nil, err + return stratum{}, err } - s := make(stratum) + s := stratum{ + Plan: make(plan), + Name: name, + } for actionStr, jobs := range ay { action, err := parseAction(actionStr) if err != nil { - return nil, err + return stratum{}, err } - if s[action] == nil { - s[action] = make(osToJob) + if s.Plan[action] == nil { + s.Plan[action] = make(osToJob) } for _, j := range jobs { for _, osStr := range j.OS { os, err := parseOS(osStr) if err != nil { - return nil, err + return stratum{}, err + } + + slices.Sort(j.Dependencies) + if len(slices.Compact(j.Dependencies)) != len(j.Dependencies) { + return stratum{}, errors.New("duplicate packages in dependencies") } steps := make([]step, 0) @@ -153,20 +165,20 @@ func ReadStratum(c Config, name string) (stratum, error) { case map[string]any: n, ok := v["name"] if !ok { - return nil, errors.Errorf("invalid value action=%v os=%v step-index=%d", action, os, i) + return stratum{}, errors.Errorf("invalid value action=%v os=%v step-index=%d", action, os, i) } r, ok := v["run"] if !ok { - return nil, errors.Errorf("invalid value action=%v os=%v step-index=%d", action, os, i) + return stratum{}, errors.Errorf("invalid value action=%v os=%v step-index=%d", action, os, i) } nn, ok := n.(string) if !ok { - return nil, errors.Errorf("invalid value action=%v os=%v step-index=%d", action, os, i) + return stratum{}, errors.Errorf("invalid value action=%v os=%v step-index=%d", action, os, i) } rr, ok := r.(string) if !ok { - return nil, errors.Errorf("invalid value action=%v os=%v step-index=%d", action, os, i) + return stratum{}, errors.Errorf("invalid value action=%v os=%v step-index=%d", action, os, i) } steps = append(steps, step{ @@ -174,11 +186,11 @@ func ReadStratum(c Config, name string) (stratum, error) { run: rr, }) default: - return nil, errors.Errorf("invalid value action=%v os=%v step-index=%d", action, os, i) + return stratum{}, errors.Errorf("invalid value action=%v os=%v step-index=%d", action, os, i) } } - s[action][os] = job{ + s.Plan[action][os] = job{ dependencies: j.Dependencies, steps: steps, } @@ -191,8 +203,26 @@ func ReadStratum(c Config, name string) (stratum, error) { var shellEscapeReplacer = strings.NewReplacer("$", `\$`) +var ErrPackageAlreadyInstalled = errors.New("package already installed") + +func IsAlreadyInstalled(s stratum) (bool, error) { + path, err := exec.LookPath(s.Name) + if err != nil { + if errors.Is(err, exec.ErrNotFound) { + return false, nil + } else { + return false, err + } + } + if len(path) == 0 { + return false, nil + } + + return true, nil +} + // Execute aquiferを実行する。 -func Execute(c Config, s stratum, action Action) error { +func Execute(c Config, s stratum, action Action, stdout, stderr io.Writer) error { cmd := exec.Command(c.Shell) cmd.Env = append(cmd.Environ(), @@ -200,10 +230,9 @@ func Execute(c Config, s stratum, action Action) error { fmt.Sprintf("QPM_ARCH=%s", runtime.GOARCH), ) - cmd.Stdout = bufio.NewWriter(os.Stdout) - cmd.Stderr = bufio.NewWriter(os.Stderr) + cmd.Stdout, cmd.Stderr = stdout, stderr - a, ok := s[action] + a, ok := s.Plan[action] if !ok { return errors.Errorf("%s is an Action not defined in the stratum", action) } diff --git a/qpm_test.go b/qpm_test.go index 513d80b..43fea4b 100644 --- a/qpm_test.go +++ b/qpm_test.go @@ -1,7 +1,9 @@ package qpm import ( + "bufio" "net/url" + "os" "testing" "github.com/google/go-cmp/cmp" @@ -34,7 +36,7 @@ func TestQPM_Execute(t *testing.T) { Shell: "zsh", } - err := Execute(c, stratum{}, "") + err := Execute(c, stratum{}, "", bufio.NewWriter(os.Stdout), bufio.NewWriter(os.Stderr)) if err != nil { t.Fatal(err) }