Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 93 additions & 46 deletions bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We create a stamp file for 'install hooks' action even if the user entered n, and because of the stamp file, next time bootstrap.py thinks the hooks were already installed, when they weren't:

aheejin@aheejin:~/emscripten$ ./bootstrap.py 
Up-to-date: npm packages
Up-to-date: create entry points
Up-to-date: git submodules
Out-of-date: install hooks
Install emscripten git hooks (see tools/maint/git-hooks)? (y/n): n
aheejin@aheejin:~/emscripten$ ./bootstrap.py 
Up-to-date: npm packages
Up-to-date: create entry points
Up-to-date: git submodules
Up-to-date: install hooks

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is the idea. I only want to ask the user once. So guess we should call it "maybe install hooks" to "ask install hooks" ? But the idea is to avoid asking again.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the out/ directory (or just the stamp) or running with --force will cause it ask again.

@aheejin aheejin Jul 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about printing that you can install the hooks later with -i option when the user enters n and don't ask again? And printing Up-to-date: install hooks when they weren't installed can be somewhat confusing.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, I'll see if I can come up with a better phrasing.

utils.write_file(stamp_file, 'Timestamp file created by bootstrap.py')
return 0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion tools/maint/pre-push → tools/maint/git-hooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down