Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support dangling symlinks created with ctx.actions.declare_symlink #112

Merged
merged 2 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ build --color=yes
build --cxxopt=-std=c++20
build --incompatible_strict_action_env
build --keep_going
common --experimental_allow_unresolved_symlinks # Only required for Bazel 5
common --noenable_bzlmod # This line is automatically removed in CI for Bazel 5
test --announce_rc
test --keep_going
Expand Down
10 changes: 9 additions & 1 deletion appimage/private/tool/mkappimage.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,15 @@ def populate_appdir(appdir: Path, params: AppDirParams) -> None:

linkpairs: List[Tuple[Path, Path]] = []
for file in manifest_data["files"]:
src = Path(file["src"]).resolve()
src = Path(file["src"])
src_actual = src.resolve()
if not src_actual.exists():
# src is declared as an input, but is a dangling symlink. Let's keep it as is.
pass
else:
# It's ok to resolve the file here as it's supposed to be an actual input file.
# Runfile symlinks are handled below.
src = src_actual
dst = Path(appdir / file["dst"]).resolve()
linkpairs.extend(copy_and_link(src, dst))
fix_linkpair(linkpairs)
Expand Down
26 changes: 19 additions & 7 deletions tests/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ load("@rules_appimage_py_deps//:requirements.bzl", "requirement")
load("@rules_cc//cc:defs.bzl", "cc_binary")
load("@rules_python//python:defs.bzl", "py_binary", "py_test")
load("//appimage:appimage.bzl", "appimage", "appimage_test")
load(":testrules.bzl", "rules_appimage_test_rule")
load(":testrules.bzl", "declared_symlink", "runfiles_symlink")

sh_binary(
name = "test_sh",
Expand Down Expand Up @@ -61,8 +61,10 @@ py_binary(
data = [
"data.txt",
"dir", # this is a relative directory, not a target label
":absolutely_invalid_link",
":external_bin.appimage",
":symlink_and_emptyfile",
":path/to/the/runfiles_symlink",
":relatively_invalid_link",
],
env = {"MY_BINARY_ENV": "propagated only in Bazel 7+"},
main = "test.py",
Expand Down Expand Up @@ -117,11 +119,6 @@ appimage(
},
)

rules_appimage_test_rule(
name = "symlink_and_emptyfile",
symlink = "data.txt",
)

sh_test(
name = "runfiles_test_sh",
size = "small",
Expand All @@ -137,3 +134,18 @@ appimage_test(
tags = ["requires-fakeroot"],
target_compatible_with = ["@platforms//os:linux"],
)

declared_symlink(
name = "relatively_invalid_link",
target = "././.././idonotexist",
)

declared_symlink(
name = "absolutely_invalid_link",
target = "/💣",
)

runfiles_symlink(
name = "path/to/the/runfiles_symlink",
target = ":data.txt",
)
1 change: 0 additions & 1 deletion tests/dir/subdir/invalid_link

This file was deleted.

23 changes: 17 additions & 6 deletions tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,29 @@ def test_symlinks() -> None:
assert abs_link.is_symlink()
assert os.readlink(abs_link) == "/bin/sh"

invalid_link = Path("tests/dir/subdir/invalid_link")
assert invalid_link.is_symlink()
assert os.readlink(invalid_link) == "invalid/target"
assert not invalid_link.is_file()

dir_link = Path("tests/dir/subdir/dir_link")
assert dir_link.is_symlink()
assert os.readlink(dir_link) == "dir"
assert dir_link.resolve().is_dir()


def test_declared_symlinks() -> None:
"""Test that symlinks declared via `ctx.actions.declare_symlink(...)` are handled correctly."""
invalid_link = Path("tests/relatively_invalid_link")
assert invalid_link.is_symlink()
target = os.readlink(invalid_link)
assert target == "../idonotexist", target
assert not invalid_link.is_file()

invalid_link = Path("tests/absolutely_invalid_link")
assert invalid_link.is_symlink()
target = os.readlink(invalid_link)
assert target == "/💣", target
assert not invalid_link.is_file()


def test_runfiles_symlinks() -> None:
"""Test that runfiles symlinks are handled correctly."""
"""Test that symlinks coming from `ctx.runfiles(symlinks = {...})` are handled correctly."""
runfiles_symlink = Path("path/to/the/runfiles_symlink")
assert runfiles_symlink.is_symlink()
assert os.readlink(runfiles_symlink) == "../../../tests/data.txt"
Expand Down Expand Up @@ -112,6 +122,7 @@ def greeter() -> None:
test_appimage_datadep()
test_external_bin()
test_symlinks()
test_declared_symlinks()
test_runfiles_symlinks()
test_binary_env()
greeter()
7 changes: 5 additions & 2 deletions tests/test_appimage.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,11 @@ def test_symlinks() -> None:
extracted_path = Path(_TMPDIR) / "squashfs-root"
symlinks_present = False
for file in extracted_path.glob("**/*"):
if file.is_symlink() and file.name != "invalid_link":
assert file.resolve().exists(), f"{file} resolves to {file.resolve()}, which does not exist!"
if file.is_symlink():
if file.name in {"relatively_invalid_link", "absolutely_invalid_link"}:
assert not file.resolve().exists(), f"{file} resolves to {file.resolve()}, which should not exist!"
else:
assert file.resolve().exists(), f"{file} resolves to {file.resolve()}, which does not exist!"
symlinks_present = True
assert symlinks_present

Expand Down
33 changes: 27 additions & 6 deletions tests/testrules.bzl
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
"""Bazel rule that creates a runfile symlink."""
"""Bazel rules that creates specific versions of symlinks."""

def _rules_appimage_test_rule_impl(ctx):
runfiles = ctx.runfiles(symlinks = {"path/to/the/runfiles_symlink": ctx.files.symlink[0]})
def _runfiles_symlink_impl(ctx):
symlinks_dict = {ctx.attr.name: ctx.file.target}
runfiles = ctx.runfiles(symlinks = symlinks_dict)
return [DefaultInfo(runfiles = runfiles)]

rules_appimage_test_rule = rule(
implementation = _rules_appimage_test_rule_impl,
runfiles_symlink = rule(
implementation = _runfiles_symlink_impl,
attrs = {
"symlink": attr.label(mandatory = True, allow_single_file = True),
"target": attr.label(mandatory = True, allow_single_file = True),
},
)

def _declared_symlink_impl(ctx):
declared_symlink = ctx.actions.declare_symlink(ctx.attr.name)
ctx.actions.run_shell(
outputs = [declared_symlink],
command = " ".join([
"ln -s",
repr(ctx.attr.target),
repr(declared_symlink.path),
]),
)
runfiles = ctx.runfiles(files = [declared_symlink])
return [DefaultInfo(runfiles = runfiles)]

declared_symlink = rule(
implementation = _declared_symlink_impl,
attrs = {
"target": attr.string(mandatory = True),
},
)