kythe.io@v0.0.68-0.20240422202219-7225dbc01741/kythe/go/platform/tools/kzip/viewcmd/viewcmd.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 viewcmd displays the contents of compilation units stored in .kzip files.
    18  package viewcmd // import "kythe.io/kythe/go/platform/tools/kzip/viewcmd"
    19  
    20  import (
    21  	"context"
    22  	"flag"
    23  	"fmt"
    24  	"io/ioutil"
    25  	"os"
    26  	"path/filepath"
    27  	"strings"
    28  	"time"
    29  
    30  	"kythe.io/kythe/go/platform/kzip"
    31  	"kythe.io/kythe/go/platform/vfs"
    32  	"kythe.io/kythe/go/util/cmdutil"
    33  	"kythe.io/kythe/go/util/log"
    34  
    35  	"github.com/google/subcommands"
    36  	"golang.org/x/sync/errgroup"
    37  	"golang.org/x/sync/semaphore"
    38  	"google.golang.org/protobuf/encoding/protojson"
    39  	"google.golang.org/protobuf/proto"
    40  )
    41  
    42  type cmd struct {
    43  	cmdutil.Info
    44  	extractDir string
    45  }
    46  
    47  // New returns an implementation of the "view" subcommand.
    48  func New() subcommands.Command {
    49  	const usage = `Usage: view [options] <file-path>...
    50  
    51  Print or extract compilation units stored in .kzip files.  By default, the
    52  compilation unit is printed to stdout in JSON format.
    53  
    54  With -extract the compilation record and the full contents of all the required
    55  input files are extracted into the named directory, preserving the path
    56  structure specified in the compilation record.`
    57  
    58  	return &cmd{
    59  		Info: cmdutil.NewInfo("view", "view compilation units stored in .kzip files", usage),
    60  	}
    61  }
    62  
    63  // SetFlags implements part of subcommands.Command.
    64  func (c *cmd) SetFlags(fs *flag.FlagSet) {
    65  	fs.StringVar(&c.extractDir, "extract", "", "Extract files to this directory")
    66  }
    67  
    68  // Execute implements part of subcommands.Command.
    69  func (c *cmd) Execute(ctx context.Context, fs *flag.FlagSet, args ...any) subcommands.ExitStatus {
    70  	var hasErrors bool
    71  	for _, path := range fs.Args() {
    72  		ext := filepath.Ext(path)
    73  		base := filepath.Base(strings.TrimSuffix(path, ext))
    74  
    75  		f, err := vfs.Open(ctx, path)
    76  		if err != nil {
    77  			log.ErrorContextf(ctx, "opening .kzip file: %v", err)
    78  			hasErrors = true
    79  			continue
    80  		}
    81  		defer f.Close()
    82  
    83  		err = kzip.Scan(f, func(r *kzip.Reader, unit *kzip.Unit) error {
    84  			if err := c.writeUnit(base+"-"+unit.Digest, unit.Proto); err != nil {
    85  				return fmt.Errorf("writing unit: %v", err)
    86  			} else if fd, err := c.kzipFiles(r, unit); err != nil {
    87  				return fmt.Errorf("extracting files: %v", err)
    88  			} else {
    89  				return c.writeFiles(fd)
    90  			}
    91  		})
    92  		if err != nil {
    93  			log.ErrorContextf(ctx, "scanning .kzip file: %v", err)
    94  			hasErrors = true
    95  		}
    96  	}
    97  	if hasErrors {
    98  		return subcommands.ExitFailure
    99  	}
   100  	return subcommands.ExitSuccess
   101  }
   102  
   103  var marshaler = &protojson.MarshalOptions{UseProtoNames: true}
   104  
   105  func (c *cmd) writeUnit(base string, msg proto.Message) error {
   106  	if c.extractDir == "" {
   107  		rec, err := marshaler.Marshal(msg)
   108  		if err != nil {
   109  			return err
   110  		}
   111  		_, err = os.Stdout.Write(rec)
   112  		if err == nil {
   113  			fmt.Println()
   114  		}
   115  		return err
   116  	}
   117  	if err := os.MkdirAll(c.extractDir, 0750); err != nil {
   118  		return fmt.Errorf("creating output directory: %v", err)
   119  	}
   120  	f, err := os.Create(filepath.Join(c.extractDir, base+".unit"))
   121  	if err != nil {
   122  		return err
   123  	}
   124  	rec, err := marshaler.Marshal(msg)
   125  	if err != nil {
   126  		f.Close()
   127  		return err
   128  	}
   129  	_, err = f.Write(rec)
   130  	cerr := f.Close()
   131  	if err != nil {
   132  		return err
   133  	}
   134  	return cerr
   135  }
   136  
   137  func (c *cmd) writeFiles(fd []fileData) error {
   138  	if c.extractDir == "" {
   139  		return nil
   140  	}
   141  	g, ctx := errgroup.WithContext(context.Background())
   142  	start := time.Now()
   143  	defer func() { log.Infof("Extracted %d files in %v", len(fd), time.Since(start)) }()
   144  	sem := semaphore.NewWeighted(64) // limit concurrency on large compilations
   145  	for _, file := range fd {
   146  		file := file
   147  		g.Go(func() error {
   148  			sem.Acquire(ctx, 1)
   149  			defer sem.Release(1)
   150  
   151  			path := filepath.Join(c.extractDir, file.path)
   152  			dir := filepath.Dir(path)
   153  			if err := os.MkdirAll(dir, 0750); err != nil {
   154  				return err
   155  			}
   156  			return ioutil.WriteFile(path, file.data, 0644)
   157  		})
   158  	}
   159  	return g.Wait()
   160  }
   161  
   162  type fileData struct {
   163  	path string
   164  	data []byte
   165  }
   166  
   167  // kzipFiles extracts the file paths and contents from a kzip compilation.
   168  func (c *cmd) kzipFiles(r *kzip.Reader, unit *kzip.Unit) ([]fileData, error) {
   169  	out := make([]fileData, len(unit.Proto.RequiredInput))
   170  	for i, ri := range unit.Proto.RequiredInput {
   171  		info := ri.GetInfo()
   172  		out[i].path = info.GetPath()
   173  		data, err := r.ReadAll(info.GetDigest())
   174  		if err != nil {
   175  			return nil, err
   176  		}
   177  		out[i].data = data
   178  	}
   179  	return out, nil
   180  }