blob: 0aca1fd7a4137f7453ddbb7eecbf1c70e54c5c62 [file]
package main
import (
"bytes"
"errors"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
func nogo(args []string) error {
// Parse arguments.
args, _, err := expandParamsFiles(args)
if err != nil {
return err
}
fs := flag.NewFlagSet("GoNogo", flag.ExitOnError)
goenv := envFlags(fs)
var unfilteredSrcs, ignoreSrcs, recompileInternalDeps multiFlag
var deps, facts archiveMultiFlag
var importPath, packagePath, nogoPath, packageListPath, goVersion string
var testFilter string
var outFactsPath, outPath string
var coverMode string
var factsOnly bool
fs.Var(&unfilteredSrcs, "src", ".go, .c, .cc, .m, .mm, .s, or .S file to be filtered and checked")
fs.Var(&ignoreSrcs, "ignore_src", ".go, .c, .cc, .m, .mm, .s, or .S file to be filtered and checked, but with its diagnostics ignored")
fs.Var(&deps, "arc", "Import path, package path, and file name of a direct dependency, separated by '='")
fs.Var(&facts, "facts", "Import path, package path, and file name of a direct dependency's nogo facts file, separated by '='")
fs.BoolVar(&factsOnly, "facts_only", false, "If true, only nogo facts are emitted, no nogo checks are run")
fs.StringVar(&importPath, "importpath", "", "The import path of the package being compiled. Not passed to the compiler, but may be displayed in debug data.")
fs.StringVar(&packagePath, "p", "", "The package path (importmap) of the package being compiled")
fs.StringVar(&packageListPath, "package_list", "", "The file containing the list of standard library packages")
fs.Var(&recompileInternalDeps, "recompile_internal_deps", "The import path of the direct dependencies that needs to be recompiled.")
fs.StringVar(&coverMode, "cover_mode", "", "The coverage mode to use. Empty if coverage instrumentation should not be added.")
fs.StringVar(&testFilter, "testfilter", "off", "Controls test package filtering")
fs.StringVar(&nogoPath, "nogo", "", "The nogo binary")
fs.StringVar(&goVersion, "go_version", "", "The SDK Go version to forward to nogo, without the leading 'go' prefix (for example 1.24.3).")
fs.StringVar(&outFactsPath, "out_facts", "", "The file to emit serialized nogo facts to")
fs.StringVar(&outPath, "out", "", "The path of the directory to emit logs and fixes into")
if err := fs.Parse(args); err != nil {
return err
}
if err := goenv.checkFlagsAndSetGoroot(); err != nil {
return err
}
if importPath == "" {
importPath = packagePath
}
// Filter sources.
srcs, err := filterAndSplitFiles(append(unfilteredSrcs, ignoreSrcs...))
if err != nil {
return err
}
err = applyTestFilter(testFilter, &srcs)
if err != nil {
return err
}
var goSrcs []string
haveCgo := false
for _, src := range srcs.goSrcs {
if src.isCgo {
haveCgo = true
} else {
goSrcs = append(goSrcs, src.filename)
}
}
workDir, cleanup, err := goenv.workDir()
if err != nil {
return err
}
defer cleanup()
compilingWithCgo := os.Getenv("CGO_ENABLED") == "1" && haveCgo
importcfgPath, err := checkImportsAndBuildCfg(goenv, importPath, srcs, deps, packageListPath, recompileInternalDeps, compilingWithCgo, coverMode, workDir)
if err != nil {
return err
}
return runNogo(workDir, nogoPath, goSrcs, ignoreSrcs, facts, factsOnly, importPath, importcfgPath, goVersion, outFactsPath, outPath)
}
func runNogo(workDir string, nogoPath string, srcs, ignores []string, facts []archive, factsOnly bool, packagePath, importcfgPath, goVersion, outFactsPath, outDirPath string) error {
if len(srcs) == 0 {
// emit_compilepkg expects a nogo facts file, even if it's empty.
err := os.WriteFile(outFactsPath, nil, 0o666)
if err != nil {
return fmt.Errorf("error writing empty nogo facts file: %v", err)
}
return nil
}
args := []string{nogoPath}
args = append(args, "-p", packagePath)
args = append(args, "-fix_dir", outDirPath)
args = append(args, "-importcfg", importcfgPath)
if goVersion != "" {
args = append(args, "-go_version", goVersion)
}
for _, fact := range facts {
args = append(args, "-fact", fmt.Sprintf("%s=%s", fact.importPath, fact.file))
}
if factsOnly {
args = append(args, "-facts_only")
}
args = append(args, "-x", outFactsPath)
for _, ignore := range ignores {
args = append(args, "-ignore", ignore)
}
args = append(args, srcs...)
paramsFile := filepath.Join(workDir, "nogo.param")
if err := writeParamsFile(paramsFile, args[1:]); err != nil {
return fmt.Errorf("error writing nogo params file: %v", err)
}
cmd := exec.Command(args[0], "-param="+paramsFile)
out := &bytes.Buffer{}
cmd.Stdout, cmd.Stderr = out, out
err := cmd.Run()
if err == nil {
return nil
}
if exitErr, ok := err.(*exec.ExitError); ok {
if !exitErr.Exited() {
cmdLine := strings.Join(args, " ")
return fmt.Errorf("nogo command '%s' exited unexpectedly: %s", cmdLine, exitErr.String())
}
prettyOut := relativizePaths(out.Bytes())
if exitErr.ExitCode() != nogoViolation {
return errors.New(string(prettyOut))
}
outLog, err := os.Create(filepath.Join(outDirPath, nogoLogBasename))
if err != nil {
return fmt.Errorf("error creating nogo log file: %v", err)
}
defer outLog.Close()
_, err = outLog.Write(prettyOut)
if err != nil {
return fmt.Errorf("error writing nogo log file: %v", err)
}
// Do not fail the action if nogo has findings so that facts are
// still available for downstream targets.
return nil
}
return err
}