Skip to content

Commit 667ac72

Browse files
committed
Support runfiles root_symlinks in images
Fixes symlink support and adds root_symlink support. Previously I was seeing that symlinks (regardless of whether the target file existed in runfiles.file), did not point to the correct location in the image. For that reason, we use `_final_file_path` instead of `layer_file_path`. This seems to work, but I am not sure if I'm missing a use-case here. The overall method we take here is to create real files where the [root_]symlink is declared if the target `File` is not seen in `file_map`. This PR also adds some extra test functionality to check that symlinks point at the expected location.
1 parent 6db7c12 commit 667ac72

File tree

3 files changed

+79
-12
lines changed

3 files changed

+79
-12
lines changed

lang/image.bzl

+24-5
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ def _default_symlinks(dep):
126126
else:
127127
return dep[DefaultInfo].default_runfiles.symlinks
128128

129+
def _default_root_symlinks(dep):
130+
if FilterLayerInfo in dep:
131+
return dep[FilterLayerInfo].runfiles.root_symlinks
132+
else:
133+
return dep[DefaultInfo].default_runfiles.root_symlinks
134+
129135
def _app_layer_impl(ctx, runfiles = None, emptyfiles = None):
130136
"""Appends a layer for a single dependency's runfiles.
131137
@@ -186,11 +192,24 @@ def _app_layer_impl(ctx, runfiles = None, emptyfiles = None):
186192
# app layer, we can already create symlinks to the runfiles path.
187193
if ctx.attr.binary:
188194
# Include any symlinks from the runfiles of the target for which we are synthesizing the layer.
189-
symlinks.update({
190-
(_reference_dir(ctx) + "/" + s.path): layer_file_path(ctx, s.target_file)
191-
for s in _default_symlinks(dep).to_list()
192-
if hasattr(s, "path") # "path" and "target_file" are exposed to starlark since bazel 0.21.0.
193-
})
195+
for s in _default_symlinks(dep).to_list():
196+
symlink_path = _reference_dir(ctx) + "/" + s.path
197+
if filepath(ctx, s.target_file) in file_map: # If the target is a real file, link to it
198+
symlinks.update({
199+
symlink_path: _final_file_path(ctx, s.target_file),
200+
})
201+
else:
202+
file_map[symlink_path] = s.target_file # Otherwise, synthesize the "symlink" as a real file
203+
204+
# Include root_symlinks
205+
for s in _default_root_symlinks(dep).to_list():
206+
symlink_path = _runfiles_dir(ctx) + "/" + s.path
207+
if filepath(ctx, s.target_file) in file_map:
208+
symlinks.update({
209+
symlink_path: _final_file_path(ctx, s.target_file),
210+
})
211+
else:
212+
file_map[symlink_path] = s.target_file
194213

195214
symlinks.update({
196215
_final_file_path(ctx, f): layer_file_path(ctx, f)

testdata/utils.bzl

+15-3
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,21 @@ def generate_deb(name, args = [], metadata_compression_type = "none"):
2828
)
2929

3030
def _rule_with_symlinks_impl(ctx):
31-
f = ctx.actions.declare_file("foo.txt")
32-
ctx.actions.write(f, "test content")
33-
runfiles = ctx.runfiles(files = [f], symlinks = {"foo-symlink.txt": f})
31+
foo = ctx.actions.declare_file("foo.txt")
32+
ctx.actions.write(foo, "test content")
33+
bar = ctx.actions.declare_file("bar.txt")
34+
ctx.actions.write(bar, "test content")
35+
baz = ctx.actions.declare_file("baz.txt")
36+
ctx.actions.write(baz, "test content")
37+
runfiles = ctx.runfiles(
38+
# bar and baz are specifically excluded from runfiles.files
39+
# We want to test that we don't create broken symlinks for [root_]symlinks that don't
40+
# have a corresponding file in runfiles.files.
41+
# We expect that for those [root_]symlinks, a real file exists at the symlink path.
42+
files = [foo],
43+
symlinks = {"foo-symlink.txt": foo, "baz/dir/baz-symlink-real.txt": baz},
44+
root_symlinks = {"foo-root-symlink.txt": foo, "bar-root-symlink-real.txt": bar},
45+
)
3446
return DefaultInfo(runfiles = runfiles)
3547

3648
rule_with_symlinks = rule(

tests/container/image_test.py

+40-4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
from io import BytesIO
16+
import contextlib
1617
import datetime
1718
import json
1819
import os
@@ -53,20 +54,33 @@ def assertTarballContains(self, tar, paths):
5354
self.maxDiff = None
5455
self.assertEqual(paths, tar.getnames())
5556

56-
def assertLayerNContains(self, img, n, paths):
57+
def assertTarballSymlink(self, tar, path, target):
58+
self.assertEqual(target, tar.getmember(path).linkname)
59+
60+
@contextlib.contextmanager
61+
def _tarball_layer_n(self, img, n):
5762
buf = BytesIO(img.blob(img.fs_layers()[n]))
58-
with tarfile.open(fileobj=buf, mode='r') as layer:
63+
yield tarfile.open(fileobj=buf, mode='r')
64+
65+
def assertLayerNContains(self, img, n, paths):
66+
with self._tarball_layer_n(img, n) as layer:
5967
self.assertTarballContains(layer, paths)
6068

69+
def assertLayerNSymlink(self, img, n, path, target):
70+
with self._tarball_layer_n(img, n) as layer:
71+
self.assertTarballSymlink(layer, path, target)
72+
6173
def assertNonZeroMtimesInTopLayer(self, img):
62-
buf = BytesIO(img.blob(img.fs_layers()[0]))
63-
with tarfile.open(fileobj=buf, mode='r') as layer:
74+
with self._tarball_layer_n(img, 0) as layer:
6475
for member in layer.getmembers():
6576
self.assertNotEqual(member.mtime, 0)
6677

6778
def assertTopLayerContains(self, img, paths):
6879
self.assertLayerNContains(img, 0, paths)
6980

81+
def assertTopLayerSymlink(self, img, path, target):
82+
self.assertLayerNSymlink(img, 0, path, target)
83+
7084
def assertConfigEqual(self, img, key, value):
7185
cfg = json.loads(img.config_file())
7286
self.assertEqual(value, cfg.get('config', {}).get(key))
@@ -562,6 +576,12 @@ def test_py_image_with_symlinks_in_data(self):
562576
'./app/testdata/py_image_with_symlinks_in_data.binary.runfiles/io_bazel_rules_docker/testdata/py_image.py',
563577
'./app/testdata/py_image_with_symlinks_in_data.binary.runfiles/io_bazel_rules_docker/testdata/py_image_with_symlinks_in_data.binary',
564578
'./app/testdata/py_image_with_symlinks_in_data.binary.runfiles/io_bazel_rules_docker/testdata/foo.txt',
579+
# baz-symlink-real.txt is a real file, since the File that it points to is not in runfiles.files
580+
'./app/testdata/py_image_with_symlinks_in_data.binary.runfiles/io_bazel_rules_docker/baz',
581+
'./app/testdata/py_image_with_symlinks_in_data.binary.runfiles/io_bazel_rules_docker/baz/dir',
582+
'./app/testdata/py_image_with_symlinks_in_data.binary.runfiles/io_bazel_rules_docker/baz/dir/baz-symlink-real.txt',
583+
# bar-root-symlink-real.txt is a real file, since the File that it points to is not in runfiles.files
584+
'./app/testdata/py_image_with_symlinks_in_data.binary.runfiles/bar-root-symlink-real.txt',
565585
'./app/testdata/py_image_with_symlinks_in_data.binary.runfiles/io_bazel_rules_docker/testdata/__init__.py',
566586
'./app/io_bazel_rules_docker',
567587
# TODO(mattmoor): The path normalization for symlinks should match
@@ -571,10 +591,18 @@ def test_py_image_with_symlinks_in_data(self):
571591
'/app/testdata/py_image_with_symlinks_in_data.binary.runfiles',
572592
'/app/testdata/py_image_with_symlinks_in_data.binary.runfiles/io_bazel_rules_docker',
573593
'/app/testdata/py_image_with_symlinks_in_data.binary.runfiles/io_bazel_rules_docker/foo-symlink.txt',
594+
'/app/testdata/py_image_with_symlinks_in_data.binary.runfiles/foo-root-symlink.txt',
574595
'/app/testdata/py_image_with_symlinks_in_data.binary',
575596
'/app/testdata/py_image_with_symlinks_in_data.binary.runfiles/io_bazel_rules_docker/external',
576597
])
577598

599+
# Test that root_symlinks that point to a file that is also in the runfiles tree is a symlink
600+
self.assertTopLayerSymlink(
601+
img,
602+
'/app/testdata/py_image_with_symlinks_in_data.binary.runfiles/foo-root-symlink.txt',
603+
'/app/testdata/py_image_with_symlinks_in_data.binary.runfiles/io_bazel_rules_docker/testdata/foo.txt',
604+
)
605+
578606
# Below that, we have a layer that generates symlinks for the library layer.
579607
self.assertLayerNContains(img, 1, [
580608
'.',
@@ -586,6 +614,14 @@ def test_py_image_with_symlinks_in_data(self):
586614
'/app/testdata/py_image_with_symlinks_in_data.binary.runfiles/io_bazel_rules_docker/testdata/py_image_library.py',
587615
])
588616

617+
# Validate that library symlink is actually a symlink to the right path
618+
self.assertLayerNSymlink(
619+
img,
620+
1,
621+
'/app/testdata/py_image_with_symlinks_in_data.binary.runfiles/io_bazel_rules_docker/testdata/py_image_library.py',
622+
'/app/io_bazel_rules_docker/testdata/py_image_library.py',
623+
)
624+
589625
# Check the library layer, which is two below our application layer.
590626
self.assertLayerNContains(img, 2, [
591627
'.',

0 commit comments

Comments
 (0)