Skip to content

Commit db37b31

Browse files
Add support for pie
1 parent 6925d21 commit db37b31

File tree

8 files changed

+224
-37
lines changed

8 files changed

+224
-37
lines changed

commands/root.go

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ func CommonCommands() []*console.Command {
5252
localCommands := []*console.Command{
5353
binConsoleWrapper,
5454
composerWrapper,
55+
pieWrapper,
5556
phpWrapper,
5657
bookCheckReqsCmd,
5758
bookCheckoutCmd,
@@ -143,6 +144,7 @@ func WelcomeAction(c *console.Context) error {
143144
localServerStopCmd,
144145
localSecurityCheckCmd,
145146
composerWrapper,
147+
pieWrapper,
146148
binConsoleWrapper,
147149
phpWrapper,
148150
})

commands/wrappers.go

+10
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ var (
3737
return console.IncorrectUsageError{ParentError: errors.New(`This command can only be run as "symfony composer"`)}
3838
},
3939
}
40+
pieWrapper = &console.Command{
41+
Usage: "Runs PIE",
42+
Hidden: console.Hide,
43+
// we use an alias to avoid the command being shown in the help but
44+
// still be available for completion
45+
Aliases: []*console.Alias{{Name: "pie"}},
46+
Action: func(c *console.Context) error {
47+
return console.IncorrectUsageError{ParentError: errors.New(`This command can only be run as "symfony pie"`)}
48+
},
49+
}
4050
binConsoleWrapper = &console.Command{
4151
Usage: "Runs the Symfony Console (bin/console) for current project",
4252
Hidden: console.Hide,

local/php/assert.go

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package php
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"os"
7+
)
8+
9+
// assertIsPHPScript checks that the composer file is indeed a phar/PHP script (not a .bat file)
10+
func assertIsPHPScript(path string) bool {
11+
if path == "" {
12+
return false
13+
}
14+
file, err := os.Open(path)
15+
if err != nil {
16+
return false
17+
}
18+
defer file.Close()
19+
reader := bufio.NewReader(file)
20+
byteSlice, _, err := reader.ReadLine()
21+
if err != nil {
22+
return false
23+
}
24+
25+
if bytes.Equal(byteSlice, []byte("<?php")) {
26+
return true
27+
}
28+
29+
return bytes.HasPrefix(byteSlice, []byte("#!/")) && bytes.HasSuffix(byteSlice, []byte("php"))
30+
}

local/php/composer_test.go renamed to local/php/assert_test.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,24 @@ import (
2525
. "gopkg.in/check.v1"
2626
)
2727

28-
type ComposerSuite struct{}
28+
type AssertSuite struct{}
2929

30-
var _ = Suite(&ComposerSuite{})
30+
var _ = Suite(&AssertSuite{})
3131

