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

Containerizing applications with runfiles #254

Closed
purkhusid opened this issue May 31, 2023 · 3 comments
Closed

Containerizing applications with runfiles #254

purkhusid opened this issue May 31, 2023 · 3 comments

Comments

@purkhusid
Copy link

Currently there is no obvious way to containerize applications that require the runfiles trees to be persisted in the way that Bazel lays them out. This is required for applications that use language specific runfiles libraries to locate runfiles.

Currently rules_oci relies on rules_pkg to create the layers for the container but rules_pkg does not have the capability to package the runfiles trees.

See bazelbuild/rules_pkg#579 for the issue on the rules_pkg side.

@aaliddell
Copy link
Contributor

There's a demo at https://github.com/aspect-build/bazel-examples/tree/main/oci_python_image that I've found useful for packaging up runfiles into an OCI image with these rules, due to that bug you linked.

Based on the code there, I shortened the various files into a single rule and macro combo that is close enough to a drop-in replacement to the old rules_docker py_image. It's probably broken in some subtle way, so beware, but this could be re-purposed for other languages that need runfiles bundling:

load("@rules_oci//oci:defs.bzl", "oci_image")
load("@rules_pkg//:pkg.bzl", "pkg_tar")
load("@rules_pkg//:providers.bzl", "PackageFilegroupInfo", "PackageFilesInfo", "PackageSymlinkInfo")
load("@rules_python//python:defs.bzl", "PyInfo")

def _dest_path(ctx, root, file):
    """Get the required output path under the root, such that runfiles structure is preserved."""
    return root + "/" + (
        (ctx.workspace_name + "/" + file.short_path) if not file.short_path.startswith("../") else file.short_path[3:]
    )

def _py_oci_image_layer_impl(ctx):
    """
    Build a Python OCI image tar layer provider containing the binary, its runfiles (and therefore
    also the bundled Python executable itself).

    Derived from:
      - https://github.com/aspect-build/bazel-examples/blob/d1dd2310df5ed06343eb37cf699ced5551ac476f/oci_python_image/py_image_layer.bzl
      - https://github.com/aspect-build/bazel-examples/blob/d1dd2310df5ed06343eb37cf699ced5551ac476f/oci_python_image/workaround_rules_pkg_153/runfiles.bzl

    """

    # Prohibit relative roots
    if not ctx.attr.root.startswith("/"):
        fail("root path must start with '/' but got '{root}', expected '/{root}'".format(root = ctx.attr.root))

    # Get root directory for all runfiles
    default_info = ctx.attr.binary[DefaultInfo]
    runfiles_root = "/".join([
        ctx.attr.root,
        default_info.files_to_run.runfiles_manifest.short_path.rpartition("/")[0],
    ])

    # Map files into the tar and also search for the Python binary
    mapped_files = {}
    python_binary_file = None
    for file in default_info.default_runfiles.files.to_list():
        mapped_files[_dest_path(ctx, runfiles_root, file)] = file

        if file.path.endswith("/bin/python") or file.path.endswith("/bin/python3"):
            # TODO: do this a more sensible way
            python_binary_file = file

    # Map symlinks
    symlinks = []
    for symlink in default_info.default_runfiles.symlinks.to_list():
        symlinks.append([PackageSymlinkInfo(
            target = _dest_path(ctx, runfiles_root, symlink.target_file),
            destination = "/".join([runfiles_root, ctx.workspace_name, symlink.path]),
            attributes = {"mode": "0777"},
        ), ctx.label])

    for symlink in default_info.default_runfiles.root_symlinks.to_list():
        symlinks.append([PackageSymlinkInfo(
            target = _dest_path(ctx, runfiles_root, symlink.target_file),
            destination = "/".join([runfiles_root, symlink.path]),
            attributes = {"mode": "0777"},
        ), ctx.label])

    # Put entrypoint symlink to executable at deterministic location, such that we can reference it
    # in oci_image within macro
    symlinks.append((PackageSymlinkInfo(
        target = _dest_path(ctx, runfiles_root, default_info.files_to_run.executable),
        destination = ctx.attr.root + "/" + "entrypoint",
        attributes = {"mode": "0777"},
    ), ctx.label))

    # If we found a Python binary, symlink it into PATH such that it can be used by stub script
    # generated by py_binary.
    # This allows us to use py_oci_image with base images that contain no other Python interpreter
    # binary. This is also a QoL improvement such that a Python interpreter can quickly be started
    # in a container with `docker exec ... python`
    if python_binary_file != None:
        symlinks.append((PackageSymlinkInfo(
            target = _dest_path(ctx, runfiles_root, python_binary_file),
            destination = "/usr/local/bin/python",
            attributes = {"mode": "0777"},
        ), ctx.label))
        symlinks.append((PackageSymlinkInfo(
            target = _dest_path(ctx, runfiles_root, python_binary_file),
            destination = "/usr/local/bin/python3",
            attributes = {"mode": "0777"},
        ), ctx.label))

    # Build providers of tar structure
    return [
        PackageFilegroupInfo(
            pkg_dirs = [],
            pkg_files = [
                [PackageFilesInfo(
                    dest_src_map = mapped_files,
                ), ctx.label],
            ],
            pkg_symlinks = symlinks,
        ),
        DefaultInfo(files = default_info.default_runfiles.files),
    ]

_py_oci_image_layer = rule(
    implementation = _py_oci_image_layer_impl,
    attrs = {
        "binary": attr.label(
            mandatory = True,
            providers = [PyInfo],  # To ensure we only receive a py_binary or equivalent
        ),
        "root": attr.string(
            mandatory = True,
        ),
    },
)

def py_oci_image(name, binary, root = "/opt", **kwargs):
    """
    Build an oci_image from a py_binary target.

    Args:
      - binary: The py_binary target label.
      - root: The root directory to use as a base for the files within the container.
      - **kwargs: All other args are forwarded to oci_image.
    """

    # Build a tar layer containing the py_binary runfiles
    _py_oci_image_layer(
        name = "_{}/runfiles".format(name),
        binary = binary,
        root = root,
        tags = kwargs.get("tags", None),
    )

    pkg_tar(
        name = "_{}/layer".format(name),
        srcs = [
            "_{}/runfiles".format(name),
        ],
        tags = kwargs.get("tags", None),
    )

    # Build image with sensible defaults
    env = {
        "LANG": "C.UTF-8",  # http://bugs.python.org/issue19846
    }
    env.update(kwargs.get("env", {}))
    oci_image(
        name = name,
        tars = ["_{}/layer".format(name)],
        entrypoint = kwargs.get("entrypoint", [root + "/entrypoint"]),
        env = env,
        **{k: v for k, v in kwargs.items() if k not in ["env", "entrypoint"]}
    )

@thesayyn
Copy link
Collaborator

this is fixed both in rules_pkg and https://github.com/aspect-build/bazel-lib/blob/main/docs/tar.md. Closing as there's no action to be taken here.

@yuan-attrove
Copy link

For future readers:

The rules_pkg fix needs https://github.com/bazelbuild/rules_pkg/pull/754 in the release. As the time of writing rules_pkg was at 0.9.1 released on May 2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants