go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucicfg/cli/base/generate.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  package base
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"fmt"
    21  	"io"
    22  	"os"
    23  	"path/filepath"
    24  
    25  	"go.chromium.org/luci/common/logging"
    26  	"go.chromium.org/luci/starlark/interpreter"
    27  
    28  	"go.chromium.org/luci/lucicfg"
    29  )
    30  
    31  // GenerateConfigs executes the Starlark script and assembles final values for
    32  // meta config.
    33  //
    34  // It is a common part of subcommands that generate configs.
    35  //
    36  // 'meta' is initial Meta config with default parameters, it will be mutated
    37  // in-place to contain the final parameters (based on lucicfg.config(...) calls
    38  // in Starlark and the config populated via CLI flags, passed as 'flags').
    39  // 'flags' are also mutated in-place to rebase ConfigDir onto cwd.
    40  //
    41  // 'vars' are a collection of k=v pairs passed via CLI flags as `-var k=v`. They
    42  // are used to pre-set lucicfg.var(..., exposed_as=<k>) variables.
    43  func GenerateConfigs(ctx context.Context, inputFile string, meta, flags *lucicfg.Meta, vars map[string]string) (*lucicfg.State, error) {
    44  	abs, err := filepath.Abs(inputFile)
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  
    49  	// Make sure the input file exists, to make the error message in this case be
    50  	// more humane. lucicfg.Generate will formulate this error as "no such module"
    51  	// which looks confusing.
    52  	//
    53  	// Also check that the script starts with "#!..." line, indicating it is
    54  	// executable. This gives a hint in case lucicfg is mistakenly invoked with
    55  	// some library script. Executing such scripts directly usually causes very
    56  	// confusing errors.
    57  	switch f, err := os.Open(abs); {
    58  	case os.IsNotExist(err):
    59  		return nil, fmt.Errorf("no such file: %s", inputFile)
    60  	case err != nil:
    61  		return nil, err
    62  	default:
    63  		yes, err := startsWithShebang(f)
    64  		f.Close()
    65  		switch {
    66  		case err != nil:
    67  			return nil, err
    68  		case !yes:
    69  			fmt.Fprintf(os.Stderr,
    70  				`================================= WARNING =================================
    71  Body of the script %s doesn't start with "#!".
    72  
    73  It is likely not a correct entry point script and lucicfg execution will fail
    74  with cryptic errors or unexpected results. Many configs consist of more than
    75  one *.star file, but there's usually only one entry point script that should
    76  be passed to lucicfg.
    77  
    78  If it is the correct script, make sure it starts with the following line to
    79  indicate it is executable (and remove this warning):
    80  
    81      #!/usr/bin/env lucicfg
    82  
    83  You may also optionally set +x flag on it, but this is not required.
    84  ===========================================================================
    85  
    86  `, filepath.Base(abs))
    87  		}
    88  	}
    89  
    90  	// The directory with the input file becomes the root of the main package.
    91  	root, main := filepath.Split(abs)
    92  
    93  	// Generate everything, storing the result in memory.
    94  	logging.Infof(ctx, "Generating configs using %s...", lucicfg.UserAgent)
    95  	state, err := lucicfg.Generate(ctx, lucicfg.Inputs{
    96  		Code:  interpreter.FileSystemLoader(root),
    97  		Path:  root,
    98  		Entry: main,
    99  		Meta:  meta,
   100  		Vars:  vars,
   101  	})
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	// Config dir in the default meta, and if set from Starlark, is relative to
   107  	// the main package root. It is relative to cwd ONLY when explicitly provided
   108  	// via -config-dir CLI flag. Note that ".." is allowed.
   109  	cwd, err := os.Getwd()
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  	meta.RebaseConfigDir(root)
   114  	state.Meta.RebaseConfigDir(root)
   115  	flags.RebaseConfigDir(cwd)
   116  
   117  	// Figure out the final meta config: values set via starlark override
   118  	// defaults, and values passed explicitly via CLI flags override what is
   119  	// in starlark.
   120  	meta.PopulateFromTouchedIn(&state.Meta)
   121  	meta.PopulateFromTouchedIn(flags)
   122  	meta.Log(ctx)
   123  
   124  	// Discard changes to the non-tracked files by loading their original bodies
   125  	// (if any) from disk. We replace them to make sure the output is still
   126  	// validated as a whole, it is just only partially generated in this case.
   127  	if len(meta.TrackedFiles) != 0 {
   128  		if err := state.Output.DiscardChangesToUntracked(ctx, meta.TrackedFiles, meta.ConfigDir); err != nil {
   129  			return nil, err
   130  		}
   131  	}
   132  
   133  	return state, nil
   134  }
   135  
   136  func startsWithShebang(r io.Reader) (bool, error) {
   137  	buf := make([]byte, 2)
   138  	switch _, err := io.ReadFull(r, buf); {
   139  	case err == io.EOF || err == io.ErrUnexpectedEOF:
   140  		return false, nil // the file is smaller than 2 bytes
   141  	case err != nil:
   142  		return false, err
   143  	default:
   144  		return bytes.Equal(buf, []byte("#!")), nil
   145  	}
   146  }