code-intelligence.com/cifuzz@v0.40.0/internal/cmd/coverage/bazel/bazel.go (about)

     1  package bazel
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"os/exec"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"github.com/pkg/errors"
    12  
    13  	"code-intelligence.com/cifuzz/internal/build"
    14  	"code-intelligence.com/cifuzz/internal/build/bazel"
    15  	"code-intelligence.com/cifuzz/internal/cmd/coverage/summary"
    16  	"code-intelligence.com/cifuzz/internal/cmdutils"
    17  	"code-intelligence.com/cifuzz/pkg/log"
    18  	"code-intelligence.com/cifuzz/pkg/runfiles"
    19  	"code-intelligence.com/cifuzz/util/envutil"
    20  )
    21  
    22  type CoverageGenerator struct {
    23  	FuzzTest        string
    24  	OutputFormat    string
    25  	OutputPath      string
    26  	BuildSystemArgs []string
    27  	ProjectDir      string
    28  	Engine          string
    29  	NumJobs         uint
    30  	Stdout          io.Writer
    31  	Stderr          io.Writer
    32  	BuildStdout     io.Writer
    33  	BuildStderr     io.Writer
    34  	Verbose         bool
    35  }
    36  
    37  func (cov *CoverageGenerator) BuildFuzzTestForCoverage() error {
    38  	var err error
    39  
    40  	// The cc_fuzz_test rule defines multiple bazel targets: If the
    41  	// name is "foo", it defines the targets "foo", "foo_bin", and
    42  	// others. We need to run the "foo" target here but want to
    43  	// allow users to specify either "foo" or "foo_bin", so we check
    44  	// if the fuzz test name  with a "_bin" suffix removed is a valid
    45  	// target and use that in that case.
    46  	if strings.HasSuffix(cov.FuzzTest, "_bin") {
    47  		trimmedLabel := strings.TrimSuffix(cov.FuzzTest, "_bin")
    48  		cmd := exec.Command("bazel", "query", trimmedLabel)
    49  		err = cmd.Run()
    50  		if err == nil {
    51  			cov.FuzzTest = trimmedLabel
    52  		}
    53  	}
    54  
    55  	commonFlags, err := cov.getBazelCommandFlags()
    56  	if err != nil {
    57  		return err
    58  	}
    59  
    60  	// Flags which should only be used for bazel run because they are
    61  	// not supported by the other bazel commands we use
    62  	coverageFlags := []string{
    63  		// Build with debug symbols
    64  		"-c", "opt", "--copt", "-g",
    65  		// Disable source fortification, which is currently not supported
    66  		// in combination with ASan, see https://github.com/google/sanitizers/issues/247
    67  		"--copt", "-U_FORTIFY_SOURCE",
    68  		// Build with the rules_fuzzing replayer
    69  		"--@rules_fuzzing//fuzzing:cc_engine=@rules_fuzzing//fuzzing/engines:replay",
    70  		"--@rules_fuzzing//fuzzing:cc_engine_instrumentation=none",
    71  		"--@rules_fuzzing//fuzzing:cc_engine_sanitizer=none",
    72  		"--instrument_test_targets",
    73  		"--combined_report=lcov",
    74  		"--experimental_use_llvm_covmap",
    75  		"--experimental_generate_llvm_lcov",
    76  		"--verbose_failures",
    77  	}
    78  	if os.Getenv("BAZEL_SUBCOMMANDS") != "" {
    79  		coverageFlags = append(coverageFlags, "--subcommands")
    80  	}
    81  
    82  	args := []string{"coverage"}
    83  	args = append(args, commonFlags...)
    84  	args = append(args, coverageFlags...)
    85  	args = append(args, cov.BuildSystemArgs...)
    86  	args = append(args, cov.FuzzTest)
    87  
    88  	cmd := exec.Command("bazel", args...)
    89  	// Redirect the build command's stdout to stderr to only have
    90  	// reports printed to stdout
    91  	cmd.Stdout = cov.BuildStdout
    92  	cmd.Stderr = cov.BuildStderr
    93  	log.Debugf("Command: %s", cmd.String())
    94  	err = cmd.Run()
    95  	if err != nil {
    96  		return cmdutils.WrapExecError(errors.WithStack(err), cmd)
    97  	}
    98  
    99  	return nil
   100  }
   101  
   102  func (cov *CoverageGenerator) GenerateCoverageReport() (string, error) {
   103  	// Get the path of the created lcov report
   104  	cmd := exec.Command("bazel", "info", "output_path")
   105  	out, err := cmd.Output()
   106  	if err != nil {
   107  		return "", cmdutils.WrapExecError(errors.WithStack(err), cmd)
   108  	}
   109  	bazelOutputDir := strings.TrimSpace(string(out))
   110  	reportPath := filepath.Join(bazelOutputDir, "_coverage", "_coverage_report.dat")
   111  
   112  	log.Debugf("Parsing lcov report %s", reportPath)
   113  
   114  	lcovReportContent, err := os.ReadFile(reportPath)
   115  	if err != nil {
   116  		return "", errors.WithStack(err)
   117  	}
   118  	reportReader := strings.NewReader(string(lcovReportContent))
   119  	summary.ParseLcov(reportReader).PrintTable(cov.Stderr)
   120  
   121  	commonFlags, err := cov.getBazelCommandFlags()
   122  	if err != nil {
   123  		return "", err
   124  	}
   125  
   126  	if cov.OutputFormat == "lcov" {
   127  		if cov.OutputPath == "" {
   128  			path, err := bazel.PathFromLabel(cov.FuzzTest, commonFlags)
   129  			if err != nil {
   130  				return "", err
   131  			}
   132  			name := strings.ReplaceAll(path, "/", "-")
   133  			cov.OutputPath = name + ".coverage.lcov"
   134  		}
   135  		// We don't use copy.Copy here to be able to set the permissions
   136  		// to 0o644 before umask - copy.Copy just copies the permissions
   137  		// from the source file, which has permissions 555 like all
   138  		// files created by bazel.
   139  		content, err := os.ReadFile(reportPath)
   140  		if err != nil {
   141  			return "", errors.WithStack(err)
   142  		}
   143  		err = os.WriteFile(cov.OutputPath, content, 0o644)
   144  		if err != nil {
   145  			return "", errors.WithStack(err)
   146  		}
   147  		return cov.OutputPath, nil
   148  	}
   149  
   150  	// If no output path was specified, create the coverage report in a
   151  	// temporary directory
   152  	if cov.OutputPath == "" {
   153  		outputDir, err := os.MkdirTemp("", "coverage-")
   154  		if err != nil {
   155  			return "", errors.WithStack(err)
   156  		}
   157  		path, err := bazel.PathFromLabel(cov.FuzzTest, commonFlags)
   158  		if err != nil {
   159  			return "", err
   160  		}
   161  		cov.OutputPath = filepath.Join(outputDir, path)
   162  	}
   163  
   164  	// Create an HTML report via genhtml
   165  	genHTML, err := runfiles.Finder.GenHTMLPath()
   166  	if err != nil {
   167  		return "", err
   168  	}
   169  	args := []string{"--output", cov.OutputPath, reportPath}
   170  
   171  	cmd = exec.Command(genHTML, args...)
   172  	cmd.Dir = cov.ProjectDir
   173  	cmd.Stderr = os.Stderr
   174  	log.Debugf("Command: %s", cmd.String())
   175  	err = cmd.Run()
   176  	if err != nil {
   177  		return "", errors.WithStack(err)
   178  	}
   179  
   180  	return cov.OutputPath, nil
   181  }
   182  
   183  // getBazelCommandFlags returns flags to be used when executing a bazel command
   184  // to avoid part of the loading and/or analysis phase to rerun.
   185  func (cov *CoverageGenerator) getBazelCommandFlags() ([]string, error) {
   186  	env, err := build.CommonBuildEnv()
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  
   191  	flags := []string{
   192  		"--repo_env=CC=" + envutil.Getenv(env, "CC"),
   193  		"--repo_env=CXX" + envutil.Getenv(env, "CXX"),
   194  		// Don't use the LLVM from Xcode
   195  		"--repo_env=BAZEL_USE_CPP_ONLY_TOOLCHAIN=1",
   196  	}
   197  	if cov.NumJobs != 0 {
   198  		flags = append(flags, "--jobs", fmt.Sprint(cov.NumJobs))
   199  	}
   200  
   201  	llvmCov, err := runfiles.Finder.LLVMCovPath()
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  	llvmProfData, err := runfiles.Finder.LLVMProfDataPath()
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  	flags = append(flags,
   210  		"--repo_env=BAZEL_USE_LLVM_NATIVE_COVERAGE=1",
   211  		"--repo_env=BAZEL_LLVM_COV="+llvmCov,
   212  		"--repo_env=BAZEL_LLVM_PROFDATA="+llvmProfData,
   213  		"--repo_env=GCOV="+llvmProfData,
   214  	)
   215  
   216  	return flags, nil
   217  }