func/pkg/oci/go_builder.go

216 lines
5.3 KiB
Go

package oci
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
slashpath "path"
"path/filepath"
"strings"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/tarball"
)
type goBuilder struct{}
func (b goBuilder) Base() string {
return "" // scratch
}
func (b goBuilder) Configure(_ buildJob, _ v1.Platform, cf v1.ConfigFile) (v1.ConfigFile, error) {
// : Using Cmd rather than Entrypoint due to it being overrideable.
cf.Config.Cmd = []string{"/func/f"}
return cf, nil
}
func (b goBuilder) WriteShared(_ buildJob) ([]imageLayer, error) {
return []imageLayer{}, nil // no shared dependencies generated on build
}
// ForPlatform returns layers from source code as Go, cross compiled for the given
// platform, placing the statically linked binary in a tarred layer and return
// the Descriptor and Layer metadata.
func (b goBuilder) WritePlatform(cfg buildJob, p v1.Platform) (layers []imageLayer, err error) {
var desc v1.Descriptor
var layer v1.Layer
// Executable
exe, err := goBuild(cfg, p) // Compile binary returning its path
if err != nil {
return
}
// Tarball
target := filepath.Join(cfg.buildDir(), fmt.Sprintf("execlayer.%v.%v.tar.gz", p.OS, p.Architecture))
if err = goExeTarball(exe, target, cfg.verbose); err != nil {
return
}
// Layer
if layer, err = tarball.LayerFromFile(target); err != nil {
return
}
// Descriptor
if desc, err = newDescriptor(layer); err != nil {
return
}
desc.Platform = &p
// Blob
blob := filepath.Join(cfg.blobsDir(), desc.Digest.Hex)
if cfg.verbose {
fmt.Printf("mv %v %v\n", rel(cfg.buildDir(), target), rel(cfg.buildDir(), blob))
}
err = os.Rename(target, blob)
if err != nil {
return nil, fmt.Errorf("cannot rename blob: %w", err)
}
// NOTE: base is intentionally blank indiciating it is to be built without
// a base layer.
return []imageLayer{{Descriptor: desc, Layer: layer}}, nil
}
func goBuild(cfg buildJob, p v1.Platform) (binPath string, err error) {
gobin, args, outpath, err := goBuildCmd(p, cfg)
if err != nil {
return
}
envs := goBuildEnvs(p)
if cfg.verbose {
fmt.Printf("%v %v\n", gobin, strings.Join(args, " "))
} else {
fmt.Printf(" %v\n", filepath.Base(outpath))
}
// Build the function
cmd := exec.CommandContext(cfg.ctx, gobin, args...)
cmd.Env = envs
cmd.Dir = cfg.buildDir()
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
return outpath, cmd.Run()
}
func goBuildCmd(p v1.Platform, cfg buildJob) (gobin string, args []string, outpath string, err error) {
/* TODO: Use Build Command override from the function if provided
* A future PR will include the ability to specify a
* f.Build.BuildCommand, or BuildArgs for use here to customize
* This will be useful when, for example, the function is written in
* Go and the function developer needs Libc compatibility, in which case
* the default command will need to be replaced with:
* go build -ldflags "-linkmode 'external' -extldflags '-static'"
* Pseudocode:
* if BuildArgs or BuildCommand
* Validate command or args are safe to run
* no other commands injected
* does not contain Go's "toolexec"
* does not specify the output path
* Either replace or append to gobin
*/
// Use the binary specified FUNC_GO_PATH if defined
gobin = os.Getenv("FUNC_GO_PATH") // TODO: move to main and plumb through
if gobin == "" {
gobin = "go"
}
// Build as ./func/builds/$PID/result/f.$OS.$Architecture
name := fmt.Sprintf("f.%v.%v", p.OS, p.Architecture)
if p.Variant != "" {
name = name + "." + p.Variant
}
outpath = filepath.Join(cfg.buildDir(), "result", name)
args = []string{"build", "-o", outpath}
return gobin, args, outpath, nil
}
func goBuildEnvs(p v1.Platform) (envs []string) {
pegged := []string{
"CGO_ENABLED=0",
"GOOS=" + p.OS,
"GOARCH=" + p.Architecture,
}
if p.Variant != "" && p.Architecture == "arm" {
pegged = append(pegged, "GOARM="+strings.TrimPrefix(p.Variant, "v"))
} else if p.Variant != "" && p.Architecture == "amd64" {
pegged = append(pegged, "GOAMD64="+p.Variant)
}
isPegged := func(env string) bool {
for _, v := range pegged {
name := strings.Split(v, "=")[0]
if strings.HasPrefix(env, name) {
return true
}
}
return false
}
envs = append(envs, pegged...)
for _, env := range os.Environ() {
if !isPegged(env) {
envs = append(envs, env)
}
}
return envs
}
func goExeTarball(source, target string, verbose bool) error {
targetFile, err := os.Create(target)
if err != nil {
return err
}
defer targetFile.Close()
gw := gzip.NewWriter(targetFile)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()
info, err := os.Stat(source)
if err != nil {
return err
}
header, err := tar.FileInfoHeader(info, info.Name())
if err != nil {
return err
}
header.Mode = (header.Mode & ^int64(fs.ModePerm)) | 0755
header.Name = slashpath.Join("/func", "f")
// TODO: should we set file timestamps to the build start time of cfg.t?
// header.ModTime = timestampArgument
if err = tw.WriteHeader(header); err != nil {
return err
}
if verbose {
fmt.Printf("→ %v \n", header.Name)
}
file, err := os.Open(source)
if err != nil {
return err
}
defer file.Close()
i, err := io.Copy(tw, file)
if err != nil {
return err
}
if verbose {
fmt.Printf(" wrote %v bytes \n", i)
}
return nil
}