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

     1  package bundle
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"path/filepath"
     8  	"runtime"
     9  
    10  	"github.com/pkg/errors"
    11  	"github.com/pterm/pterm"
    12  	"github.com/spf13/cobra"
    13  
    14  	"code-intelligence.com/cifuzz/internal/bundler"
    15  	"code-intelligence.com/cifuzz/internal/cmdutils"
    16  	"code-intelligence.com/cifuzz/internal/cmdutils/logging"
    17  	"code-intelligence.com/cifuzz/internal/cmdutils/resolve"
    18  	"code-intelligence.com/cifuzz/internal/completion"
    19  	"code-intelligence.com/cifuzz/internal/config"
    20  	"code-intelligence.com/cifuzz/pkg/log"
    21  )
    22  
    23  type options struct {
    24  	bundler.Opts `mapstructure:",squash"`
    25  }
    26  
    27  func (opts *options) Validate() error {
    28  	err := config.ValidateBuildSystem(opts.BuildSystem)
    29  	if err != nil {
    30  		log.Error(err)
    31  		return cmdutils.WrapSilentError(err)
    32  	}
    33  
    34  	if opts.BuildSystem == config.BuildSystemNodeJS && !config.AllowUnsupportedPlatforms() {
    35  		err = errors.Errorf(config.NotSupportedErrorMessage("bundle", opts.BuildSystem))
    36  		log.Error(err)
    37  		return cmdutils.WrapSilentError(err)
    38  	}
    39  
    40  	return opts.Opts.Validate()
    41  }
    42  
    43  func New() *cobra.Command {
    44  	return newWithOptions(&options{})
    45  }
    46  
    47  func newWithOptions(opts *options) *cobra.Command {
    48  	var bindFlags func()
    49  	cmd := &cobra.Command{
    50  		Use:   "bundle [flags] [<fuzz test>]...",
    51  		Short: "Bundles fuzz tests into an archive",
    52  		Long: `This command bundles all runtime artifacts required by the
    53  given fuzz tests into a self-contained archive (bundle) that can be executed
    54  on CI Sense.
    55  
    56  The inputs found in the inputs directory of the fuzz test are also added
    57  to the bundle in addition to optional input directories specified with
    58  the seed-corpus flag.
    59  More details about the build system specific inputs directory location
    60  can be found in the help message of the run command.
    61  
    62  The usage of this command depends on the build system
    63  configured for the project.
    64  
    65  This command will select an appropriate Docker image for execution based
    66  on the build system. This can be overridden with a docker-image flag.
    67  
    68  ` + pterm.Style{pterm.Reset, pterm.Bold}.Sprint("CMake") + `
    69    <fuzz test> is the name of the fuzz test defined in the add_fuzz_test
    70    command in your CMakeLists.txt.
    71  
    72    Command completion for the <fuzz test> argument is supported when the
    73    fuzz test was built before or after running 'cifuzz reload'.
    74  
    75    The --build-command flag is ignored.
    76  
    77    Additional CMake arguments can be passed after a "--". For example:
    78  
    79      cifuzz run my_fuzz_test -- -G Ninja
    80  
    81    If no fuzz tests are specified, all fuzz tests are added to the bundle.
    82  
    83  ` + pterm.Style{pterm.Reset, pterm.Bold}.Sprint("Bazel") + `
    84    <fuzz test> is the name of the cc_fuzz_test target as defined in your
    85    BUILD file, either as a relative or absolute Bazel label.
    86  
    87    Command completion for the <fuzz test> argument is supported.
    88  
    89    The '--build-command' flag is ignored.
    90  
    91    Additional Bazel arguments can be passed after a "--". For example:
    92  
    93      cifuzz run my_fuzz_test -- --sandbox_debug
    94  
    95  ` + pterm.Style{pterm.Reset, pterm.Bold}.Sprint("Maven/Gradle") + `
    96    <fuzz test> is the name of the class containing the fuzz test.
    97  
    98    Command completion for the <fuzz test> argument is supported.
    99  
   100    The --build-command flag is ignored.
   101  
   102    If no fuzz tests are specified, all fuzz tests are added to the bundle.
   103  
   104  ` + pterm.Style{pterm.Reset, pterm.Bold}.Sprint("Other build systems") + `
   105    <fuzz test> is either the path or basename of the fuzz test executable
   106    created by the build command. If it's the basename, it will be searched
   107    for recursively in the current working directory.
   108  
   109    A command which builds the fuzz test executable must be provided via
   110    the --build-command flag or the build-command setting in cifuzz.yaml.
   111  
   112    The value specified for <fuzz test> is made available to the build
   113    command in the FUZZ_TEST environment variable. For example:
   114  
   115      echo "build-command: make clean && make \$FUZZ_TEST" >> cifuzz.yaml
   116      cifuzz run my_fuzz_test
   117  
   118    To avoid cleaning the build artifacts after building each fuzz test, you
   119    can provide a clean command using the --clean-command flag or specifying
   120    the "clean-command" option in cifuzz.yaml. The clean command is then
   121    executed once before building the fuzz tests.
   122  
   123  `,
   124  		ValidArgsFunction: completion.ValidFuzzTests,
   125  		Args:              cobra.ArbitraryArgs,
   126  		PreRunE: func(cmd *cobra.Command, args []string) error {
   127  			// Bind viper keys to flags. We can't do this in the New
   128  			// function, because that would re-bind viper keys which
   129  			// were bound to the flags of other commands before.
   130  			bindFlags()
   131  
   132  			err := SetUpBundleLogging(cmd, &opts.Opts)
   133  			if err != nil {
   134  				log.Errorf(err, "Failed to setup logging: %v", err.Error())
   135  				return cmdutils.WrapSilentError(err)
   136  			}
   137  
   138  			var argsToPass []string
   139  			if cmd.ArgsLenAtDash() != -1 {
   140  				argsToPass = args[cmd.ArgsLenAtDash():]
   141  				args = args[:cmd.ArgsLenAtDash()]
   142  			}
   143  
   144  			err = config.FindAndParseProjectConfig(opts)
   145  			if err != nil {
   146  				log.Errorf(err, "Failed to parse cifuzz.yaml: %v", err.Error())
   147  				return cmdutils.WrapSilentError(err)
   148  			}
   149  
   150  			// Fail early if the platform is not supported. Creating the
   151  			// bundle actually works on all platforms, but the backend
   152  			// currently only supports running a bundle on Linux, so the
   153  			// user can't do anything useful with a bundle created on
   154  			// other platforms.
   155  			//
   156  			// We set CIFUZZ_ALLOW_UNSUPPORTED_PLATFORMS in tests to
   157  			// still be able to test that creating the bundle works on
   158  			// all platforms.
   159  			isOSIndependent := opts.BuildSystem == config.BuildSystemMaven ||
   160  				opts.BuildSystem == config.BuildSystemGradle
   161  			if runtime.GOOS != "linux" && !isOSIndependent &&
   162  				!config.AllowUnsupportedPlatforms() {
   163  				err = errors.Errorf(config.NotSupportedErrorMessage("bundle", runtime.GOOS))
   164  				log.Error(err)
   165  				return cmdutils.WrapSilentError(err)
   166  			}
   167  
   168  			fuzzTests, err := resolve.FuzzTestArguments(opts.ResolveSourceFilePath, args, opts.BuildSystem, opts.ProjectDir)
   169  			opts.FuzzTests = fuzzTests
   170  			opts.BuildSystemArgs = argsToPass
   171  
   172  			return opts.Validate()
   173  		},
   174  		RunE: func(c *cobra.Command, args []string) error {
   175  			logging.StartBuildProgressSpinner(log.BundleInProgressMsg)
   176  
   177  			_, err := bundler.New(&opts.Opts).Bundle()
   178  			if err != nil {
   179  				logging.StopBuildProgressSpinnerOnError(log.BundleInProgressErrorMsg)
   180  				var execErr *cmdutils.ExecError
   181  				if errors.As(err, &execErr) {
   182  					// It is expected that some commands might fail due to user
   183  					// configuration so we print the error without the stack trace
   184  					// (in non-verbose mode) and silence it
   185  					log.Error(err)
   186  					return cmdutils.ErrSilent
   187  				}
   188  
   189  				return err
   190  			}
   191  
   192  			logging.StopBuildProgressSpinnerOnSuccess(log.BundleInProgressSuccessMsg, true)
   193  			log.Successf("Successfully created bundle: %s", opts.OutputPath)
   194  
   195  			return nil
   196  		},
   197  	}
   198  
   199  	bindFlags = cmdutils.AddFlags(cmd,
   200  		cmdutils.AddAdditionalFilesFlag,
   201  		cmdutils.AddBranchFlag,
   202  		cmdutils.AddBuildCommandFlag,
   203  		cmdutils.AddCleanCommandFlag,
   204  		cmdutils.AddBuildJobsFlag,
   205  		cmdutils.AddCommitFlag,
   206  		cmdutils.AddDictFlag,
   207  		cmdutils.AddDockerImageFlag,
   208  		cmdutils.AddEngineArgFlag,
   209  		cmdutils.AddEnvFlag,
   210  		cmdutils.AddProjectDirFlag,
   211  		cmdutils.AddSeedCorpusFlag,
   212  		cmdutils.AddTimeoutFlag,
   213  		cmdutils.AddResolveSourceFileFlag,
   214  	)
   215  	cmd.Flags().StringVarP(&opts.OutputPath, "output", "o", "", "Output path of the bundle (.tar.gz)")
   216  
   217  	return cmd
   218  }
   219  
   220  // SetUpBundleLogging configures the verbose log and build log file for the bundle command.
   221  func SetUpBundleLogging(cmd *cobra.Command, opts *bundler.Opts) error {
   222  	var err error
   223  
   224  	logDir, err := logging.CreateLogDir(opts.ProjectDir)
   225  	if err != nil {
   226  		return err
   227  	}
   228  	logSuffix := logging.SuffixForLog(opts.FuzzTests)
   229  	opts.BundleBuildLogFile = filepath.Join(logDir, fmt.Sprintf("%s.log", logSuffix))
   230  
   231  	log.VerboseSecondaryOutput, err = os.OpenFile(opts.BundleBuildLogFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
   232  	if err != nil {
   233  		return err
   234  	}
   235  
   236  	if logging.ShouldLogBuildToFile() {
   237  		var buildStdout io.Writer
   238  		buildStdout, err = logging.BuildOutputToFile(opts.ProjectDir, opts.FuzzTests)
   239  		if err != nil {
   240  			return err
   241  		}
   242  
   243  		opts.BuildStdout = io.MultiWriter(buildStdout, log.VerboseSecondaryOutput)
   244  		opts.BuildStderr = io.MultiWriter(opts.BuildStdout, log.VerboseSecondaryOutput)
   245  		return nil
   246  	}
   247  
   248  	opts.BuildStdout = io.MultiWriter(cmd.OutOrStdout(), log.VerboseSecondaryOutput)
   249  	opts.BuildStderr = io.MultiWriter(cmd.OutOrStderr(), log.VerboseSecondaryOutput)
   250  	return nil
   251  }