go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucicfg/cli/cmds/generate/generate.go (about) 1 // Copyright 2018 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 // Package generate implements 'generate' subcommand. 16 package generate 17 18 import ( 19 "context" 20 "fmt" 21 "os" 22 "path/filepath" 23 24 "github.com/bazelbuild/buildtools/build" 25 "github.com/maruel/subcommands" 26 27 "go.chromium.org/luci/common/cli" 28 "go.chromium.org/luci/common/logging" 29 30 "go.chromium.org/luci/lucicfg" 31 "go.chromium.org/luci/lucicfg/buildifier" 32 "go.chromium.org/luci/lucicfg/cli/base" 33 ) 34 35 // Cmd is 'generate' subcommand. 36 func Cmd(params base.Parameters) *subcommands.Command { 37 return &subcommands.Command{ 38 UsageLine: "generate SCRIPT", 39 ShortDesc: "interprets a high-level config, generating *.cfg files", 40 LongDesc: `Interprets a high-level config, generating *.cfg files. 41 42 Writes generated configs to the directory given via -config-dir or via 43 lucicfg.config(config_dir=...) statement in the script. If it is '-', just 44 prints them to stdout. 45 46 If -validate is given, sends the generated config to LUCI Config service for 47 validation. This can also be done separately via 'validate' subcommand. 48 49 If the generation stage fails, doesn't overwrite any files on disk. If the 50 generation succeeds, but the validation fails, the new generated files are kept 51 on disk, so they can be manually examined for reasons they are invalid. 52 `, 53 CommandRun: func() subcommands.CommandRun { 54 gr := &generateRun{} 55 gr.Init(params) 56 gr.AddGeneratorFlags() 57 gr.Flags.BoolVar(&gr.force, "force", false, "Rewrite existing output files on disk even if they are semantically equal to generated ones") 58 gr.Flags.BoolVar(&gr.validate, "validate", false, "Validate the generate configs by sending them to LUCI Config") 59 gr.Flags.StringVar(&gr.emitToStdout, "emit-to-stdout", "", 60 "When set to a path, keep generated configs in memory (don't touch disk) and just emit this single config file to stdout") 61 return gr 62 }, 63 } 64 } 65 66 type generateRun struct { 67 base.Subcommand 68 69 force bool 70 validate bool 71 emitToStdout string 72 } 73 74 type generateResult struct { 75 // Meta is the final meta parameters used by the generator. 76 Meta *lucicfg.Meta `json:"meta,omitempty"` 77 // LinterFindings is linter findings (if enabled). 78 LinterFindings []*buildifier.Finding `json:"linter_findings,omitempty"` 79 // Validation is per config set validation results (if -validate was used). 80 Validation []*lucicfg.ValidationResult `json:"validation,omitempty"` 81 82 // Changed is a list of config files that have changed or been created. 83 Changed []string `json:"changed,omitempty"` 84 // Unchanged is a list of config files that haven't changed. 85 Unchanged []string `json:"unchanged,omitempty"` 86 // Deleted is a list of config files deleted from disk due to staleness. 87 Deleted []string `json:"deleted,omitempty"` 88 } 89 90 func (gr *generateRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { 91 if !gr.CheckArgs(args, 1, 1) { 92 return 1 93 } 94 ctx := cli.GetContext(a, gr, env) 95 return gr.Done(gr.run(ctx, args[0])) 96 } 97 98 func (gr *generateRun) run(ctx context.Context, inputFile string) (*generateResult, error) { 99 meta := gr.DefaultMeta() 100 state, err := base.GenerateConfigs(ctx, inputFile, &meta, &gr.Meta, gr.Vars) 101 if err != nil { 102 return nil, err 103 } 104 output := state.Output 105 106 result := &generateResult{Meta: &meta} 107 108 switch { 109 case gr.emitToStdout != "": 110 // When using -emit-to-stdout, just print the requested file to stdout and 111 // do not touch configs on disk. This also overrides `config_dir = "-"`, 112 // since we don't want to print two different sources to stdout. 113 datum := output.Data[gr.emitToStdout] 114 if datum == nil { 115 return nil, fmt.Errorf("-emit-to-stdout: no such generated file %q", gr.emitToStdout) 116 } 117 blob, err := datum.Bytes() 118 if err != nil { 119 return nil, err 120 } 121 if _, err := os.Stdout.Write(blob); err != nil { 122 return nil, fmt.Errorf("when writing to stdout: %s", err) 123 } 124 125 case meta.ConfigDir == "-": 126 // Note: the result of this output is generally not parsable and should not 127 // be used in any scripting. 128 output.DebugDump() 129 130 default: 131 // Get rid of stale output in ConfigDir by deleting tracked files that are 132 // no longer in the output. Note that if TrackedFiles is empty (default), 133 // nothing is deleted, it is the responsibility of lucicfg users to make 134 // sure there's no stale output in this case. 135 tracked, err := lucicfg.FindTrackedFiles(meta.ConfigDir, meta.TrackedFiles) 136 if err != nil { 137 return result, err 138 } 139 for _, f := range tracked { 140 if _, present := output.Data[f]; !present { 141 result.Deleted = append(result.Deleted, f) 142 logging.Warningf(ctx, "Deleting tracked file no longer present in the output: %q", f) 143 if err := os.Remove(filepath.Join(meta.ConfigDir, filepath.FromSlash(f))); err != nil { 144 return result, err 145 } 146 } 147 } 148 // Write the new output there. 149 result.Changed, result.Unchanged, err = output.Write(meta.ConfigDir, gr.force) 150 if err != nil { 151 return result, err 152 } 153 } 154 155 entryPath, err := filepath.Abs(filepath.Dir(inputFile)) 156 if err != nil { 157 return nil, err 158 } 159 160 if err := base.CheckForBogusConfig(entryPath); err != nil { 161 return nil, err 162 } 163 164 rewriterFactory, err := base.GetRewriterFactory(filepath.Join(entryPath, base.ConfigName)) 165 if err != nil { 166 return nil, err 167 } 168 169 // Optionally validate via RPC and apply linters. This is slow, thus off by 170 // default. 171 if gr.validate { 172 result.LinterFindings, result.Validation, err = base.Validate(ctx, base.ValidateParams{ 173 Loader: state.Inputs.Code, 174 Source: state.Visited, 175 Output: output, 176 Meta: meta, 177 LegacyConfigServiceClient: gr.LegacyConfigServiceClient, 178 ConfigServiceConn: gr.MakeConfigServiceConn, 179 }, func(path string) (*build.Rewriter, error) { 180 // GetRewriter needs to see absolute paths; In Validate the paths are all 181 // relative to the entrypoint (e.g. main.star) becuase they refer to 182 // Starlark module import paths. 183 // 184 // Adjusting state.Visited above will fail because part of Validate's 185 // functionality needs to retain these relative paths. 186 return rewriterFactory.GetRewriter(filepath.Join(entryPath, path)) 187 }) 188 } 189 return result, err 190 }