Skip to content

Instantly share code, notes, and snippets.

@BeyondEvil
Created October 29, 2022 15:46
Show Gist options
  • Save BeyondEvil/95155a60418c29aa7ddb7f46a58e6975 to your computer and use it in GitHub Desktop.
Save BeyondEvil/95155a60418c29aa7ddb7f46a58e6975 to your computer and use it in GitHub Desktop.
bazel js image
# apps/gamma/BUILD.bazel
load("//docker:js_image_layer.bzl", "js_image_layer")
load("@aspect_rules_js//js:defs.bzl", "js_binary", "js_library")
load("@npm//:defs.bzl", "npm_link_all_packages")
load("@io_bazel_rules_docker//container:container.bzl", "container_image", "container_push")
load("@io_bazel_rules_docker//container:layer.bzl", "container_layer")
npm_link_all_packages(name = "node_modules")
js_library(
name = "gamma",
srcs = ["src/main.js"],
deps = [
"//:node_modules/express",
],
)
js_binary(
name = "main",
data = [":gamma"],
entry_point = "src/main.js",
)
js_image_layer(
name = "layers",
binary = ":main",
root = "/app",
tags = ["no-remote-exec"],
)
container_layer(
name = "app_layer",
tars = [":layers/app.tar"],
)
container_layer(
name = "node_modules_layer",
tars = [":layers/node_modules.tar"],
)
container_image(
name = "image",
architecture = "arm64",
base = "@debian_arm64//image",
# workdir = "/app/apps/gamma/main.sh.runfiles/bazel_poc",
# cmd = ["apps/gamma/main.sh"],
cmd = ["/app/apps/gamma/main.sh"],
entrypoint = ["bash"],
# env = {
# "JS_BINARY__CHDIR": "/app/apps/gamma/main.sh.runfiles/bazel_poc",
# },
layers = [
":app_layer",
":node_modules_layer",
],
)
container_push(
name = "push",
format = "Docker",
registry = "localhost:5001",
repository = "proxyco/bazel-poc-gamma",
image = ":image",
)
# docker/js_image_layer.bzl
"contains container helper functions for js_binary"
load("@rules_pkg//:providers.bzl", "PackageFilegroupInfo", "PackageFilesInfo", "PackageSymlinkInfo")
load("@rules_pkg//:pkg.bzl", "pkg_tar")
load("@bazel_skylib//rules:write_file.bzl", "write_file")
load("@aspect_bazel_lib//lib:paths.bzl", "to_manifest_path")
load("@rules_python//python:defs.bzl", "py_binary")
# BAZEL_BINDIR has to be set to '.' so that js_binary preserves the PWD when running inside container.
# See https://github.com/aspect-build/rules_js/tree/dbb5af0d2a9a2bb50e4cf4a96dbc582b27567155#running-nodejs-programs
# for why this is needed.
_LAUNCHER_TMPL = """
export BAZEL_BINDIR=.
source {executable_path}
"""
def _write_laucher(ctx, executable_path):
"Creates a call-through shell entrypoint which sets BAZEL_BINDIR to '.' then immediately invokes the original entrypoint."
launcher = ctx.actions.declare_file("%s_launcher.sh" % ctx.label.package)
ctx.actions.write(
output = launcher,
content = _LAUNCHER_TMPL.format(executable_path = executable_path),
is_executable = True,
)
return launcher
def _runfile_path(ctx, file, runfiles_dir):
return "/".join([runfiles_dir, to_manifest_path(ctx, file)])
def _should_include(destination, include, exclude):
included = include in destination or include == ""
excluded = exclude in destination and exclude != ""
return included and not excluded
def _runfiles_impl(ctx):
default = ctx.attr.binary[DefaultInfo]
executable = default.files_to_run.executable
executable_path = "/".join([ctx.attr.root, executable.short_path])
original_executable_path = executable_path.replace(".sh", "_.sh")
launcher = _write_laucher(ctx, original_executable_path)
file_map = {}
if _should_include(original_executable_path, ctx.attr.include, ctx.attr.exclude):
file_map[original_executable_path] = executable
if _should_include(executable_path, ctx.attr.include, ctx.attr.exclude):
file_map[executable_path] = launcher
manifest = default.files_to_run.runfiles_manifest
runfiles_dir = "/".join([ctx.attr.root, manifest.short_path.replace(manifest.basename, "")[:-1]])
files = depset(transitive = [default.files, default.default_runfiles.files])
for file in files.to_list():
destination = _runfile_path(ctx, file, runfiles_dir)
if _should_include(destination, ctx.attr.include, ctx.attr.exclude):
file_map[destination] = file
# executable and launcher should not go into runfiles directory so we add it to files here
files = depset([executable, launcher], transitive = [files])
symlinks = []
# NOTE: symlinks is different than root_symlinks. See: https://bazel.build/rules/rules#runfiles_symlinks for distinction between
# root_symlinks and symlinks and why they have to be handled differently.
for symlink in default.data_runfiles.symlinks.to_list():
destination = "/".join([runfiles_dir, ctx.workspace_name, symlink.path])
if not _should_include(destination, ctx.attr.include, ctx.attr.exclude):
continue
if hasattr(file_map, destination):
file_map.pop(destination)
info = PackageSymlinkInfo(
target = "/%s" % _runfile_path(ctx, symlink.target_file, runfiles_dir),
destination = destination,
attributes = {"mode": "0777"},
)
symlinks.append([info, symlink.target_file.owner])
for symlink in default.data_runfiles.root_symlinks.to_list():
destination = "/".join([runfiles_dir, symlink.path])
if not _should_include(destination, ctx.attr.include, ctx.attr.exclude):
continue
if hasattr(file_map, destination):
file_map.pop(destination)
info = PackageSymlinkInfo(
target = "/%s" % _runfile_path(ctx, symlink.target_file, runfiles_dir),
destination = destination,
attributes = {"mode": "0777"},
)
symlinks.append([info, symlink.target_file.owner])
return [
PackageFilegroupInfo(
pkg_dirs = [],
pkg_files = [
[PackageFilesInfo(
dest_src_map = file_map,
attributes = {},
), ctx.label],
],
pkg_symlinks = symlinks,
),
DefaultInfo(files = files),
]
runfiles = rule(
implementation = _runfiles_impl,
attrs = {
"binary": attr.label(mandatory = True),
"root": attr.string(),
"include": attr.string(),
"exclude": attr.string(),
},
)
# pkg_tar has poor support for symlinks due to bazel not providing enough information about symlinks.
#
# ```starlark
# actual_file = ctx.actions.declare_file("thefile.txt")
# symlink_file = ctx.actions.declare_file("this_is_a_symlink.txt")
# ctx.actions.symlink(symlink_file, target_file = actual_file)
# ```
# to determine a file is file or directory pkg_tar checks `is_directory` if it is false then it makes the assumption
# that the file is in fact is a file which not true for `symlink_file` above. this is where pkg_tar fails to build
# a correct tar file.
# In order to fix this, manifest written pkg_tar should be fixed by looking for files that are symlink instead of file.
# TODO(thesayyn): remove this once pkg_tar is fixed.
#
# See: https://github.com/bazelbuild/rules_pkg/issues/115#issuecomment-1190494335
BUILD_TAR = """
import os
import sys
import json
import subprocess
from rules_python.python.runfiles import runfiles
r = runfiles.Create()
build_tar_path = r.Rlocation("rules_pkg/pkg/private/tar/build_tar")
manifest_path = next((argv.replace("--manifest=", "") for argv in sys.argv if argv.startswith("--manifest=")), None)
with open(manifest_path, "r") as manifest_fp:
manifest = json.load(manifest_fp)
def strip_execroot(p):
parts = p.split(os.sep)
i = parts.index("execroot")
return os.sep.join(parts[i+2:])
def get_runfiles_path(p):
for entry in manifest:
if entry[2] == p:
return entry[1]
raise Exception("could not find a corresponding file for %s within manifest.")
for entry in manifest:
p = entry[2]
if "node_modules" in p and os.path.islink(p):
link_to = os.path.realpath(p)
link_to_execroot_stripped = strip_execroot(link_to)
overwritten_to = get_runfiles_path(link_to_execroot_stripped)
# fix it!
entry[0] = 1 # make it a symlink
entry[2] = "/%s" % overwritten_to
entry[3] = "0777"
os.remove(manifest_path)
with open(manifest_path, "w") as manifest_w:
manifest_w.write(json.dumps(manifest))
r = subprocess.run([build_tar_path] + sys.argv[1:])
sys.exit(r.returncode)
"""
def js_image_layer(name, binary, root = None, **kwargs):
"""Creates two tar files `:<name>/app.tar` and `:<name>/node_modules.tar`
Final directory tree will look like below
/{root of js_image_layer}/{package_name() if any}/{name of js_binary}.sh -> entrypoint
/{root of js_image_layer}/{package_name() if any}/{name of js_binary}.sh.runfiles -> runfiles directory (almost identical to one bazel lays out)
Args:
name: name for this target. Not reflected anywhere in the final tar.
binary: label to js_image target
root: Path where the js_binary will reside inside the final container image.
**kwargs: Passed to pkg_tar. See: https://github.com/bazelbuild/rules_pkg/blob/main/docs/0.7.0/reference.md#pkg_tar
"""
if root != None and not root.startswith("/"):
fail("root path must start with '/' but got '{root}', expected '/{root}'".format(root = root))
if kwargs.pop("package_dir", None):
fail("use 'root' attribute instead of 'package_dir'.")
entrypoint_name = "%s_build_tar_entrypoint.py" % name
write_file(
name = "%s.build_tar_entrypoint" % name,
out = entrypoint_name,
content = [BUILD_TAR],
tags = ["manual"],
)
py_binary(
name = "%s.build_tar" % name,
srcs = [entrypoint_name],
main = entrypoint_name,
deps = ["@rules_python//python/runfiles"],
data = [
"@rules_pkg//pkg/private/tar:build_tar",
],
tags = ["manual"],
)
common_kwargs = {
"tags": kwargs.pop("tags", None),
"visibility": kwargs.pop("visibility", None),
}
runfiles_kwargs = dict(
common_kwargs,
binary = binary,
root = root,
)
pkg_tar_kwargs = dict(
kwargs,
# Be careful with this option. Leave it as is if you don't know what you are doing
strip_prefix = kwargs.pop("strip_prefix", "."),
build_tar = "%s.build_tar" % name,
**common_kwargs
)
runfiles(
name = "%s/app/runfiles" % name,
exclude = "/node_modules/",
**runfiles_kwargs
)
pkg_tar(
name = "%s/app" % name,
srcs = ["%s/app/runfiles" % name],
**pkg_tar_kwargs
)
runfiles(
name = "%s/node_modules/runfiles" % name,
include = "/node_modules/",
**runfiles_kwargs
)
pkg_tar(
name = "%s/node_modules" % name,
srcs = ["%s/node_modules/runfiles" % name],
**pkg_tar_kwargs
)
native.filegroup(
name = name,
srcs = [
"%s/node_modules" % name,
"%s/app" % name,
],
**common_kwargs
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment