Skip to content

Commit 26bb051

Browse files
authored
Merge pull request #27 from tucksaun/feat/variant-support
feat: add support for flavor selection
2 parents 530d44a + 2440310 commit 26bb051

File tree

4 files changed

+221
-33
lines changed

4 files changed

+221
-33
lines changed

store.go

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,24 @@ type PHPStore struct {
4444

4545
// New creates a new PHP store
4646
func New(configDir string, reload bool, logger func(msg string, a ...interface{})) *PHPStore {
47-
s := &PHPStore{
48-
configDir: configDir,
49-
seen: make(map[string]int),
50-
discoveryLogFunc: logger,
51-
}
47+
s := newEmpty(configDir, logger)
48+
5249
if reload {
5350
_ = os.Remove(filepath.Join(configDir, "php_versions.json"))
5451
}
5552
s.loadVersions()
5653
return s
5754
}
5855

56+
// newEmpty creates a new "empty" (without loading versions) PHP store
57+
func newEmpty(configDir string, logger func(msg string, a ...interface{})) *PHPStore {
58+
return &PHPStore{
59+
configDir: configDir,
60+
seen: make(map[string]int),
61+
discoveryLogFunc: logger,
62+
}
63+
}
64+
5965
// Versions returns all available PHP versions
6066
func (s *PHPStore) Versions() []*Version {
6167
return s.versions
@@ -136,47 +142,51 @@ func (s *PHPStore) BestVersionForDir(dir string) (*Version, string, string, erro
136142
return s.fallbackVersion("")
137143
}
138144

139-
// bestVersion returns the latest patch version for the given major (X), minor (X.Y), or patch (X.Y.Z)
140-
// version can be 7 or 7.1 or 7.1.2
141-
// non-symlinked versions have priority
145+
// bestVersion returns the latest patch version for the given major (X),
146+
// minor (X.Y), or patch (X.Y.Z).
147+
// Version can be 7 or 7.1 or 7.1.2 and optionally suffixed with a flavor.
148+
// Non-symlinked versions have priority.
149+
// If the asked version contains a flavor (e.g. "7.4-fpm"), it will only accept
150+
// versions supporting this flavor.
142151
// If the asked version is a patch one (X.Y.Z) and is not available, the lookup
143-
// will fallback to the last path version for the minor version (X.Y).
152+
// will fallback to the last patch version for the minor version (X.Y).
144153
// There's no fallback to the major version because PHP is known to occasionally
145154
// break BC in minor versions, so we can't safely fall back.
146155
func (s *PHPStore) bestVersion(versionPrefix, source string) (*Version, string, string, error) {
147156
warning := ""
157+
flavor := ""
148158

149-
isPatchVersion := false
150-
pos := strings.LastIndexByte(versionPrefix, '.')
151-
if pos != strings.IndexByte(versionPrefix, '.') {
152-
if versionPrefix[pos+1:] == "99" {
153-
versionPrefix = versionPrefix[:pos]
154-
pos = strings.LastIndexByte(versionPrefix, '.')
155-
} else {
156-
isPatchVersion = true
157-
}
159+
// Check if versionPrefix has an expected-flavor constraint, if so first do an
160+
// exact match lookup and fallback to a minor version check
161+
if pos := strings.LastIndexByte(versionPrefix, '-'); pos != -1 {
162+
flavor = versionPrefix[pos+1:]
163+
versionPrefix = versionPrefix[:pos]
158164
}
159165

160166
// Check if versionPrefix is actually a patch version, if so first do an
161167
// exact match lookup and fallback to a minor version check
162-
if isPatchVersion {
168+
if pos := strings.LastIndexByte(versionPrefix, '.'); pos != strings.IndexByte(versionPrefix, '.') {
163169
// look for an exact match, the order does not matter here
164170
for _, v := range s.versions {
165-
if v.Version == versionPrefix {
171+
if v.Version == versionPrefix && v.SupportsFlavor(flavor) {
172+
v.ForceFlavor(flavor)
166173
return v, source, "", nil
167174
}
168175
}
169176

170177
// exact match not found, fallback to minor version check
171178
newVersionPrefix := versionPrefix[:pos]
172-
warning = fmt.Sprintf(`the current dir requires PHP %s (%s), but this version is not available: fallback to %s`, versionPrefix, source, newVersionPrefix)
179+
if versionPrefix[pos+1:] != "99" {
180+
warning = fmt.Sprintf(`the current dir requires PHP %s (%s), but this version is not available: fallback to %s`, versionPrefix, source, newVersionPrefix)
181+
}
173182
versionPrefix = newVersionPrefix
174183
}
175184

176185
// start from the end as versions are always sorted
177186
for i := len(s.versions) - 1; i >= 0; i-- {
178187
v := s.versions[i]
179-
if strings.HasPrefix(v.Version, versionPrefix) {
188+
if strings.HasPrefix(v.Version, versionPrefix) && v.SupportsFlavor(flavor) {
189+
v.ForceFlavor(flavor)
180190
return v, source, warning, nil
181191
}
182192
}

store_test.go

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,42 @@ package phpstore
22

33
import (
44
"path/filepath"
5+
"sort"
56
"testing"
67
)
78

89
func TestBestVersion(t *testing.T) {
9-
store := New("/dev/null", false, nil)
10+
store := newEmpty("/dev/null", nil)
1011
for _, v := range []string{"7.4.33", "8.0.27", "8.1.2", "8.1.14", "8.2.1"} {
11-
store.addVersion(&Version{
12-
Version: v,
13-
PHPPath: filepath.Join("/foo", v, "bin", "php"),
14-
})
12+
ver := NewVersion(v)
13+
ver.PHPPath = filepath.Join("/foo", v, "bin", "php")
14+
store.addVersion(ver)
1515

1616
if !store.IsVersionAvailable(v) {
1717
t.Errorf("Version %s should be shown as available", v)
1818
}
1919
}
2020

21+
{
22+
v := "8.0.26"
23+
ver := NewVersion(v)
24+
ver.PHPPath = filepath.Join("/foo", v, "bin", "php")
25+
ver.FPMPath = filepath.Join("/foo", v, "bin", "php-fpm")
26+
store.addVersion(ver)
27+
28+
if !store.IsVersionAvailable(v) {
29+
t.Errorf("Version %s should be shown as available", v)
30+
}
31+
}
32+
33+
sort.Sort(store.versions)
34+
2135
{
2236
bestVersion, _, _, _ := store.bestVersion("8", "testing")
2337
if bestVersion == nil {
2438
t.Error("8 requirement should find a best version")
2539
} else if bestVersion.Version != "8.2.1" {
26-
t.Error("8 requirement should find 8.2.1 as best version")
40+
t.Errorf("8 requirement should find 8.2.1 as best version, got %s", bestVersion.Version)
2741
}
2842
}
2943

@@ -32,7 +46,7 @@ func TestBestVersion(t *testing.T) {
3246
if bestVersion == nil {
3347
t.Error("8.1 requirement should find a best version")
3448
} else if bestVersion.Version != "8.1.14" {
35-
t.Error("8.1 requirement should find 8.1.14 as best version")
49+
t.Errorf("8.1 requirement should find 8.1.14 as best version, got %s", bestVersion.Version)
3650
}
3751
}
3852

@@ -41,7 +55,7 @@ func TestBestVersion(t *testing.T) {
4155
if bestVersion == nil {
4256
t.Error("8.0.10 requirement should find a best version")
4357
} else if bestVersion.Version != "8.0.27" {
44-
t.Error("8.0.10 requirement should find 8.0.27 as best version")
58+
t.Errorf("8.0.10 requirement should find 8.0.27 as best version, got %s", bestVersion.Version)
4559
} else if warning == "" {
4660
t.Error("8.0.10 requirement should trigger a warning")
4761
}
@@ -52,9 +66,22 @@ func TestBestVersion(t *testing.T) {
5266
if bestVersion == nil {
5367
t.Error("8.0.99 requirement should find a best version")
5468
} else if bestVersion.Version != "8.0.27" {
55-
t.Error("8.0.99 requirement should find 8.0.27 as best version")
69+
t.Errorf("8.0.99 requirement should find 8.0.27 as best version, got %s", bestVersion.Version)
5670
} else if warning != "" {
5771
t.Error("8.0.99 requirement should not trigger a warning")
5872
}
5973
}
74+
75+
{
76+
bestVersion, _, warning, _ := store.bestVersion("8.0-fpm", "testing")
77+
if bestVersion == nil {
78+
t.Error("8.0-fpm requirement should find a best version")
79+
} else if bestVersion.Version != "8.0.26" {
80+
t.Errorf("8.0-fpm requirement should find 8.0.26 as best version, got %s", bestVersion.Version)
81+
} else if bestVersion.serverType() != fpmServer {
82+
t.Error("8.0-fpm requirement should find an FPM expectedFlavors")
83+
} else if warning != "" {
84+
t.Error("8.0-fpm requirement should not trigger a warning")
85+
}
86+
}
6087
}

version.go

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,20 @@ import (
3030
type serverType int
3131

3232
const (
33-
fpmServer serverType = iota
34-
cgiServer
33+
noServerType serverType = iota
3534
cliServer
35+
cgiServer
36+
fpmServer
3637
frankenphpServer
3738
)
3839

40+
const (
41+
FlavorCLI string = "cli"
42+
FlavorCGI string = "cgi"
43+
FlavorFPM string = "fpm"
44+
FlavorFrankenPHP string = "frankenphp"
45+
)
46+
3947
// Version stores information about an installed PHP version
4048
type Version struct {
4149
FullVersion *version.Version `json:"-"`
@@ -49,6 +57,15 @@ type Version struct {
4957
PHPdbgPath string `json:"phpdbg_path"`
5058
IsSystem bool `json:"is_system"`
5159
FrankenPHP bool `json:"frankenphp"`
60+
61+
typeOverride serverType
62+
}
63+
64+
func NewVersion(v string) *Version {
65+
return &Version{
66+
Version: v,
67+
FullVersion: version.Must(version.NewVersion(v)),
68+
}
5269
}
5370

5471
type versions []*Version
@@ -106,9 +123,14 @@ func (v *Version) IsFrankenPHPServer() bool {
106123
}
107124

108125
func (v *Version) serverType() serverType {
126+
// FrankenPHP is a special case as it will not support several server types
127+
// for a single installation.
109128
if v.FrankenPHP {
110129
return frankenphpServer
111130
}
131+
if v.typeOverride != noServerType {
132+
return v.typeOverride
133+
}
112134
if v.FPMPath != "" {
113135
return fpmServer
114136
}
@@ -119,6 +141,46 @@ func (v *Version) serverType() serverType {
119141
return cliServer
120142
}
121143

144+
func (v *Version) ForceFlavor(flavor string) {
145+
if flavor == "" {
146+
return
147+
}
148+
149+
switch flavor {
150+
case FlavorCLI:
151+
v.typeOverride = cliServer
152+
case FlavorCGI:
153+
v.typeOverride = cgiServer
154+
case FlavorFPM:
155+
v.typeOverride = fpmServer
156+
}
157+
}
158+
159+
func (v *Version) SupportsFlavor(flavor string) bool {
160+
if flavor == "" {
161+
return true
162+
}
163+
164+
serverFlavor := v.serverType()
165+
if serverFlavor == frankenphpServer {
166+
return flavor == FlavorFrankenPHP
167+
}
168+
169+
// CLI flavor is always supported
170+
if flavor == FlavorCLI {
171+
return true
172+
}
173+
174+
switch serverFlavor {
175+
case cgiServer:
176+
return flavor == FlavorCGI
177+
case fpmServer:
178+
return flavor == FlavorFPM
179+
}
180+
181+
return false
182+
}
183+
122184
func (v *Version) setServer(fpm, cgi, phpconfig, phpize, phpdbg string) string {
123185
msg := fmt.Sprintf(" Found PHP: %s", v.PHPPath)
124186
fpm = filepath.Clean(fpm)

version_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright (c) 2021-present Fabien Potencier <fabien@symfony.com>
3+
*
4+
* This file is part of Symfony CLI project
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as
8+
* published by the Free Software Foundation, either version 3 of the
9+
* License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
package phpstore
21+
22+
import (
23+
"testing"
24+
)
25+
26+
func TestVersion_SupportsFlavor(t *testing.T) {
27+
testCases := []struct {
28+
version *Version
29+
expectedFlavors []string
30+
}{
31+
{
32+
version: func() *Version {
33+
v := NewVersion("8.1")
34+
v.FPMPath = "/usr/bin/php-fpm8.1"
35+
v.PHPPath = "/usr/bin/php-8.1"
36+
return v
37+
}(),
38+
expectedFlavors: []string{FlavorFPM, FlavorCLI},
39+
},
40+
{
41+
version: func() *Version {
42+
v := NewVersion("8.2")
43+
v.CGIPath = "/usr/bin/php-cgi8.1"
44+
v.PHPPath = "/usr/bin/php-8.1"
45+
return v
46+
}(),
47+
expectedFlavors: []string{FlavorCGI, FlavorCLI},
48+
},
49+
{
50+
version: func() *Version {
51+
v := NewVersion("8.3")
52+
v.PHPPath = "/usr/bin/php-8.3"
53+
return v
54+
}(),
55+
expectedFlavors: []string{FlavorCLI},
56+
},
57+
{
58+
version: func() *Version {
59+
v := NewVersion("8.4")
60+
v.PHPPath = "/usr/bin/frankenphp"
61+
v.FrankenPHP = true
62+
return v
63+
}(),
64+
expectedFlavors: []string{FlavorFrankenPHP},
65+
},
66+
}
67+
for _, testCase := range testCases {
68+
if !testCase.version.SupportsFlavor("") {
69+
t.Error("version.SupportsFlavor('') should return true, got false")
70+
}
71+
for _, flavor := range testCase.expectedFlavors {
72+
if !testCase.version.SupportsFlavor(flavor) {
73+
t.Errorf("version.SupportsFlavor(%v) should return true, got false", flavor)
74+
}
75+
}
76+
flavorLoop:
77+
for _, possibleFlavor := range []string{FlavorCLI, FlavorCGI, FlavorFPM, FlavorFrankenPHP} {
78+
for _, flavor := range testCase.expectedFlavors {
79+
if flavor == possibleFlavor {
80+
continue flavorLoop
81+
}
82+
}
83+
84+
if testCase.version.SupportsFlavor(possibleFlavor) {
85+
t.Errorf("version.SupportsFlavor(%v) should return false, got true", possibleFlavor)
86+
}
87+
}
88+
}
89+
}

0 commit comments

Comments
 (0)