kythe.io@v0.0.68-0.20240422202219-7225dbc01741/kythe/go/platform/tools/kzip/createcmd/createcmd.go (about)

     1  /*
     2   * Copyright 2019 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 createcmd provides the kzip command for creating simple kzip archives.
    18  package createcmd // import "kythe.io/kythe/go/platform/tools/kzip/createcmd"
    19  
    20  import (
    21  	"context"
    22  	"flag"
    23  	"os"
    24  	"path/filepath"
    25  
    26  	"kythe.io/kythe/go/platform/kzip"
    27  	"kythe.io/kythe/go/platform/tools/kzip/flags"
    28  	"kythe.io/kythe/go/platform/vfs"
    29  	"kythe.io/kythe/go/util/cmdutil"
    30  	"kythe.io/kythe/go/util/flagutil"
    31  	"kythe.io/kythe/go/util/vnameutil"
    32  
    33  	"github.com/google/subcommands"
    34  
    35  	anypb "github.com/golang/protobuf/ptypes/any"
    36  	apb "kythe.io/kythe/proto/analysis_go_proto"
    37  	spb "kythe.io/kythe/proto/storage_go_proto"
    38  )
    39  
    40  type createCommand struct {
    41  	cmdutil.Info
    42  
    43  	output string
    44  	rules  vnameRules
    45  
    46  	uri          kytheURI
    47  	source       flagutil.StringSet
    48  	inputs       flagutil.StringSet
    49  	hasError     bool
    50  	argument     repeatedString
    51  	outputKey    string
    52  	workingDir   string
    53  	entryContext string
    54  	environment  repeatedEnv
    55  	details      repeatedAny
    56  	encoding     flags.EncodingFlag
    57  }
    58  
    59  // New creates a new subcommand for merging kzip files.
    60  func New() subcommands.Command {
    61  	return &createCommand{
    62  		Info: cmdutil.NewInfo("create", "create simple kzip archives", `[options] -- arguments*
    63  
    64  Construct a kzip file written to -output with the vname specified by -uri.
    65  Each of -source_file, -required_input and -details may be specified multiple times with each
    66  occurrence of the flag being appended to the corresponding field in the compilation unit.
    67  Directories specified in -source_file or -required_input will be added recursively.
    68  
    69  Any additional positional arguments are included as arguments in the compilation unit.
    70  `),
    71  		encoding: flags.EncodingFlag{Encoding: kzip.EncodingJSON},
    72  	}
    73  }
    74  
    75  // SetFlags implements the subcommands interface and provides command-specific flags
    76  // for creating kzip files.
    77  func (c *createCommand) SetFlags(fs *flag.FlagSet) {
    78  	fs.StringVar(&c.output, "output", "", "Path for output kzip file (required)")
    79  	fs.Var(&c.rules, "rules", "Path to vnames.json file (optional)")
    80  
    81  	fs.Var(&c.uri, "uri", "A Kythe URI naming the compilation unit VName (required)")
    82  	fs.Var(&c.source, "source_file", "Repeated paths for input source files or directories (required)")
    83  	fs.Var(&c.inputs, "required_input", "Repeated paths for additional required inputs (optional)")
    84  	fs.BoolVar(&c.hasError, "has_compile_errors", false, "Whether this unit had compilation errors (optional)")
    85  	fs.Var(&c.argument, "argument", "Repeated arguments to add to compilation unit (optional)")
    86  	fs.StringVar(&c.outputKey, "output_key", "", "Name by which the output of this compilation is known to dependents (optional)")
    87  	fs.StringVar(&c.workingDir, "working_directory", "", "Absolute path of the directory from which the build tool was invoked (optional)")
    88  	fs.StringVar(&c.entryContext, "entry_context", "", "Language-specific context to provide the indexer (optional)")
    89  	fs.Var(&c.environment, "env", "Repeated KEY=VALUE pairs of environment variables to add to the compilation unit (optional)")
    90  	fs.Var(&c.details, "details", "Repeated JSON-encoded Any messages to embed as compilation details (optional)")
    91  	fs.Var(&c.encoding, "encoding", "Encoding to use on output, one of JSON, PROTO, or ALL")
    92  }
    93  
    94  // Execute implements the subcommands interface and creates the requested file.
    95  func (c *createCommand) Execute(ctx context.Context, fs *flag.FlagSet, _ ...any) subcommands.ExitStatus {
    96  	switch {
    97  	case c.uri.Corpus == "":
    98  		return c.Fail("Missing required -uri")
    99  	case c.output == "":
   100  		return c.Fail("Missing required -output")
   101  	case c.source.Len() == 0:
   102  		return c.Fail("Missing required -source_file")
   103  	}
   104  
   105  	opt := kzip.WithEncoding(c.encoding.Encoding)
   106  	out, err := openWriter(ctx, c.output, opt)
   107  	if err != nil {
   108  		return c.Fail("Error opening -output: %v", err)
   109  	}
   110  
   111  	// Create a new compilation populating its VName with the values specified
   112  	// within the specified Kythe URI.
   113  	cb := compilationBuilder{&apb.CompilationUnit{
   114  		VName: &spb.VName{
   115  			Corpus:    c.uri.Corpus,
   116  			Language:  c.uri.Language,
   117  			Signature: c.uri.Signature,
   118  			Root:      c.uri.Root,
   119  			Path:      c.uri.Path,
   120  		},
   121  		HasCompileErrors: c.hasError,
   122  		Argument:         append(c.argument, fs.Args()...),
   123  		OutputKey:        c.outputKey,
   124  		WorkingDirectory: c.workingDir,
   125  		EntryContext:     c.entryContext,
   126  		Environment:      c.environment.ToProto(),
   127  		Details:          ([]*anypb.Any)(c.details),
   128  	}, out, &c.rules.Rules}
   129  
   130  	sources, err := cb.addFiles(ctx, c.source.Elements())
   131  	if err != nil {
   132  		return c.Fail("Error adding source files: %v", err)
   133  	}
   134  	cb.unit.SourceFile = sources
   135  
   136  	if _, err = cb.addFiles(ctx, c.inputs.Elements()); err != nil {
   137  		return c.Fail("Error adding input files: %v", err)
   138  	}
   139  
   140  	if err := cb.done(); err != nil {
   141  		return c.Fail("Error writing compilation to -output: %v", err)
   142  	}
   143  	return subcommands.ExitSuccess
   144  }
   145  
   146  func openWriter(ctx context.Context, path string, opts ...kzip.WriterOption) (*kzip.Writer, error) {
   147  	out, err := vfs.Create(ctx, path)
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  	return kzip.NewWriteCloser(out, opts...)
   152  }
   153  
   154  type compilationBuilder struct {
   155  	unit  *apb.CompilationUnit
   156  	out   *kzip.Writer
   157  	rules *vnameutil.Rules
   158  }
   159  
   160  // addFiles adds the given files as required input.
   161  // If the path is a directory, its contents are added recursively.
   162  // Returns the paths of the non-directory files added.
   163  func (cb *compilationBuilder) addFiles(ctx context.Context, paths []string) ([]string, error) {
   164  	var files []string
   165  	for _, path := range paths {
   166  		f, err := cb.addFile(ctx, path)
   167  		if err != nil {
   168  			return files, err
   169  		}
   170  		files = append(files, f...)
   171  	}
   172  	return files, nil
   173  }
   174  
   175  // addFile adds the given file as a required input.
   176  // If the path is a directory, its contents are added recursively.
   177  // Returns the paths of the non-directory files added.
   178  func (cb *compilationBuilder) addFile(ctx context.Context, root string) ([]string, error) {
   179  	var files []string
   180  	err := vfs.Walk(ctx, root, func(path string, info os.FileInfo, err error) error {
   181  		if err != nil || info.IsDir() {
   182  			return err
   183  		}
   184  		input, err := vfs.Open(ctx, path)
   185  		if err != nil {
   186  			return err
   187  		}
   188  		defer input.Close()
   189  
   190  		digest, err := cb.out.AddFile(input)
   191  		if err != nil {
   192  			return err
   193  		}
   194  
   195  		path = cb.tryMakeRelative(path)
   196  		vname, ok := cb.rules.Apply(path)
   197  		if !ok {
   198  			vname = &spb.VName{
   199  				Corpus: cb.unit.VName.Corpus,
   200  				Root:   cb.unit.VName.Root,
   201  				Path:   path,
   202  			}
   203  		} else if vname.Corpus == "" {
   204  			vname.Corpus = cb.unit.VName.Corpus
   205  
   206  		}
   207  		cb.unit.RequiredInput = append(cb.unit.RequiredInput, &apb.CompilationUnit_FileInput{
   208  			VName: vname,
   209  			Info: &apb.FileInfo{
   210  				Path:   path,
   211  				Digest: digest,
   212  			},
   213  		})
   214  		files = append(files, path)
   215  		return nil
   216  	})
   217  	return files, err
   218  }
   219  
   220  func (cb *compilationBuilder) done() error {
   221  	_, err := cb.out.AddUnit(cb.unit, nil)
   222  	if err != nil {
   223  		return err
   224  	}
   225  	cb.unit = nil
   226  	return cb.out.Close()
   227  }
   228  
   229  // tryeMakeRelative attempts to relativize path against unit.WorkingDirectory or CWD,
   230  // returning path unmodified on failure.
   231  func (cb *compilationBuilder) tryMakeRelative(path string) string {
   232  	abs, err := filepath.Abs(path)
   233  	if err != nil {
   234  		return path
   235  	}
   236  	var dir string
   237  	if cb.unit.WorkingDirectory != "" {
   238  		dir = cb.unit.WorkingDirectory
   239  	} else {
   240  		dir, err = filepath.Abs(".")
   241  		if err != nil {
   242  			return path
   243  		}
   244  	}
   245  	rel, err := filepath.Rel(dir, abs)
   246  	if err != nil {
   247  		return path
   248  	}
   249  	return rel
   250  
   251  }