File: build.py

package info (click to toggle)
charliecloud 0.43-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,116 kB
  • sloc: python: 6,021; sh: 4,284; ansic: 3,863; makefile: 598
file content (164 lines) | stat: -rw-r--r-- 5,783 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# Implementation of "ch-image build".

import os
import os.path
import re
import sys

import charliecloud as ch
import build_cache as bu
import filesystem as fs
import force
import image as im

import irtree

# See image.py for the messy import of this.
lark = im.lark

## Main ##

def main(cli):
   irtree.cli = cli

   cli_process_common(cli)

   # Process CLI. Make appropriate modifications to “cli” instance and return
   # Dockerfile text.
   text = cli_process(cli)

   tree = parse_dockerfile(text, cli)

   # Count the number of stages (i.e., FROM instructions)
   image_ct = sum(1 for i in tree.children_("from_"))

   irtree.parse_tree_traverse(tree, image_ct, cli)

## Functions ##

# Function that processes parsed CLI, modifying the passed “cli” object
# appropriately as it does. Returns the text of the file used for the build
# operation. Note that Python passes variables to functions by their object
# reference, so changes made to mutable objects (which “cli” is) will persist in
# the scope of the caller.'
def cli_process(cli):
   # Infer input file if needed.
   if (cli.file is None):
      cli.file = cli.context + "/Dockerfile"

   # Infer image name if needed.
   if (cli.tag is None):
      path = os.path.basename(cli.file)
      if ("." in path):
         (base, ext_all) = str(path).split(".", maxsplit=1)
         (base_all, ext_last) = str(path).rsplit(".", maxsplit=1)
      else:
         base = None
         ext_last = None
      if (base == "Dockerfile"):
         cli.tag = ext_all
         ch.VERBOSE("inferring name from Dockerfile extension: %s" % cli.tag)
      elif (ext_last in ("df", "dockerfile")):
         cli.tag = base_all
         ch.VERBOSE("inferring name from Dockerfile basename: %s" % cli.tag)
      elif (os.path.abspath(cli.context) != "/"):
         cli.tag = os.path.basename(os.path.abspath(cli.context))
         ch.VERBOSE("inferring name from context directory: %s" % cli.tag)
      else:
         assert (os.path.abspath(cli.context) == "/")
         cli.tag = "root"
         ch.VERBOSE("inferring name with root context directory: %s" % cli.tag)
      cli.tag = re.sub(r"[^a-z0-9_.-]", "", cli.tag.lower())
      ch.INFO("inferred image name: %s" % cli.tag)


   ch.DEBUG(cli)

   # Guess whether the context is a URL, and error out if so. This can be a
   # typical looking URL e.g. “https://...” or also something like
   # “git@github.com:...”. The line noise in the second line of the regex is
   # to match this second form. Username and host characters from
   # https://tools.ietf.org/html/rfc3986.
   if (re.search(r"""  ^((git|git+ssh|http|https|ssh)://
                     | ^[\w.~%!$&'\(\)\*\+,;=-]+@[\w.~%!$&'\(\)\*\+,;=-]+:)""",
                 cli.context, re.VERBOSE) is not None):
      ch.FATAL("not yet supported: issue #773: URL context: %s" % cli.context)
   if (os.path.exists(cli.context + "/.dockerignore")):
      ch.WARNING("not yet supported, ignored: issue #777: .dockerignore file")

   # Read input file.
   if (cli.file == "-" or cli.context == "-"):
      text = ch.ossafe("can’t read stdin", sys.stdin.read)
   elif (not os.path.isdir(cli.context)):
      ch.FATAL("context must be a directory: %s" % cli.context)
   else:
      fp = fs.Path(cli.file).open("rt")
      text = ch.ossafe("can’t read: %s" % cli.file, fp.read)
      ch.close_(fp)

   return text

# Process common opts between modify and build.
def cli_process_common(cli):
   # --force and friends.
   if (cli.force_cmd and cli.force == ch.Force_Mode.FAKEROOT):
      ch.FATAL("--force-cmd and --force=fakeroot are incompatible")
   if (not cli.force_cmd):
      cli.force_cmd = force.FORCE_CMD_DEFAULT
   else:
      cli.force = ch.Force_Mode.SECCOMP
      # convert cli.force_cmd to parsed dict
      force_cmd = dict()
      for line in cli.force_cmd:
         (cmd, args) = force.force_cmd_parse(line)
         force_cmd[cmd] = args
      cli.force_cmd = force_cmd
   ch.VERBOSE("force mode: %s" % cli.force)
   if (cli.force == ch.Force_Mode.SECCOMP):
      for (cmd, args) in cli.force_cmd.items():
         ch.VERBOSE("force command: %s" % ch.argv_to_string([cmd] + args))
   if (    cli.force == ch.Force_Mode.SECCOMP
       and ch.cmd([ch.CH_BIN + "/ch-run", "--feature=seccomp"],
                  fail_ok=True) != 0):
      ch.FATAL("ch-run was not built with seccomp(2) support")

   # Deal with build arguments.
   def build_arg_get(arg):
      kv = arg.split("=")
      if (len(kv) == 2):
         return kv
      else:
         v = os.getenv(kv[0])
         if (v is None):
            ch.FATAL("--build-arg: %s: no value and not in environment" % kv[0])
         return (kv[0], v)
   cli.build_arg = dict( build_arg_get(i) for i in cli.build_arg )

def parse_dockerfile(text, cli):
   # Parse it.
   parser = lark.Lark(im.GRAMMAR_DOCKERFILE, parser="earley",
                      propagate_positions=True, tree_class=im.Tree)
   # Avoid Lark issue #237: lark.exceptions.UnexpectedEOF if the file does not
   # end in newline.
   text += "\n"
   try:
      tree = parser.parse(text)
   except lark.exceptions.UnexpectedInput as x:
      ch.VERBOSE(x)  # noise about what was expected in the grammar
      ch.FATAL("can’t parse: %s:%d,%d\n\n%s"
               % (cli.file, x.line, x.column, x.get_context(text, 39)))
   ch.VERBOSE(tree.pretty()[:-1])  # rm trailing newline

   # Sometimes we exit after parsing.
   if (cli.parse_only):
      ch.exit(0)

   # If we use RSYNC, error out quickly if appropriate rsync(1) not present.
   if (tree.child("rsync") is not None):
      try:
         ch.version_check(["rsync", "--version"], ch.RSYNC_MIN)
      except ch.Fatal_Error:
         ch.ERROR("Dockerfile uses RSYNC, so rsync(1) is required")
         raise

   return tree