Skip to content

Commit

Permalink
feat: add a use_execroot_entry_point attribute to js_binary to allow …
Browse files Browse the repository at this point in the history
…for running without a runfiles (#1428)
  • Loading branch information
gregmagolan authored Jan 7, 2024
1 parent 42b751a commit 9a4b7c8
Show file tree
Hide file tree
Showing 12 changed files with 211 additions and 121 deletions.
14 changes: 0 additions & 14 deletions .bazelrc.common
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,6 @@ build:debug --compilation_mode=dbg
# https://github.com/aspect-build/bazel-lib/blob/main/docs/copy_directory.md
startup --host_jvm_args=-DBAZEL_TRACK_SOURCE_DIRECTORIES=1

# In general, the rules in this repository assume that runfiles
# are enabled as we do not support no runfiles case.
#
# If you are developing on Windows, you must either run bazel
# with administrator priviledges or enable developer mode. If
# you do not you may hit this error on Windows:
#
# Bazel needs to create symlinks to build the runfiles tree.
# Creating symlinks on Windows requires one of the following:
# 1. Bazel is run with administrator privileges.
# 2. The system version is Windows 10 Creators Update (1703) or later
# and developer mode is enabled.
build --enable_runfiles

build --incompatible_allow_tags_propagation

# Turn off legacy external runfiles on all platforms except Windows.
Expand Down
3 changes: 0 additions & 3 deletions docs/js_binary.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions e2e/bzlmod/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ load("@npm//:less/package_json.bzl", less_bin = "bin")
load("@npm//:jasmine/package_json.bzl", jasmine_bin = "bin")
load("@npm_meaning-of-life__links//:defs.bzl", npm_link_meaning_of_life = "npm_link_imported_package")

not_windows = select({
"@platforms//os:windows": ["@platforms//:incompatible"],
"//conditions:default": [],
})

npm_link_all_packages(
name = "node_modules",
imported_links = [
Expand Down Expand Up @@ -36,12 +41,16 @@ assert_contains(
name = "check_styles",
actual = "my.css",
expected = ".box,\n.bar {\n width: 100px;",
# assert_contains currently requires runfiles; needs fixing upstream
target_compatible_with = not_windows,
)

jasmine_bin.jasmine_test(
name = "jasmine_test",
args = ["*.spec.js"],
data = ["test.spec.js"],
# jasmine doesn't know to run without runfiles
target_compatible_with = not_windows,
)

build_test(
Expand Down
2 changes: 1 addition & 1 deletion js/private/coverage/coverage.sh.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ if [ ! -f "$entry_point" ]; then
exit 1
fi

node="$RUNFILES/{{node}}"
node="$RUNFILES/{{workspace_name}}/{{node}}"
if [ ! -f "$node" ]; then
logf_fatal "node binary '%s' not found in runfiles" "$node"
exit 1
Expand Down
6 changes: 3 additions & 3 deletions js/private/coverage/merger.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ _ATTRS = {
# https://github.com/bazelbuild/rules_nodejs/blob/8b5d27400db51e7027fe95ae413eeabea4856f8e/nodejs/toolchain.bzl#L50
# to get back to the short_path.
# TODO: fix toolchain so we don't have to do this
def _target_tool_short_path(workspace_name, path):
return (workspace_name + "/../" + path[len("external/"):]) if path.startswith("external/") else path
def _target_tool_path_to_short_path(tool_path):
return ("../" + tool_path[len("external/"):]) if tool_path.startswith("external/") else tool_path

def _coverage_merger_impl(ctx):
is_windows = ctx.target_platform_has_constraint(ctx.attr._windows_constraint[platform_common.ConstraintValueInfo])
node_bin = ctx.toolchains["@rules_nodejs//nodejs:toolchain_type"].nodeinfo

# Create launcher
bash_launcher = ctx.actions.declare_file("%s.sh" % ctx.label.name)
node_path = _target_tool_short_path(ctx.workspace_name, ctx.toolchains["@rules_nodejs//nodejs:toolchain_type"].nodeinfo.target_tool_path)
node_path = _target_tool_path_to_short_path(ctx.toolchains["@rules_nodejs//nodejs:toolchain_type"].nodeinfo.target_tool_path)
ctx.actions.expand_template(
template = ctx.file._launcher_template,
output = bash_launcher,
Expand Down
16 changes: 6 additions & 10 deletions js/private/js_binary.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,6 @@ The following environment variables are made available to the Node.js runtime ba
* JS_BINARY__NODE_WRAPPER: the Node.js wrapper script used to run Node.js which is available as `node` on the `PATH` at runtime
* JS_BINARY__RUNFILES: the absolute path to the Bazel runfiles directory
* JS_BINARY__EXECROOT: the absolute path to the root of the execution root for the action; if in the sandbox, this path absolute path to the root of the execution root within the sandbox
This rules requires that Bazel was run with
[`--enable_runfiles`](https://docs.bazel.build/versions/main/command-line-reference.html#flag--enable_runfiles).
"""

_ATTRS = {
Expand Down Expand Up @@ -330,8 +327,8 @@ _NODE_OPTION = """JS_BINARY__NODE_OPTIONS+=(\"{value}\")"""
# https://github.com/bazelbuild/rules_nodejs/blob/8b5d27400db51e7027fe95ae413eeabea4856f8e/nodejs/toolchain.bzl#L50
# to get back to the short_path.
# TODO: fix toolchain so we don't have to do this
def _target_tool_short_path(workspace_name, path):
return (workspace_name + "/../" + path[len("external/"):]) if path.startswith("external/") else path
def _target_tool_path_to_short_path(tool_path):
return ("../" + tool_path[len("external/"):]) if tool_path.startswith("external/") else tool_path

# Generate a consistent label string between Bazel versions.
# TODO: hoist this function to bazel-lib and use from there (as well as the dup in npm/private/utils.bzl)
Expand Down Expand Up @@ -382,6 +379,8 @@ def _bash_launcher(ctx, node_toolchain, entry_point_path, log_prefix_rule_set, l
),
"JS_BINARY__WORKSPACE": ctx.workspace_name,
}
if is_windows and not ctx.attr.enable_runfiles:
builtins["JS_BINARY__NO_RUNFILES"] = "1"
for (key, value) in builtins.items():
envs.append(_ENV_SET.format(var = key, value = value))

Expand Down Expand Up @@ -441,7 +440,7 @@ def _bash_launcher(ctx, node_toolchain, entry_point_path, log_prefix_rule_set, l

npm_path = ""
if ctx.attr.include_npm:
npm_path = _target_tool_short_path(ctx.workspace_name, node_toolchain.nodeinfo.npm_path)
npm_path = _target_tool_path_to_short_path(node_toolchain.nodeinfo.npm_path)
if is_windows:
npm_wrapper = ctx.actions.declare_file("%s_node_bin/npm.bat" % ctx.label.name)
ctx.actions.expand_template(
Expand All @@ -460,7 +459,7 @@ def _bash_launcher(ctx, node_toolchain, entry_point_path, log_prefix_rule_set, l
)
toolchain_files.append(npm_wrapper)

node_path = _target_tool_short_path(ctx.workspace_name, node_toolchain.nodeinfo.target_tool_path)
node_path = _target_tool_path_to_short_path(node_toolchain.nodeinfo.target_tool_path)

launcher_subst = {
"{{target_label}}": _consistent_label_str(ctx.workspace_name, ctx.label),
Expand Down Expand Up @@ -503,9 +502,6 @@ def _create_launcher(ctx, log_prefix_rule_set, log_prefix_rule, fixed_args = [],
else:
node_toolchain = ctx.toolchains["@rules_nodejs//nodejs:toolchain_type"]

if is_windows and not ctx.attr.enable_runfiles:
fail("need --enable_runfiles on Windows for to support rules_js")

if ctx.attr.include_npm and not hasattr(node_toolchain.nodeinfo, "npm_files"):
fail("include_npm requires a minimum @rules_nodejs version of 5.7.0")

Expand Down
119 changes: 85 additions & 34 deletions js/private/js_binary.sh.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,24 @@ function logf_debug {
fi
}

function resolve_execroot_bin_path {
local short_path="$1"
if [[ "$short_path" == ../* ]]; then
echo "$JS_BINARY__EXECROOT/${BAZEL_BINDIR:-$JS_BINARY__BINDIR}/external/${short_path:3}"
else
echo "$JS_BINARY__EXECROOT/${BAZEL_BINDIR:-$JS_BINARY__BINDIR}/$short_path"
fi
}

function resolve_execroot_src_path {
local short_path="$1"
if [[ "$short_path" == ../* ]]; then
echo "$JS_BINARY__EXECROOT/external/${short_path:3}"
else
echo "$JS_BINARY__EXECROOT/$short_path"
fi
}

_exit() {
EXIT_CODE=$?

Expand Down Expand Up @@ -158,8 +176,8 @@ elif [[ "$PWD" == *"/bazel-~1/"* ]]; then
fi

if [[ "${bazel_out_segment:-}" ]]; then
if [ "${JS_BINARY__USE_EXECROOT_ENTRY_POINT:-}" ] && [ "${JS_BINARY__EXECROOT:-}" ]; then
logf_debug "inheriting JS_BINARY__EXECROOT %s from parent js_binary process as JS_BINARY__USE_EXECROOT_ENTRY_POINT is set" "$JS_BINARY__EXECROOT"
if [ "${JS_RUN_BINARY__USE_EXECROOT_ENTRY_POINT:-}" ] && [ "${JS_BINARY__EXECROOT:-}" ]; then
logf_debug "inheriting JS_BINARY__EXECROOT %s from parent js_binary process as JS_RUN_BINARY__USE_EXECROOT_ENTRY_POINT is set" "$JS_BINARY__EXECROOT"
else
# We in runfiles and we don't yet know the execroot
rest="${PWD#*"$bazel_out_segment"}"
Expand All @@ -171,8 +189,8 @@ if [[ "${bazel_out_segment:-}" ]]; then
JS_BINARY__EXECROOT="${PWD:0:$index}"
fi
else
if [ "${JS_BINARY__USE_EXECROOT_ENTRY_POINT:-}" ] && [ "${JS_BINARY__EXECROOT:-}" ]; then
logf_debug "inheriting JS_BINARY__EXECROOT %s from parent js_binary process as JS_BINARY__USE_EXECROOT_ENTRY_POINT is set" "$JS_BINARY__EXECROOT"
if [ "${JS_RUN_BINARY__USE_EXECROOT_ENTRY_POINT:-}" ] && [ "${JS_BINARY__EXECROOT:-}" ]; then
logf_debug "inheriting JS_BINARY__EXECROOT %s from parent js_binary process as JS_RUN_BINARY__USE_EXECROOT_ENTRY_POINT is set" "$JS_BINARY__EXECROOT"
else
# We are in execroot or in some other context all together such as a nodejs_image or a manually run js_binary
JS_BINARY__EXECROOT="$PWD"
Expand Down Expand Up @@ -200,21 +218,28 @@ aspect_rules_js README https://github.com/aspect-build/rules_js/tree/dbb5af0d2a9
fi
export JS_BINARY__EXECROOT

if [ "${JS_BINARY__USE_EXECROOT_ENTRY_POINT:-}" ]; then
if [ "${JS_RUN_BINARY__USE_EXECROOT_ENTRY_POINT:-}" ]; then
if [ -z "${BAZEL_BINDIR:-}" ]; then
logf_fatal "Expected BAZEL_BINDIR to be set when JS_BINARY__USE_EXECROOT_ENTRY_POINT is set"
logf_fatal "Expected BAZEL_BINDIR to be set when JS_RUN_BINARY__USE_EXECROOT_ENTRY_POINT is set"
exit 1
fi
if [ -z "${JS_BINARY__COPY_DATA_TO_BIN:-}" ] && [ -z "${JS_BINARY__ALLOW_EXECROOT_ENTRY_POINT_WITH_NO_COPY_DATA_TO_BIN:-}" ]; then
logf_fatal "Expected js_binary copy_data_to_bin to be True when js_run_binary use_execroot_entry_point is True. \
To disable this validation you can set allow_execroot_entry_point_with_no_copy_data_to_bin to True in js_run_binary"
exit 1
fi
entry_point="{{entry_point_path}}"
if [[ "$entry_point" == ../* ]]; then
entry_point="external/${entry_point:3}"
fi

if [ "${JS_BINARY__NO_RUNFILES:-}" ]; then
if [ -z "${JS_BINARY__COPY_DATA_TO_BIN:-}" ] && [ -z "${JS_BINARY__ALLOW_EXECROOT_ENTRY_POINT_WITH_NO_COPY_DATA_TO_BIN:-}" ]; then
logf_fatal "Expected js_binary copy_data_to_bin to be True when js_binary use_execroot_entry_point is True. \
To disable this validation you can set allow_execroot_entry_point_with_no_copy_data_to_bin to True in js_run_binary"
exit 1
fi
entry_point="$JS_BINARY__EXECROOT/$BAZEL_BINDIR/$entry_point"
fi

if [ "${JS_RUN_BINARY__USE_EXECROOT_ENTRY_POINT:-}" ] || [ "${JS_BINARY__NO_RUNFILES:-}" ]; then
entry_point=$(resolve_execroot_bin_path "{{entry_point_path}}")
else
entry_point="$JS_BINARY__RUNFILES/{{workspace_name}}/{{entry_point_path}}"
fi
Expand All @@ -232,9 +257,14 @@ if [ "${node:0:1}" = "/" ]; then
exit 1
fi
else
export JS_BINARY__NODE_BINARY="$JS_BINARY__RUNFILES/{{node}}"
if [ "${JS_BINARY__NO_RUNFILES:-}" ]; then
export JS_BINARY__NODE_BINARY
JS_BINARY__NODE_BINARY=$(resolve_execroot_src_path "{{node}}")
else
export JS_BINARY__NODE_BINARY="$JS_BINARY__RUNFILES/{{workspace_name}}/{{node}}"
fi
if [ ! -f "$JS_BINARY__NODE_BINARY" ]; then
logf_fatal "node binary '%s' not found in runfiles" "$JS_BINARY__NODE_BINARY"
logf_fatal "node binary '%s' not found" "$JS_BINARY__NODE_BINARY"
exit 1
fi
fi
Expand All @@ -254,9 +284,14 @@ if [ "$npm" ]; then
exit 1
fi
else
export JS_BINARY__NPM_BINARY="$JS_BINARY__RUNFILES/{{npm}}"
if [ "${JS_BINARY__NO_RUNFILES:-}" ]; then
export JS_BINARY__NPM_BINARY
JS_BINARY__NPM_BINARY=$(resolve_execroot_src_path "{{npm}}")
else
export JS_BINARY__NPM_BINARY="$JS_BINARY__RUNFILES/{{workspace_name}}/{{npm}}"
fi
if [ ! -f "$JS_BINARY__NPM_BINARY" ]; then
logf_fatal "npm binary '%s' not found in runfiles" "$JS_BINARY__NPM_BINARY"
logf_fatal "npm binary '%s' not found" "$JS_BINARY__NPM_BINARY"
exit 1
fi
fi
Expand All @@ -266,19 +301,29 @@ if [ "$npm" ]; then
fi
fi

export JS_BINARY__NODE_WRAPPER="$JS_BINARY__RUNFILES/{{workspace_name}}/{{node_wrapper}}"
if [ "${JS_BINARY__NO_RUNFILES:-}" ]; then
export JS_BINARY__NODE_WRAPPER
JS_BINARY__NODE_WRAPPER=$(resolve_execroot_bin_path "{{node_wrapper}}")
else
export JS_BINARY__NODE_WRAPPER="$JS_BINARY__RUNFILES/{{workspace_name}}/{{node_wrapper}}"
fi
if [ ! -f "$JS_BINARY__NODE_WRAPPER" ]; then
logf_fatal "node wrapper '%s' not found in runfiles" "$JS_BINARY__NODE_WRAPPER"
logf_fatal "node wrapper '%s' not found" "$JS_BINARY__NODE_WRAPPER"
exit 1
fi
if [ "$_IS_WINDOWS" -ne "1" ] && [ ! -x "$JS_BINARY__NODE_WRAPPER" ]; then
logf_fatal "node wrapper '%s' is not executable" "$JS_BINARY__NODE_WRAPPER"
exit 1
fi

export JS_BINARY__NODE_PATCHES="$JS_BINARY__RUNFILES/{{workspace_name}}/{{node_patches}}"
if [ "${JS_BINARY__NO_RUNFILES:-}" ]; then
export JS_BINARY__NODE_PATCHES
JS_BINARY__NODE_PATCHES=$(resolve_execroot_src_path "{{node_patches}}")
else
export JS_BINARY__NODE_PATCHES="$JS_BINARY__RUNFILES/{{workspace_name}}/{{node_patches}}"
fi
if [ ! -f "$JS_BINARY__NODE_PATCHES" ]; then
logf_fatal "node patches '%s' not found in runfiles" "$JS_BINARY__NODE_PATCHES"
logf_fatal "node patches '%s' not found" "$JS_BINARY__NODE_PATCHES"
exit 1
fi

Expand Down Expand Up @@ -351,32 +396,38 @@ if [ "${JS_BINARY__LOG_DEBUG:-}" ]; then
if [ "${BAZEL_WORKSPACE:-}" ]; then
logf_debug "BAZEL_WORKSPACE %s" "$BAZEL_WORKSPACE"
fi
logf_debug "js_binary FS_PATCH_ROOTS %s" "${JS_BINARY__FS_PATCH_ROOTS:-}"
logf_debug "js_binary NODE_PATCHES %s" "${JS_BINARY__NODE_PATCHES:-}"
logf_debug "js_binary NODE_OPTIONS %s" "${JS_BINARY__NODE_OPTIONS:-}"
logf_debug "js_binary BINDIR %s" "${JS_BINARY__BINDIR:-}"
logf_debug "js_binary BUILD_FILE_PATH %s" "${JS_BINARY__BUILD_FILE_PATH:-}"
logf_debug "js_binary COMPILATION_MODE %s" "${JS_BINARY__COMPILATION_MODE:-}"
logf_debug "js_binary NODE_BINARY %s" "${JS_BINARY__NODE_BINARY:-}"
logf_debug "js_binary NODE_WRAPPER %s" "${JS_BINARY__NODE_WRAPPER:-}"
logf_debug "JS_BINARY__FS_PATCH_ROOTS %s" "${JS_BINARY__FS_PATCH_ROOTS:-}"
logf_debug "JS_BINARY__NODE_PATCHES %s" "${JS_BINARY__NODE_PATCHES:-}"
logf_debug "JS_BINARY__NODE_OPTIONS %s" "${JS_BINARY__NODE_OPTIONS:-}"
logf_debug "JS_BINARY__BINDIR %s" "${JS_BINARY__BINDIR:-}"
logf_debug "JS_BINARY__BUILD_FILE_PATH %s" "${JS_BINARY__BUILD_FILE_PATH:-}"
logf_debug "JS_BINARY__COMPILATION_MODE %s" "${JS_BINARY__COMPILATION_MODE:-}"
logf_debug "JS_BINARY__NODE_BINARY %s" "${JS_BINARY__NODE_BINARY:-}"
logf_debug "JS_BINARY__NODE_WRAPPER %s" "${JS_BINARY__NODE_WRAPPER:-}"
if [ "${JS_BINARY__NPM_BINARY:-}" ]; then
logf_debug "js_binary NPM_BINARY %s" "$JS_BINARY__NPM_BINARY"
logf_debug "JS_BINARY__NPM_BINARY %s" "$JS_BINARY__NPM_BINARY"
fi
if [ "${JS_BINARY__NO_RUNFILES:-}" ]; then
logf_debug "JS_BINARY__NO_RUNFILES %s" "$JS_BINARY__NO_RUNFILES"
fi
logf_debug "js_binary PACKAGE %s" "${JS_BINARY__PACKAGE:-}"
logf_debug "js_binary TARGET_CPU %s" "${JS_BINARY__TARGET_CPU:-}"
logf_debug "js_binary TARGET_NAME %s" "${JS_BINARY__TARGET_NAME:-}"
logf_debug "js_binary WORKSPACE %s" "${JS_BINARY__WORKSPACE:-}"
logf_debug "JS_BINARY__PACKAGE %s" "${JS_BINARY__PACKAGE:-}"
logf_debug "JS_BINARY__TARGET_CPU %s" "${JS_BINARY__TARGET_CPU:-}"
logf_debug "JS_BINARY__TARGET_NAME %s" "${JS_BINARY__TARGET_NAME:-}"
logf_debug "JS_BINARY__WORKSPACE %s" "${JS_BINARY__WORKSPACE:-}"
logf_debug "js_binary entry point %s" "$entry_point"
if [ "${JS_RUN_BINARY__USE_EXECROOT_ENTRY_POINT:-}" ]; then
logf_debug "JS_RUN_BINARY__USE_EXECROOT_ENTRY_POINT %s" "$JS_RUN_BINARY__USE_EXECROOT_ENTRY_POINT"
fi
fi

# Info logs
if [ "${JS_BINARY__LOG_INFO:-}" ]; then
if [ "${BAZEL_TARGET:-}" ]; then
logf_info "BAZEL_TARGET %s" "${BAZEL_TARGET:-}"
fi
logf_info "js_binary TARGET %s" "${JS_BINARY__TARGET:-}"
logf_info "js_binary RUNFILES %s" "${JS_BINARY__RUNFILES:-}"
logf_info "js_binary EXECROOT %s" "${JS_BINARY__EXECROOT:-}"
logf_info "JS_BINARY__TARGET %s" "${JS_BINARY__TARGET:-}"
logf_info "JS_BINARY__RUNFILES %s" "${JS_BINARY__RUNFILES:-}"
logf_info "JS_BINARY__EXECROOT %s" "${JS_BINARY__EXECROOT:-}"
logf_info "PWD %s" "$PWD"
fi

Expand Down
2 changes: 1 addition & 1 deletion js/private/js_run_binary.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ See https://github.com/aspect-build/rules_js/tree/main/docs#using-binaries-publi

# Configure run from execroot
if use_execroot_entry_point:
fixed_env["JS_BINARY__USE_EXECROOT_ENTRY_POINT"] = "1"
fixed_env["JS_RUN_BINARY__USE_EXECROOT_ENTRY_POINT"] = "1"

# hoist all runfiles to srcs when running from execroot
js_runfiles_lib_name = "{}_runfiles_lib".format(name)
Expand Down
14 changes: 7 additions & 7 deletions js/private/js_run_devserver.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,18 @@ function mkdirpSync(p) {
// isNodeModulePath('/private/var/.../node_modules/lodash') // true
// isNodeModulePath('/private/var/.../some-file.js') // false
function isNodeModulePath(srcPath) {
const parentDir = path.dirname(srcPath);
const parentDirName = path.basename(parentDir);
const parentDir = path.dirname(srcPath)
const parentDirName = path.basename(parentDir)

if (parentDirName === 'node_modules') {
// unscoped module like 'lodash'
return true;
return true
} else if (parentDirName.startsWith('@')) {
// scoped module like '@babel/core'
const parentParentDir = path.dirname(parentDir);
return path.basename(parentParentDir) === 'node_modules';
const parentParentDir = path.dirname(parentDir)
return path.basename(parentParentDir) === 'node_modules'
}
return false;
return false
}

// Recursively copies a file, symlink or directory to a destination. If the file has been previously
Expand Down Expand Up @@ -209,7 +209,7 @@ async function main(args, sandbox) {
// to determine the execroot entry point but since the tool is running
// in a custom sandbox we don't want to cd into the BAZEL_BINDIR in the launcher
// (JS_BINARY__NO_CD_BINDIR is set above)
env['JS_BINARY__USE_EXECROOT_ENTRY_POINT'] = '1'
env['JS_RUN_BINARY__USE_EXECROOT_ENTRY_POINT'] = '1'
env['BAZEL_BINDIR'] = config.bazel_bindir
if (config.allow_execroot_entry_point_with_no_copy_data_to_bin) {
env[
Expand Down
2 changes: 1 addition & 1 deletion js/private/test/image/structure/digests.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
9546c440ffbbe36177f305a1aec4bf89fa08eea5926fe828c8e9edd8e08a16a7
3437ee527e86bfa0bc88427fa33a2cfc625a5bb5f19c9c409e49640a8e89da4e
ea187b7d895f6fecc13eb08ff07bc9a1765a39cd082d4cfbb65739d0e66dcf95
Loading

0 comments on commit 9a4b7c8

Please sign in to comment.