Skip to content

Commit 5072c3b

Browse files
committed
[ADD] Hestia CP Support.
1 parent 16cf08c commit 5072c3b

3 files changed

Lines changed: 140 additions & 22 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ composer global update evolution-cms/installer
7777

7878
- GitHub API rate limit: set `GITHUB_TOKEN` (classic or fine-grained token with public repo access) in your environment.
7979
- Permissions: ensure the package `bin/` directory is writable (the bootstrapper installs `bin/evo.bin` or `bin/evo.exe` there).
80+
- Composer not found / Composer is a shell alias (common on hosting panels like Hestia): install a Composer executable on `PATH` or set `EVO_COMPOSER_BIN` to the full path, e.g. `EVO_COMPOSER_BIN=$HOME/.composer/composer evo install`.
8081

8182
## Usage
8283

bin/evo

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -177,14 +177,30 @@ final class EvoBootstrapper
177177
{
178178
$re = '/Composer\\s+(?:version\\s+)?([0-9]+\\.[0-9]+\\.[0-9]+)/i';
179179

180-
$out = $this->runCommandCaptureOutput('composer -V 2>&1');
181-
if ($out !== null && preg_match($re, $out, $m)) {
182-
return $m[1];
183-
}
180+
$bins = ['composer', 'composer2'];
181+
$binEnv = getenv('EVO_COMPOSER_BIN') ?: '';
182+
if ($binEnv !== '') {
183+
array_unshift($bins, $binEnv);
184+
}
185+
$home = getenv('HOME') ?: '';
186+
if ($home !== '') {
187+
// Hosting panels (e.g. Hestia) sometimes provide Composer via a shell alias to a user-local path.
188+
$bins[] = $home . '/.composer/composer';
189+
$bins[] = $home . '/.composer/vendor/bin/composer';
190+
$bins[] = $home . '/bin/composer';
191+
}
192+
193+
foreach ($bins as $bin) {
194+
$arg = escapeshellarg($bin);
195+
$out = $this->runCommandCaptureOutput($arg . ' -V 2>&1');
196+
if ($out !== null && preg_match($re, $out, $m)) {
197+
return $m[1];
198+
}
184199

185-
$out = $this->runCommandCaptureOutput('composer --version 2>&1');
186-
if ($out !== null && preg_match($re, $out, $m)) {
187-
return $m[1];
200+
$out = $this->runCommandCaptureOutput($arg . ' --version 2>&1');
201+
if ($out !== null && preg_match($re, $out, $m)) {
202+
return $m[1];
203+
}
188204
}
189205

190206
return null;

cmd/evo/main.go

Lines changed: 116 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"flag"
77
"fmt"
8+
"io/fs"
89
"os"
910
"os/exec"
1011
"os/signal"
@@ -60,32 +61,132 @@ func run(ctx context.Context, args []string) int {
6061
}
6162
}
6263

63-
var composerVersionMajorRe = regexp.MustCompile(`(?i)\bComposer version\s+(\d+)\.`)
64+
var composerVersionMajorRe = regexp.MustCompile(`(?i)\bComposer\s+(?:version\s+)?(\d+)\.`)
6465

6566
func ensureComposer2(ctx context.Context) bool {
66-
out, err := exec.CommandContext(ctx, "composer", "--version").CombinedOutput()
67-
if err != nil {
68-
fmt.Fprintln(os.Stderr, "Composer 2.x is required.")
69-
fmt.Fprintln(os.Stderr, "Please upgrade Composer before continuing.")
70-
return false
67+
candidates := composerCandidates()
68+
var lastErr error
69+
var lastOut string
70+
var detectedMajor int
71+
var detectedBin string
72+
73+
for _, bin := range candidates {
74+
outBytes, err := exec.CommandContext(ctx, bin, "--no-ansi", "--version").CombinedOutput()
75+
out := strings.TrimSpace(string(outBytes))
76+
77+
m := composerVersionMajorRe.FindStringSubmatch(out)
78+
if len(m) >= 2 {
79+
major, parseErr := strconv.Atoi(m[1])
80+
if parseErr == nil {
81+
if major >= 2 {
82+
return true
83+
}
84+
detectedMajor = major
85+
detectedBin = bin
86+
}
87+
}
88+
89+
if err == nil {
90+
lastErr = nil
91+
lastOut = out
92+
continue
93+
}
94+
95+
lastErr = err
96+
lastOut = out
97+
98+
// If the executable isn't found, try other common names/paths.
99+
if errors.Is(err, exec.ErrNotFound) {
100+
continue
101+
}
71102
}
72103

73-
m := composerVersionMajorRe.FindStringSubmatch(string(out))
74-
if len(m) < 2 {
75-
fmt.Fprintln(os.Stderr, "Composer 2.x is required.")
104+
fmt.Fprintln(os.Stderr, "Composer 2.x is required.")
105+
if detectedMajor > 0 && detectedMajor < 2 {
106+
fmt.Fprintf(os.Stderr, "Detected Composer %d.x via %q.\n", detectedMajor, detectedBin)
107+
fmt.Fprintln(os.Stderr, "Your system uses Composer 1.x which is incompatible with PHP 8.3.")
76108
fmt.Fprintln(os.Stderr, "Please upgrade Composer before continuing.")
77109
return false
78110
}
79111

80-
major, err := strconv.Atoi(m[1])
81-
if err != nil || major < 2 {
82-
fmt.Fprintln(os.Stderr, "Composer 2.x is required.")
83-
fmt.Fprintln(os.Stderr, "Your system uses Composer 1.x which is incompatible with PHP 8.3.")
84-
fmt.Fprintln(os.Stderr, "Please upgrade Composer before continuing.")
112+
if lastErr != nil {
113+
// Help users who have Composer only as a shell alias/function (not an executable on PATH).
114+
if errors.Is(lastErr, exec.ErrNotFound) {
115+
fmt.Fprintln(os.Stderr, "Composer executable was not found in PATH.")
116+
} else {
117+
fmt.Fprintf(os.Stderr, "Could not run Composer: %v\n", lastErr)
118+
}
119+
}
120+
if lastOut != "" {
121+
fmt.Fprintf(os.Stderr, "Composer output: %s\n", firstLine(lastOut))
122+
}
123+
fmt.Fprintln(os.Stderr, "Please upgrade Composer before continuing.")
124+
fmt.Fprintln(os.Stderr, "Tip: if `composer` works in your shell but not here, it may be an alias/function; install the Composer binary or set EVO_COMPOSER_BIN.")
125+
return false
126+
}
127+
128+
func composerCandidates() []string {
129+
var candidates []string
130+
seen := map[string]struct{}{}
131+
132+
add := func(bin string) {
133+
bin = strings.TrimSpace(bin)
134+
if bin == "" {
135+
return
136+
}
137+
if _, ok := seen[bin]; ok {
138+
return
139+
}
140+
seen[bin] = struct{}{}
141+
candidates = append(candidates, bin)
142+
}
143+
144+
if v := os.Getenv("EVO_COMPOSER_BIN"); strings.TrimSpace(v) != "" {
145+
add(v)
146+
}
147+
148+
// Standard names that may be on PATH.
149+
add("composer")
150+
add("composer2")
151+
152+
// Common system locations.
153+
add("/usr/local/bin/composer")
154+
add("/usr/bin/composer")
155+
add("/bin/composer")
156+
157+
// Hosting panels (e.g. Hestia) sometimes provide Composer as a shell alias to a user-local path.
158+
if home, err := os.UserHomeDir(); err == nil && strings.TrimSpace(home) != "" {
159+
userLocal := []string{
160+
home + "/.composer/composer",
161+
home + "/.composer/vendor/bin/composer",
162+
home + "/bin/composer",
163+
}
164+
for _, p := range userLocal {
165+
if isExecutableFile(p) {
166+
add(p)
167+
}
168+
}
169+
}
170+
171+
return candidates
172+
}
173+
174+
func isExecutableFile(path string) bool {
175+
info, err := os.Stat(path)
176+
if err != nil {
177+
return false
178+
}
179+
if !info.Mode().IsRegular() {
85180
return false
86181
}
182+
return info.Mode().Perm()&fs.FileMode(0o111) != 0
183+
}
87184

88-
return true
185+
func firstLine(s string) string {
186+
if i := strings.IndexByte(s, '\n'); i >= 0 {
187+
return strings.TrimSpace(s[:i])
188+
}
189+
return strings.TrimSpace(s)
89190
}
90191

91192
func runInstall(ctx context.Context, args []string) int {

0 commit comments

Comments
 (0)