pkg.tk-software.de/gotice@v0.4.1-0.20240224130243-6adec687b106/generate.go (about)

     1  // Copyright 2023-2024 Tobias Koch. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"flag"
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  
    14  	"pkg.tk-software.de/gotice/module"
    15  	"pkg.tk-software.de/gotice/notice"
    16  	"pkg.tk-software.de/spartan/io/file"
    17  )
    18  
    19  // GenerateCommand implements the subcommand `generate`.
    20  type GenerateCommand struct {
    21  	fs *flag.FlagSet
    22  
    23  	// The source directory containing the go.mod file.
    24  	srcd string
    25  
    26  	// The destination notice file that shall be created.
    27  	dstf string
    28  }
    29  
    30  // NewGenerateCommand creates and returns the subcommand `generate`.
    31  func NewGenerateCommand() *GenerateCommand {
    32  	cmd := &GenerateCommand{
    33  		fs: flag.NewFlagSet("generate", flag.ContinueOnError),
    34  	}
    35  
    36  	return cmd
    37  }
    38  
    39  // Name returns the name of the subcommand.
    40  func (g *GenerateCommand) Name() string {
    41  	return g.fs.Name()
    42  }
    43  
    44  // Description returns the description of the subcommand.
    45  func (g *GenerateCommand) Description() string {
    46  	return "Generates a notice file"
    47  }
    48  
    49  // Init initializes the subcommand with the given command line arguments.
    50  func (g *GenerateCommand) Init(args []string) error {
    51  	if err := g.fs.Parse(args); err != nil {
    52  		return err
    53  	}
    54  
    55  	if g.fs.NArg() < 2 {
    56  		return ErrMissingArguments
    57  	}
    58  
    59  	g.srcd = g.fs.Arg(0)
    60  	g.dstf = g.fs.Arg(1)
    61  
    62  	return nil
    63  }
    64  
    65  // Usage prints a usage message documenting the subcommand.
    66  func (g *GenerateCommand) Usage() {
    67  	fmt.Println("Usage: gotice generate [project dir] [output file]")
    68  	fmt.Println(g.Description())
    69  	fmt.Println()
    70  }
    71  
    72  // Run executes the subcommand.
    73  func (g *GenerateCommand) Run() error {
    74  	modf := filepath.Join(g.srcd, "go.mod")
    75  
    76  	if !file.Exists(modf) {
    77  		return fmt.Errorf("file %s not found", modf)
    78  	}
    79  
    80  	mods, err := module.NewFromGoModule(g.srcd)
    81  	if err != nil {
    82  		return fmt.Errorf("unable to parse %s: %w", modf, err)
    83  	}
    84  
    85  	opt := readOptionsOrDefault(g.srcd)
    86  
    87  	if err := generate(*mods, *opt, g.srcd, g.dstf); err != nil {
    88  		return err
    89  	}
    90  
    91  	return nil
    92  }
    93  
    94  func generate(mods module.Modules, opt notice.Options, srcd, dstf string) error {
    95  	ns, err := generateNotice(mods)
    96  	if err != nil {
    97  		return err
    98  	}
    99  
   100  	tmpl, err := readTemplate(srcd, opt.Template)
   101  	if err != nil {
   102  		return err
   103  	}
   104  
   105  	if err := writeNotice(dstf, tmpl, opt.Rendering, ns); err != nil {
   106  		return err
   107  	}
   108  
   109  	return nil
   110  }
   111  
   112  func readTemplate(dir, template string) (string, error) {
   113  	switch strings.ToLower(template) {
   114  	case "built-in:txt":
   115  		return notice.TextTemplate, nil
   116  
   117  	case "built-in:md":
   118  		return notice.MarkdownTemplate, nil
   119  
   120  	case "built-in:html":
   121  		return notice.HtmlTemplate, nil
   122  
   123  	default:
   124  		customTemplate := filepath.Join(dir, template)
   125  
   126  		if !file.Exists(customTemplate) {
   127  			return "", fmt.Errorf("template %s not found", template)
   128  		}
   129  
   130  		d, err := os.ReadFile(customTemplate)
   131  		if err != nil {
   132  			return "", err
   133  		}
   134  
   135  		return string(d), nil
   136  	}
   137  }
   138  
   139  func readOptionsOrDefault(d string) *notice.Options {
   140  	f := filepath.Join(d, notice.OptionsFileName)
   141  	if !file.Exists(f) {
   142  		return notice.NewOptions()
   143  	}
   144  
   145  	fh, err := os.Open(f)
   146  	if err != nil {
   147  		return notice.NewOptions()
   148  	}
   149  	defer fh.Close()
   150  
   151  	o, err := notice.ReadOptions(fh)
   152  	if err != nil {
   153  		return notice.NewOptions()
   154  	}
   155  
   156  	return o
   157  }
   158  
   159  func generateNotice(m module.Modules) ([]notice.Notice, error) {
   160  	var ns []notice.Notice
   161  
   162  	for _, mod := range m {
   163  		n := notice.New()
   164  		n.Path = mod.Path
   165  		n.Version = mod.Version
   166  
   167  		lt, err := notice.GetLicenseText(n.Path, n.Version)
   168  		if err != nil {
   169  			return nil, fmt.Errorf("unable to detect license text of %s@%s: %w", n.Path, n.Version, err)
   170  		}
   171  		n.LicenseText = lt
   172  
   173  		ns = append(ns, *n)
   174  	}
   175  
   176  	return ns, nil
   177  }
   178  
   179  func writeNotice(f, tmpl string, r notice.Rendering, n []notice.Notice) error {
   180  	of, err := os.OpenFile(f, os.O_CREATE|os.O_WRONLY, 0666)
   181  	if err != nil {
   182  		return fmt.Errorf("unable to open notice file %s: %w", f, err)
   183  	}
   184  	defer of.Close()
   185  
   186  	switch r {
   187  	case notice.Text:
   188  		if err := notice.WriteText(of, tmpl, n); err != nil {
   189  			return fmt.Errorf("unable to write text notice file %s: %w", f, err)
   190  		}
   191  
   192  	case notice.Html:
   193  		if err := notice.WriteHtml(of, tmpl, n); err != nil {
   194  			return fmt.Errorf("unable to write html notice file %s: %w", f, err)
   195  		}
   196  
   197  	default:
   198  		return fmt.Errorf("invalid rendering %q", r)
   199  	}
   200  
   201  	return nil
   202  }