github.com/distbuild/reclient@v0.0.0-20240401075343-3de72e395564/internal/pkg/inputprocessor/action/clangcl/preprocessor.go (about) 1 // Copyright 2023 Google LLC 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 clangcl performs include processing given a valid clangCl action. 16 package clangcl 17 18 import ( 19 "fmt" 20 "os" 21 "path/filepath" 22 "regexp" 23 "strconv" 24 "strings" 25 "sync" 26 27 "github.com/bazelbuild/reclient/internal/pkg/inputprocessor" 28 "github.com/bazelbuild/reclient/internal/pkg/inputprocessor/action/cppcompile" 29 30 "github.com/bazelbuild/remote-apis-sdks/go/pkg/cache" 31 "github.com/bazelbuild/remote-apis-sdks/go/pkg/command" 32 33 log "github.com/golang/glog" 34 ) 35 36 type resourceDirInfo struct { 37 fileInfo os.FileInfo 38 resourceDir string 39 } 40 41 type version struct { 42 major, minor, micro, build int 43 text string 44 } 45 46 var ( 47 resourceDirsMu sync.Mutex 48 resourceDirs = map[string]resourceDirInfo{} 49 toAbsArgs = map[string]bool{} 50 virtualInputFlags = map[string]bool{"-I": true, "/I": true, "-imsvc": true, "-winsysroot": true} 51 versionRegex = regexp.MustCompile(`^([0-9]+)(\.([0-9]+)(\.([0-9]+)(\.([0-9]+))?)?)?$`) 52 ) 53 54 // Preprocessor is the preprocessor of clang-cl compile actions. 55 type Preprocessor struct { 56 *cppcompile.Preprocessor 57 winSDKCache cache.SingleFlight 58 vcToolchainCache cache.SingleFlight 59 } 60 61 // ParseFlags parses the commands flags and populates the ActionSpec object with inferred 62 // information. 63 func (p *Preprocessor) ParseFlags() error { 64 f, err := parseFlags(p.Ctx, p.Options.Cmd, p.Options.WorkingDir, p.Options.ExecRoot) 65 if err != nil { 66 p.Err = fmt.Errorf("flag parsing failed. %v", err) 67 return p.Err 68 } 69 p.Flags = f 70 p.FlagsToActionSpec() 71 return nil 72 } 73 74 // ComputeSpec computes cpp header dependencies. 75 func (p *Preprocessor) ComputeSpec() error { 76 s := &inputprocessor.ActionSpec{InputSpec: &command.InputSpec{}} 77 defer p.AppendSpec(s) 78 79 args := p.BuildCommandLine("/Fo", true, toAbsArgs) 80 if p.CPPDepScanner.Capabilities().GetExpectsResourceDir() { 81 args = p.addResourceDir(args) 82 } 83 headerInputFiles, err := p.FindDependencies(args) 84 if err != nil { 85 s.UsedShallowMode = true 86 return err 87 } 88 89 s.InputSpec = &command.InputSpec{ 90 Inputs: headerInputFiles, 91 VirtualInputs: cppcompile.VirtualInputs(p.Flags, p), 92 } 93 return nil 94 } 95 96 // IsVirtualInput returns true if the flag specifies a virtual input to be added to InputSpec. 97 func (p *Preprocessor) IsVirtualInput(flag string) bool { 98 return virtualInputFlags[flag] 99 } 100 101 // AppendVirtualInput appends a virtual input to res. If the flag="-winsysroot" the content of 102 // the path is processed and Win SDK and VC toolchain paths are added to virtual inputs. 103 func (p *Preprocessor) AppendVirtualInput(res []*command.VirtualInput, flag, path string) []*command.VirtualInput { 104 if flag == "-winsysroot" { 105 // clang-cl tries to extract win SDK version and VC toolchain version from within 106 // the winsysroot path. Those are used for setting -internal-isystem flag values. 107 // We need to reproduce clang's path traversing logic and upload the required paths 108 // so clang-cl on a remote worker generates the same -internal-isystem paths as it would 109 // locally. 110 winsysroot := path 111 absWinsysroot := filepath.Join(p.Flags.ExecRoot, path) 112 cacheEntry, _ := p.winSDKCache.LoadOrStore(winsysroot, func() (interface{}, error) { 113 absDir, err := winSDKDir(absWinsysroot) 114 if err != nil { 115 // If failed to get Win SDK dir, return winsysroot instead (don't return the error) 116 // This will prevent the logic to be re-executed for each action as 117 // errors will likely stem from unexpected directories structure on FS. 118 log.Warningf("Failed to get Win SDK path for %q. %v", winsysroot, err) 119 return winsysroot, nil 120 } 121 dir, err := filepath.Rel(p.Flags.ExecRoot, absDir) 122 if err != nil { 123 log.Warningf("Failed to make %q relative to %q. %v", absDir, p.Flags.ExecRoot, err) 124 return winsysroot, nil 125 } 126 return dir, nil 127 }) 128 computedPath := cacheEntry.(string) 129 if computedPath != winsysroot { 130 res = p.Preprocessor.AppendVirtualInput(res, flag, computedPath) 131 } 132 cacheEntry, _ = p.vcToolchainCache.LoadOrStore(winsysroot, func() (interface{}, error) { 133 absDir, err := vcToolchainDir(absWinsysroot) 134 if err != nil { 135 log.Warningf("Failed to get VC toolchain path for %q. %v", winsysroot, err) 136 return winsysroot, nil 137 } 138 dir, err := filepath.Rel(p.Flags.ExecRoot, absDir) 139 if err != nil { 140 log.Warningf("Failed to make %q relative to %q. %v", absDir, p.Flags.ExecRoot, err) 141 return winsysroot, nil 142 } 143 return dir, nil 144 }) 145 computedPath = cacheEntry.(string) 146 if computedPath != winsysroot { 147 res = p.Preprocessor.AppendVirtualInput(res, flag, computedPath) 148 } 149 return res 150 } 151 return p.Preprocessor.AppendVirtualInput(res, flag, path) 152 } 153 154 func (p *Preprocessor) addResourceDir(args []string) []string { 155 for _, arg := range args { 156 if arg == "-resource-dir" || strings.HasPrefix(arg, "-resource-dir=") { 157 return args 158 } 159 } 160 resourceDir := p.resourceDir(args) 161 if resourceDir != "" { 162 return append(args, "-resource-dir", resourceDir) 163 } 164 return args 165 } 166 167 func (p *Preprocessor) resourceDir(args []string) string { 168 return p.ResourceDir(args, "--version", func(stdout string) (string, error) { 169 if !strings.HasPrefix(stdout, "clang version ") { 170 return "", fmt.Errorf("unexpected version string of %s: %q", args[0], stdout) 171 } 172 version := strings.TrimPrefix(stdout, "clang version ") 173 i := strings.IndexByte(version, ' ') 174 if i < 0 { 175 return "", fmt.Errorf("unexpected version string of %s: %q", args[0], stdout) 176 } 177 version = version[:i] 178 resourceDir := filepath.Join(filepath.Dir(args[0]), "..", "lib", "clang", version) 179 log.Infof("%s --version => %q => %q => resource-dir:%q", args[0], stdout, version, resourceDir) 180 return resourceDir, nil 181 }) 182 } 183 184 func winSDKDir(winsysroot string) (string, error) { 185 // Reproduces the behavior from 186 // https://github.com/llvm/llvm-project/blob/main/llvm/lib/WindowsDriver/MSVCPaths.cpp#L108 187 path, err := getMaxVersionDir(filepath.Join(winsysroot, "Windows Kits")) 188 if err != nil { 189 return "", err 190 } 191 if path, err = getMaxVersionDir(filepath.Join(path, "Include")); err != nil { 192 return "", err 193 } 194 return path, nil 195 } 196 197 func vcToolchainDir(winsysroot string) (string, error) { 198 path, err := getMaxVersionDir(filepath.Join(winsysroot, "VC", "Tools", "MSVC")) 199 if err != nil { 200 return "", err 201 } 202 return path, nil 203 } 204 205 func getMaxVersionDir(path string) (string, error) { 206 if _, err := os.Stat(path); err != nil { 207 return "", err 208 } 209 maxVersion, err := getMaxVersionFromPath(path) 210 if err != nil { 211 return "", fmt.Errorf("No version dir under %q", path) 212 } 213 return filepath.Join(path, maxVersion), nil 214 } 215 216 func getMaxVersionFromPath(path string) (string, error) { 217 entries, err := os.ReadDir(path) 218 if err != nil { 219 return "", err 220 } 221 maxVersion := version{} 222 for _, entry := range entries { 223 if !entry.IsDir() { 224 continue 225 } 226 if tuple, err := newVersion(entry.Name()); err == nil && tuple.gt(maxVersion) { 227 maxVersion = tuple 228 } 229 } 230 if maxVersion.isEmpty() { 231 return "", fmt.Errorf("No version dir under %q", path) 232 } 233 return maxVersion.text, nil 234 } 235 236 func newVersion(text string) (version, error) { 237 match := versionRegex.FindStringSubmatch(text) 238 if len(match) == 0 { 239 return version{}, fmt.Errorf("%q is not a valid version string", text) 240 } 241 tuple := version{text: text} 242 tuple.major, _ = strconv.Atoi(match[1]) 243 tuple.minor, _ = strconv.Atoi(match[3]) 244 tuple.micro, _ = strconv.Atoi(match[5]) 245 tuple.build, _ = strconv.Atoi(match[7]) 246 return tuple, nil 247 } 248 249 func (a *version) gt(b version) bool { 250 if a.major != b.major { 251 return a.major > b.major 252 } 253 if a.minor != b.minor { 254 return a.minor > b.minor 255 } 256 if a.micro != b.micro { 257 return a.micro > b.micro 258 } 259 return a.build > b.build 260 } 261 262 func (a *version) isEmpty() bool { 263 return len(a.text) == 0 264 }