├── .bazelversion ├── .gitignore ├── BUILD.bazel ├── README.md ├── WORKSPACE ├── app ├── BUILD.bazel ├── community_data.txt ├── community_lib.cc ├── enterprise_data.txt ├── enterprise_lib.cc ├── main.cc ├── test_community.sh └── test_enterprise.sh ├── config └── BUILD.bazel └── flag ├── BUILD.bazel └── defs.bzl /.bazelversion: -------------------------------------------------------------------------------- 1 | 4.0.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bazel-* 2 | -------------------------------------------------------------------------------- /BUILD.bazel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aherrmann/bazel-transitions-demo/f22cf40a62131eace14829f262e8d7c00b0a9a19/BUILD.bazel -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Demo of Bazel's configuration transitions 2 | 3 | ## Configuration flags 4 | 5 | Build and run the default configuration: 6 | 7 | ``` 8 | $ bazel run //app:bin 9 | Using community edition 10 | Data file: COMMUNITY 11 | ``` 12 | 13 | Specify the configuration on the command line: 14 | 15 | ``` 16 | $ bazel run //app:bin --//flag:edition=enterprise 17 | Using enterprise edition 18 | Data file: ENTERPRISE 19 | ``` 20 | 21 | ## Configuration transitions 22 | 23 | The following targets use configuration transitions to pin a particular 24 | configuration regardless of any command line flags: 25 | 26 | ``` 27 | $ bazel run //app:bin-ce 28 | Using community edition 29 | Data file: COMMUNITY 30 | 31 | $ bazel run //app:bin-ee 32 | Using enterprise edition 33 | Data file: ENTERPRISE 34 | ``` 35 | 36 | The following uses configuration transitions to generate a test-suite over all 37 | configurations: 38 | 39 | ``` 40 | $ bazel test //app:test-all 41 | //app:test-all_test_community PASSED in 0.0s 42 | //app:test-all_test_enterprise PASSED in 0.0s 43 | ``` 44 | 45 | ## Bundling multiple configurations 46 | 47 | The following uses configuration transitions and careful combinations of the 48 | `pkg_tar` rule to bundle multiple configurations into one artifact. 49 | 50 | ``` 51 | $ bazel build //app:bundle-all 52 | Target //app:bundle-all up-to-date: 53 | bazel-bin/app/bundle-all.tar 54 | $ tar tvf bazel-bin/app/bundle-all.tar 55 | drwxr-xr-x 0/0 0 2000-01-01 01:00 ./ 56 | drwxr-xr-x 0/0 0 2000-01-01 01:00 ./community/ 57 | -r-xr-xr-x 0/0 117016 2000-01-01 01:00 ./community/bin 58 | -r-xr-xr-x 0/0 10 2000-01-01 01:00 ./community/data.txt 59 | drwxr-xr-x 0/0 0 2000-01-01 01:00 ./community/bin.runfiles/ 60 | drwxr-xr-x 0/0 0 2000-01-01 01:00 ./community/bin.runfiles/transitions-demo/ 61 | drwxr-xr-x 0/0 0 2000-01-01 01:00 ./community/bin.runfiles/transitions-demo/app/ 62 | lrwxr-xr-x 0/0 0 2000-01-01 01:00 ./community/bin.runfiles/transitions-demo/app/data.txt -> ../../../data.txt 63 | drwxr-xr-x 0/0 0 2000-01-01 01:00 ./enterprise/ 64 | -r-xr-xr-x 0/0 117024 2000-01-01 01:00 ./enterprise/bin 65 | -r-xr-xr-x 0/0 11 2000-01-01 01:00 ./enterprise/data.txt 66 | drwxr-xr-x 0/0 0 2000-01-01 01:00 ./enterprise/bin.runfiles/ 67 | drwxr-xr-x 0/0 0 2000-01-01 01:00 ./enterprise/bin.runfiles/transitions-demo/ 68 | drwxr-xr-x 0/0 0 2000-01-01 01:00 ./enterprise/bin.runfiles/transitions-demo/app/ 69 | lrwxr-xr-x 0/0 0 2000-01-01 01:00 ./enterprise/bin.runfiles/transitions-demo/app/data.txt -> ../../../data.txt 70 | ``` 71 | 72 | ## Gotchas 73 | 74 | Combining different configurations of the same target requires some care. It is 75 | possible to cause collisions when combining such targets in one rule. 76 | 77 | ``` 78 | $ bazel build //app:gotcha 79 | INFO: From Writing: bazel-out/k8-fastbuild/bin/app/gotcha.tar: 80 | Duplicate file in archive: ./bin, picking first occurrence 81 | Duplicate file in archive: ./data.txt, picking first occurrence 82 | Target //app:gotcha up-to-date: 83 | bazel-bin/app/gotcha.tar 84 | $ tar tvf bazel-bin/app/gotcha.tar 85 | drwxr-xr-x 0/0 0 2000-01-01 01:00 ./ 86 | -r-xr-xr-x 0/0 117016 2000-01-01 01:00 ./bin-ce 87 | -r-xr-xr-x 0/0 117016 2000-01-01 01:00 ./bin 88 | -r-xr-xr-x 0/0 10 2000-01-01 01:00 ./data.txt 89 | -r-xr-xr-x 0/0 117024 2000-01-01 01:00 ./bin-ee 90 | ``` 91 | 92 | This caused a collision in the files `bin` and `data.txt`. The resulting 93 | artifact will only contain one of the configurations. 94 | 95 | ## Build definitions 96 | 97 | Take a look at [`app/BUILD.bazel`](./app/BUILD.bazel) to see the definitions of 98 | the targets above. Take a look at [`flag/defs.bzl`](./flag/defs.bzl) for the 99 | implementation. 100 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | workspace(name = "transitions-demo") 2 | 3 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 4 | 5 | http_archive( 6 | name = "bazel_skylib", 7 | sha256 = "1c531376ac7e5a180e0237938a2536de0c54d93f5c278634818e0efc952dd56c", 8 | urls = [ 9 | "https://github.com/bazelbuild/bazel-skylib/releases/download/1.0.3/bazel-skylib-1.0.3.tar.gz", 10 | "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.0.3/bazel-skylib-1.0.3.tar.gz", 11 | ], 12 | ) 13 | 14 | http_archive( 15 | name = "rules_pkg", 16 | sha256 = "038f1caa773a7e35b3663865ffb003169c6a71dc995e39bf4815792f385d837d", 17 | urls = [ 18 | "https://mirror.bazel.build/github.com/bazelbuild/rules_pkg/releases/download/0.4.0/rules_pkg-0.4.0.tar.gz", 19 | "https://github.com/bazelbuild/rules_pkg/releases/download/0.4.0/rules_pkg-0.4.0.tar.gz", 20 | ], 21 | ) 22 | 23 | load("@rules_pkg//:deps.bzl", "rules_pkg_dependencies") 24 | 25 | rules_pkg_dependencies() 26 | -------------------------------------------------------------------------------- /app/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@bazel_skylib//rules:copy_file.bzl", "copy_file") 2 | load("@rules_pkg//:pkg.bzl", "pkg_tar") 3 | load( 4 | "//flag:defs.bzl", 5 | "edition_binary", 6 | "edition_files", 7 | "editions_files", 8 | "editions_test_suite", 9 | ) 10 | 11 | # ---------------------------------------------------------- 12 | # Example of edition specific libraries. 13 | # 14 | # We define dedicated targets for each library and define an alias to switch 15 | # between the two depending on the configured edition. 16 | 17 | cc_library( 18 | name = "community_lib", 19 | srcs = ["community_lib.cc"], 20 | ) 21 | 22 | cc_library( 23 | name = "enterprise_lib", 24 | srcs = ["enterprise_lib.cc"], 25 | ) 26 | 27 | alias( 28 | name = "lib", 29 | actual = select({ 30 | "//config:community_edition": "community_lib", 31 | "//config:enterprise_edition": "enterprise_lib", 32 | }), 33 | ) 34 | 35 | # ---------------------------------------------------------- 36 | # Example of edition specific data files. 37 | # 38 | # We include both edition's raw source files and copy the appropriate file to 39 | # the expected name. 40 | 41 | copy_file( 42 | name = "data", 43 | src = select({ 44 | "//config:community_edition": "community_data.txt", 45 | "//config:enterprise_edition": "enterprise_data.txt", 46 | }), 47 | out = "data.txt", 48 | ) 49 | 50 | # ---------------------------------------------------------- 51 | # Example of a binary depending on edition specific targets. 52 | # 53 | # The target itself does not explicitly depend on the edition. 54 | 55 | cc_binary( 56 | name = "bin", 57 | srcs = ["main.cc"], 58 | data = ["data"], 59 | deps = [ 60 | ":lib", 61 | "@bazel_tools//tools/cpp/runfiles", 62 | ], 63 | ) 64 | 65 | # ---------------------------------------------------------- 66 | # Example of a test depending on edition specific sources. 67 | # 68 | # The test executes the binary above that has edition specific dependencies. 69 | # 70 | # Uses editions_test_suite to generate a test suite covering all editions. 71 | 72 | sh_test( 73 | name = "test", 74 | srcs = select({ 75 | "//config:community_edition": ["test_community.sh"], 76 | "//config:enterprise_edition": ["test_enterprise.sh"], 77 | }), 78 | args = ["$(rootpath :bin)"], 79 | data = ["bin"], 80 | ) 81 | 82 | editions_test_suite( 83 | name = "test-all", 84 | tests = ["test"], 85 | ) 86 | 87 | # ---------------------------------------------------------- 88 | # Explicitly configured edition binaries. 89 | 90 | edition_binary( 91 | name = "bin-ce", 92 | edition = "community", 93 | executable = "bin", 94 | ) 95 | 96 | edition_binary( 97 | name = "bin-ee", 98 | edition = "enterprise", 99 | executable = "bin", 100 | ) 101 | 102 | # ---------------------------------------------------------- 103 | # Example of a gotcha. 104 | # 105 | # This will see the different editions as duplicates. It warns 106 | # 107 | # > Duplicate file in archive: ./bin, picking first occurrence 108 | # > Duplicate file in archive: ./data.txt, picking first occurrence 109 | # 110 | # and produces the following archive. 111 | # 112 | # > ./bin 113 | # > ./bin-ce 114 | # > ./bin-ee 115 | # > ./data.txt 116 | # 117 | # I.e. data.txt and bin will correspond to one of the editions and the other 118 | # will be missing. Note that the `edition_binary` symlinks are converted to 119 | # copies. 120 | 121 | pkg_tar( 122 | name = "gotcha", 123 | srcs = [ 124 | "bin-ce", 125 | "bin-ee", 126 | ], 127 | include_runfiles = True, 128 | ) 129 | 130 | # ---------------------------------------------------------- 131 | # Example bundling both editions into one artifact. 132 | # 133 | # First, create a pkg_tar for each edition based on the configuration. 134 | # Then multiplex it into multiple files using the editions_files rule. 135 | # Finally, bundle the multiplexed version into a combined pkg_tar. 136 | 137 | pkg_tar( 138 | name = "bundle-config", 139 | srcs = [ 140 | "bin", 141 | ], 142 | include_runfiles = True, 143 | package_dir = select({ 144 | "//config:community_edition": "community", 145 | "//config:enterprise_edition": "enterprise", 146 | }), 147 | # pkg_tar doesn't reconstruct runfiles trees, so we do it manually. 148 | symlinks = select({ 149 | "//config:community_edition": {"community/bin.runfiles/transitions-demo/app/data.txt": "../../../data.txt"}, 150 | "//config:enterprise_edition": {"enterprise/bin.runfiles/transitions-demo/app/data.txt": "../../../data.txt"}, 151 | }), 152 | ) 153 | 154 | editions_files( 155 | name = "bundle-each", 156 | srcs = ["bundle-config"], 157 | editions = [ 158 | "community", 159 | "enterprise", 160 | ], 161 | ) 162 | 163 | pkg_tar( 164 | name = "bundle-all", 165 | deps = ["bundle-each"], 166 | ) 167 | -------------------------------------------------------------------------------- /app/community_data.txt: -------------------------------------------------------------------------------- 1 | COMMUNITY 2 | -------------------------------------------------------------------------------- /app/community_lib.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void edition_function() { 4 | std::cerr << "Using community edition\n"; 5 | } 6 | -------------------------------------------------------------------------------- /app/enterprise_data.txt: -------------------------------------------------------------------------------- 1 | ENTERPRISE 2 | -------------------------------------------------------------------------------- /app/enterprise_lib.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void edition_function() { 4 | std::cerr << "Using enterprise edition\n"; 5 | } 6 | -------------------------------------------------------------------------------- /app/main.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "tools/cpp/runfiles/runfiles.h" 4 | 5 | void edition_function(); 6 | 7 | int main(int argc, char **argv) { 8 | edition_function(); 9 | 10 | using bazel::tools::cpp::runfiles::Runfiles; 11 | 12 | std::string error; 13 | std::unique_ptr runfiles(Runfiles::Create(argv[0], &error)); 14 | if (runfiles == nullptr) { 15 | std::cerr << "Failed to create runfiles: " << error << "\n"; 16 | std::exit(1); 17 | } 18 | std::string path = runfiles->Rlocation("transitions-demo/app/data.txt"); 19 | std::ifstream data(path); 20 | if (!data.is_open()) { 21 | std::cerr << "Failed to open data file: " << path << "\n"; 22 | std::exit(1); 23 | } 24 | std::cerr << "Data file: " << data.rdbuf() << "\n"; 25 | } 26 | -------------------------------------------------------------------------------- /app/test_community.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | set -x 4 | 5 | ACTUAL="$($1 2>&1)" 6 | EXPECTED="Using community edition 7 | Data file: COMMUNITY" 8 | 9 | if [[ $ACTUAL != $EXPECTED ]]; then 10 | echo "Mismatched output:" >&2 11 | echo "Expected:" >&2 12 | echo "$EXPECTED" >&2 13 | echo "Actual:" >&2 14 | echo "$ACTUAL" >&2 15 | exit 1 16 | fi 17 | -------------------------------------------------------------------------------- /app/test_enterprise.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | set -x 4 | 5 | ACTUAL="$($1 2>&1)" 6 | EXPECTED="Using enterprise edition 7 | Data file: ENTERPRISE" 8 | 9 | if [[ $ACTUAL != $EXPECTED ]]; then 10 | echo "Mismatched output:" >&2 11 | echo "Expected:" >&2 12 | echo "$EXPECTED" >&2 13 | echo "Actual:" >&2 14 | echo "$ACTUAL" >&2 15 | exit 1 16 | fi 17 | -------------------------------------------------------------------------------- /config/BUILD.bazel: -------------------------------------------------------------------------------- 1 | config_setting( 2 | name = "community_edition", 3 | flag_values = { 4 | "//flag:edition": "community", 5 | }, 6 | visibility = ["//visibility:public"], 7 | ) 8 | 9 | config_setting( 10 | name = "enterprise_edition", 11 | flag_values = { 12 | "//flag:edition": "enterprise", 13 | }, 14 | visibility = ["//visibility:public"], 15 | ) 16 | -------------------------------------------------------------------------------- /flag/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("defs.bzl", "edition_flag") 2 | 3 | edition_flag( 4 | name = "edition", 5 | build_setting_default = "community", 6 | visibility = ["//visibility:public"], 7 | ) 8 | -------------------------------------------------------------------------------- /flag/defs.bzl: -------------------------------------------------------------------------------- 1 | load("@bazel_skylib//lib:paths.bzl", "paths") 2 | 3 | EditionProvider = provider(fields = ["edition"]) 4 | 5 | editions = ["community", "enterprise"] 6 | 7 | _unknown_edition_error = "--{label} expects values {expected} but was set to {actual}." 8 | 9 | def _edition_flag_impl(ctx): 10 | value = ctx.build_setting_value 11 | if value not in editions: 12 | fail(_unknown_edition_error.format( 13 | label = str(ctx.label), 14 | expected = repr(editions), 15 | actual = repr(value), 16 | )) 17 | return EditionProvider(edition = value) 18 | 19 | edition_flag = rule( 20 | implementation = _edition_flag_impl, 21 | build_setting = config.string(flag = True), 22 | ) 23 | 24 | def _edition_transition_impl(settings, attr): 25 | edition = attr.edition 26 | return {"//flag:edition": edition} 27 | 28 | edition_transition = transition( 29 | implementation = _edition_transition_impl, 30 | inputs = [], 31 | outputs = ["//flag:edition"], 32 | ) 33 | 34 | def _editions_transition_impl(settings, attr): 35 | editions = attr.editions 36 | return [ 37 | {"//flag:edition": edition} 38 | for edition in editions 39 | ] 40 | 41 | editions_transition = transition( 42 | implementation = _editions_transition_impl, 43 | inputs = [], 44 | outputs = ["//flag:edition"], 45 | ) 46 | 47 | def _edition_files_impl(ctx): 48 | files = ctx.files.srcs 49 | return [DefaultInfo( 50 | files = depset(direct = files), 51 | )] 52 | 53 | edition_files = rule( 54 | _edition_files_impl, 55 | attrs = { 56 | "_allowlist_function_transition": attr.label( 57 | default = "@bazel_tools//tools/allowlists/function_transition_allowlist", 58 | ), 59 | "edition": attr.string(), 60 | "srcs": attr.label_list(allow_files = True), 61 | }, 62 | cfg = edition_transition, 63 | ) 64 | 65 | def _editions_files_impl(ctx): 66 | files = ctx.files.srcs 67 | return [DefaultInfo( 68 | files = depset(direct = files), 69 | )] 70 | 71 | editions_files = rule( 72 | _editions_files_impl, 73 | attrs = { 74 | "_allowlist_function_transition": attr.label( 75 | default = "@bazel_tools//tools/allowlists/function_transition_allowlist", 76 | ), 77 | "editions": attr.string_list(), 78 | "srcs": attr.label_list( 79 | allow_files = True, 80 | cfg = editions_transition, 81 | ), 82 | }, 83 | ) 84 | 85 | def _edition_binary_impl(ctx): 86 | (_, extension) = paths.split_extension(ctx.executable.executable.path) 87 | executable = ctx.actions.declare_file( 88 | ctx.label.name + extension, 89 | ) 90 | ctx.actions.symlink( 91 | output = executable, 92 | target_file = ctx.executable.executable, 93 | is_executable = True, 94 | ) 95 | 96 | runfiles = ctx.runfiles(files = [executable, ctx.executable.executable] + ctx.files.data) 97 | runfiles = runfiles.merge(ctx.attr.executable[DefaultInfo].default_runfiles) 98 | for data_dep in ctx.attr.data: 99 | runfiles = runfiles.merge(data_dep[DefaultInfo].default_runfiles) 100 | 101 | return [DefaultInfo( 102 | executable = executable, 103 | files = depset(direct = [executable]), 104 | runfiles = runfiles, 105 | )] 106 | 107 | edition_binary = rule( 108 | _edition_binary_impl, 109 | attrs = { 110 | "_allowlist_function_transition": attr.label( 111 | default = "@bazel_tools//tools/allowlists/function_transition_allowlist", 112 | ), 113 | "data": attr.label_list(allow_files = True), 114 | "edition": attr.string(), 115 | "executable": attr.label( 116 | cfg = "target", 117 | executable = True, 118 | ), 119 | }, 120 | cfg = edition_transition, 121 | executable = True, 122 | ) 123 | 124 | TestAspectInfo = provider(fields = ["args", "env"]) 125 | 126 | def _test_aspect_impl(target, ctx): 127 | data = getattr(ctx.rule.attr, "data", []) 128 | args = getattr(ctx.rule.attr, "args", []) 129 | env = getattr(ctx.rule.attr, "env", []) 130 | args = [ctx.expand_location(arg, data) for arg in args] 131 | env = {k: ctx.expand_location(v, data) for (k, v) in env.items()} 132 | return [TestAspectInfo( 133 | args = args, 134 | env = env, 135 | )] 136 | 137 | _test_aspect = aspect(_test_aspect_impl) 138 | 139 | def _edition_test_impl(ctx): 140 | test_aspect_info = ctx.attr.test[TestAspectInfo] 141 | (_, extension) = paths.split_extension(ctx.executable.test.path) 142 | executable = ctx.actions.declare_file( 143 | ctx.label.name + extension, 144 | ) 145 | ctx.actions.write( 146 | output = executable, 147 | content = """\ 148 | #!/usr/bin/env bash 149 | set -euo pipefail 150 | {commands} 151 | """.format( 152 | commands = "\n".join([ 153 | " \\\n".join([ 154 | '{}="{}"'.format(k, v) 155 | for k, v in test_aspect_info.env.items() 156 | ] + [ 157 | ctx.executable.test.short_path, 158 | ] + test_aspect_info.args), 159 | ]), 160 | ), 161 | is_executable = True, 162 | ) 163 | 164 | runfiles = ctx.runfiles(files = [executable, ctx.executable.test] + ctx.files.data) 165 | runfiles = runfiles.merge(ctx.attr.test[DefaultInfo].default_runfiles) 166 | for data_dep in ctx.attr.data: 167 | runfiles = runfiles.merge(data_dep[DefaultInfo].default_runfiles) 168 | 169 | return [DefaultInfo( 170 | executable = executable, 171 | files = depset(direct = [executable]), 172 | runfiles = runfiles, 173 | )] 174 | 175 | edition_test = rule( 176 | _edition_test_impl, 177 | attrs = { 178 | "_allowlist_function_transition": attr.label( 179 | default = "@bazel_tools//tools/allowlists/function_transition_allowlist", 180 | ), 181 | "data": attr.label_list(allow_files = True), 182 | "edition": attr.string(), 183 | "test": attr.label( 184 | aspects = [_test_aspect], 185 | cfg = "target", 186 | executable = True, 187 | ), 188 | }, 189 | cfg = edition_transition, 190 | executable = True, 191 | test = True, 192 | ) 193 | 194 | def editions_test_suite(name, tests = [], editions = editions, **kwargs): 195 | test_suite_tests = [] 196 | for test in tests: 197 | if test.startswith(":") or test.startswith("//") or test.startswith("@"): 198 | test_name = Label(test).name 199 | else: 200 | test_name = test 201 | for edition in editions: 202 | edition_test_name = "{}_{}_{}".format(name, test_name, edition) 203 | test_suite_tests.append(edition_test_name) 204 | edition_test( 205 | name = edition_test_name, 206 | edition = edition, 207 | test = test, 208 | ) 209 | native.test_suite( 210 | name = name, 211 | tests = test_suite_tests, 212 | **kwargs 213 | ) 214 | --------------------------------------------------------------------------------