diff --git a/cmd/cardano-up/install.go b/cmd/cardano-up/install.go index 9705d5b..3a464c6 100644 --- a/cmd/cardano-up/install.go +++ b/cmd/cardano-up/install.go @@ -42,7 +42,7 @@ func installCommand() *cobra.Command { return errors.New("no package provided") } if len(args) > 1 { - return errors.New("only one package may be specified a a time") + return errors.New("only one package may be specified at a time") } return nil }, diff --git a/cmd/cardano-up/main.go b/cmd/cardano-up/main.go index 2d4d7f4..b0a0a43 100644 --- a/cmd/cardano-up/main.go +++ b/cmd/cardano-up/main.go @@ -70,6 +70,7 @@ func main() { infoCommand(), installCommand(), uninstallCommand(), + upgradeCommand(), ) if err := rootCmd.Execute(); err != nil { diff --git a/cmd/cardano-up/uninstall.go b/cmd/cardano-up/uninstall.go index cae3d5f..3a44cde 100644 --- a/cmd/cardano-up/uninstall.go +++ b/cmd/cardano-up/uninstall.go @@ -15,6 +15,7 @@ package main import ( + "errors" "fmt" "log/slog" "os" @@ -27,15 +28,16 @@ func uninstallCommand() *cobra.Command { return &cobra.Command{ Use: "uninstall", Short: "Uninstall package", - Run: func(cmd *cobra.Command, args []string) { + Args: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { - slog.Error("no package provided") - os.Exit(1) + return errors.New("no package provided") } if len(args) > 1 { - slog.Error("only one package may be specified a a time") - os.Exit(1) + return errors.New("only one package may be specified at a time") } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { pm, err := pkgmgr.NewDefaultPackageManager() if err != nil { slog.Error(fmt.Sprintf("failed to create package manager: %s", err)) diff --git a/cmd/cardano-up/upgrade.go b/cmd/cardano-up/upgrade.go new file mode 100644 index 0000000..d1dad9e --- /dev/null +++ b/cmd/cardano-up/upgrade.go @@ -0,0 +1,54 @@ +// Copyright 2024 Blink Labs Software +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "errors" + "fmt" + "log/slog" + "os" + + "github.com/blinklabs-io/cardano-up/pkgmgr" + "github.com/spf13/cobra" +) + +func upgradeCommand() *cobra.Command { + upgradeCmd := &cobra.Command{ + Use: "upgrade", + Short: "Upgrade package", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("no package provided") + } + if len(args) > 1 { + return errors.New("only one package may be specified at a time") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + pm, err := pkgmgr.NewDefaultPackageManager() + if err != nil { + slog.Error(fmt.Sprintf("failed to create package manager: %s", err)) + os.Exit(1) + } + // Upgrade requested package + if err := pm.Upgrade(args[0]); err != nil { + slog.Error(err.Error()) + os.Exit(1) + } + }, + } + return upgradeCmd +} diff --git a/pkgmgr/error.go b/pkgmgr/error.go index cb518d9..83abeef 100644 --- a/pkgmgr/error.go +++ b/pkgmgr/error.go @@ -97,3 +97,10 @@ func NewPackageUninstallWouldBreakDepsError(uninstallPkgName string, uninstallPk dependentPkgVersion, ) } + +func NewNoPackageAvailableForUpgrade(pkgSpec string) error { + return fmt.Errorf( + "no package available for upgrade: %s", + pkgSpec, + ) +} diff --git a/pkgmgr/installed_package.go b/pkgmgr/installed_package.go index c0334df..27d1db9 100644 --- a/pkgmgr/installed_package.go +++ b/pkgmgr/installed_package.go @@ -33,3 +33,7 @@ func NewInstalledPackage(pkg Package, context string, postInstallNotes string) I PostInstallNotes: postInstallNotes, } } + +func (i InstalledPackage) IsEmpty() bool { + return i.InstalledTime.IsZero() +} diff --git a/pkgmgr/pkgmgr.go b/pkgmgr/pkgmgr.go index ff2e1a1..76533ec 100644 --- a/pkgmgr/pkgmgr.go +++ b/pkgmgr/pkgmgr.go @@ -108,6 +108,7 @@ func (p *PackageManager) Install(pkgs ...string) error { resolver, err := NewResolver( p.InstalledPackages(), p.AvailablePackages(), + activeContextName, p.config.Logger, ) if err != nil { @@ -160,6 +161,73 @@ func (p *PackageManager) Install(pkgs ...string) error { return nil } +func (p *PackageManager) Upgrade(pkgs ...string) error { + activeContextName, _ := p.ActiveContext() + resolver, err := NewResolver( + p.InstalledPackages(), + p.AvailablePackages(), + activeContextName, + p.config.Logger, + ) + if err != nil { + return err + } + upgradePkgs, err := resolver.Upgrade(pkgs...) + if err != nil { + return err + } + var installedPkgs []string + var notesOutput string + for _, upgradePkg := range upgradePkgs { + p.config.Logger.Info( + fmt.Sprintf( + "Upgrading package %s (%s => %s)", + upgradePkg.Installed.Package.Name, + upgradePkg.Installed.Package.Version, + upgradePkg.Upgrade.Version, + ), + ) + // Uninstall old version + if err := p.uninstallPackage(upgradePkg.Installed); err != nil { + return err + } + // Install new version + notes, err := upgradePkg.Upgrade.install(p.config, activeContextName) + if err != nil { + return err + } + installedPkg := NewInstalledPackage(upgradePkg.Upgrade, activeContextName, notes) + p.state.InstalledPackages = append(p.state.InstalledPackages, installedPkg) + if err := p.state.Save(); err != nil { + return err + } + installedPkgs = append(installedPkgs, upgradePkg.Upgrade.Name) + if notes != "" { + notesOutput += fmt.Sprintf( + "\nPost-install notes for %s (= %s):\n\n%s\n", + upgradePkg.Upgrade.Name, + upgradePkg.Upgrade.Version, + notes, + ) + } + if err := p.state.Save(); err != nil { + return err + } + } + // Display post-install notes + if notesOutput != "" { + p.config.Logger.Info(notesOutput) + } + p.config.Logger.Info( + fmt.Sprintf( + "Successfully upgraded/installed package(s) in context %q: %s", + activeContextName, + strings.Join(installedPkgs, ", "), + ), + ) + return nil +} + func (p *PackageManager) Uninstall(pkgs ...string) error { // Find installed packages activeContextName, _ := p.ActiveContext() @@ -185,6 +253,7 @@ func (p *PackageManager) Uninstall(pkgs ...string) error { resolver, err := NewResolver( p.InstalledPackages(), p.AvailablePackages(), + activeContextName, p.config.Logger, ) if err != nil { @@ -194,21 +263,9 @@ func (p *PackageManager) Uninstall(pkgs ...string) error { return err } for _, uninstallPkg := range uninstallPkgs { - // Uninstall package - if err := uninstallPkg.Package.uninstall(p.config, uninstallPkg.Context); err != nil { + if err := p.uninstallPackage(uninstallPkg); err != nil { return err } - // Remove package from installed packages - var tmpInstalledPackages []InstalledPackage - for _, tmpInstalledPkg := range p.state.InstalledPackages { - if tmpInstalledPkg.Context == uninstallPkg.Context && - tmpInstalledPkg.Package.Name == uninstallPkg.Package.Name && - tmpInstalledPkg.Package.Version == uninstallPkg.Package.Version { - continue - } - tmpInstalledPackages = append(tmpInstalledPackages, tmpInstalledPkg) - } - p.state.InstalledPackages = tmpInstalledPackages[:] if err := p.state.Save(); err != nil { return err } @@ -269,6 +326,25 @@ func (p *PackageManager) Info(pkgs ...string) error { return nil } +func (p *PackageManager) uninstallPackage(uninstallPkg InstalledPackage) error { + // Uninstall package + if err := uninstallPkg.Package.uninstall(p.config, uninstallPkg.Context); err != nil { + return err + } + // Remove package from installed packages + var tmpInstalledPackages []InstalledPackage + for _, tmpInstalledPkg := range p.state.InstalledPackages { + if tmpInstalledPkg.Context == uninstallPkg.Context && + tmpInstalledPkg.Package.Name == uninstallPkg.Package.Name && + tmpInstalledPkg.Package.Version == uninstallPkg.Package.Version { + continue + } + tmpInstalledPackages = append(tmpInstalledPackages, tmpInstalledPkg) + } + p.state.InstalledPackages = tmpInstalledPackages[:] + return nil +} + func (p *PackageManager) Contexts() map[string]Context { return p.state.Contexts } diff --git a/pkgmgr/registry.go b/pkgmgr/registry.go index ac3ae52..7a90046 100644 --- a/pkgmgr/registry.go +++ b/pkgmgr/registry.go @@ -98,6 +98,11 @@ docker run --rm -ti ghcr.io/blinklabs-io/mithril-client:0.7.0-1 $@ Version: "1.0.2", PostInstallNotes: "Notes for {{ .Package.Name }}", }, + { + Name: "test-packageA", + Version: "1.0.3", + PostInstallNotes: "Notes for {{ .Package.Name }}", + }, { Name: "test-packageA", Version: "2.1.3", diff --git a/pkgmgr/resolver.go b/pkgmgr/resolver.go index 86393ba..67847bb 100644 --- a/pkgmgr/resolver.go +++ b/pkgmgr/resolver.go @@ -23,14 +23,21 @@ import ( ) type Resolver struct { + context string logger *slog.Logger installedPkgs []InstalledPackage availablePkgs []Package installedConstraints map[string]version.Constraints } -func NewResolver(installedPkgs []InstalledPackage, availablePkgs []Package, logger *slog.Logger) (*Resolver, error) { +type ResolverUpgradeSet struct { + Installed InstalledPackage + Upgrade Package +} + +func NewResolver(installedPkgs []InstalledPackage, availablePkgs []Package, context string, logger *slog.Logger) (*Resolver, error) { r := &Resolver{ + context: context, logger: logger, installedPkgs: installedPkgs[:], availablePkgs: availablePkgs[:], @@ -38,28 +45,6 @@ func NewResolver(installedPkgs []InstalledPackage, availablePkgs []Package, logg } // Calculate package constraints from installed packages for _, installedPkg := range installedPkgs { - pkgName := installedPkg.Package.Name - // Add implicit constraint for other versions of the same package - tmpConstraints, err := version.NewConstraint( - fmt.Sprintf("= %s", installedPkg.Package.Version), - ) - if err != nil { - return nil, err - } - if _, ok := r.installedConstraints[pkgName]; !ok { - r.installedConstraints[pkgName] = make(version.Constraints, 0) - } - r.installedConstraints[pkgName] = append( - r.installedConstraints[pkgName], - tmpConstraints..., - ) - logger.Debug( - fmt.Sprintf( - "added constraint for installed package %q: %s", - pkgName, - tmpConstraints.String(), - ), - ) // Add constraint for each explicit dependency for _, dep := range installedPkg.Package.Dependencies { depPkgName, depPkgVersionSpec := r.splitPackage(dep) @@ -74,7 +59,7 @@ func NewResolver(installedPkgs []InstalledPackage, availablePkgs []Package, logg logger.Debug( fmt.Sprintf( "added constraint for installed package %q dependency: %q: %s", - pkgName, + installedPkg.Package.Name, depPkgName, tmpConstraints.String(), ), @@ -87,17 +72,13 @@ func NewResolver(installedPkgs []InstalledPackage, availablePkgs []Package, logg func (r *Resolver) Install(pkgs ...string) ([]Package, error) { var ret []Package for _, pkg := range pkgs { - pkgName, pkgVersion := r.splitPackage(pkg) + pkgName, pkgVersionSpec := r.splitPackage(pkg) if pkg, err := r.findInstalled(pkgName, ""); err != nil { return nil, err - } else if !pkg.InstalledTime.IsZero() { + } else if !pkg.IsEmpty() { return nil, NewResolverPackageAlreadyInstalledError(pkgName) } - availablePkgs, err := r.findAvailable(pkgName, pkgVersion) - if err != nil { - return nil, err - } - latestPkg, err := r.latestPackage(availablePkgs...) + latestPkg, err := r.latestAvailablePackage(pkgName, pkgVersionSpec, nil) if err != nil { return nil, err } @@ -113,6 +94,52 @@ func (r *Resolver) Install(pkgs ...string) ([]Package, error) { return ret, nil } +func (r *Resolver) Upgrade(pkgs ...string) ([]ResolverUpgradeSet, error) { + var ret []ResolverUpgradeSet + for _, pkg := range pkgs { + pkgName, pkgVersionSpec := r.splitPackage(pkg) + installedPkg, err := r.findInstalled(pkgName, "") + if err != nil { + return nil, err + } else if installedPkg.IsEmpty() { + return nil, NewPackageNotInstalledError(pkgName, r.context) + } + latestPkg, err := r.latestAvailablePackage(pkgName, pkgVersionSpec, nil) + if err != nil { + return nil, err + } + if latestPkg.Version == "" || latestPkg.Version == installedPkg.Package.Version { + return nil, NewNoPackageAvailableForUpgrade(pkg) + } + ret = append( + ret, + ResolverUpgradeSet{ + Installed: installedPkg, + Upgrade: latestPkg, + }, + ) + // Calculate dependencies + neededPkgs, err := r.getNeededDeps(latestPkg) + if err != nil { + return nil, err + } + for _, neededPkg := range neededPkgs { + tmpInstalled, err := r.findInstalled(neededPkg.Name, "") + if err != nil { + return nil, err + } + ret = append( + ret, + ResolverUpgradeSet{ + Installed: tmpInstalled, + Upgrade: neededPkg, + }, + ) + } + } + return ret, nil +} + func (r *Resolver) Uninstall(pkgs ...InstalledPackage) error { for _, pkg := range pkgs { pkgVersion, err := version.NewVersion(pkg.Package.Version) @@ -156,23 +183,23 @@ func (r *Resolver) getNeededDeps(pkg Package) ([]Package, error) { // Check if we already have an installed package that satisfies the dependency if pkg, err := r.findInstalled(depPkgName, depPkgVersionSpec); err != nil { return nil, err - } else if !pkg.InstalledTime.IsZero() { + } else if !pkg.IsEmpty() { continue } // Check if we already have any installed version of the package if pkg, err := r.findInstalled(depPkgName, depPkgVersionSpec); err != nil { return nil, err - } else if !pkg.InstalledTime.IsZero() { + } else if !pkg.IsEmpty() { return nil, NewResolverInstalledPackageNoMatchVersionSpecError(pkg.Package.Name, pkg.Package.Version, dep) } - availablePkgs, err := r.findAvailable(depPkgName, depPkgVersionSpec) + availablePkgs, err := r.findAvailable(depPkgName, depPkgVersionSpec, nil) if err != nil { return nil, err } if len(availablePkgs) == 0 { return nil, NewResolverNoAvailablePackageDependencyError(dep) } - latestPkg, err := r.latestPackage(availablePkgs...) + latestPkg, err := r.latestPackage(availablePkgs, nil) if err != nil { return nil, err } @@ -220,7 +247,7 @@ func (r *Resolver) findInstalled(pkgName string, pkgVersionSpec string) (Install return InstalledPackage{}, nil } -func (r *Resolver) findAvailable(pkgName string, pkgVersionSpec string) ([]Package, error) { +func (r *Resolver) findAvailable(pkgName string, pkgVersionSpec string, extraConstraints version.Constraints) ([]Package, error) { var constraints version.Constraints // Filter to versions matching our version spec if pkgVersionSpec != "" { @@ -230,15 +257,21 @@ func (r *Resolver) findAvailable(pkgName string, pkgVersionSpec string) ([]Packa } constraints = tmpConstraints } - // Filter to versions matching constraints from installed packages - if r.installedConstraints != nil { - if pkgConstraints, ok := r.installedConstraints[pkgName]; ok { - constraints = append( - constraints, - pkgConstraints..., - ) + // Use installed package constraints if none provided + if extraConstraints == nil { + if r.installedConstraints != nil { + if pkgConstraints, ok := r.installedConstraints[pkgName]; ok { + extraConstraints = pkgConstraints + } } } + // Filter to versions matching provided constraints + if extraConstraints != nil { + constraints = append( + constraints, + extraConstraints..., + ) + } var ret []Package for _, availablePkg := range r.availablePkgs { if availablePkg.Name != pkgName { @@ -266,7 +299,15 @@ func (r *Resolver) findAvailable(pkgName string, pkgVersionSpec string) ([]Packa return ret, nil } -func (r *Resolver) latestPackage(pkgs ...Package) (Package, error) { +func (r *Resolver) latestAvailablePackage(pkgName string, pkgVersionSpec string, constraints version.Constraints) (Package, error) { + pkgs, err := r.findAvailable(pkgName, pkgVersionSpec, constraints) + if err != nil { + return Package{}, err + } + return r.latestPackage(pkgs, constraints) +} + +func (r *Resolver) latestPackage(pkgs []Package, constraints version.Constraints) (Package, error) { var ret Package var retVer *version.Version for _, pkg := range pkgs { @@ -274,6 +315,13 @@ func (r *Resolver) latestPackage(pkgs ...Package) (Package, error) { if err != nil { return ret, err } + // Skip package if it doesn't match provided constraints + if len(constraints) > 0 { + if !constraints.Check(pkgVer) { + continue + } + } + // Set this package as the latest if none set or newer than previous set if retVer == nil || pkgVer.GreaterThan(retVer) { ret = pkg retVer = pkgVer