runtime/builder with golang implementation (#2003)
This commit is contained in:
		
							
								
								
									
										189
									
								
								runtime/builder/golang/golang.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								runtime/builder/golang/golang.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | ||||
| package golang | ||||
|  | ||||
| import ( | ||||
| 	"archive/tar" | ||||
| 	"archive/zip" | ||||
| 	"bytes" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"path/filepath" | ||||
|  | ||||
| 	"github.com/micro/go-micro/v3/runtime/builder" | ||||
| 	"github.com/micro/go-micro/v3/runtime/local" | ||||
| ) | ||||
|  | ||||
| // NewBuilder returns a golang builder which can build a go binary given some source | ||||
| func NewBuilder() (builder.Builder, error) { | ||||
| 	path, err := locateGo() | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("Error locating go binary: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	return &golang{ | ||||
| 		cmdPath: path, | ||||
| 		tmpDir:  os.TempDir(), | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| type golang struct { | ||||
| 	cmdPath string | ||||
| 	tmpDir  string | ||||
| } | ||||
|  | ||||
| // Build a binary using source. If an archive was used, e.g. tar, this should be specified in the | ||||
| // options. If no archive option is passed, the builder will treat the source as a single file. | ||||
| func (g *golang) Build(src io.Reader, opts ...builder.Option) (io.Reader, error) { | ||||
| 	// parse the options | ||||
| 	var options builder.Options | ||||
| 	for _, o := range opts { | ||||
| 		o(&options) | ||||
| 	} | ||||
|  | ||||
| 	// create a tmp dir to contain the source | ||||
| 	dir, err := ioutil.TempDir(g.tmpDir, "src") | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer os.RemoveAll(dir) | ||||
|  | ||||
| 	// decode the source and write to the tmp directory | ||||
| 	switch options.Archive { | ||||
| 	case "": | ||||
| 		err = writeFile(src, dir) | ||||
| 	case "tar": | ||||
| 		err = unarchiveTar(src, dir) | ||||
| 	case "zip": | ||||
| 		err = unarchiveZip(src, dir) | ||||
| 	default: | ||||
| 		return nil, errors.New("Invalid Archive") | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// determine the entrypoint if one wasn't provided | ||||
| 	if len(options.Entrypoint) == 0 { | ||||
| 		ep, err := local.Entrypoint(dir) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		options.Entrypoint = ep | ||||
| 	} | ||||
|  | ||||
| 	// build the binary | ||||
| 	cmd := exec.Command(g.cmdPath, "build", "-o", "micro_build", filepath.Dir(options.Entrypoint)) | ||||
| 	cmd.Env = append(os.Environ(), "GO111MODULE=auto") | ||||
| 	cmd.Dir = dir | ||||
| 	if err := cmd.Run(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// read the bytes from the file | ||||
| 	dst, err := ioutil.ReadFile(filepath.Join(dir, "micro_build")) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return bytes.NewBuffer(dst), nil | ||||
| } | ||||
|  | ||||
| // writeFile takes a single file to a directory | ||||
| func writeFile(src io.Reader, dir string) error { | ||||
| 	// copy the source to the temp file | ||||
| 	bytes, err := ioutil.ReadAll(src) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// write the file, note: in order for the golang builder to access the file, it cannot be | ||||
| 	// os.ModeTemp. This is okay because we delete all the files in the tmp dir at the end of this | ||||
| 	// function. | ||||
| 	return ioutil.WriteFile(filepath.Join(dir, "main.go"), bytes, os.ModePerm) | ||||
| } | ||||
|  | ||||
| // unarchiveTar decodes the source in a tar and writes it to a directory | ||||
| func unarchiveTar(src io.Reader, dir string) error { | ||||
| 	tr := tar.NewReader(src) | ||||
| 	for { | ||||
| 		hdr, err := tr.Next() | ||||
| 		if err == io.EOF { | ||||
| 			break | ||||
| 		} else if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		path := filepath.Join(dir, hdr.Name) | ||||
| 		bytes, err := ioutil.ReadAll(tr) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		if err := ioutil.WriteFile(path, bytes, os.ModePerm); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // unarchiveZip decodes the source in a zip and writes it to a directory | ||||
| func unarchiveZip(src io.Reader, dir string) error { | ||||
| 	// create a new buffer with the source, this is required because zip.NewReader takes a io.ReaderAt | ||||
| 	// and not an io.Reader | ||||
| 	buff := bytes.NewBuffer([]byte{}) | ||||
| 	size, err := io.Copy(buff, src) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// create the zip | ||||
| 	reader := bytes.NewReader(buff.Bytes()) | ||||
| 	zip, err := zip.NewReader(reader, size) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// write the files in the zip to our tmp dir | ||||
| 	for _, f := range zip.File { | ||||
| 		rc, err := f.Open() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		bytes, err := ioutil.ReadAll(rc) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		path := filepath.Join(dir, f.Name) | ||||
| 		if err := ioutil.WriteFile(path, bytes, os.ModePerm); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		if err := rc.Close(); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // locateGo locates the go command | ||||
| func locateGo() (string, error) { | ||||
| 	if gr := os.Getenv("GOROOT"); len(gr) > 0 { | ||||
| 		return filepath.Join(gr, "bin", "go"), nil | ||||
| 	} | ||||
|  | ||||
| 	// check path | ||||
| 	for _, p := range filepath.SplitList(os.Getenv("PATH")) { | ||||
| 		bin := filepath.Join(p, "go") | ||||
| 		if _, err := os.Stat(bin); err == nil { | ||||
| 			return bin, nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return exec.LookPath("go") | ||||
| } | ||||
							
								
								
									
										148
									
								
								runtime/builder/golang/golang_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								runtime/builder/golang/golang_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,148 @@ | ||||
| package golang | ||||
|  | ||||
| import ( | ||||
| 	"archive/tar" | ||||
| 	"archive/zip" | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/micro/go-micro/v3/runtime/builder" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	testMainGo   = "package main; import \"fmt\"; func main() { fmt.Println(\"HelloWorld\") }" | ||||
| 	testSecondGo = "package main; import \"fmt\"; func init() { fmt.Println(\"Init\") }" | ||||
| ) | ||||
|  | ||||
| func TestGolangBuilder(t *testing.T) { | ||||
| 	t.Run("NoArchive", func(t *testing.T) { | ||||
| 		buf := bytes.NewBuffer([]byte(testMainGo)) | ||||
| 		err := testBuilder(t, buf) | ||||
| 		assert.Nil(t, err, "No error should be returned") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("InvalidArchive", func(t *testing.T) { | ||||
| 		buf := bytes.NewBuffer([]byte(testMainGo)) | ||||
| 		err := testBuilder(t, buf, builder.Archive("foo")) | ||||
| 		assert.Error(t, err, "An error should be returned") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("TarArchive", func(t *testing.T) { | ||||
| 		// Create a tar writer | ||||
| 		tf := bytes.NewBuffer(nil) | ||||
| 		tw := tar.NewWriter(tf) | ||||
|  | ||||
| 		// Add some files to the archive. | ||||
| 		var files = []struct { | ||||
| 			Name, Body string | ||||
| 		}{ | ||||
| 			{"main.go", testMainGo}, | ||||
| 			{"second.go", testSecondGo}, | ||||
| 		} | ||||
| 		for _, file := range files { | ||||
| 			hdr := &tar.Header{ | ||||
| 				Name: file.Name, | ||||
| 				Mode: 0600, | ||||
| 				Size: int64(len(file.Body)), | ||||
| 			} | ||||
| 			if err := tw.WriteHeader(hdr); err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 			if _, err := tw.Write([]byte(file.Body)); err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 		} | ||||
| 		if err := tw.Close(); err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		err := testBuilder(t, tf, builder.Archive("tar")) | ||||
| 		assert.Nil(t, err, "No error should be returned") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("ZipArchive", func(t *testing.T) { | ||||
| 		// Create a buffer to write our archive to. | ||||
| 		buf := new(bytes.Buffer) | ||||
|  | ||||
| 		// Create a new zip archive. | ||||
| 		w := zip.NewWriter(buf) | ||||
| 		defer w.Close() | ||||
|  | ||||
| 		// Add some files to the archive. | ||||
| 		var files = []struct { | ||||
| 			Name, Body string | ||||
| 		}{ | ||||
| 			{"main.go", testMainGo}, | ||||
| 			{"second.go", testSecondGo}, | ||||
| 		} | ||||
| 		for _, file := range files { | ||||
| 			f, err := w.Create(file.Name) | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 			_, err = f.Write([]byte(file.Body)) | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 		} | ||||
| 		if err := w.Close(); err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		err := testBuilder(t, buf, builder.Archive("zip")) | ||||
| 		assert.Nil(t, err, "No error should be returned") | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func testBuilder(t *testing.T, buf io.Reader, opts ...builder.Option) error { | ||||
| 	// setup the builder | ||||
| 	builder, err := NewBuilder() | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("Error creating the builder: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// build the source | ||||
| 	res, err := builder.Build(buf, opts...) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("Error building source: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// write the binary to a tmp file and make it executable | ||||
| 	file, err := ioutil.TempFile(os.TempDir(), "res") | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("Error creating tmp output file: %v", err) | ||||
| 	} | ||||
| 	if _, err := io.Copy(file, res); err != nil { | ||||
| 		return fmt.Errorf("Error copying binary to tmp file: %v", err) | ||||
| 	} | ||||
| 	if err := file.Close(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if err := os.Chmod(file.Name(), 0111); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer os.Remove(file.Name()) | ||||
|  | ||||
| 	// execute the binary | ||||
| 	cmd := exec.Command(file.Name()) | ||||
| 	outp, err := cmd.Output() | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("Error executing binary: %v", err) | ||||
| 	} | ||||
| 	if !strings.Contains(string(outp), "HelloWorld") { | ||||
| 		return fmt.Errorf("Output does not contain HelloWorld") | ||||
| 	} | ||||
| 	// when an archive is used we also check for the second file to be loaded | ||||
| 	if len(opts) > 0 && !strings.Contains(string(outp), "Init") { | ||||
| 		return fmt.Errorf("Output does not contain Init") | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
		Reference in New Issue
	
	Block a user