#!/usr/bin/python3
# Copyright (C) 2017 SUSE LLC
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import os
import re
import sys
import json
import stat
import shlex
import random
import shutil
import string
import hashlib
import logging
import argparse
import tempfile
import subprocess

__version__ = "0.1.0"

class attrdict(object):
	"Dumb implementation of attrdict."
	def __init__(self, *args, **kwargs):
		self.__dict__ = dict(*args, **kwargs)


class SubprocessError(Exception):
	pass

class DockerfileFormatError(Exception):
	pass


def os_path_join(*parts):
	"""
	os_path_join is a sane implementation of os_path_join when it comes to
	concatenating absolute paths. We also make all of the joining safe.
	"""
	if len(parts) == 0:
		parts = ["."]
	isabs = os.path.isabs(parts[0])

	parts = [os_path_clean(part) for part in parts]
	for idx, part in enumerate(parts):
		if part.startswith("/"):
			part = part.lstrip("/")
		parts[idx] = part

	if isabs:
		parts = ["/"] + parts
	return os.path.join(*parts)

def os_path_clean(orig_path):
	"""
	os_path_clean is a reimplementation of Go's path/filepath.Clean. This logic
	originally comes from Plan 9, and it returns a lexically identical path to
	the provided path (note that it might not agree with what you would get if
	you actually evaluted the filesystem).

	  >>> os_path_clean("abc")
	  'abc'
	  >>> os_path_clean("abc/.//def/")
	  'abc/def'
	  >>> os_path_clean("/./../abc/def")
	  '/abc/def'
	  >>> os_path_clean("abc/def/../ghi/../jkl")
	  'abc/jkl'
	  >>> os_path_clean("abc/def/../..")
	  '.'
	  >>> os_path_clean("abc/def/../../../ghi/jkl/../../../mno")
	  '../../mno'
	  >>> os_path_clean("/../abc")
	  '/abc'
	  >>> os_path_clean("abc//./../def")
	  'def'
	  >>> os_path_clean("abc/../../././../def")
	  '../../def'
	"""

	prev = None
	path = orig_path

	# We apply the same algorithm iteratively until it stops changing the input.
	while prev != path:
		parts = path.split(os.path.sep)

		# 1. Replace multiple Separator elements with a single one.
		parts = [part for part in parts if part != ""]

		# 2. Eliminate each . path name element (the current directory).
		parts = [part for part in parts if part != "."]

		# 3. Eliminate each inner .. path name element (the parent directory)
		#    along with the non-.. element that precedes it.
		new_parts = []
		for part in parts:
			if part != ".." or len(new_parts) == 0 or new_parts[-1] == "..":
				new_parts.append(part)
				continue
			new_parts = new_parts[:-1]
		parts = new_parts

		# 4. Eliminate .. elements that begin a rooted path;
		#    that is, replace "/.." by "/" at the beginning of a path,
		#    assuming Separator is '/'.
		if os.path.isabs(orig_path):
			while len(parts) > 0 and parts[0] == "..":
				parts = parts[1:]

		prev = path
		path = "."
		if parts:
			path = os.path.join(*parts)

	# The "multiple separator" code above silently converts absolute paths to
	# be relative to the root. So we have to fix that here.
	if os.path.isabs(orig_path):
		path = os.path.sep + path
	if path == "/.":
		path = "/"

	return path

def secure_join(root, unsafe):
	"""
	secure_join joins two given path components, similar to os.path.join,
	except that the returned path is guaranteed to be scoped inside the
	provided root path (when evaluated). Any symbolic links in the path are
	evaluated with the given root treated as the root of the filesystem,
	similar to a chroot.

	Note that the guarantees provided by this function only apply if the path
	components in the returned string are not modified (in other words are not
	replaced with symlinks or modified mountpoints) after this function has
	returned.  Such a symlink race is necessarily out-of-scope of secure_join.
	"""

	n = 0
	path = ""
	while unsafe:
		if n > 255:
			raise SecureJoinError("Too many levels of symbolic links")

		# Next path component, partial.
		try:
			i = unsafe.index(os.path.sep)
			partial, unsafe = unsafe[:i], unsafe[i+1:]
		except ValueError:
			partial, unsafe = unsafe, ""

		# Create a cleaned path, using the lexical semantics of /../a, to
		# create a "scoped" path component which can safely be joined to fullP
		# for evaluation. At this point, path.String() doesn't contain any
		# symlink components.
		clean_path = os_path_clean(os.path.sep + path + partial)
		if clean_path == os.path.sep:
			path = ""
			continue
		full_path = os_path_clean(root + clean_path)

		# Is the path a symlink?
		islnk = False
		try:
			stat_t = os.lstat(full_path)
			islnk = stat.S_ISLNK(stat_t.st_mode)
		except FileNotFoundError:
			pass

		# Treat non-existent path components the same as non-symlinks (we can't
		# do any better here).
		if not islnk:
			path += partial + os.path.sep
			continue

		# Only increment if we dereference a link.
		n += 1

		# Figure out the target link and clean up unsafe.
		dest = os.readlink(full_path)
		if os.path.isabs(dest):
			path = ""
		unsafe = dest + os.path.sep + unsafe

	full_path = os_path_clean(os.path.sep + path)
	return os_path_clean(root + full_path)

