Skip to content

Commit 69c3eba

Browse files
committed
Extract code from Symfony CLI
0 parents  commit 69c3eba

9 files changed

+1465
-0
lines changed

LICENSE

+661
Large diffs are not rendered by default.

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
PHP Store
2+
=========
3+
4+
PHP Store allows to find and manage local PHP installations.

discovery.go

+325
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
package phpstore
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"regexp"
11+
"runtime"
12+
"strings"
13+
14+
version "github.com/hashicorp/go-version"
15+
"github.com/pkg/errors"
16+
)
17+
18+
// discover tries to find all PHP versions on the current machine
19+
func (s *PHPStore) discover() {
20+
s.doDiscover()
21+
22+
// Under $PATH
23+
paths := s.pathDirectories(s.configDir)
24+
s.log("Looking for PHP in the PATH (%s)", paths)
25+
for _, path := range paths {
26+
for _, version := range s.findFromDir(path, nil, "PATH") {
27+
idx := s.addVersion(version)
28+
// the first one is the default/system PHP binary
29+
if s.pathVersion == nil {
30+
s.pathVersion = s.versions[idx]
31+
s.pathVersion.IsSystem = true
32+
s.log(" System PHP version (first in PATH)")
33+
}
34+
}
35+
}
36+
}
37+
38+
func (s *PHPStore) discoverFromDir(root string, phpRegexp *regexp.Regexp, pathRegexp *regexp.Regexp, why string) {
39+
maxDepth := 1
40+
if pathRegexp != nil {
41+
maxDepth += strings.Count(pathRegexp.String(), "/")
42+
}
43+
filepath.Walk(root, func(path string, finfo os.FileInfo, err error) error {
44+
if err != nil {
45+
// prevent panic by handling failure accessing a path
46+
return nil
47+
}
48+
// bypass current directory and non-directory
49+
if root == path || !finfo.IsDir() {
50+
return nil
51+
}
52+
rel, err := filepath.Rel(root, path)
53+
if err != nil {
54+
return errors.WithStack(err)
55+
}
56+
// only maxDepth+1 levels of depth
57+
if strings.Count(rel, string(os.PathSeparator)) > maxDepth {
58+
return filepath.SkipDir
59+
}
60+
s.log("Looking for PHP in %s (%+v) -- %s", path, pathRegexp, why)
61+
if pathRegexp == nil || pathRegexp.MatchString(rel) {
62+
s.addFromDir(path, phpRegexp, why)
63+
return filepath.SkipDir
64+
}
65+
return nil
66+
})
67+
}
68+
69+
func (s *PHPStore) addFromDir(dir string, phpRegexp *regexp.Regexp, why string) {
70+
for _, v := range s.findFromDir(dir, phpRegexp, why) {
71+
s.addVersion(v)
72+
}
73+
}
74+
75+
func (s *PHPStore) findFromDir(dir string, phpRegexp *regexp.Regexp, why string) []*Version {
76+
s.log("Looking for PHP in %s (%+v) -- %s", dir, phpRegexp, why)
77+
78+
root := dir
79+
if filepath.Base(dir) == "bin" {
80+
dir = filepath.Dir(dir)
81+
} else if runtime.GOOS != "windows" {
82+
root = filepath.Join(dir, "bin")
83+
}
84+
85+
if phpRegexp == nil {
86+
if v := s.discoverPHP(dir, "php"); v != nil {
87+
return []*Version{v}
88+
}
89+
return nil
90+
}
91+
92+
if _, err := os.Stat(root); err != nil {
93+
s.log(" Skipping %s as it does not exist", root)
94+
return nil
95+
}
96+
97+
var versions []*Version
98+
filepath.Walk(root, func(path string, finfo os.FileInfo, err error) error {
99+
if err != nil {
100+
// prevent panic by handling failure accessing a path
101+
return nil
102+
}
103+
if root != path && finfo.IsDir() {
104+
return filepath.SkipDir
105+
}
106+
if phpRegexp.MatchString(filepath.Base(path)) {
107+
if i := s.discoverPHP(dir, filepath.Base(path)); i != nil {
108+
versions = append(versions, i)
109+
}
110+
return nil
111+
}
112+
return nil
113+
})
114+
return versions
115+
}
116+
117+
func (s *PHPStore) discoverPHP(dir, binName string) *Version {
118+
// when php-config is not available/useable, fallback to discovering via php, slower but always work
119+
if runtime.GOOS == "windows" {
120+
// php-config does not exist on Windows
121+
return s.discoverPHPViaPHP(dir, binName)
122+
}
123+
124+
phpConfigPath := filepath.Join(dir, "bin", strings.Replace(binName, "php", "php-config", 1))
125+
fi, err := os.Lstat(phpConfigPath)
126+
if err != nil {
127+
return s.discoverPHPViaPHP(dir, binName)
128+
}
129+
130+
// on Linux, when using alternatives, php-config does not point to right PHP version, so, it cannot be used
131+
if fi.Mode()&os.ModeSymlink != 0 {
132+
if path, err := os.Readlink(phpConfigPath); err == nil && strings.Contains(path, "/alternatives/") {
133+
return s.discoverPHPViaPHP(dir, binName)
134+
}
135+
}
136+
137+
return s.discoverPHPViaPHPConfig(dir, binName)
138+
}
139+
140+
func (s *PHPStore) discoverPHPViaPHP(dir, binName string) *Version {
141+
php := filepath.Join(dir, "bin", binName)
142+
if runtime.GOOS == "windows" {
143+
binName += ".exe"
144+
php = filepath.Join(dir, binName)
145+
}
146+
147+
if _, err := os.Stat(php); err != nil {
148+
return nil
149+
}
150+
151+
var buf bytes.Buffer
152+
cmd := exec.Command(php, "--version")
153+
cmd.Stdout = &buf
154+
cmd.Stderr = &buf
155+
if err := cmd.Run(); err != nil {
156+
s.log(` Unable to run "%s --version: %s"`, php, err)
157+
return nil
158+
}
159+
r := regexp.MustCompile("PHP (\\d+\\.\\d+\\.\\d+)")
160+
data := r.FindSubmatch(buf.Bytes())
161+
if data == nil {
162+
s.log(" %s is not a PHP binary", php)
163+
return nil
164+
}
165+
php = filepath.Clean(php)
166+
var err error
167+
php, err = filepath.EvalSymlinks(php)
168+
if err != nil {
169+
s.log(" %s is not a valid symlink", php)
170+
return nil
171+
}
172+
v := s.validateVersion(dir, normalizeVersion(string(data[1])))
173+
if v == nil {
174+
return nil
175+
}
176+
version := &Version{
177+
Path: dir,
178+
Version: v.String(),
179+
FullVersion: v,
180+
PHPPath: php,
181+
}
182+
fpm := filepath.Join(dir, "sbin", strings.Replace(binName, "php", "php-fpm", 1))
183+
cgi := filepath.Join(dir, "bin", strings.Replace(binName, "php", "php-cgi", 1))
184+
phpconfig := filepath.Join(dir, "bin", strings.Replace(binName, "php", "php-config", 1))
185+
phpize := filepath.Join(dir, "bin", strings.Replace(binName, "php", "phpize", 1))
186+
phpdbg := filepath.Join(dir, "bin", strings.Replace(binName, "php", "phpdbg", 1))
187+
if runtime.GOOS == "windows" {
188+
fpm = filepath.Join(dir, strings.Replace(binName, "php", "php-fpm", 1))
189+
cgi = filepath.Join(dir, strings.Replace(binName, "php", "php-cgi", 1))
190+
phpconfig = filepath.Join(dir, strings.Replace(binName, "php", "php-config", 1))
191+
phpize = filepath.Join(dir, strings.Replace(binName, "php", "phpize", 1))
192+
phpdbg = filepath.Join(dir, strings.Replace(binName, "php", "phpdbg", 1))
193+
}
194+
s.log(version.setServer(fpm, cgi, phpconfig, phpize, phpdbg))
195+
return version
196+
}
197+
198+
func (s *PHPStore) discoverPHPViaPHPConfig(dir, binName string) *Version {
199+
phpConfig := filepath.Join(dir, "bin", strings.Replace(binName, "php", "php-config", 1))
200+
file, err := os.Open(phpConfig)
201+
if err != nil {
202+
s.log(" Unable to open %s: %s", phpConfig, err)
203+
return nil
204+
}
205+
version := &Version{
206+
Path: dir,
207+
}
208+
sc := bufio.NewScanner(file)
209+
programPrefix := ""
210+
programSuffix := ""
211+
programExtension := ""
212+
phpCgiBinary := ""
213+
allFound := 0
214+
for sc.Scan() {
215+
if strings.HasPrefix(sc.Text(), "vernum=") {
216+
v := s.validateVersion(dir, strings.Trim(sc.Text()[len("vernum="):], `"`))
217+
if v == nil {
218+
return nil
219+
}
220+
version.Version = v.String()
221+
version.FullVersion = v
222+
allFound++
223+
} else if strings.HasPrefix(sc.Text(), "program_prefix=") {
224+
programPrefix = strings.Trim(sc.Text()[len("program_prefix="):], `"`)
225+
allFound++
226+
} else if strings.HasPrefix(sc.Text(), "program_suffix=") {
227+
programSuffix = strings.Trim(sc.Text()[len("program_suffix="):], `"`)
228+
allFound++
229+
} else if strings.HasPrefix(sc.Text(), " php_cgi_binary=") {
230+
phpCgiBinary = strings.Trim(sc.Text()[len(" php_cgi_binary="):], `"`)
231+
allFound++
232+
} else if strings.HasPrefix(sc.Text(), "exe_extension=") {
233+
programExtension = strings.Trim(sc.Text()[len("exe_extension="):], `"`)
234+
allFound++
235+
}
236+
}
237+
if version.FullVersion == nil {
238+
s.log(" Unable to find version in %s", phpConfig)
239+
return nil
240+
}
241+
if allFound != 5 {
242+
s.log(" Unable to parse all information from %s", phpConfig)
243+
return nil
244+
}
245+
if phpCgiBinary == "" {
246+
phpCgiBinary = fmt.Sprintf("%sphp%s-cgi%s", programPrefix, programSuffix, programExtension)
247+
} else {
248+
phpCgiBinary = strings.Replace(phpCgiBinary, "${program_prefix}", programPrefix, 1)
249+
phpCgiBinary = strings.Replace(phpCgiBinary, "${program_suffix}", programSuffix, 1)
250+
phpCgiBinary = strings.Replace(phpCgiBinary, "${exe_extension}", programExtension, 1)
251+
phpCgiBinary = strings.Replace(phpCgiBinary, "${exec_prefix}/", "", 1)
252+
phpCgiBinary = strings.Replace(phpCgiBinary, "bin/", "", 1)
253+
}
254+
version.PHPPath = filepath.Join(version.Path, "bin", fmt.Sprintf("%sphp%s%s", programPrefix, programSuffix, programExtension))
255+
s.log(version.setServer(
256+
filepath.Join(version.Path, "sbin", fmt.Sprintf("%sphp-fpm%s%s", programPrefix, programSuffix, programExtension)),
257+
filepath.Join(version.Path, "bin", phpCgiBinary),
258+
filepath.Join(version.Path, "bin", fmt.Sprintf("%sphp-config%s%s", programPrefix, programSuffix, programExtension)),
259+
filepath.Join(version.Path, "bin", fmt.Sprintf("%sphpize%s%s", programPrefix, programSuffix, programExtension)),
260+
filepath.Join(version.Path, "bin", fmt.Sprintf("%sphpdbg%s%s", programPrefix, programSuffix, programExtension)),
261+
))
262+
return version
263+
}
264+
265+
func (s *PHPStore) validateVersion(path, v string) *version.Version {
266+
if len(v) != 5 {
267+
s.log(" Unable to parse version %s for PHP at %s: version is non-standard", v, path)
268+
return nil
269+
}
270+
version, err := version.NewVersion(fmt.Sprintf("%c.%s.%s", v[0], v[1:3], v[3:5]))
271+
if err != nil {
272+
s.log(" Unable to parse version %s for PHP at %s: %s", v, path, err)
273+
return nil
274+
}
275+
return version
276+
}
277+
278+
func normalizeVersion(v string) string {
279+
// version is XYYZZ
280+
parts := strings.Split(v, ".")
281+
version := parts[0]
282+
if len(parts[1]) == 1 {
283+
version += "0"
284+
}
285+
version += parts[1]
286+
if len(parts[2]) == 1 {
287+
version += "0"
288+
}
289+
return version + parts[2]
290+
}
291+
292+
func (s *PHPStore) pathDirectories(configDir string) []string {
293+
phpShimDir := filepath.Join(configDir, "bin")
294+
path := os.Getenv("PATH")
295+
if runtime.GOOS == "windows" {
296+
path = os.Getenv("Path")
297+
}
298+
user := os.Getenv("USERPROFILE")
299+
dirs := []string{}
300+
seen := make(map[string]bool)
301+
for _, dir := range filepath.SplitList(path) {
302+
dir = strings.Replace(dir, "%%USERPROFILE%%", user, 1)
303+
edir, err := filepath.EvalSymlinks(dir)
304+
if err != nil {
305+
continue
306+
}
307+
if edir == phpShimDir {
308+
continue
309+
}
310+
if edir == "" {
311+
continue
312+
}
313+
if _, ok := seen[edir]; ok {
314+
if dir != edir {
315+
s.log(" Skipping %s (alias of %s), already in the PATH", dir, edir)
316+
} else {
317+
s.log(" Skipping %s, already in the PATH", dir)
318+
}
319+
continue
320+
}
321+
dirs = append(dirs, edir)
322+
seen[edir] = true
323+
}
324+
return dirs
325+
}

