diff --git a/bootstrap.py b/bootstrap.py index 00375cac33724..7ed6823571fd8 100755 --- a/bootstrap.py +++ b/bootstrap.py @@ -23,25 +23,6 @@ # created. Bootstrap.py needs to be run before an .emscripten config exists. from tools import utils -actions = [ - ('npm packages', [ - 'package.json', - 'package-lock.json', - ], ['npm', 'ci']), - ('create entry points', [ - 'tools/maint/create_entry_points.py', - 'tools/pylauncher/pylauncher.exe', - 'tools/maint/run_python.bat', - 'tools/maint/run_python.sh', - 'tools/maint/run_python.ps1', - ], [sys.executable, 'tools/maint/create_entry_points.py']), - ('git submodules', [ - 'test/third_party/posixtestsuite/', - 'test/third_party/googletest', - 'test/third_party/wasi-test-suite', - ], ['git', 'submodule', 'update', '--init']), -] - def get_stamp_file(action_name): return os.path.join(STAMP_DIR, action_name.replace(' ', '_') + '.stamp') @@ -64,44 +45,110 @@ def check(): utils.exit_with_error(f'emscripten setup is not complete ("{name}" is out-of-date). Run `bootstrap.py` to update') +def ask_yes_no(question): + while True: + try: + reply = input(f"{question} (y/n): ").strip().lower() + except EOFError: + return False + + if reply in {'y', 'yes'}: + return True + if reply in {'n', 'no'}: + return False + + print("Invalid input. Please enter 'y' or 'n'.") + + +def maybe_install_hooks(): + if os.path.exists('.git/hooks/pre-push'): + print('git hooks already installed; skipping') + return + if os.environ.get('CI') or not sys.stdin.isatty(): + # Do nothing when running in CI or non-interactive shell + return + if ask_yes_no('Install emscripten git hooks (see tools/maint/git-hooks)?'): + install_hooks() + + +def install_hooks(): + if not os.path.exists(utils.path_from_root('.git')): + print('--install-git-hooks requires a git checkout') + return 1 + + dst = utils.path_from_root('.git/hooks') + if not os.path.exists(dst): + os.mkdir(dst) + + for src in ('tools/maint/git-hooks/post-checkout', 'tools/maint/git-hooks/pre-push'): + shutil.copy(utils.path_from_root(src), dst) + return 0 + + +def run_cmd(cmd): + orig_exe = cmd[0] + if not os.path.isabs(orig_exe): + cmd[0] = shutil.which(orig_exe) + if not cmd[0]: + utils.exit_with_error(f'command not found: {orig_exe}') + print(' -> %s' % ' '.join(cmd)) + subprocess.run(cmd, check=True, text=True, encoding='utf-8', cwd=utils.path_from_root()) + + +actions = [ + ('npm packages', [ + 'package.json', + 'package-lock.json', + ], ['npm', 'ci']), + ('create entry points', [ + 'tools/maint/create_entry_points.py', + 'tools/pylauncher/pylauncher.exe', + 'tools/maint/run_python.bat', + 'tools/maint/run_python.sh', + 'tools/maint/run_python.ps1', + ], [sys.executable, 'tools/maint/create_entry_points.py']), + ('git submodules', [ + 'test/third_party/posixtestsuite/', + 'test/third_party/googletest', + 'test/third_party/wasi-test-suite', + ], ['git', 'submodule', 'update', '--init']), + ('install hooks', [ + 'tools/maint/git-hooks/post-checkout', + 'tools/maint/git-hooks/pre-push', + ], maybe_install_hooks), +] + + def main(args): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('-v', '--verbose', action='store_true', help='verbose', default=False) + parser.add_argument('-f', '--force', action='store_true', help='force all actions to run', default=False) parser.add_argument('-n', '--dry-run', action='store_true', help='dry run', default=False) parser.add_argument('-i', '--install-git-hooks', action='store_true', help='install emscripten git hooks', default=False) args = parser.parse_args() if args.install_git_hooks: - if not os.path.exists(utils.path_from_root('.git')): - print('--install-git-hooks requires git checkout') - return 1 - - dst = utils.path_from_root('.git/hooks') - if not os.path.exists(dst): - os.mkdir(dst) - - src = utils.path_from_root('tools/maint/post-checkout') - for src in ('tools/maint/post-checkout', 'tools/maint/pre-push'): - shutil.copy(utils.path_from_root(src), dst) - return 0 - - for name, deps, cmd in actions: - if check_deps(name, deps): - print('Up-to-date: %s' % name) - continue - print('Out-of-date: %s' % name) - stamp_file = get_stamp_file(name) + return install_hooks() + + for name, deps, action in actions: + if not args.force: + if check_deps(name, deps): + print('Up-to-date: %s' % name) + continue + print('Out-of-date: %s' % name) if args.dry_run: - print(' (skipping: dry run) -> %s' % ' '.join(cmd)) + if type(action) == list: + action_str = ' '.join(action) + else: + action_str = action.__name__ + print(f' (skipping: dry run) -> {action_str}') continue - orig_exe = cmd[0] - if not os.path.isabs(orig_exe): - cmd[0] = shutil.which(orig_exe) - if not cmd[0]: - utils.exit_with_error(f'command not found: {orig_exe}') - print(' -> %s' % ' '.join(cmd)) - subprocess.run(cmd, check=True, text=True, encoding='utf-8', cwd=utils.path_from_root()) + if type(action) == list: + run_cmd(action) + else: + action() utils.safe_ensure_dirs(STAMP_DIR) + stamp_file = get_stamp_file(name) utils.write_file(stamp_file, 'Timestamp file created by bootstrap.py') return 0 diff --git a/tools/maint/post-checkout b/tools/maint/git-hooks/post-checkout similarity index 88% rename from tools/maint/post-checkout rename to tools/maint/git-hooks/post-checkout index af7e11f5b2ea6..d64ca612a7ef6 100755 --- a/tools/maint/post-checkout +++ b/tools/maint/git-hooks/post-checkout @@ -6,7 +6,7 @@ # The bootstrap script itself is smart enough to basically do nothing unless # one of the relevant files was updated (e.g. package.json). # -# This script can be installed using `bootstrap.py -i`. +# This script is normally installed by `bootstrap.py`. # Test for the existence of the bootstrap script itself to handle branches # that predate its existence. diff --git a/tools/maint/pre-push b/tools/maint/git-hooks/pre-push similarity index 64% rename from tools/maint/pre-push rename to tools/maint/git-hooks/pre-push index 37f28bea7e7af..0408735329590 100755 --- a/tools/maint/pre-push +++ b/tools/maint/git-hooks/pre-push @@ -2,7 +2,7 @@ # # Git pre-push script that runs some quick/simple tests. # -# This script can be installed using `bootstrap.py -i`. +# This script is normally installed by `bootstrap.py`. set -o errexit