| // Copyright 2020 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| //go:build go1.15 |
| // +build go1.15 |
| |
| /*Command godocfx generates DocFX YAML for Go code. |
| |
| Usage: |
| |
| godocfx [flags] path |
| |
| # New modules with the given prefix. Delete any previous output. |
| godocfx -rm -project my-project -new-modules cloud.google.com/go |
| # Process a single module @latest. |
| godocfx cloud.google.com/go |
| # Process and print, instead of save. |
| godocfx -print cloud.google.com/go/storage@latest |
| # Change output directory. |
| godocfx -out custom/output/dir cloud.google.com/go |
| |
| See: |
| * https://dotnet.github.io/docfx/spec/metadata_format_spec.html |
| * https://github.com/googleapis/doc-templates |
| * https://github.com/googleapis/doc-pipeline |
| */ |
| package main |
| |
| import ( |
| "context" |
| "flag" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "log" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strings" |
| "time" |
| |
| "golang.org/x/tools/go/packages" |
| "gopkg.in/yaml.v2" |
| ) |
| |
| func main() { |
| print := flag.Bool("print", false, "Print instead of save (default false)") |
| rm := flag.Bool("rm", false, "Delete out directory before generating") |
| outDir := flag.String("out", "obj/api", "Output directory (default obj/api)") |
| projectID := flag.String("project", "", "Project ID to use. Required when using -new-modules.") |
| newMods := flag.Bool("new-modules", false, "Process all new modules with the given prefix. Uses timestamp in Datastore. Stores results in $out/$mod.") |
| // TODO: flag to set output URL path |
| |
| log.SetPrefix("[godocfx] ") |
| |
| flag.Parse() |
| if flag.NArg() == 0 { |
| log.Fatalf("%s missing required argument: module path/prefix", os.Args[0]) |
| } |
| |
| modNames := flag.Args() |
| var mods []indexEntry |
| if *newMods { |
| if *projectID == "" { |
| log.Fatal("Must set -project when using -new-modules") |
| } |
| var err error |
| mods, err = newModules(context.Background(), indexClient{}, &dsTimeSaver{projectID: *projectID}, modNames) |
| if err != nil { |
| log.Fatal(err) |
| } |
| } else { |
| for _, mod := range modNames { |
| modPath := mod |
| version := "latest" |
| if strings.Contains(mod, "@") { |
| parts := strings.Split(mod, "@") |
| if len(parts) != 2 { |
| log.Fatal("module arg expected only one '@'") |
| } |
| modPath = parts[0] |
| version = parts[1] |
| } |
| modPath = strings.TrimSuffix(modPath, "/...") // No /... needed. |
| mods = append(mods, indexEntry{ |
| Path: modPath, |
| Version: version, |
| }) |
| } |
| } |
| |
| if *rm { |
| os.RemoveAll(*outDir) |
| } |
| if len(mods) == 0 { |
| log.Println("No new modules to process") |
| return |
| } |
| // Create a temp module so we can get the exact version asked for. |
| workingDir, err := ioutil.TempDir("", "godocfx-*") |
| if err != nil { |
| log.Fatalf("ioutil.TempDir: %v", err) |
| } |
| // Use a fake module that doesn't start with cloud.google.com/go. |
| runCmd(workingDir, "go", "mod", "init", "cloud.google.com/lets-build-some-docs") |
| |
| failed := false |
| for _, m := range mods { |
| log.Printf("Processing %s@%s", m.Path, m.Version) |
| |
| // Always output to specific directory. |
| path := filepath.Join(*outDir, fmt.Sprintf("%s@%s", m.Path, m.Version)) |
| if err := process(m, workingDir, path, *print); err != nil { |
| log.Printf("Failed to process %v: %v", m, err) |
| failed = true |
| } |
| log.Printf("Done with %s@%s", m.Path, m.Version) |
| } |
| if failed { |
| os.Exit(1) |
| } |
| } |
| |
| func runCmd(dir, name string, args ...string) error { |
| log.Printf("> [%s] %s %s", dir, name, strings.Join(args, " ")) |
| cmd := exec.Command(name, args...) |
| cmd.Dir = dir |
| cmd.Stdout = os.Stdout |
| cmd.Stderr = os.Stderr |
| if err := cmd.Start(); err != nil { |
| return fmt.Errorf("Start: %v", err) |
| } |
| if err := cmd.Wait(); err != nil { |
| return fmt.Errorf("Wait: %s", err) |
| } |
| return nil |
| } |
| |
| func process(mod indexEntry, workingDir, outDir string, print bool) error { |
| filter := []string{ |
| "cloud.google.com/go/analytics", |
| "cloud.google.com/go/area120", |
| "cloud.google.com/go/gsuiteaddons", |
| |
| "google.golang.org/appengine/v2/cmd", |
| } |
| if hasPrefix(mod.Path, filter) { |
| log.Printf("%q filtered out, nothing to do: here is the filter: %q", mod.Path, filter) |
| return nil |
| } |
| |
| // Be sure to get the module and run the module loader in the tempDir. |
| if err := runCmd(workingDir, "go", "mod", "tidy"); err != nil { |
| return fmt.Errorf("go mod tidy error: %v", err) |
| } |
| // Don't do /... because it fails on submodules. |
| if err := runCmd(workingDir, "go", "get", "-d", "-t", mod.Path+"@"+mod.Version); err != nil { |
| return fmt.Errorf("go get %s@%s: %v", mod.Path, mod.Version, err) |
| } |
| |
| log.Println("Starting to parse") |
| optionalExtraFiles := []string{} |
| r, err := parse(mod.Path+"/...", workingDir, optionalExtraFiles, filter) |
| if err != nil { |
| return fmt.Errorf("parse: %v", err) |
| } |
| |
| if print { |
| if err := yaml.NewEncoder(os.Stdout).Encode(r.pages); err != nil { |
| return fmt.Errorf("Encode: %v", err) |
| } |
| fmt.Println("----- toc.yaml") |
| if err := yaml.NewEncoder(os.Stdout).Encode(r.toc); err != nil { |
| return fmt.Errorf("Encode: %v", err) |
| } |
| return nil |
| } |
| |
| if err := write(outDir, r); err != nil { |
| log.Fatalf("write: %v", err) |
| } |
| return nil |
| } |
| |
| func write(outDir string, r *result) error { |
| if err := os.MkdirAll(outDir, os.ModePerm); err != nil { |
| return fmt.Errorf("os.MkdirAll: %v", err) |
| } |
| for path, p := range r.pages { |
| // Make the module root page the index. |
| if path == r.module.Path { |
| path = "index" |
| } |
| // Trim the module path from all other paths. |
| path = strings.TrimPrefix(path, r.module.Path+"/") |
| path = filepath.Join(outDir, path+".yml") |
| if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { |
| return fmt.Errorf("os.MkdirAll: %v", err) |
| } |
| f, err := os.Create(path) |
| if err != nil { |
| return err |
| } |
| defer f.Close() |
| fmt.Fprintln(f, "### YamlMime:UniversalReference") |
| if err := yaml.NewEncoder(f).Encode(p); err != nil { |
| return err |
| } |
| |
| path = filepath.Join(outDir, "toc.yml") |
| f, err = os.Create(path) |
| if err != nil { |
| return err |
| } |
| defer f.Close() |
| fmt.Fprintln(f, "### YamlMime:TableOfContent") |
| if err := yaml.NewEncoder(f).Encode(r.toc); err != nil { |
| return err |
| } |
| } |
| |
| for _, ef := range r.extraFiles { |
| src, err := os.Open(filepath.Join(r.module.Dir, ef.srcRelativePath)) |
| if err != nil { |
| return err |
| } |
| dst, err := os.Create(filepath.Join(outDir, ef.dstRelativePath)) |
| if err != nil { |
| return err |
| } |
| if _, err := io.Copy(dst, src); err != nil { |
| return nil |
| } |
| } |
| |
| // Write the docuploader docs.metadata file. Not for DocFX. |
| // See https://github.com/googleapis/docuploader/issues/11. |
| // Example: |
| /* |
| update_time { |
| seconds: 1600048103 |
| nanos: 183052000 |
| } |
| name: "cloud.google.com/go" |
| version: "v0.65.0" |
| language: "go" |
| */ |
| path := filepath.Join(outDir, "docs.metadata") |
| f, err := os.Create(path) |
| if err != nil { |
| return err |
| } |
| defer f.Close() |
| now := time.Now().UTC() |
| writeMetadata(f, now, r.module) |
| return nil |
| } |
| |
| func writeMetadata(w io.Writer, now time.Time, module *packages.Module) { |
| fmt.Fprintf(w, `update_time { |
| seconds: %d |
| nanos: %d |
| } |
| name: %q |
| version: %q |
| language: "go" |
| `, now.Unix(), now.Nanosecond(), module.Path, module.Version) |
| |
| // Some modules specify a different path to serve from. |
| // The URL will be /[stem]/[version]/[pkg path relative to module]. |
| // Alternatively, we could plumb this through command line flags. |
| switch module.Path { |
| case "google.golang.org/appengine": |
| fmt.Fprintf(w, "stem: \"/appengine/docs/standard/go111/reference\"\n") |
| case "google.golang.org/appengine/v2": |
| fmt.Fprintf(w, "stem: \"/appengine/docs/standard/go/reference/services/bundled\"\n") |
| } |
| } |