-
Sébastien Luttringer authoredaf952ba4
is 30.06 KiB
#!/usr/bin/python
# -*- coding: utf-8 -*-
# This file is part of Installsystems.
#
# Installsystems is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Installsystems 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Installsystems. If not, see <http://www.gnu.org/licenses/>.
'''
InstallSystems Command line Tool
'''
import os
import datetime
import re
import fnmatch
import warnings
import argparse
import psutil
import socket
import sys
import locale
import installsystems
import installsystems.printer
import installsystems.tools as istools
from installsystems.exception import *
from installsystems.printer import *
from installsystems.repository import Repository
from installsystems.repository import RepositoryManager
from installsystems.repository import RepositoryConfig
from installsystems.image import PackageImage, SourceImage
from installsystems.config import MainConfigFile, RepoConfigFile
################################################################################
# Common functions
################################################################################
def load_repositories(args):
'''
Load repositories on a repository manager
'''
# remove cache is asked
if args.no_cache:
args.cache = None
# split filter and search in list
args.repo_filter = Repository.split_repository_list(args.repo_filter)
args.repo_search = Repository.split_repository_list(args.repo_search)
# init repo cache object
repoman = RepositoryManager(args.cache, timeout=args.repo_timeout or args.timeout,
filter=args.repo_filter, search=args.repo_search)
# register repositories (order matter)
# load repo configs from command line
if args.repo_path != "":
repoconf = RepositoryConfig(istools.smd5sum(args.repo_path)[:8],
path=args.repo_path)
repoman.register(repoconf, temp=True, nosync=args.no_sync)
# load repo configs from config
for repoconf in RepoConfigFile(args.repo_config).repos:
repoman.register(repoconf, nosync=args.no_sync)
return repoman
def get_images(patterns, repoman, local=True, min=None, max=None):
'''
Select and load a package image from a standard naming type
Allowed type are a direct filename on filesystem
or [repo/]image[:version]
Return the repository as second argument
'''
ans = []
for pattern in patterns:
# check if image is a local file
if local and istools.isfile(pattern) and os.path.isfile(pattern):
ans.append((pattern, None))
else: # we need to find image in a repository
ans += sorted(repoman.select_images([pattern]).items())
# check selected images cound
if min is not None and len(ans) < min:
raise ISError(u"%s images found. Should be at least %s" % (
len(ans), min))
# check max selected images
if max is not None and len(ans) > max:
raise ISError(u"Too many selected images: %s. Max is %s" % (
", ".join([n[0] for n in ans]), max))
for item in ans:
if item[1] is None:
yield PackageImage(item[0]), None
else:
r = item[1]
yield repoman[r["repo"]].get(r["name"], r["version"]), repoman[r["repo"]]
################################################################################
# Commands functions
################################################################################
def c_add(args):
'''
Add packaged images into a repository
'''
repoman = load_repositories(args)
repo = repoman[args.repository]
for image in args.path:
pkg = PackageImage(image)
repo.add(pkg, delete=not args.preserve)
def c_build(args):
'''
Build a source image in the current directory
'''
gdt = 0
for path in args.paths:
arrow("Build %s" % path)
# chdir inside path if --chdir
if args.chdir:
cwd = os.getcwdu()
os.chdir(path)
path = "."
arrowlevel(1)
# load source image
simg = SourceImage(path)
# do the job
dt = simg.build(force=args.force, force_payload=args.payload,
check=not args.no_check, script=not args.no_script)
gdt += dt
arrow(u"Build time: %s" % datetime.timedelta(seconds=dt))
if args.chdir:
os.chdir(cwd)
arrowlevel(-1)
if len(args.paths) > 1:
arrow(u"Global build time: %s" % datetime.timedelta(seconds=gdt))
def c_cat(args):
'''
Display files inside a packaged image
'''
repoman = load_repositories(args)
image, repo = next(get_images([args.pattern], repoman, min=1, max=1))
for filename in args.file:
image.cat(filename)
def c_changelog(args):
'''
Display changelog of packaged images
'''
repoman = load_repositories(args)
for image, repo in get_images(args.pattern, repoman, min=1):
image.changelog.show(int(image.version), args.all_version)
def c_check(args):
'''
Sanity checks on repositories
'''
repoman = load_repositories(args)
for reponame in args.repository:
repoman[reponame].check()
def c_chroot(args):
'''
Helper to go cleanly inside a chroot
'''
istools.chroot(args.path, shell=args.shell, mount=not args.no_mount)
def c_clean(args):
'''
Remove unreferenced files from repositories
'''
repoman = load_repositories(args)
for reponame in args.repository:
repoman[reponame].clean(args.force)
def c_copy(args):
'''
Copy an image from a repository to another one
'''
repoman = load_repositories(args)
dstrepo = repoman[args.repository]
todo = list(get_images(args.pattern, repoman, local=False, min=1))
# check user really want to this
if not args.force:
out("You will copy the following images:")
for img, repo in todo:
out(u" %s/%s:%s" % (repo.config.name, img.name, img.version))
out(u"Inside repository: #l##b#%s#R#" % dstrepo.config.name)
if not confirm():
raise ISError("Aborted!")
# copy it for real
for srcimg, srcrepo in todo:
arrow("Copying %s v%s from repository %s to %s" %
(srcimg.name, srcimg.version,
srcrepo.config.name, dstrepo.config.name))
arrowlevel(1)
dstrepo.add(srcimg)
arrowlevel(-1)
def c_del(args):
'''
Remove an image package from a repository
'''
repoman = load_repositories(args)
todo = list(get_images(args.pattern, repoman, local=False, min=1))
# check all source repository are local (need by deletion)
for img, repo in todo:
if not repo.local:
raise ISError("Repository %s is not local. Unable to delete" %
repo.config.name)
# check user really want to this
if not args.force:
out("You will remove the following images:")
for img, repo in todo:
out(u" %s/%s:%s" % (repo.config.name, img.name, img.version))
if not confirm():
raise ISError("Aborted!")
# delete it for real
for img, repo in todo:
arrow("Deleting %s v%s from repository %s" %
(img.name, img.version, repo.config.name))
arrowlevel(1)
repo.delete(img.name, img.version, payloads=not args.preserve)
arrowlevel(-1)
def c_diff(args):
'''
Show difference between two repositories or packaged images
'''
repoman = load_repositories(args)
if args.object[0] in repoman.onlines and args.object[1] in repoman.onlines:
Repository.diff(repoman[args.object[0]], repoman[args.object[1]])
else:
img = get_images(args.object, repoman, min=2, max=2)
img1, repo1 = next(img)
img2, repo2 = next(img)
PackageImage.diff(img1, img2)
def c_extract(args):
'''
Extract a packaged image inside a directory
'''
repoman = load_repositories(args)
for image, repo in get_images([args.pattern], repoman, min=1, max=1):
image.extract(args.path, payload=args.payload, force=args.force,
gendescription=args.gen_description)
def c_get(args):
'''
Get packaged images from repository to current directory
'''
repoman = load_repositories(args)
for image, repo in get_images(args.pattern, repoman, local=False, min=1):
image.download(".", image=not args.no_image, payload=args.payload, force=args.force)
def c_help(args):
'''
Show help
'''
if args.command not in args.subparser.choices:
args.parser.print_help()
else:
args.subparser.choices[args.command].print_help()
def c_info(args):
'''
Display info about packaged images
'''
repoman = load_repositories(args)
for image, repo in get_images(args.pattern, repoman, min=1):
image.show(o_verbose=args.verbose, o_changelog=args.changelog,
o_json=args.json)
def c_init(args):
'''
Initialize an empty repository
'''
repoman = load_repositories(args)
for reponame in args.repository:
repoman[reponame].init()
def c_install(args):
'''
Install a packaged image
'''
# remove old image args
args.install_parser._remove_action(
[d for d in args.install_parser._actions if d.dest == "pattern"][0])
# create a subparser for current image to have a sexy display of args
subparser = args.install_parser.add_subparsers().add_parser(args.pattern)
# select image to install
repoman = load_repositories(args)
image, repo = next(get_images([args.pattern], repoman, min=1, max=1))
# Print setup information
arrow(u"Installing %s v%s" % (image.name, image.version))
# let's go
dt = image.run(args.parser, subparser, run_setup=not args.dry_run)
arrow(u"Install time: %s" % datetime.timedelta(seconds=dt))
def c_list(args):
'''
List packaged images in repositories
'''
repoman = load_repositories(args)
if len(args.pattern) == 0 and len(repoman.search) == 0:
args.pattern = ["*/*"]
elif len(args.pattern) == 0:
args.pattern = ["*"]
repoman.show_images(args.pattern, o_long=args.long, o_json=args.json,
o_md5=args.md5, o_date=args.date, o_author=args.author,
o_size=args.size, o_url=args.url,
o_description=args.description)
def c_move(args):
'''
Move packaged image from a repository to another one
'''
repoman = load_repositories(args)
dstrepo = repoman[args.repository]
todo = list(get_images(args.pattern, repoman, local=False, min=1))
# check all source repository are local (need by deletion)
for img, repo in todo:
if not repo.local:
raise ISError("Repository %s is not local. Unable to move" %
repo.config.name)
# check user really want to this
if not args.force:
out("You will copy and remove the following images:")
for img, repo in todo:
out(u" %s/%s:%s" % (repo.config.name, img.name, img.version))
out(u"Inside repository: #l##b#%s#R#" % dstrepo.config.name)
if not confirm():
raise ISError("Aborted!")
# move it for real
for srcimg, srcrepo in todo:
arrow("Moving %s v%s from repository %s to %s" %
(srcimg.name, srcimg.version,
srcrepo.config.name, dstrepo.config.name))
arrowlevel(1)
dstrepo.add(srcimg)
srcrepo.delete(srcimg.name, srcimg.version)
arrowlevel(-1)
def c_new(args):
'''
Create a new source image
'''
SourceImage.create(args.path, args.force)
def c_payload(args):
'''
List payloads
'''
repoman = load_repositories(args)
repoman.show_payloads(args.payload, o_images=args.images, o_json=args.json)
def c_prepare_chroot(args):
'''
Helper to prepare a path to be chrooted
'''
istools.prepare_chroot(args.path, mount=not args.no_mount)
def c_repo(args):
'''
List repositories
'''
# in cleaning mode we doesn't needs to sync repositories
if args.purge:
args.no_sync = True
repoman = load_repositories(args)
if args.purge:
repoman.purge_repositories(args.repository)
else:
repoman.show_repositories(args.repository,
online=args.online, local=args.local,
o_url=args.url, o_state=args.state,
o_json=args.json)
def c_search(args):
'''
Search for packaged images in repositories
'''
repoman = load_repositories(args)
repoman.search_image(args.pattern)
def c_unprepare_chroot(args):
'''
Helper to remove chroot preparation of a path
'''
istools.unprepare_chroot(args.path, mount=not args.no_umount)
def c_version(args):
'''
Display installsystems version
'''
out(installsystems.version)
def arg_parser_init():
'''
Create command parser
'''
# Top level argument parsing
parser = argparse.ArgumentParser()
parser.add_argument("-V", "--version", action="version",
version=installsystems.version)
# exclusive group on verbosity
g = parser.add_mutually_exclusive_group()
g.add_argument("-v", "--verbosity", default=1,
type=int, choices=[0,1,2],
help="define verbosity level (0: quiet, 1:normal, 2:debug)")
g.add_argument("-d", "--debug", dest="verbosity",
action="store_const", const=2,
help="active debug mode")
g.add_argument("-q", "--quiet", dest="verbosity",
action="store_const", const=0,
help="active quiet mode")
# common options
parser.add_argument("-c", "--config", default=u"installsystems",
metavar="PATH", help="config file path")
parser.add_argument("-R", "--repo-config", default=u"repository",
metavar="REPO", help="repository config file path")
parser.add_argument("-s", "--repo-search", default=u"",
metavar="REPO,REPO,...",
help="search for images inside those repositories")
parser.add_argument("-f", "--repo-filter", default=u"",
metavar="REPO,REPO,...",
help="filter repositories by name")
parser.add_argument("-r", "--repo-path", default=u"", metavar="PATH",
help="define a temporary repository")
parser.add_argument("-T", "--repo-timeout", type=int, default=None,
metavar="SECONDS", help="repository access timeout")
parser.add_argument("-C", "--cache", default=u"", metavar="PATH",
help="path of repositories cache")
parser.add_argument("-t", "--timeout", dest="timeout", type=int, default=None,
metavar="SECONDS", help="socket timeout")
parser.add_argument("--no-cache", action="store_true",
help="not use persistent database caching")
parser.add_argument("--no-sync", action="store_true",
help="doesn't sync repository database cache")
parser.add_argument("--no-color", action="store_true",
help="dot not display colored output")
parser.add_argument("--nice", type=int, default=None,
help="nice of the process")
parser.add_argument("--ionice-class", choices=["none","rt", "be","idle"],
help="ionice class of the process (default: none)")
parser.add_argument("--ionice-level", type=int, default=None,
help="ionice class level of the process")
# create a subparser for commands
subparser = parser.add_subparsers()
# add command parser
p = subparser.add_parser("add", help=c_add.__doc__.lower())
p.add_argument("-p", "--preserve", action="store_true",
help="don't remove image after adding to database")
p.add_argument("repository", help="repository where images will be added")
p.add_argument("path", nargs="+", help="local packaged image path")
p.set_defaults(func=c_add)
# build command parser
p = subparser.add_parser("build", help=c_build.__doc__.lower())
p.add_argument("-c", "--no-check", action="store_true",
help="do not check compilation before adding scripts")
p.add_argument("-C", "--chdir", action="store_true",
help="build image inside source image directory, not in current directory")
p.add_argument("-f", "--force", action="store_true",
help="rebuild image if already exists")
p.add_argument("-p", "--payload", action="store_true",
help="rebuild payloads if already exists")
p.add_argument("-s", "--no-script", action="store_true",
help="doesn't execute build script")
p.add_argument("paths", nargs="*", default=u".")
p.set_defaults(func=c_build)
# cat command parser
p = subparser.add_parser("cat", help=c_cat.__doc__.lower())
p.add_argument("pattern", help="path|[repository/]image[:version]")
p.add_argument("file", nargs="+",
help="file inside image to cat (globbing allowed)")
p.set_defaults(func=c_cat)
# changelog command parser
p = subparser.add_parser("changelog", help=c_changelog.__doc__.lower())
p.add_argument("-v", "--all-version", action="store_true",
help="display changelog for all versions")
p.add_argument("pattern", nargs="+", help="path|[repository/]image[:version]")
p.set_defaults(func=c_changelog)
# check command parser
p = subparser.add_parser("check", help=c_check.__doc__.lower())
p.add_argument("repository", nargs="+", help="repositories to check")
p.set_defaults(func=c_check)
# chroot command parser
p = subparser.add_parser("chroot", help=c_chroot.__doc__.lower())
p.add_argument("-m", "--no-mount", action="store_true",
help="disable mounting of /{proc,dev,sys} inside chroot")
p.add_argument("-s", "--shell", default=u"/bin/bash",
help="shell to call inside chroot")
p.add_argument("path")
p.set_defaults(func=c_chroot)
# clean command parser
p = subparser.add_parser("clean", help=c_clean.__doc__.lower())
p.add_argument("-f", "--force", action="store_true",
help="clean repository without confirmation")
p.add_argument("repository", nargs="+", help="repositories to clean")
p.set_defaults(func=c_clean)
# copy command parser
p = subparser.add_parser("copy", help=c_copy.__doc__.lower())
p.add_argument("-f", "--force", action="store_true",
help="copy image without confirmation")
p.add_argument("pattern", nargs="+",
help="[repository/]image[:version]")
p.add_argument("repository", help="destination repository")
p.set_defaults(func=c_copy)
# del command parser
p = subparser.add_parser("del", help=c_del.__doc__.lower())
p.add_argument("pattern", nargs="+",
help="[repository/]image[:version]")
p.add_argument("-f", "--force", action="store_true",
help="delete image without confirmation")
p.add_argument("-p", "--preserve", action="store_true",
help="preserve payloads. doesn't remove it from repository")
p.set_defaults(func=c_del)
# diff command parser
p = subparser.add_parser("diff", help=c_diff.__doc__.lower())
p.add_argument("object", nargs="+",
help="path|repository|[repository/]image[:version]")
p.set_defaults(func=c_diff)
# extract command parser
p = subparser.add_parser("extract", help=c_extract.__doc__.lower())
p.add_argument("-f", "--force", action="store_true",
help="overwrite existing destinations")
p.add_argument("-g", "--gen-description", action="store_true",
help="generate a description file from metadata")
p.add_argument("-p", "--payload", action="store_true",
help="extract payloads")
p.add_argument("pattern",
help="path|[repository/]image[:version]")
p.add_argument("path", help="image will be extracted in path")
p.set_defaults(func=c_extract)
# get command parser
p = subparser.add_parser("get", help=c_get.__doc__.lower())
p.add_argument("-f", "--force", action="store_true",
help="overwrite existing destinations")
p.add_argument("-I", "--no-image", action="store_true",
help="do not get image")
p.add_argument("-p", "--payload", action="store_true",
help="get payloads")
p.add_argument("pattern", nargs="+",
help="[repository/]image[:version]")
p.set_defaults(func=c_get)
# help command parser
p = subparser.add_parser("help", help=c_help.__doc__.lower())
p.add_argument("command", nargs="?", help="command name")
p.set_defaults(func=c_help, parser=parser, subparser=subparser)
# info command parser
p = subparser.add_parser("info", help=c_info.__doc__.lower())
p.add_argument("-c", "--changelog", action="store_true",
help="display image changelog")
p.add_argument("-j", "--json", action="store_true",
help="output is formated in json")
p.add_argument("-v", "--verbose", action="store_true",
help="verbose output")
p.add_argument("pattern", nargs="+",
help="path|[repository/]image[:version]")
p.set_defaults(func=c_info)
# init command parser
p = subparser.add_parser("init", help=c_init.__doc__.lower())
p.add_argument("repository", nargs="+",
help="repository to initialize")
p.set_defaults(func=c_init)
# install command parser
p = subparser.add_parser("install", add_help=False,
help=c_install.__doc__.lower())
p.add_argument("--dry-run", action="store_true",
help="doesn't execute setup scripts")
p.add_argument("pattern", help="path|[repository/]image[:version]")
p.set_defaults(func=c_install, parser=parser, install_parser=p)
# list command parser
p = subparser.add_parser("list", help=c_list.__doc__.lower())
p.add_argument("-A", "--author", action="store_true",
help="display image author")
p.add_argument("-d", "--date", action="store_true",
help="display image date")
p.add_argument("-D", "--description", action="store_true",
help="display image description")
p.add_argument("-j", "--json", action="store_true",
help="output is formated in json")
p.add_argument("-l", "--long", action="store_true",
help="long display")
p.add_argument("-m", "--md5", action="store_true",
help="display image md5")
p.add_argument("-s", "--size", action="store_true",
help="display image size")
p.add_argument("-u", "--url", action="store_true",
help="display image url")
p.add_argument("pattern", nargs="*", default=[],
help="[repository/]image[:version]")
p.set_defaults(func=c_list)
# move command parser
p = subparser.add_parser("move", help=c_move.__doc__.lower())
p.add_argument("-f", "--force", action="store_true",
help="move image without confirmation")
p.add_argument("pattern", nargs="+",
help="[repository/]image[:version]")
p.add_argument("repository", help="destination repository")
p.set_defaults(func=c_move)
# new command parser
p = subparser.add_parser("new", help=c_new.__doc__.lower())
p.add_argument("-f", "--force", action="store_true",
help="overwrite existing source image")
p.add_argument("path", help="new image directory path")
p.set_defaults(func=c_new)
# payload command parser
p = subparser.add_parser("payload", help=c_payload.__doc__.lower())
p.add_argument("-j", "--json", action="store_true",
help="output is formated in json")
p.add_argument("-i", "--images", action="store_true",
help="list images using payload")
p.add_argument("payload", nargs='*', default=[u""],
help="payload md5 pattern")
p.set_defaults(func=c_payload)
# prepare_chroot command parser
p = subparser.add_parser("prepare_chroot",
help=c_prepare_chroot.__doc__.lower())
p.add_argument("-m", "--no-mount", action="store_true",
help="disable mounting of /{proc,dev,sys}")
p.add_argument("path")
p.set_defaults(func=c_prepare_chroot)
# repo command parser
p = subparser.add_parser("repo", help=c_repo.__doc__.lower())
g = p.add_mutually_exclusive_group()
p.add_argument("-j", "--json", action="store_true",
help="output is formated in json")
g.add_argument("-l", "--local", action="store_true", default=None,
help="list local repository (filter)")
g.add_argument("-r", "--remote", action="store_false", dest="local",
help="list remote repository (filter)")
g = p.add_mutually_exclusive_group()
g.add_argument("-o", "--online", action="store_true", default=None,
help="list online repository (filter)")
g.add_argument("-O", "--offline", action="store_false", dest="online",
help="list offline repository (filter)")
p.add_argument("-s", "--state", action="store_true",
help="display repository state (online/offline/local/remote)")
p.add_argument("-u", "--url", action="store_true",
help="display repository url")
p.add_argument("--purge", action="store_true",
help="remove cache databases")
p.add_argument("repository", nargs='*', default=[u"*"], help="repository pattern")
p.set_defaults(func=c_repo)
# search command parser
p = subparser.add_parser("search", help=c_search.__doc__.lower())
p.add_argument("pattern", help="pattern to search in repositories")
p.set_defaults(func=c_search)
# unprepare_chroot command parser
p = subparser.add_parser("unprepare_chroot",
help=c_unprepare_chroot.__doc__.lower())
p.add_argument("-m", "--no-umount", action="store_true",
help="disable unmounting of /{proc,dev,sys}")
p.add_argument("path")
p.set_defaults(func=c_unprepare_chroot)
# version command parser
p = subparser.add_parser("version", help=c_version.__doc__.lower())
p.set_defaults(func=c_version)
# return main parser
return parser
def main():
'''
Program main
'''
try:
# init arg parser
arg_parser = arg_parser_init()
# encode command line arguments to utf-8
try:
args = [ unicode(x, encoding=locale.getpreferredencoding()) for x in sys.argv[1:]]
except UnicodeDecodeError as e:
raise ISError("Invalid character encoding in command line")
# first partial parsing, to get early debug and config path
options = arg_parser.parse_known_args(args=args)[0]
# set early command line verbosity and color
installsystems.verbosity = options.verbosity
installsystems.printer.NOCOLOR = options.no_color
# load main config file options
config_parser = MainConfigFile(options.config, "installsystems")
options = config_parser.parse()
# second partial parsing, command line option overwrite config file
options = arg_parser.parse_known_args(args=args, namespace=options)[0]
# set verbosity and color
installsystems.verbosity = options.verbosity
installsystems.printer.NOCOLOR = options.no_color
# no warning if we are not in debug mode
if installsystems.verbosity < 2:
warnings.filterwarnings("ignore")
# nice and ionice process
if options.nice is not None or options.ionice_class is not None:
proc = psutil.Process(os.getpid())
if options.nice is not None:
try:
proc.nice = options.nice
debug("Setting nice to %d" % options.nice)
except Exception:
warn(u"Unable to nice process to %s" % options.nice)
if options.ionice_class is not None:
try:
ioclassmap = {
"none": psutil.IOPRIO_CLASS_NONE,
"rt": psutil.IOPRIO_CLASS_RT,
"be": psutil.IOPRIO_CLASS_BE,
"idle": psutil.IOPRIO_CLASS_IDLE}
proc.set_ionice(ioclassmap[options.ionice_class], options.ionice_level)
debug(u"Setting ionice to class %s, level %s" %
(options.ionice_class, options.ionice_level))
except Exception:
warn(u"Unable to ionice process to %s" % options.ionice_class)
# set timeout option
if options.timeout is not None:
socket.setdefaulttimeout(options.timeout)
debug("Global timeout setted to %ds" % options.timeout)
# except for install command we parse all args!
# install command is responsible of parsing
if options.func is not c_install:
options = arg_parser.parse_args(args=args, namespace=options)
# let's go
options.func(options)
exit(0)
except UnicodeDecodeError as e:
error("Unable to decode some characters. Check your locale settings.")
except KeyboardInterrupt:
warn("Keyboard Interrupted")
exit(1)
except ISError as e:
error(exception=e)
except Exception as e:
error(u"Unexpected error, please report it with debug enabled", exception=e)
# Entry point
if __name__ == '__main__':
main()