github.com/symfony-cli/symfony-cli@v0.0.0-20240514161054-ece2df437dfa/local/php/composer.go (about) 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 "bufio" 24 "bytes" 25 "crypto/sha512" 26 "encoding/hex" 27 "encoding/json" 28 "fmt" 29 "io" 30 "net/http" 31 "os" 32 "path/filepath" 33 "strings" 34 35 "github.com/pkg/errors" 36 "github.com/rs/zerolog" 37 "github.com/symfony-cli/symfony-cli/util" 38 ) 39 40 const DefaultComposerVersion = 2 41 42 type ComposerResult struct { 43 code int 44 error error 45 } 46 47 func (c ComposerResult) Error() string { 48 if c.error != nil { 49 return c.error.Error() 50 } 51 52 return "" 53 } 54 55 func (c ComposerResult) ExitCode() int { 56 return c.code 57 } 58 59 func Composer(dir string, args, env []string, stdout, stderr, logger io.Writer, debugLogger zerolog.Logger) ComposerResult { 60 if os.Getenv("COMPOSER_MEMORY_LIMIT") == "" { 61 env = append(env, "COMPOSER_MEMORY_LIMIT=-1") 62 } 63 e := &Executor{ 64 Dir: dir, 65 BinName: "php", 66 Stdout: stdout, 67 Stderr: stderr, 68 SkipNbArgs: -1, 69 ExtraEnv: env, 70 Logger: debugLogger, 71 } 72 composerBin := "composer1" 73 if composerVersion() == 2 { 74 composerBin = "composer2" 75 } 76 path, err := e.findComposer(composerBin) 77 if err != nil || !isPHPScript(path) { 78 fmt.Fprintln(logger, " WARNING: Unable to find Composer, downloading one. It is recommended to install Composer yourself at https://getcomposer.org/download/") 79 // we don't store it under bin/ to avoid it being found by findComposer as we want to only use it as a fallback 80 binDir := filepath.Join(util.GetHomeDir(), "composer") 81 if path, err = downloadComposer(binDir); err != nil { 82 return ComposerResult{ 83 code: 1, 84 error: errors.Wrap(err, "unable to find composer, get it at https://getcomposer.org/download/"), 85 } 86 } 87 } 88 89 e.Args = append([]string{"php", path}, args...) 90 fmt.Fprintf(logger, " (running %s %s)\n\n", path, strings.TrimSpace(strings.Join(args, " "))) 91 ret := e.Execute(false) 92 if ret != 0 { 93 return ComposerResult{ 94 code: ret, 95 error: errors.Errorf("unable to run %s %s", path, strings.Join(args, " ")), 96 } 97 } 98 return ComposerResult{} 99 } 100 101 // isPHPScript checks that the composer file is indeed a phar/PHP script (not a .bat file) 102 func isPHPScript(path string) bool { 103 file, err := os.Open(path) 104 if err != nil { 105 return false 106 } 107 defer file.Close() 108 reader := bufio.NewReader(file) 109 byteSlice, _, err := reader.ReadLine() 110 if err != nil { 111 return false 112 } 113 114 return bytes.HasPrefix(byteSlice, []byte("#!/")) && bytes.HasSuffix(byteSlice, []byte("php")) 115 } 116 117 func composerVersion() int { 118 var lock struct { 119 Version string `json:"plugin-api-version"` 120 } 121 cwd, err := os.Getwd() 122 if err != nil { 123 return DefaultComposerVersion 124 } 125 contents, err := os.ReadFile(filepath.Join(cwd, "composer.lock")) 126 if err != nil { 127 return DefaultComposerVersion 128 } 129 if err = json.Unmarshal(contents, &lock); err != nil { 130 return DefaultComposerVersion 131 } 132 if strings.HasPrefix(lock.Version, "1.") { 133 return 1 134 } 135 return DefaultComposerVersion 136 } 137 138 func findComposer(extraBin string) (string, error) { 139 // Special support for OS specific things. They need to run before the 140 // PATH detection because most of them adds shell wrappers that we 141 // can't run via PHP. 142 if pharPath := findComposerSystemSpecific(extraBin); pharPath != "" { 143 return pharPath, nil 144 } 145 for _, file := range []string{extraBin, "composer", "composer.phar"} { 146 if pharPath, _ := LookPath(file); pharPath != "" { 147 // On Windows, we don't want the .bat, but the real composer phar/PHP file 148 if strings.HasSuffix(pharPath, ".bat") { 149 pharPath = pharPath[:len(pharPath)-4] + ".phar" 150 } 151 return pharPath, nil 152 } 153 } 154 155 return "", os.ErrNotExist 156 } 157 158 func downloadComposer(dir string) (string, error) { 159 if err := os.MkdirAll(dir, 0755); err != nil { 160 return "", err 161 } 162 path := filepath.Join(dir, "composer.phar") 163 if _, err := os.Stat(path); err == nil { 164 return path, nil 165 } 166 167 sig, err := downloadComposerInstallerSignature() 168 if err != nil { 169 return "", err 170 } 171 installer, err := downloadComposerInstaller() 172 if err != nil { 173 return "", err 174 } 175 h := sha512.New384() 176 h.Write(installer) 177 sigh := h.Sum(nil) 178 sigd := make([]byte, hex.EncodedLen(len(sigh))) 179 hex.Encode(sigd, sigh) 180 if !bytes.Equal(sigd, sig) { 181 return "", errors.New("signature was wrong when downloading Composer; please try again") 182 } 183 setupPath := filepath.Join(dir, "composer-setup.php") 184 os.WriteFile(setupPath, installer, 0666) 185 186 var stdout bytes.Buffer 187 e := &Executor{ 188 Dir: dir, 189 BinName: "php", 190 Args: []string{"php", setupPath, "--quiet"}, 191 SkipNbArgs: 1, 192 Stdout: &stdout, 193 Stderr: &stdout, 194 } 195 ret := e.Execute(false) 196 if ret == 1 { 197 return "", errors.New("unable to setup Composer") 198 } 199 if err := os.Chmod(path, 0755); err != nil { 200 return "", err 201 } 202 if err := os.Remove(filepath.Join(dir, "composer-setup.php")); err != nil { 203 return "", err 204 } 205 206 return path, nil 207 } 208 209 func downloadComposerInstaller() ([]byte, error) { 210 resp, err := http.Get("https://getcomposer.org/installer") 211 if err != nil { 212 return nil, err 213 } 214 defer resp.Body.Close() 215 return io.ReadAll(resp.Body) 216 } 217 218 func downloadComposerInstallerSignature() ([]byte, error) { 219 resp, err := http.Get("https://composer.github.io/installer.sig") 220 if err != nil { 221 return nil, err 222 } 223 defer resp.Body.Close() 224 return io.ReadAll(resp.Body) 225 }