blob: 27c369c49b77b271e68cc78a2c9ce51b5a00afeb [file] [log] [blame]
// Copyright 2018 The LUCI Authors.
//
// 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.
// Package generate implements 'generate' subcommand.
package generate
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/bazelbuild/buildtools/build"
"github.com/maruel/subcommands"
"go.chromium.org/luci/common/cli"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/lucicfg"
"go.chromium.org/luci/lucicfg/buildifier"
"go.chromium.org/luci/lucicfg/cli/base"
)
// Cmd is 'generate' subcommand.
func Cmd(params base.Parameters) *subcommands.Command {
return &subcommands.Command{
UsageLine: "generate SCRIPT",
ShortDesc: "interprets a high-level config, generating *.cfg files",
LongDesc: `Interprets a high-level config, generating *.cfg files.
Writes generated configs to the directory given via -config-dir or via
lucicfg.config(config_dir=...) statement in the script. If it is '-', just
prints them to stdout.
If -validate is given, sends the generated config to LUCI Config service for
validation. This can also be done separately via 'validate' subcommand.
If the generation stage fails, doesn't overwrite any files on disk. If the
generation succeeds, but the validation fails, the new generated files are kept
on disk, so they can be manually examined for reasons they are invalid.
`,
CommandRun: func() subcommands.CommandRun {
gr := &generateRun{}
gr.Init(params)
gr.AddGeneratorFlags()
gr.Flags.BoolVar(&gr.force, "force", false, "Rewrite existing output files on disk even if they are semantically equal to generated ones")
gr.Flags.BoolVar(&gr.validate, "validate", false, "Validate the generate configs by sending them to LUCI Config")
gr.Flags.StringVar(&gr.emitToStdout, "emit-to-stdout", "",
"When set to a path, keep generated configs in memory (don't touch disk) and just emit this single config file to stdout")
return gr
},
}
}
type generateRun struct {
base.Subcommand
force bool
validate bool
emitToStdout string
}
type generateResult struct {
// Meta is the final meta parameters used by the generator.
Meta *lucicfg.Meta `json:"meta,omitempty"`
// LinterFindings is linter findings (if enabled).
LinterFindings []*buildifier.Finding `json:"linter_findings,omitempty"`
// Validation is per config set validation results (if -validate was used).
Validation []*lucicfg.ValidationResult `json:"validation,omitempty"`
// Changed is a list of config files that have changed or been created.
Changed []string `json:"changed,omitempty"`
// Unchanged is a list of config files that haven't changed.
Unchanged []string `json:"unchanged,omitempty"`
// Deleted is a list of config files deleted from disk due to staleness.
Deleted []string `json:"deleted,omitempty"`
}
func (gr *generateRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if !gr.CheckArgs(args, 1, 1) {
return 1
}
ctx := cli.GetContext(a, gr, env)
return gr.Done(gr.run(ctx, args[0]))
}
func (gr *generateRun) run(ctx context.Context, inputFile string) (*generateResult, error) {
meta := gr.DefaultMeta()
state, err := base.GenerateConfigs(ctx, inputFile, &meta, &gr.Meta, gr.Vars)
if err != nil {
return nil, err
}
output := state.Output
result := &generateResult{Meta: &meta}
switch {
case gr.emitToStdout != "":
// When using -emit-to-stdout, just print the requested file to stdout and
// do not touch configs on disk. This also overrides `config_dir = "-"`,
// since we don't want to print two different sources to stdout.
datum := output.Data[gr.emitToStdout]
if datum == nil {
return nil, fmt.Errorf("-emit-to-stdout: no such generated file %q", gr.emitToStdout)
}
blob, err := datum.Bytes()
if err != nil {
return nil, err
}
if _, err := os.Stdout.Write(blob); err != nil {
return nil, fmt.Errorf("when writing to stdout: %s", err)
}
case meta.ConfigDir == "-":
// Note: the result of this output is generally not parsable and should not
// be used in any scripting.
output.DebugDump()
default:
// Get rid of stale output in ConfigDir by deleting tracked files that are
// no longer in the output. Note that if TrackedFiles is empty (default),
// nothing is deleted, it is the responsibility of lucicfg users to make
// sure there's no stale output in this case.
tracked, err := lucicfg.FindTrackedFiles(meta.ConfigDir, meta.TrackedFiles)
if err != nil {
return result, err
}
for _, f := range tracked {
if _, present := output.Data[f]; !present {
result.Deleted = append(result.Deleted, f)
logging.Warningf(ctx, "Deleting tracked file no longer present in the output: %q", f)
if err := os.Remove(filepath.Join(meta.ConfigDir, filepath.FromSlash(f))); err != nil {
return result, err
}
}
}
// Write the new output there.
result.Changed, result.Unchanged, err = output.Write(meta.ConfigDir, gr.force)
if err != nil {
return result, err
}
}
entryPath, err := filepath.Abs(filepath.Dir(inputFile))
if err != nil {
return nil, err
}
if err := base.CheckForBogusConfig(entryPath); err != nil {
return nil, err
}
rewriterFactory, err := base.GetRewriterFactory(filepath.Join(entryPath, base.ConfigName))
if err != nil {
return nil, err
}
// Optionally validate via RPC and apply linters. This is slow, thus off by
// default.
if gr.validate {
result.LinterFindings, result.Validation, err = base.Validate(ctx, base.ValidateParams{
Loader: state.Inputs.Code,
Source: state.Visited,
Output: output,
Meta: meta,
LegacyConfigServiceClient: gr.LegacyConfigServiceClient,
ConfigServiceConn: gr.MakeConfigServiceConn,
}, func(path string) (*build.Rewriter, error) {
// GetRewriter needs to see absolute paths; In Validate the paths are all
// relative to the entrypoint (e.g. main.star) becuase they refer to
// Starlark module import paths.
//
// Adjusting state.Visited above will fail because part of Validate's
// functionality needs to retain these relative paths.
return rewriterFactory.GetRewriter(filepath.Join(entryPath, path))
})
}
return result, err
}