Skip to content

Commit c40916f

Browse files
Update command
1 parent bf8ee47 commit c40916f

5 files changed

Lines changed: 364 additions & 9 deletions

File tree

internal/cli/update.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package cli
22

3-
import "github.com/spf13/cobra"
3+
import (
4+
"github.com/php-debugger/installer/internal/installer"
5+
"github.com/php-debugger/installer/internal/platform"
6+
"github.com/spf13/cobra"
7+
)
48

59
// updateOptions holds flags specific to the update command.
610
type updateOptions struct {
@@ -21,7 +25,12 @@ func newUpdateCmd() *cobra.Command {
2125
"are installed.",
2226
Args: cobra.NoArgs,
2327
RunE: func(cmd *cobra.Command, args []string) error {
24-
return errNotImplemented("update")
28+
return installer.Update(cmd.Context(), installer.Options{
29+
Scope: platform.ScopeFromUserFlag(globalOpts.User),
30+
AssumeYes: globalOpts.Yes,
31+
Out: cmd.OutOrStdout(),
32+
In: cmd.InOrStdin(),
33+
}, opts.Interpreter, opts.Extension)
2534
},
2635
}
2736

internal/installer/extension.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,20 @@ func InstallExtension(ctx context.Context, opts Options) error {
4949
return fmt.Errorf("php at %s reports no extension_dir; cannot install the extension", path)
5050
}
5151

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

5861
client := opts.Client
5962
if client == nil {
6063
client = release.NewClient()
6164
}
62-
rel, err := client.LatestRelease(ctx)
65+
rel, err := opts.latestRelease(ctx, client)
6366
if err != nil {
6467
return err
6568
}

internal/installer/install.go

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,30 @@ type Options struct {
3737
// `switch` so a newly installed variant activates where the current one lives.
3838
BinDir string
3939

40+
// Force skips "nothing to do" short-circuits (used by `update` to reinstall
41+
// even when the debugger is already present).
42+
Force bool
43+
4044
// Client and Env are optional overrides for testing. When nil, real ones are
4145
// constructed.
4246
Client *release.Client
4347
Env *platform.Env
4448

49+
// preloadedRelease, when set, is used instead of fetching the latest release
50+
// (so `update` can fetch once to compare versions, then reuse it).
51+
preloadedRelease *release.Release
52+
4553
now func() time.Time // optional clock override for tests
4654
}
4755

56+
// latestRelease returns the preloaded release if set, otherwise fetches it.
57+
func (o Options) latestRelease(ctx context.Context, client *release.Client) (*release.Release, error) {
58+
if o.preloadedRelease != nil {
59+
return o.preloadedRelease, nil
60+
}
61+
return client.LatestRelease(ctx)
62+
}
63+
4864
func (o Options) logf(format string, args ...any) {
4965
if o.Out != nil {
5066
fmt.Fprintf(o.Out, format+"\n", args...)
@@ -103,7 +119,7 @@ func InstallInterpreter(ctx context.Context, opts Options) error {
103119
if client == nil {
104120
client = release.NewClient()
105121
}
106-
rel, err := client.LatestRelease(ctx)
122+
rel, err := opts.latestRelease(ctx, client)
107123
if err != nil {
108124
return err
109125
}
@@ -171,10 +187,24 @@ func InstallInterpreter(ctx context.Context, opts Options) error {
171187
// --- place the binary into the versioned directory ---
172188
versionDir := layout.VersionDir(series, opts.ZTS)
173189
binTarget := filepath.Join(versionDir, "bin", phpBinaryName(p.OS))
190+
// If the version dir already exists (an update/reinstall), preserve the old
191+
// binary so a failed install can be rolled back to a working state; else the
192+
// whole fresh dir is removed on rollback.
193+
versionExisted := isDir(versionDir)
194+
if versionExisted {
195+
if err := registerFileRestore(binTarget, rb, 0o755); err != nil {
196+
return err
197+
}
198+
}
174199
if err := installFile(dlPath, binTarget); err != nil {
200+
if !versionExisted {
201+
os.RemoveAll(versionDir)
202+
}
175203
return fmt.Errorf("installing interpreter binary: %w", err)
176204
}
177-
rb.add(func() error { return os.RemoveAll(versionDir) })
205+
if !versionExisted {
206+
rb.add(func() error { return os.RemoveAll(versionDir) })
207+
}
178208

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

360+
func isDir(p string) bool {
361+
fi, err := os.Stat(p)
362+
return err == nil && fi.IsDir()
363+
}
364+
330365
func threading(zts bool) string {
331366
if zts {
332367
return "zts"

internal/installer/update.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package installer
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/php-debugger/installer/internal/manifest"
9+
"github.com/php-debugger/installer/internal/platform"
10+
"github.com/php-debugger/installer/internal/release"
11+
)
12+
13+
// Update reinstalls the active interpreter and/or the extension against the
14+
// latest release. With neither wantInterp nor wantExt set it updates whatever is
15+
// installed, erroring if both are present (ambiguous). Each target is skipped if
16+
// already on the latest release.
17+
func Update(ctx context.Context, opts Options, wantInterp, wantExt bool) error {
18+
env, err := opts.env()
19+
if err != nil {
20+
return err
21+
}
22+
layout, err := platform.Resolve(env, opts.Scope)
23+
if err != nil {
24+
return err
25+
}
26+
m, err := manifest.Load(layout.ManifestPath())
27+
if err != nil {
28+
return err
29+
}
30+
31+
hasInterp := m.Active() != ""
32+
hasExt := m.Extension != nil
33+
34+
if !wantInterp && !wantExt {
35+
switch {
36+
case hasInterp && hasExt:
37+
return errors.New("both an interpreter and an extension are installed; " +
38+
"specify --interpreter or --extension")
39+
case hasInterp:
40+
wantInterp = true
41+
case hasExt:
42+
wantExt = true
43+
default:
44+
return errors.New("nothing installed to update")
45+
}
46+
}
47+
if wantInterp && !hasInterp {
48+
return errors.New("no interpreter installed to update")
49+
}
50+
if wantExt && !hasExt {
51+
return errors.New("no extension installed to update")
52+
}
53+
54+
// Fetch the latest release once and reuse it for the install(s).
55+
client := opts.Client
56+
if client == nil {
57+
client = release.NewClient()
58+
}
59+
rel, err := client.LatestRelease(ctx)
60+
if err != nil {
61+
return err
62+
}
63+
64+
if wantInterp {
65+
if err := updateInterpreter(ctx, opts, m, rel, client); err != nil {
66+
return err
67+
}
68+
}
69+
if wantExt {
70+
if err := updateExtension(ctx, opts, m, rel, client); err != nil {
71+
return err
72+
}
73+
}
74+
return nil
75+
}
76+
77+
func updateInterpreter(ctx context.Context, opts Options, m *manifest.Manifest, rel *release.Release, client *release.Client) error {
78+
key := m.Active()
79+
it, _ := m.Interpreter(key)
80+
if it.ReleaseTag == rel.TagName {
81+
opts.logf("Interpreter php %s is already up to date (release %s).", it.Series, rel.TagName)
82+
return nil
83+
}
84+
opts.logf("Updating interpreter php %s (%s): %s -> %s ...",
85+
it.Series, threading(it.ZTS), it.ReleaseTag, rel.TagName)
86+
87+
io := opts
88+
io.PHPVersion = it.Series
89+
io.ZTS = it.ZTS
90+
io.BinDir = m.BinDir
91+
io.Client = client
92+
io.preloadedRelease = rel
93+
return InstallInterpreter(ctx, io)
94+
}
95+
96+
func updateExtension(ctx context.Context, opts Options, m *manifest.Manifest, rel *release.Release, client *release.Client) error {
97+
ext := m.Extension
98+
if ext.ReleaseTag == rel.TagName {
99+
opts.logf("Extension for php %s is already up to date (release %s).", ext.Series, rel.TagName)
100+
return nil
101+
}
102+
opts.logf("Updating extension for php %s: %s -> %s ...", ext.Series, ext.ReleaseTag, rel.TagName)
103+
104+
io := opts
105+
io.Force = true
106+
io.Client = client
107+
io.preloadedRelease = rel
108+
if err := InstallExtension(ctx, io); err != nil {
109+
return fmt.Errorf("updating extension: %w", err)
110+
}
111+
return nil
112+
}

0 commit comments

Comments
 (0)