|
5 | 5 | "errors" |
6 | 6 | "flag" |
7 | 7 | "fmt" |
| 8 | + "io/fs" |
8 | 9 | "os" |
9 | 10 | "os/exec" |
10 | 11 | "os/signal" |
@@ -60,32 +61,132 @@ func run(ctx context.Context, args []string) int { |
60 | 61 | } |
61 | 62 | } |
62 | 63 |
|
63 | | -var composerVersionMajorRe = regexp.MustCompile(`(?i)\bComposer version\s+(\d+)\.`) |
| 64 | +var composerVersionMajorRe = regexp.MustCompile(`(?i)\bComposer\s+(?:version\s+)?(\d+)\.`) |
64 | 65 |
|
65 | 66 | 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 | + } |
71 | 102 | } |
72 | 103 |
|
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.") |
76 | 108 | fmt.Fprintln(os.Stderr, "Please upgrade Composer before continuing.") |
77 | 109 | return false |
78 | 110 | } |
79 | 111 |
|
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() { |
85 | 180 | return false |
86 | 181 | } |
| 182 | + return info.Mode().Perm()&fs.FileMode(0o111) != 0 |
| 183 | +} |
87 | 184 |
|
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) |
89 | 190 | } |
90 | 191 |
|
91 | 192 | func runInstall(ctx context.Context, args []string) int { |
|
0 commit comments