Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions internal/cli/update.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package cli

import "github.com/spf13/cobra"
import (
"github.com/php-debugger/installer/internal/installer"
"github.com/php-debugger/installer/internal/platform"
"github.com/spf13/cobra"
)

// updateOptions holds flags specific to the update command.
type updateOptions struct {
Expand All @@ -21,7 +25,12 @@ func newUpdateCmd() *cobra.Command {
"are installed.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return errNotImplemented("update")
return installer.Update(cmd.Context(), installer.Options{
Scope: platform.ScopeFromUserFlag(globalOpts.User),
AssumeYes: globalOpts.Yes,
Out: cmd.OutOrStdout(),
In: cmd.InOrStdin(),
}, opts.Interpreter, opts.Extension)
},
}

Expand Down
13 changes: 8 additions & 5 deletions internal/installer/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,20 @@ func InstallExtension(ctx context.Context, opts Options) error {
return fmt.Errorf("php at %s reports no extension_dir; cannot install the extension", path)
}

// Nothing to do if the debugger is already present (e.g. our own interpreter).
if has, _ := php.HasModule(ctx, path, php.DebuggerModule); has {
opts.logf("%s already has the %s module; nothing to do.", path, php.DebuggerModule)
return nil
// Nothing to do if the debugger is already present (e.g. our own
// interpreter), unless forced (as by `update`).
if !opts.Force {
if has, _ := php.HasModule(ctx, path, php.DebuggerModule); has {
opts.logf("%s already has the %s module; nothing to do.", path, php.DebuggerModule)
return nil
}
}

client := opts.Client
if client == nil {
client = release.NewClient()
}
rel, err := client.LatestRelease(ctx)
rel, err := opts.latestRelease(ctx, client)
if err != nil {
return err
}
Expand Down
39 changes: 37 additions & 2 deletions internal/installer/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,30 @@ type Options struct {
// `switch` so a newly installed variant activates where the current one lives.
BinDir string

// Force skips "nothing to do" short-circuits (used by `update` to reinstall
// even when the debugger is already present).
Force bool

// Client and Env are optional overrides for testing. When nil, real ones are
// constructed.
Client *release.Client
Env *platform.Env

// preloadedRelease, when set, is used instead of fetching the latest release
// (so `update` can fetch once to compare versions, then reuse it).
preloadedRelease *release.Release

now func() time.Time // optional clock override for tests
}

// latestRelease returns the preloaded release if set, otherwise fetches it.
func (o Options) latestRelease(ctx context.Context, client *release.Client) (*release.Release, error) {
if o.preloadedRelease != nil {
return o.preloadedRelease, nil
}
return client.LatestRelease(ctx)
}

func (o Options) logf(format string, args ...any) {
if o.Out != nil {
fmt.Fprintf(o.Out, format+"\n", args...)
Expand Down Expand Up @@ -103,7 +119,7 @@ func InstallInterpreter(ctx context.Context, opts Options) error {
if client == nil {
client = release.NewClient()
}
rel, err := client.LatestRelease(ctx)
rel, err := opts.latestRelease(ctx, client)
if err != nil {
return err
}
Expand Down Expand Up @@ -171,10 +187,24 @@ func InstallInterpreter(ctx context.Context, opts Options) error {
// --- place the binary into the versioned directory ---
versionDir := layout.VersionDir(series, opts.ZTS)
binTarget := filepath.Join(versionDir, "bin", phpBinaryName(p.OS))
// If the version dir already exists (an update/reinstall), preserve the old
// binary so a failed install can be rolled back to a working state; else the
// whole fresh dir is removed on rollback.
versionExisted := isDir(versionDir)
if versionExisted {
if err := registerFileRestore(binTarget, rb, 0o755); err != nil {
return err
}
}
if err := installFile(dlPath, binTarget); err != nil {
if !versionExisted {
os.RemoveAll(versionDir)
}
return fmt.Errorf("installing interpreter binary: %w", err)
}
rb.add(func() error { return os.RemoveAll(versionDir) })
if !versionExisted {
rb.add(func() error { return os.RemoveAll(versionDir) })
}

// --- copy the existing interpreter's ini config into the new one ---
var configFiles []string
Expand Down Expand Up @@ -327,6 +357,11 @@ func maybeWarnManaged(opts Options, existing *php.Info) {
}
}

func isDir(p string) bool {
fi, err := os.Stat(p)
return err == nil && fi.IsDir()
}

func threading(zts bool) string {
if zts {
return "zts"
Expand Down
112 changes: 112 additions & 0 deletions internal/installer/update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package installer

import (
"context"
"errors"
"fmt"

"github.com/php-debugger/installer/internal/manifest"
"github.com/php-debugger/installer/internal/platform"
"github.com/php-debugger/installer/internal/release"
)

// Update reinstalls the active interpreter and/or the extension against the
// latest release. With neither wantInterp nor wantExt set it updates whatever is
// installed, erroring if both are present (ambiguous). Each target is skipped if
// already on the latest release.
func Update(ctx context.Context, opts Options, wantInterp, wantExt bool) error {
env, err := opts.env()
if err != nil {
return err
}
layout, err := platform.Resolve(env, opts.Scope)
if err != nil {
return err
}
m, err := manifest.Load(layout.ManifestPath())
if err != nil {
return err
}

hasInterp := m.Active() != ""
hasExt := m.Extension != nil

if !wantInterp && !wantExt {
switch {
case hasInterp && hasExt:
return errors.New("both an interpreter and an extension are installed; " +
"specify --interpreter or --extension")
case hasInterp:
wantInterp = true
case hasExt:
wantExt = true
default:
return errors.New("nothing installed to update")
}
}
if wantInterp && !hasInterp {
return errors.New("no interpreter installed to update")
}
if wantExt && !hasExt {
return errors.New("no extension installed to update")
}

// Fetch the latest release once and reuse it for the install(s).
client := opts.Client
if client == nil {
client = release.NewClient()
}
rel, err := client.LatestRelease(ctx)
if err != nil {
return err
}

if wantInterp {
if err := updateInterpreter(ctx, opts, m, rel, client); err != nil {
return err
}
}
if wantExt {
if err := updateExtension(ctx, opts, m, rel, client); err != nil {
return err
}
}
return nil
}

func updateInterpreter(ctx context.Context, opts Options, m *manifest.Manifest, rel *release.Release, client *release.Client) error {
key := m.Active()
it, _ := m.Interpreter(key)
if it.ReleaseTag == rel.TagName {
opts.logf("Interpreter php %s is already up to date (release %s).", it.Series, rel.TagName)
return nil
}
opts.logf("Updating interpreter php %s (%s): %s -> %s ...",
it.Series, threading(it.ZTS), it.ReleaseTag, rel.TagName)

io := opts
io.PHPVersion = it.Series
io.ZTS = it.ZTS
io.BinDir = m.BinDir
io.Client = client
io.preloadedRelease = rel
return InstallInterpreter(ctx, io)
}

func updateExtension(ctx context.Context, opts Options, m *manifest.Manifest, rel *release.Release, client *release.Client) error {
ext := m.Extension
if ext.ReleaseTag == rel.TagName {
opts.logf("Extension for php %s is already up to date (release %s).", ext.Series, rel.TagName)
return nil
}
opts.logf("Updating extension for php %s: %s -> %s ...", ext.Series, ext.ReleaseTag, rel.TagName)

io := opts
io.Force = true
io.Client = client
io.preloadedRelease = rel
if err := InstallExtension(ctx, io); err != nil {
return fmt.Errorf("updating extension: %w", err)
}
return nil
}
Loading
Loading