32-
func (s *ComposerSuite) TestIsComposerPHPScript(c *C) {
32+
func (s *AssertSuite) TestAssertIsPHPScript(c *C) {
3333
dir, err := filepath.Abs("testdata/php_scripts")
3434
c.Assert(err, IsNil)
3535

36-
c.Assert(isPHPScript(""), Equals, false)
37-
c.Assert(isPHPScript(filepath.Join(dir, "unknown")), Equals, false)
38-
c.Assert(isPHPScript(filepath.Join(dir, "invalid")), Equals, false)
36+
c.Assert(assertIsPHPScript(""), Equals, false)
37+
c.Assert(assertIsPHPScript(filepath.Join(dir, "unknown")), Equals, false)
38+
c.Assert(assertIsPHPScript(filepath.Join(dir, "invalid")), Equals, false)
3939

4040
for _, validScripts := range []string{
4141
"usual-one",
4242
"debian-style",
4343
"custom-one",
4444
"plain-one.php",
4545
} {
46-
c.Assert(isPHPScript(filepath.Join(dir, validScripts)), Equals, true)
46+
c.Assert(assertIsPHPScript(filepath.Join(dir, validScripts)), Equals, true)
4747
}
4848
}

local/php/composer.go

+1-25
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
package php
2121

2222
import (
23-
"bufio"
2423
"bytes"
2524
"crypto/sha512"
2625
"encoding/hex"
@@ -77,7 +76,7 @@ func Composer(dir string, args, env []string, stdout, stderr, logger io.Writer,
7776
if composerPath := os.Getenv("SYMFONY_COMPOSER_PATH"); composerPath != "" {
7877
debugLogger.Debug().Str("SYMFONY_COMPOSER_PATH", composerPath).Msg("SYMFONY_COMPOSER_PATH has been defined. User is taking control over Composer detection and execution.")
7978
e.Args = append([]string{composerPath}, args...)
80-
} else if path, err := e.findComposer(composerBin); err == nil && isPHPScript(path) {
79+
} else if path, err := e.findComposer(composerBin); err == nil && assertIsPHPScript(path) {
8180
e.Args = append([]string{"php", path}, args...)
8281
} else {
8382
reason := "No Composer installation found."
@@ -108,29 +107,6 @@ func Composer(dir string, args, env []string, stdout, stderr, logger io.Writer,
108107
return ComposerResult{}
109108
}
110109

111-
// isPHPScript checks that the composer file is indeed a phar/PHP script (not a .bat file)
112-
func isPHPScript(path string) bool {
113-
if path == "" {
114-
return false
115-
}
116-
file, err := os.Open(path)
117-
if err != nil {
118-
return false
119-
}
120-
defer file.Close()
121-
reader := bufio.NewReader(file)
122-
byteSlice, _, err := reader.ReadLine()
123-
if err != nil {
124-
return false
125-
}
126-
127-
if bytes.Equal(byteSlice, []byte("<?php")) {
128-
return true
129-
}
130-
131-
return bytes.HasPrefix(byteSlice, []byte("#!/")) && bytes.HasSuffix(byteSlice, []byte("php"))
132-
}
133-
134110
func composerVersion() int {
135111
var lock struct {
136112
Version string `json:"plugin-api-version"`

local/php/executor.go

+20
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,26 @@ func (e *Executor) findComposer(extraBin string) (string, error) {
417417
return findComposer(extraBin, e.Logger)
418418
}
419419

420+
// findPie locates the PIE binary depending on the configuration
421+
func (e *Executor) findPie() (string, error) {
422+
if scriptDir, err := e.DetectScriptDir(); err == nil {
423+
for _, file := range []string{"pie.phar", "pie"} {
424+
path := filepath.Join(scriptDir, file)
425+
e.Logger.Debug().Str("source", "PIE").Msgf(`Looking for PIE under "%s"`, path)
426+
d, err := os.Stat(path)
427+
if err != nil {
428+
continue
429+
}
430+
if m := d.Mode(); !m.IsDir() {
431+
e.Logger.Debug().Str("source", "PIE").Msgf(`Found potential PIE as "%s"`, path)
432+
return path, nil
433+
}
434+
}
435+
}
436+
437+
return findPie(e.Logger)
438+
}
439+
420440
// Execute executes the right version of PHP depending on the configuration
421441
func (e *Executor) Execute(loadDotEnv bool) int {
422442
if err := e.Config(loadDotEnv); err != nil {

local/php/pie.go

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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 php
21+
22+
import (
23+
"fmt"
24+
"github.com/pkg/errors"
25+
"github.com/rs/zerolog"
26+
"github.com/symfony-cli/symfony-cli/util"
27+
"io"
28+
"net/http"
29+
"os"
30+
"path/filepath"
31+
"strings"
32+
)
33+
34+
type PieResult struct {
35+
code int
36+
error error
37+
}
38+
39+
func (p PieResult) Error() string {
40+
if p.error != nil {
41+
return p.error.Error()
42+
}
43+
44+
return ""
45+
}
46+
47+
func (p PieResult) ExitCode() int {
48+
return p.code
49+
}
50+
51+
func Pie(dir string, args, env []string, stdout, stderr, logger io.Writer, debugLogger zerolog.Logger) PieResult {
52+
e := &Executor{
53+
Dir: dir,
54+
BinName: "php",
55+
Stdout: stdout,
56+
Stderr: stderr,
57+
SkipNbArgs: -1,
58+
ExtraEnv: env,
59+
Logger: debugLogger,
60+
}
61+
62+
if piePath := os.Getenv("SYMFONY_PIE_PATH"); piePath != "" {
63+
debugLogger.Debug().Str("SYMFONY_PIE_PATH", piePath).Msg("SYMFONY_PIE_PATH has been defined. User is taking control over PIE detection and execution.")
64+
e.Args = append([]string{piePath}, args...)
65+
} else if path, err := e.findPie(); err == nil && assertIsPHPScript(path) {
66+
e.Args = append([]string{"php", path}, args...)
67+
} else {
68+
reason := "No PIE installation found."
69+
if path != "" {
70+
reason = fmt.Sprintf("Detected PIE file (%s) is not a valid PHAR or PHP script.", path)
71+
}
72+
fmt.Fprintln(logger, " WARNING:", reason)
73+
fmt.Fprintln(logger, " Downloading PIE for you, but it is recommended to install PIE yourself, instructions available at https://github.com/php/pie")
74+
// we don't store it under bin/ to avoid it being found by findPie as we want to only use it as a fallback
75+
binDir := filepath.Join(util.GetHomeDir(), "pie")
76+
if path, err = downloadPie(binDir); err != nil {
77+
return PieResult{
78+
code: 1,
79+
error: errors.Wrap(err, "unable to find pie, get it at https://github.com/php/pie"),
80+
}
81+
}
82+
e.Args = append([]string{"php", path}, args...)
83+
fmt.Fprintf(logger, " (running %s)\n\n", e.CommandLine())
84+
}
85+
86+
ret := e.Execute(false)
87+
if ret != 0 {
88+
return PieResult{
89+
code: ret,
90+
error: errors.Errorf("unable to run %s", e.CommandLine()),
91+
}
92+
}
93+
return PieResult{}
94+
}
95+
96+
func findPie(logger zerolog.Logger) (string, error) {
97+
for _, file := range []string{"pie", "pie.phar"} {
98+
logger.Debug().Str("source", "PIE").Msgf(`Looking for PIE in the PATH as "%s"`, file)
99+
if pharPath, _ := LookPath(file); pharPath != "" {
100+
// On Windows, we don't want the .bat, but the real pie phar/PHP file
101+
if strings.HasSuffix(pharPath, ".bat") {
102+
pharPath = pharPath[:len(pharPath)-4] + ".phar"
103+
}
104+
logger.Debug().Str("source", "PIE").Msgf(`Found potential PIE as "%s"`, pharPath)
105+
return pharPath, nil
106+
}
107+
}
108+
109+
return "", os.ErrNotExist
110+
}
111+
112+
func downloadPie(dir string) (string, error) {
113+
if err := os.MkdirAll(dir, 0755); err != nil {
114+
return "", err
115+
}
116+
path := filepath.Join(dir, "pie.phar")
117+
if _, err := os.Stat(path); err == nil {
118+
return path, nil
119+
}
120+
121+
piePhar, err := downloadPiePhar()
122+
if err != nil {
123+
return "", err
124+
}
125+
126+
err = os.WriteFile(path, piePhar, 0755)
127+
if err != nil {
128+
return "", err
129+
}
130+
131+
return path, nil
132+
}
133+
134+
func downloadPiePhar() ([]byte, error) {
135+
resp, err := http.Get("https://github.com/php/pie/releases/latest/download/pie.phar")
136+
if err != nil {
137+
return nil, err
138+
}
139+
defer resp.Body.Close()
140+
return io.ReadAll(resp.Body)
141+
}

main.go

+13-5
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,19 @@ func main() {
7979
os.Exit(executor.Execute(false))
8080
}
8181
}
82-
// called via "symfony composer"?
83-
if len(args) >= 2 && args[1] == "composer" {
84-
res := php.Composer("", args[2:], getCliExtraEnv(), os.Stdout, os.Stderr, os.Stderr, terminal.Logger)
85-
terminal.Eprintln(res.Error())
86-
os.Exit(res.ExitCode())
82+
// called via "symfony composer" or "symfony pie"?
83+
if len(args) >= 2 {
84+
if args[1] == "composer" {
85+
res := php.Composer("", args[2:], getCliExtraEnv(), os.Stdout, os.Stderr, os.Stderr, terminal.Logger)
86+
terminal.Eprintln(res.Error())
87+
os.Exit(res.ExitCode())
88+
}
89+
90+
if args[1] == "pie" {
91+
res := php.Pie("", args[2:], getCliExtraEnv(), os.Stdout, os.Stderr, os.Stderr, terminal.Logger)
92+
terminal.Eprintln(res.Error())
93+
os.Exit(res.ExitCode())
94+
}
8795
}
8896

8997
for _, env := range []string{"BRANCH", "ENV", "APPLICATION_NAME"} {

0 commit comments

Comments
 (0)