mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-11-22 15:42:02 +00:00
555 lines
18 KiB
Python
Executable File
555 lines
18 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
import subprocess
|
|
import os
|
|
import argparse
|
|
import logging
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import List, Optional
|
|
|
|
SCRIPT_PATH = Path(__file__).absolute()
|
|
IMAGE_TYPE = "binary"
|
|
IMAGE_NAME = f"clickhouse/{IMAGE_TYPE}-builder"
|
|
|
|
|
|
class BuildException(Exception):
|
|
pass
|
|
|
|
|
|
def check_image_exists_locally(image_name: str) -> bool:
|
|
try:
|
|
output = subprocess.check_output(
|
|
f"docker images -q {image_name} 2> /dev/null", shell=True
|
|
)
|
|
return output != b""
|
|
except subprocess.CalledProcessError:
|
|
return False
|
|
|
|
|
|
def pull_image(image_name: str) -> bool:
|
|
try:
|
|
subprocess.check_call(f"docker pull {image_name}", shell=True)
|
|
return True
|
|
except subprocess.CalledProcessError:
|
|
logging.info("Cannot pull image %s", image_name)
|
|
return False
|
|
|
|
|
|
def build_image(image_name: str, filepath: Path) -> None:
|
|
context = filepath.parent
|
|
build_cmd = f"docker build --network=host -t {image_name} -f {filepath} {context}"
|
|
logging.info("Will build image with cmd: '%s'", build_cmd)
|
|
subprocess.check_call(
|
|
build_cmd,
|
|
shell=True,
|
|
)
|
|
|
|
|
|
def pre_build(repo_path: Path, env_variables: List[str]) -> None:
|
|
if "WITH_PERFORMANCE=1" in env_variables:
|
|
current_branch = subprocess.check_output(
|
|
"git branch --show-current", shell=True, encoding="utf-8"
|
|
).strip()
|
|
is_shallow = (
|
|
subprocess.check_output(
|
|
"git rev-parse --is-shallow-repository", shell=True, encoding="utf-8"
|
|
)
|
|
== "true\n"
|
|
)
|
|
if is_shallow:
|
|
# I've spent quite some time on looking around the problem, and my
|
|
# conclusion is: in the current state the easiest way to go is to force
|
|
# unshallow repository for performance artifacts.
|
|
# To change it we need to rework our performance tests docker image
|
|
raise BuildException(
|
|
"shallow repository is not suitable for performance builds"
|
|
)
|
|
if current_branch != "master":
|
|
cmd = (
|
|
f"git -C {repo_path} fetch --no-recurse-submodules "
|
|
"--no-tags origin master:master"
|
|
)
|
|
logging.info("Getting master branch for performance artifact: '%s'", cmd)
|
|
subprocess.check_call(cmd, shell=True)
|
|
|
|
|
|
def run_docker_image_with_env(
|
|
image_name: str,
|
|
as_root: bool,
|
|
output_dir: Path,
|
|
env_variables: List[str],
|
|
ch_root: Path,
|
|
cargo_cache_dir: Path,
|
|
ccache_dir: Optional[Path],
|
|
) -> None:
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
cargo_cache_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
env_part = " -e ".join(env_variables)
|
|
if env_part:
|
|
env_part = " -e " + env_part
|
|
|
|
if sys.stdout.isatty():
|
|
interactive = "-it"
|
|
else:
|
|
interactive = ""
|
|
|
|
if as_root:
|
|
user = "0:0"
|
|
else:
|
|
user = f"{os.geteuid()}:{os.getegid()}"
|
|
|
|
ccache_mount = f"--volume={ccache_dir}:/ccache"
|
|
if ccache_dir is None:
|
|
ccache_mount = ""
|
|
|
|
cmd = (
|
|
f"docker run --network=host --user={user} --rm {ccache_mount} "
|
|
f"--volume={output_dir}:/output --volume={ch_root}:/build {env_part} "
|
|
f"--volume={cargo_cache_dir}:/rust/cargo/registry {interactive} {image_name}"
|
|
)
|
|
|
|
logging.info("Will build ClickHouse pkg with cmd: '%s'", cmd)
|
|
|
|
subprocess.check_call(cmd, shell=True)
|
|
|
|
|
|
def is_release_build(
|
|
debug_build: bool, package_type: str, sanitizer: str, coverage: bool
|
|
) -> bool:
|
|
return (
|
|
not debug_build and package_type == "deb" and sanitizer == "" and not coverage
|
|
)
|
|
|
|
|
|
def parse_env_variables(
|
|
debug_build: bool,
|
|
coverage: bool,
|
|
compiler: str,
|
|
sanitizer: str,
|
|
package_type: str,
|
|
cache: str,
|
|
s3_bucket: str,
|
|
s3_directory: str,
|
|
s3_rw_access: bool,
|
|
clang_tidy: bool,
|
|
version: str,
|
|
official: bool,
|
|
additional_pkgs: bool,
|
|
with_profiler: bool,
|
|
with_coverage: bool,
|
|
with_binaries: str,
|
|
) -> List[str]:
|
|
DARWIN_SUFFIX = "-darwin"
|
|
DARWIN_ARM_SUFFIX = "-darwin-aarch64"
|
|
ARM_SUFFIX = "-aarch64"
|
|
ARM_V80COMPAT_SUFFIX = "-aarch64-v80compat"
|
|
FREEBSD_SUFFIX = "-freebsd"
|
|
PPC_SUFFIX = "-ppc64le"
|
|
RISCV_SUFFIX = "-riscv64"
|
|
S390X_SUFFIX = "-s390x"
|
|
AMD64_COMPAT_SUFFIX = "-amd64-compat"
|
|
AMD64_MUSL_SUFFIX = "-amd64-musl"
|
|
|
|
result = []
|
|
result.append("OUTPUT_DIR=/output")
|
|
cmake_flags = ["$CMAKE_FLAGS"]
|
|
if package_type == "fuzzers":
|
|
build_target = "fuzzers"
|
|
else:
|
|
build_target = "clickhouse-bundle"
|
|
|
|
is_cross_darwin = compiler.endswith(DARWIN_SUFFIX)
|
|
is_cross_darwin_arm = compiler.endswith(DARWIN_ARM_SUFFIX)
|
|
is_cross_arm = compiler.endswith(ARM_SUFFIX)
|
|
is_cross_arm_v80compat = compiler.endswith(ARM_V80COMPAT_SUFFIX)
|
|
is_cross_ppc = compiler.endswith(PPC_SUFFIX)
|
|
is_cross_riscv = compiler.endswith(RISCV_SUFFIX)
|
|
is_cross_s390x = compiler.endswith(S390X_SUFFIX)
|
|
is_cross_freebsd = compiler.endswith(FREEBSD_SUFFIX)
|
|
is_amd64_compat = compiler.endswith(AMD64_COMPAT_SUFFIX)
|
|
is_amd64_musl = compiler.endswith(AMD64_MUSL_SUFFIX)
|
|
|
|
if is_cross_darwin:
|
|
cc = compiler[: -len(DARWIN_SUFFIX)]
|
|
cmake_flags.append("-DCMAKE_AR:FILEPATH=/cctools/bin/x86_64-apple-darwin-ar")
|
|
cmake_flags.append(
|
|
"-DCMAKE_INSTALL_NAME_TOOL=/cctools/bin/"
|
|
"x86_64-apple-darwin-install_name_tool"
|
|
)
|
|
cmake_flags.append(
|
|
"-DCMAKE_RANLIB:FILEPATH=/cctools/bin/x86_64-apple-darwin-ranlib"
|
|
)
|
|
cmake_flags.append("-DLINKER_NAME=/cctools/bin/x86_64-apple-darwin-ld")
|
|
cmake_flags.append(
|
|
"-DCMAKE_TOOLCHAIN_FILE=/build/cmake/darwin/toolchain-x86_64.cmake"
|
|
)
|
|
result.append("EXTRACT_TOOLCHAIN_DARWIN=1")
|
|
result.append("EXPORT_SOURCES_WITH_SUBMODULES=1")
|
|
elif is_cross_darwin_arm:
|
|
cc = compiler[: -len(DARWIN_ARM_SUFFIX)]
|
|
cmake_flags.append("-DCMAKE_AR:FILEPATH=/cctools/bin/aarch64-apple-darwin-ar")
|
|
cmake_flags.append(
|
|
"-DCMAKE_INSTALL_NAME_TOOL=/cctools/bin/"
|
|
"aarch64-apple-darwin-install_name_tool"
|
|
)
|
|
cmake_flags.append(
|
|
"-DCMAKE_RANLIB:FILEPATH=/cctools/bin/aarch64-apple-darwin-ranlib"
|
|
)
|
|
cmake_flags.append("-DLINKER_NAME=/cctools/bin/aarch64-apple-darwin-ld")
|
|
cmake_flags.append(
|
|
"-DCMAKE_TOOLCHAIN_FILE=/build/cmake/darwin/toolchain-aarch64.cmake"
|
|
)
|
|
result.append("EXTRACT_TOOLCHAIN_DARWIN=1")
|
|
elif is_cross_arm:
|
|
cc = compiler[: -len(ARM_SUFFIX)]
|
|
cmake_flags.append(
|
|
"-DCMAKE_TOOLCHAIN_FILE=/build/cmake/linux/toolchain-aarch64.cmake"
|
|
)
|
|
result.append("DEB_ARCH=arm64")
|
|
elif is_cross_arm_v80compat:
|
|
cc = compiler[: -len(ARM_V80COMPAT_SUFFIX)]
|
|
cmake_flags.append(
|
|
"-DCMAKE_TOOLCHAIN_FILE=/build/cmake/linux/toolchain-aarch64.cmake"
|
|
)
|
|
cmake_flags.append("-DNO_ARMV81_OR_HIGHER=1")
|
|
result.append("DEB_ARCH=arm64")
|
|
elif is_cross_freebsd:
|
|
cc = compiler[: -len(FREEBSD_SUFFIX)]
|
|
cmake_flags.append(
|
|
"-DCMAKE_TOOLCHAIN_FILE=/build/cmake/freebsd/toolchain-x86_64.cmake"
|
|
)
|
|
elif is_cross_ppc:
|
|
cc = compiler[: -len(PPC_SUFFIX)]
|
|
cmake_flags.append(
|
|
"-DCMAKE_TOOLCHAIN_FILE=/build/cmake/linux/toolchain-ppc64le.cmake"
|
|
)
|
|
elif is_cross_riscv:
|
|
cc = compiler[: -len(RISCV_SUFFIX)]
|
|
cmake_flags.append(
|
|
"-DCMAKE_TOOLCHAIN_FILE=/build/cmake/linux/toolchain-riscv64.cmake"
|
|
)
|
|
elif is_cross_s390x:
|
|
cc = compiler[: -len(S390X_SUFFIX)]
|
|
cmake_flags.append(
|
|
"-DCMAKE_TOOLCHAIN_FILE=/build/cmake/linux/toolchain-s390x.cmake"
|
|
)
|
|
elif is_amd64_compat:
|
|
cc = compiler[: -len(AMD64_COMPAT_SUFFIX)]
|
|
result.append("DEB_ARCH=amd64")
|
|
cmake_flags.append("-DNO_SSE3_OR_HIGHER=1")
|
|
elif is_amd64_musl:
|
|
cc = compiler[: -len(AMD64_MUSL_SUFFIX)]
|
|
result.append("DEB_ARCH=amd64")
|
|
cmake_flags.append(
|
|
"-DCMAKE_TOOLCHAIN_FILE=/build/cmake/linux/toolchain-x86_64-musl.cmake"
|
|
)
|
|
else:
|
|
cc = compiler
|
|
result.append("DEB_ARCH=amd64")
|
|
|
|
cxx = cc.replace("clang", "clang++")
|
|
|
|
if package_type == "deb":
|
|
# NOTE: This is the env for packages/build script
|
|
result.append("MAKE_DEB=true")
|
|
cmake_flags.append("-DENABLE_TESTS=0")
|
|
cmake_flags.append("-DENABLE_UTILS=0")
|
|
cmake_flags.append("-DCMAKE_FIND_PACKAGE_NO_PACKAGE_REGISTRY=ON")
|
|
cmake_flags.append("-DCMAKE_INSTALL_PREFIX=/usr")
|
|
cmake_flags.append("-DCMAKE_INSTALL_SYSCONFDIR=/etc")
|
|
cmake_flags.append("-DCMAKE_INSTALL_LOCALSTATEDIR=/var")
|
|
# Reduce linking and building time by avoid *install/all dependencies
|
|
cmake_flags.append("-DCMAKE_SKIP_INSTALL_ALL_DEPENDENCY=ON")
|
|
# Add bridges to the build target
|
|
build_target = (
|
|
f"{build_target} clickhouse-odbc-bridge clickhouse-library-bridge"
|
|
)
|
|
if is_release_build(debug_build, package_type, sanitizer, coverage):
|
|
cmake_flags.append("-DSPLIT_DEBUG_SYMBOLS=ON")
|
|
result.append("WITH_PERFORMANCE=1")
|
|
if is_cross_arm:
|
|
cmake_flags.append("-DBUILD_STANDALONE_KEEPER=1")
|
|
else:
|
|
result.append("BUILD_MUSL_KEEPER=1")
|
|
elif package_type == "fuzzers":
|
|
cmake_flags.append("-DENABLE_FUZZING=1")
|
|
cmake_flags.append("-DENABLE_PROTOBUF=1")
|
|
cmake_flags.append("-DWITH_COVERAGE=1")
|
|
# Reduce linking and building time by avoid *install/all dependencies
|
|
cmake_flags.append("-DCMAKE_SKIP_INSTALL_ALL_DEPENDENCY=ON")
|
|
|
|
result.append(f"CC={cc}")
|
|
result.append(f"CXX={cxx}")
|
|
cmake_flags.append(f"-DCMAKE_C_COMPILER={cc}")
|
|
cmake_flags.append(f"-DCMAKE_CXX_COMPILER={cxx}")
|
|
|
|
if sanitizer:
|
|
result.append(f"SANITIZER={sanitizer}")
|
|
if debug_build:
|
|
result.append("BUILD_TYPE=Debug")
|
|
else:
|
|
result.append("BUILD_TYPE=None")
|
|
|
|
if coverage:
|
|
cmake_flags.append("-DSANITIZE_COVERAGE=1 -DBUILD_STANDALONE_KEEPER=0")
|
|
|
|
if not cache:
|
|
cmake_flags.append("-DCOMPILER_CACHE=disabled")
|
|
|
|
if cache == "ccache":
|
|
cmake_flags.append("-DCOMPILER_CACHE=ccache")
|
|
result.append("CCACHE_DIR=/ccache")
|
|
result.append("CCACHE_COMPRESSLEVEL=5")
|
|
result.append("CCACHE_BASEDIR=/build")
|
|
result.append("CCACHE_NOHASHDIR=true")
|
|
result.append("CCACHE_COMPILERCHECK=content")
|
|
result.append("CCACHE_MAXSIZE=15G")
|
|
|
|
if cache == "sccache":
|
|
cmake_flags.append("-DCOMPILER_CACHE=sccache")
|
|
# see https://github.com/mozilla/sccache/blob/main/docs/S3.md
|
|
result.append(f"SCCACHE_BUCKET={s3_bucket}")
|
|
sccache_dir = "sccache"
|
|
if s3_directory:
|
|
sccache_dir = f"{s3_directory}/{sccache_dir}"
|
|
result.append(f"SCCACHE_S3_KEY_PREFIX={sccache_dir}")
|
|
if not s3_rw_access:
|
|
result.append("SCCACHE_S3_NO_CREDENTIALS=true")
|
|
|
|
if clang_tidy:
|
|
# `CTCACHE_DIR` has the same purpose as the `CCACHE_DIR` above.
|
|
# It's there to have the clang-tidy cache embedded into our standard `CCACHE_DIR`
|
|
if cache == "ccache":
|
|
result.append("CTCACHE_DIR=/ccache/clang-tidy-cache")
|
|
if s3_bucket:
|
|
# see https://github.com/matus-chochlik/ctcache#environment-variables
|
|
ctcache_dir = "clang-tidy-cache"
|
|
if s3_directory:
|
|
ctcache_dir = f"{s3_directory}/{ctcache_dir}"
|
|
result.append(f"CTCACHE_S3_BUCKET={s3_bucket}")
|
|
result.append(f"CTCACHE_S3_FOLDER={ctcache_dir}")
|
|
if not s3_rw_access:
|
|
result.append("CTCACHE_S3_NO_CREDENTIALS=true")
|
|
|
|
if additional_pkgs:
|
|
# NOTE: This are the env for packages/build script
|
|
result.append("MAKE_RPM=true")
|
|
result.append("MAKE_TGZ=true")
|
|
|
|
if with_binaries == "programs":
|
|
result.append("BINARY_OUTPUT=programs")
|
|
elif with_binaries == "tests":
|
|
result.append("ENABLE_TESTS=1")
|
|
result.append("BINARY_OUTPUT=tests")
|
|
cmake_flags.append("-DENABLE_TESTS=1")
|
|
|
|
if clang_tidy:
|
|
cmake_flags.append("-DENABLE_CLANG_TIDY=1")
|
|
cmake_flags.append("-DENABLE_TESTS=1")
|
|
cmake_flags.append("-DENABLE_EXAMPLES=1")
|
|
# Don't stop on first error to find more clang-tidy errors in one run.
|
|
result.append("NINJA_FLAGS=-k0")
|
|
|
|
cmake_flags.append("-DENABLE_UTILS=1")
|
|
# utils are not included into clickhouse-bundle, so build everything
|
|
build_target = "all"
|
|
|
|
if with_profiler:
|
|
cmake_flags.append("-DENABLE_BUILD_PROFILING=1")
|
|
|
|
if with_coverage:
|
|
cmake_flags.append("-DWITH_COVERAGE=1")
|
|
|
|
if version:
|
|
result.append(f"VERSION_STRING='{version}'")
|
|
|
|
if official:
|
|
cmake_flags.append("-DCLICKHOUSE_OFFICIAL_BUILD=1")
|
|
|
|
result.append('CMAKE_FLAGS="' + " ".join(cmake_flags) + '"')
|
|
result.append(f"BUILD_TARGET='{build_target}'")
|
|
|
|
return result
|
|
|
|
|
|
def dir_name(name: str) -> Path:
|
|
path = Path(name)
|
|
if not path.is_absolute():
|
|
path = Path.cwd() / name
|
|
return path
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
description="ClickHouse building script using prebuilt Docker image",
|
|
)
|
|
parser.add_argument(
|
|
"--package-type",
|
|
choices=["deb", "binary", "fuzzers"],
|
|
required=True,
|
|
)
|
|
parser.add_argument(
|
|
"--clickhouse-repo-path",
|
|
default=SCRIPT_PATH.parents[2],
|
|
type=dir_name,
|
|
help="ClickHouse git repository",
|
|
)
|
|
parser.add_argument("--output-dir", type=dir_name, required=True)
|
|
parser.add_argument("--debug-build", action="store_true")
|
|
|
|
parser.add_argument(
|
|
"--compiler",
|
|
choices=(
|
|
"clang-17",
|
|
"clang-17-darwin",
|
|
"clang-17-darwin-aarch64",
|
|
"clang-17-aarch64",
|
|
"clang-17-aarch64-v80compat",
|
|
"clang-17-ppc64le",
|
|
"clang-17-riscv64",
|
|
"clang-17-s390x",
|
|
"clang-17-amd64-compat",
|
|
"clang-17-amd64-musl",
|
|
"clang-17-freebsd",
|
|
),
|
|
default="clang-17",
|
|
help="a compiler to use",
|
|
)
|
|
parser.add_argument(
|
|
"--sanitizer",
|
|
choices=("address", "thread", "memory", "undefined", ""),
|
|
default="",
|
|
)
|
|
parser.add_argument(
|
|
"--coverage",
|
|
action="store_true",
|
|
help="enable granular coverage with introspection",
|
|
)
|
|
|
|
parser.add_argument("--clang-tidy", action="store_true")
|
|
parser.add_argument(
|
|
"--cache",
|
|
choices=("ccache", "sccache", ""),
|
|
default="",
|
|
help="ccache or sccache for objects caching; sccache uses only S3 buckets",
|
|
)
|
|
parser.add_argument(
|
|
"--ccache-dir",
|
|
default=Path.home() / ".ccache",
|
|
type=dir_name,
|
|
help="a directory with ccache",
|
|
)
|
|
parser.add_argument(
|
|
"--s3-bucket",
|
|
help="an S3 bucket used for sscache and clang-tidy-cache",
|
|
)
|
|
parser.add_argument(
|
|
"--s3-directory",
|
|
default="ccache",
|
|
help="an S3 directory prefix used for sscache and clang-tidy-cache",
|
|
)
|
|
parser.add_argument(
|
|
"--s3-rw-access",
|
|
action="store_true",
|
|
help="if set, the build fails on errors writing cache to S3",
|
|
)
|
|
parser.add_argument(
|
|
"--cargo-cache-dir",
|
|
default=Path(os.getenv("CARGO_HOME", "") or Path.home() / ".cargo")
|
|
/ "registry",
|
|
type=dir_name,
|
|
help="a directory to preserve the rust cargo crates",
|
|
)
|
|
parser.add_argument("--force-build-image", action="store_true")
|
|
parser.add_argument("--version")
|
|
parser.add_argument("--official", action="store_true")
|
|
parser.add_argument("--additional-pkgs", action="store_true")
|
|
parser.add_argument("--with-profiler", action="store_true")
|
|
parser.add_argument("--with-coverage", action="store_true")
|
|
parser.add_argument(
|
|
"--with-binaries", choices=("programs", "tests", ""), default=""
|
|
)
|
|
parser.add_argument(
|
|
"--docker-image-version", default="latest", help="docker image tag to use"
|
|
)
|
|
parser.add_argument(
|
|
"--as-root", action="store_true", help="if the container should run as root"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.additional_pkgs and args.package_type != "deb":
|
|
raise argparse.ArgumentTypeError(
|
|
"Can build additional packages only in deb build"
|
|
)
|
|
|
|
if args.cache != "ccache":
|
|
args.ccache_dir = None
|
|
|
|
if args.with_binaries != "":
|
|
if args.package_type != "deb":
|
|
raise argparse.ArgumentTypeError(
|
|
"Can add additional binaries only in deb build"
|
|
)
|
|
logging.info("Should place %s to output", args.with_binaries)
|
|
|
|
if args.cache == "sccache":
|
|
if not args.s3_bucket:
|
|
raise argparse.ArgumentTypeError("sccache must have --s3-bucket set")
|
|
|
|
return args
|
|
|
|
|
|
def main() -> None:
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
|
|
args = parse_args()
|
|
|
|
ch_root = args.clickhouse_repo_path
|
|
|
|
dockerfile = ch_root / "docker/packager" / IMAGE_TYPE / "Dockerfile"
|
|
image_with_version = IMAGE_NAME + ":" + args.docker_image_version
|
|
if args.force_build_image:
|
|
build_image(image_with_version, dockerfile)
|
|
elif not (
|
|
check_image_exists_locally(image_with_version) or pull_image(image_with_version)
|
|
):
|
|
build_image(image_with_version, dockerfile)
|
|
|
|
env_prepared = parse_env_variables(
|
|
args.debug_build,
|
|
args.coverage,
|
|
args.compiler,
|
|
args.sanitizer,
|
|
args.package_type,
|
|
args.cache,
|
|
args.s3_bucket,
|
|
args.s3_directory,
|
|
args.s3_rw_access,
|
|
args.clang_tidy,
|
|
args.version,
|
|
args.official,
|
|
args.additional_pkgs,
|
|
args.with_profiler,
|
|
args.with_coverage,
|
|
args.with_binaries,
|
|
)
|
|
|
|
pre_build(args.clickhouse_repo_path, env_prepared)
|
|
run_docker_image_with_env(
|
|
image_with_version,
|
|
args.as_root,
|
|
args.output_dir,
|
|
env_prepared,
|
|
ch_root,
|
|
args.cargo_cache_dir,
|
|
args.ccache_dir,
|
|
)
|
|
logging.info("Output placed into %s", args.output_dir)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|