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