Skip to content

Commit dc57f44

Browse files
Release v1.0.49 with on-demand PHP runtime installs and installer defaults cleanup.
This ships PHP 8.4-only default provisioning, admin-driven installation of additional PHP versions, and coordinated backend/UI updates for safer PHP version selection and system settings behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 8da1240 commit dc57f44

File tree

10 files changed

+207
-13
lines changed

10 files changed

+207
-13
lines changed

cmd/fastcp/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ func main() {
180180
r.Put("/system/ssh-config", apiHandler.SetSSHConfig)
181181
r.Get("/system/php-default-config", apiHandler.GetPHPDefaultConfig)
182182
r.Put("/system/php-default-config", apiHandler.SetPHPDefaultConfig)
183+
r.Post("/system/php/install-version", apiHandler.InstallPHPVersion)
183184
r.Get("/system/caddy-config", apiHandler.GetCaddyConfig)
184185
r.Put("/system/caddy-config", apiHandler.SetCaddyConfig)
185186
r.Get("/system/firewall", apiHandler.GetFirewallStatus)

cmd/fastcp/ui/dist/index.html

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,24 @@ <h3 class="text-lg font-semibold text-gray-800 mb-1">Default PHP Version</h3>
10411041
</select>
10421042
<p class="text-xs text-gray-400 mt-1">Only installed PHP versions are listed.</p>
10431043
</div>
1044+
<div>
1045+
<label class="block text-sm font-medium text-gray-700 mb-1">Install Additional PHP Version</label>
1046+
<div class="flex flex-wrap gap-2">
1047+
<template x-for="version in ['8.2', '8.3', '8.4', '8.5']" :key="'install-php-' + version">
1048+
<button @click="installPHPVersion(version)"
1049+
:disabled="isPHPVersionInstalled(version) || phpVersionInstalling[version]"
1050+
class="inline-flex items-center px-3 py-2 border rounded-lg text-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
1051+
:class="isPHPVersionInstalled(version) ? 'border-gray-200 text-gray-400 bg-gray-50' : 'border-primary-200 text-primary-700 hover:bg-primary-50'">
1052+
<svg x-show="phpVersionInstalling[version]" class="animate-spin -ml-0.5 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
1053+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
1054+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
1055+
</svg>
1056+
<span x-text="isPHPVersionInstalled(version) ? ('PHP ' + version + ' Installed') : (phpVersionInstalling[version] ? ('Installing PHP ' + version + '...') : ('Install PHP ' + version))"></span>
1057+
</button>
1058+
</template>
1059+
</div>
1060+
<p class="text-xs text-gray-400 mt-1">FastCP installs PHP 8.4 by default. Install other versions only when needed.</p>
1061+
</div>
10441062
</div>
10451063

10461064
<div class="mt-6 flex items-center">
@@ -2007,6 +2025,7 @@ <h3 class="text-lg font-semibold text-gray-900" x-text="confirmModalTitle"></h3>
20072025
phpDefaultConfig: null,
20082026
phpDefaultConfigLoading: false,
20092027
phpDefaultConfigSaving: false,
2028+
phpVersionInstalling: {},
20102029
phpDefaultConfigError: '',
20112030
phpDefaultConfigSuccess: '',
20122031
mysqlConfig: null,
@@ -2239,6 +2258,27 @@ <h3 class="text-lg font-semibold text-gray-900" x-text="confirmModalTitle"></h3>
22392258
}
22402259
this.phpDefaultConfigLoading = false;
22412260
},
2261+
isPHPVersionInstalled(version) {
2262+
const installed = (this.phpDefaultConfig?.available_php_versions || []).map(v => (v || '').toString().trim());
2263+
return installed.includes((version || '').toString().trim());
2264+
},
2265+
async installPHPVersion(version) {
2266+
const v = (version || '').toString().trim();
2267+
if (!v || this.isPHPVersionInstalled(v)) return;
2268+
this.phpVersionInstalling = { ...this.phpVersionInstalling, [v]: true };
2269+
this.phpDefaultConfigError = '';
2270+
this.phpDefaultConfigSuccess = '';
2271+
try {
2272+
await this.api('/system/php/install-version', 'POST', { version: v });
2273+
this.phpDefaultConfigSuccess = `PHP ${v} installed successfully.`;
2274+
await this.loadPHPDefaultConfig();
2275+
await this.loadSystemStatus();
2276+
} catch (e) {
2277+
this.phpDefaultConfigError = e.message || `Failed to install PHP ${v}`;
2278+
} finally {
2279+
this.phpVersionInstalling = { ...this.phpVersionInstalling, [v]: false };
2280+
}
2281+
},
22422282
async savePHPDefaultConfig() {
22432283
this.phpDefaultConfigSaving = true;
22442284
this.phpDefaultConfigError = '';

internal/agent/client.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,11 @@ func (c *Client) SetPHPDefaultConfig(ctx context.Context, cfg *PHPDefaultConfig)
239239
return err
240240
}
241241