def expandvars(string, envs):
	"""
	Variable expansion implementation, and additionally implementing ${a:+a}
	and ${a:-a} modifiers (which are required for Dockerfile support).
	Currently this is implemented using regular expressions, so recursive shell
	definitions won't be correctly handled.
	"""

	def replace(matches):
		matches = [m for m in matches.groups() if m is not None]
		if len(matches) != 1:
			raise DockerfileFormatError("Unknown environment variable format: '%s'" % (matches,))

		var, modifier = matches[0], None
		if ":" in var:
			var, modifier = var.split(":", maxsplit=1)
			if len(modifier) == 0:
				raise DockerfileFormatError("Environment variable modifier cannot be empty: %s" % (var,))
			if modifier[0] not in {'+', '-'}:
				raise DockerfileFormatError("Invalid environment variable modifier: '%s'" % (modifier,))

		value = envs.get(var)
		if modifier is not None:
			mod, word = modifier[0], modifier[1:]
			if mod == '+' and value is not None:
				value = word
			elif mod == '-' and value is None:
				value = word

		return value

	# XXX: Variables are actually a context-free grammar, so using regular
	#      expressions is wrong here. We'd need to parse things properly.
	return re.sub(r"(?<!\\)\$(?:(\w+)|\{([^\}]+)\})", replace, string)

def os_system(*args):
	"""
	Execute a command in the foreground, waiting until the command exits.
	Standard I/O is inherited from this process.
	"""
	debug("Executing %s." % (args,))
	print("  ---> [%s]" % (args[0],))
	ret = subprocess.call(args, stdin=subprocess.DEVNULL)
	print("  <--- [%s]" % (args[0],))
	if ret != 0:
		raise SubprocessError("%s failed with error code %d" % (" ".join(args), ret))

# Wrappers/aliases for logging functions.
debug = logging.debug
info = logging.info
warn = logging.warn

def fatal(*args):
	logging.fatal(*args)
	sys.exit(1)

def generate_id(size=32, chars=string.ascii_uppercase + string.ascii_lowercase + string.digits):
	return ''.join(random.choice(chars) for _ in range(size))

def hash_digest(algo, contents):
	h = hashlib.new(algo)
	h.update(contents.encode("utf-8"))
	return h.hexdigest()


class DockerfileParser(object):
	"""
	DockerfileParser returns the parsed output
	"""

	def __init__(self, data):
		self.data = data

	def parse(self):
		# First clean up the comments and line continuations and then split up
		# the arguments and commands. While this isn't as "full featured" as
		# Docker's quite complicated Dockerfile parsing code, it should work
		# fine in most cases.
		self.data = re.sub(r"^#.*$", r"", self.data, flags=re.MULTILINE)
		self.data = re.sub(r"\\\n", r" ", self.data, flags=re.MULTILINE)

		# Split up the arguments and commands.
		steps = []
		for line in self.data.split('\n'):
			if not line.strip():
				continue

			cmd, rest = line.split(" ", maxsplit=1)

			# Try to parse it as JSON and fall back to regular splitting.
			# NOTE: This doesn't handle the fact that some commands don't
			#       accept JSON arguments. We can handle that later though.
			#       Also I'm not sure how we should handle --style arguments to
			#       commands.
			args = None
			try:
				args = json.loads(rest.strip())
			except json.decoder.JSONDecodeError:
				pass

			# Commands only accept JSON lists.
			isjson = isinstance(args, list)
			if not isjson:
				args = shlex.split(rest)

			steps.append(attrdict({
				"cmd": cmd,
				"args": args,
				"isjson": isjson,
			}))

		# Make sure there's at least one step.
		if len(steps) == 0:
			raise DockerfileFormatError("Dockerfile contained no instructions.")

		# The first step must be FROM.
		if steps[0].cmd.lower() != "from":
			raise DockerfileFormatError("Dockerfiles must start with FROM.")

		# Return the steps.
		return steps


