From cb8d7f237e0993b483ba5ec1a2f84ceb235a2c3b Mon Sep 17 00:00:00 2001 From: ZhouSiLe Date: Sun, 9 Jun 2024 16:00:50 +0800 Subject: [PATCH 01/11] feat: git hook pre-push --- pre-push.sh | 24 ++++++++++++++++++++++++ x.py | 17 ++++++++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100755 pre-push.sh diff --git a/pre-push.sh b/pre-push.sh new file mode 100755 index 00000000000..6525006a7ea --- /dev/null +++ b/pre-push.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Check 'format' and 'golangci-lint' before 'git push', +# Copy this script to .git/hooks to activate, +# and remove it from .git/hooks to deactivate. + +set -Euo pipefail + +unset GIT_DIR +ROOT_DIR="$(git rev-parse --show-toplevel)" +cd "$ROOT_DIR" + +run_check() { + local check_name=$1 + echo "Running pre-push script $ROOT_DIR/x.py $check_name" + ./x.py check "$check_name" + + if [ $? -ne 0 ]; then + echo "You may use \`git push --no-verify\` to skip this check." + exit 1 + fi +} + +run_check format +run_check golangci-lint diff --git a/x.py b/x.py index 464d3987e42..943c0755a3d 100755 --- a/x.py +++ b/x.py @@ -19,9 +19,10 @@ from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter, REMAINDER from glob import glob -from os import makedirs +import os from pathlib import Path import re +import stat from subprocess import Popen, PIPE import sys from typing import List, Any, Optional, TextIO, Tuple @@ -91,7 +92,14 @@ def check_version(current: str, required: Tuple[int, int, int], prog_name: Optio raise RuntimeError(f"{prog_name} {require_version} or higher is required, got: {current}") return semver - +def enable_git_hook(hook_path : str) -> None: + hook_name = os.path.basename(hook_path) + dst = ".git/hooks/" + hook_name.split('.')[0] + if os.path.exists(dst): + os.remove(dst) + os.link(hook_path, dst) + os.chmod(dst, os.stat(dst).st_mode | stat.S_IEXEC) + print(hook_name, "installed at", dst) def build(dir: str, jobs: Optional[int], ghproxy: bool, ninja: bool, unittest: bool, compiler: str, cmake_path: str, D: List[str], skip_build: bool) -> None: @@ -106,7 +114,7 @@ def build(dir: str, jobs: Optional[int], ghproxy: bool, ninja: bool, unittest: b cmake_version = output.read().strip() check_version(cmake_version, CMAKE_REQUIRE_VERSION, "CMake") - makedirs(dir, exist_ok=True) + os.makedirs(dir, exist_ok=True) cmake_options = ["-DCMAKE_BUILD_TYPE=RelWithDebInfo"] if ghproxy: @@ -122,6 +130,9 @@ def build(dir: str, jobs: Optional[int], ghproxy: bool, ninja: bool, unittest: b run(cmake, str(basedir), *cmake_options, verbose=True, cwd=dir) + os.makedirs("./git/hooks", exist_ok=True) + enable_git_hook("pre-push.sh") + if skip_build: return From 04cda426056ecb3a5acc705d93a0277964fb20b5 Mon Sep 17 00:00:00 2001 From: ZhouSiLe Date: Sun, 9 Jun 2024 16:20:26 +0800 Subject: [PATCH 02/11] fix: add apache license --- pre-push.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pre-push.sh b/pre-push.sh index 6525006a7ea..8ebd0637089 100755 --- a/pre-push.sh +++ b/pre-push.sh @@ -1,4 +1,21 @@ #!/usr/bin/env bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + # Check 'format' and 'golangci-lint' before 'git push', # Copy this script to .git/hooks to activate, # and remove it from .git/hooks to deactivate. From 22e6b4e5451622f2d113f5bfa36ca3134c473aaf Mon Sep 17 00:00:00 2001 From: ZhouSiLe Date: Mon, 10 Jun 2024 12:08:59 +0800 Subject: [PATCH 03/11] feat: x.py config-git-hooks --- pre-push.sh => utils/git-hooks/pre-push.sh | 0 x.py | 68 ++++++++++++++++++---- 2 files changed, 57 insertions(+), 11 deletions(-) rename pre-push.sh => utils/git-hooks/pre-push.sh (100%) diff --git a/pre-push.sh b/utils/git-hooks/pre-push.sh similarity index 100% rename from pre-push.sh rename to utils/git-hooks/pre-push.sh diff --git a/x.py b/x.py index 943c0755a3d..13105dc969c 100755 --- a/x.py +++ b/x.py @@ -92,15 +92,38 @@ def check_version(current: str, required: Tuple[int, int, int], prog_name: Optio raise RuntimeError(f"{prog_name} {require_version} or higher is required, got: {current}") return semver -def enable_git_hook(hook_path : str) -> None: - hook_name = os.path.basename(hook_path) - dst = ".git/hooks/" + hook_name.split('.')[0] - if os.path.exists(dst): - os.remove(dst) - os.link(hook_path, dst) - os.chmod(dst, os.stat(dst).st_mode | stat.S_IEXEC) - print(hook_name, "installed at", dst) +GIT_HOOKS_DIR = ".git/hooks/" +KVROCKS_GIT_HOOKS_DIR = "./utils/git-hooks/" + +def enable_git_hooks(target_hook_name: str) -> None: + os.makedirs(GIT_HOOKS_DIR, exist_ok=True) + for hook in os.listdir(KVROCKS_GIT_HOOKS_DIR): + if target_hook_name != "all" and target_hook_name != hook.split('.')[0]: + continue + hook_path = os.path.join(KVROCKS_GIT_HOOKS_DIR, hook) + dst = os.path.join(GIT_HOOKS_DIR, hook.split('.')[0]) + if os.path.exists(dst): + os.remove(dst) + os.link(hook_path, dst) + os.chmod(dst, os.stat(dst).st_mode | stat.S_IEXEC) + print(target_hook_name, "installed at", dst) + +def disable_git_hooks(target_hook_name: str) -> None: + for hook in os.listdir(KVROCKS_GIT_HOOKS_DIR): + if target_hook_name != "all" and target_hook_name != hook.split('.')[0]: + continue + dst = os.path.join(GIT_HOOKS_DIR, hook.split('.')[0]) + if os.path.exists(dst): + os.remove(dst) + print(dst, "disabled") + +def list_git_hooks() -> None: + for hook in os.listdir(KVROCKS_GIT_HOOKS_DIR): + hook_name = hook.split('.')[0] + dst = os.path.join(GIT_HOOKS_DIR, hook_name) + status = "enabled" if os.path.exists(dst) else "disabled" + print(hook_name, status) def build(dir: str, jobs: Optional[int], ghproxy: bool, ninja: bool, unittest: bool, compiler: str, cmake_path: str, D: List[str], skip_build: bool) -> None: basedir = Path(__file__).parent.absolute() @@ -130,9 +153,6 @@ def build(dir: str, jobs: Optional[int], ghproxy: bool, ninja: bool, unittest: b run(cmake, str(basedir), *cmake_options, verbose=True, cwd=dir) - os.makedirs("./git/hooks", exist_ok=True) - enable_git_hook("pre-push.sh") - if skip_build: return @@ -426,6 +446,32 @@ def test_go(dir: str, cli_path: str, rest: List[str]) -> None: parser_test_go.add_argument('rest', nargs=REMAINDER, help="the rest of arguments to forward to go test") parser_test_go.set_defaults(func=test_go) + parser_config_git_hooks = subparsers.add_parser( + 'config-git-hooks', + description="Config git hooks", + help="Config git hooks in utils/git-hooks" + ) + parser_check.set_defaults(func=parser_config_git_hooks.print_help) + parser_config_git_hooks_subparsers = parser_config_git_hooks.add_subparsers() + parser_enable_git_hooks = parser_config_git_hooks_subparsers.add_parser( + 'enable' + , description="enable git hooks", + help="enable git hooks in utils/git-hooks") + parser_enable_git_hooks.add_argument("--target_hook_name", default="all") + parser_enable_git_hooks.set_defaults(func=enable_git_hooks) + parser_disable_git_hooks = parser_config_git_hooks_subparsers.add_parser( + 'disable', + description="disable git hooks", + help="disable git hooks in .git/hooks") + parser_disable_git_hooks.add_argument("--target_hook_name", default="all") + parser_disable_git_hooks.set_defaults(func=disable_git_hooks) + parser_list_git_hooks = parser_config_git_hooks_subparsers.add_parser( + 'list', + description="list status of git hooks", + help="list status of git hooks in utils/git-hooks" + ) + parser_list_git_hooks.set_defaults(func=list_git_hooks) + args = parser.parse_args() arg_dict = dict(vars(args)) From 84781b74c6c2a8a463dc726627335e16d0e2f7f5 Mon Sep 17 00:00:00 2001 From: ZhouSiLe Date: Mon, 10 Jun 2024 17:04:11 +0800 Subject: [PATCH 04/11] feat: argument help msg --- x.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x.py b/x.py index 13105dc969c..f9ca2035946 100755 --- a/x.py +++ b/x.py @@ -124,6 +124,7 @@ def list_git_hooks() -> None: dst = os.path.join(GIT_HOOKS_DIR, hook_name) status = "enabled" if os.path.exists(dst) else "disabled" print(hook_name, status) + def build(dir: str, jobs: Optional[int], ghproxy: bool, ninja: bool, unittest: bool, compiler: str, cmake_path: str, D: List[str], skip_build: bool) -> None: basedir = Path(__file__).parent.absolute() @@ -457,13 +458,13 @@ def test_go(dir: str, cli_path: str, rest: List[str]) -> None: 'enable' , description="enable git hooks", help="enable git hooks in utils/git-hooks") - parser_enable_git_hooks.add_argument("--target_hook_name", default="all") + parser_enable_git_hooks.add_argument("--target_hook_name", default="all", help="default=all, enable all git hooks") parser_enable_git_hooks.set_defaults(func=enable_git_hooks) parser_disable_git_hooks = parser_config_git_hooks_subparsers.add_parser( 'disable', description="disable git hooks", help="disable git hooks in .git/hooks") - parser_disable_git_hooks.add_argument("--target_hook_name", default="all") + parser_disable_git_hooks.add_argument("--target_hook_name", default="all", help="default=all, disable all git hooks") parser_disable_git_hooks.set_defaults(func=disable_git_hooks) parser_list_git_hooks = parser_config_git_hooks_subparsers.add_parser( 'list', From 63953846d9c4bbc2066b6fae6247ed389c1dc764 Mon Sep 17 00:00:00 2001 From: ZhouSiLe Date: Mon, 10 Jun 2024 17:21:59 +0800 Subject: [PATCH 05/11] fix: enable "all" msg --- x.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x.py b/x.py index f9ca2035946..c73b61fe1ab 100755 --- a/x.py +++ b/x.py @@ -107,7 +107,7 @@ def enable_git_hooks(target_hook_name: str) -> None: os.remove(dst) os.link(hook_path, dst) os.chmod(dst, os.stat(dst).st_mode | stat.S_IEXEC) - print(target_hook_name, "installed at", dst) + print(hook.split('.')[0], "installed at", dst) def disable_git_hooks(target_hook_name: str) -> None: for hook in os.listdir(KVROCKS_GIT_HOOKS_DIR): From f941a820b01b6409ab64521eeb13cb2a208fc10c Mon Sep 17 00:00:00 2001 From: ZhouSiLe Date: Mon, 10 Jun 2024 21:34:30 +0800 Subject: [PATCH 06/11] refactor: prepare subcommand --- x.py | 51 +++++++-------------------------------------------- 1 file changed, 7 insertions(+), 44 deletions(-) diff --git a/x.py b/x.py index c73b61fe1ab..1e8c3e8151b 100755 --- a/x.py +++ b/x.py @@ -95,12 +95,10 @@ def check_version(current: str, required: Tuple[int, int, int], prog_name: Optio GIT_HOOKS_DIR = ".git/hooks/" KVROCKS_GIT_HOOKS_DIR = "./utils/git-hooks/" - -def enable_git_hooks(target_hook_name: str) -> None: +def prepare() -> None: + # install git hooks os.makedirs(GIT_HOOKS_DIR, exist_ok=True) for hook in os.listdir(KVROCKS_GIT_HOOKS_DIR): - if target_hook_name != "all" and target_hook_name != hook.split('.')[0]: - continue hook_path = os.path.join(KVROCKS_GIT_HOOKS_DIR, hook) dst = os.path.join(GIT_HOOKS_DIR, hook.split('.')[0]) if os.path.exists(dst): @@ -109,22 +107,6 @@ def enable_git_hooks(target_hook_name: str) -> None: os.chmod(dst, os.stat(dst).st_mode | stat.S_IEXEC) print(hook.split('.')[0], "installed at", dst) -def disable_git_hooks(target_hook_name: str) -> None: - for hook in os.listdir(KVROCKS_GIT_HOOKS_DIR): - if target_hook_name != "all" and target_hook_name != hook.split('.')[0]: - continue - dst = os.path.join(GIT_HOOKS_DIR, hook.split('.')[0]) - if os.path.exists(dst): - os.remove(dst) - print(dst, "disabled") - -def list_git_hooks() -> None: - for hook in os.listdir(KVROCKS_GIT_HOOKS_DIR): - hook_name = hook.split('.')[0] - dst = os.path.join(GIT_HOOKS_DIR, hook_name) - status = "enabled" if os.path.exists(dst) else "disabled" - print(hook_name, status) - def build(dir: str, jobs: Optional[int], ghproxy: bool, ninja: bool, unittest: bool, compiler: str, cmake_path: str, D: List[str], skip_build: bool) -> None: basedir = Path(__file__).parent.absolute() @@ -447,31 +429,12 @@ def test_go(dir: str, cli_path: str, rest: List[str]) -> None: parser_test_go.add_argument('rest', nargs=REMAINDER, help="the rest of arguments to forward to go test") parser_test_go.set_defaults(func=test_go) - parser_config_git_hooks = subparsers.add_parser( - 'config-git-hooks', - description="Config git hooks", - help="Config git hooks in utils/git-hooks" + parser_prepare = subparsers.add_parser( + 'prepare', + description="Prepare scripts such as git hooks", + help="Prepare scripts such as git hooks" ) - parser_check.set_defaults(func=parser_config_git_hooks.print_help) - parser_config_git_hooks_subparsers = parser_config_git_hooks.add_subparsers() - parser_enable_git_hooks = parser_config_git_hooks_subparsers.add_parser( - 'enable' - , description="enable git hooks", - help="enable git hooks in utils/git-hooks") - parser_enable_git_hooks.add_argument("--target_hook_name", default="all", help="default=all, enable all git hooks") - parser_enable_git_hooks.set_defaults(func=enable_git_hooks) - parser_disable_git_hooks = parser_config_git_hooks_subparsers.add_parser( - 'disable', - description="disable git hooks", - help="disable git hooks in .git/hooks") - parser_disable_git_hooks.add_argument("--target_hook_name", default="all", help="default=all, disable all git hooks") - parser_disable_git_hooks.set_defaults(func=disable_git_hooks) - parser_list_git_hooks = parser_config_git_hooks_subparsers.add_parser( - 'list', - description="list status of git hooks", - help="list status of git hooks in utils/git-hooks" - ) - parser_list_git_hooks.set_defaults(func=list_git_hooks) + parser_prepare.set_defaults(func=prepare) args = parser.parse_args() From 474d5c50e4c5b7218a13d5466659decf43a03d17 Mon Sep 17 00:00:00 2001 From: ZhouSiLe Date: Thu, 13 Jun 2024 11:05:01 +0800 Subject: [PATCH 07/11] feat: use basedir & symlink --- x.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/x.py b/x.py index 1e8c3e8151b..581382f0de1 100755 --- a/x.py +++ b/x.py @@ -93,19 +93,21 @@ def check_version(current: str, required: Tuple[int, int, int], prog_name: Optio return semver -GIT_HOOKS_DIR = ".git/hooks/" -KVROCKS_GIT_HOOKS_DIR = "./utils/git-hooks/" + def prepare() -> None: + basedir = Path(__file__).parent.absolute() # install git hooks - os.makedirs(GIT_HOOKS_DIR, exist_ok=True) - for hook in os.listdir(KVROCKS_GIT_HOOKS_DIR): - hook_path = os.path.join(KVROCKS_GIT_HOOKS_DIR, hook) - dst = os.path.join(GIT_HOOKS_DIR, hook.split('.')[0]) + git_hooks_dir = basedir / ".git/hooks/" + kvrocks_git_hooks_dir = basedir / "utils/git-hooks/" + os.makedirs(git_hooks_dir, exist_ok=True) + for hook in os.listdir(kvrocks_git_hooks_dir): + dst = os.path.join(git_hooks_dir, hook.split('.')[0]) + hook_path = os.path.join(kvrocks_git_hooks_dir, hook) if os.path.exists(dst): os.remove(dst) - os.link(hook_path, dst) + os.symlink(hook_path, dst) os.chmod(dst, os.stat(dst).st_mode | stat.S_IEXEC) - print(hook.split('.')[0], "installed at", dst) + print(hook.split('.')[0], "installed at", os.path.abspath(dst)) def build(dir: str, jobs: Optional[int], ghproxy: bool, ninja: bool, unittest: bool, compiler: str, cmake_path: str, D: List[str], skip_build: bool) -> None: From 444d59c8e4298ef3870eeaab7da3472e9d6da2eb Mon Sep 17 00:00:00 2001 From: ZhouSiLe Date: Thu, 13 Jun 2024 11:11:45 +0800 Subject: [PATCH 08/11] format: remove meaningless blank lines --- x.py | 1 - 1 file changed, 1 deletion(-) diff --git a/x.py b/x.py index 581382f0de1..7f1886f90d2 100755 --- a/x.py +++ b/x.py @@ -93,7 +93,6 @@ def check_version(current: str, required: Tuple[int, int, int], prog_name: Optio return semver - def prepare() -> None: basedir = Path(__file__).parent.absolute() # install git hooks From 945eb155205344d69753e5fda274c3437756db0c Mon Sep 17 00:00:00 2001 From: ZhouSiLe Date: Fri, 14 Jun 2024 11:11:30 +0800 Subject: [PATCH 09/11] refactor: user-friendly & pathlib --- utils/git-hooks/{pre-push.sh => pre-push} | 0 x.py | 24 ++++++++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) rename utils/git-hooks/{pre-push.sh => pre-push} (100%) diff --git a/utils/git-hooks/pre-push.sh b/utils/git-hooks/pre-push similarity index 100% rename from utils/git-hooks/pre-push.sh rename to utils/git-hooks/pre-push diff --git a/x.py b/x.py index 7f1886f90d2..b875aab3a83 100755 --- a/x.py +++ b/x.py @@ -98,15 +98,21 @@ def prepare() -> None: # install git hooks git_hooks_dir = basedir / ".git/hooks/" kvrocks_git_hooks_dir = basedir / "utils/git-hooks/" - os.makedirs(git_hooks_dir, exist_ok=True) - for hook in os.listdir(kvrocks_git_hooks_dir): - dst = os.path.join(git_hooks_dir, hook.split('.')[0]) - hook_path = os.path.join(kvrocks_git_hooks_dir, hook) - if os.path.exists(dst): - os.remove(dst) - os.symlink(hook_path, dst) - os.chmod(dst, os.stat(dst).st_mode | stat.S_IEXEC) - print(hook.split('.')[0], "installed at", os.path.abspath(dst)) + git_hooks_dir.mkdir(exist_ok=True) + for hook in kvrocks_git_hooks_dir.iterdir(): + dst = git_hooks_dir / hook.name + hook.chmod(hook.stat().st_mode | stat.S_IEXEC) + if dst.exists(): + response = input(f"{dst} already exists. Do you want to delete it and create a new symlink? (y/n): ") + if response.lower() != 'y': + print(f"Skipping installation of {hook.name} as {dst} already exists.") + continue + else: + print(f"Deleting {dst}.") + dst.unlink() + dst.symlink_to(hook) + print(hook.name, "installed at", dst) + def build(dir: str, jobs: Optional[int], ghproxy: bool, ninja: bool, unittest: bool, compiler: str, cmake_path: str, D: List[str], skip_build: bool) -> None: From 22c498a8fe747ebbe9cc1cdf3cc8f755e50ba67f Mon Sep 17 00:00:00 2001 From: ZhouSiLe Date: Fri, 14 Jun 2024 11:15:02 +0800 Subject: [PATCH 10/11] refactor: format --- x.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x.py b/x.py index b875aab3a83..9b70f46c6ed 100755 --- a/x.py +++ b/x.py @@ -111,8 +111,7 @@ def prepare() -> None: print(f"Deleting {dst}.") dst.unlink() dst.symlink_to(hook) - print(hook.name, "installed at", dst) - + print(f"{hook.name} installed at {dst}.") def build(dir: str, jobs: Optional[int], ghproxy: bool, ninja: bool, unittest: bool, compiler: str, cmake_path: str, D: List[str], skip_build: bool) -> None: From 24b21590ac9a7d354cd2aeb8f3cdca36d0c1676f Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 14 Jun 2024 12:27:58 +0800 Subject: [PATCH 11/11] improve code and UX Signed-off-by: tison --- {utils/git-hooks => dev/hooks}/pre-push | 2 +- x.py | 31 ++++++++++++------------- 2 files changed, 16 insertions(+), 17 deletions(-) rename {utils/git-hooks => dev/hooks}/pre-push (94%) diff --git a/utils/git-hooks/pre-push b/dev/hooks/pre-push similarity index 94% rename from utils/git-hooks/pre-push rename to dev/hooks/pre-push index 8ebd0637089..f7a0f5239a5 100755 --- a/utils/git-hooks/pre-push +++ b/dev/hooks/pre-push @@ -32,7 +32,7 @@ run_check() { ./x.py check "$check_name" if [ $? -ne 0 ]; then - echo "You may use \`git push --no-verify\` to skip this check." + echo 'You may use `git push --no-verify` to skip this check.' exit 1 fi } diff --git a/x.py b/x.py index 9b70f46c6ed..710534a2632 100755 --- a/x.py +++ b/x.py @@ -22,7 +22,7 @@ import os from pathlib import Path import re -import stat +import filecmp from subprocess import Popen, PIPE import sys from typing import List, Any, Optional, TextIO, Tuple @@ -95,23 +95,22 @@ def check_version(current: str, required: Tuple[int, int, int], prog_name: Optio def prepare() -> None: basedir = Path(__file__).parent.absolute() - # install git hooks - git_hooks_dir = basedir / ".git/hooks/" - kvrocks_git_hooks_dir = basedir / "utils/git-hooks/" - git_hooks_dir.mkdir(exist_ok=True) - for hook in kvrocks_git_hooks_dir.iterdir(): - dst = git_hooks_dir / hook.name - hook.chmod(hook.stat().st_mode | stat.S_IEXEC) + + # Install Git hooks + hooks = basedir / "dev" / "hooks" + git_hooks = basedir / ".git" / "hooks" + + git_hooks.mkdir(exist_ok=True) + for hook in hooks.iterdir(): + dst = git_hooks / hook.name if dst.exists(): - response = input(f"{dst} already exists. Do you want to delete it and create a new symlink? (y/n): ") - if response.lower() != 'y': - print(f"Skipping installation of {hook.name} as {dst} already exists.") + if filecmp.cmp(hook, dst, shallow=False): + print(f"{hook.name} already installed.") continue - else: - print(f"Deleting {dst}.") - dst.unlink() - dst.symlink_to(hook) - print(f"{hook.name} installed at {dst}.") + raise RuntimeError(f"{dst} already exists; please remove it first") + else: + dst.symlink_to(hook) + print(f"{hook.name} installed at {dst}.") def build(dir: str, jobs: Optional[int], ghproxy: bool, ninja: bool, unittest: bool, compiler: str, cmake_path: str, D: List[str], skip_build: bool) -> None: