1// Copyright 2024 Google Inc. All rights reserved. 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 15package release_config_lib 16 17import ( 18 "encoding/json" 19 "fmt" 20 "io/fs" 21 "os" 22 "os/exec" 23 "path/filepath" 24 "regexp" 25 "slices" 26 "strings" 27 28 "github.com/google/blueprint/pathtools" 29 "google.golang.org/protobuf/encoding/prototext" 30 "google.golang.org/protobuf/proto" 31) 32 33var ( 34 disableWarnings bool 35 containerRegexp, _ = regexp.Compile("^[a-z][a-z0-9]*([._][a-z][a-z0-9]*)*$") 36) 37 38type StringList []string 39 40func (l *StringList) Set(v string) error { 41 *l = append(*l, v) 42 return nil 43} 44 45func (l *StringList) String() string { 46 return fmt.Sprintf("%v", *l) 47} 48 49// Write a marshalled message to a file. 50// 51// Marshal the message based on the extension of the path we are writing it to. 52// 53// Args: 54// 55// path string: the path of the file to write to. Directories are not created. 56// Supported extensions are: ".json", ".pb", and ".textproto". 57// message proto.Message: the message to write. 58// 59// Returns: 60// 61// error: any error encountered. 62func WriteMessage(path string, message proto.Message) (err error) { 63 format := filepath.Ext(path) 64 if len(format) > 1 { 65 // Strip any leading dot. 66 format = format[1:] 67 } 68 return WriteFormattedMessage(path, format, message) 69} 70 71// Write a marshalled message to a file. 72// 73// Marshal the message using the given format. 74// 75// Args: 76// 77// path string: the path of the file to write to. Directories are not created. 78// Supported extensions are: ".json", ".pb", and ".textproto". 79// format string: one of "json", "pb", or "textproto". 80// message proto.Message: the message to write. 81// 82// Returns: 83// 84// error: any error encountered. 85func WriteFormattedMessage(path, format string, message proto.Message) (err error) { 86 var data []byte 87 if _, err := os.Stat(filepath.Dir(path)); err != nil { 88 if err = os.MkdirAll(filepath.Dir(path), 0775); err != nil { 89 return err 90 } 91 } 92 switch format { 93 case "json": 94 data, err = json.MarshalIndent(message, "", " ") 95 case "pb", "binaryproto", "protobuf": 96 data, err = proto.Marshal(message) 97 case "textproto": 98 data, err = prototext.MarshalOptions{Multiline: true}.Marshal(message) 99 default: 100 return fmt.Errorf("Unknown message format for %s", path) 101 } 102 if err != nil { 103 return err 104 } 105 return pathtools.WriteFileIfChanged(path, data, 0644) 106} 107 108// Read a message from a file. 109// 110// The message is unmarshalled based on the extension of the file read. 111// 112// Args: 113// 114// path string: the path of the file to read. 115// message proto.Message: the message to unmarshal the message into. 116// 117// Returns: 118// 119// error: any error encountered. 120func LoadMessage(path string, message proto.Message) error { 121 data, err := os.ReadFile(path) 122 if err != nil { 123 return err 124 } 125 switch filepath.Ext(path) { 126 case ".json": 127 return json.Unmarshal(data, message) 128 case ".pb", ".protobuf", ".binaryproto": 129 return proto.Unmarshal(data, message) 130 case ".textproto": 131 return prototext.Unmarshal(data, message) 132 } 133 return fmt.Errorf("Unknown message format for %s", path) 134} 135 136// Call Func for any textproto files found in {root}/{subdir}. 137func WalkTextprotoFiles(root string, subdir string, Func fs.WalkDirFunc) error { 138 path := filepath.Join(root, subdir) 139 if _, err := os.Stat(path); err != nil { 140 // Missing subdirs are not an error. 141 return nil 142 } 143 return filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error { 144 if err != nil { 145 return err 146 } 147 if strings.HasSuffix(d.Name(), ".textproto") && d.Type().IsRegular() { 148 return Func(path, d, err) 149 } 150 return nil 151 }) 152} 153 154// Turn off all warning output 155func DisableWarnings() { 156 disableWarnings = true 157} 158 159// warnf will log to stdout if warnings are enabled. In make code, 160// stdout is redirected to a file, so the warnings will not be shown 161// in the terminal. 162func warnf(format string, args ...any) (n int, err error) { 163 if !disableWarnings { 164 return fmt.Printf(format, args...) 165 } 166 return 0, nil 167} 168 169func SortedMapKeys(inputMap map[string]bool) []string { 170 ret := []string{} 171 for k := range inputMap { 172 ret = append(ret, k) 173 } 174 slices.Sort(ret) 175 return ret 176} 177 178func validContainer(container string) bool { 179 return containerRegexp.MatchString(container) 180} 181 182// Returns the default value for release config artifacts. 183func GetDefaultOutDir() string { 184 outEnv := os.Getenv("OUT_DIR") 185 if outEnv == "" { 186 outEnv = "out" 187 } 188 return filepath.Join(outEnv, "soong", "release-config") 189} 190 191// Find the top of the workspace. 192// 193// This mirrors the logic in build/envsetup.sh's gettop(). 194func GetTopDir() (topDir string, err error) { 195 workingDir, err := os.Getwd() 196 if err != nil { 197 return 198 } 199 topFile := "build/make/core/envsetup.mk" 200 for topDir = workingDir; topDir != "/"; topDir = filepath.Dir(topDir) { 201 if _, err = os.Stat(filepath.Join(topDir, topFile)); err == nil { 202 return filepath.Rel(workingDir, topDir) 203 } 204 } 205 return "", fmt.Errorf("Unable to locate top of workspace") 206} 207 208// Return the default list of map files to use. 209func GetDefaultMapPaths(queryMaps bool) (defaultMapPaths StringList, err error) { 210 var defaultLocations StringList 211 workingDir, err := os.Getwd() 212 if err != nil { 213 return 214 } 215 defer func() { 216 os.Chdir(workingDir) 217 }() 218 topDir, err := GetTopDir() 219 os.Chdir(topDir) 220 221 defaultLocations = StringList{ 222 "build/release/release_config_map.textproto", 223 "vendor/google_shared/build/release/release_config_map.textproto", 224 "vendor/google/release/release_config_map.textproto", 225 } 226 for _, path := range defaultLocations { 227 if _, err = os.Stat(path); err == nil { 228 defaultMapPaths = append(defaultMapPaths, path) 229 } 230 } 231 232 var prodMaps string 233 if queryMaps { 234 getBuildVar := exec.Command("build/soong/soong_ui.bash", "--dumpvar-mode", "PRODUCT_RELEASE_CONFIG_MAPS") 235 var stdout strings.Builder 236 getBuildVar.Stdin = strings.NewReader("") 237 getBuildVar.Stdout = &stdout 238 getBuildVar.Stderr = os.Stderr 239 err = getBuildVar.Run() 240 if err != nil { 241 return 242 } 243 prodMaps = stdout.String() 244 } else { 245 prodMaps = os.Getenv("PRODUCT_RELEASE_CONFIG_MAPS") 246 } 247 prodMaps = strings.TrimSpace(prodMaps) 248 if len(prodMaps) > 0 { 249 defaultMapPaths = append(defaultMapPaths, strings.Split(prodMaps, " ")...) 250 } 251 return 252} 253