class Builder(object):
	"""
	Builder represents a builder instance.
	"""

	VARIABLE_COMMANDS = {"add", "copy", "env", "expose", "label", "user", "workdir", "volume", "stopsignal"}

	def safepath(self, path):
		"Returns a safe version of the given path."
		return secure_join(self.root, path)

	def __init__(self, root, build_args=None, script="Dockerfile", rootless=False):
		self.root = root
		self.rootless = rootless

		# Internal tags and image information.
		self.image_path = None
		self.our_tags = []
		self.source_tag = None
		self.destination_tag = None
		self.runc_root = tempfile.mkdtemp(prefix="orca-runcroot.")
		debug("Created new runC root for build: %s" % (self.runc_root,))

		# Instruction metadata.
		self.default_shell = ["/bin/sh", "-c"]
		self.build_args = build_args
		if self.build_args is None:
			self.build_args = {}

		# The actual script.
		with open(self.safepath(script)) as f:
			contents = f.read()
			self.script = DockerfileParser(contents).parse()
			self.script_hash = hash_digest("sha256", contents) + "-dest"

		# Tool paths.
		# TODO: Make these configurable and absolute paths.
		self.skopeo = "skopeo"
		self.umoci = "umoci"
		self.runc = "runc"

	def umoci_config(self, *args):
		# Update the configuration.
		oci_source = "%s:%s" % (self.image_path, self.source_tag)
		oci_dest = self.destination_tag
		os_system(self.umoci, "config", "--image="+oci_source, "--tag="+oci_dest, *args)

		# The destination has become the ma^H^Hsource.
		self.source_tag = self.destination_tag

	def umoci_unpack_bundle(self):
		"Extracts the self.source_tag into a new bundle and returns the path."

		bundle_path = tempfile.mkdtemp(prefix="orca-bundle.")
		debug("Created new bundle for build: %s" % (bundle_path,))

		oci_source = "%s:%s" % (self.image_path, self.source_tag)
		unpack_args = ["unpack"]
		if self.rootless:
			unpack_args += ["--rootless"]
		unpack_args += ["--image="+oci_source, bundle_path]
		os_system(self.umoci, *unpack_args)

		return bundle_path

	def umoci_repack_bundle(self, bundle_path):
		"Repacks the given bundle into self.destination_tag and deletes it."

		# Repack the image.
		oci_destination = "%s:%s" % (self.image_path, self.destination_tag)
		os_system(self.umoci, "repack", "--image="+oci_destination, bundle_path)
		# TODO: Delete the bundle.

		# The destination has become the ma^H^Hsource.
		self.source_tag = self.destination_tag

	def umoci_runtimejson(self, bundle_path=None):
		"""
		Return the runtime-spec config.json generated from a given image. This
		is a hack at the moment (and is quite inefficient) but will be improved
		in umoci in the future with umoci-raw-config(1). This method returns
		the parsed JSON.
		"""

		if bundle_path is None:
			_, config_path = tempfile.mkstemp(prefix="config.json.")
			oci_source = "%s:%s" % (self.image_path, self.source_tag)
			os_system(self.umoci, "raw", "runtime-config", "--image="+oci_source, config_path)
		else:
			config_path = os_path_join(bundle_path, "config.json")

		with open(config_path) as f:
			return json.load(f)

	def compute_env(self, config, build_args=None):
		"""
		Compute the environment variable dictionary, substituting build_args
		as per the Dockerfile precedence rules.
		"""

		if build_args is None:
			build_args = self.build_args

		# ENV always overrides ARG.
		envdict = build_args.copy()
		for env in config["process"]["env"]:
			key, value = env, ""
			if "=" in key:
				key, value = env.split("=", maxsplit=1)
			envdict[key] = value

		return envdict

	def _generate_destination(self):
		self.destination_tag = hash_digest("sha256", self.script_hash + str(self.destination_tag) + "-next") + "-dest"
		self.our_tags.append(self.destination_tag)

	def _dispatch_from(self, *args, isjson=False):
		if len(args) != 1:
			raise DockerfileFormatError("FROM can only have one argument.")
		if isjson:
			raise DockerfileFormatError("FROM doesn't support JSON arguments.")
		docker_source = "docker://%s" % (args[0],)

		if self.source_tag is None:
			self.source_tag = hash_digest("sha256", docker_source) + "-src"
			self.our_tags.append(self.source_tag)
		else:
			raise RuntimeError("source_tag is not None in FROM: %s" % (self.source_tag,))

		if docker_source != "docker://scratch":
			# TODO: At the moment we only really support importing from a Docker
			#       registry. We need to extend the Dockerfile syntax to enable
			#       sourcing OCI images from the local filesystem.
			oci_destination = "oci:%s:%s" % (self.image_path, self.source_tag)
			os_system(self.skopeo, "copy", docker_source, oci_destination)
		else:
			os.rmdir(self.image_path)
			os_system(self.umoci, "init", "--layout="+self.image_path)
			os_system(self.umoci, "new", "--image=%s:%s" % (self.image_path, self.source_tag))

	def _dispatch_run(self, *args, isjson=False):
		bundle_path = self.umoci_unpack_bundle()
		config = self.umoci_runtimejson(bundle_path)

		# Modify the config.
		if not isjson:
			args = self.default_shell + [" ".join(args)]
		config["process"]["args"] = args
		config["process"]["terminal"] = False # XXX: Currently terminal=true breaks because stdin is /dev/null.
		config["root"]["readonly"] = False
		# ARG doesn't persist in the image, so we have to set it here.
		config["process"]["env"] = ["%s=%s" % (k, v) for k, v in self.compute_env(config).items()]

		# Save the modified config.json.
		config_path = os_path_join(bundle_path, "config.json")
		with open(config_path, "w") as f:
			json.dump(config, f)

		# Run the container.
		ctr_id = "orca-build-" + generate_id()
		os_system(self.runc, "--root="+self.runc_root, "run", "--bundle="+bundle_path, ctr_id)

		# Repack the damn thing.
		self.umoci_repack_bundle(bundle_path)

	def _dispatch_entrypoint(self, *args, isjson=False):
		# The interactions with CMD and ENTRYPOINT are quite complicated
		# because of backwards compatibility. Unfortuately that means that it's
		# quite hard to emulate certain cases (specifically the shell form of
		# ENTRYPOINT). Thus we emit a warning.
		# https://docs.docker.com/v1.13/engine/reference/builder/#understand-how-cmd-and-entrypoint-interact
		if not isjson:
			warn("Using ENTRYPOINT in the shell form is not a good idea. Orca can't emulate some of the Docker semantics at the moment. Consider switching to the JSON form.")

		# Decide whether we need to clear or change the entrypoint.
		if len(args) == 0:
			entrypoint_args = ["--clear=config.entrypoint"]
		else:
			if not isjson:
				args = self.default_shell + [" ".join(args)]
			entrypoint_args = ["--config.entrypoint="+arg for arg in args]
		self.umoci_config(*entrypoint_args)

	def _dispatch_cmd(self, *args, isjson=False):
		# Same logic as ENTRYPOINT.
		# https://docs.docker.com/v1.13/engine/reference/builder/#understand-how-cmd-and-entrypoint-interact
		if not isjson:
			warn("Using CMD in the shell form is not a good idea. Orca can't emulate some of the Docker semantics at the moment. Consider switching to the JSON form.")

		# Decide whether we need to clear or change the cmd.
		if len(args) == 0:
			cmd_args = ["--clear=config.cmd"]
		else:
			if not isjson:
				args = self.default_shell + [" ".join(args)]
			cmd_args = ["--config.cmd="+arg for arg in args]
		self.umoci_config(*cmd_args)

	def _dispatch_label(self, *args, isjson=False):
		if isjson:
			raise DockerfileFormatError("LABEL doesn't support JSON arguments.")
		# Generate args.
		label_args = ["--config.label="+arg for arg in args]
		self.umoci_config(*label_args)

	def _dispatch_maintainer(self, *args, isjson=False):
		if isjson:
			raise DockerfileFormatError("MAINTAINER doesn't support JSON arguments.")
		# Generate args.
		author = " ".join(args)
		maintainer_args = ["--author="+author, "--config.label=maintainer="+author]
		self.umoci_config(*maintainer_args)

	def _dispatch_expose(self, *args, isjson=False):
		if isjson:
			raise DockerfileFormatError("EXPOSE doesn't support JSON arguments.")
		# Generate args.
		# NOTE: There's no way AFAIK of clearing exposedports from a Dockerfile.
		expose_args = ["--config.exposedports="+arg for arg in args]
		self.umoci_config(*expose_args)

	def _dispatch_copy(self, *args, isjson=False):
		if len(args) != 2:
			# TODO: This isn't true and needs to be extended.
			raise DockerfileFormatError("COPY can only have two arguments.")

		bundle_path = self.umoci_unpack_bundle()
		rootfs_path = os_path_join(bundle_path, "rootfs")

		# Copy the source to the destination.
		src = secure_join(self.root, args[0])
		dst = secure_join(rootfs_path, args[1])
		debug("recursive copy %s -> %s" % (src, dst))
		if os.path.isdir(dst):
			# This probably isn't correct.
			dst = secure_join(dst, os.path.basename(src))
		if os.path.isdir(src):
			shutil.copytree(src, dst, symlinks=True)
		else:
			shutil.copy2(src, dst)

		# Repack the damn thing.
		self.umoci_repack_bundle(bundle_path)

	def _dispatch_add(self, *args, isjson=False):
		warn("ADD implementation doesn't support {decompression,remote,chown} at the moment.")
		return self._dispatch_copy(self, *args, isjson=isjson)

	def _dispatch_volume(self, *args, isjson=False):
		# Generate args.
		# NOTE: There's no way AFAIK of clearing volumes from a Dockerfile.
		volume_args = ["--config.volume="+arg for arg in args]
		self.umoci_config(*volume_args)

	def _dispatch_user(self, *args, isjson=False):
		if len(args) != 1:
			raise DockerfileFormatError("USER can only have one argument.")
		if isjson:
			raise DockerfileFormatError("USER doesn't support JSON arguments.")

		# Generate args.
		user_args = ["--config.user="+args[0]]
		self.umoci_config(*user_args)

	def _dispatch_workdir(self, *args, isjson=False):
		if len(args) != 1:
			raise DockerfileFormatError("WORKDIR can only have one argument.")
		if isjson:
			raise DockerfileFormatError("WORKDIR doesn't support JSON arguments.")

		# Generate args.
		workdir_args = ["--config.workdir="+args[0]]
		self.umoci_config(*workdir_args)

	def _dispatch_env(self, *args, isjson=False):
		if isjson:
			raise DockerfileFormatError("ENV doesn't support JSON arguments.")
		# Generate args.
		# NOTE: There's no way AFAIK of clearing environment variables from a Dockerfile.
		env_args = ["--config.env="+arg for arg in args]
		self.umoci_config(*env_args)

	def _dispatch_arg(self, *args, isjson=False):
		if len(args) != 1:
			raise DockerfileFormatError("ARG can only have one argument.")
		if isjson:
			raise DockerfileFormatError("ARG doesn't support JSON arguments.")

		# ARG doesn't persist in the image.
		arg = args[0]
		if "=" not in arg:
			# It's not clear what Docker does here when --build-arg hasn't been
			# used either. It looks like they just ignore this case?
			return
		name, value = arg.split("=", maxsplit=1)
		if name not in self.build_args:
			self.build_args[name] = value

	def _dispatch_shell(self, *args, isjson=False):
		if not isjson:
			raise DockerfileFormatError("SHELL only supports JSON arguments.")
		self.default_shell = list(args)

	def _dispatch_stopsignal(self, *args, isjson=False):
		if len(args) != 1:
			raise DockerfileFormatError("STOPSIGNAL can only have one argument.")
		if isjson:
			raise DockerfileFormatError("STOPSIGNAL doesn't support JSON arguments.")
		warn("STOPSIGNAL is not yet implemented (requires newer OCI imagespec)")

	def _dispatch_onbuild(self, *args, isjson=False):
		warn("ONBUILD is not supported by OCI. Ignoring.")

	def _dispatch_healthcheck(self, *args, isjson=False):
		warn("HEALTHCHECK is not supported by OCI. Ignoring.")

	def build(self, output=None, tags=None, clean=False, gc=False, **_):
		if output is None:
			self.image_path = tempfile.mkdtemp(prefix="orca-build.")
			info("Created new image for build: %s" % (self.image_path,))
		else:
			self.image_path = output
			info("Using existing image for build: %s" % (self.image_path,))

		for idx, step in enumerate(self.script):
			cmd = step.cmd.lower()
			args = step.args
			isjson = step.isjson

			info("BUILD[%d of %d]: %s %r [json=%r]" % (idx+1, len(self.script), cmd, args, isjson))

			try:
				# Variable substitution.
				if cmd in self.VARIABLE_COMMANDS:
					envs = self.compute_env(self.umoci_runtimejson())
					args = [expandvars(arg, envs) for arg in args]

				# Dispatch to method.
				fn = "_dispatch_%s" % (cmd,)
				if hasattr(self, fn):
					# Do this before each step.
					self._generate_destination()
					getattr(self, fn)(*args, isjson=step.isjson)
				else:
					raise DockerfileFormatError("Unknown build command %s" % (cmd,))
			except DockerfileFormatError as e:
				fatal("Dockerfile format error: %s" % (e.args[0],))
			except SubprocessError as e:
				fatal("Error executing subprocess: %s" % (e.args[0],))

		info("BUILD: finished")

		# Create the output tag.
		if tags is not None:
			oci_source = "%s:%s" % (self.image_path, self.source_tag)
			for tag in tags:
				os_system(self.umoci, "tag", "--image="+oci_source, tag)
		else:
			tags = [self.source_tag]
			self.our_tags = [tag for tag in self.our_tags if tag != self.source_tag]

		info("BUILD: created tags %r" % (tags,))

		# Clean any unused tags.
		if clean:
			for tag in self.our_tags:
				oci_source = "%s:%s" % (self.image_path, tag)
				os_system(self.umoci, "rm", "--image="+oci_source)
			info("BUILD: cleaned intermediate tags")

		# Garbage collect.
		if gc:
			os_system(self.umoci, "gc", "--layout="+self.image_path)
			info("BUILD: garbage collected unused blobs")


