diff --git a/pkg/WORKSPACE b/pkg/WORKSPACE
index 6075c907..06d13f15 100644
--- a/pkg/WORKSPACE
+++ b/pkg/WORKSPACE
@@ -46,7 +46,13 @@ load("@bazel_stardoc//:setup.bzl", "stardoc_repositories")
stardoc_repositories()
+# TODO(nacl): remove this when the "new" framework is ready.
local_repository(
name = "experimental_test_external_repo",
path = "experimental/tests/external_repo",
)
+
+local_repository(
+ name = "mappings_test_external_repo",
+ path = "tests/mappings/external_repo",
+)
diff --git a/pkg/docs/reference.md b/pkg/docs/reference.md
index a9a9fe20..1a7aca9b 100644
--- a/pkg/docs/reference.md
+++ b/pkg/docs/reference.md
@@ -18,17 +18,54 @@ These attributes are used in several rules within this module.
**ATTRIBUTES**
-| Name | Description | Type | Mandatory | Default |
-| :------------- | :------------- | :-------------: | :-------------: | :------------- |
-| out | Name of the output file. This file will always be created and used to access the package content. If `package_file_name` is also specified, `out` will be a symlink. | String | required | |
-| package_file_name | The name of the file which will contain the package. The name may contain variables in the form `{var}`. The values for substitution are specified through `package_variables`.| String | optional | package type specific |
-| package_variables | A target that provides `PackageVariablesInfo` to substitute into `package_file_name`.| Label | optional | None |
+| Name | Description | Type | Mandatory | Default |
+| :------------- | :------------- | :-------------: | :-------------: | :------------- |
+| out | Name of the output file. This file will always be created and used to access the package content. If `package_file_name` is also specified, `out` will be a symlink. | String | required | |
+| package_file_name | The name of the file which will contain the package. The name may contain variables in the form `{var}`. The values for substitution are specified through `package_variables`. | String | optional | package type specific |
+| package_variables | A target that provides `PackageVariablesInfo` to substitute into `package_file_name`. | Label | optional | None |
+| attributes | Attributes to set on entities created within packages. Not to be confused with bazel rule attributes. See 'Mapping "Attributes"' below | dict of String -> String List | optional | Varies. See 'Mapping "Attributes"' below |
See
[examples/naming_package_files](https://github.com/bazelbuild/rules_pkg/tree/main/examples/naming_package_files)
for examples of how `out`, `package_file_name`, and `package_variables`
interact.
+
+### Mapping "Attributes"
+
+The "attributes" attribute specifies properties of package contents as used in
+rules such as `pkg_files`, and `pkg_mkdirs`. These allow fine-grained control
+of the contents of your package. For example:
+
+```python
+attributes = {
+ "unix": ("0644", "myapp", "myapp"),
+ "my_custom_attribute": ("some custom value",),
+}
+```
+
+Known attributes include the following:
+
+#### `unix`
+
+Specifies basic UNIX-style filesystem permissions. For example:
+
+```python
+{"unix": ("0755", "root", "root")}
+```
+
+This is a three-element tuple, providing the filesystem mode (as an octal
+string, like above), user, and group.
+
+`"-"` may be provided in place of any of the values in the tuple. Packaging
+rules will set these to a default, which will vary based on the underlying
+package builder and is otherwise unspecified. Relying upon values set by it is
+not recommended.
+
+Package mapping rules typically set this to `("-", "-", "-")`.
+
+---
+
## pkg_tar
diff --git a/pkg/mappings.bzl b/pkg/mappings.bzl
new file mode 100644
index 00000000..613920cd
--- /dev/null
+++ b/pkg/mappings.bzl
@@ -0,0 +1,387 @@
+# Copyright 2021 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Package creation helper mapping rules.
+
+This module declares Provider interfaces and rules for specifying the contents
+of packages in a package-type-agnostic way. The main rules supported here are
+the following:
+
+- `pkg_files` describes destinations for rule outputs
+- `pkg_mkdirs` describes directory structures
+
+Rules that actually make use of the outputs of the above rules are not specified
+here. TODO(nacl): implement one.
+"""
+
+load("@bazel_skylib//lib:paths.bzl", "paths")
+load("//:providers.bzl", "PackageDirsInfo", "PackageFilesInfo")
+
+_PKGFILEGROUP_STRIP_ALL = "."
+
+def _sp_files_only():
+ return _PKGFILEGROUP_STRIP_ALL
+
+def _sp_from_pkg(path = ""):
+ if path.startswith("/"):
+ return path[1:]
+ else:
+ return path
+
+def _sp_from_root(path = ""):
+ if path.startswith("/"):
+ return path
+ else:
+ return "/" + path
+
+strip_prefix = struct(
+ _doc = """pkg_files `strip_prefix` helper. Instructs `pkg_files` what to do with directory prefixes of files.
+
+ Each member is a function that equates to:
+
+ - `files_only()`: strip all directory components from all paths
+
+ - `from_pkg(path)`: strip all directory components up to the current
+ package, plus what's in `path`, if provided.
+
+ - `from_root(path)`: strip beginning from the file's WORKSPACE root (even if
+ it is in an external workspace) plus what's in `path`, if provided.
+
+ Prefix stripping is applied to each `src` in a `pkg_files` rule
+ independently.
+ """,
+ files_only = _sp_files_only,
+ from_pkg = _sp_from_pkg,
+ from_root = _sp_from_root,
+)
+
+####
+# Internal helpers
+####
+
+def _validate_attributes(attributes):
+ # TODO(nacl): this needs to be rethought. There could, for example, be
+ # attributes for additional packaging types that live outside of rules_pkg.
+
+ # We could do more here, perhaps
+ if "unix" in attributes.keys():
+ if len(attributes["unix"]) != 3:
+ fail("'unix' attributes key must have three child values")
+
+def _do_strip_prefix(path, to_strip, src_file):
+ if to_strip == "":
+ # We were asked to strip nothing, which is valid. Just return the
+ # original path.
+ return path
+
+ path_norm = paths.normalize(path)
+ to_strip_norm = paths.normalize(to_strip) + "/"
+
+ if path_norm.startswith(to_strip_norm):
+ return path_norm[len(to_strip_norm):]
+ else:
+ # Avoid user surprise by failing if prefix stripping doesn't work as
+ # expected.
+ #
+ # We already leave enough breadcrumbs, so if File.owner() returns None,
+ # this won't be a problem.
+ fail("Could not strip prefix '{}' from file {} ({})".format(to_strip, str(src_file), str(src_file.owner)))
+
+# The below routines make use of some path checking magic that may difficult to
+# understand out of the box. This following table may be helpful to demonstrate
+# how some of these members may look like in real-world usage:
+#
+# Note: "F" is "File", "FO": is "File.owner".
+
+# | File type | Repo | `F.path` | `F.root.path` | `F.short_path` | `FO.workspace_name` | `FO.workspace_root` |
+# |-----------|----------|----------------------------------------------------------|------------------------------|-------------------------|---------------------|---------------------|
+# | Source | Local | `dirA/fooA` | | `dirA/fooA` | | |
+# | Generated | Local | `bazel-out/k8-fastbuild/bin/dirA/gen.out` | `bazel-out/k8-fastbuild/bin` | `dirA/gen.out` | | |
+# | Source | External | `external/repo2/dirA/fooA` | | `../repo2/dirA/fooA` | `repo2` | `external/repo2` |
+# | Generated | External | `bazel-out/k8-fastbuild/bin/external/repo2/dirA/gen.out` | `bazel-out/k8-fastbuild/bin` | `../repo2/dirA/gen.out` | `repo2` | `external/repo2` |
+
+def _owner(file):
+ # File.owner allows us to find a label associated with a file. While highly
+ # convenient, it may return None in certain circumstances, which seem to be
+ # primarily when bazel doesn't know about the files in question.
+ #
+ # Given that a sizeable amount of the code we have here relies on it, we
+ # should fail() when we encounter this if only to make the rare error more
+ # clear.
+ #
+ # File.owner returns a Label structure
+ if file.owner == None:
+ fail("File {} ({}) has no owner attribute; cannot continue".format(file, file.path))
+ else:
+ return file.owner
+
+def _relative_workspace_root(label):
+ # Helper function that returns the workspace root relative to the bazel File
+ # "short_path", so we can exclude external workspace names in the common
+ # path stripping logic.
+ #
+ # This currently is "../$LABEL_WORKSPACE_ROOT" if the label has a specific
+ # workspace name specified, else it's just an empty string.
+ #
+ # TODO(nacl): Make this not a hack
+ return paths.join("..", label.workspace_name) if label.workspace_name else ""
+
+def _path_relative_to_package(file):
+ # Helper function that returns a path to a file relative to its package.
+ owner = _owner(file)
+ return paths.relativize(
+ file.short_path,
+ paths.join(_relative_workspace_root(owner), owner.package),
+ )
+
+def _path_relative_to_repo_root(file):
+ # Helper function that returns a path to a file relative to its workspace root.
+ return paths.relativize(
+ file.short_path,
+ _relative_workspace_root(_owner(file)),
+ )
+
+def _pkg_files_impl(ctx):
+ # The input sources are already known. Let's calculate the destinations...
+
+ # Exclude excludes
+ srcs = [f for f in ctx.files.srcs if f not in ctx.files.excludes]
+
+ if ctx.attr.strip_prefix == _PKGFILEGROUP_STRIP_ALL:
+ src_dest_paths_map = {src: paths.join(ctx.attr.prefix, src.basename) for src in srcs}
+ elif ctx.attr.strip_prefix.startswith("/"):
+ # Relative to workspace/repository root
+ src_dest_paths_map = {src: paths.join(
+ ctx.attr.prefix,
+ _do_strip_prefix(
+ _path_relative_to_repo_root(src),
+ ctx.attr.strip_prefix[1:],
+ src,
+ ),
+ ) for src in srcs}
+ else:
+ # Relative to package
+ src_dest_paths_map = {src: paths.join(
+ ctx.attr.prefix,
+ _do_strip_prefix(
+ _path_relative_to_package(src),
+ ctx.attr.strip_prefix,
+ src,
+ ),
+ ) for src in srcs}
+
+ _validate_attributes(ctx.attr.attributes)
+
+ # Do file renaming
+ for rename_src, rename_dest in ctx.attr.renames.items():
+ # rename_src.files is a depset
+ rename_src_files = rename_src.files.to_list()
+
+ # Need to do a length check before proceeding. We cannot rename
+ # multiple files simultaneously.
+ if len(rename_src_files) != 1:
+ fail(
+ "Target {} expands to multiple files, should only refer to one".format(rename_src),
+ "renames",
+ )
+
+ src_file = rename_src_files[0]
+ if src_file not in src_dest_paths_map:
+ fail(
+ "File remapping from {0} to {1} is invalid: {0} is not provided to this rule or was excluded".format(rename_src, rename_dest),
+ "renames",
+ )
+ src_dest_paths_map[src_file] = paths.join(ctx.attr.prefix, rename_dest)
+
+ # At this point, we have a fully valid src -> dest mapping in src_dest_paths_map.
+ #
+ # Construct the inverse of this mapping to pass to the output providers, and
+ # check for duplicated destinations.
+ dest_src_map = {}
+ for src, dest in src_dest_paths_map.items():
+ if dest in dest_src_map:
+ fail("After renames, multiple sources (at least {0}, {1}) map to the same destination. Consider adjusting strip_prefix and/or renames".format(dest_src_map[dest].path, src.path))
+ dest_src_map[dest] = src
+
+ return [
+ PackageFilesInfo(
+ dest_src_map = dest_src_map,
+ attributes = ctx.attr.attributes,
+ ),
+ DefaultInfo(
+ # Simple passthrough
+ files = depset(dest_src_map.values()),
+ ),
+ ]
+
+pkg_files = rule(
+ doc = """General-purpose package target-to-destination mapping rule.
+
+ This rule provides a specification for the locations and attributes of
+ targets when they are packaged. No outputs are created other than Providers
+ that are intended to be consumed by other packaging rules, such as
+ `pkg_rpm`.
+
+ Labels associated with these rules are not passed directly to packaging
+ rules, instead, they should be passed to an associated `pkg_filegroup` rule,
+ which in turn should be passed to packaging rules.
+
+ Consumers of `pkg_files`s will, where possible, create the necessary
+ directory structure for your files so you do not have to unless you have
+ special requirements. Consult `pkg_mkdirs` for more details.
+ """,
+ implementation = _pkg_files_impl,
+ # @unsorted-dict-items
+ attrs = {
+ "srcs": attr.label_list(
+ doc = """Files/Labels to include in the outputs of these rules""",
+ mandatory = True,
+ allow_files = True,
+ ),
+ "attributes": attr.string_list_dict(
+ doc = """Attributes to set on packaged files.
+
+ Consult the "Mapping Attributes" documentation in the rules_pkg
+ reference for more details.
+ """,
+ default = {"unix": ["-", "-", "-"]},
+ ),
+ "prefix": attr.string(
+ doc = """Installation prefix.
+
+ This may be an arbitrary string, but it should be understandable by
+ the packaging system you are using to have the desired outcome. For
+ example, RPM macros like `%{_libdir}` may work correctly in paths
+ for RPM packages, not, say, Debian packages.
+
+ If any part of the directory structure of the computed destination
+ of a file provided to `pkg_filegroup` or any similar rule does not
+ already exist within a package, the package builder will create it
+ for you with a reasonable set of default permissions (typically
+ `0755 root.root`).
+
+ It is possible to establish directory structures with arbitrary
+ permissions using `pkg_mkdirs`.
+ """,
+ default = "",
+ ),
+ "strip_prefix": attr.string(
+ doc = """What prefix of a file's path to discard prior to installation.
+
+ This specifies what prefix of an incoming file's path should not be
+ included in the output package at after being appended to the
+ install prefix (the `prefix` attribute). Note that this is only
+ applied to full directory names, see `strip_prefix` for more
+ details.
+
+ Use the `strip_prefix` struct to define this attribute. If this
+ attribute is not specified, all directories will be stripped from
+ all files prior to being included in packages
+ (`strip_prefix.files_only()`).
+
+ If prefix stripping fails on any file provided in `srcs`, the build
+ will fail.
+
+ Note that this only functions on paths that are known at analysis
+ time. Specifically, this will not consider directories within
+ TreeArtifacts (directory outputs), or the directories themselves.
+ See also #269.
+ """,
+ default = strip_prefix.files_only(),
+ ),
+ "excludes": attr.label_list(
+ doc = """List of files or labels to exclude from the inputs to this rule.
+
+ Mostly useful for removing files from generated outputs or
+ preexisting `filegroup`s.
+ """,
+ default = [],
+ allow_files = True,
+ ),
+ "renames": attr.label_keyed_string_dict(
+ doc = """Destination override map.
+
+ This attribute allows the user to override destinations of files in
+ `pkg_file`s relative to the `prefix` attribute. Keys to the
+ dict are source files/labels, values are destinations relative to
+ the `prefix`, ignoring whatever value was provided for
+ `strip_prefix`.
+
+ The following keys are rejected:
+
+ - Any label that expands to more than one file (mappings must be
+ one-to-one).
+
+ - Any label or file that was either not provided or explicitly
+ `exclude`d.
+ """,
+ default = {},
+ allow_files = True,
+ ),
+ },
+ provides = [PackageFilesInfo],
+)
+
+def _pkg_mkdirs_impl(ctx):
+ _validate_attributes(ctx.attr.attributes)
+
+ return [
+ PackageDirsInfo(
+ dirs = ctx.attr.dirs,
+ attributes = ctx.attr.attributes,
+ ),
+ ]
+
+pkg_mkdirs = rule(
+ doc = """Defines creation and ownership of directories in packages
+
+ Use this if:
+
+ 1) You need to create an empty directory in your package.
+
+ 2) Your package needs to explicitly own a directory, even if it already owns
+ files in those directories.
+
+ 3) You need nonstandard permissions (typically, not "0755") on a directory
+ in your package.
+
+ For some package management systems (e.g. RPM), directory ownership (2) may
+ imply additional semantics. Consult your package manager's and target
+ distribution's documentation for more details.
+ """,
+ implementation = _pkg_mkdirs_impl,
+ # @unsorted-dict-items
+ attrs = {
+ "dirs": attr.string_list(
+ doc = """Directory names to make within the package
+
+ If any part of the requested directory structure does not already
+ exist within a package, the package builder will create it for you
+ with a reasonable set of default permissions (typically `0755
+ root.root`).
+
+ """,
+ mandatory = True,
+ ),
+ "attributes": attr.string_list_dict(
+ doc = """Attributes to set on packaged directories.
+
+ Consult the "Mapping Attributes" documentation in the rules_pkg
+ reference for more details.
+ """,
+ default = {"unix": ["-", "-", "-"]},
+ ),
+ },
+ provides = [PackageDirsInfo],
+)
diff --git a/pkg/tests/BUILD b/pkg/tests/BUILD
index a7da3cb5..f94f3ad2 100644
--- a/pkg/tests/BUILD
+++ b/pkg/tests/BUILD
@@ -20,6 +20,8 @@ load("@rules_python//python:defs.bzl", "py_test")
load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
load(":my_package_name.bzl", "my_package_naming")
+exports_files(glob(["testdata/**"]))
+
filegroup(
name = "archive_testdata",
srcs = glob(["testdata/**"]) + [
diff --git a/pkg/tests/mappings/BUILD b/pkg/tests/mappings/BUILD
new file mode 100644
index 00000000..c3c72ce7
--- /dev/null
+++ b/pkg/tests/mappings/BUILD
@@ -0,0 +1,19 @@
+# Copyright 2021 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+load(":mappings_test.bzl", "mappings_analysis_tests", "mappings_unit_tests")
+
+mappings_analysis_tests()
+
+mappings_unit_tests()
diff --git a/pkg/tests/mappings/external_repo/WORKSPACE b/pkg/tests/mappings/external_repo/WORKSPACE
new file mode 100644
index 00000000..ff40799d
--- /dev/null
+++ b/pkg/tests/mappings/external_repo/WORKSPACE
@@ -0,0 +1,15 @@
+# Copyright 2021 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+workspace(name = "test_external_project")
diff --git a/pkg/tests/mappings/external_repo/pkg/BUILD b/pkg/tests/mappings/external_repo/pkg/BUILD
new file mode 100644
index 00000000..fab0a4ba
--- /dev/null
+++ b/pkg/tests/mappings/external_repo/pkg/BUILD
@@ -0,0 +1,40 @@
+# Copyright 2021 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+load("@//:mappings.bzl", "pkg_files", "strip_prefix")
+load("test.bzl", "test_referencing_remote_file")
+
+package(default_visibility = ["//visibility:public"])
+
+exports_files(
+ glob(["**"]),
+)
+
+sh_binary(
+ name = "dir/script",
+ srcs = ["dir/extproj.sh"],
+ visibility = ["//visibility:public"],
+)
+
+pkg_files(
+ name = "extproj_script_pf",
+ srcs = ["dir/extproj.sh"],
+ prefix = "usr/bin",
+ strip_prefix = strip_prefix.from_pkg(),
+ tags = ["manual"],
+)
+
+test_referencing_remote_file(
+ name = "pf_local_file_in_extrepo",
+)
diff --git a/pkg/tests/mappings/external_repo/pkg/dir/extproj.sh b/pkg/tests/mappings/external_repo/pkg/dir/extproj.sh
new file mode 100644
index 00000000..3d6f2af0
--- /dev/null
+++ b/pkg/tests/mappings/external_repo/pkg/dir/extproj.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# Copyright 2021 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+echo "External project script!"
diff --git a/pkg/tests/mappings/external_repo/pkg/test.bzl b/pkg/tests/mappings/external_repo/pkg/test.bzl
new file mode 100644
index 00000000..4951cd71
--- /dev/null
+++ b/pkg/tests/mappings/external_repo/pkg/test.bzl
@@ -0,0 +1,68 @@
+# Copyright 2021 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tests for file mapping routines in pkg/mappings.bzl.
+
+Test implementation copied from pkg/mappings.bzl
+
+"""
+
+load("@//:mappings.bzl", "pkg_files", "strip_prefix")
+load("@//:providers.bzl", "PackageFilesInfo")
+load("@bazel_skylib//lib:new_sets.bzl", "sets")
+load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts")
+
+#### BEGIN copied code
+
+def _pkg_files_contents_test_impl(ctx):
+ env = analysistest.begin(ctx)
+ target_under_test = analysistest.target_under_test(env)
+
+ expected_dests = sets.make(ctx.attr.expected_dests)
+ actual_dests = sets.make(target_under_test[PackageFilesInfo].dest_src_map.keys())
+
+ asserts.new_set_equals(env, expected_dests, actual_dests, "pkg_files dests do not match expectations")
+
+ return analysistest.end(env)
+
+pkg_files_contents_test = analysistest.make(
+ _pkg_files_contents_test_impl,
+ attrs = {
+ # Other attributes can be tested here, but the most important one is the
+ # destinations.
+ "expected_dests": attr.string_list(
+ mandatory = True,
+ ),
+ # attrs are always passed through unchanged (and maybe rejected)
+ },
+)
+
+#### END copied code
+
+# Called from the rules_pkg tests
+def test_referencing_remote_file(name):
+ pkg_files(
+ name = "{}_g".format(name),
+ prefix = "usr/share",
+ srcs = ["@//tests:loremipsum_txt"],
+ # The prefix in rules_pkg. Why yes, this is knotty
+ strip_prefix = strip_prefix.from_root("tests"),
+ tags = ["manual"],
+ )
+
+ pkg_files_contents_test(
+ name = name,
+ target_under_test = ":{}_g".format(name),
+ expected_dests = ["usr/share/testdata/loremipsum.txt"],
+ )
diff --git a/pkg/tests/mappings/mappings_test.bzl b/pkg/tests/mappings/mappings_test.bzl
new file mode 100644
index 00000000..0a3421b5
--- /dev/null
+++ b/pkg/tests/mappings/mappings_test.bzl
@@ -0,0 +1,587 @@
+# Copyright 2021 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tests for file mapping routines in pkg/mappings.bzl"""
+
+load("@bazel_skylib//lib:new_sets.bzl", "sets")
+load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts", "unittest")
+load(
+ "//:mappings.bzl",
+ "pkg_files",
+ "pkg_mkdirs",
+ "strip_prefix",
+)
+load("//:providers.bzl", "PackageDirsInfo", "PackageFilesInfo")
+
+def _pkg_files_contents_test_impl(ctx):
+ env = analysistest.begin(ctx)
+ target_under_test = analysistest.target_under_test(env)
+
+ expected_dests = sets.make(ctx.attr.expected_dests)
+ actual_dests = sets.make(target_under_test[PackageFilesInfo].dest_src_map.keys())
+
+ asserts.new_set_equals(env, expected_dests, actual_dests, "pkg_files dests do not match expectations")
+
+ return analysistest.end(env)
+
+pkg_files_contents_test = analysistest.make(
+ _pkg_files_contents_test_impl,
+ attrs = {
+ # Other attributes can be tested here, but the most important one is the
+ # destinations.
+ "expected_dests": attr.string_list(
+ mandatory = True,
+ ),
+ # attrs are always passed through unchanged (and maybe rejected)
+ },
+)
+
+# Generic negative test boilerplate
+def _generic_neg_test_impl(ctx):
+ env = analysistest.begin(ctx)
+
+ asserts.expect_failure(env, ctx.attr.reason)
+
+ return analysistest.end(env)
+
+generic_neg_test = analysistest.make(
+ _generic_neg_test_impl,
+ attrs = {
+ "reason": attr.string(
+ default = "",
+ ),
+ },
+ expect_failure = True,
+)
+
+def _test_pkg_files_contents():
+ # Test stripping when no arguments are provided (same as strip_prefix.files_only())
+ pkg_files(
+ name = "pf_no_strip_prefix_g",
+ srcs = ["testdata/hello.txt"],
+ tags = ["manual"],
+ )
+
+ pkg_files_contents_test(
+ name = "pf_no_strip_prefix",
+ target_under_test = ":pf_no_strip_prefix_g",
+ expected_dests = ["hello.txt"],
+ )
+
+ # And now, files_only = True
+ pkg_files(
+ name = "pf_files_only_g",
+ srcs = ["testdata/hello.txt"],
+ strip_prefix = strip_prefix.files_only(),
+ tags = ["manual"],
+ )
+
+ pkg_files_contents_test(
+ name = "pf_files_only",
+ target_under_test = ":pf_files_only_g",
+ expected_dests = ["hello.txt"],
+ )
+
+ # Used in the following tests
+ #
+ # Note that since the pkg_files rule is never actually used in anything
+ # other than this test, nonexistent_script can be included with no ill effects. :P
+ native.sh_binary(
+ name = "testdata/test_script",
+ srcs = ["testdata/nonexistent_script.sh"],
+ tags = ["manual"],
+ )
+
+ # Test stripping from the package root
+ pkg_files(
+ name = "pf_from_pkg_g",
+ srcs = [
+ "testdata/hello.txt",
+ ":testdata/test_script",
+ ],
+ strip_prefix = strip_prefix.from_pkg("testdata/"),
+ tags = ["manual"],
+ )
+
+ pkg_files_contents_test(
+ name = "pf_strip_testdata_from_pkg",
+ target_under_test = ":pf_from_pkg_g",
+ expected_dests = [
+ # Static file
+ "hello.txt",
+ # The script itself
+ "nonexistent_script.sh",
+ # The generated target output, in this case, a symlink
+ "test_script",
+ ],
+ )
+
+ # Test the stripping from root.
+ #
+ # In this case, the components to be stripped are taken relative to the root
+ # of the package. Local and generated files should have the same prefix in
+ # all cases.
+
+ pkg_files(
+ name = "pf_from_root_g",
+ srcs = [":testdata/test_script"],
+ strip_prefix = strip_prefix.from_root("tests/mappings"),
+ tags = ["manual"],
+ )
+
+ pkg_files_contents_test(
+ name = "pf_strip_prefix_from_root",
+ target_under_test = ":pf_from_root_g",
+ expected_dests = [
+ # The script itself
+ "testdata/nonexistent_script.sh",
+ # The generated target output, in this case, a symlink
+ "testdata/test_script",
+ ],
+ )
+
+ # Test that pkg_files rejects cases where two sources resolve to the same
+ # destination.
+ pkg_files(
+ name = "pf_destination_collision_invalid_g",
+ srcs = ["foo", "bar/foo"],
+ tags = ["manual"],
+ )
+ generic_neg_test(
+ name = "pf_destination_collision_invalid",
+ target_under_test = ":pf_destination_collision_invalid_g",
+ )
+
+ # Test strip_prefix when it can't complete the strip operation as requested.
+ pkg_files(
+ name = "pf_strip_prefix_from_package_invalid_g",
+ srcs = ["foo/foo", "bar/foo"],
+ strip_prefix = strip_prefix.from_pkg("bar"),
+ tags = ["manual"],
+ )
+ generic_neg_test(
+ name = "pf_strip_prefix_from_package_invalid",
+ target_under_test = ":pf_strip_prefix_from_package_invalid_g",
+ )
+
+ # Ditto, except strip from the root.
+ pkg_files(
+ name = "pf_strip_prefix_from_root_invalid_g",
+ srcs = ["foo", "bar"],
+ strip_prefix = strip_prefix.from_root("not/the/root"),
+ tags = ["manual"],
+ )
+ generic_neg_test(
+ name = "pf_strip_prefix_from_root_invalid",
+ target_under_test = ":pf_strip_prefix_from_root_invalid_g",
+ )
+
+def _test_pkg_files_exclusions():
+ # Normal filegroup, used in all of the below tests
+ #
+ # Needs to be here to test the distinction between files and label inputs to
+ # "excludes". This, admittedly, may be unnecessary.
+ native.filegroup(
+ name = "test_base_fg",
+ srcs = [
+ "testdata/config",
+ "testdata/hello.txt",
+ ],
+ )
+
+ # Tests to exclude from the case where stripping is done up to filenames
+ pkg_files(
+ name = "pf_exclude_by_label_strip_all_g",
+ srcs = [":test_base_fg"],
+ excludes = ["//tests/mappings:testdata/config"],
+ tags = ["manual"],
+ )
+ pkg_files_contents_test(
+ name = "pf_exclude_by_label_strip_all",
+ target_under_test = ":pf_exclude_by_label_strip_all_g",
+ expected_dests = ["hello.txt"],
+ )
+
+ pkg_files(
+ name = "pf_exclude_by_filename_strip_all_g",
+ srcs = [":test_base_fg"],
+ excludes = ["testdata/config"],
+ tags = ["manual"],
+ )
+ pkg_files_contents_test(
+ name = "pf_exclude_by_filename_strip_all",
+ target_under_test = ":pf_exclude_by_filename_strip_all_g",
+ expected_dests = ["hello.txt"],
+ )
+
+ # Tests to exclude from the case where stripping is done from the package root
+ pkg_files(
+ name = "pf_exclude_by_label_strip_from_pkg_g",
+ srcs = [":test_base_fg"],
+ excludes = ["//tests/mappings:testdata/config"],
+ strip_prefix = strip_prefix.from_pkg("testdata"),
+ tags = ["manual"],
+ )
+ pkg_files_contents_test(
+ name = "pf_exclude_by_label_strip_from_pkg",
+ target_under_test = ":pf_exclude_by_label_strip_from_pkg_g",
+ expected_dests = ["hello.txt"],
+ )
+
+ pkg_files(
+ name = "pf_exclude_by_filename_strip_from_pkg_g",
+ srcs = [":test_base_fg"],
+ excludes = ["testdata/config"],
+ strip_prefix = strip_prefix.from_pkg("testdata"),
+ tags = ["manual"],
+ )
+ pkg_files_contents_test(
+ name = "pf_exclude_by_filename_strip_from_pkg",
+ target_under_test = ":pf_exclude_by_filename_strip_from_pkg_g",
+ expected_dests = ["hello.txt"],
+ )
+
+ # Tests to exclude from the case where stripping is done from the root
+ pkg_files(
+ name = "pf_exclude_by_label_strip_from_root_g",
+ srcs = [":test_base_fg"],
+ excludes = ["//tests/mappings:testdata/config"],
+ strip_prefix = strip_prefix.from_root("tests/mappings"),
+ tags = ["manual"],
+ )
+ pkg_files_contents_test(
+ name = "pf_exclude_by_label_strip_from_root",
+ target_under_test = ":pf_exclude_by_label_strip_from_root_g",
+ expected_dests = ["testdata/hello.txt"],
+ )
+
+ pkg_files(
+ name = "pf_exclude_by_filename_strip_from_root_g",
+ srcs = [":test_base_fg"],
+ excludes = ["testdata/config"],
+ strip_prefix = strip_prefix.from_root("tests/mappings"),
+ tags = ["manual"],
+ )
+ pkg_files_contents_test(
+ name = "pf_exclude_by_filename_strip_from_root",
+ target_under_test = ":pf_exclude_by_filename_strip_from_root_g",
+ expected_dests = ["testdata/hello.txt"],
+ )
+
+# Tests involving external repositories
+def _test_pkg_files_extrepo():
+ # From external repo root, basenames only
+ pkg_files(
+ name = "pf_extrepo_strip_all_g",
+ srcs = ["@mappings_test_external_repo//pkg:dir/script"],
+ tags = ["manual"],
+ )
+ pkg_files_contents_test(
+ name = "pf_extrepo_strip_all",
+ target_under_test = ":pf_extrepo_strip_all_g",
+ expected_dests = ["extproj.sh", "script"],
+ )
+
+ # From external repo root, relative to the "pkg" package
+ pkg_files(
+ name = "pf_extrepo_strip_from_pkg_g",
+ srcs = ["@mappings_test_external_repo//pkg:dir/script"],
+ strip_prefix = strip_prefix.from_pkg("dir"),
+ tags = ["manual"],
+ )
+ pkg_files_contents_test(
+ name = "pf_extrepo_strip_from_pkg",
+ target_under_test = ":pf_extrepo_strip_from_pkg_g",
+ expected_dests = [
+ "extproj.sh",
+ "script",
+ ],
+ )
+
+ # From external repo root, relative to the "pkg" directory
+ pkg_files(
+ name = "pf_extrepo_strip_from_root_g",
+ srcs = ["@mappings_test_external_repo//pkg:dir/script"],
+ strip_prefix = strip_prefix.from_root("pkg"),
+ tags = ["manual"],
+ )
+ pkg_files_contents_test(
+ name = "pf_extrepo_strip_from_root",
+ target_under_test = ":pf_extrepo_strip_from_root_g",
+ expected_dests = ["dir/extproj.sh", "dir/script"],
+ )
+
+ native.filegroup(
+ name = "extrepo_test_fg",
+ srcs = ["@mappings_test_external_repo//pkg:dir/extproj.sh"],
+ )
+
+ # Test the case when a have a pkg_files that targets a local filegroup
+ # that has files in an external repo.
+ pkg_files(
+ name = "pf_extrepo_filegroup_strip_from_pkg_g",
+ srcs = [":extrepo_test_fg"],
+ # Files within filegroups should be considered relative to their
+ # destination paths.
+ strip_prefix = strip_prefix.from_pkg(""),
+ )
+ pkg_files_contents_test(
+ name = "pf_extrepo_filegroup_strip_from_pkg",
+ target_under_test = ":pf_extrepo_filegroup_strip_from_pkg_g",
+ expected_dests = ["dir/extproj.sh"],
+ )
+
+ # Ditto, except strip from the workspace root instead
+ pkg_files(
+ name = "pf_extrepo_filegroup_strip_from_root_g",
+ srcs = [":extrepo_test_fg"],
+ # Files within filegroups should be considered relative to their
+ # destination paths.
+ strip_prefix = strip_prefix.from_root("pkg"),
+ )
+ pkg_files_contents_test(
+ name = "pf_extrepo_filegroup_strip_from_root",
+ target_under_test = ":pf_extrepo_filegroup_strip_from_root_g",
+ expected_dests = ["dir/extproj.sh"],
+ )
+
+ # Reference a pkg_files in @mappings_test_external_repo
+ pkg_files_contents_test(
+ name = "pf_pkg_files_in_extrepo",
+ target_under_test = "@mappings_test_external_repo//pkg:extproj_script_pf",
+ expected_dests = ["usr/bin/dir/extproj.sh"],
+ )
+
+def _test_pkg_files_rename():
+ pkg_files(
+ name = "pf_rename_multiple_g",
+ srcs = [
+ "testdata/hello.txt",
+ "testdata/loremipsum.txt",
+ ],
+ prefix = "usr",
+ renames = {
+ "testdata/hello.txt": "share/goodbye.txt",
+ "testdata/loremipsum.txt": "doc/dolorsitamet.txt",
+ },
+ tags = ["manual"],
+ )
+ pkg_files_contents_test(
+ name = "pf_rename_multiple",
+ target_under_test = ":pf_rename_multiple_g",
+ expected_dests = [
+ "usr/share/goodbye.txt",
+ "usr/doc/dolorsitamet.txt",
+ ],
+ )
+
+ # Used in the following tests
+ #
+ # Note that since the pkg_files rule is never actually used in anything
+ # other than this test, nonexistent_script can be included with no ill
+ # effects. :P
+ native.sh_binary(
+ name = "test_script_rename",
+ srcs = ["testdata/nonexistent_script.sh"],
+ tags = ["manual"],
+ )
+
+ # test_script_rename produces multiple outputs. Thus, this test should
+ # fail, as pkg_files can't figure out what should actually be mapped to
+ # the output destination.
+ pkg_files(
+ name = "pf_rename_rule_with_multiple_outputs_g",
+ srcs = ["test_script_rename"],
+ renames = {
+ ":test_script_rename": "still_nonexistent_script",
+ },
+ tags = ["manual"],
+ )
+ generic_neg_test(
+ name = "pf_rename_rule_with_multiple_outputs",
+ target_under_test = ":pf_rename_rule_with_multiple_outputs_g",
+ )
+
+ # Fail because we tried to install a file that wasn't mentioned in the deps
+ # list
+ pkg_files(
+ name = "pf_rename_single_missing_value_g",
+ srcs = ["testdata/hello.txt"],
+ prefix = "usr",
+ renames = {
+ "nonexistent_script": "nonexistent_output_location",
+ },
+ tags = ["manual"],
+ )
+ generic_neg_test(
+ name = "pf_rename_single_missing_value",
+ target_under_test = ":pf_rename_single_missing_value_g",
+ )
+
+ # Ditto, except for exclusions
+ pkg_files(
+ name = "pf_rename_single_excluded_value_g",
+ srcs = [
+ "testdata/hello.txt",
+ "testdata/loremipsum.txt",
+ ],
+ prefix = "usr",
+ excludes = [
+ "testdata/hello.txt",
+ ],
+ renames = {
+ "testdata/hello.txt": "share/goodbye.txt",
+ },
+ tags = ["manual"],
+ )
+ generic_neg_test(
+ name = "pf_rename_single_excluded_value",
+ target_under_test = ":pf_rename_single_excluded_value_g",
+ )
+
+ # Test whether or not destination collisions are detected after renaming.
+ pkg_files(
+ name = "pf_rename_destination_collision_g",
+ srcs = [
+ "foo",
+ "bar",
+ ],
+ renames = {"foo": "bar"},
+ tags = ["manual"],
+ )
+ generic_neg_test(
+ name = "pf_rename_destination_collision",
+ target_under_test = ":pf_rename_destination_collision_g",
+ )
+
+##########
+# Test pkg_mkdirs
+##########
+
+def _pkg_mkdirs_contents_test_impl(ctx):
+ env = analysistest.begin(ctx)
+ target_under_test = analysistest.target_under_test(env)
+
+ expected_dirs = sets.make(ctx.attr.expected_dirs)
+ actual_dirs = sets.make(target_under_test[PackageDirsInfo].dirs)
+
+ asserts.new_set_equals(env, expected_dirs, actual_dirs, "pkg_mkdirs dirs do not match expectations")
+
+ # Simple equality checks for the others
+ asserts.equals(
+ env,
+ ctx.attr.expected_attributes,
+ target_under_test[PackageDirsInfo].attributes,
+ "pkg_mkdir attributes do not match expectations",
+ )
+
+ return analysistest.end(env)
+
+pkg_mkdirs_contents_test = analysistest.make(
+ _pkg_mkdirs_contents_test_impl,
+ attrs = {
+ "expected_attributes": attr.string_list_dict(),
+ "expected_dirs": attr.string_list(
+ mandatory = True,
+ ),
+ },
+)
+
+def _test_pkg_mkdirs():
+ # Reasonable base case
+ pkg_mkdirs(
+ name = "pkg_mkdirs_base_g",
+ dirs = ["foo/bar", "baz"],
+ attributes = {"unix": ["0711", "root", "sudo"]},
+ tags = ["manual"],
+ )
+ pkg_mkdirs_contents_test(
+ name = "pkg_mkdirs_base",
+ target_under_test = "pkg_mkdirs_base_g",
+ expected_dirs = ["foo/bar", "baz"],
+ expected_attributes = {"unix": ["0711", "root", "sudo"]},
+ )
+
+##########
+# Test strip_prefix pseudo-module
+##########
+
+def _strip_prefix_test_impl(ctx):
+ env = unittest.begin(ctx)
+ asserts.equals(env, ".", strip_prefix.files_only())
+ asserts.equals(env, "path", strip_prefix.from_pkg("path"))
+ asserts.equals(env, "path", strip_prefix.from_pkg("/path"))
+ asserts.equals(env, "/path", strip_prefix.from_root("path"))
+ asserts.equals(env, "/path", strip_prefix.from_root("/path"))
+ return unittest.end(env)
+
+strip_prefix_test = unittest.make(_strip_prefix_test_impl)
+
+def mappings_analysis_tests():
+ """Declare mappings.bzl analysis tests"""
+ _test_pkg_files_contents()
+ _test_pkg_files_exclusions()
+ _test_pkg_files_extrepo()
+ _test_pkg_files_rename()
+ _test_pkg_mkdirs()
+
+ native.test_suite(
+ name = "pkg_files_analysis_tests",
+ # We should find a way to get rid of this test list; it would be nice if
+ # it could be derived from something else...
+ tests = [
+ # buildifier: don't sort
+ # Simple tests
+ ":pf_no_strip_prefix",
+ ":pf_files_only",
+ ":pf_strip_testdata_from_pkg",
+ ":pf_strip_prefix_from_root",
+ # Tests involving excluded files
+ ":pf_exclude_by_label_strip_all",
+ ":pf_exclude_by_filename_strip_all",
+ ":pf_exclude_by_label_strip_from_pkg",
+ ":pf_exclude_by_filename_strip_from_pkg",
+ ":pf_exclude_by_label_strip_from_root",
+ ":pf_exclude_by_filename_strip_from_root",
+ # Negative tests
+ ":pf_destination_collision_invalid",
+ ":pf_strip_prefix_from_package_invalid",
+ ":pf_strip_prefix_from_root_invalid",
+ # Tests involving external repositories
+ ":pf_extrepo_strip_all",
+ ":pf_extrepo_strip_from_pkg",
+ ":pf_extrepo_strip_from_root",
+ ":pf_extrepo_filegroup_strip_from_pkg",
+ ":pf_extrepo_filegroup_strip_from_root",
+ ":pf_pkg_files_in_extrepo",
+ # This one fits into the same category, but can't be aliased, apparently.
+ #
+ # The main purpose behind it is to verify cases wherein we build a
+ # file, but then have it consumed by some remote package.
+ "@mappings_test_external_repo//pkg:pf_local_file_in_extrepo",
+ # Tests involving file renaming
+ ":pf_rename_multiple",
+ ":pf_rename_rule_with_multiple_outputs",
+ ":pf_rename_single_missing_value",
+ ":pf_rename_single_excluded_value",
+ # Tests involving pkg_mkdirs
+ ":pkg_mkdirs_base",
+ ],
+ )
+
+def mappings_unit_tests():
+ unittest.suite(
+ "mappings_unit_tests",
+ strip_prefix_test,
+ )
diff --git a/pkg/tests/mappings/testdata b/pkg/tests/mappings/testdata
new file mode 120000
index 00000000..4f7c1168
--- /dev/null
+++ b/pkg/tests/mappings/testdata
@@ -0,0 +1 @@
+../testdata
\ No newline at end of file