github.com/docker/docker@v299999999.0.0-20200612211812-aaf470eca7b5+incompatible/builder/dockerfile/dispatchers_windows.go (about)

     1  package dockerfile // import "github.com/docker/docker/builder/dockerfile"
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"path"
     8  	"path/filepath"
     9  	"regexp"
    10  	"strings"
    11  
    12  	"github.com/docker/docker/api/types/container"
    13  	"github.com/docker/docker/pkg/system"
    14  	"github.com/moby/buildkit/frontend/dockerfile/instructions"
    15  )
    16  
    17  var pattern = regexp.MustCompile(`^[a-zA-Z]:\.$`)
    18  
    19  // normalizeWorkdir normalizes a user requested working directory in a
    20  // platform semantically consistent way.
    21  func normalizeWorkdir(platform string, current string, requested string) (string, error) {
    22  	if platform == "" {
    23  		platform = "windows"
    24  	}
    25  	if platform == "windows" {
    26  		return normalizeWorkdirWindows(current, requested)
    27  	}
    28  	return normalizeWorkdirUnix(current, requested)
    29  }
    30  
    31  // normalizeWorkdirUnix normalizes a user requested working directory in a
    32  // platform semantically consistent way.
    33  func normalizeWorkdirUnix(current string, requested string) (string, error) {
    34  	if requested == "" {
    35  		return "", errors.New("cannot normalize nothing")
    36  	}
    37  	current = strings.Replace(current, string(os.PathSeparator), "/", -1)
    38  	requested = strings.Replace(requested, string(os.PathSeparator), "/", -1)
    39  	if !path.IsAbs(requested) {
    40  		return path.Join(`/`, current, requested), nil
    41  	}
    42  	return requested, nil
    43  }
    44  
    45  // normalizeWorkdirWindows normalizes a user requested working directory in a
    46  // platform semantically consistent way.
    47  func normalizeWorkdirWindows(current string, requested string) (string, error) {
    48  	if requested == "" {
    49  		return "", errors.New("cannot normalize nothing")
    50  	}
    51  
    52  	// `filepath.Clean` will replace "" with "." so skip in that case
    53  	if current != "" {
    54  		current = filepath.Clean(current)
    55  	}
    56  	if requested != "" {
    57  		requested = filepath.Clean(requested)
    58  	}
    59  
    60  	// If either current or requested in Windows is:
    61  	// C:
    62  	// C:.
    63  	// then an error will be thrown as the definition for the above
    64  	// refers to `current directory on drive C:`
    65  	// Since filepath.Clean() will automatically normalize the above
    66  	// to `C:.`, we only need to check the last format
    67  	if pattern.MatchString(current) {
    68  		return "", fmt.Errorf("%s is not a directory. If you are specifying a drive letter, please add a trailing '\\'", current)
    69  	}
    70  	if pattern.MatchString(requested) {
    71  		return "", fmt.Errorf("%s is not a directory. If you are specifying a drive letter, please add a trailing '\\'", requested)
    72  	}
    73  
    74  	// Target semantics is C:\somefolder, specifically in the format:
    75  	// UPPERCASEDriveLetter-Colon-Backslash-FolderName. We are already
    76  	// guaranteed that `current`, if set, is consistent. This allows us to
    77  	// cope correctly with any of the following in a Dockerfile:
    78  	//	WORKDIR a                       --> C:\a
    79  	//	WORKDIR c:\\foo                 --> C:\foo
    80  	//	WORKDIR \\foo                   --> C:\foo
    81  	//	WORKDIR /foo                    --> C:\foo
    82  	//	WORKDIR c:\\foo \ WORKDIR bar   --> C:\foo --> C:\foo\bar
    83  	//	WORKDIR C:/foo \ WORKDIR bar    --> C:\foo --> C:\foo\bar
    84  	//	WORKDIR C:/foo \ WORKDIR \\bar  --> C:\foo --> C:\bar
    85  	//	WORKDIR /foo \ WORKDIR c:/bar   --> C:\foo --> C:\bar
    86  	if len(current) == 0 || system.IsAbs(requested) {
    87  		if (requested[0] == os.PathSeparator) ||
    88  			(len(requested) > 1 && string(requested[1]) != ":") ||
    89  			(len(requested) == 1) {
    90  			requested = filepath.Join(`C:\`, requested)
    91  		}
    92  	} else {
    93  		requested = filepath.Join(current, requested)
    94  	}
    95  	// Upper-case drive letter
    96  	return (strings.ToUpper(string(requested[0])) + requested[1:]), nil
    97  }
    98  
    99  // resolveCmdLine takes a command line arg set and optionally prepends a platform-specific
   100  // shell in front of it. It returns either an array of arguments and an indication that
   101  // the arguments are not yet escaped; Or, an array containing a single command line element
   102  // along with an indication that the arguments are escaped so the runtime shouldn't escape.
   103  //
   104  // A better solution could be made, but it would be exceptionally invasive throughout
   105  // many parts of the daemon which are coded assuming Linux args array only only, not taking
   106  // account of Windows-natural command line semantics and it's argv handling. Put another way,
   107  // while what is here is good-enough, it could be improved, but would be highly invasive.
   108  //
   109  // The commands when this function is called are RUN, ENTRYPOINT and CMD.
   110  func resolveCmdLine(cmd instructions.ShellDependantCmdLine, runConfig *container.Config, os, command, original string) ([]string, bool) {
   111  
   112  	// Make sure we return an empty array if there is no cmd.CmdLine
   113  	if len(cmd.CmdLine) == 0 {
   114  		return []string{}, runConfig.ArgsEscaped
   115  	}
   116  
   117  	if os == "windows" { // ie WCOW
   118  		if cmd.PrependShell {
   119  			// WCOW shell-form. Return a single-element array containing the original command line prepended with the shell.
   120  			// Also indicate that it has not been escaped (so will be passed through directly to HCS). Note that
   121  			// we go back to the original un-parsed command line in the dockerfile line, strip off both the command part of
   122  			// it (RUN/ENTRYPOINT/CMD), and also strip any leading white space. IOW, we deliberately ignore any prior parsing
   123  			// so as to ensure it is treated exactly as a command line. For those interested, `RUN mkdir "c:/foo"` is a particularly
   124  			// good example of why this is necessary if you fancy debugging how cmd.exe and its builtin mkdir works. (Windows
   125  			// doesn't have a mkdir.exe, and I'm guessing cmd.exe has some very long unavoidable and unchangeable historical
   126  			// design decisions over how both its built-in echo and mkdir are coded. Probably more too.)
   127  			original = original[len(command):]               // Strip off the command
   128  			original = strings.TrimLeft(original, " \t\v\n") // Strip of leading whitespace
   129  			return []string{strings.Join(getShell(runConfig, os), " ") + " " + original}, true
   130  		}
   131  
   132  		// WCOW JSON/"exec" form.
   133  		return cmd.CmdLine, false
   134  	}
   135  
   136  	// LCOW - use args as an array, same as LCOL.
   137  	if cmd.PrependShell && cmd.CmdLine != nil {
   138  		return append(getShell(runConfig, os), cmd.CmdLine...), false
   139  	}
   140  	return cmd.CmdLine, false
   141  }