github.com/grailbio/bigslice@v0.0.0-20230519005545-30c4c12152ad/cmd/bigslice/bigslicecmd/build.go (about) 1 // Package bigslicecmd provides the core functionality of the bigslice command 2 // as a package for easier integration into external toolchains and setups. 3 // The expected use is that the bigslice run and build commands are common 4 // to all users of bigslice, but the 'setup-ec2' class of commands are specific. 5 package bigslicecmd 6 7 import ( 8 "context" 9 "fmt" 10 "go/ast" 11 "go/parser" 12 "go/scanner" 13 "go/token" 14 "io" 15 "io/ioutil" 16 "os" 17 "os/exec" 18 "path/filepath" 19 "runtime" 20 "strings" 21 "text/template" 22 23 "github.com/grailbio/base/fatbin" 24 "github.com/grailbio/base/must" 25 ) 26 27 var bigsliceMain = template.Must(template.New("bigslice_main.go").Parse(`package main 28 29 import ( 30 "flag" 31 32 "github.com/grailbio/base/log" 33 "github.com/grailbio/bigslice/sliceconfig" 34 {{.Name}} "{{.ImportPath}}" 35 ) 36 37 func main() { 38 sess, shutdown := sliceconfig.Parse() 39 defer shutdown() 40 err := {{.Name}}.BigsliceMain(sess, flag.Args()) 41 if err != nil { 42 log.Fatal(err) 43 } 44 } 45 `)) 46 47 // BuildUsage is the usage message for the Build command. 48 const BuildUsage = `usage: bigslice build [-o output] [inputs] 49 50 Command build builds a bigslice binary for the given package or 51 source files. If no input is given, it is taken to be the package 52 ".". 53 54 Build uses the "go" tool to build a fat bigslice binary, consisting 55 of the native binary for the host GOOS and GOARCH, concatenated with 56 the binary for GOOS=linux and GOARCH=amd64, since that is the target 57 platform for Bigslice workers. See package 58 github.com/grailbio/base/fatbin for more details. 59 60 If the host is GOOS=linux, GOARCH=amd64, then the user can use the 61 regular go tool to build binaries, as the extra build target is not 62 needed. 63 64 The flags are: 65 ` 66 67 // Build builds the bigslice binary and writes it out to specified output filename, 68 // if that string is empty then a suitable name is computed and returned. 69 func Build(ctx context.Context, paths []string, output string) string { 70 must.True(len(paths) > 0, "no paths defined") 71 72 // If we are passed multiple paths, then they must be Go files. 73 if len(paths) > 1 { 74 for _, path := range paths { 75 must.True(filepath.Ext(path) == ".go", 76 "multiple paths provided, but ", path, " is not a Go file") 77 } 78 } 79 80 var info *packageInfo 81 if filepath.Ext(paths[0]) != ".go" { 82 info = mustLoad(paths[0]) 83 } 84 if output == "" { 85 if info == nil { 86 output = strings.TrimSuffix(filepath.Base(paths[0]), ".go") 87 } else if info.Name == "main" { 88 abs, err := filepath.Abs(paths[0]) 89 must.Nil(err) 90 output = filepath.Base(abs) 91 } else { 92 output = info.Name 93 } 94 } 95 96 if info == nil { 97 fileSet := token.NewFileSet() 98 for _, filename := range paths { 99 f, err := parser.ParseFile(fileSet, filename, nil, parser.ParseComments) 100 if err != nil { 101 printGoErrors(err) 102 os.Exit(1) 103 } 104 must.True(f.Name.Name == "main", fileSet.Position(f.Name.NamePos), ": package must be main, not ", f.Name.Name) 105 } 106 } else if info.Name != "main" { 107 var ( 108 fileSet = token.NewFileSet() 109 foundMain bool 110 ) 111 for _, filename := range info.GoFiles { 112 filename = filepath.Join(info.Dir, filename) 113 f, err := parser.ParseFile(fileSet, filename, nil, parser.ParseComments) 114 if err != nil { 115 printGoErrors(err) 116 os.Exit(1) 117 } 118 for _, d := range f.Decls { 119 fn, ok := d.(*ast.FuncDecl) 120 if !ok { 121 continue 122 } 123 if fn.Recv != nil { 124 continue 125 } 126 if fn.Name.String() != "BigsliceMain" { 127 continue 128 } 129 check := func(v bool) { 130 if v { 131 return 132 } 133 pos := fileSet.Position(fn.Pos()) 134 pos.Filename = shortPath(pos.Filename) 135 fmt.Fprintf(os.Stderr, "%s: func BigsliceMain has wrong type; "+ 136 "expected func(*exec.Session, []string) error\n", pos) 137 os.Exit(1) 138 } 139 check(fn.Type.Results != nil && len(fn.Type.Results.List) == 1 && 140 fn.Type.Params.List != nil && 141 len(fn.Type.Params.List) == 2 && 142 len(fn.Type.Params.List[0].Names) <= 1 && 143 len(fn.Type.Params.List[1].Names) <= 1) 144 145 // Check that the first argument is *Session or *foo.Session. 146 // Imports are not resolved here, so we have to check it 147 // syntactically. 148 ptr, ok := fn.Type.Params.List[0].Type.(*ast.StarExpr) 149 check(ok) 150 name, ok := ptr.X.(*ast.Ident) 151 asName := ok && name.Name == "Session" 152 sel, ok := ptr.X.(*ast.SelectorExpr) 153 asSel := ok && sel.Sel.Name == "Session" 154 check(asName || asSel) 155 156 slice, ok := fn.Type.Params.List[1].Type.(*ast.ArrayType) 157 check(ok) 158 name, ok = slice.Elt.(*ast.Ident) 159 check(ok && name.Name == "string") 160 161 name, ok = fn.Type.Results.List[0].Type.(*ast.Ident) 162 check(ok && name.Name == "error") 163 164 foundMain = true 165 } 166 } 167 if !foundMain { 168 fmt.Fprintln(os.Stderr, "func BigsliceMain not found in package", info.ImportPath) 169 } 170 171 f, err := ioutil.TempFile("", info.Name+"*.go") 172 must.Nil(err) 173 must.Nil(bigsliceMain.Execute(f, info)) 174 paths = []string{f.Name()} 175 must.Nil(f.Close()) 176 defer func() { 177 must.Nil(os.Remove(paths[0])) 178 }() 179 } 180 181 build := exec.Command("go", append([]string{"build", "-o", output}, paths...)...) 182 build.Stdout = os.Stdout 183 build.Stderr = os.Stderr 184 must.Nil(build.Run()) 185 186 // We currently assume that the target is linux/amd64. 187 // TODO(marius): don't hard code this. 188 if runtime.GOOS == "linux" && runtime.GOARCH == "amd64" { 189 return output 190 } 191 192 f, err := ioutil.TempFile("", output) 193 must.Nil(err) 194 object := f.Name() 195 must.Nil(f.Close()) 196 197 build = exec.Command("go", append([]string{"build", "-o", object}, paths...)...) 198 build.Stdout = os.Stdout 199 build.Stderr = os.Stderr 200 build.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64") 201 must.Nil(build.Run()) 202 203 outputFile, err := os.OpenFile(output, os.O_WRONLY|os.O_APPEND, 0777) 204 must.Nil(err) 205 outputInfo, err := outputFile.Stat() 206 must.Nil(err) 207 fat := fatbin.NewWriter(outputFile, outputInfo.Size(), runtime.GOOS, runtime.GOARCH) 208 209 linuxAmd64File, err := os.Open(object) 210 must.Nil(err) 211 w, err := fat.Create("linux", "amd64") 212 must.Nil(err) 213 _, err = io.Copy(w, linuxAmd64File) 214 must.Nil(err) 215 must.Nil(os.Remove(object)) 216 must.Nil(fat.Close()) 217 must.Nil(linuxAmd64File.Close()) 218 must.Nil(outputFile.Close()) 219 220 return output 221 } 222 223 func printGoErrors(err error) { 224 if err, ok := err.(scanner.ErrorList); ok { 225 for _, e := range err { 226 e.Pos.Filename = shortPath(e.Pos.Filename) 227 fmt.Fprintln(os.Stderr, err.Error()) 228 } 229 return 230 } 231 fmt.Fprintln(os.Stderr, err.Error()) 232 } 233 234 func shortPath(path string) string { 235 if rel, err := filepath.Rel(cwd, path); err == nil && len(rel) < len(path) { 236 return rel 237 } 238 return path 239 }