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  }