From 13a828e0263f7c42db12e2e9a258be7b60a5f388 Mon Sep 17 00:00:00 2001 From: Lorenz Bauer Date: Fri, 21 Jun 2024 18:11:40 +0100 Subject: [PATCH] bpf2go: export targets Move target and force users to use one of the predefined targets. Also export logic to generate build contraints from goarches. Signed-off-by: Lorenz Bauer --- cmd/bpf2go/flags.go | 12 --- cmd/bpf2go/gen/compile.go | 15 ++-- cmd/bpf2go/gen/target.go | 155 ++++++++++++++++++++++++++++++++++ cmd/bpf2go/gen/target_test.go | 155 ++++++++++++++++++++++++++++++++++ cmd/bpf2go/main.go | 143 ++++++------------------------- cmd/bpf2go/main_test.go | 155 +--------------------------------- 6 files changed, 346 insertions(+), 289 deletions(-) create mode 100644 cmd/bpf2go/gen/target.go create mode 100644 cmd/bpf2go/gen/target_test.go diff --git a/cmd/bpf2go/flags.go b/cmd/bpf2go/flags.go index ca8852a29..806d2cd2c 100644 --- a/cmd/bpf2go/flags.go +++ b/cmd/bpf2go/flags.go @@ -43,15 +43,3 @@ func andConstraints(x, y constraint.Expr) constraint.Expr { return &constraint.AndExpr{X: x, Y: y} } - -func orConstraints(x, y constraint.Expr) constraint.Expr { - if x == nil { - return y - } - - if y == nil { - return x - } - - return &constraint.OrExpr{X: x, Y: y} -} diff --git a/cmd/bpf2go/gen/compile.go b/cmd/bpf2go/gen/compile.go index 4c0da5cf8..09d57da8d 100644 --- a/cmd/bpf2go/gen/compile.go +++ b/cmd/bpf2go/gen/compile.go @@ -20,8 +20,8 @@ type CompileArgs struct { Source string // Absolute output file name Dest string - // Target to compile for, defaults to "bpf". - Target string + // Target to compile for, defaults to compiling generic BPF in host endianness. + Target Target DisableStripping bool } @@ -48,13 +48,18 @@ func Compile(args CompileArgs) error { } target := args.Target - if target == "" { - target = "bpf" + if target == (Target{}) { + target.clang = "bpf" } // C flags that can't be overridden. + if linux := target.linux; linux != "" { + cmd.Args = append(cmd.Args, "-D__TARGET_ARCH_"+linux) + } + cmd.Args = append(cmd.Args, - "-target", target, + "-Wunused-command-line-argument", + "-target", target.clang, "-c", args.Source, "-o", args.Dest, // Don't include clang version diff --git a/cmd/bpf2go/gen/target.go b/cmd/bpf2go/gen/target.go new file mode 100644 index 000000000..f5484cccf --- /dev/null +++ b/cmd/bpf2go/gen/target.go @@ -0,0 +1,155 @@ +package gen + +import ( + "errors" + "fmt" + "go/build/constraint" + "maps" + "runtime" + "slices" +) + +var ErrInvalidTarget = errors.New("unsupported target") + +var targetsByGoArch = map[GoArch]Target{ + "386": {"bpfel", "x86"}, + "amd64": {"bpfel", "x86"}, + "arm": {"bpfel", "arm"}, + "arm64": {"bpfel", "arm64"}, + "loong64": {"bpfel", "loongarch"}, + "mips": {"bpfeb", "mips"}, + "mipsle": {"bpfel", ""}, + "mips64": {"bpfeb", ""}, + "mips64le": {"bpfel", ""}, + "ppc64": {"bpfeb", "powerpc"}, + "ppc64le": {"bpfel", "powerpc"}, + "riscv64": {"bpfel", "riscv"}, + "s390x": {"bpfeb", "s390"}, +} + +type Target struct { + // Clang arch string, used to define the clang -target flag, as per + // "clang -print-targets". + clang string + // Linux arch string, used to define __TARGET_ARCH_xzy macros used by + // https://github.com/libbpf/libbpf/blob/master/src/bpf_tracing.h + linux string +} + +// TargetsByGoArch returns all supported targets. +func TargetsByGoArch() map[GoArch]Target { + return maps.Clone(targetsByGoArch) +} + +// IsGeneric returns true if the target will compile to generic BPF. +func (tgt *Target) IsGeneric() bool { + return tgt.linux == "" +} + +// Suffix returns a a string suitable for appending to a file name to +// identify the target. +func (tgt *Target) Suffix() string { + // The output filename must not match any of the following patterns: + // + // *_GOOS + // *_GOARCH + // *_GOOS_GOARCH + // + // Otherwise it is interpreted as a build constraint by the Go toolchain. + stem := tgt.clang + if tgt.linux != "" { + stem = fmt.Sprintf("%s_%s", tgt.linux, tgt.clang) + } + return stem +} + +// ObsoleteSuffix returns an obsolete suffix for a subset of targets. +// +// It's used to work around an old bug and should not be used in new code. +func (tgt *Target) ObsoleteSuffix() string { + if tgt.linux == "" { + return "" + } + + return fmt.Sprintf("%s_%s", tgt.clang, tgt.linux) +} + +// GoArch is a Go arch string. +// +// See https://go.dev/doc/install/source#environment for valid GOARCHes when +// GOOS=linux. +type GoArch string + +type GoArches []GoArch + +// Constraints is satisfied when GOARCH is any of the arches. +func (arches GoArches) Constraint() constraint.Expr { + var archConstraint constraint.Expr + for _, goarch := range arches { + tag := &constraint.TagExpr{Tag: string(goarch)} + archConstraint = orConstraints(archConstraint, tag) + } + return archConstraint +} + +// FindTarget turns a list of identifiers into targets and their respective +// GoArches. +// +// The following are valid identifiers: +// +// - bpf: compile generic BPF for host endianness +// - bpfel: compile generic BPF for little endian +// - bpfeb: compile generic BPF for big endian +// - native: compile BPF for host target +// - $GOARCH: compile BPF for $GOARCH target +// +// Generic BPF can run on any target goarch with the correct endianness, +// but doesn't have access to some arch specific tracing functionality. +func FindTarget(id string) (Target, GoArches, error) { + switch id { + case "bpf", "bpfel", "bpfeb": + var goarches []GoArch + for arch, archTarget := range targetsByGoArch { + if archTarget.clang == id { + // Include tags for all goarches that have the same endianness. + goarches = append(goarches, arch) + } + } + slices.Sort(goarches) + return Target{id, ""}, goarches, nil + + case "native": + id = runtime.GOARCH + fallthrough + + default: + archTarget, ok := targetsByGoArch[GoArch(id)] + if !ok || archTarget.linux == "" { + return Target{}, nil, fmt.Errorf("%q: %w", id, ErrInvalidTarget) + } + + var goarches []GoArch + for goarch, lt := range targetsByGoArch { + if lt == archTarget { + // Include tags for all goarches that have the same + // target. + goarches = append(goarches, goarch) + } + } + + slices.Sort(goarches) + return archTarget, goarches, nil + } +} + +func orConstraints(x, y constraint.Expr) constraint.Expr { + if x == nil { + return y + } + + if y == nil { + return x + } + + return &constraint.OrExpr{X: x, Y: y} +} diff --git a/cmd/bpf2go/gen/target_test.go b/cmd/bpf2go/gen/target_test.go new file mode 100644 index 000000000..021b9dda7 --- /dev/null +++ b/cmd/bpf2go/gen/target_test.go @@ -0,0 +1,155 @@ +package gen + +import ( + "errors" + "os/exec" + "slices" + "testing" + + "github.com/go-quicktest/qt" +) + +func TestCollectTargets(t *testing.T) { + clangArches := make(map[string][]GoArch) + linuxArchesLE := make(map[string][]GoArch) + linuxArchesBE := make(map[string][]GoArch) + for arch, archTarget := range targetsByGoArch { + clangArches[archTarget.clang] = append(clangArches[archTarget.clang], arch) + if archTarget.clang == "bpfel" { + linuxArchesLE[archTarget.linux] = append(linuxArchesLE[archTarget.linux], arch) + continue + } + linuxArchesBE[archTarget.linux] = append(linuxArchesBE[archTarget.linux], arch) + } + for i := range clangArches { + slices.Sort(clangArches[i]) + } + for i := range linuxArchesLE { + slices.Sort(linuxArchesLE[i]) + } + for i := range linuxArchesBE { + slices.Sort(linuxArchesBE[i]) + } + + nativeTarget, nativeArches, err := FindTarget("native") + qt.Assert(t, qt.IsNil(err)) + + tests := []struct { + short string + target Target + arches GoArches + }{ + { + "bpf", + Target{"bpf", ""}, + nil, + }, + { + "bpfel", + Target{"bpfel", ""}, + clangArches["bpfel"], + }, + { + "bpfeb", + Target{"bpfeb", ""}, + clangArches["bpfeb"], + }, + { + "amd64", + Target{"bpfel", "x86"}, + linuxArchesLE["x86"], + }, + { + "386", + Target{"bpfel", "x86"}, + linuxArchesLE["x86"], + }, + { + "ppc64", + Target{"bpfeb", "powerpc"}, + linuxArchesBE["powerpc"], + }, + { + "native", + nativeTarget, + nativeArches, + }, + } + + for _, test := range tests { + t.Run(test.short, func(t *testing.T) { + target, arches, err := FindTarget(test.short) + qt.Assert(t, qt.IsNil(err)) + qt.Assert(t, qt.Equals(target, test.target)) + qt.Assert(t, qt.DeepEquals(arches, test.arches)) + }) + } +} + +func TestCollectTargetsErrors(t *testing.T) { + tests := []struct { + name string + target string + }{ + {"unknown", "frood"}, + {"no linux target", "mipsle"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, _, err := FindTarget(test.target) + if err == nil { + t.Fatal("Function did not return an error") + } + t.Log("Error message:", err) + }) + } +} + +func TestGoarches(t *testing.T) { + exe := goBin(t) + + for GoArch := range targetsByGoArch { + t.Run(string(GoArch), func(t *testing.T) { + goEnv := exec.Command(exe, "env") + goEnv.Env = []string{"GOROOT=/", "GOOS=linux", "GOARCH=" + string(GoArch)} + output, err := goEnv.CombinedOutput() + qt.Assert(t, qt.IsNil(err), qt.Commentf("go output is:\n%s", string(output))) + }) + } +} + +func TestClangTargets(t *testing.T) { + exe := goBin(t) + + clangTargets := map[string]struct{}{} + for _, tgt := range targetsByGoArch { + clangTargets[tgt.clang] = struct{}{} + } + + for target := range clangTargets { + for _, env := range []string{"GOOS", "GOARCH"} { + env += "=" + target + t.Run(env, func(t *testing.T) { + goEnv := exec.Command(exe, "env") + goEnv.Env = []string{"GOROOT=/", env} + output, err := goEnv.CombinedOutput() + t.Log("go output is:", string(output)) + qt.Assert(t, qt.IsNotNil(err), qt.Commentf("No clang target should be a valid build constraint")) + }) + } + + } +} + +func goBin(t *testing.T) string { + t.Helper() + + exe, err := exec.LookPath("go") + if errors.Is(err, exec.ErrNotFound) { + t.Skip("go binary is not in PATH") + } + qt.Assert(t, qt.IsNil(err)) + + return exe +} diff --git a/cmd/bpf2go/main.go b/cmd/bpf2go/main.go index eb09e8803..fb077e139 100644 --- a/cmd/bpf2go/main.go +++ b/cmd/bpf2go/main.go @@ -4,13 +4,11 @@ import ( "errors" "flag" "fmt" - "go/build/constraint" "io" "os" "os/exec" "path/filepath" "regexp" - "runtime" "slices" "sort" "strings" @@ -44,29 +42,6 @@ Options: ` -// Targets understood by bpf2go. -// -// Targets without a Linux string can't be used directly and are only included -// for the generic bpf, bpfel, bpfeb targets. -// -// See https://go.dev/doc/install/source#environment for valid GOARCHes when -// GOOS=linux. -var targetByGoArch = map[goarch]target{ - "386": {"bpfel", "x86"}, - "amd64": {"bpfel", "x86"}, - "arm": {"bpfel", "arm"}, - "arm64": {"bpfel", "arm64"}, - "loong64": {"bpfel", "loongarch"}, - "mips": {"bpfeb", "mips"}, - "mipsle": {"bpfel", ""}, - "mips64": {"bpfeb", ""}, - "mips64le": {"bpfel", ""}, - "ppc64": {"bpfeb", "powerpc"}, - "ppc64le": {"bpfel", "powerpc"}, - "riscv64": {"bpfel", "riscv"}, - "s390x": {"bpfeb", "s390"}, -} - func run(stdout io.Writer, args []string) (err error) { b2g, err := newB2G(stdout, args) switch { @@ -92,7 +67,7 @@ type bpf2go struct { // Valid go identifier. identStem string // Targets to build for. - targetArches map[target][]goarch + targetArches map[gen.Target]gen.GoArches // C compiler. cc string // Command used to strip DWARF. @@ -208,14 +183,18 @@ func newB2G(stdout io.Writer, args []string) (*bpf2go, error) { return nil, fmt.Errorf("-output-stem %q must not contain path separation characters", b2g.outputStem) } - targetArches, err := collectTargets(strings.Split(*flagTarget, ",")) - if errors.Is(err, errInvalidTarget) { - printTargets(b2g.stdout) - fmt.Fprintln(b2g.stdout) - return nil, err - } - if err != nil { - return nil, err + targetArches := make(map[gen.Target]gen.GoArches) + for _, tgt := range strings.Split(*flagTarget, ",") { + target, goarches, err := gen.FindTarget(tgt) + if err != nil { + if errors.Is(err, gen.ErrInvalidTarget) { + printTargets(b2g.stdout) + fmt.Fprintln(b2g.stdout) + } + return nil, err + } + + targetArches[target] = goarches } if len(targetArches) == 0 { @@ -303,7 +282,7 @@ func (b2g *bpf2go) convertAll() (err error) { return nil } -func (b2g *bpf2go) convert(tgt target, goarches []goarch) (err error) { +func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) { removeOnError := func(f *os.File) { if err != nil { os.Remove(f.Name()) @@ -316,17 +295,7 @@ func (b2g *bpf2go) convert(tgt target, goarches []goarch) (err error) { outputStem = strings.ToLower(b2g.identStem) } - // The output filename must not match any of the following patterns: - // - // *_GOOS - // *_GOARCH - // *_GOOS_GOARCH - // - // Otherwise it is interpreted as a build constraint by the Go toolchain. - stem := fmt.Sprintf("%s_%s", outputStem, tgt.clang) - if tgt.linux != "" { - stem = fmt.Sprintf("%s_%s_%s", outputStem, tgt.linux, tgt.clang) - } + stem := fmt.Sprintf("%s_%s", outputStem, tgt.Suffix()) absOutPath, err := filepath.Abs(b2g.outputDir) if err != nil { @@ -340,25 +309,15 @@ func (b2g *bpf2go) convert(tgt target, goarches []goarch) (err error) { return err } - var archConstraint constraint.Expr - for _, goarch := range goarches { - tag := &constraint.TagExpr{Tag: string(goarch)} - archConstraint = orConstraints(archConstraint, tag) - } - + archConstraint := goarches.Constraint() constraints := andConstraints(archConstraint, b2g.tags.Expr) - cFlags := make([]string, len(b2g.cFlags)) - copy(cFlags, b2g.cFlags) - if tgt.linux != "" { - cFlags = append(cFlags, "-D__TARGET_ARCH_"+tgt.linux) - } - if err := b2g.removeOldOutputFiles(outputStem, tgt); err != nil { return fmt.Errorf("remove obsolete output: %w", err) } var depInput *os.File + cFlags := slices.Clone(b2g.cFlags) if b2g.makeBase != "" { depInput, err = os.CreateTemp("", "bpf2go") if err != nil { @@ -383,7 +342,7 @@ func (b2g *bpf2go) convert(tgt target, goarches []goarch) (err error) { Strip: b2g.strip, DisableStripping: b2g.disableStripping, Flags: cFlags, - Target: tgt.clang, + Target: tgt, Workdir: cwd, Source: b2g.sourceFile, Dest: objFileName, @@ -479,12 +438,13 @@ func (b2g *bpf2go) convert(tgt target, goarches []goarch) (err error) { // // In the old scheme some linux targets were interpreted as build constraints // by the go toolchain. -func (b2g *bpf2go) removeOldOutputFiles(outputStem string, tgt target) error { - if tgt.linux == "" { +func (b2g *bpf2go) removeOldOutputFiles(outputStem string, tgt gen.Target) error { + suffix := tgt.ObsoleteSuffix() + if suffix == "" { return nil } - stem := fmt.Sprintf("%s_%s_%s", outputStem, tgt.clang, tgt.linux) + stem := fmt.Sprintf("%s_%s", outputStem, suffix) for _, ext := range []string{".o", ".go"} { filename := filepath.Join(b2g.outputDir, stem+ext) @@ -500,21 +460,10 @@ func (b2g *bpf2go) removeOldOutputFiles(outputStem string, tgt target) error { return nil } -type target struct { - // Clang arch string, used to define the clang -target flag, as per - // "clang -print-targets". - clang string - // Linux arch string, used to define __TARGET_ARCH_xzy macros used by - // https://github.com/libbpf/libbpf/blob/master/src/bpf_tracing.h - linux string -} - -type goarch string - func printTargets(w io.Writer) { var arches []string - for goarch, archTarget := range targetByGoArch { - if archTarget.linux == "" { + for goarch, archTarget := range gen.TargetsByGoArch() { + if archTarget.IsGeneric() { continue } arches = append(arches, string(goarch)) @@ -528,50 +477,6 @@ func printTargets(w io.Writer) { } } -var errInvalidTarget = errors.New("unsupported target") - -func collectTargets(targets []string) (map[target][]goarch, error) { - result := make(map[target][]goarch) - for _, tgt := range targets { - switch tgt { - case "bpf", "bpfel", "bpfeb": - var goarches []goarch - for arch, archTarget := range targetByGoArch { - if archTarget.clang == tgt { - // Include tags for all goarches that have the same endianness. - goarches = append(goarches, arch) - } - } - slices.Sort(goarches) - result[target{tgt, ""}] = goarches - - case "native": - tgt = runtime.GOARCH - fallthrough - - default: - archTarget, ok := targetByGoArch[goarch(tgt)] - if !ok || archTarget.linux == "" { - return nil, fmt.Errorf("%q: %w", tgt, errInvalidTarget) - } - - var goarches []goarch - for goarch, lt := range targetByGoArch { - if lt == archTarget { - // Include tags for all goarches that have the same - // target. - goarches = append(goarches, goarch) - } - } - - slices.Sort(goarches) - result[archTarget] = goarches - } - } - - return result, nil -} - func collectCTypes(types *btf.Spec, names []string) ([]btf.Type, error) { var result []btf.Type for _, cType := range names { diff --git a/cmd/bpf2go/main_test.go b/cmd/bpf2go/main_test.go index f9605d3d2..218bb8742 100644 --- a/cmd/bpf2go/main_test.go +++ b/cmd/bpf2go/main_test.go @@ -2,21 +2,18 @@ package main import ( "bytes" - "errors" "fmt" "io" "os" "os/exec" "path/filepath" - "runtime" - "slices" "strings" "testing" + "github.com/cilium/ebpf/cmd/bpf2go/gen" "github.com/cilium/ebpf/cmd/bpf2go/internal" "github.com/cilium/ebpf/internal/testutils" "github.com/go-quicktest/qt" - "github.com/google/go-cmp/cmp" ) const minimalSocketFilter = `__attribute__((section("socket"), used)) int main() { return 0; }` @@ -144,106 +141,6 @@ func TestDisableStripping(t *testing.T) { } } -func TestCollectTargets(t *testing.T) { - clangArches := make(map[string][]goarch) - linuxArchesLE := make(map[string][]goarch) - linuxArchesBE := make(map[string][]goarch) - for arch, archTarget := range targetByGoArch { - clangArches[archTarget.clang] = append(clangArches[archTarget.clang], arch) - if archTarget.clang == "bpfel" { - linuxArchesLE[archTarget.linux] = append(linuxArchesLE[archTarget.linux], arch) - continue - } - linuxArchesBE[archTarget.linux] = append(linuxArchesBE[archTarget.linux], arch) - } - for i := range clangArches { - slices.Sort(clangArches[i]) - } - for i := range linuxArchesLE { - slices.Sort(linuxArchesLE[i]) - } - for i := range linuxArchesBE { - slices.Sort(linuxArchesBE[i]) - } - - nativeTarget := make(map[target][]goarch) - for arch, archTarget := range targetByGoArch { - if arch == goarch(runtime.GOARCH) { - if archTarget.clang == "bpfel" { - nativeTarget[archTarget] = linuxArchesLE[archTarget.linux] - } else { - nativeTarget[archTarget] = linuxArchesBE[archTarget.linux] - } - break - } - } - - tests := []struct { - targets []string - want map[target][]goarch - }{ - { - []string{"bpf", "bpfel", "bpfeb"}, - map[target][]goarch{ - {"bpf", ""}: nil, - {"bpfel", ""}: clangArches["bpfel"], - {"bpfeb", ""}: clangArches["bpfeb"], - }, - }, - { - []string{"amd64", "386"}, - map[target][]goarch{ - {"bpfel", "x86"}: linuxArchesLE["x86"], - }, - }, - { - []string{"amd64", "ppc64"}, - map[target][]goarch{ - {"bpfeb", "powerpc"}: linuxArchesBE["powerpc"], - {"bpfel", "x86"}: linuxArchesLE["x86"], - }, - }, - { - []string{"native"}, - nativeTarget, - }, - } - - for _, test := range tests { - name := strings.Join(test.targets, ",") - t.Run(name, func(t *testing.T) { - have, err := collectTargets(test.targets) - if err != nil { - t.Fatal(err) - } - - if diff := cmp.Diff(test.want, have); diff != "" { - t.Errorf("Result mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestCollectTargetsErrors(t *testing.T) { - tests := []struct { - name string - target string - }{ - {"unknown", "frood"}, - {"no linux target", "mipsle"}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - _, err := collectTargets([]string{test.target}) - if err == nil { - t.Fatal("Function did not return an error") - } - t.Log("Error message:", err) - }) - } -} - func TestConvertGOARCH(t *testing.T) { tmp := t.TempDir() mustWriteFile(t, tmp, "test.c", @@ -263,7 +160,7 @@ func TestConvertGOARCH(t *testing.T) { outputDir: tmp, } - if err := b2g.convert(targetByGoArch["amd64"], nil); err != nil { + if err := b2g.convert(gen.TargetsByGoArch()["amd64"], nil); err != nil { t.Fatal("Can't target GOARCH:", err) } } @@ -482,54 +379,6 @@ func TestParseArgs(t *testing.T) { }) } -func TestGoarches(t *testing.T) { - exe := goBin(t) - - for goarch := range targetByGoArch { - t.Run(string(goarch), func(t *testing.T) { - goEnv := exec.Command(exe, "env") - goEnv.Env = []string{"GOROOT=/", "GOOS=linux", "GOARCH=" + string(goarch)} - output, err := goEnv.CombinedOutput() - qt.Assert(t, qt.IsNil(err), qt.Commentf("go output is:\n%s", string(output))) - }) - } -} - -func TestClangTargets(t *testing.T) { - exe := goBin(t) - - clangTargets := map[string]struct{}{} - for _, tgt := range targetByGoArch { - clangTargets[tgt.clang] = struct{}{} - } - - for target := range clangTargets { - for _, env := range []string{"GOOS", "GOARCH"} { - env += "=" + target - t.Run(env, func(t *testing.T) { - goEnv := exec.Command(exe, "env") - goEnv.Env = []string{"GOROOT=/", env} - output, err := goEnv.CombinedOutput() - t.Log("go output is:", string(output)) - qt.Assert(t, qt.IsNotNil(err), qt.Commentf("No clang target should be a valid build constraint")) - }) - } - - } -} - -func goBin(t *testing.T) string { - t.Helper() - - exe, err := exec.LookPath("go") - if errors.Is(err, exec.ErrNotFound) { - t.Skip("go binary is not in PATH") - } - qt.Assert(t, qt.IsNil(err)) - - return exe -} - func mustWriteFile(tb testing.TB, dir, name, contents string) { tb.Helper() tmpFile := filepath.Join(dir, name)