1// Copyright 2019 The ChromiumOS Authors 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package main 6 7import ( 8 "context" 9 "fmt" 10 "io" 11 "os" 12 "os/exec" 13 "path/filepath" 14 "strings" 15 "time" 16) 17 18type command struct { 19 Path string `json:"path"` 20 Args []string `json:"args"` 21 // Updates and additions have the form: 22 // `NAME=VALUE` 23 // Removals have the form: 24 // `NAME=`. 25 EnvUpdates []string `json:"env_updates,omitempty"` 26} 27 28func newProcessCommand() *command { 29 return &command{ 30 Path: os.Args[0], 31 Args: os.Args[1:], 32 } 33} 34 35func mergeEnvValues(values []string, updates []string) []string { 36 envMap := map[string]string{} 37 for _, entry := range values { 38 equalPos := strings.IndexRune(entry, '=') 39 envMap[entry[:equalPos]] = entry[equalPos+1:] 40 } 41 for _, update := range updates { 42 equalPos := strings.IndexRune(update, '=') 43 key := update[:equalPos] 44 value := update[equalPos+1:] 45 if value == "" { 46 delete(envMap, key) 47 } else { 48 envMap[key] = value 49 } 50 } 51 env := []string{} 52 for key, value := range envMap { 53 env = append(env, key+"="+value) 54 } 55 return env 56} 57 58func runCmd(env env, cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { 59 execCmd := exec.Command(cmd.Path, cmd.Args...) 60 execCmd.Env = mergeEnvValues(env.environ(), cmd.EnvUpdates) 61 execCmd.Dir = env.getwd() 62 execCmd.Stdin = stdin 63 execCmd.Stdout = stdout 64 execCmd.Stderr = stderr 65 return execCmd.Run() 66} 67 68func runCmdWithTimeout(env env, cmd *command, t time.Duration) error { 69 ctx, cancel := context.WithTimeout(context.Background(), t) 70 defer cancel() 71 cmdCtx := exec.CommandContext(ctx, cmd.Path, cmd.Args...) 72 cmdCtx.Env = mergeEnvValues(env.environ(), cmd.EnvUpdates) 73 cmdCtx.Dir = env.getwd() 74 cmdCtx.Stdin = env.stdin() 75 cmdCtx.Stdout = env.stdout() 76 cmdCtx.Stderr = env.stderr() 77 78 if err := cmdCtx.Start(); err != nil { 79 return fmt.Errorf("exec error: %w", err) 80 } 81 err := cmdCtx.Wait() 82 if ctx.Err() == nil { 83 return err 84 } 85 return ctx.Err() 86} 87 88func resolveAgainstPathEnv(env env, cmd string) (string, error) { 89 path, _ := env.getenv("PATH") 90 for _, path := range strings.Split(path, ":") { 91 resolvedPath := filepath.Join(path, cmd) 92 if _, err := os.Lstat(resolvedPath); err == nil { 93 return resolvedPath, nil 94 } 95 } 96 return "", fmt.Errorf("couldn't find cmd %q in path", cmd) 97} 98 99func getAbsCmdPath(env env, cmd *command) string { 100 path := cmd.Path 101 if !filepath.IsAbs(path) { 102 path = filepath.Join(env.getwd(), path) 103 } 104 return path 105} 106 107func newCommandBuilder(env env, cfg *config, cmd *command) (*commandBuilder, error) { 108 basename := filepath.Base(cmd.Path) 109 var nameParts []string 110 if basename == "clang-tidy" { 111 nameParts = []string{basename} 112 } else { 113 nameParts = strings.Split(basename, "-") 114 } 115 target := builderTarget{} 116 switch len(nameParts) { 117 case 1: 118 // E.g. gcc 119 target = builderTarget{ 120 compiler: nameParts[0], 121 } 122 case 4: 123 // E.g. armv7m-cros-eabi-gcc 124 target = builderTarget{ 125 arch: nameParts[0], 126 vendor: nameParts[1], 127 abi: nameParts[2], 128 compiler: nameParts[3], 129 target: basename[:strings.LastIndex(basename, "-")], 130 } 131 case 5: 132 // E.g. x86_64-cros-linux-gnu-gcc 133 target = builderTarget{ 134 arch: nameParts[0], 135 vendor: nameParts[1], 136 sys: nameParts[2], 137 abi: nameParts[3], 138 compiler: nameParts[4], 139 target: basename[:strings.LastIndex(basename, "-")], 140 } 141 default: 142 return nil, newErrorwithSourceLocf("unexpected compiler name pattern. Actual: %s", basename) 143 } 144 145 var compilerType compilerType 146 switch { 147 case strings.HasPrefix(target.compiler, "clang-tidy"): 148 compilerType = clangTidyType 149 case strings.HasPrefix(target.compiler, "clang"): 150 compilerType = clangType 151 default: 152 compilerType = gccType 153 } 154 target.compilerType = compilerType 155 absWrapperPath, err := getAbsWrapperPath(env, cmd) 156 if err != nil { 157 return nil, err 158 } 159 var rootPath string 160 if compilerType == gccType { 161 rootPath = filepath.Join(filepath.Dir(absWrapperPath), cfg.gccRootRelPath) 162 } else { 163 rootPath = filepath.Join(filepath.Dir(absWrapperPath), cfg.clangRootRelPath) 164 } 165 return &commandBuilder{ 166 path: cmd.Path, 167 args: createBuilderArgs( /*fromUser=*/ true, cmd.Args), 168 env: env, 169 cfg: cfg, 170 rootPath: rootPath, 171 absWrapperPath: absWrapperPath, 172 target: target, 173 }, nil 174} 175 176type commandBuilder struct { 177 path string 178 target builderTarget 179 args []builderArg 180 envUpdates []string 181 env env 182 cfg *config 183 rootPath string 184 absWrapperPath string 185} 186 187type builderArg struct { 188 value string 189 fromUser bool 190} 191 192type compilerType int32 193 194const ( 195 gccType compilerType = iota 196 clangType 197 clangTidyType 198) 199 200type builderTarget struct { 201 target string 202 arch string 203 vendor string 204 sys string 205 abi string 206 compiler string 207 compilerType compilerType 208} 209 210func createBuilderArgs(fromUser bool, args []string) []builderArg { 211 builderArgs := make([]builderArg, len(args)) 212 for i, arg := range args { 213 builderArgs[i] = builderArg{value: arg, fromUser: fromUser} 214 } 215 return builderArgs 216} 217 218func (builder *commandBuilder) clone() *commandBuilder { 219 return &commandBuilder{ 220 path: builder.path, 221 args: append([]builderArg{}, builder.args...), 222 env: builder.env, 223 cfg: builder.cfg, 224 rootPath: builder.rootPath, 225 target: builder.target, 226 absWrapperPath: builder.absWrapperPath, 227 } 228} 229 230func (builder *commandBuilder) wrapPath(path string, extraFlags ...string) { 231 newArgs := createBuilderArgs( /*fromUser=*/ false, extraFlags) 232 newArgs = append(newArgs, builderArg{value: builder.path, fromUser: false}) 233 builder.args = append(newArgs, builder.args...) 234 builder.path = path 235} 236 237func (builder *commandBuilder) addPreUserArgs(args ...string) { 238 index := 0 239 for _, arg := range builder.args { 240 if arg.fromUser { 241 break 242 } 243 index++ 244 } 245 builder.args = append(builder.args[:index], append(createBuilderArgs( /*fromUser=*/ false, args), builder.args[index:]...)...) 246} 247 248func (builder *commandBuilder) addPostUserArgs(args ...string) { 249 builder.args = append(builder.args, createBuilderArgs( /*fromUser=*/ false, args)...) 250} 251 252// Allows to map and filter arguments. Filters when the callback returns an empty string. 253func (builder *commandBuilder) transformArgs(transform func(arg builderArg) string) { 254 // See https://github.com/golang/go/wiki/SliceTricks 255 newArgs := builder.args[:0] 256 for _, arg := range builder.args { 257 newArg := transform(arg) 258 if newArg != "" { 259 newArgs = append(newArgs, builderArg{ 260 value: newArg, 261 fromUser: arg.fromUser, 262 }) 263 } 264 } 265 builder.args = newArgs 266} 267 268// Allows to filter arg pairs, useful for eg when having adjacent unsupported args 269// like "-Wl,-z -Wl,defs" 270func (builder *commandBuilder) filterArgPairs(keepPair func(arg1, arg2 builderArg) bool) { 271 newArgs := builder.args[:0] 272 for i := 0; i < len(builder.args); i++ { 273 if i == len(builder.args)-1 || keepPair(builder.args[i], builder.args[i+1]) { 274 newArgs = append(newArgs, builder.args[i]) 275 } else { 276 // skip builder.args[i]) as well as next item 277 i++ 278 } 279 } 280 builder.args = newArgs 281} 282 283func (builder *commandBuilder) updateEnv(updates ...string) { 284 builder.envUpdates = append(builder.envUpdates, updates...) 285} 286 287func (builder *commandBuilder) build() *command { 288 cmdArgs := make([]string, len(builder.args)) 289 for i, builderArg := range builder.args { 290 cmdArgs[i] = builderArg.value 291 } 292 return &command{ 293 Path: builder.path, 294 Args: cmdArgs, 295 EnvUpdates: builder.envUpdates, 296 } 297} 298