go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/vpython/spec/resolve.go (about)

     1  // Copyright 2017 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 spec
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"os"
    21  	"path/filepath"
    22  
    23  	"go.chromium.org/luci/common/errors"
    24  	"go.chromium.org/luci/common/logging"
    25  	"go.chromium.org/luci/common/system/filesystem"
    26  
    27  	"go.chromium.org/luci/vpython/api/vpython"
    28  	"go.chromium.org/luci/vpython/python"
    29  )
    30  
    31  // IsUserError is tagged into errors caused by bad user inputs (e.g. modules or
    32  // scripts which don't exist).
    33  var IsUserError = errors.BoolTag{
    34  	Key: errors.NewTagKey("this error occurred due to a user input."),
    35  }
    36  
    37  // ResolveSpec resolves the configured environment specification. The resulting
    38  // spec is installed into o's EnvConfig.Spec field.
    39  func ResolveSpec(c context.Context, l *Loader, target python.Target, workDir string) (*vpython.Spec, error) {
    40  	// If there's no target, then we're dropping to an interactive shell
    41  	_, interactive := target.(python.NoTarget)
    42  
    43  	// Reading script from stdin or executing code from command line args are
    44  	// the same as no script in that we don't have a source file to key off
    45  	// of to find the spec, so resolve from CWD
    46  	//
    47  	// Executing code from command line args.
    48  	_, isCommandTarget := target.(python.CommandTarget)
    49  	//
    50  	// Reading script from stdin.
    51  	script, isScriptTarget := target.(python.ScriptTarget)
    52  	loadFromStdin := isScriptTarget && (script.Path == "-")
    53  
    54  	// If we're loading a module, then we could attempt to find the module and
    55  	// start the search there. But resolving the module path in full generality
    56  	// would be slow and/or complicated. Perhaps we'll revisit in the future,
    57  	// but for now let's just start the search in the CWD, as this is at least
    58  	// a subset of the paths we should search.
    59  	_, isModuleTarget := target.(python.ModuleTarget)
    60  
    61  	// We're either dropping to interactive mode, reading a script from stdin or
    62  	// command-line, or loading a module. Regardless, try to resolve the spec
    63  	// from the CWD.
    64  	if interactive || isCommandTarget || loadFromStdin || isModuleTarget {
    65  		spec, path, err := l.LoadForScript(c, workDir, false)
    66  		if err != nil {
    67  			return nil, errors.Annotate(err, "failed to load spec for script: %s", target).Err()
    68  		}
    69  		if spec != nil {
    70  			relpath, err := filepath.Rel(workDir, path)
    71  			if err != nil {
    72  				return nil, errors.Annotate(err, "failed to get relative path for %s", path).Err()
    73  			}
    74  
    75  			if interactive {
    76  				fmt.Fprintf(os.Stderr, "Starting interactive mode, loading vpython spec from %s\n", relpath)
    77  			}
    78  
    79  			if loadFromStdin {
    80  				fmt.Fprintf(os.Stderr, "Reading from stdin, loading vpython spec from %s\n", relpath)
    81  			}
    82  
    83  			return spec, nil
    84  		}
    85  	}
    86  
    87  	// If we're running a Python script, assert that the target script exists.
    88  	// Additionally, track whether it's a file or a module (directory).
    89  	isModule := false
    90  	if isScriptTarget {
    91  		logging.Debugf(c, "Resolved Python target script: %s", target)
    92  
    93  		// Resolve to absolute script path.
    94  		if err := filesystem.AbsPath(&script.Path); err != nil {
    95  			return nil, errors.Annotate(err, "failed to get absolute path of: %s", target).Err()
    96  		}
    97  
    98  		// Confirm that the script path actually exists.
    99  		st, err := os.Stat(script.Path)
   100  		if err != nil {
   101  			return nil, IsUserError.Apply(err)
   102  		}
   103  
   104  		// If the script is a directory, then we assume that we're doing a module
   105  		// invocation (__main__.py).
   106  		isModule = st.IsDir()
   107  	}
   108  
   109  	// If it's a script, try resolving from filesystem first.
   110  	if isScriptTarget {
   111  		spec, _, err := l.LoadForScript(c, script.Path, isModule)
   112  		if err != nil {
   113  			kind := "script"
   114  			if isModule {
   115  				kind = "module"
   116  			}
   117  			return nil, errors.Annotate(err, "failed to load spec for %s: %s", kind, target).Err()
   118  		}
   119  		if spec != nil {
   120  			return spec, nil
   121  		}
   122  	}
   123  
   124  	// If standard resolution doesn't yield a spec, fall back on our default spec.
   125  	logging.Infof(c, "Unable to resolve specification path. Using default specification.")
   126  	return nil, nil
   127  }