diff --git a/build.go b/build.go index 631e816c..328ac3d1 100644 --- a/build.go +++ b/build.go @@ -1,18 +1,16 @@ package gobuild import ( - "errors" "path/filepath" "time" "github.com/paketo-buildpacks/packit" "github.com/paketo-buildpacks/packit/chronos" - "github.com/paketo-buildpacks/packit/scribe" ) //go:generate faux --interface BuildProcess --output fakes/build_process.go type BuildProcess interface { - Execute(config GoBuildConfiguration) (command string, err error) + Execute(config GoBuildConfiguration) (binaries []string, err error) } //go:generate faux --interface PathManager --output fakes/path_manager.go @@ -31,7 +29,7 @@ func Build( buildProcess BuildProcess, pathManager PathManager, clock chronos.Clock, - logs scribe.Emitter, + logs LogEmitter, sourceRemover SourceRemover, ) packit.BuildFunc { @@ -64,7 +62,7 @@ func Build( return packit.BuildResult{}, err } - command, err := buildProcess.Execute(GoBuildConfiguration{ + binaries, err := buildProcess.Execute(GoBuildConfiguration{ Workspace: path, Output: filepath.Join(targetsLayer.Path, "bin"), GoPath: goPath, @@ -83,12 +81,6 @@ func Build( targetsLayer.Metadata = map[string]interface{}{ "built_at": clock.Now().Format(time.RFC3339Nano), - "command": command, - } - - command, ok := targetsLayer.Metadata["command"].(string) - if !ok { - return packit.BuildResult{}, errors.New("failed to identify start command from reused layer metadata") } err = sourceRemover.Clear(context.WorkingDir) @@ -96,19 +88,29 @@ func Build( return packit.BuildResult{}, err } + processes := []packit.Process{ + { + Type: "web", + Command: binaries[0], + Direct: context.Stack == TinyStackName, + }, + } + + for _, binary := range binaries { + processes = append(processes, packit.Process{ + Type: filepath.Base(binary), + Command: binary, + Direct: context.Stack == TinyStackName, + }) + } + logs.Process("Assigning launch processes") - logs.Subprocess("web: %s", command) + logs.ListProcesses(processes) return packit.BuildResult{ Layers: []packit.Layer{targetsLayer, goCacheLayer}, Launch: packit.LaunchMetadata{ - Processes: []packit.Process{ - { - Type: "web", - Command: command, - Direct: context.Stack == TinyStackName, - }, - }, + Processes: processes, }, }, nil } diff --git a/build_test.go b/build_test.go index 6c23e319..d6b795d5 100644 --- a/build_test.go +++ b/build_test.go @@ -13,7 +13,6 @@ import ( "github.com/paketo-buildpacks/go-build/fakes" "github.com/paketo-buildpacks/packit" "github.com/paketo-buildpacks/packit/chronos" - "github.com/paketo-buildpacks/packit/scribe" "github.com/sclevine/spec" . "github.com/onsi/gomega" @@ -49,7 +48,7 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { Expect(err).NotTo(HaveOccurred()) buildProcess = &fakes.BuildProcess{} - buildProcess.ExecuteCall.Returns.Command = "some-start-command" + buildProcess.ExecuteCall.Returns.Binaries = []string{"path/some-start-command", "path/another-start-command"} pathManager = &fakes.PathManager{} pathManager.SetupCall.Returns.GoPath = "some-go-path" @@ -76,7 +75,7 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { buildProcess, pathManager, clock, - scribe.NewEmitter(logs), + gobuild.NewLogEmitter(logs), sourceRemover, ) }) @@ -113,7 +112,6 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { Cache: false, Metadata: map[string]interface{}{ "built_at": timestamp.Format(time.RFC3339Nano), - "command": "some-start-command", }, }, { @@ -131,7 +129,17 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { Processes: []packit.Process{ { Type: "web", - Command: "some-start-command", + Command: "path/some-start-command", + Direct: false, + }, + { + Type: "some-start-command", + Command: "path/some-start-command", + Direct: false, + }, + { + Type: "another-start-command", + Command: "path/another-start-command", Direct: false, }, }, @@ -159,7 +167,9 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { Expect(logs.String()).To(ContainSubstring("Some Buildpack some-version")) Expect(logs.String()).To(ContainSubstring("Assigning launch processes")) - Expect(logs.String()).To(ContainSubstring("web: some-start-command")) + Expect(logs.String()).To(ContainSubstring("web: path/some-start-command")) + Expect(logs.String()).To(ContainSubstring("some-start-command: path/some-start-command")) + Expect(logs.String()).To(ContainSubstring("another-start-command: path/another-start-command")) }) context("when the stack is tiny", func() { @@ -189,7 +199,6 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { Cache: false, Metadata: map[string]interface{}{ "built_at": timestamp.Format(time.RFC3339Nano), - "command": "some-start-command", }, }, { @@ -207,7 +216,17 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { Processes: []packit.Process{ { Type: "web", - Command: "some-start-command", + Command: "path/some-start-command", + Direct: true, + }, + { + Type: "some-start-command", + Command: "path/some-start-command", + Direct: true, + }, + { + Type: "another-start-command", + Command: "path/another-start-command", Direct: true, }, }, diff --git a/fakes/build_process.go b/fakes/build_process.go index faed40a0..3434aeaf 100644 --- a/fakes/build_process.go +++ b/fakes/build_process.go @@ -14,14 +14,14 @@ type BuildProcess struct { Config gobuild.GoBuildConfiguration } Returns struct { - Command string - Err error + Binaries []string + Err error } - Stub func(gobuild.GoBuildConfiguration) (string, error) + Stub func(gobuild.GoBuildConfiguration) ([]string, error) } } -func (f *BuildProcess) Execute(param1 gobuild.GoBuildConfiguration) (string, error) { +func (f *BuildProcess) Execute(param1 gobuild.GoBuildConfiguration) ([]string, error) { f.ExecuteCall.Lock() defer f.ExecuteCall.Unlock() f.ExecuteCall.CallCount++ @@ -29,5 +29,5 @@ func (f *BuildProcess) Execute(param1 gobuild.GoBuildConfiguration) (string, err if f.ExecuteCall.Stub != nil { return f.ExecuteCall.Stub(param1) } - return f.ExecuteCall.Returns.Command, f.ExecuteCall.Returns.Err + return f.ExecuteCall.Returns.Binaries, f.ExecuteCall.Returns.Err } diff --git a/go_build_process.go b/go_build_process.go index 2a6234a1..cb22fc3f 100644 --- a/go_build_process.go +++ b/go_build_process.go @@ -2,6 +2,7 @@ package gobuild import ( "bytes" + "encoding/json" "errors" "fmt" "os" @@ -13,7 +14,6 @@ import ( "github.com/paketo-buildpacks/packit/chronos" "github.com/paketo-buildpacks/packit/pexec" - "github.com/paketo-buildpacks/packit/scribe" ) //go:generate faux --interface Executable --output fakes/executable.go @@ -32,11 +32,11 @@ type GoBuildConfiguration struct { type GoBuildProcess struct { executable Executable - logs scribe.Emitter + logs LogEmitter clock chronos.Clock } -func NewGoBuildProcess(executable Executable, logs scribe.Emitter, clock chronos.Clock) GoBuildProcess { +func NewGoBuildProcess(executable Executable, logs LogEmitter, clock chronos.Clock) GoBuildProcess { return GoBuildProcess{ executable: executable, logs: logs, @@ -44,12 +44,12 @@ func NewGoBuildProcess(executable Executable, logs scribe.Emitter, clock chronos } } -func (p GoBuildProcess) Execute(config GoBuildConfiguration) (string, error) { +func (p GoBuildProcess) Execute(config GoBuildConfiguration) ([]string, error) { p.logs.Process("Executing build process") err := os.MkdirAll(config.Output, os.ModePerm) if err != nil { - return "", fmt.Errorf("failed to create targets output directory: %w", err) + return nil, fmt.Errorf("failed to create targets output directory: %w", err) } contains := func(flags []string, match string) bool { @@ -94,22 +94,44 @@ func (p GoBuildProcess) Execute(config GoBuildConfiguration) (string, error) { p.logs.Action("Failed after %s", duration.Round(time.Millisecond)) p.logs.Detail(buffer.String()) - return "", fmt.Errorf("failed to execute 'go build': %w", err) + return nil, fmt.Errorf("failed to execute 'go build': %w", err) } p.logs.Action("Completed in %s", duration.Round(time.Millisecond)) p.logs.Break() - paths, err := filepath.Glob(fmt.Sprintf("%s/*", config.Output)) - if err != nil { - return "", fmt.Errorf("failed to list targets: %w", err) + var paths []string + for _, target := range config.Targets { + buffer = bytes.NewBuffer(nil) + err := p.executable.Execute(pexec.Execution{ + Args: []string{"list", "--json", target}, + Dir: config.Workspace, + Env: env, + Stdout: buffer, + Stderr: buffer, + }) + if err != nil { + p.logs.Detail(buffer.String()) + + return nil, fmt.Errorf("failed to execute 'go list': %w", err) + } + + var list struct { + ImportPath string `json:"ImportPath"` + } + err = json.Unmarshal(buffer.Bytes(), &list) + if err != nil { + return nil, fmt.Errorf("failed to parse 'go list' output: %w", err) + } + + paths = append(paths, filepath.Join(config.Output, filepath.Base(list.ImportPath))) } if len(paths) == 0 { - return "", errors.New("failed to determine go executable start command") + return nil, errors.New("failed to determine go executable start command") } - return paths[0], nil + return paths, nil } func formatArg(arg string) string { diff --git a/go_build_process_test.go b/go_build_process_test.go index 0fa89947..a7ddc76f 100644 --- a/go_build_process_test.go +++ b/go_build_process_test.go @@ -14,7 +14,6 @@ import ( "github.com/paketo-buildpacks/go-build/fakes" "github.com/paketo-buildpacks/packit/chronos" "github.com/paketo-buildpacks/packit/pexec" - "github.com/paketo-buildpacks/packit/scribe" "github.com/sclevine/spec" . "github.com/onsi/gomega" @@ -28,8 +27,10 @@ func testGoBuildProcess(t *testing.T, context spec.G, it spec.S) { workspacePath string goPath string goCache string - executable *fakes.Executable - logs *bytes.Buffer + executions []pexec.Execution + + executable *fakes.Executable + logs *bytes.Buffer buildProcess gobuild.GoBuildProcess ) @@ -48,27 +49,20 @@ func testGoBuildProcess(t *testing.T, context spec.G, it spec.S) { goCache, err = ioutil.TempDir("", "gocache") Expect(err).NotTo(HaveOccurred()) + logs = bytes.NewBuffer(nil) + executable = &fakes.Executable{} executable.ExecuteCall.Stub = func(execution pexec.Execution) error { - path := execution.Args[2] - - if err := ioutil.WriteFile(filepath.Join(path, "c_command"), nil, 0755); err != nil { - return err - } - - if err := ioutil.WriteFile(filepath.Join(path, "b_command"), nil, 0755); err != nil { - return err - } + executions = append(executions, execution) - if err := ioutil.WriteFile(filepath.Join(path, "a_command"), nil, 0755); err != nil { - return err + if execution.Args[0] == "list" { + fmt.Fprintf(execution.Stdout, `{ + "ImportPath": "%s" + }`, filepath.Join("some-dir", execution.Args[len(execution.Args)-1])) } - return nil } - logs = bytes.NewBuffer(nil) - now := time.Now() times := []time.Time{now, now.Add(1 * time.Second)} @@ -82,7 +76,7 @@ func testGoBuildProcess(t *testing.T, context spec.G, it spec.S) { return t }) - buildProcess = gobuild.NewGoBuildProcess(executable, scribe.NewEmitter(logs), clock) + buildProcess = gobuild.NewGoBuildProcess(executable, gobuild.NewLogEmitter(logs), clock) }) it.After(func() { @@ -93,7 +87,7 @@ func testGoBuildProcess(t *testing.T, context spec.G, it spec.S) { }) it("executes the go build process", func() { - command, err := buildProcess.Execute(gobuild.GoBuildConfiguration{ + binaries, err := buildProcess.Execute(gobuild.GoBuildConfiguration{ Workspace: workspacePath, Output: filepath.Join(layerPath, "bin"), GoPath: goPath, @@ -101,16 +95,32 @@ func testGoBuildProcess(t *testing.T, context spec.G, it spec.S) { Targets: []string{"./some-target", "./other-target"}, }) Expect(err).NotTo(HaveOccurred()) - Expect(command).To(Equal(filepath.Join(layerPath, "bin", "a_command"))) + Expect(binaries).To(Equal([]string{ + filepath.Join(layerPath, "bin", "some-target"), + filepath.Join(layerPath, "bin", "other-target"), + })) Expect(filepath.Join(layerPath, "bin")).To(BeADirectory()) - Expect(executable.ExecuteCall.Receives.Execution.Args).To(Equal([]string{ + Expect(executions[0].Args).To(Equal([]string{ "build", "-o", filepath.Join(layerPath, "bin"), "-buildmode", "pie", "./some-target", "./other-target", })) + + Expect(executions[1].Args).To(Equal([]string{ + "list", + "--json", + "./some-target", + })) + + Expect(executions[2].Args).To(Equal([]string{ + "list", + "--json", + "./other-target", + })) + Expect(executable.ExecuteCall.Receives.Execution.Dir).To(Equal(workspacePath)) Expect(executable.ExecuteCall.Receives.Execution.Env).To(ContainElement(fmt.Sprintf("GOPATH=%s", goPath))) Expect(executable.ExecuteCall.Receives.Execution.Env).To(ContainElement(fmt.Sprintf("GOCACHE=%s", goCache))) @@ -127,7 +137,7 @@ func testGoBuildProcess(t *testing.T, context spec.G, it spec.S) { }) it("executes the go build process with those flags", func() { - command, err := buildProcess.Execute(gobuild.GoBuildConfiguration{ + binaries, err := buildProcess.Execute(gobuild.GoBuildConfiguration{ Workspace: workspacePath, Output: filepath.Join(layerPath, "bin"), GoCache: goCache, @@ -135,11 +145,13 @@ func testGoBuildProcess(t *testing.T, context spec.G, it spec.S) { Flags: []string{"-buildmode", "default", "-ldflags", "-X main.variable=some-value", "-mod", "mod"}, }) Expect(err).NotTo(HaveOccurred()) - Expect(command).To(Equal(filepath.Join(layerPath, "bin", "a_command"))) + Expect(binaries).To(Equal([]string{ + filepath.Join(layerPath, "bin", "some-dir"), + })) Expect(filepath.Join(layerPath, "bin")).To(BeADirectory()) - Expect(executable.ExecuteCall.Receives.Execution.Args).To(Equal([]string{ + Expect(executions[0].Args).To(Equal([]string{ "build", "-o", filepath.Join(layerPath, "bin"), "-buildmode", "default", @@ -147,6 +159,13 @@ func testGoBuildProcess(t *testing.T, context spec.G, it spec.S) { "-mod", "mod", ".", })) + + Expect(executions[1].Args).To(Equal([]string{ + "list", + "--json", + ".", + })) + Expect(executable.ExecuteCall.Receives.Execution.Dir).To(Equal(workspacePath)) Expect(executable.ExecuteCall.Receives.Execution.Env).To(ContainElement(fmt.Sprintf("GOCACHE=%s", goCache))) @@ -163,15 +182,18 @@ func testGoBuildProcess(t *testing.T, context spec.G, it spec.S) { }) it("executes the go build process without setting GOPATH", func() { - command, err := buildProcess.Execute(gobuild.GoBuildConfiguration{ + binaries, err := buildProcess.Execute(gobuild.GoBuildConfiguration{ Workspace: workspacePath, Output: filepath.Join(layerPath, "bin"), GoPath: "", GoCache: goCache, - Targets: []string{"./some-target", "./other-target"}, + Targets: []string{"./other-target", "./some-target"}, }) Expect(err).NotTo(HaveOccurred()) - Expect(command).To(Equal(filepath.Join(layerPath, "bin", "a_command"))) + Expect(binaries).To(Equal([]string{ + filepath.Join(layerPath, "bin", "other-target"), + filepath.Join(layerPath, "bin", "some-target"), + })) Expect(filepath.Join(layerPath, "bin")).To(BeADirectory()) @@ -198,7 +220,7 @@ func testGoBuildProcess(t *testing.T, context spec.G, it spec.S) { }) }) - context("when the executable fails", func() { + context("when the executable fails go build", func() { it.Before(func() { executable.ExecuteCall.Stub = func(execution pexec.Execution) error { fmt.Fprintln(execution.Stdout, "build error stdout") @@ -224,9 +246,43 @@ func testGoBuildProcess(t *testing.T, context spec.G, it spec.S) { }) }) - context("when 'go build' doesn't create any executables", func() { + context("when the executable fails go list", func() { it.Before(func() { - executable.ExecuteCall.Stub = nil + executable.ExecuteCall.Stub = func(execution pexec.Execution) error { + if execution.Args[0] == "list" { + fmt.Fprintln(execution.Stdout, "build error stdout") + fmt.Fprintln(execution.Stderr, "build error stderr") + return errors.New("command failed") + } + + return nil + } + }) + + it("returns an error", func() { + _, err := buildProcess.Execute(gobuild.GoBuildConfiguration{ + Workspace: workspacePath, + Output: filepath.Join(layerPath, "bin"), + GoPath: goPath, + GoCache: goCache, + Targets: []string{"./some-target", "./other-target"}, + }) + Expect(err).To(MatchError("failed to execute 'go list': command failed")) + + Expect(logs.String()).To(ContainSubstring(" build error stdout")) + Expect(logs.String()).To(ContainSubstring(" build error stderr")) + }) + }) + + context("when the json parse of go list fails", func() { + it.Before(func() { + executable.ExecuteCall.Stub = func(execution pexec.Execution) error { + if execution.Args[0] == "list" { + fmt.Fprintln(execution.Stdout, "%%%") + } + + return nil + } }) it("returns an error", func() { @@ -237,7 +293,7 @@ func testGoBuildProcess(t *testing.T, context spec.G, it spec.S) { GoCache: goCache, Targets: []string{"./some-target", "./other-target"}, }) - Expect(err).To(MatchError("failed to determine go executable start command")) + Expect(err).To(MatchError(ContainSubstring("failed to parse 'go list' output:"))) }) }) }) diff --git a/go_buildpack_yml_parser.go b/go_buildpack_yml_parser.go index fa0c1d2b..82818276 100644 --- a/go_buildpack_yml_parser.go +++ b/go_buildpack_yml_parser.go @@ -6,17 +6,16 @@ import ( "path/filepath" "strings" + "github.com/Masterminds/semver" "github.com/buildkite/interpolate" - "github.com/paketo-buildpacks/packit/scribe" "gopkg.in/yaml.v2" - "github.com/Masterminds/semver" ) type GoBuildpackYMLParser struct { - logger scribe.Emitter + logger LogEmitter } -func NewGoBuildpackYMLParser(logger scribe.Emitter) GoBuildpackYMLParser { +func NewGoBuildpackYMLParser(logger LogEmitter) GoBuildpackYMLParser { return GoBuildpackYMLParser{ logger: logger, } diff --git a/go_buildpack_yml_parser_test.go b/go_buildpack_yml_parser_test.go index 30ec5818..529a07d9 100644 --- a/go_buildpack_yml_parser_test.go +++ b/go_buildpack_yml_parser_test.go @@ -8,7 +8,6 @@ import ( "testing" gobuild "github.com/paketo-buildpacks/go-build" - "github.com/paketo-buildpacks/packit/scribe" "github.com/sclevine/spec" . "github.com/onsi/gomega" @@ -43,7 +42,7 @@ go: `), 0644)).To(Succeed()) logs = bytes.NewBuffer(nil) - goBuildpackYMLParser = gobuild.NewGoBuildpackYMLParser(scribe.NewEmitter(logs)) + goBuildpackYMLParser = gobuild.NewGoBuildpackYMLParser(gobuild.NewLogEmitter(logs)) }) it.After(func() { diff --git a/integration/targets_test.go b/integration/targets_test.go index aa2de463..057f5c4e 100644 --- a/integration/targets_test.go +++ b/integration/targets_test.go @@ -73,7 +73,7 @@ func testTargets(t *testing.T, context spec.G, it spec.S) { Execute(image.ID) Expect(err).NotTo(HaveOccurred()) - Eventually(container).Should(Serve(ContainSubstring("go1.15")).OnPort(8080)) + Eventually(container).Should(Serve(ContainSubstring("first: go1.15")).OnPort(8080)) Expect(logs).To(ContainLines( MatchRegexp(fmt.Sprintf(`%s \d+\.\d+\.\d+`, settings.Buildpack.Name)), @@ -83,7 +83,34 @@ func testTargets(t *testing.T, context spec.G, it spec.S) { "", " Assigning launch processes", fmt.Sprintf(" web: /layers/%s/targets/bin/first", strings.ReplaceAll(settings.Buildpack.ID, "/", "_")), + fmt.Sprintf(" first: /layers/%s/targets/bin/first", strings.ReplaceAll(settings.Buildpack.ID, "/", "_")), + fmt.Sprintf(" second: /layers/%s/targets/bin/second", strings.ReplaceAll(settings.Buildpack.ID, "/", "_")), )) }) + + it("the other binary can be accessed using it's name as an entrypoint", func() { + var err error + var logs fmt.Stringer + image, logs, err = pack.Build. + WithPullPolicy("never"). + WithEnv(map[string]string{"BP_GO_TARGETS": "first:./second"}). + WithBuildpacks( + settings.Buildpacks.GoDist.Online, + settings.Buildpacks.GoBuild.Online, + ). + Execute(name, source) + Expect(err).ToNot(HaveOccurred(), logs.String) + + container, err = docker.Container.Run. + WithEnv(map[string]string{"PORT": "8080"}). + WithPublish("8080"). + WithPublishAll(). + WithEntrypoint("second"). + Execute(image.ID) + Expect(err).NotTo(HaveOccurred()) + + Eventually(container).Should(Serve(ContainSubstring("second: go1.15")).OnPort(8080)) + + }) }) } diff --git a/log_emitter.go b/log_emitter.go new file mode 100644 index 00000000..ced9154d --- /dev/null +++ b/log_emitter.go @@ -0,0 +1,24 @@ +package gobuild + +import ( + "io" + + "github.com/paketo-buildpacks/packit" + "github.com/paketo-buildpacks/packit/scribe" +) + +type LogEmitter struct { + scribe.Emitter +} + +func NewLogEmitter(output io.Writer) LogEmitter { + return LogEmitter{ + Emitter: scribe.NewEmitter(output), + } +} + +func (l LogEmitter) ListProcesses(processes []packit.Process) { + for _, p := range processes { + l.Subprocess("%s: %s", p.Type, p.Command) + } +} diff --git a/run/main.go b/run/main.go index de2608bf..3ba71e11 100644 --- a/run/main.go +++ b/run/main.go @@ -7,11 +7,10 @@ import ( "github.com/paketo-buildpacks/packit" "github.com/paketo-buildpacks/packit/chronos" "github.com/paketo-buildpacks/packit/pexec" - "github.com/paketo-buildpacks/packit/scribe" ) func main() { - logEmitter := scribe.NewEmitter(os.Stdout) + logEmitter := gobuild.NewLogEmitter(os.Stdout) configParser := gobuild.NewBuildConfigurationParser(gobuild.NewGoTargetManager(), gobuild.NewGoBuildpackYMLParser(logEmitter)) packit.Run(