github.com/yourbase/yb@v0.7.1/cmd/yb/build.go (about) 1 package main 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "os" 8 "strings" 9 "time" 10 11 docker "github.com/fsouza/go-dockerclient" 12 "github.com/google/shlex" 13 "github.com/spf13/cobra" 14 "github.com/yourbase/yb" 15 "github.com/yourbase/yb/internal/biome" 16 "github.com/yourbase/yb/internal/build" 17 "github.com/yourbase/yb/internal/ybdata" 18 "github.com/yourbase/yb/internal/ybtrace" 19 "go.opentelemetry.io/otel/api/global" 20 "go.opentelemetry.io/otel/api/trace" 21 "go.opentelemetry.io/otel/codes" 22 sdktrace "go.opentelemetry.io/otel/sdk/trace" 23 "zombiezen.com/go/log" 24 ) 25 26 type buildCmd struct { 27 targetNames []string 28 env []commandLineEnv 29 netrcFiles []string 30 execPrefix string 31 mode executionMode 32 dependenciesOnly bool 33 } 34 35 func newBuildCmd() *cobra.Command { 36 b := new(buildCmd) 37 c := &cobra.Command{ 38 Use: "build [options] [TARGET [...]]", 39 Short: "Build target(s)", 40 Long: `Builds one or more targets in the current package. If no argument is given, ` + 41 `uses the target named "` + yb.DefaultTarget + `", if there is one.` + 42 "\n\n" + 43 `yb build will search for the .yourbase.yml file in the current directory ` + 44 `and its parent directories. The target's commands will be run in the ` + 45 `directory the .yourbase.yml file appears in.`, 46 Args: cobra.ArbitraryArgs, 47 DisableFlagsInUseLine: true, 48 SilenceErrors: true, 49 SilenceUsage: true, 50 RunE: func(cmd *cobra.Command, args []string) error { 51 if len(args) == 0 { 52 b.targetNames = []string{yb.DefaultTarget} 53 } else { 54 b.targetNames = args 55 } 56 return b.run(cmd.Context()) 57 }, 58 ValidArgsFunction: func(cc *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 59 return autocompleteTargetName(toComplete) 60 }, 61 } 62 envFlagsVar(c.Flags(), &b.env) 63 netrcFlagVar(c.Flags(), &b.netrcFiles) 64 executionModeVar(c.Flags(), &b.mode) 65 c.Flags().BoolVar(&b.dependenciesOnly, "deps-only", false, "Install only dependencies, don't do anything else") 66 c.Flags().StringVar(&b.execPrefix, "exec-prefix", "", "Add a prefix to all executed commands (useful for timing or wrapping things)") 67 return c 68 } 69 70 func (b *buildCmd) run(ctx context.Context) error { 71 // Set up trace sink. 72 buildTraces := new(traceSink) 73 tp, err := sdktrace.NewProvider(sdktrace.WithSyncer(buildTraces)) 74 if err != nil { 75 return err 76 } 77 global.SetTraceProvider(tp) 78 79 // Obtain global dependencies. 80 dataDirs, err := ybdata.DirsFromEnv() 81 if err != nil { 82 return err 83 } 84 downloader := ybdata.NewDownloader(dataDirs.Downloads()) 85 baseEnv, err := envFromCommandLine(b.env) 86 if err != nil { 87 return err 88 } 89 execPrefix, err := shlex.Split(b.execPrefix) 90 if err != nil { 91 return fmt.Errorf("parse --exec-prefix: %w", err) 92 } 93 dockerClient, err := connectDockerClient(b.mode) 94 if err != nil { 95 return err 96 } 97 98 startTime := time.Now() 99 ctx, span := ybtrace.Start(ctx, "Build", trace.WithNewRoot()) 100 defer span.End() 101 ctx = withLogOutput(ctx, os.Stdout) 102 103 log.Infof(ctx, "Build started at %s", startTime.Format(longTimeFormat)) 104 105 targetPackage, _, err := findPackage() 106 if err != nil { 107 return err 108 } 109 desired := make([]*yb.Target, 0, len(b.targetNames)) 110 for _, name := range b.targetNames { 111 target := targetPackage.Targets[name] 112 if target == nil { 113 return fmt.Errorf("%s: no such target (found: %s)", name, strings.Join(listTargetNames(targetPackage.Targets), ", ")) 114 } 115 desired = append(desired, target) 116 } 117 buildTargets := yb.BuildOrder(desired...) 118 showDockerWarningsIfNeeded(ctx, b.mode, buildTargets) 119 120 // Do the build! 121 log.Debugf(ctx, "Building package %s in %s...", targetPackage.Name, targetPackage.Path) 122 123 buildError := doTargetList(ctx, targetPackage, buildTargets, &doOptions{ 124 output: os.Stdout, 125 executionMode: b.mode, 126 dockerClient: dockerClient, 127 dataDirs: dataDirs, 128 downloader: downloader, 129 execPrefix: execPrefix, 130 setupOnly: b.dependenciesOnly, 131 baseEnv: baseEnv, 132 netrcFiles: b.netrcFiles, 133 }) 134 if buildError != nil { 135 span.SetStatus(codes.Unknown, buildError.Error()) 136 log.Errorf(ctx, "%v", buildError) 137 } 138 span.End() 139 endTime := time.Now() 140 buildTime := endTime.Sub(startTime) 141 142 fmt.Printf("\nBuild finished at %s, taking %v\n\n", endTime.Format(longTimeFormat), buildTime.Truncate(time.Millisecond)) 143 fmt.Println(buildTraces.dump()) 144 145 style := termStylesFromEnv() 146 if buildError != nil { 147 fmt.Printf("%sBUILD FAILED%s ❌\n", style.buildResult(false), style.reset()) 148 return alreadyLoggedError{buildError} 149 } 150 151 fmt.Printf("%sBUILD PASSED%s ️✔️\n", style.buildResult(true), style.reset()) 152 return nil 153 } 154 155 type doOptions struct { 156 output io.Writer 157 dataDirs *ybdata.Dirs 158 downloader *ybdata.Downloader 159 executionMode executionMode 160 dockerClient *docker.Client 161 dockerNetworkID string 162 baseEnv biome.Environment 163 netrcFiles []string 164 execPrefix []string 165 setupOnly bool 166 } 167 168 func doTargetList(ctx context.Context, pkg *yb.Package, targets []*yb.Target, opts *doOptions) error { 169 if len(targets) == 0 { 170 return nil 171 } 172 orderMsg := new(strings.Builder) 173 orderMsg.WriteString("Going to build targets in the following order:") 174 for _, target := range targets { 175 fmt.Fprintf(orderMsg, "\n - %s", target.Name) 176 } 177 log.Debugf(ctx, "%s", orderMsg) 178 179 // Create a Docker network, if needed. 180 if opts.dockerNetworkID == "" { 181 opts2 := new(doOptions) 182 *opts2 = *opts 183 var cleanup func() 184 var err error 185 opts2.dockerNetworkID, cleanup, err = newDockerNetwork(ctx, opts.dockerClient, opts.executionMode, targets) 186 if err != nil { 187 return err 188 } 189 defer cleanup() 190 opts = opts2 191 } 192 for _, target := range targets { 193 err := doTarget(ctx, pkg, target, opts) 194 if err != nil { 195 return err 196 } 197 } 198 return nil 199 } 200 201 func doTarget(ctx context.Context, pkg *yb.Package, target *yb.Target, opts *doOptions) error { 202 announceTarget(opts.output, target.Name) 203 204 ctx = withLogPrefix(ctx, target.Name) 205 206 bio, err := newBiome(ctx, target, newBiomeOptions{ 207 packageDir: pkg.Path, 208 dataDirs: opts.dataDirs, 209 downloader: opts.downloader, 210 baseEnv: opts.baseEnv, 211 netrcFiles: opts.netrcFiles, 212 executionMode: opts.executionMode, 213 dockerClient: opts.dockerClient, 214 dockerNetworkID: opts.dockerNetworkID, 215 }) 216 if err != nil { 217 return fmt.Errorf("target %s: %w", target.Name, err) 218 } 219 defer func() { 220 if err := bio.Close(); err != nil { 221 log.Warnf(ctx, "Clean up environment: %v", err) 222 } 223 }() 224 output := newLinePrefixWriter(opts.output, target.Name) 225 sys := build.Sys{ 226 Biome: bio, 227 Downloader: opts.downloader, 228 DockerClient: opts.dockerClient, 229 DockerNetworkID: opts.dockerNetworkID, 230 231 Stdout: output, 232 Stderr: output, 233 } 234 execBiome, err := build.Setup(withLogPrefix(ctx, setupLogPrefix), sys, target) 235 if err != nil { 236 return err 237 } 238 sys.Biome = biome.ExecPrefix{ 239 Biome: execBiome, 240 PrependArgv: opts.execPrefix, 241 } 242 defer func() { 243 if err := execBiome.Close(); err != nil { 244 log.Errorf(ctx, "Clean up target %s: %v", target.Name, err) 245 } 246 }() 247 if opts.setupOnly { 248 return nil 249 } 250 251 return build.Execute(withLogOutput(ctx, opts.output), sys, announceCommand(opts.output), target) 252 } 253 254 func announceTarget(out io.Writer, targetName string) { 255 style := termStylesFromEnv() 256 fmt.Fprintf(out, "\n🎯 %sTarget: %s%s\n", style.target(), targetName, style.reset()) 257 } 258 259 func announceCommand(out io.Writer) func(string) { 260 return func(cmdString string) { 261 style := termStylesFromEnv() 262 fmt.Fprintf(out, "%s> %s%s\n", style.command(), cmdString, style.reset()) 263 } 264 } 265 266 func pathExists(path string) bool { 267 _, err := os.Lstat(path) 268 return !os.IsNotExist(err) 269 }