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