dispatchers_windows.go 5.8 KB

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