-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjs_image_layer.bzl
272 lines (224 loc) · 9.31 KB
/
js_image_layer.bzl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
"contains container helper functions for js_binary"
# https://github.com/aspect-build/rules_js/tree/main/e2e/js_image
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
)