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 }