diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..aaf198b --- /dev/null +++ b/.clang-format @@ -0,0 +1,4 @@ +--- +BasedOnStyle: Mozilla + +... diff --git a/LICENSE b/LICENSE index 804457b..bf12b6e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 P. F. Chimento +Copyright (c) 2018 Philip Chimento Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 6b936d3..ff711af 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ -# spidermonkey-embedding-examples -Documentation and examples for people who want to embed the SpiderMonkey JavaScript engine. +# SpiderMonkey Embedding Resources # + +This repository contains documentation and examples for people who want +to embed the SpiderMonkey JavaScript engine. + +The information on this `esr60` branch applies to SpiderMonkey 60.x, an +Extended Support Release (ESR). +For other versions of SpiderMonkey, check the other branches: `next` for +information that will apply in the next as-yet-unreleased ESR, or +earlier `esr` branches for previous versions. + diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..c9a7e21 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,4 @@ +# SpiderMonkey Library Documentation # + +In this directory you will find documentation pages with explanations of +core SpiderMonkey concepts, and how-to pages. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..9559e49 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,21 @@ +# SpiderMonkey Embedding Examples # + +## To build ## + +To compile these examples, build in the toplevel directory: +```sh +meson _build +ninja -C _build +``` + +## To contribute ## + +Install the clang-format commit hook: + +```sh +tools/git-pre-commit-format install +``` + +If adding a new example to the examples directory, make sure to build it +in the `meson.build` file, and add a description of it to this +`README.md` file. diff --git a/tools/apply-format b/tools/apply-format new file mode 100755 index 0000000..a601fe5 --- /dev/null +++ b/tools/apply-format @@ -0,0 +1,321 @@ +#! /bin/bash +# +# Copyright 2018 Undo Ltd. +# +# https://github.com/barisione/clang-format-hooks + +# Force variable declaration before access. +set -u +# Make any failure in piped commands be reflected in the exit code. +set -o pipefail + +readonly bash_source="${BASH_SOURCE[0]:-$0}" + +################## +# Misc functions # +################## + +function error_exit() { + for str in "$@"; do + echo -n "$str" >&2 + done + echo >&2 + + exit 1 +} + + +######################## +# Command line parsing # +######################## + +function show_help() { + if [ -t 1 ] && hash tput 2> /dev/null; then + local -r b=$(tput bold) + local -r i=$(tput sitm) + local -r n=$(tput sgr0) + else + local -r b= + local -r i= + local -r n= + fi + + cat << EOF +${b}SYNOPSIS${n} + + To reformat git diffs: + + ${i}$bash_source [OPTIONS] [FILES-OR-GIT-DIFF-OPTIONS]${n} + + To reformat whole files, including unchanged parts: + + ${i}$bash_source [-f | --whole-file] FILES${n} + +${b}DESCRIPTION${n} + + Reformat C or C++ code to match a specified formatting style. + + This command can either work on diffs, to reformat only changed parts of + the code, or on whole files (if -f or --whole-file is used). + + ${b}FILES-OR-GIT-DIFF-OPTIONS${n} + List of files to consider when applying clang-format to a diff. This is + passed to "git diff" as is, so it can also include extra git options or + revisions. + For example, to apply clang-format on the changes made in the last few + revisions you could use: + ${i}\$ $bash_source HEAD~3${n} + + ${b}FILES${n} + List of files to completely reformat. + + ${b}-f, --whole-file${n} + Reformat the specified files completely (including parts you didn't + change). + The patch is printed on stdout by default. Use -i if you want to modify + the files on disk. + + ${b}--staged, --cached${n} + Reformat only code which is staged for commit. + The patch is printed on stdout by default. Use -i if you want to modify + the files on disk. + + ${b}-i${n} + Reformat the code and apply the changes to the files on disk (instead + of just printing the patch on stdout). + + ${b}--apply-to-staged${n} + This is like specifying both --staged and -i, but the formatting + changes are also staged for commit (so you can just use "git commit" + to commit what you planned to, but formatted correctly). + + ${b}--style STYLE${n} + The style to use for reformatting code. + If no style is specified, then it's assumed there's a .clang-format + file in the current directory or one of its parents. + + ${b}--help, -h, -?${n} + Show this help. +EOF +} + +# getopts doesn't support long options. +# getopt mangles stuff. +# So we parse manually... +declare positionals=() +declare has_positionals=false +declare whole_file=false +declare apply_to_staged=false +declare staged=false +declare in_place=false +declare style=file +while [ $# -gt 0 ]; do + declare arg="$1" + shift # Past option. + case "$arg" in + -h | -\? | --help ) + show_help + exit 0 + ;; + -f | --whole-file ) + whole_file=true + ;; + --apply-to-staged ) + apply_to_staged=true + ;; + --cached | --staged ) + staged=true + ;; + -i ) + in_place=true + ;; + --style=* ) + style="${arg//--style=/}" + ;; + --style ) + [ $# -gt 0 ] || \ + error_exit "No argument for --style option." + style="$1" + shift + ;; + -- ) + # Stop processing further arguments. + if [ $# -gt 0 ]; then + positionals+=("$@") + has_positionals=true + fi + break + ;; + -* ) + error_exit "Unknown argument: $arg" + ;; + *) + positionals+=("$arg") + ;; + esac +done + +# Restore positional arguments, access them from "$@". +if [ ${#positionals[@]} -gt 0 ]; then + set -- "${positionals[@]}" + has_positionals=true +fi + +[ -n "$style" ] || \ + error_exit "If you use --style you need to specify a valid style." + +####################################### +# Detection of clang-format & friends # +####################################### + +# clang-format. +declare format="${CLANG_FORMAT:-}" +if [ -z "$format" ]; then + format=$(type -p clang-format) +fi + +if [ -z "$format" ]; then + error_exit \ + $'You need to install clang-format.\n' \ + $'\n' \ + $'On Ubuntu/Debian this is available in the clang-format package or, in\n' \ + $'older distro versions, clang-format-VERSION.\n' \ + $'On Fedora it\'s available in the clang package.\n' \ + $'You can also specify your own path for clang-format by setting the\n' \ + $'$CLANG_FORMAT environment variable.' +fi + +# clang-format-diff. +if [ "$whole_file" = false ]; then + invalid="/dev/null/invalid/path" + if [ "${OSTYPE:-}" = "linux-gnu" ]; then + readonly sort_version=-V + else + # On macOS, sort doesn't have -V. + readonly sort_version=-n + fi + declare paths_to_try=() + # .deb packages directly from upstream. + # We try these first as they are probably newer than the system ones. + while read -r f; do + paths_to_try+=("$f") + done < <(compgen -G "/usr/share/clang/clang-format-*/clang-format-diff.py" | sort "$sort_version" -r) + # LLVM official releases (just untarred in /usr/local). + while read -r f; do + paths_to_try+=("$f") + done < <(compgen -G "/usr/local/clang+llvm*/share/clang/clang-format-diff.py" | sort "$sort_version" -r) + # Maybe it's in the $PATH already? This is true for Ubuntu and Debian. + paths_to_try+=( \ + "$(type -p clang-format-diff 2> /dev/null || echo "$invalid")" \ + "$(type -p clang-format-diff.py 2> /dev/null || echo "$invalid")" \ + ) + # Fedora. + paths_to_try+=( \ + /usr/share/clang/clang-format-diff.py \ + ) + # Gentoo. + while read -r f; do + paths_to_try+=("$f") + done < <(compgen -G "/usr/lib/llvm/*/share/clang/clang-format-diff.py" | sort -n -r) + # Homebrew. + while read -r f; do + paths_to_try+=("$f") + done < <(compgen -G "/usr/local/Cellar/clang-format/*/share/clang/clang-format-diff.py" | sort -n -r) + + declare format_diff= + + # Did the user specify a path? + if [ -n "${CLANG_FORMAT_DIFF:-}" ]; then + format_diff="$CLANG_FORMAT_DIFF" + else + for path in "${paths_to_try[@]}"; do + if [ -e "$path" ]; then + # Found! + format_diff="$path" + if [ ! -x "$format_diff" ]; then + format_diff="python $format_diff" + fi + break + fi + done + fi + + if [ -z "$format_diff" ]; then + error_exit \ + $'Cannot find clang-format-diff which should be shipped as part of the same\n' \ + $'package where clang-format is.\n' \ + $'\n' \ + $'Please find out where clang-format-diff is in your distro and report an issue\n' \ + $'at https://github.com/barisione/clang-format-hooks/issues with details about\n' \ + $'your operating system and setup.\n' \ + $'\n' \ + $'You can also specify your own path for clang-format-diff by setting the\n' \ + $'$CLANG_FORMAT_DIFF environment variable, for instance:\n' \ + $'\n' \ + $' CLANG_FORMAT_DIFF="python /.../clang-format-diff.py" \\\n' \ + $' ' "$bash_source" + fi + + readonly format_diff +fi + + +############################ +# Actually run the command # +############################ + +if [ "$whole_file" = true ]; then + + [ "$has_positionals" = true ] || \ + error_exit "No files to reformat specified." + [ "$staged" = false ] || \ + error_exit "--staged/--cached only make sense when applying to a diff." + + read -r -a format_args <<< "$format" + format_args+=("-style=file") + [ "$in_place" = true ] && format_args+=("-i") + + "${format_args[@]}" "$@" + +else # Diff-only. + + if [ "$apply_to_staged" = true ]; then + [ "$staged" = false ] || \ + error_exit "You don't need --staged/--cached with --apply-to-staged." + [ "$in_place" = false ] || \ + error_exit "You don't need -i with --apply-to-staged." + staged=true + readonly patch_dest=$(mktemp) + trap '{ rm -f "$patch_dest"; }' EXIT + else + readonly patch_dest=/dev/stdout + fi + + declare git_args=(git diff -U0 --no-color) + [ "$staged" = true ] && git_args+=("--staged") + + # $format_diff may contain a command ("python") and the script to excute, so we + # need to split it. + read -r -a format_diff_args <<< "$format_diff" + [ "$in_place" = true ] && format_diff_args+=("-i") + + "${git_args[@]}" "$@" \ + | "${format_diff_args[@]}" \ + -p1 \ + -style="$style" \ + -iregex='^.*\.(c|cpp|cxx|cc|h|m|mm|js|java)$' \ + > "$patch_dest" \ + || exit 1 + + if [ "$apply_to_staged" = true ]; then + if [ ! -s "$patch_dest" ]; then + echo "No formatting changes to apply." + exit 0 + fi + patch -p0 < "$patch_dest" || \ + error_exit "Cannot apply patch to local files." + git apply -p0 --cached < "$patch_dest" || \ + error_exit "Cannot apply patch to git staged changes." + fi + +fi diff --git a/tools/git-pre-commit-format b/tools/git-pre-commit-format new file mode 100755 index 0000000..90fcc33 --- /dev/null +++ b/tools/git-pre-commit-format @@ -0,0 +1,367 @@ +#! /bin/bash +# +# Copyright 2018 Undo Ltd. +# +# https://github.com/barisione/clang-format-hooks + +# Force variable declaration before access. +set -u +# Make any failure in piped commands be reflected in the exit code. +set -o pipefail + +readonly bash_source="${BASH_SOURCE[0]:-$0}" + +if [ -t 1 ] && hash tput 2> /dev/null; then + readonly b=$(tput bold) + readonly i=$(tput sitm) + readonly n=$(tput sgr0) +else + readonly b= + readonly i= + readonly n= +fi + +function error_exit() { + for str in "$@"; do + echo -n "$b$str$n" >&2 + done + echo >&2 + + exit 1 +} + +# realpath is not available everywhere. +function realpath() { + if [ "${OSTYPE:-}" = "linux-gnu" ]; then + readlink -m "$@" + else + # Python should always be available on macOS. + # We use sys.stdout.write instead of print so it's compatible with both Python 2 and 3. + python -c "import sys; import os.path; sys.stdout.write(os.path.realpath('''$1''') + '\\n')" + fi +} + +# realpath --relative-to is only available on recent Linux distros. +# This function behaves identical to Python's os.path.relpath() and doesn't need files to exist. +function rel_realpath() { + local -r path=$(realpath "$1") + local -r rel_to=$(realpath "${2:-$PWD}") + + # Split the paths into components. + IFS='/' read -r -a path_parts <<< "$path" + IFS='/' read -r -a rel_to_parts <<< "$rel_to" + + # Search for the first different component. + for ((idx=1; idx<${#path_parts[@]}; idx++)); do + if [ "${path_parts[idx]}" != "${rel_to_parts[idx]:-}" ]; then + break + fi + done + + result=() + # Add the required ".." to the $result array. + local -r first_different_idx="$idx" + for ((idx=first_different_idx; idx<${#rel_to_parts[@]}; idx++)); do + result+=("..") + done + # Add the required components from $path. + for ((idx=first_different_idx; idx<${#path_parts[@]}; idx++)); do + result+=("${path_parts[idx]}") + done + + if [ "${#result[@]}" -gt 0 ]; then + # Join the array with a "/" as separator. + echo "$(export IFS='/'; echo "${result[*]}")" + else + echo . + fi +} + +# Find the top-level git directory (taking into account we could be in a submodule). +declare git_test_dir=. +declare top_dir + +while true; do + top_dir=$(cd "$git_test_dir" && git rev-parse --show-toplevel) || \ + error_exit "You need to be in the git repository to run this script." + + [ -e "$top_dir/.git" ] || \ + error_exit "No .git directory in $top_dir." + + if [ -d "$top_dir/.git" ]; then + # We are done! top_dir is the root git directory. + break + elif [ -f "$top_dir/.git" ]; then + # We are in a submodule or git work-tree if .git is a file! + if [ -z "$(git rev-parse --show-superproject-working-tree)" ]; then + # The --show-superproject-working-tree option is available and we + # are in a work tree. + gitdir=$(<"$top_dir/.git") + gitdir=${gitdir#gitdir: } + topdir_basename=${gitdir##*/} + git_test_dir=${gitdir%/worktrees/$topdir_basename} + break + fi + # If show-superproject-working-tree returns non-empty string, either: + # + # 1) --show-superproject-working-tree is not defined for this version of git + # + # 2) --show-superproject-working-tree is defined and we are in a submodule + # + # In the first case we will assume it is not a work tree because people + # using that advanced technology will be using a recent version of git. + # + # In second case, we could use the value returned by + # --show-superproject-working-tree directly but we do not here because + # that would require extra work. + # + git_test_dir="$git_test_dir/.." + fi +done + +readonly top_dir + +hook_path="$top_dir/.git/hooks/pre-commit" +readonly hook_path + +me=$(realpath "$bash_source") || exit 1 +readonly me + +me_relative_to_hook=$(rel_realpath "$me" "$(dirname "$hook_path")") || exit 1 +readonly me_relative_to_hook + +my_dir=$(dirname "$me") || exit 1 +readonly my_dir + +apply_format="$my_dir/apply-format" +readonly apply_format + +apply_format_relative_to_top_dir=$(rel_realpath "$apply_format" "$top_dir") || exit 1 +readonly apply_format_relative_to_top_dir + +function is_installed() { + if [ ! -e "$hook_path" ]; then + echo nothing + else + existing_hook_target=$(realpath "$hook_path") || exit 1 + readonly existing_hook_target + + if [ "$existing_hook_target" = "$me" ]; then + # Already installed. + echo installed + else + # There's a hook, but it's not us. + echo different + fi + fi +} + +function install() { + if ln -s "$me_relative_to_hook" "$hook_path" 2> /dev/null; then + echo "Pre-commit hook installed." + else + local -r res=$(is_installed) + if [ "$res" = installed ]; then + error_exit "The hook is already installed." + elif [ "$res" = different ]; then + error_exit "There's already an existing pre-commit hook, but for something else." + elif [ "$res" = nothing ]; then + error_exit "There's no pre-commit hook, but we couldn't create a symlink." + else + error_exit "Unexpected failure." + fi + fi +} + +function uninstall() { + local -r res=$(is_installed) + if [ "$res" = installed ]; then + rm "$hook_path" || \ + error_exit "Couldn't remove the pre-commit hook." + elif [ "$res" = different ]; then + error_exit "There's a pre-commit hook installed, but for something else. Not removing." + elif [ "$res" = nothing ]; then + error_exit "There's no pre-commit hook, nothing to uninstall." + else + error_exit "Unexpected failure detecting the pre-commit hook status." + fi +} + +function show_help() { + cat << EOF +${b}SYNOPSIS${n} + + $bash_source [install|uninstall] + +${b}DESCRIPTION${n} + + Git hook to verify and fix formatting before committing. + + The script is invoked automatically when you commit, so you need to call it + directly only to set up the hook or remove it. + + To setup the hook run this script passing "install" on the command line. + To remove the hook run passing "uninstall". + +${b}CONFIGURATION${n} + + You can configure the hook using the "git config" command. + + ${b}hooks.clangFormatDiffInteractive${n} (default: true) + By default, the hook requires user input. If you don't run git from a + terminal, you can disable the interactive prompt with: + ${i}\$ git config hooks.clangFormatDiffInteractive false${n} + + ${b}hooks.clangFormatDiffStyle${n} (default: file) + Unless a different style is specified, the hook expects a file named + .clang-format to exist in the repository. This file should contain the + configuration for clang-format. + You can specify a different style (in this example, the WebKit one) + with: + ${i}\$ git config hooks.clangFormatDiffStyle WebKit${n} +EOF +} + +if [ $# = 1 ]; then + case "$1" in + -h | -\? | --help ) + show_help + exit 0 + ;; + install ) + install + exit 0 + ;; + uninstall ) + uninstall + exit 0 + ;; + esac +fi + +[ $# = 0 ] || error_exit "Invalid arguments: $*" + + +# This is a real run of the hook, not a install/uninstall run. + +if [ -z "${GIT_DIR:-}" ] && [ -z "${GIT_INDEX_FILE:-}" ]; then + error_exit \ + $'It looks like you invoked this script directly, but it\'s supposed to be used\n' \ + $'as a pre-commit git hook.\n' \ + $'\n' \ + $'To install the hook try:\n' \ + $' ' "$bash_source" $' install\n' \ + $'\n' \ + $'For more details on this script try:\n' \ + $' ' "$bash_source" $' --help\n' +fi + +[ -x "$apply_format" ] || \ + error_exit \ + $'Cannot find the apply-format script.\n' \ + $'I expected it here:\n' \ + $' ' "$apply_format" + +readonly style=$(cd "$top_dir" && git config hooks.clangFormatDiffStyle || echo file) + +readonly patch=$(mktemp) +trap '{ rm -f "$patch"; }' EXIT +"$apply_format" --style="$style" --cached > "$patch" || \ + error_exit $'\nThe apply-format script failed.' + +if [ "$(wc -l < "$patch")" -eq 0 ]; then + echo "The staged content is formatted correctly." + exit 0 +fi + + +# The code is not formatted correctly. + +interactive=$(cd "$top_dir" && git config --bool hooks.clangFormatDiffInteractive) +if [ "$interactive" != false ]; then + # Interactive is the default, so anything that is not false is converted to + # true, including possibly invalid values. + interactive=true +fi +readonly interactive + +if [ "$interactive" = false ]; then + echo "${b}The staged content is not formatted correctly.${n}" + echo "You can fix the formatting with:" + echo " ${i}\$ ./$apply_format_relative_to_top_dir --apply-to-staged${n}" + echo + echo "You can also make this script interactive (if you use git from a terminal) with:" + echo " ${i}\$ git config hooks.clangFormatDiffInteractive true${n}" + exit 1 +fi + +if hash colordiff 2> /dev/null; then + colordiff < "$patch" +else + echo "${b}(Install colordiff to see this diff in color!)${n}" + echo + cat "$patch" +fi + +echo +echo "${b}The staged content is not formatted correctly.${n}" +echo "The patch shown above can be applied automatically to fix the formatting." +echo + +echo "You can:" +echo " [a]: Apply the patch" +echo " [f]: Force and commit anyway (not recommended!)" +echo " [c]: Cancel the commit" +echo " [?]: Show help" +echo + +readonly tty=${PRE_COMMIT_HOOK_TTY:-/dev/tty} + +while true; do + echo -n "What would you like to do? [a/f/c/?] " + read -r answer < "$tty" + case "$answer" in + + [aA] ) + patch -p0 < "$patch" || \ + error_exit \ + $'\n' \ + $'Cannot apply patch to local files.\n' \ + $'Have you modified the file locally after starting the commit?' + git apply -p0 --cached < "$patch" || \ + error_exit \ + $'\n' \ + $'Cannot apply patch to git staged changes.\n' \ + $'This may happen if you have some overlapping unstaged changes. To solve\n' \ + $'you need to stage or reset changes manually.' + ;; + + [fF] ) + echo + echo "Will commit anyway!" + echo "You can always abort by quitting your editor with no commit message." + echo + echo -n "Press return to continue." + read -r < "$tty" + exit 0 + ;; + + [cC] ) + error_exit "Commit aborted as requested." + ;; + + \? ) + echo + show_help + echo + continue + ;; + + * ) + echo 'Invalid answer. Type "a", "f" or "c".' + echo + continue + + esac + break +done