kythe.io@v0.0.68-0.20240422202219-7225dbc01741/kythe/go/extractors/config/runextractor/cmakecmd/cmakecmd.go (about)

     1  /*
     2   * Copyright 2018 The Kythe Authors. All rights reserved.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *   http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  // Package cmakecmd extracts from a CMake-based repository.
    18  package cmakecmd // import "kythe.io/kythe/go/extractors/config/runextractor/cmakecmd"
    19  
    20  import (
    21  	"context"
    22  	"flag"
    23  	"fmt"
    24  	"io/ioutil"
    25  	"os"
    26  	"os/exec"
    27  	"path/filepath"
    28  
    29  	"kythe.io/kythe/go/extractors/config/runextractor/compdb"
    30  	"kythe.io/kythe/go/util/cmdutil"
    31  	"kythe.io/kythe/go/util/flagutil"
    32  	"kythe.io/kythe/go/util/log"
    33  
    34  	"github.com/google/subcommands"
    35  )
    36  
    37  type cmakeCommand struct {
    38  	cmdutil.Info
    39  
    40  	extractor      string
    41  	buildDir       string
    42  	sourceDir      string
    43  	extraCmakeArgs flagutil.StringList
    44  }
    45  
    46  // New creates a new subcommand for running cmake extraction.
    47  func New() subcommands.Command {
    48  	return &cmakeCommand{
    49  		Info: cmdutil.NewInfo("cmake", "extract a repo build with CMake", `documight`),
    50  	}
    51  }
    52  
    53  // SetFlags implements the subcommands interface and provides command-specific
    54  // flags for cmake extraction.
    55  func (c *cmakeCommand) SetFlags(fs *flag.FlagSet) {
    56  	fs.StringVar(&c.extractor, "extractor", "", "A required path to the extractor binary to use.")
    57  	fs.StringVar(&c.sourceDir, "sourcedir", ".", "A required path to the repository root. Defaults to the current directory.")
    58  	fs.StringVar(&c.buildDir, "builddir", "", "An optional path to the directory in which to build. If empty, defaults to a unique subdirectory of sourcedir.")
    59  	fs.Var(&c.extraCmakeArgs, "extra_cmake_args", "A comma-separated list of extra arguments to pass to CMake.")
    60  }
    61  
    62  func (c *cmakeCommand) checkFlags() error {
    63  	for _, key := range []string{"KYTHE_CORPUS", "KYTHE_ROOT_DIRECTORY", "KYTHE_OUTPUT_DIRECTORY"} {
    64  		if os.Getenv(key) == "" {
    65  			return fmt.Errorf("required %s not set", key)
    66  		}
    67  	}
    68  	if c.extractor == "" {
    69  		return fmt.Errorf("required -extractor not set")
    70  	}
    71  	if c.sourceDir == "" {
    72  		return fmt.Errorf("required -sourcedir not set")
    73  	}
    74  	return nil
    75  }
    76  
    77  // Execute implements the subcommands interface and runs cmake extraction.
    78  func (c *cmakeCommand) Execute(ctx context.Context, fs *flag.FlagSet, args ...any) subcommands.ExitStatus {
    79  	if err := c.checkFlags(); err != nil {
    80  		return c.Fail("Incorrect flags: %v", err)
    81  	}
    82  	// Since we have to change our working directory, resolve all of our paths early.
    83  	extractor, err := filepath.Abs(c.extractor)
    84  	if err != nil {
    85  		return c.Fail("Unable to resolve path to extractor: %v", err)
    86  	}
    87  	sourceDir, err := filepath.Abs(c.sourceDir)
    88  	if err != nil {
    89  		return c.Fail("Unable to resolve source directory: %v", err)
    90  	}
    91  	var buildDir string
    92  	if c.buildDir == "" {
    93  		// sourceDir is already an absolute directory
    94  		if buildDir, err = ioutil.TempDir(sourceDir, "build-"); err != nil {
    95  			// Unlike below, we need to fail if the "unique" directory exists.
    96  			return c.Fail("Unable to create build directory: %v", err)
    97  		}
    98  		// Only clean up the build directory if it was unspecified.
    99  		defer cleanBuild(buildDir)
   100  	} else {
   101  		buildDir, err = filepath.Abs(c.buildDir)
   102  		if err != nil {
   103  			return c.Fail("Unable to resolve build directory: %v", err)
   104  		}
   105  	}
   106  
   107  	// Create the build directory if it doesn't already exist.
   108  	if err := os.MkdirAll(buildDir, 0755); err != nil {
   109  		return c.Fail("Unable to create build directory: %v", err)
   110  	}
   111  
   112  	// The Kythe extractor is clang based, so tell cmake to use clang so the right
   113  	// commands are generated.
   114  	cmakeArgs := []string{"-DCMAKE_CXX_COMPILER=clang++", "-DCMAKE_C_COMPILER=clang", "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", sourceDir}
   115  	cmakeArgs = append(cmakeArgs, c.extraCmakeArgs...)
   116  	if err := runIn(exec.CommandContext(ctx, "cmake", cmakeArgs...), buildDir); err != nil {
   117  		return c.Fail("Error configuring cmake: %v", err)
   118  	}
   119  
   120  	if err := runIn(exec.CommandContext(ctx, "cmake", "--build", "."), buildDir); err != nil {
   121  		return c.Fail("Error building repository: %v", err)
   122  	}
   123  
   124  	if err := compdb.ExtractCompilations(ctx, extractor, filepath.Join(buildDir, "compile_commands.json"), nil); err != nil {
   125  		return c.Fail("Error extracting repository: %v", err)
   126  	}
   127  	return subcommands.ExitSuccess
   128  }
   129  
   130  // cleanBuild removes dir and all files beneath it, logging an error if it fails.
   131  func cleanBuild(dir string) {
   132  	if err := os.RemoveAll(dir); err != nil {
   133  		log.Errorf("unable to remove build directory: %v", err)
   134  	}
   135  }
   136  
   137  // runIn changes the cmd working directory to dir and calls Run. The cmd's
   138  // stderr is forwarded to stderr.
   139  func runIn(cmd *exec.Cmd, dir string) error {
   140  	cmd.Dir = dir
   141  	cmd.Stderr = os.Stderr
   142  	log.Infof("Running: %q in %q", cmd, cmd.Dir)
   143  	return cmd.Run()
   144  }