go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucicfg/cmd/docgen/main.go (about)

     1  // Copyright 2019 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Command docgen is the documentation generator.
    16  //
    17  // It is used by go:generate in "docs" directory. Not really supposed to be
    18  // used standalone.
    19  package main
    20  
    21  import (
    22  	"flag"
    23  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  
    28  	"go.chromium.org/luci/starlark/docgen"
    29  )
    30  
    31  func main() {
    32  	templates := flag.String("templates", "", "a directory with input *.mdt templates")
    33  	out := flag.String("out", "", "where to put generated files")
    34  	starlark := flag.String("starlark", "", "directory with the starlark source code")
    35  
    36  	flag.Parse()
    37  
    38  	if err := run(*templates, *starlark, *out); err != nil {
    39  		fmt.Fprintf(os.Stderr, "docgen: %s\n", err)
    40  		os.Exit(1)
    41  	}
    42  }
    43  
    44  func run(templates, starlark, outDir string) error {
    45  	// All input templates with paths relative to 'templates' dir.
    46  	var files []string
    47  	err := filepath.Walk(templates, func(path string, info os.FileInfo, err error) error {
    48  		if err == nil && strings.HasSuffix(path, ".mdt") {
    49  			var rel string
    50  			if rel, err = filepath.Rel(templates, path); err == nil {
    51  				files = append(files, rel)
    52  			}
    53  		}
    54  		return err
    55  	})
    56  	if err != nil {
    57  		return fmt.Errorf("failed to walk %q - %s", templates, err)
    58  	}
    59  
    60  	// Prepare the generator that can load starlark modules to extract docs from
    61  	// them.
    62  	gen := docgen.Generator{
    63  		Normalize: func(p, s string) (string, error) { return s, nil },
    64  		Starlark:  sourceProvider(starlark),
    65  	}
    66  
    67  	// For each input template spit out the corresponding generated *.md file.
    68  	haveErrs := false
    69  	for _, f := range files {
    70  		in := filepath.Join(templates, f)
    71  		out := filepath.Join(outDir, f)
    72  
    73  		// Replace ".mdt" with ".md".
    74  		out = out[:len(out)-1]
    75  
    76  		fmt.Printf("Rendering %s\n", f)
    77  		if err := generate(&gen, in, out); err != nil {
    78  			fmt.Fprintf(os.Stderr, "%s\n", err)
    79  			haveErrs = true
    80  		}
    81  	}
    82  
    83  	if haveErrs {
    84  		return fmt.Errorf("failed")
    85  	}
    86  	return nil
    87  }
    88  
    89  func generate(gen *docgen.Generator, in, out string) error {
    90  	body, err := os.ReadFile(in)
    91  	if err != nil {
    92  		return err
    93  	}
    94  	output, err := gen.Render(string(body))
    95  	if err != nil {
    96  		return err
    97  	}
    98  	return os.WriteFile(out, output, 0666)
    99  }
   100  
   101  func sourceProvider(root string) func(string) (string, error) {
   102  	return func(module string) (src string, err error) {
   103  		// 'module' here is something like "@stdlib//path".
   104  		chunks := strings.SplitN(module, "//", 2)
   105  		if len(chunks) != 2 || !strings.HasPrefix(chunks[0], "@") {
   106  			return "", fmt.Errorf("unrecognized module path %s", module)
   107  		}
   108  		pkg := chunks[0][1:]
   109  		path := chunks[1]
   110  
   111  		// @proto package is not explorable for now. Can potentially auto-generate
   112  		// it from proto descriptors, if necessary.
   113  		if pkg == "proto" {
   114  			return "", nil
   115  		}
   116  
   117  		fmt.Printf("  loading %q\n", module)
   118  		body, err := os.ReadFile(filepath.Join(root, pkg, path))
   119  		if err != nil {
   120  			return "", err
   121  		}
   122  		return string(body), nil
   123  	}
   124  }