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  }