discovery_others.go

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//go:build !windows
2+
// +build !windows
3+
4+
package phpstore
5+
6+
import (
7+
"os/exec"
8+
"path/filepath"
9+
"regexp"
10+
"runtime"
11+
"strings"
12+
13+
homedir "github.com/mitchellh/go-homedir"
14+
)
15+
16+
func (s *PHPStore) doDiscover() {
17+
// Defaults
18+
s.addFromDir("/usr", nil, "*nix")
19+
s.addFromDir("/usr/local", nil, "*nix")
20+
21+
homeDir, err := homedir.Dir()
22+
if err != nil {
23+
homeDir = ""
24+
s.log("Could not find home directory: %s", err)
25+
}
26+
27+
// phpbrew
28+
if homeDir != "" {
29+
s.discoverFromDir(filepath.Join(homeDir, ".phpbrew", "php"), nil, nil, "phpbrew")
30+
}
31+
32+
// phpenv
33+
if homeDir != "" {
34+
s.discoverFromDir(filepath.Join(homeDir, ".phpenv", "versions"), nil, regexp.MustCompile("^[\\d\\.]+(?:RC|BETA|snapshot)?$"), "phpenv")
35+
}
36+
37+
// XAMPP
38+
s.addFromDir("/opt/lampp", nil, "XAMPP")
39+
40+
if runtime.GOOS == "darwin" {
41+
// homebrew
42+
if out, err := exec.Command("brew", "--cellar").Output(); err == nil {
43+
prefix := strings.Trim(string(out), "\n")
44+
// pattern example: [email protected]/5.6.33_9
45+
s.discoverFromDir(prefix, nil, regexp.MustCompile("^php@(?:[\\d\\.]+)/(?:[\\d\\._]+)$"), "homebrew")
46+
// pattern example: php/7.2.11
47+
s.discoverFromDir(prefix, nil, regexp.MustCompile("^php/(?:[\\d\\._]+)$"), "homebrew")
48+
}
49+
50+
// Liip PHP https://php-osx.liip.ch/ (pattern example: php5-7.2.0RC1-20170907-205032/bin/php)
51+
s.discoverFromDir("/usr/local", nil, regexp.MustCompile("^php5\\-[\\d\\.]+(?:RC|BETA)?\\d*\\-\\d+\\-\\d+$"), "Liip PHP")
52+
53+
// MAMP
54+
s.discoverFromDir("/Applications/MAMP/bin/php/", nil, regexp.MustCompile("^php[\\d\\.]+(?:RC|BETA)?$"), "MAMP")
55+
56+
// MacPorts (/opt/local/sbin/php-fpm71, /opt/local/bin/php71)
57+
s.discoverFromDir("/opt/local", regexp.MustCompile("^php(?:[\\d\\.]+)$"), nil, "MacPorts")
58+
}
59+
60+
if runtime.GOOS == "linux" {
61+
// Ondrej PPA on Linux (bin/php7.2)
62+
s.discoverFromDir("/usr", regexp.MustCompile("^php(?:[\\d\\.]+)$"), nil, "Ondrej PPA")
63+
64+
// Remi's RPM repository
65+
s.discoverFromDir("/opt/remi", nil, regexp.MustCompile("^php(?:\\d+)/root/usr$"), "Remi's RPM")
66+
}
67+
}

0 commit comments

Comments
 (0)