242+
func (c *Client) InstallPHPVersion(ctx context.Context, version string) error {
243+
_, err := c.call(ctx, "system.installPhpVersion", &PHPVersionInstallRequest{Version: version})
244+
return err
245+
}
246+
242247
func (c *Client) GetCaddyConfig(ctx context.Context) (*CaddyConfig, error) {
243248
result, err := c.call(ctx, "system.getCaddyConfig", nil)
244249
if err != nil {

internal/agent/handlers.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2950,6 +2950,108 @@ func (s *Server) handleSetPHPDefaultConfig(ctx context.Context, params json.RawM
29502950
return map[string]string{"status": "ok"}, nil
29512951
}
29522952

2953+
func runAptCommand(args ...string) ([]byte, error) {
2954+
cmd := exec.Command("apt-get", args...)
2955+
cmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive", "NEEDRESTART_SUSPEND=1")
2956+
return cmd.CombinedOutput()
2957+
}
2958+
2959+
func phpPackageExists(pkg string) bool {
2960+
return exec.Command("apt-cache", "show", pkg).Run() == nil
2961+
}
2962+
2963+
func resolveInstallablePHPPackages(version string) ([]string, error) {
2964+
base := []string{
2965+
"php" + version,
2966+
"php" + version + "-fpm",
2967+
}
2968+
modules := []string{
2969+
"bcmath", "bz2", "cli", "common", "curl", "gd", "gmp", "igbinary", "imagick",
2970+
"imap", "intl", "mbstring", "mysql", "opcache", "readline", "redis", "soap",
2971+
"sqlite3", "xml", "xmlrpc", "zip",
2972+
}
2973+
pkgs := make([]string, 0, len(base)+len(modules))
2974+
for _, pkg := range base {
2975+
if phpPackageExists(pkg) {
2976+
pkgs = append(pkgs, pkg)
2977+
}
2978+
}
2979+
hasFPM := false
2980+
for _, p := range pkgs {
2981+
if p == "php"+version+"-fpm" {
2982+
hasFPM = true
2983+
break
2984+
}
2985+
}
2986+
if !hasFPM {
2987+
return nil, fmt.Errorf("php%s-fpm is not available in apt repositories", version)
2988+
}
2989+
for _, m := range modules {
2990+
pkg := "php" + version + "-" + m
2991+
if phpPackageExists(pkg) {
2992+
pkgs = append(pkgs, pkg)
2993+
}
2994+
}
2995+
return pkgs, nil
2996+
}
2997+
2998+
func (s *Server) startOrRestartPHPFPM(version string) error {
2999+
service := "php" + version + "-fpm"
3000+
if s.hasSystemd() {
3001+
_ = s.runSystemctl("enable", service)
3002+
return s.serviceReloadOrRestart(service)
3003+
}
3004+
if output, err := exec.Command("service", service, "restart").CombinedOutput(); err == nil {
3005+
return nil
3006+
} else {
3007+
return fmt.Errorf("failed to restart %s: %w: %s", service, err, strings.TrimSpace(string(output)))
3008+
}
3009+
}
3010+
3011+
func (s *Server) handleInstallPHPVersion(ctx context.Context, params json.RawMessage) (any, error) {
3012+
var req PHPVersionInstallRequest
3013+
if err := json.Unmarshal(params, &req); err != nil {
3014+
return nil, fmt.Errorf("invalid params: %w", err)
3015+
}
3016+
version := normalizePHPVersion(req.Version)
3017+
if version != strings.TrimSpace(req.Version) {
3018+
return nil, fmt.Errorf("unsupported php version %q", req.Version)
3019+
}
3020+
3021+
for _, v := range detectAvailablePHPVersions() {
3022+
if v == version {
3023+
return map[string]string{"status": "ok", "message": "php version already installed"}, nil
3024+
}
3025+
}
3026+
3027+
if output, err := runAptCommand("update", "-qq"); err != nil {
3028+
return nil, fmt.Errorf("failed to update apt indexes: %w: %s", err, strings.TrimSpace(string(output)))
3029+
}
3030+
pkgs, err := resolveInstallablePHPPackages(version)
3031+
if err != nil {
3032+
return nil, err
3033+
}
3034+
args := append([]string{"install", "-y", "-qq"}, pkgs...)
3035+
if output, err := runAptCommand(args...); err != nil {
3036+
return nil, fmt.Errorf("failed to install php%s packages: %w: %s", version, err, strings.TrimSpace(string(output)))
3037+
}
3038+
if err := s.startOrRestartPHPFPM(version); err != nil {
3039+
return nil, err
3040+
}
3041+
if err := s.generateCaddyfile(); err != nil {
3042+
return nil, fmt.Errorf("failed to regenerate Caddyfile after php install: %w", err)
3043+
}
3044+
if !s.isCaddyRunning() {
3045+
if err := s.startCaddy(); err != nil {
3046+
return nil, fmt.Errorf("failed to start Caddy after php install: %w", err)
3047+
}
3048+
} else if err := s.reloadCaddy(); err != nil {
3049+
return nil, fmt.Errorf("failed to reload Caddy after php install: %w", err)
3050+
}
3051+
slog.Info("installed php runtime on demand", "version", version)
3052+
return map[string]string{"status": "ok", "message": "php version installed"}, nil
3053+
}
3054+
29533055
func (s *Server) handleGetMySQLConfig(ctx context.Context, params json.RawMessage) (any, error) {
29543056
cfg := &MySQLConfig{BufferPoolMB: 128, MaxConnections: 30, PerfSchema: false}
29553057

internal/agent/handlers_darwin.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,10 @@ func (s *Server) handleSetPHPDefaultConfig(ctx context.Context, params json.RawM
540540
return nil, fmt.Errorf("PHP default setting is not supported on macOS - use Ubuntu for production")
541541
}
542542

543+
func (s *Server) handleInstallPHPVersion(ctx context.Context, params json.RawMessage) (any, error) {
544+
return nil, fmt.Errorf("PHP runtime install is not supported on macOS - use Ubuntu for production")
545+
}
546+
543547
func (s *Server) handleGetFirewallStatus(ctx context.Context, params json.RawMessage) (any, error) {
544548
return &FirewallStatus{
545549
Installed: false,

internal/agent/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ func (s *Server) registerHandlers() {
173173
s.handlers["system.setSshConfig"] = s.handleSetSSHConfig
174174
s.handlers["system.getPhpDefaultConfig"] = s.handleGetPHPDefaultConfig
175175
s.handlers["system.setPhpDefaultConfig"] = s.handleSetPHPDefaultConfig
176+
s.handlers["system.installPhpVersion"] = s.handleInstallPHPVersion
176177
s.handlers["system.getCaddyConfig"] = s.handleGetCaddyConfig
177178
s.handlers["system.setCaddyConfig"] = s.handleSetCaddyConfig
178179
s.handlers["system.getFirewallStatus"] = s.handleGetFirewallStatus

internal/agent/types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ type PHPDefaultConfig struct {
129129
AvailablePHPVersions []string `json:"available_php_versions,omitempty"`
130130
}
131131

132+
// PHPVersionInstallRequest requests installation of a specific PHP runtime.
133+
type PHPVersionInstallRequest struct {
134+
Version string `json:"version"`
135+
}
136+
132137
// CaddyConfig represents tunable Caddy performance/logging settings
133138
type CaddyConfig struct {
134139
Profile string `json:"profile"` // balanced | low_ram | high_throughput

internal/api/handler.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,25 @@ func (h *Handler) SetPHPDefaultConfig(w http.ResponseWriter, r *http.Request) {
592592
h.json(w, http.StatusOK, map[string]string{"status": "ok", "message": "Default PHP version updated."})
593593
}
594594

595+
func (h *Handler) InstallPHPVersion(w http.ResponseWriter, r *http.Request) {
596+
var req struct {
597+
Version string `json:"version"`
598+
}
599+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
600+
h.error(w, http.StatusBadRequest, "invalid request body")
601+
return
602+
}
603+
if strings.TrimSpace(req.Version) == "" {
604+
h.error(w, http.StatusBadRequest, "php version is required")
605+
return
606+
}
607+
if err := h.sysService.InstallPHPVersion(r.Context(), req.Version); err != nil {
608+
h.error(w, http.StatusInternalServerError, err.Error())
609+
return
610+
}
611+
h.json(w, http.StatusOK, map[string]string{"status": "ok", "message": "PHP version installed successfully."})
612+
}
613+
595614
func (h *Handler) GetCaddyConfig(w http.ResponseWriter, r *http.Request) {
596615
cfg, err := h.sysService.GetCaddyConfig(r.Context())
597616
if err != nil {

internal/api/system.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ func (s *SystemService) SetPHPDefaultConfig(ctx context.Context, cfg *agent.PHPD
155155
return s.agent.SetPHPDefaultConfig(ctx, cfg)
156156
}
157157

158+
func (s *SystemService) InstallPHPVersion(ctx context.Context, version string) error {
159+
return s.agent.InstallPHPVersion(ctx, version)
160+
}
161+
158162
// GetCaddyConfig returns current Caddy performance/logging settings
159163
func (s *SystemService) GetCaddyConfig(ctx context.Context) (*agent.CaddyConfig, error) {
160164
return s.agent.GetCaddyConfig(ctx)

scripts/install.sh

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,18 @@ fi
170170
wait_for_apt
171171
apt-get update -qq
172172

173-
COMMON_PHP_MODULES="bcmath bz2 cli common curl fpm gd gmp igbinary imagick imap intl mbstring mysql opcache readline redis soap sqlite3 xml xmlrpc zip"
173+
COMMON_PHP_MODULES="bcmath bz2 cli common curl gd gmp igbinary imagick imap intl mbstring mysql opcache readline redis soap sqlite3 xml xmlrpc zip"
174174
php_package_exists() {
175175
apt-cache show "$1" >/dev/null 2>&1
176176
}
177177

178-
PHP_TARGET_VERSIONS="8.2 8.3 8.4 8.5"
178+
declare -A PHP_PACKAGE_SET=()
179+
add_php_package() {
180+
local pkg="$1"
181+
[[ -n "$pkg" ]] && PHP_PACKAGE_SET["$pkg"]=1
182+
}
183+
184+
PHP_TARGET_VERSIONS="8.4"
179185
AVAILABLE_PHP_VERSIONS=""
180186
for v in $PHP_TARGET_VERSIONS; do
181187
if php_package_exists "php${v}-fpm"; then
@@ -185,29 +191,36 @@ done
185191
AVAILABLE_PHP_VERSIONS=$(echo "$AVAILABLE_PHP_VERSIONS" | xargs)
186192

187193
if [[ -z "$AVAILABLE_PHP_VERSIONS" ]]; then
188-
error "No supported PHP-FPM packages found (expected one of: ${PHP_TARGET_VERSIONS})"
194+
error "Default PHP ${PHP_TARGET_VERSIONS} is not available in apt repositories"
189195
fi
190196

191197
log "Detected PHP versions in apt repo: ${AVAILABLE_PHP_VERSIONS}"
192198
for v in $AVAILABLE_PHP_VERSIONS; do
193-
log "Configuring PHP ${v}..."
194-
base_pkgs=""
195-
php_package_exists "php${v}" && base_pkgs="$base_pkgs php${v}"
196-
php_package_exists "php${v}-fpm" && base_pkgs="$base_pkgs php${v}-fpm"
197-
if [[ -n "$base_pkgs" ]]; then
198-
wait_for_apt
199-
apt-get install -y -qq $base_pkgs > /dev/null
200-
fi
199+
log "Preparing package list for PHP ${v}..."
200+
php_package_exists "php${v}" && add_php_package "php${v}"
201+
php_package_exists "php${v}-fpm" && add_php_package "php${v}-fpm"
201202

202203
for m in $COMMON_PHP_MODULES; do
203204
pkg="php${v}-${m}"
204205
if php_package_exists "$pkg"; then
205-
wait_for_apt
206-
apt-get install -y -qq "$pkg" > /dev/null
206+
add_php_package "$pkg"
207207
fi
208208
done
209209
done
210210

211+
mapfile -t PHP_PACKAGES < <(printf "%s\n" "${!PHP_PACKAGE_SET[@]}" | sort)
212+
if [[ ${#PHP_PACKAGES[@]} -eq 0 ]]; then
213+
error "No PHP packages resolved for installation"
214+
fi
215+
216+
log "Downloading PHP packages (${#PHP_PACKAGES[@]})..."
217+
wait_for_apt
218+
apt-get install -y -qq --download-only "${PHP_PACKAGES[@]}" > /dev/null
219+
220+
log "Installing and configuring PHP packages..."
221+
wait_for_apt
222+
apt-get install -y -qq "${PHP_PACKAGES[@]}" > /dev/null
223+
211224
# Download plain Caddy (lightweight root reverse proxy -- no PHP)
212225
log "Downloading Caddy..."
213226
CADDY_ARCH="amd64"

0 commit comments

Comments
 (0)