go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/build/main.go (about) 1 // Copyright 2020 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package build 16 17 import ( 18 "bytes" 19 "compress/zlib" 20 "context" 21 "encoding/json" 22 "flag" 23 "fmt" 24 "io" 25 "os" 26 "path/filepath" 27 "runtime/debug" 28 "time" 29 30 "golang.org/x/time/rate" 31 "google.golang.org/protobuf/proto" 32 33 bbpb "go.chromium.org/luci/buildbucket/proto" 34 "go.chromium.org/luci/common/errors" 35 "go.chromium.org/luci/common/logging" 36 "go.chromium.org/luci/common/logging/gologger" 37 "go.chromium.org/luci/common/system/environ" 38 "go.chromium.org/luci/logdog/client/butlerlib/bootstrap" 39 "go.chromium.org/luci/logdog/client/butlerlib/streamclient" 40 "go.chromium.org/luci/lucictx" 41 "go.chromium.org/luci/luciexe" 42 ) 43 44 var errNonSuccess = errors.New("build state != SUCCESS") 45 46 // Main implements all the 'command-line' behaviors of the luciexe 'exe' 47 // protocol, including: 48 // 49 // - parsing command line for "--output", "--help", etc. 50 // - parsing stdin (as appropriate) for the incoming Build message 51 // - creating and configuring a logdog client to send State evolutions. 52 // - Configuring a logdog client from the environment. 53 // - Writing Build state updates to the logdog "build.proto" stream. 54 // - Start'ing the build in this process. 55 // - End'ing the build with the returned error from your function 56 // 57 // If `inputMsg` is nil, the top-level properties will be ignored. 58 // 59 // If `writeFnptr` and `mergeFnptr` are nil, they're ignored. Otherwise 60 // they work as they would for MakePropertyModifier. 61 // 62 // CLI Arguments parsed: 63 // - -h / --help : Print help for this binary (including input/output 64 // property type info) 65 // - --strict-input : Enable strict property parsing (see OptStrictInputProperties) 66 // - --output : luciexe "output" flag; See 67 // https://pkg.go.dev/go.chromium.org/luci/luciexe#hdr-Recursive_Invocation 68 // - --working-dir : The working directory to run from; Default is $PWD unless 69 // LUCIEXE_FAKEBUILD is set, in which case a temp dir is used and cleaned up after. 70 // - -- : Any extra arguments after a "--" token are passed to your callback 71 // as-is. 72 // 73 // Example: 74 // 75 // func main() { 76 // input := *MyInputProps{} 77 // var writeOutputProps func(*MyOutputProps) 78 // var mergeOutputProps func(*MyOutputProps) 79 // 80 // Main(input, &writeOutputProps, &mergeOutputProps, func(ctx context.Context, args []string, st *build.State) error { 81 // // actual build code here, build is already Start'd 82 // // input was parsed from build.Input.Properties 83 // writeOutputProps(&MyOutputProps{...}) 84 // return nil // will mark the Build as SUCCESS 85 // }) 86 // } 87 // 88 // Main also supports running a luciexe build locally by specifying the 89 // LUCIEXE_FAKEBUILD environment variable. The value of the variable should 90 // be a path to a file containing a JSON-encoded Build proto. 91 // 92 // TODO(iannucci): LUCIEXE_FAKEBUILD does not support nested invocations. 93 // It should set up bbagent and butler in order to aggregate logs. 94 // 95 // NOTE: These types are pretty bad; There's significant opportunity to improve 96 // them with Go2 generics. 97 func Main(inputMsg proto.Message, writeFnptr, mergeFnptr any, cb func(context.Context, []string, *State) error) { 98 ctx := gologger.StdConfig.Use(context.Background()) 99 100 switch err := main(ctx, os.Args, os.Stdin, inputMsg, writeFnptr, mergeFnptr, cb); err { 101 case nil: 102 os.Exit(0) 103 104 case errNonSuccess: 105 os.Exit(1) 106 default: 107 errors.Log(ctx, err) 108 os.Exit(2) 109 } 110 } 111 112 func main(ctx context.Context, args []string, stdin io.Reader, inputMsg proto.Message, writeFnptr, mergeFnptr any, cb func(context.Context, []string, *State) error) (err error) { 113 args = append([]string(nil), args...) 114 var userArgs []string 115 for i, a := range args { 116 if a == "--" { 117 userArgs = args[i+1:] 118 args = args[:i] 119 break 120 } 121 } 122 123 opts := []StartOption{ 124 OptParseProperties(inputMsg), 125 } 126 if writeFnptr != nil || mergeFnptr != nil { 127 opts = append(opts, OptOutputProperties(writeFnptr, mergeFnptr)) 128 } 129 130 outputFile, wd, strict, help := parseArgs(args) 131 if strict { 132 opts = append(opts, OptStrictInputProperties()) 133 } 134 if wd != "" { 135 if err := os.Chdir(wd); err != nil { 136 return err 137 } 138 } 139 140 var initial *bbpb.Build 141 var lastBuild *bbpb.Build 142 143 defer func() { 144 if outputFile != "" && lastBuild != nil { 145 if err := luciexe.WriteBuildFile(outputFile, lastBuild); err != nil { 146 logging.Errorf(ctx, "failed to write outputFile: %s", err) 147 } 148 } 149 }() 150 151 if !help { 152 if path := environ.FromCtx(ctx).Get(luciexeFakeVar); path != "" { 153 var cleanup func(*error) 154 ctx, initial, cleanup, err = prepFromFakeLuciexeEnv(ctx, wd, path) 155 if cleanup != nil { 156 defer cleanup(&err) 157 } 158 } else { 159 var moreOpts []StartOption 160 initial, moreOpts, err = prepOptsFromLuciexeEnv(ctx, stdin, &lastBuild) 161 if err != nil { 162 return err 163 } 164 opts = append(opts, moreOpts...) 165 } 166 } 167 168 state, ictx, err := Start(ctx, initial, opts...) 169 if err != nil { 170 return err 171 } 172 173 if help { 174 logging.Infof(ctx, "`%s` is a `luciexe` binary. See go.chromium.org/luci/luciexe.", args[0]) 175 logging.Infof(ctx, "======= I/O Proto =======") 176 177 buf := &bytes.Buffer{} 178 state.SynthesizeIOProto(buf) 179 logging.Infof(ctx, "%s", buf.Bytes()) 180 return nil 181 } 182 183 runUserCb(ictx, userArgs, state, cb) 184 if state.buildPb.Output.Status != bbpb.Status_SUCCESS { 185 return errNonSuccess 186 } 187 return nil 188 } 189 190 // overridden in tests 191 var mainSendRate = rate.Every(time.Second) 192 193 const luciexeFakeVar = "LUCIEXE_FAKEBUILD" 194 195 func prepFromFakeLuciexeEnv(ctx context.Context, wd, buildPath string) (newCtx context.Context, initial *bbpb.Build, cleanup func(*error), err error) { 196 // This is a fake build. 197 logging.Infof(ctx, "Running fake build because %s is set. Build proto path: %s", luciexeFakeVar, buildPath) 198 199 // Pull the Build proto from the path in the environment variable and 200 // set up the environment according to the LUCI User Code Contract. 201 newCtx = ctx 202 env := environ.FromCtx(ctx) 203 204 // Defensively clear the environment variable so it doesn't propagate to subtasks. 205 env.Remove(luciexeFakeVar) 206 207 // Read the Build proto. 208 initial, err = readLuciexeFakeBuild(buildPath) 209 if err != nil { 210 return 211 } 212 213 if wd == "" { 214 // Create working directory heirarchy in a new temporary directory. 215 wd, err = os.MkdirTemp("", fmt.Sprintf("luciexe-fakebuild-%s-", filepath.Base(os.Args[0]))) 216 if err != nil { 217 return 218 } 219 err = os.Chdir(wd) 220 if err != nil { 221 return 222 } 223 cleanup = func(err *error) { 224 logging.Infof(ctx, "Cleaning up working directory: %s", wd) 225 if r := os.RemoveAll(wd); r != nil && *err == nil { 226 *err = r 227 } else if r != nil { 228 logging.Errorf(ctx, "Failed to clean up working directory: %s", wd) 229 } 230 } 231 } 232 logging.Infof(ctx, "Working directory: %s", wd) 233 234 tmpDir := filepath.Join(wd, "tmp") 235 if err = os.MkdirAll(tmpDir, os.ModePerm); err != nil { 236 return 237 } 238 cacheDir := filepath.Join(wd, "cache") 239 if err = os.MkdirAll(cacheDir, os.ModePerm); err != nil { 240 return 241 } 242 243 // Set up environment and LUCI_CONTEXT. 244 for _, key := range luciexe.TempDirEnvVars { 245 env.Set(key, tmpDir) 246 } 247 newCtx = env.SetInCtx(newCtx) 248 newCtx = lucictx.SetLUCIExe(newCtx, &lucictx.LUCIExe{ 249 CacheDir: cacheDir, 250 }) 251 252 return 253 } 254 255 func prepOptsFromLuciexeEnv(ctx context.Context, stdin io.Reader, lastBuild **bbpb.Build) (initial *bbpb.Build, opts []StartOption, err error) { 256 initial, err = readStdinBuild(stdin) 257 if err != nil { 258 return 259 } 260 261 bs, err := bootstrap.GetFromEnv(environ.FromCtx(ctx)) 262 if err != nil { 263 return 264 } 265 266 buildStream, err := bs.Client.NewDatagramStream( 267 ctx, "build.proto", 268 streamclient.WithContentType(luciexe.BuildProtoZlibContentType)) 269 if err != nil { 270 return 271 } 272 273 zlibBuf := &bytes.Buffer{} 274 zlibWriter := zlib.NewWriter(zlibBuf) 275 276 opts = append(opts, 277 OptLogsink(bs.Client), 278 OptSend(mainSendRate, func(vers int64, build *bbpb.Build) { 279 *lastBuild = build 280 data, err := proto.Marshal(build) 281 if err != nil { 282 panic(err) 283 } 284 285 zlibBuf.Reset() 286 zlibWriter.Reset(zlibBuf) 287 288 if _, err := zlibWriter.Write(data); err != nil { 289 panic(err) 290 } 291 if err := zlibWriter.Close(); err != nil { 292 panic(err) 293 } 294 if err := buildStream.WriteDatagram(zlibBuf.Bytes()); err != nil { 295 panic(err) 296 } 297 }), 298 ) 299 300 return 301 } 302 303 func runUserCb(ctx context.Context, userArgs []string, state *State, cb func(context.Context, []string, *State) error) { 304 var err error 305 defer func() { 306 state.End(err) 307 if thing := recover(); thing != nil { 308 logging.Errorf(ctx, "recovered panic: %s", thing) 309 logging.Errorf(ctx, "traceback:\n%s", debug.Stack()) 310 } 311 }() 312 err = cb(ctx, userArgs, state) 313 } 314 315 func readStdinBuild(stdin io.Reader) (*bbpb.Build, error) { 316 data, err := io.ReadAll(stdin) 317 if err != nil { 318 return nil, errors.Annotate(err, "reading *Build from stdin").Err() 319 } 320 321 ret := &bbpb.Build{} 322 err = proto.Unmarshal(data, ret) 323 return ret, err 324 } 325 326 func readLuciexeFakeBuild(filename string) (*bbpb.Build, error) { 327 // N.B. This proto is JSON-encoded. 328 data, err := os.ReadFile(filename) 329 if err != nil { 330 return nil, errors.Annotate(err, "reading *Build from %s=%s", luciexeFakeVar, filename).Err() 331 } 332 ret := &bbpb.Build{} 333 err = json.Unmarshal(data, ret) 334 return ret, errors.Annotate(err, "decoding *Build from %s=%s", luciexeFakeVar, filename).Err() 335 } 336 337 func parseArgs(args []string) (output, wd string, strict, help bool) { 338 fs := flag.FlagSet{} 339 fs.BoolVar(&strict, "strict-input", false, "Strict input parsing; Input properties supplied which aren't understood by this program will print an error and quit.") 340 fs.StringVar(&wd, "working-dir", "", "The working directory to run from; Default is $PWD unless LUCIEXE_FAKEBUILD is set, in which case a temp dir is used and cleaned up after.") 341 fs.StringVar(&output, "output", "", "Output final Build message to this path. Must end with {.json,.pb,.textpb}") 342 fs.BoolVar(&help, "help", false, "Print help for this executable") 343 fs.BoolVar(&help, "h", false, "Print help for this executable") 344 if err := fs.Parse(args[1:]); err != nil { 345 panic(err) 346 } 347 return 348 }