diff --git a/internal/commands/buildpack_new.go b/internal/commands/buildpack_new.go index d3d7d6d43..263cfd375 100644 --- a/internal/commands/buildpack_new.go +++ b/internal/commands/buildpack_new.go @@ -5,11 +5,13 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strings" "github.com/spf13/cobra" "github.com/buildpacks/pack/internal/style" + "github.com/buildpacks/pack/internal/target" "github.com/buildpacks/pack/pkg/client" "github.com/buildpacks/pack/pkg/dist" "github.com/buildpacks/pack/pkg/logging" @@ -17,9 +19,11 @@ import ( // BuildpackNewFlags define flags provided to the BuildpackNew command type BuildpackNewFlags struct { - API string - Path string + API string + Path string + // Deprecated: Stacks are deprecated Stacks []string + Targets []string Version string } @@ -66,11 +70,24 @@ func BuildpackNew(logger logging.Logger, creator BuildpackCreator) *cobra.Comman }) } + var targets []dist.Target + if len(flags.Targets) == 0 && len(flags.Stacks) == 0 { + targets = []dist.Target{{ + OS: runtime.GOOS, + Arch: runtime.GOARCH, + }} + } else { + if targets, err = target.ParseTargets(flags.Targets, logger); err != nil { + return err + } + } + if err := creator.NewBuildpack(cmd.Context(), client.NewBuildpackOptions{ API: flags.API, ID: id, Path: path, Stacks: stacks, + Targets: targets, Version: flags.Version, }); err != nil { return err @@ -84,7 +101,14 @@ func BuildpackNew(logger logging.Logger, creator BuildpackCreator) *cobra.Comman cmd.Flags().StringVarP(&flags.API, "api", "a", "0.8", "Buildpack API compatibility of the generated buildpack") cmd.Flags().StringVarP(&flags.Path, "path", "p", "", "Path to generate the buildpack") cmd.Flags().StringVarP(&flags.Version, "version", "V", "1.0.0", "Version of the generated buildpack") - cmd.Flags().StringSliceVarP(&flags.Stacks, "stacks", "s", []string{"io.buildpacks.stacks.jammy"}, "Stack(s) this buildpack will be compatible with"+stringSliceHelp("stack")) + cmd.Flags().StringSliceVarP(&flags.Stacks, "stacks", "s", nil, "Stack(s) this buildpack will be compatible with"+stringSliceHelp("stack")) + cmd.Flags().MarkDeprecated("stacks", "prefer `--targets` instead: https://github.com/buildpacks/rfcs/blob/main/text/0096-remove-stacks-mixins.md") + cmd.Flags().StringSliceVarP(&flags.Targets, "targets", "t", nil, + `Targets are the list platforms that one targeting, these are generated as part of scaffolding inside buildpack.toml file. one can provide target platforms in format [os][/arch][/variant]:[distroname@osversion@anotherversion];[distroname@osversion] + - Base case for two different architectures : '--targets "linux/amd64" --targets "linux/arm64"' + - case for distribution version: '--targets "windows/amd64:windows-nano@10.0.19041.1415"' + - case for different architecture with distributed versions : '--targets "linux/arm/v6:ubuntu@14.04" --targets "linux/arm/v6:ubuntu@16.04"' + `) AddHelpFlag(cmd, "new") return cmd diff --git a/internal/commands/buildpack_new_test.go b/internal/commands/buildpack_new_test.go index 178061c0e..4a988672d 100644 --- a/internal/commands/buildpack_new_test.go +++ b/internal/commands/buildpack_new_test.go @@ -4,6 +4,7 @@ import ( "bytes" "os" "path/filepath" + "runtime" "testing" "github.com/buildpacks/pack/pkg/client" @@ -36,6 +37,10 @@ func testBuildpackNewCommand(t *testing.T, when spec.G, it spec.S) { mockClient *testmocks.MockPackClient tmpDir string ) + targets := []dist.Target{{ + OS: runtime.GOOS, + Arch: runtime.GOARCH, + }} it.Before(func() { var err error @@ -60,10 +65,7 @@ func testBuildpackNewCommand(t *testing.T, when spec.G, it spec.S) { ID: "example/some-cnb", Path: filepath.Join(tmpDir, "some-cnb"), Version: "1.0.0", - Stacks: []dist.Stack{{ - ID: "io.buildpacks.stacks.jammy", - Mixins: []string{}, - }}, + Targets: targets, }).Return(nil).MaxTimes(1) path := filepath.Join(tmpDir, "some-cnb") @@ -82,5 +84,121 @@ func testBuildpackNewCommand(t *testing.T, when spec.G, it spec.S) { h.AssertNotNil(t, err) h.AssertContains(t, outBuf.String(), "ERROR: directory") }) + + when("target flag is specified, ", func() { + it("it uses target to generate artifacts", func() { + mockClient.EXPECT().NewBuildpack(gomock.Any(), client.NewBuildpackOptions{ + API: "0.8", + ID: "example/targets", + Path: filepath.Join(tmpDir, "targets"), + Version: "1.0.0", + Targets: []dist.Target{{ + OS: "linux", + Arch: "arm", + ArchVariant: "v6", + Distributions: []dist.Distribution{{ + Name: "ubuntu", + Versions: []string{"14.04", "16.04"}, + }}, + }}, + }).Return(nil).MaxTimes(1) + + path := filepath.Join(tmpDir, "targets") + command.SetArgs([]string{"--path", path, "example/targets", "--targets", "linux/arm/v6:ubuntu@14.04@16.04"}) + + err := command.Execute() + h.AssertNil(t, err) + }) + it("it should show error when invalid [os]/[arch] passed", func() { + mockClient.EXPECT().NewBuildpack(gomock.Any(), client.NewBuildpackOptions{ + API: "0.8", + ID: "example/targets", + Path: filepath.Join(tmpDir, "targets"), + Version: "1.0.0", + Targets: []dist.Target{{ + OS: "os", + Arch: "arm", + ArchVariant: "v6", + Distributions: []dist.Distribution{{ + Name: "ubuntu", + Versions: []string{"14.04", "16.04"}, + }}, + }}, + }).Return(nil).MaxTimes(1) + + path := filepath.Join(tmpDir, "targets") + command.SetArgs([]string{"--path", path, "example/targets", "--targets", "os/arm/v6:ubuntu@14.04@16.04"}) + + err := command.Execute() + h.AssertNotNil(t, err) + }) + when("it should", func() { + it("support format [os][/arch][/variant]:[name@version@version2];[some-name@version@version2]", func() { + mockClient.EXPECT().NewBuildpack(gomock.Any(), client.NewBuildpackOptions{ + API: "0.8", + ID: "example/targets", + Path: filepath.Join(tmpDir, "targets"), + Version: "1.0.0", + Targets: []dist.Target{ + { + OS: "linux", + Arch: "arm", + ArchVariant: "v6", + Distributions: []dist.Distribution{ + { + Name: "ubuntu", + Versions: []string{"14.04", "16.04"}, + }, + { + Name: "debian", + Versions: []string{"8.10", "10.9"}, + }, + }, + }, + { + OS: "windows", + Arch: "amd64", + Distributions: []dist.Distribution{ + { + Name: "windows-nano", + Versions: []string{"10.0.19041.1415"}, + }, + }, + }, + }, + }).Return(nil).MaxTimes(1) + + path := filepath.Join(tmpDir, "targets") + command.SetArgs([]string{"--path", path, "example/targets", "--targets", "linux/arm/v6:ubuntu@14.04@16.04;debian@8.10@10.9", "-t", "windows/amd64:windows-nano@10.0.19041.1415"}) + + err := command.Execute() + h.AssertNil(t, err) + }) + }) + when("stacks ", func() { + it("flag should show deprecated message when used", func() { + mockClient.EXPECT().NewBuildpack(gomock.Any(), client.NewBuildpackOptions{ + API: "0.8", + ID: "example/stacks", + Path: filepath.Join(tmpDir, "stacks"), + Version: "1.0.0", + Stacks: []dist.Stack{{ + ID: "io.buildpacks.stacks.jammy", + Mixins: []string{}, + }}, + }).Return(nil).MaxTimes(1) + + path := filepath.Join(tmpDir, "stacks") + output := new(bytes.Buffer) + command.SetOut(output) + command.SetErr(output) + command.SetArgs([]string{"--path", path, "example/stacks", "--stacks", "io.buildpacks.stacks.jammy"}) + + err := command.Execute() + h.AssertNil(t, err) + h.AssertContains(t, output.String(), "Flag --stacks has been deprecated,") + }) + }) + }) }) } diff --git a/internal/target/parse.go b/internal/target/parse.go new file mode 100644 index 000000000..f5ea95589 --- /dev/null +++ b/internal/target/parse.go @@ -0,0 +1,101 @@ +package target + +import ( + "strings" + + "github.com/pkg/errors" + + "github.com/buildpacks/pack/internal/style" + "github.com/buildpacks/pack/pkg/dist" + "github.com/buildpacks/pack/pkg/logging" +) + +func ParseTargets(t []string, logger logging.Logger) (targets []dist.Target, err error) { + for _, v := range t { + target, err := ParseTarget(v, logger) + if err != nil { + return nil, err + } + targets = append(targets, target) + } + return targets, nil +} + +func ParseTarget(t string, logger logging.Logger) (output dist.Target, err error) { + nonDistro, distros, err := getTarget(t, logger) + if v, _ := getSliceAt[string](nonDistro, 0); len(nonDistro) <= 1 && v == "" { + logger.Warn("os/arch must be defined") + } + if err != nil { + return output, err + } + os, arch, variant, err := getPlatform(nonDistro, logger) + if err != nil { + return output, err + } + v, err := ParseDistros(distros, logger) + if err != nil { + return output, err + } + output = dist.Target{ + OS: os, + Arch: arch, + ArchVariant: variant, + Distributions: v, + } + return output, err +} + +func ParseDistros(distroSlice string, logger logging.Logger) (distros []dist.Distribution, err error) { + distro := strings.Split(distroSlice, ";") + if l := len(distro); l == 1 && distro[0] == "" { + return nil, err + } + for _, d := range distro { + v, err := ParseDistro(d, logger) + if err != nil { + return nil, err + } + distros = append(distros, v) + } + return distros, nil +} + +func ParseDistro(distroString string, logger logging.Logger) (distro dist.Distribution, err error) { + d := strings.Split(distroString, "@") + if d[0] == "" || len(d) == 0 { + return distro, errors.Errorf("distro's versions %s cannot be specified without distro's name", style.Symbol("@"+strings.Join(d[1:], "@"))) + } + if len(d) <= 2 && (strings.Contains(strings.Join(d[1:], ""), "") || d[1] == "") { + logger.Warnf("distro with name %s has no specific version!", style.Symbol(d[0])) + } + distro.Name = d[0] + distro.Versions = d[1:] + return distro, err +} + +func getTarget(t string, logger logging.Logger) (nonDistro []string, distros string, err error) { + target := strings.Split(t, ":") + if (len(target) == 1 && target[0] == "") || len(target) == 0 { + return nonDistro, distros, errors.Errorf("invalid target %s, atleast one of [os][/arch][/archVariant] must be specified", t) + } + if len(target) == 2 && target[0] == "" { + v, _ := getSliceAt[string](target, 1) + logger.Warn(style.Warn("adding distros %s without [os][/arch][/variant]", v)) + } else { + i, _ := getSliceAt[string](target, 0) + nonDistro = strings.Split(i, "/") + } + if i, err := getSliceAt[string](target, 1); err == nil { + distros = i + } + return nonDistro, distros, err +} + +func getSliceAt[T interface{}](slice []T, index int) (value T, err error) { + if index < 0 || index >= len(slice) { + return value, errors.Errorf("index out of bound, cannot access item at index %d of slice with length %d", index, len(slice)) + } + + return slice[index], err +} diff --git a/internal/target/parse_test.go b/internal/target/parse_test.go new file mode 100644 index 000000000..030f2c772 --- /dev/null +++ b/internal/target/parse_test.go @@ -0,0 +1,138 @@ +package target_test + +import ( + "bytes" + "testing" + + "github.com/heroku/color" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/pack/internal/target" + "github.com/buildpacks/pack/pkg/dist" + "github.com/buildpacks/pack/pkg/logging" + h "github.com/buildpacks/pack/testhelpers" +) + +func TestParseTargets(t *testing.T) { + color.Disable(true) + defer color.Disable(false) + spec.Run(t, "ParseTargets", testParseTargets, spec.Parallel(), spec.Report(report.Terminal{})) +} + +func testParseTargets(t *testing.T, when spec.G, it spec.S) { + outBuf := bytes.Buffer{} + it.Before(func() { + outBuf = bytes.Buffer{} + h.AssertEq(t, outBuf.String(), "") + var err error + h.AssertNil(t, err) + }) + + when("target#ParseTarget", func() { + it("should show a warn when [os][/arch][/variant] is nil", func() { + target.ParseTarget(":distro@version", logging.NewLogWithWriters(&outBuf, &outBuf)) + h.AssertNotEq(t, outBuf.String(), "") + }) + it("should parse target as expected", func() { + output, err := target.ParseTarget("linux/arm/v6", logging.NewLogWithWriters(&outBuf, &outBuf)) + h.AssertEq(t, outBuf.String(), "") + h.AssertNil(t, err) + h.AssertEq(t, output, dist.Target{ + OS: "linux", + Arch: "arm", + ArchVariant: "v6", + }) + }) + it("should return an error", func() { + _, err := target.ParseTarget("", logging.NewLogWithWriters(&outBuf, &outBuf)) + h.AssertNotNil(t, err) + }) + it("should log a warning when only [os] has typo or is unknown", func() { + target.ParseTarget("os/arm/v6", logging.NewLogWithWriters(&outBuf, &outBuf)) + h.AssertNotEq(t, outBuf.String(), "") + }) + it("should log a warning when only [arch] has typo or is unknown", func() { + target.ParseTarget("darwin/arm/v6", logging.NewLogWithWriters(&outBuf, &outBuf)) + h.AssertNotEq(t, outBuf.String(), "") + }) + it("should log a warning when only [variant] has typo or is unknown", func() { + target.ParseTarget("linux/arm/unknown", logging.NewLogWithWriters(&outBuf, &outBuf)) + h.AssertNotEq(t, outBuf.String(), "") + }) + }) + when("target#ParseTargets", func() { + it("should throw an error when atleast one target throws error", func() { + _, err := target.ParseTargets([]string{"linux/arm/v6", ":distro@version"}, logging.NewLogWithWriters(&outBuf, &outBuf)) + h.AssertNotNil(t, err) + }) + it("should parse targets as expected", func() { + output, err := target.ParseTargets([]string{"linux/arm/v6", "linux/amd64:ubuntu@22.04;debian@8.10@10.06"}, logging.NewLogWithWriters(&outBuf, &outBuf)) + h.AssertNil(t, err) + h.AssertEq(t, output, []dist.Target{ + { + OS: "linux", + Arch: "arm", + ArchVariant: "v6", + }, + { + OS: "linux", + Arch: "amd64", + Distributions: []dist.Distribution{ + { + Name: "ubuntu", + Versions: []string{"22.04"}, + }, + { + Name: "debian", + Versions: []string{"8.10", "10.06"}, + }, + }, + }, + }) + }) + }) + when("target#ParseDistro", func() { + it("should parse distro as expected", func() { + output, err := target.ParseDistro("ubuntu@22.04@20.08", logging.NewLogWithWriters(&outBuf, &outBuf)) + h.AssertEq(t, output, dist.Distribution{ + Name: "ubuntu", + Versions: []string{"22.04", "20.08"}, + }) + h.AssertNil(t, err) + }) + it("should return an error", func() { + _, err := target.ParseDistro("@22.04@20.08", logging.NewLogWithWriters(&outBuf, &outBuf)) + h.AssertNotNil(t, err) + }) + it("should warn when distro version is not specified", func() { + target.ParseDistro("ubuntu", logging.NewLogWithWriters(&outBuf, &outBuf)) + h.AssertNotEq(t, outBuf.String(), "") + }) + }) + when("target#ParseDistros", func() { + it("should parse distros as expected", func() { + output, err := target.ParseDistros("ubuntu@22.04@20.08;debian@8.10@10.06", logging.NewLogWithWriters(&outBuf, &outBuf)) + h.AssertEq(t, output, []dist.Distribution{ + { + Name: "ubuntu", + Versions: []string{"22.04", "20.08"}, + }, + { + Name: "debian", + Versions: []string{"8.10", "10.06"}, + }, + }) + h.AssertNil(t, err) + }) + it("result should be nil", func() { + output, err := target.ParseDistros("", logging.NewLogWithWriters(&outBuf, &outBuf)) + h.AssertEq(t, output, []dist.Distribution(nil)) + h.AssertNil(t, err) + }) + it("should return an error", func() { + _, err := target.ParseDistros(";", logging.NewLogWithWriters(&outBuf, &outBuf)) + h.AssertNotNil(t, err) + }) + }) +} diff --git a/internal/target/platform.go b/internal/target/platform.go new file mode 100644 index 000000000..4d48d6845 --- /dev/null +++ b/internal/target/platform.go @@ -0,0 +1,91 @@ +package target + +import ( + "strings" + + "github.com/pkg/errors" + + "github.com/buildpacks/pack/internal/style" + "github.com/buildpacks/pack/pkg/logging" +) + +func getPlatform(t []string, logger logging.Logger) (os, arch, variant string, err error) { + os, _ = getSliceAt[string](t, 0) + arch, _ = getSliceAt[string](t, 1) + variant, _ = getSliceAt[string](t, 2) + if !supportsOS(os) && supportsVariant(arch, variant) { + logger.Warn(style.Warn("unknown os %s, is this a typo", os)) + } + if supportsArch(os, arch) && !supportsVariant(arch, variant) { + logger.Warn(style.Warn("unknown variant %s", variant)) + } + if supportsOS(os) && !supportsArch(os, arch) && supportsVariant(arch, variant) { + logger.Warn(style.Warn("unknown arch %s", arch)) + } + if !SupportsPlatform(os, arch, variant) { + return os, arch, variant, errors.Errorf("unknown target: %s", style.Symbol(strings.Join(t, "/"))) + } + return os, arch, variant, err +} + +var supportedOSArchs = map[string][]string{ + "aix": {"ppc64"}, + "android": {"386", "amd64", "arm", "arm64"}, + "darwin": {"amd64", "arm64"}, + "dragonfly": {"amd64"}, + "freebsd": {"386", "amd64", "arm"}, + "illumos": {"amd64"}, + "ios": {"arm64"}, + "js": {"wasm"}, + "linux": {"386", "amd64", "arm", "arm64", "loong64", "mips", "mipsle", "mips64", "mips64le", "ppc64", "ppc64le", "riscv64", "s390x"}, + "netbsd": {"386", "amd64", "arm"}, + "openbsd": {"386", "amd64", "arm", "arm64"}, + "plan9": {"386", "amd64", "arm"}, + "solaris": {"amd64"}, + "wasip1": {"wasm"}, + "windows": {"386", "amd64", "arm", "arm64"}, +} + +var supportedArchVariants = map[string][]string{ + "386": {"softfloat", "sse2"}, + "arm": {"v5", "v6", "v7"}, + "amd64": {"v1", "v2", "v3", "v4"}, + "mips": {"hardfloat", "softfloat"}, + "mipsle": {"hardfloat", "softfloat"}, + "mips64": {"hardfloat", "softfloat"}, + "mips64le": {"hardfloat", "softfloat"}, + "ppc64": {"power8", "power9"}, + "ppc64le": {"power8", "power9"}, + "wasm": {"satconv", "signext"}, +} + +func supportsOS(os string) bool { + return supportedOSArchs[os] != nil +} + +func supportsArch(os, arch string) bool { + if supportsOS(os) { + for _, s := range supportedOSArchs[os] { + if s == arch { + return true + } + } + } + return false +} + +func supportsVariant(arch, variant string) (supported bool) { + if variant == "" || len(variant) == 0 { + return true + } + for _, s := range supportedArchVariants[arch] { + if s == variant { + return true + } + } + return supported +} + +func SupportsPlatform(os, arch, variant string) bool { + return supportsArch(os, arch) && supportsVariant(arch, variant) +} diff --git a/internal/target/platform_test.go b/internal/target/platform_test.go new file mode 100644 index 000000000..469bd8896 --- /dev/null +++ b/internal/target/platform_test.go @@ -0,0 +1,35 @@ +package target_test + +import ( + "testing" + + "github.com/heroku/color" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/pack/internal/target" + h "github.com/buildpacks/pack/testhelpers" +) + +func TestPlatforms(t *testing.T) { + color.Disable(true) + defer color.Disable(false) + spec.Run(t, "TestPlatforms", testPlatforms, spec.Parallel(), spec.Report(report.Terminal{})) +} + +func testPlatforms(t *testing.T, when spec.G, it spec.S) { + it.Before(func() { + var err error + h.AssertNil(t, err) + }) + when("target#SupportsPlatform", func() { + it("should return false when target not supported", func() { + b := target.SupportsPlatform("os", "arm", "v6") + h.AssertFalse(t, b) + }) + it("should parse targets as expected", func() { + b := target.SupportsPlatform("linux", "arm", "v6") + h.AssertTrue(t, b) + }) + }) +} diff --git a/pkg/client/build.go b/pkg/client/build.go index c7c35465e..058dd89c5 100644 --- a/pkg/client/build.go +++ b/pkg/client/build.go @@ -1307,7 +1307,7 @@ func createInlineBuildpack(bp projectTypes.Buildpack, stackID string) (string, e bp.Version = "0.0.0" } - if err = createBuildpackTOML(pathToInlineBuilpack, bp.ID, bp.Version, bp.Script.API, []dist.Stack{{ID: stackID}}, nil); err != nil { + if err = createBuildpackTOML(pathToInlineBuilpack, bp.ID, bp.Version, bp.Script.API, []dist.Stack{{ID: stackID}}, []dist.Target{}, nil); err != nil { return pathToInlineBuilpack, err } diff --git a/pkg/client/new_buildpack.go b/pkg/client/new_buildpack.go index cd23f75d3..d5648dbda 100644 --- a/pkg/client/new_buildpack.go +++ b/pkg/client/new_buildpack.go @@ -43,12 +43,15 @@ type NewBuildpackOptions struct { // version of the output buildpack artifact. Version string - // The stacks this buildpack will work with + // Deprecated: The stacks this buildpack will work with Stacks []dist.Stack + + // the targets this buildpack will work with + Targets []dist.Target } func (c *Client) NewBuildpack(ctx context.Context, opts NewBuildpackOptions) error { - err := createBuildpackTOML(opts.Path, opts.ID, opts.Version, opts.API, opts.Stacks, c) + err := createBuildpackTOML(opts.Path, opts.ID, opts.Version, opts.API, opts.Stacks, opts.Targets, c) if err != nil { return err } @@ -94,15 +97,16 @@ func createBinScript(path, name, contents string, c *Client) error { return nil } -func createBuildpackTOML(path, id, version, apiStr string, stacks []dist.Stack, c *Client) error { +func createBuildpackTOML(path, id, version, apiStr string, stacks []dist.Stack, targets []dist.Target, c *Client) error { api, err := api.NewVersion(apiStr) if err != nil { return err } buildpackTOML := dist.BuildpackDescriptor{ - WithAPI: api, - WithStacks: stacks, + WithAPI: api, + WithStacks: stacks, + WithTargets: targets, WithInfo: dist.ModuleInfo{ ID: id, Version: version, diff --git a/pkg/dist/buildmodule.go b/pkg/dist/buildmodule.go index ec3dc99ec..5126d988e 100644 --- a/pkg/dist/buildmodule.go +++ b/pkg/dist/buildmodule.go @@ -55,6 +55,7 @@ type Stack struct { type Target struct { OS string `json:"os" toml:"os"` Arch string `json:"arch" toml:"arch"` + ArchVariant string `json:"variant,omitempty" toml:"variant,omitempty"` Distributions []Distribution `json:"distributions,omitempty" toml:"distributions,omitempty"` } diff --git a/pkg/dist/buildpack_descriptor.go b/pkg/dist/buildpack_descriptor.go index 7ac46b97b..cf93daa2a 100644 --- a/pkg/dist/buildpack_descriptor.go +++ b/pkg/dist/buildpack_descriptor.go @@ -15,7 +15,7 @@ import ( type BuildpackDescriptor struct { WithAPI *api.Version `toml:"api"` WithInfo ModuleInfo `toml:"buildpack"` - WithStacks []Stack `toml:"stacks"` + WithStacks []Stack `toml:"stacks,omitempty"` WithTargets []Target `toml:"targets,omitempty"` WithOrder Order `toml:"order"` WithWindowsBuild bool