Skip to content

Commit b60af6b

Browse files
Install extension (#10)
1 parent d084586 commit b60af6b

5 files changed

Lines changed: 486 additions & 40 deletions

File tree

internal/cli/install.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,18 @@ func newInstallCmd() *cobra.Command {
3030
"into the currently active PHP.",
3131
Args: cobra.NoArgs,
3232
RunE: func(cmd *cobra.Command, args []string) error {
33-
if opts.ExtensionOnly {
34-
// Extension-only install is implemented in a later step.
35-
return errNotImplemented("extension install")
36-
}
37-
return installer.InstallInterpreter(cmd.Context(), installer.Options{
33+
o := installer.Options{
3834
Scope: platform.ScopeFromUserFlag(globalOpts.User),
3935
PHPVersion: opts.PHPVersion,
4036
ZTS: opts.ZTS,
4137
AssumeYes: globalOpts.Yes,
4238
Out: cmd.OutOrStdout(),
4339
In: cmd.InOrStdin(),
44-
})
40+
}
41+
if opts.ExtensionOnly {
42+
return installer.InstallExtension(cmd.Context(), o)
43+
}
44+
return installer.InstallInterpreter(cmd.Context(), o)
4545
},
4646
}
4747

internal/installer/extension.go

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package installer
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
10+
"github.com/php-debugger/installer/internal/manifest"
11+
"github.com/php-debugger/installer/internal/php"
12+
"github.com/php-debugger/installer/internal/platform"
13+
"github.com/php-debugger/installer/internal/release"
14+
)
15+
16+
// ErrNoInterpreter is returned by InstallExtension when no php is found on PATH.
17+
var ErrNoInterpreter = errors.New(
18+
"no PHP interpreter found on PATH.\n" +
19+
"The extension needs an existing PHP to install into. Install the debugger\n" +
20+
"interpreter instead (php-debugger install), or install PHP and retry.")
21+
22+
// InstallExtension installs just the debugger extension into the current PHP:
23+
// it downloads the extension matching that php's version/threading, copies it
24+
// into the php's extension_dir, disables any real xdebug in the existing ini
25+
// files, enables the extension, and verifies it loads — reverting everything on
26+
// failure.
27+
func InstallExtension(ctx context.Context, opts Options) error {
28+
env, err := opts.env()
29+
if err != nil {
30+
return err
31+
}
32+
p := platform.Platform{OS: env.OS, Arch: env.Arch}
33+
34+
layout, err := platform.Resolve(env, opts.Scope)
35+
if err != nil {
36+
return err
37+
}
38+
39+
// Require an existing interpreter.
40+
path, err := php.Detect()
41+
if err != nil {
42+
return ErrNoInterpreter
43+
}
44+
existing, err := php.Query(ctx, path)
45+
if err != nil {
46+
return fmt.Errorf("querying php at %s: %w", path, err)
47+
}
48+
if existing.ExtensionDir == "" {
49+
return fmt.Errorf("php at %s reports no extension_dir; cannot install the extension", path)
50+
}
51+
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
56+
}
57+
58+
client := opts.Client
59+
if client == nil {
60+
client = release.NewClient()
61+
}
62+
rel, err := client.LatestRelease(ctx)
63+
if err != nil {
64+
return err
65+
}
66+
asset, err := release.SelectAsset(rel.Assets, release.Selector{
67+
Kind: release.Extension,
68+
Series: existing.Series,
69+
ZTS: existing.ZTS,
70+
OS: p.OS,
71+
Arch: p.Arch,
72+
})
73+
if err != nil {
74+
return err
75+
}
76+
77+
opts.logf("Installing php-debugger extension for php %s (%s) %s, release %s",
78+
existing.Series, threading(existing.ZTS), p, rel.TagName)
79+
80+
tmpDir, err := os.MkdirTemp("", "php-debugger-ext-")
81+
if err != nil {
82+
return err
83+
}
84+
defer os.RemoveAll(tmpDir)
85+
86+
opts.logf("Downloading %s ...", asset.Name)
87+
dlPath, err := client.Download(ctx, asset, tmpDir)
88+
if err != nil {
89+
return err
90+
}
91+
92+
rb := &rollback{}
93+
94+
// --- copy the extension into extension_dir ---
95+
soDst := filepath.Join(existing.ExtensionDir, asset.Name)
96+
if err := registerFileRestore(soDst, rb, 0o755); err != nil {
97+
return err
98+
}
99+
if err := installFile(dlPath, soDst); err != nil {
100+
return fmt.Errorf("installing extension into %s: %w", existing.ExtensionDir, err)
101+
}
102+
opts.logf(" copied %s", soDst)
103+
104+
// --- disable any real xdebug in the existing ini files ---
105+
if err := stripXdebugFromExisting(existing, rb, opts); err != nil {
106+
rb.run()
107+
return fmt.Errorf("updating existing ini files; reverted: %w", err)
108+
}
109+
110+
// --- enable the extension ---
111+
iniPath, err := enableExtension(existing, soDst, rb)
112+
if err != nil {
113+
rb.run()
114+
return fmt.Errorf("enabling extension; reverted: %w", err)
115+
}
116+
opts.logf(" enabled via %s", iniPath)
117+
118+
// --- verify it loads ---
119+
opts.logf("Confirming the extension loads ...")
120+
has, err := php.HasModule(ctx, path, php.DebuggerModule)
121+
if err != nil {
122+
rb.run()
123+
return fmt.Errorf("checking for the debugger module; reverted: %w", err)
124+
}
125+
if !has {
126+
rb.run()
127+
return fmt.Errorf("the extension did not load in %s (its build may not match this php); reverted", path)
128+
}
129+
130+
// --- record in the manifest ---
131+
m, err := manifest.Load(layout.ManifestPath())
132+
if err != nil {
133+
rb.run()
134+
return err
135+
}
136+
m.InstallRoot = layout.Root
137+
m.SetExtension(manifest.Extension{
138+
Series: existing.Series,
139+
PHPVersion: existing.Version,
140+
ZTS: existing.ZTS,
141+
ReleaseTag: rel.TagName,
142+
SoPath: soDst,
143+
IniPath: iniPath,
144+
InstalledAt: opts.clock(),
145+
})
146+
if err := m.Save(layout.ManifestPath()); err != nil {
147+
rb.run()
148+
return fmt.Errorf("saving manifest; reverted: %w", err)
149+
}
150+
151+
opts.logf("Installed the php-debugger extension into %s.", path)
152+
return nil
153+
}
154+
155+
// stripXdebugFromExisting disables a real xdebug in the existing php's ini files
156+
// (in place) so it does not conflict with the debugger's simulated one. It reuses
157+
// the shared ini rules (see rewriteIniFiles) without commenting other loaders —
158+
// the existing php can load its own extensions.
159+
func stripXdebugFromExisting(existing *php.Info, rb *rollback, opts Options) error {
160+
var files []string
161+
if existing.Ini.LoadedFile != "" {
162+
files = append(files, existing.Ini.LoadedFile)
163+
}
164+
files = append(files, existing.Ini.AdditionalFiles...)
165+
return sanitizeInPlace(files, rb, opts)
166+
}
167+
168+
// enableExtension writes a loader that enables the debugger extension: a
169+
// dedicated ini in the scan dir if there is one, otherwise appended to the main
170+
// php.ini. Returns the ini file path. Registers undo on rb.
171+
func enableExtension(existing *php.Info, soPath string, rb *rollback) (string, error) {
172+
line := "; Enables the php-debugger extension (added by php-debugger)\nzend_extension=" + soPath + "\n"
173+
174+
if existing.Ini.ScanDir != "" {
175+
iniPath := filepath.Join(existing.Ini.ScanDir, "99-php-debugger.ini")
176+
if err := registerFileRestore(iniPath, rb, 0o644); err != nil {
177+
return "", err
178+
}
179+
if err := os.MkdirAll(existing.Ini.ScanDir, 0o755); err != nil {
180+
return "", fmt.Errorf("creating scan dir: %w", err)
181+
}
182+
if err := os.WriteFile(iniPath, []byte(line), 0o644); err != nil {
183+
return "", err
184+
}
185+
return iniPath, nil
186+
}
187+
188+
if existing.Ini.LoadedFile != "" {
189+
iniPath := existing.Ini.LoadedFile
190+
prev, err := os.ReadFile(iniPath)
191+
if err != nil {
192+
return "", fmt.Errorf("reading %s: %w", iniPath, err)
193+
}
194+
if err := registerFileRestore(iniPath, rb, 0o644); err != nil {
195+
return "", err
196+
}
197+
if err := os.WriteFile(iniPath, append(prev, []byte("\n"+line)...), 0o644); err != nil {
198+
return "", err
199+
}
200+
return iniPath, nil
201+
}
202+
203+
return "", errors.New("no ini file available to enable the extension (php reports no scan dir or loaded php.ini)")
204+
}

0 commit comments

Comments
 (0)