From ebe6976c3352bc7d4947bc5ae393ad3bddde2987 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Thu, 20 Mar 2025 18:14:00 +0100 Subject: [PATCH] Add support for `pie` --- commands/root.go | 2 + commands/wrappers.go | 10 ++ local/php/composer.go | 24 --- local/php/executor.go | 20 +++ local/php/pie.go | 137 ++++++++++++++++++ local/php/utils.go | 30 ++++ local/php/{composer_test.go => utils_test.go} | 6 +- main.go | 18 ++- 8 files changed, 215 insertions(+), 32 deletions(-) create mode 100644 local/php/pie.go create mode 100644 local/php/utils.go rename local/php/{composer_test.go => utils_test.go} (91%) diff --git a/commands/root.go b/commands/root.go index 01cbaa44..afe25ffa 100644 --- a/commands/root.go +++ b/commands/root.go @@ -52,6 +52,7 @@ func CommonCommands() []*console.Command { localCommands := []*console.Command{ binConsoleWrapper, composerWrapper, + pieWrapper, phpWrapper, bookCheckReqsCmd, bookCheckoutCmd, @@ -179,6 +180,7 @@ Environment variables to use Platform.sh/Upsun relationships or Docker services {{with .Command "composer"}} {{.PreferredName}}{{"\t"}}{{.Usage}}{{end}} {{with .Command "console"}} {{.PreferredName}}{{"\t"}}{{.Usage}}{{end}} {{with .Command "php"}} {{.PreferredName}}{{"\t"}}{{.Usage}}{{end}} +{{with .Command "pie"}} {{.PreferredName}}{{"\t"}}{{.Usage}}{{end}} ` } diff --git a/commands/wrappers.go b/commands/wrappers.go index 896e50ba..311472ee 100644 --- a/commands/wrappers.go +++ b/commands/wrappers.go @@ -37,6 +37,16 @@ var ( return console.IncorrectUsageError{ParentError: errors.New(`This command can only be run as "symfony composer"`)} }, } + pieWrapper = &console.Command{ + Usage: "Runs PIE", + Hidden: console.Hide, + // we use an alias to avoid the command being shown in the help but + // still be available for completion + Aliases: []*console.Alias{{Name: "pie"}}, + Action: func(c *console.Context) error { + return console.IncorrectUsageError{ParentError: errors.New(`This command can only be run as "symfony pie"`)} + }, + } binConsoleWrapper = &console.Command{ Usage: "Runs the Symfony Console (bin/console) for current project", Hidden: console.Hide, diff --git a/local/php/composer.go b/local/php/composer.go index 828bd420..a3b5de6e 100644 --- a/local/php/composer.go +++ b/local/php/composer.go @@ -20,7 +20,6 @@ package php import ( - "bufio" "bytes" "crypto/sha512" "encoding/hex" @@ -108,29 +107,6 @@ func Composer(dir string, args, env []string, stdout, stderr, logger io.Writer, return ComposerResult{} } -// isPHPScript checks that the composer file is indeed a phar/PHP script (not a .bat file) -func isPHPScript(path string) bool { - if path == "" { - return false - } - file, err := os.Open(path) - if err != nil { - return false - } - defer file.Close() - reader := bufio.NewReader(file) - byteSlice, _, err := reader.ReadLine() - if err != nil { - return false - } - - if bytes.Equal(byteSlice, []byte(" + * + * This file is part of Symfony CLI project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package php + +import ( + "fmt" + "io" + "net/http" + "os" + "path/filepath" + + "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/symfony-cli/symfony-cli/util" +) + +type PieResult struct { + code int + error error +} + +func (p PieResult) Error() string { + if p.error != nil { + return p.error.Error() + } + + return "" +} + +func (p PieResult) ExitCode() int { + return p.code +} + +func Pie(dir string, args, env []string, stdout, stderr, logger io.Writer, debugLogger zerolog.Logger) PieResult { + e := &Executor{ + Dir: dir, + BinName: "php", + Stdout: stdout, + Stderr: stderr, + SkipNbArgs: -1, + ExtraEnv: env, + Logger: debugLogger, + } + + if piePath := os.Getenv("SYMFONY_PIE_PATH"); piePath != "" { + debugLogger.Debug().Str("SYMFONY_PIE_PATH", piePath).Msg("SYMFONY_PIE_PATH has been defined. User is taking control over PIE detection and execution.") + e.Args = append([]string{piePath}, args...) + } else if path, err := e.findPie(); err == nil && isPHPScript(path) { + e.Args = append([]string{"php", path}, args...) + } else { + reason := "No PIE installation found." + if path != "" { + reason = fmt.Sprintf("Detected PIE file (%s) is not a valid PHAR or PHP script.", path) + } + fmt.Fprintln(logger, " WARNING:", reason) + fmt.Fprintln(logger, " Downloading PIE for you, but it is recommended to install PIE yourself, instructions available at https://github.com/php/pie") + // we don't store it under bin/ to avoid it being found by findPie as we want to only use it as a fallback + binDir := filepath.Join(util.GetHomeDir(), "pie") + if path, err = downloadPie(binDir); err != nil { + return PieResult{ + code: 1, + error: errors.Wrap(err, "unable to find pie, get it at https://github.com/php/pie"), + } + } + e.Args = append([]string{"php", path}, args...) + fmt.Fprintf(logger, " (running %s)\n\n", e.CommandLine()) + } + + ret := e.Execute(false) + if ret != 0 { + return PieResult{ + code: ret, + error: errors.Errorf("unable to run %s", e.CommandLine()), + } + } + return PieResult{} +} + +func findPie(logger zerolog.Logger) (string, error) { + for _, file := range []string{"pie", "pie.phar"} { + logger.Debug().Str("source", "PIE").Msgf(`Looking for PIE in the PATH as "%s"`, file) + if pharPath, _ := LookPath(file); pharPath != "" { + logger.Debug().Str("source", "PIE").Msgf(`Found potential PIE as "%s"`, pharPath) + return pharPath, nil + } + } + + return "", os.ErrNotExist +} + +func downloadPie(dir string) (string, error) { + if err := os.MkdirAll(dir, 0755); err != nil { + return "", err + } + path := filepath.Join(dir, "pie.phar") + if _, err := os.Stat(path); err == nil { + return path, nil + } + + piePhar, err := downloadPiePhar() + if err != nil { + return "", err + } + + err = os.WriteFile(path, piePhar, 0755) + if err != nil { + return "", err + } + + return path, nil +} + +func downloadPiePhar() ([]byte, error) { + resp, err := http.Get("https://github.com/php/pie/releases/latest/download/pie.phar") + if err != nil { + return nil, err + } + defer resp.Body.Close() + return io.ReadAll(resp.Body) +} diff --git a/local/php/utils.go b/local/php/utils.go new file mode 100644 index 00000000..860b73de --- /dev/null +++ b/local/php/utils.go @@ -0,0 +1,30 @@ +package php + +import ( + "bufio" + "bytes" + "os" +) + +// isPHPScript checks that the provided file is indeed a phar/PHP script (not a .bat file) +func isPHPScript(path string) bool { + if path == "" { + return false + } + file, err := os.Open(path) + if err != nil { + return false + } + defer file.Close() + reader := bufio.NewReader(file) + byteSlice, _, err := reader.ReadLine() + if err != nil { + return false + } + + if bytes.Equal(byteSlice, []byte("= 2 && args[1] == "composer" { - res := php.Composer("", args[2:], getCliExtraEnv(), os.Stdout, os.Stderr, os.Stderr, terminal.Logger) - terminal.Eprintln(res.Error()) - os.Exit(res.ExitCode()) + // called via "symfony composer" or "symfony pie"? + if len(args) >= 2 { + if args[1] == "composer" { + res := php.Composer("", args[2:], getCliExtraEnv(), os.Stdout, os.Stderr, os.Stderr, terminal.Logger) + terminal.Eprintln(res.Error()) + os.Exit(res.ExitCode()) + } + + if args[1] == "pie" { + res := php.Pie("", args[2:], getCliExtraEnv(), os.Stdout, os.Stderr, os.Stderr, terminal.Logger) + terminal.Eprintln(res.Error()) + os.Exit(res.ExitCode()) + } } for _, env := range []string{"BRANCH", "ENV", "APPLICATION_NAME"} {