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 }