diff --git a/googet.goospec b/googet.goospec index fe1c509..def96b1 100644 --- a/googet.goospec +++ b/googet.goospec @@ -1,6 +1,6 @@ { "name": "googet", - "version": "2.12.0@1", + "version": "2.13.0@1", "arch": "x86_64", "authors": "ajackura@google.com", "license": "http://www.apache.org/licenses/LICENSE-2.0", @@ -12,6 +12,7 @@ "path": "install.ps1" }, "releaseNotes": [ + "2.13.0 - Add Conflicts and Replaces fields to the GooGet PkgSpec.", "2.12.0 - Store goo files unextracted in cache, ensure checksum match on reinstall.", "2.11.0 - Add allowunsafeurl config option to enable HTTP repos, otherwise disable.", " - Force the use of a package checksum when downloading from a repository.", diff --git a/googet_clean.go b/googet_clean.go index 6dfc55a..e089dc6 100644 --- a/googet_clean.go +++ b/googet_clean.go @@ -66,7 +66,7 @@ func cleanPackages(pl []string) { for _, pkg := range *state { if goolib.ContainsString(pkg.PackageSpec.Name, pl) { - if err := oswrap.RemoveAll(pkg.UnpackDir); err != nil { + if err := oswrap.RemoveAll(pkg.LocalPath); err != nil { logger.Error(err) } } @@ -95,7 +95,7 @@ func cleanOld() { var il []string for _, pkg := range *state { - il = append(il, pkg.UnpackDir) + il = append(il, pkg.LocalPath) } clean(il) } diff --git a/googet_test.go b/googet_test.go index 9896742..29be2e5 100644 --- a/googet_test.go +++ b/googet_test.go @@ -225,19 +225,23 @@ func TestCleanOld(t *testing.T) { } defer oswrap.RemoveAll(rootDir) - wantDir := filepath.Join(rootDir, cacheDir, "want") + wantFile := filepath.Join(rootDir, cacheDir, "want.goo") notWantDir := filepath.Join(rootDir, cacheDir, "notWant") + notWantFile := filepath.Join(rootDir, cacheDir, "notWant.goo") - if err := oswrap.MkdirAll(wantDir, 0700); err != nil { + if err := oswrap.MkdirAll(notWantDir, 0700); err != nil { t.Fatal(err) } - if err := oswrap.MkdirAll(notWantDir, 0700); err != nil { + if err := ioutil.WriteFile(notWantFile, nil, 0700); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(wantFile, nil, 0700); err != nil { t.Fatal(err) } state := &client.GooGetState{ { - UnpackDir: wantDir, + LocalPath: wantFile, }, } @@ -247,13 +251,17 @@ func TestCleanOld(t *testing.T) { cleanOld() - if _, err := oswrap.Stat(wantDir); err != nil { + if _, err := oswrap.Stat(wantFile); err != nil { t.Errorf("cleanOld removed wantDir, Stat err: %v", err) } if _, err := oswrap.Stat(notWantDir); err == nil { t.Errorf("cleanOld did not remove notWantDir") } + + if _, err := oswrap.Stat(notWantFile); err == nil { + t.Errorf("cleanOld did not remove notWantFile") + } } func TestCleanPackages(t *testing.T) { @@ -264,25 +272,28 @@ func TestCleanPackages(t *testing.T) { } defer oswrap.RemoveAll(rootDir) - wantDir := filepath.Join(rootDir, cacheDir, "want") - notWantDir := filepath.Join(rootDir, cacheDir, "notWant") + wantFile := filepath.Join(rootDir, cacheDir, "want") + notWantFile := filepath.Join(rootDir, cacheDir, "notWant") - if err := oswrap.MkdirAll(wantDir, 0700); err != nil { + if err := oswrap.MkdirAll(filepath.Join(rootDir, cacheDir), 0700); err != nil { t.Fatal(err) } - if err := oswrap.MkdirAll(notWantDir, 0700); err != nil { + if err := ioutil.WriteFile(wantFile, nil, 0700); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(notWantFile, nil, 0700); err != nil { t.Fatal(err) } state := &client.GooGetState{ { - UnpackDir: wantDir, + LocalPath: wantFile, PackageSpec: &goolib.PkgSpec{ Name: "want", }, }, { - UnpackDir: notWantDir, + LocalPath: notWantFile, PackageSpec: &goolib.PkgSpec{ Name: "notWant", }, @@ -295,11 +306,11 @@ func TestCleanPackages(t *testing.T) { cleanPackages([]string{"notWant"}) - if _, err := oswrap.Stat(wantDir); err != nil { + if _, err := oswrap.Stat(wantFile); err != nil { t.Errorf("cleanPackages removed wantDir, Stat err: %v", err) } - if _, err := oswrap.Stat(notWantDir); err == nil { + if _, err := oswrap.Stat(notWantFile); err == nil { t.Errorf("cleanPackages did not remove notWantDir") } } diff --git a/goolib/goolib.go b/goolib/goolib.go index 0d562cc..c302109 100644 --- a/goolib/goolib.go +++ b/goolib/goolib.go @@ -102,6 +102,16 @@ type PackageInfo struct { Name, Arch, Ver string } +func (pi PackageInfo) String() string { + if pi.Arch != "" && pi.Ver != "" { + return fmt.Sprintf("%s.%s.%s", pi.Name, pi.Arch, pi.Ver) + } + if pi.Arch != "" { + return fmt.Sprintf("%s.%s", pi.Name, pi.Arch) + } + return pi.Name +} + // PkgName returns the proper goo package name. func (pi PackageInfo) PkgName() string { return fmt.Sprintf("%s.%s.%s.goo", pi.Name, pi.Arch, pi.Ver) diff --git a/goolib/goospec.go b/goolib/goospec.go index 50d4019..ac5e25c 100644 --- a/goolib/goospec.go +++ b/goolib/goospec.go @@ -68,7 +68,7 @@ const ( var validArch = []string{"noarch", "x86_64", "x86_32", "arm"} -// PkgSpec is the internal package specification. +// PkgSpec is an individual package specification. type PkgSpec struct { Name string Version string @@ -80,6 +80,8 @@ type PkgSpec struct { Owners string `json:",omitempty"` Tags map[string][]byte `json:",omitempty"` PkgDependencies map[string]string `json:",omitempty"` + Replaces []string + Conflicts []string Install ExecFile Uninstall ExecFile Files map[string]string `json:",omitempty"` diff --git a/goolib/goospec_test.go b/goolib/goospec_test.go index 90d7a9f..3ab2171 100644 --- a/goolib/goospec_test.go +++ b/goolib/goospec_test.go @@ -297,6 +297,8 @@ func TestMarshal(t *testing.T) { ReleaseNotes: []string{"1.2.3@4 - something new", "1.2.3@4 - something"}, Description: "blah blah", Owners: "someone", + Replaces: []string{"foo"}, + Conflicts: []string{"bar"}, Install: ExecFile{ Path: "install.ps1", }, @@ -315,6 +317,12 @@ func TestMarshal(t *testing.T) { ], "Description": "blah blah", "Owners": "someone", + "Replaces": [ + "foo" + ], + "Conflicts": [ + "bar" + ], "Install": { "Path": "install.ps1" }, diff --git a/install/install.go b/install/install.go index dfbd68c..d362762 100644 --- a/install/install.go +++ b/install/install.go @@ -28,6 +28,7 @@ import ( "github.com/google/googet/download" "github.com/google/googet/goolib" "github.com/google/googet/oswrap" + "github.com/google/googet/remove" "github.com/google/googet/system" "github.com/google/logger" ) @@ -48,8 +49,51 @@ func minInstalled(pi goolib.PackageInfo, state client.GooGetState) (bool, error) return false, nil } +func resolveConflicts(ps *goolib.PkgSpec, state *client.GooGetState) error { + // Check for any conflicting packages. + // TODO(ajackura): Make sure no conflicting packages are listed as + // dependencies or subdependancies. + for _, pkg := range ps.Conflicts { + pi := goolib.PkgNameSplit(pkg) + ins, err := minInstalled(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: pi.Ver}, *state) + if err != nil { + return err + } + if ins { + return fmt.Errorf("cannot install, conflict with installed package: %s", pi) + } + } + return nil +} + +func resolveReplacements(ps *goolib.PkgSpec, state *client.GooGetState, dbOnly bool, proxyServer string) error { + // Check for and remove any package this replaces. + // TODO(ajackura): Make sure no replacements are listed as + // dependencies or subdependancies. + for _, pkg := range ps.Replaces { + pi := goolib.PkgNameSplit(pkg) + ins, err := minInstalled(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: pi.Ver}, *state) + if err != nil { + return err + } + if !ins { + continue + } + deps, _ := remove.EnumerateDeps(pi, *state) + logger.Infof("%s replaces %s, removing", ps, pi) + if err := remove.All(pi, deps, state, dbOnly, proxyServer); err != nil { + return err + } + } + return nil +} + func installDeps(ps *goolib.PkgSpec, cache string, rm client.RepoMap, archs []string, state *client.GooGetState, dbOnly bool, proxyServer string) error { - logger.Infof("Resolving dependencies for %s %s version %s", ps.Arch, ps.Name, ps.Version) + logger.Infof("Resolving conflicts and dependencies for %s %s version %s", ps.Arch, ps.Name, ps.Version) + if err := resolveConflicts(ps, state); err != nil { + return err + } + // Check for and install any dependencies. for p, ver := range ps.PkgDependencies { pi := goolib.PkgNameSplit(p) mi, err := minInstalled(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: ver}, *state) @@ -80,7 +124,7 @@ func installDeps(ps *goolib.PkgSpec, cache string, rm client.RepoMap, archs []st return fmt.Errorf("cannot resolve dependancy, %s.%s version %s or greater not installed and not available in any repo", pi.Name, arch, ver) } } - return nil + return resolveReplacements(ps, state, dbOnly, proxyServer) } // FromRepo installs a package and all dependencies from a repository. @@ -157,6 +201,9 @@ func FromDisk(arg, cache string, state *client.GooGetState, dbOnly, ri bool) err logger.Infof("Starting install of %q, version %q from %q", zs.Name, zs.Version, arg) fmt.Printf("Installing %s %s...\n", zs.Name, zs.Version) + if err := resolveConflicts(zs, state); err != nil { + return err + } for p, ver := range zs.PkgDependencies { pi := goolib.PkgNameSplit(p) mi, err := minInstalled(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: ver}, *state) @@ -169,6 +216,16 @@ func FromDisk(arg, cache string, state *client.GooGetState, dbOnly, ri bool) err } return fmt.Errorf("package dependency %s %s (min version %s) not installed", pi.Name, pi.Arch, ver) } + for _, pkg := range zs.Replaces { + pi := goolib.PkgNameSplit(pkg) + ins, err := minInstalled(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: pi.Ver}, *state) + if err != nil { + return err + } + if ins { + return fmt.Errorf("cannot install, replaces installed package, remove first then try installation again: %s", pi) + } + } dst := filepath.Join(cache, goolib.PackageInfo{Name: zs.Name, Arch: zs.Arch, Ver: zs.Version}.PkgName()) if err := copyPkg(arg, dst); err != nil { @@ -208,6 +265,11 @@ func Reinstall(ps client.PackageState, state client.GooGetState, rd bool, proxyS pi := goolib.PackageInfo{Name: ps.PackageSpec.Name, Arch: ps.PackageSpec.Arch, Ver: ps.PackageSpec.Version} logger.Infof("Starting reinstall of %s.%s, version %s", pi.Name, pi.Arch, pi.Ver) fmt.Printf("Reinstalling %s.%s %s and dependencies...\n", pi.Name, pi.Arch, pi.Ver) + // Fix for package install by older versions of GooGet. + if ps.LocalPath == "" { + ps.LocalPath = ps.UnpackDir + ".goo" + } + f, err := os.Open(ps.LocalPath) if err != nil && !os.IsNotExist(err) { return err diff --git a/remove/remove.go b/remove/remove.go index 6f0d9ea..3fc46a4 100644 --- a/remove/remove.go +++ b/remove/remove.go @@ -33,27 +33,50 @@ func uninstallPkg(pi goolib.PackageInfo, state *client.GooGetState, dbOnly bool, if err != nil { return fmt.Errorf("package not found in state file: %v", err) } + // Fix for package install by older versions of GooGet. + if ps.LocalPath == "" { + ps.LocalPath = ps.UnpackDir + ".goo" + } if !dbOnly { - _, err := oswrap.Stat(ps.UnpackDir) + f, err := os.Open(ps.LocalPath) if err != nil && !os.IsNotExist(err) { return err } + var rd bool if os.IsNotExist(err) { - dst := ps.UnpackDir + ".goo" - logger.Infof("Package directory does not exist for %s.%s.%s, redownloading...", ps.PackageSpec.Name, ps.PackageSpec.Arch, ps.PackageSpec.Version) - if err := download.Package(ps.DownloadURL, dst, ps.Checksum, proxyServer); err != nil { - return fmt.Errorf("error redownloading %s.%s.%s, package may no longer exist in the repo, you can use the '-db_only' flag to remove it form the database: %v", pi.Name, pi.Arch, pi.Ver, err) - } - if _, err := download.ExtractPkg(dst); err != nil { - return err + logger.Infof("Local package does not exist for %s.%s.%s, redownloading...", pi.Name, pi.Arch, pi.Ver) + rd = true + } + // Force redownload if checksum does not match. + // If checksum is empty this was a local install so ignore. + if !rd && ps.Checksum != "" && goolib.Checksum(f) != ps.Checksum { + logger.Info("Local package checksum does not match, redownloading...") + rd = true + } + f.Close() + + if rd { + if ps.DownloadURL == "" { + return fmt.Errorf("can not redownload %s.%s.%s, DownloadURL not saved", pi.Name, pi.Arch, pi.Ver) } - if err := oswrap.Remove(dst); err != nil { - logger.Errorf("error cleaning up package file: %v", err) + if err := download.Package(ps.DownloadURL, ps.LocalPath, ps.Checksum, proxyServer); err != nil { + return fmt.Errorf("error redownloading %s.%s.%s, package may no longer exist in the repo, you can use the '-db_only' flag to remove it form the database: %v", pi.Name, pi.Arch, pi.Ver, err) } } - if err := system.Uninstall(ps); err != nil { + + eDir, err := download.ExtractPkg(ps.LocalPath) + if err != nil { + return err + } + + if err := system.Uninstall(eDir, ps.PackageSpec); err != nil { return err } + + if err := oswrap.RemoveAll(eDir); err != nil { + logger.Error(err) + } + if len(ps.InstalledFiles) > 0 { var dirs []string for file, chksum := range ps.InstalledFiles { @@ -76,7 +99,7 @@ func uninstallPkg(pi goolib.PackageInfo, state *client.GooGetState, dbOnly bool, } } - if err := oswrap.RemoveAll(ps.UnpackDir); err != nil { + if err := oswrap.RemoveAll(ps.LocalPath); err != nil { logger.Errorf("error removing package data from cache directory: %v", err) } return state.Remove(pi) diff --git a/remove/remove_test.go b/remove/remove_test.go index 7c4de1e..0089106 100644 --- a/remove/remove_test.go +++ b/remove/remove_test.go @@ -14,7 +14,11 @@ limitations under the License. package remove import ( + "archive/tar" + "compress/gzip" + "io" "io/ioutil" + "log" "path/filepath" "reflect" "testing" @@ -49,24 +53,57 @@ func TestUninstallPkg(t *testing.T) { t.Fatalf("Failed to create test folder: %v", err) } - testFile := filepath.Join(testFolder3, "foo") - if err := ioutil.WriteFile(testFile, []byte{}, 0666); err != nil { + f, err := oswrap.Create(filepath.Join(src, "test.goo")) + if err != nil { + log.Fatal(err) + } + defer oswrap.Remove(f.Name()) + + gw := gzip.NewWriter(f) + tw := tar.NewWriter(gw) + testFile, err := oswrap.Create(filepath.Join(testFolder3, "foo")) + if err != nil { t.Fatalf("Failed to create test file: %v", err) } + fi, err := testFile.Stat() + if err != nil { + t.Fatal(err) + } + fih, err := tar.FileInfoHeader(fi, "") + if err != nil { + t.Fatal(err) + } + if err := tw.WriteHeader(fih); err != nil { + t.Fatal(err) + } + if _, err := io.Copy(tw, testFile); err != nil { + t.Fatal(err) + } + + if err := testFile.Close(); err != nil { + t.Fatalf("Failed to close test file: %v", err) + } + + tw.Close() + gw.Close() + if err := f.Close(); err != nil { + log.Fatal(err) + } + st := &client.GooGetState{ client.PackageState{ PackageSpec: &goolib.PkgSpec{ Name: "foo", }, InstalledFiles: map[string]string{ - testFile: "chksum", - testFolder: "", - testFolder2: "", - testFolder3: "", - dst: "", + testFile.Name(): "chksum", + testFolder: "", + testFolder2: "", + testFolder3: "", + dst: "", }, - UnpackDir: dst, + LocalPath: f.Name(), }, } @@ -74,7 +111,7 @@ func TestUninstallPkg(t *testing.T) { t.Fatalf("Error running uninstallPkg: %v", err) } - for _, n := range []string{testFile, dst} { + for _, n := range []string{testFile.Name(), dst} { if _, err := oswrap.Stat(n); err == nil { t.Errorf("%s was not removed", n) } diff --git a/system/system_linux.go b/system/system_linux.go index 366cd68..662d2bf 100644 --- a/system/system_linux.go +++ b/system/system_linux.go @@ -20,7 +20,6 @@ import ( "fmt" "path/filepath" - "github.com/google/googet/client" "github.com/google/googet/goolib" "github.com/google/googet/oswrap" "github.com/google/logger" @@ -51,16 +50,15 @@ func Install(dir string, ps *goolib.PkgSpec) error { } // Uninstall performs a system specfic uninstall given a packages PackageState. -func Uninstall(st client.PackageState) error { - un := st.PackageSpec.Uninstall +func Uninstall(dir string, ps *goolib.PkgSpec) error { + un := ps.Uninstall if un.Path == "" { - logger.Info("No uninstaller specified") return nil } logger.Infof("Running uninstall: %q", un.Path) // logging is only useful for failed uninstalls - out, err := oswrap.Create(filepath.Join(st.UnpackDir, "googet_remove.log")) + out, err := oswrap.Create(filepath.Join(dir, "googet_remove.log")) if err != nil { return err } @@ -69,7 +67,7 @@ func Uninstall(st client.PackageState) error { logger.Error(err) } }() - return goolib.Exec(filepath.Join(st.UnpackDir, un.Path), un.Args, un.ExitCodes, out) + return goolib.Exec(filepath.Join(dir, un.Path), un.Args, un.ExitCodes, out) } // InstallableArchs returns a slice of archs supported by this machine. diff --git a/system/system_windows.go b/system/system_windows.go index 48b3b4c..0e49045 100644 --- a/system/system_windows.go +++ b/system/system_windows.go @@ -24,7 +24,6 @@ import ( "runtime" "github.com/StackExchange/wmi" - "github.com/google/googet/client" "github.com/google/googet/goolib" "github.com/google/googet/oswrap" "github.com/google/logger" @@ -116,16 +115,15 @@ func Install(dir string, ps *goolib.PkgSpec) error { } // Uninstall performs a system specfic uninstall given a packages PackageState. -func Uninstall(st client.PackageState) error { - un := st.PackageSpec.Uninstall +func Uninstall(dir string, ps *goolib.PkgSpec) error { + un := ps.Uninstall if un.Path == "" { - logger.Info("No uninstaller specified") return nil } - logger.Infof("Running uninstall: %q", un.Path) + logger.Infof("Running uninstall command: %q", un.Path) // logging is only useful for failed uninstall - out, err := oswrap.Create(filepath.Join(st.UnpackDir, un.Path+".log")) + out, err := oswrap.Create(filepath.Join(dir, un.Path+".log")) if err != nil { return err } @@ -134,11 +132,11 @@ func Uninstall(st client.PackageState) error { logger.Error(err) } }() - s := filepath.Join(st.UnpackDir, un.Path) + s := filepath.Join(dir, un.Path) ec := append(msiSuccessCodes, un.ExitCodes...) switch filepath.Ext(s) { case ".msi": - msiLog := filepath.Join(st.UnpackDir, "msi_uninstall.log") + msiLog := filepath.Join(dir, "msi_uninstall.log") args := append([]string{"/x", s, "/qn", "/norestart", "/log", msiLog}, un.Args...) err = goolib.Run(exec.Command("msiexec", args...), ec, out) case ".msu": @@ -147,13 +145,13 @@ func Uninstall(st client.PackageState) error { case ".exe": err = goolib.Run(exec.Command(s, un.Args...), ec, out) default: - err = goolib.Exec(filepath.Join(st.UnpackDir, un.Path), un.Args, un.ExitCodes, out) + err = goolib.Exec(filepath.Join(dir, un.Path), un.Args, un.ExitCodes, out) } if err != nil { return err } - if err := removeUninstallEntry(st.PackageSpec.Name); err != nil { + if err := removeUninstallEntry(ps.Name); err != nil { logger.Error(err) }