def main(ctx, config):
	if not os.path.exists(ctx):
		fatal("Context %s doesn't exist." % (ctx,))

	builder = Builder(ctx, build_args=config.build_args, rootless=config.rootless)
	builder.build(output=config.output, tags=config.tags, clean=config.clean, gc=config.gc)

if __name__ == "__main__":
	def __wrapped_main__():
		class BuildArgsAction(argparse.Action):
			def __call__(self, parser, namespace, argument, option_string):
				if "=" not in argument:
					parser.error("--build-arg requires arguments of format NAME=value")

				name, value = argument.split("=", maxsplit=1)
				if getattr(namespace, self.dest, None) is None:
					setattr(namespace, self.dest, {})
				getattr(namespace, self.dest)[name] = value

		parser = argparse.ArgumentParser(description="Build an OCI image from a Dockerfile context. Rootless containers are also supported out-of-the-box.")
		# Orca commands.
		parser.add_argument("--clean", action="store_true", help="Remove all intermediate image tags after successful build.")
		parser.add_argument("--gc", action="store_true", help="Run a final garbage collection on output image.")
		parser.add_argument("--output", default=None, help="Path of OCI image to output to (if unspecified, a new image is created in /tmp).")
		parser.add_argument("--verbose", action="store_const", const=True, help="Output debugging information.")
		parser.add_argument("--rootless", action="store_const", const=True, default=False, help="Enable rootless containers mode.")
		parser.add_argument("--version", action="version", version="%(prog)s " + __version__, help="Version information.")
		# Docker compat.
		parser.add_argument("--build-arg", metavar="NAME=value", dest="build_args", action=BuildArgsAction, help="Build-time arguments used in conjunction with ARG.")
		parser.add_argument("-t", "--tag", dest="tags", action="append", default=None, help="Tag(s) of the output image (by default, randomly generated).")
		parser.add_argument("ctx", nargs=1, help="Build context which is used when referencing host files. Files outside the build context cannot be accessed by the build script.")

		config = parser.parse_args()
		ctx = config.ctx[0]

		level = logging.INFO
		if config.verbose:
			level = logging.DEBUG
		logging.basicConfig(format="orca-build[%(levelname)s] %(message)s", level=level)
		main(ctx, config)

	__wrapped_main__()
