1// Copyright 2019 The Bazel Authors. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15// compilepkg compiles a complete Go package from Go, C, and assembly files. It 16// supports cgo, coverage, and nogo. It is invoked by the Go rules as an action. 17package main 18 19import ( 20 "bytes" 21 "context" 22 "errors" 23 "flag" 24 "fmt" 25 "io/ioutil" 26 "os" 27 "os/exec" 28 "path" 29 "path/filepath" 30 "sort" 31 "strings" 32) 33 34type nogoResult int 35 36const ( 37 nogoNotRun nogoResult = iota 38 nogoError 39 nogoFailed 40 nogoSucceeded 41) 42 43func compilePkg(args []string) error { 44 // Parse arguments. 45 args, _, err := expandParamsFiles(args) 46 if err != nil { 47 return err 48 } 49 50 fs := flag.NewFlagSet("GoCompilePkg", flag.ExitOnError) 51 goenv := envFlags(fs) 52 var unfilteredSrcs, coverSrcs, embedSrcs, embedLookupDirs, embedRoots, recompileInternalDeps multiFlag 53 var deps archiveMultiFlag 54 var importPath, packagePath, nogoPath, packageListPath, coverMode string 55 var outPath, outFactsPath, cgoExportHPath string 56 var testFilter string 57 var gcFlags, asmFlags, cppFlags, cFlags, cxxFlags, objcFlags, objcxxFlags, ldFlags quoteMultiFlag 58 var coverFormat string 59 fs.Var(&unfilteredSrcs, "src", ".go, .c, .cc, .m, .mm, .s, or .S file to be filtered and compiled") 60 fs.Var(&coverSrcs, "cover", ".go file that should be instrumented for coverage (must also be a -src)") 61 fs.Var(&embedSrcs, "embedsrc", "file that may be compiled into the package with a //go:embed directive") 62 fs.Var(&embedLookupDirs, "embedlookupdir", "Root-relative paths to directories relative to which //go:embed directives are resolved") 63 fs.Var(&embedRoots, "embedroot", "Bazel output root under which a file passed via -embedsrc resides") 64 fs.Var(&deps, "arc", "Import path, package path, and file name of a direct dependency, separated by '='") 65 fs.StringVar(&importPath, "importpath", "", "The import path of the package being compiled. Not passed to the compiler, but may be displayed in debug data.") 66 fs.StringVar(&packagePath, "p", "", "The package path (importmap) of the package being compiled") 67 fs.Var(&gcFlags, "gcflags", "Go compiler flags") 68 fs.Var(&asmFlags, "asmflags", "Go assembler flags") 69 fs.Var(&cppFlags, "cppflags", "C preprocessor flags") 70 fs.Var(&cFlags, "cflags", "C compiler flags") 71 fs.Var(&cxxFlags, "cxxflags", "C++ compiler flags") 72 fs.Var(&objcFlags, "objcflags", "Objective-C compiler flags") 73 fs.Var(&objcxxFlags, "objcxxflags", "Objective-C++ compiler flags") 74 fs.Var(&ldFlags, "ldflags", "C linker flags") 75 fs.StringVar(&nogoPath, "nogo", "", "The nogo binary. If unset, nogo will not be run.") 76 fs.StringVar(&packageListPath, "package_list", "", "The file containing the list of standard library packages") 77 fs.StringVar(&coverMode, "cover_mode", "", "The coverage mode to use. Empty if coverage instrumentation should not be added.") 78 fs.StringVar(&outPath, "o", "", "The output archive file to write compiled code") 79 fs.StringVar(&outFactsPath, "x", "", "The output archive file to write export data and nogo facts") 80 fs.StringVar(&cgoExportHPath, "cgoexport", "", "The _cgo_exports.h file to write") 81 fs.StringVar(&testFilter, "testfilter", "off", "Controls test package filtering") 82 fs.StringVar(&coverFormat, "cover_format", "", "Emit source file paths in coverage instrumentation suitable for the specified coverage format") 83 fs.Var(&recompileInternalDeps, "recompile_internal_deps", "The import path of the direct dependencies that needs to be recompiled.") 84 if err := fs.Parse(args); err != nil { 85 return err 86 } 87 if err := goenv.checkFlags(); err != nil { 88 return err 89 } 90 if importPath == "" { 91 importPath = packagePath 92 } 93 cgoEnabled := os.Getenv("CGO_ENABLED") == "1" 94 cc := os.Getenv("CC") 95 outPath = abs(outPath) 96 for i := range unfilteredSrcs { 97 unfilteredSrcs[i] = abs(unfilteredSrcs[i]) 98 } 99 for i := range embedSrcs { 100 embedSrcs[i] = abs(embedSrcs[i]) 101 } 102 103 // Filter sources. 104 srcs, err := filterAndSplitFiles(unfilteredSrcs) 105 if err != nil { 106 return err 107 } 108 109 // TODO(jayconrod): remove -testfilter flag. The test action should compile 110 // the main, internal, and external packages by calling compileArchive 111 // with the correct sources for each. 112 switch testFilter { 113 case "off": 114 case "only": 115 testSrcs := make([]fileInfo, 0, len(srcs.goSrcs)) 116 for _, f := range srcs.goSrcs { 117 if strings.HasSuffix(f.pkg, "_test") { 118 testSrcs = append(testSrcs, f) 119 } 120 } 121 srcs.goSrcs = testSrcs 122 case "exclude": 123 libSrcs := make([]fileInfo, 0, len(srcs.goSrcs)) 124 for _, f := range srcs.goSrcs { 125 if !strings.HasSuffix(f.pkg, "_test") { 126 libSrcs = append(libSrcs, f) 127 } 128 } 129 srcs.goSrcs = libSrcs 130 default: 131 return fmt.Errorf("invalid test filter %q", testFilter) 132 } 133 134 return compileArchive( 135 goenv, 136 importPath, 137 packagePath, 138 srcs, 139 deps, 140 coverMode, 141 coverSrcs, 142 embedSrcs, 143 embedLookupDirs, 144 embedRoots, 145 cgoEnabled, 146 cc, 147 gcFlags, 148 asmFlags, 149 cppFlags, 150 cFlags, 151 cxxFlags, 152 objcFlags, 153 objcxxFlags, 154 ldFlags, 155 nogoPath, 156 packageListPath, 157 outPath, 158 outFactsPath, 159 cgoExportHPath, 160 coverFormat, 161 recompileInternalDeps) 162} 163 164func compileArchive( 165 goenv *env, 166 importPath string, 167 packagePath string, 168 srcs archiveSrcs, 169 deps []archive, 170 coverMode string, 171 coverSrcs []string, 172 embedSrcs []string, 173 embedLookupDirs []string, 174 embedRoots []string, 175 cgoEnabled bool, 176 cc string, 177 gcFlags []string, 178 asmFlags []string, 179 cppFlags []string, 180 cFlags []string, 181 cxxFlags []string, 182 objcFlags []string, 183 objcxxFlags []string, 184 ldFlags []string, 185 nogoPath string, 186 packageListPath string, 187 outPath string, 188 outXPath string, 189 cgoExportHPath string, 190 coverFormat string, 191 recompileInternalDeps []string, 192) error { 193 workDir, cleanup, err := goenv.workDir() 194 if err != nil { 195 return err 196 } 197 defer cleanup() 198 199 // As part of compilation process, rules_go does generate and/or rewrite code 200 // based on the original source files. We should only run static analysis 201 // over original source files and not the generated source as end users have 202 // little control over the generated source. 203 // 204 // nogoSrcsOrigin maps generated/rewritten source files back to original source. 205 // If the original source path is an empty string, exclude generated source from nogo run. 206 nogoSrcsOrigin := make(map[string]string) 207 208 if len(srcs.goSrcs) == 0 { 209 // We need to run the compiler to create a valid archive, even if there's nothing in it. 210 // Otherwise, GoPack will complain if we try to add assembly or cgo objects. 211 // A truly empty archive does not include any references to source file paths, which 212 // ensures hermeticity even though the temp file path is random. 213 emptyGoFile, err := os.CreateTemp(filepath.Dir(outPath), "*.go") 214 if err != nil { 215 return err 216 } 217 defer os.Remove(emptyGoFile.Name()) 218 defer emptyGoFile.Close() 219 if _, err := emptyGoFile.WriteString("package empty\n"); err != nil { 220 return err 221 } 222 if err := emptyGoFile.Close(); err != nil { 223 return err 224 } 225 226 srcs.goSrcs = append(srcs.goSrcs, fileInfo{ 227 filename: emptyGoFile.Name(), 228 ext: goExt, 229 matched: true, 230 pkg: "empty", 231 }) 232 233 nogoSrcsOrigin[emptyGoFile.Name()] = "" 234 } 235 packageName := srcs.goSrcs[0].pkg 236 var goSrcs, cgoSrcs []string 237 for _, src := range srcs.goSrcs { 238 if src.isCgo { 239 cgoSrcs = append(cgoSrcs, src.filename) 240 } else { 241 goSrcs = append(goSrcs, src.filename) 242 } 243 } 244 cSrcs := make([]string, len(srcs.cSrcs)) 245 for i, src := range srcs.cSrcs { 246 cSrcs[i] = src.filename 247 } 248 cxxSrcs := make([]string, len(srcs.cxxSrcs)) 249 for i, src := range srcs.cxxSrcs { 250 cxxSrcs[i] = src.filename 251 } 252 objcSrcs := make([]string, len(srcs.objcSrcs)) 253 for i, src := range srcs.objcSrcs { 254 objcSrcs[i] = src.filename 255 } 256 objcxxSrcs := make([]string, len(srcs.objcxxSrcs)) 257 for i, src := range srcs.objcxxSrcs { 258 objcxxSrcs[i] = src.filename 259 } 260 sSrcs := make([]string, len(srcs.sSrcs)) 261 for i, src := range srcs.sSrcs { 262 sSrcs[i] = src.filename 263 } 264 hSrcs := make([]string, len(srcs.hSrcs)) 265 for i, src := range srcs.hSrcs { 266 hSrcs[i] = src.filename 267 } 268 haveCgo := len(cgoSrcs)+len(cSrcs)+len(cxxSrcs)+len(objcSrcs)+len(objcxxSrcs) > 0 269 270 // Instrument source files for coverage. 271 if coverMode != "" { 272 relCoverPath := make(map[string]string) 273 for _, s := range coverSrcs { 274 relCoverPath[abs(s)] = s 275 } 276 277 combined := append([]string{}, goSrcs...) 278 if cgoEnabled { 279 combined = append(combined, cgoSrcs...) 280 } 281 for i, origSrc := range combined { 282 if _, ok := relCoverPath[origSrc]; !ok { 283 continue 284 } 285 286 var srcName string 287 switch coverFormat { 288 case "go_cover": 289 srcName = origSrc 290 if importPath != "" { 291 srcName = path.Join(importPath, filepath.Base(origSrc)) 292 } 293 case "lcov": 294 // Bazel merges lcov reports across languages and thus assumes 295 // that the source file paths are relative to the exec root. 296 srcName = relCoverPath[origSrc] 297 default: 298 return fmt.Errorf("invalid value for -cover_format: %q", coverFormat) 299 } 300 301 stem := filepath.Base(origSrc) 302 if ext := filepath.Ext(stem); ext != "" { 303 stem = stem[:len(stem)-len(ext)] 304 } 305 coverVar := fmt.Sprintf("Cover_%s_%d_%s", sanitizePathForIdentifier(importPath), i, sanitizePathForIdentifier(stem)) 306 coverVar = strings.ReplaceAll(coverVar, "_", "Z") 307 coverSrc := filepath.Join(workDir, fmt.Sprintf("cover_%d.go", i)) 308 if err := instrumentForCoverage(goenv, origSrc, srcName, coverVar, coverMode, coverSrc); err != nil { 309 return err 310 } 311 312 if i < len(goSrcs) { 313 goSrcs[i] = coverSrc 314 nogoSrcsOrigin[coverSrc] = origSrc 315 continue 316 } 317 318 cgoSrcs[i-len(goSrcs)] = coverSrc 319 } 320 } 321 322 // If we have cgo, generate separate C and go files, and compile the 323 // C files. 324 var objFiles []string 325 if cgoEnabled && haveCgo { 326 // TODO(#2006): Compile .s and .S files with cgo2, not the Go assembler. 327 // If cgo is not enabled or we don't have other cgo sources, don't 328 // compile .S files. 329 var srcDir string 330 srcDir, goSrcs, objFiles, err = cgo2(goenv, goSrcs, cgoSrcs, cSrcs, cxxSrcs, objcSrcs, objcxxSrcs, nil, hSrcs, packagePath, packageName, cc, cppFlags, cFlags, cxxFlags, objcFlags, objcxxFlags, ldFlags, cgoExportHPath) 331 if err != nil { 332 return err 333 } 334 335 gcFlags = append(gcFlags, createTrimPath(gcFlags, srcDir)) 336 } else { 337 if cgoExportHPath != "" { 338 if err := ioutil.WriteFile(cgoExportHPath, nil, 0o666); err != nil { 339 return err 340 } 341 } 342 gcFlags = append(gcFlags, createTrimPath(gcFlags, ".")) 343 } 344 345 // Check that the filtered sources don't import anything outside of 346 // the standard library and the direct dependencies. 347 imports, err := checkImports(srcs.goSrcs, deps, packageListPath, importPath, recompileInternalDeps) 348 if err != nil { 349 return err 350 } 351 if cgoEnabled && len(cgoSrcs) != 0 { 352 // cgo generated code imports some extra packages. 353 imports["runtime/cgo"] = nil 354 imports["syscall"] = nil 355 imports["unsafe"] = nil 356 } 357 if coverMode != "" { 358 if coverMode == "atomic" { 359 imports["sync/atomic"] = nil 360 } 361 const coverdataPath = "github.com/bazelbuild/rules_go/go/tools/coverdata" 362 var coverdata *archive 363 for i := range deps { 364 if deps[i].importPath == coverdataPath { 365 coverdata = &deps[i] 366 break 367 } 368 } 369 if coverdata == nil { 370 return errors.New("coverage requested but coverdata dependency not provided") 371 } 372 imports[coverdataPath] = coverdata 373 } 374 375 // Build an importcfg file for the compiler. 376 importcfgPath, err := buildImportcfgFileForCompile(imports, goenv.installSuffix, filepath.Dir(outPath)) 377 if err != nil { 378 return err 379 } 380 if !goenv.shouldPreserveWorkDir { 381 defer os.Remove(importcfgPath) 382 } 383 384 // Build an embedcfg file mapping embed patterns to filenames. 385 // Embed patterns are relative to any one of a list of root directories 386 // that may contain embeddable files. Source files containing embed patterns 387 // must be in one of these root directories so the pattern appears to be 388 // relative to the source file. Due to transitions, source files can reside 389 // under Bazel roots different from both those of the go srcs and those of 390 // the compilation output. Thus, we have to consider all combinations of 391 // Bazel roots embedsrcs and root-relative paths of source files and the 392 // output binary. 393 var embedRootDirs []string 394 for _, root := range embedRoots { 395 for _, lookupDir := range embedLookupDirs { 396 embedRootDir := abs(filepath.Join(root, lookupDir)) 397 // Since we are iterating over all combinations of roots and 398 // root-relative paths, some resulting paths may not exist and 399 // should be filtered out before being passed to buildEmbedcfgFile. 400 // Since Bazel uniquified both the roots and the root-relative 401 // paths, the combinations are automatically unique. 402 if _, err := os.Stat(embedRootDir); err == nil { 403 embedRootDirs = append(embedRootDirs, embedRootDir) 404 } 405 } 406 } 407 embedcfgPath, err := buildEmbedcfgFile(srcs.goSrcs, embedSrcs, embedRootDirs, workDir) 408 if err != nil { 409 return err 410 } 411 if embedcfgPath != "" { 412 if !goenv.shouldPreserveWorkDir { 413 defer os.Remove(embedcfgPath) 414 } 415 } 416 417 // Run nogo concurrently. 418 var nogoChan chan error 419 outFactsPath := filepath.Join(workDir, nogoFact) 420 nogoSrcs := make([]string, 0, len(goSrcs)) 421 for _, goSrc := range goSrcs { 422 // If source is found in the origin map, that means it's likely to be a generated source file 423 // so feed the original source file to static analyzers instead of the generated one. 424 // 425 // If origin is empty, that means the generated source file is not based on a user-provided source file 426 // thus ignore that entry entirely. 427 if originSrc, ok := nogoSrcsOrigin[goSrc]; ok { 428 if originSrc != "" { 429 nogoSrcs = append(nogoSrcs, originSrc) 430 } 431 continue 432 } 433 434 // TODO(sluongng): most likely what remains here are CGO-generated source files as the result of calling cgo2() 435 // Need to determine whether we want to feed these CGO-generated files into static analyzers. 436 // 437 // Add unknown origin source files into the mix. 438 nogoSrcs = append(nogoSrcs, goSrc) 439 } 440 if nogoPath != "" && len(nogoSrcs) > 0 { 441 ctx, cancel := context.WithCancel(context.Background()) 442 nogoChan = make(chan error) 443 go func() { 444 nogoChan <- runNogo(ctx, workDir, nogoPath, nogoSrcs, deps, packagePath, importcfgPath, outFactsPath) 445 }() 446 defer func() { 447 if nogoChan != nil { 448 cancel() 449 <-nogoChan 450 } 451 }() 452 } 453 454 // If there are assembly files, and this is go1.12+, generate symbol ABIs. 455 asmHdrPath := "" 456 if len(srcs.sSrcs) > 0 { 457 asmHdrPath = filepath.Join(workDir, "go_asm.h") 458 } 459 symabisPath, err := buildSymabisFile(goenv, srcs.sSrcs, srcs.hSrcs, asmHdrPath) 460 if symabisPath != "" { 461 if !goenv.shouldPreserveWorkDir { 462 defer os.Remove(symabisPath) 463 } 464 } 465 if err != nil { 466 return err 467 } 468 469 // Compile the filtered .go files. 470 if err := compileGo(goenv, goSrcs, packagePath, importcfgPath, embedcfgPath, asmHdrPath, symabisPath, gcFlags, outPath); err != nil { 471 return err 472 } 473 474 // Compile the .s files. 475 if len(srcs.sSrcs) > 0 { 476 includeSet := map[string]struct{}{ 477 filepath.Join(os.Getenv("GOROOT"), "pkg", "include"): {}, 478 workDir: {}, 479 } 480 for _, hdr := range srcs.hSrcs { 481 includeSet[filepath.Dir(hdr.filename)] = struct{}{} 482 } 483 includes := make([]string, len(includeSet)) 484 for inc := range includeSet { 485 includes = append(includes, inc) 486 } 487 sort.Strings(includes) 488 for _, inc := range includes { 489 asmFlags = append(asmFlags, "-I", inc) 490 } 491 for i, sSrc := range srcs.sSrcs { 492 obj := filepath.Join(workDir, fmt.Sprintf("s%d.o", i)) 493 if err := asmFile(goenv, sSrc.filename, packagePath, asmFlags, obj); err != nil { 494 return err 495 } 496 objFiles = append(objFiles, obj) 497 } 498 } 499 500 // Pack .o files into the archive. These may come from cgo generated code, 501 // cgo dependencies (cdeps), or assembly. 502 if len(objFiles) > 0 { 503 if err := appendFiles(goenv, outPath, objFiles); err != nil { 504 return err 505 } 506 } 507 508 // Check results from nogo. 509 nogoStatus := nogoNotRun 510 if nogoChan != nil { 511 err := <-nogoChan 512 nogoChan = nil // no cancellation needed 513 if err != nil { 514 nogoStatus = nogoFailed 515 // TODO: should we still create the .x file without nogo facts in this case? 516 return err 517 } 518 nogoStatus = nogoSucceeded 519 } 520 521 // Extract the export data file and pack it in an .x archive together with the 522 // nogo facts file (if there is one). This allows compile actions to depend 523 // on .x files only, so we don't need to recompile a package when one of its 524 // imports changes in a way that doesn't affect export data. 525 // TODO(golang/go#33820): After Go 1.16 is the minimum supported version, 526 // use -linkobj to tell the compiler to create separate .a and .x files for 527 // compiled code and export data. Before that version, the linker needed 528 // export data in the .a file when building a plugin. To work around that, 529 // we copy the export data into .x ourselves. 530 if err = extractFileFromArchive(outPath, workDir, pkgDef); err != nil { 531 return err 532 } 533 pkgDefPath := filepath.Join(workDir, pkgDef) 534 if nogoStatus == nogoSucceeded { 535 return appendFiles(goenv, outXPath, []string{pkgDefPath, outFactsPath}) 536 } 537 return appendFiles(goenv, outXPath, []string{pkgDefPath}) 538} 539 540func compileGo(goenv *env, srcs []string, packagePath, importcfgPath, embedcfgPath, asmHdrPath, symabisPath string, gcFlags []string, outPath string) error { 541 args := goenv.goTool("compile") 542 args = append(args, "-p", packagePath, "-importcfg", importcfgPath, "-pack") 543 if embedcfgPath != "" { 544 args = append(args, "-embedcfg", embedcfgPath) 545 } 546 if asmHdrPath != "" { 547 args = append(args, "-asmhdr", asmHdrPath) 548 } 549 if symabisPath != "" { 550 args = append(args, "-symabis", symabisPath) 551 } 552 args = append(args, gcFlags...) 553 args = append(args, "-o", outPath) 554 args = append(args, "--") 555 args = append(args, srcs...) 556 absArgs(args, []string{"-I", "-o", "-trimpath", "-importcfg"}) 557 return goenv.runCommand(args) 558} 559 560func runNogo(ctx context.Context, workDir string, nogoPath string, srcs []string, deps []archive, packagePath, importcfgPath, outFactsPath string) error { 561 args := []string{nogoPath} 562 args = append(args, "-p", packagePath) 563 args = append(args, "-importcfg", importcfgPath) 564 for _, dep := range deps { 565 args = append(args, "-fact", fmt.Sprintf("%s=%s", dep.importPath, dep.file)) 566 } 567 args = append(args, "-x", outFactsPath) 568 args = append(args, srcs...) 569 570 paramsFile := filepath.Join(workDir, "nogo.param") 571 if err := writeParamsFile(paramsFile, args[1:]); err != nil { 572 return fmt.Errorf("error writing nogo params file: %v", err) 573 } 574 575 cmd := exec.CommandContext(ctx, args[0], "-param="+paramsFile) 576 out := &bytes.Buffer{} 577 cmd.Stdout, cmd.Stderr = out, out 578 if err := cmd.Run(); err != nil { 579 if exitErr, ok := err.(*exec.ExitError); ok { 580 if !exitErr.Exited() { 581 cmdLine := strings.Join(args, " ") 582 return fmt.Errorf("nogo command '%s' exited unexpectedly: %s", cmdLine, exitErr.String()) 583 } 584 return errors.New(string(relativizePaths(out.Bytes()))) 585 } else { 586 if out.Len() != 0 { 587 fmt.Fprintln(os.Stderr, out.String()) 588 } 589 return fmt.Errorf("error running nogo: %v", err) 590 } 591 } 592 return nil 593} 594 595func createTrimPath(gcFlags []string, path string) string { 596 for _, flag := range gcFlags { 597 if strings.HasPrefix(flag, "-trimpath=") { 598 return flag + ":" + path 599 } 600 } 601 602 return "-trimpath=" + path 603} 604 605func sanitizePathForIdentifier(path string) string { 606 return strings.Map(func(r rune) rune { 607 if 'A' <= r && r <= 'Z' || 608 'a' <= r && r <= 'z' || 609 '0' <= r && r <= '9' || 610 r == '_' { 611 return r 612 } 613 return '_' 614 }, path) 615} 616