diff --git a/policy/src/main/java/dev/cel/policy/tools/BUILD.bazel b/policy/src/main/java/dev/cel/policy/tools/BUILD.bazel new file mode 100644 index 000000000..98ecb32a1 --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/tools/BUILD.bazel @@ -0,0 +1,49 @@ +load("@rules_java//java:defs.bzl", "java_binary", "java_library") + +package( + default_applicable_licenses = [ + "//:license", + ], + default_visibility = [ + "//policy/tools:__pkg__", + ], +) + +java_library( + name = "tools", + srcs = ["CelPolicyCompilerTool.java"], + tags = [ + ], + deps = [ + "//bundle:cel", + "//bundle:environment", + "//bundle:environment_yaml_parser", + "//common:cel_ast", + "//common:cel_descriptor_util", + "//common:options", + "//common:proto_ast", + "//common:proto_v1alpha1_ast", + "//extensions", + "//extensions:optional_library", + "//optimizer/optimizers:common_subexpression_elimination", + "//optimizer/optimizers:constant_folding", + "//optimizer/optimizers:select_optimizer", + "//parser:macro", + "//policy", + "//policy:compiler", + "//policy:compiler_builder", + "//policy:compiler_factory", + "//policy:parser", + "//policy:parser_factory", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:info_picocli_picocli", + ], +) + +java_binary( + name = "cel_policy_compiler_tool", + main_class = "dev.cel.policy.tools.CelPolicyCompilerTool", + neverlink = 1, + runtime_deps = [":tools"], +) diff --git a/policy/src/main/java/dev/cel/policy/tools/CelPolicyCompilerTool.java b/policy/src/main/java/dev/cel/policy/tools/CelPolicyCompilerTool.java new file mode 100644 index 000000000..2eb0c247e --- /dev/null +++ b/policy/src/main/java/dev/cel/policy/tools/CelPolicyCompilerTool.java @@ -0,0 +1,317 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// https://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 dev.cel.policy.tools; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.base.Ascii; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.DescriptorProtos.FileDescriptorSet; +import com.google.protobuf.Descriptors.FileDescriptor; +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.Message; +import com.google.protobuf.TextFormat; +import dev.cel.bundle.Cel; +import dev.cel.bundle.CelBuilder; +import dev.cel.bundle.CelEnvironment; +import dev.cel.bundle.CelEnvironmentYamlParser; +import dev.cel.bundle.CelFactory; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelDescriptorUtil; +import dev.cel.common.CelOptions; +import dev.cel.common.CelProtoAbstractSyntaxTree; +import dev.cel.common.CelProtoV1Alpha1AbstractSyntaxTree; +import dev.cel.extensions.CelExtensions; +import dev.cel.extensions.CelOptionalLibrary; +import dev.cel.optimizer.optimizers.ConstantFoldingOptimizer; +import dev.cel.optimizer.optimizers.SelectOptimizer; +import dev.cel.optimizer.optimizers.SelectOptimizer.SelectOptimizerOptions; +import dev.cel.optimizer.optimizers.SubexpressionOptimizer; +import dev.cel.optimizer.optimizers.SubexpressionOptimizer.SubexpressionOptimizerOptions; +import dev.cel.parser.CelStandardMacro; +import dev.cel.policy.CelPolicy; +import dev.cel.policy.CelPolicyCompiler; +import dev.cel.policy.CelPolicyCompilerBuilder; +import dev.cel.policy.CelPolicyCompilerFactory; +import dev.cel.policy.CelPolicyParser; +import dev.cel.policy.CelPolicyParserFactory; +import java.io.BufferedWriter; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.Callable; +import picocli.CommandLine; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.Spec; + +/** + * CelPolicyCompilerTool is a binary tool that compiles a CEL policy (.celpolicy or YAML) into a + * CheckedExpr protobuf message and writes it to a file or stdout. + */ +public final class CelPolicyCompilerTool implements Callable { + + @Spec private CommandSpec spec; + + @Option( + names = {"--policy"}, + description = "Path to the CEL policy file") + private String policyPath = ""; + + @SuppressWarnings("FieldCanBeFinal") // Populated reflectively by picocli + @Parameters( + index = "0", + arity = "0..1", + description = "Positional path to the CEL policy file if --policy is not specified") + private String positionalPolicyPath = ""; + + @Option( + names = {"--config", "--environment_path"}, + description = "Path to the CEL environment (in YAML)") + private String configPath = ""; + + @Option( + names = {"--base_config"}, + description = "Path to the base CEL environment (in YAML)") + private String baseConfigPath = ""; + + @Option( + names = {"--transitive_descriptor_set", "--file_descriptor_set"}, + description = "Path to the transitive set of descriptors") + private String transitiveDescriptorSetPath = ""; + + @Option( + names = {"--output"}, + description = "Output path for the compiled binarypb/textpb") + private String output = ""; + + @Option( + names = {"--output_format"}, + defaultValue = "binarypb", + description = "Output format: binarypb, textpb, or textproto") + private String outputFormat = "binarypb"; + + @Option( + names = {"--output_version"}, + defaultValue = "canonical", + description = "Output version: canonical or v1alpha1") + private String outputVersion = "canonical"; + + @Option( + names = {"--optimize_field_selection"}, + description = "Optimize field selection for version skew mitigation") + private boolean optimizeFieldSelection = false; + + private static final CelOptions CEL_OPTIONS = + CelOptions.current() + .populateMacroCalls(true) + .enableHeterogeneousNumericComparisons(true) + .build(); + + private PrintWriter out() { + return spec != null + ? spec.commandLine().getOut() + : new PrintWriter(new OutputStreamWriter(System.out, UTF_8), true); + } + + private PrintWriter err() { + return spec != null + ? spec.commandLine().getErr() + : new PrintWriter(new OutputStreamWriter(System.err, UTF_8), true); + } + + @Override + public Integer call() { + String effectivePolicyPath = policyPath.isEmpty() ? positionalPolicyPath : policyPath; + if (effectivePolicyPath.isEmpty()) { + err() + .println( + "Error: Policy file path must be specified via --policy or as a positional" + + " argument."); + return -1; + } + + Cel cel; + ImmutableSet transitiveFileDescriptors; + try { + CelBuilder celBuilder = + CelFactory.plannerCelBuilder() + .setOptions(CEL_OPTIONS) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .addCompilerLibraries( + CelOptionalLibrary.INSTANCE, + CelExtensions.bindings(), + CelExtensions.comprehensions()) + .addRuntimeLibraries(CelOptionalLibrary.INSTANCE); + + if (!transitiveDescriptorSetPath.isEmpty()) { + transitiveFileDescriptors = + CelDescriptorUtil.getFileDescriptorsFromFileDescriptorSet( + load(transitiveDescriptorSetPath)); + celBuilder.addFileTypes(transitiveFileDescriptors); + } else { + transitiveFileDescriptors = ImmutableSet.of(); + } + + cel = celBuilder.build(); + + CelEnvironmentYamlParser environmentYamlParser = CelEnvironmentYamlParser.newInstance(); + if (!baseConfigPath.isEmpty()) { + validatePolicyFileType(baseConfigPath, "base CEL environment"); + String baseYaml = new String(readFileBytes(baseConfigPath), UTF_8); + CelEnvironment baseEnv = environmentYamlParser.parse(baseYaml, baseConfigPath); + cel = baseEnv.extend(cel, CEL_OPTIONS); + } + + if (!configPath.isEmpty()) { + validatePolicyFileType(configPath, "CEL environment"); + String envYaml = new String(readFileBytes(configPath), UTF_8); + CelEnvironment env = environmentYamlParser.parse(envYaml, configPath); + cel = env.extend(cel, CEL_OPTIONS); + } + } catch (Exception e) { + err().printf("Failed to create a CEL compilation environment. Reason: %s%n", e.getMessage()); + return -1; + } + + CelPolicy policy; + try { + CelPolicyParser policyParser = + CelPolicyParserFactory.newYamlParserBuilder().enableSimpleVariables(true).build(); + String policyYaml = new String(readFileBytes(effectivePolicyPath), UTF_8); + policy = policyParser.parse(policyYaml, effectivePolicyPath); + } catch (Exception e) { + err() + .printf( + "Failed to parse CEL policy: [%s]. Reason: %s%n", + effectivePolicyPath, e.getMessage()); + return -1; + } + + try { + CelPolicyCompilerBuilder policyCompilerBuilder = + CelPolicyCompilerFactory.newPolicyCompiler(cel); + + if (optimizeFieldSelection) { + policyCompilerBuilder.setOptimizers( + ImmutableList.of( + ConstantFoldingOptimizer.getInstance(), + SubexpressionOptimizer.newInstance( + SubexpressionOptimizerOptions.newBuilder().populateMacroCalls(true).build()), + SelectOptimizer.newInstance( + SelectOptimizerOptions.newBuilder().build(), transitiveFileDescriptors))); + } + + CelPolicyCompiler policyCompiler = policyCompilerBuilder.build(); + CelAbstractSyntaxTree ast = policyCompiler.compile(policy); + + writeOutput(ast, output, outputFormat, outputVersion); + } catch (Exception e) { + err() + .printf( + "%nFailed to compile CEL policy: [%s].%nReason: %s%n%n", + effectivePolicyPath, e.getMessage()); + return -1; + } + + return 0; + } + + private void writeOutput( + CelAbstractSyntaxTree ast, String filePath, String format, String version) + throws IOException { + Message checkedExpr; + if (Ascii.equalsIgnoreCase("v1alpha1", version)) { + checkedExpr = CelProtoV1Alpha1AbstractSyntaxTree.fromCelAst(ast).toCheckedExpr(); + } else if (Ascii.equalsIgnoreCase("canonical", version)) { + checkedExpr = CelProtoAbstractSyntaxTree.fromCelAst(ast).toCheckedExpr(); + } else { + throw new IllegalArgumentException( + "Unsupported output version: " + version + ". Supported versions: canonical, v1alpha1"); + } + + boolean isText = + Ascii.equalsIgnoreCase("textpb", format) || Ascii.equalsIgnoreCase("textproto", format); + if (!isText && !Ascii.equalsIgnoreCase("binarypb", format)) { + throw new IllegalArgumentException( + "Unsupported output format: " + + format + + ". Supported formats: binarypb, textpb, textproto"); + } + + if (filePath.isEmpty() || filePath.equals("-")) { + if (isText) { + PrintWriter writer = out(); + TextFormat.printer().print(checkedExpr, writer); + writer.flush(); + } else { + checkedExpr.writeTo(System.out); + System.out.flush(); + } + } else { + Path path = Paths.get(filePath); + if (path.getParent() != null) { + Files.createDirectories(path.getParent()); + } + if (isText) { + try (BufferedWriter writer = Files.newBufferedWriter(path, UTF_8)) { + TextFormat.printer().print(checkedExpr, writer); + } + } else { + try (FileOutputStream outputStream = new FileOutputStream(path.toFile())) { + checkedExpr.writeTo(outputStream); + } + } + } + } + + private static void validatePolicyFileType(String path, String description) { + String lower = Ascii.toLowerCase(path.trim()); + if (!lower.endsWith(".yaml") && !lower.endsWith(".yml") && !lower.endsWith(".celpolicy")) { + throw new IllegalArgumentException( + String.format("Only YAML format is supported for %s. Got: %s", description, path)); + } + } + + private static FileDescriptorSet load(String descriptorSetPath) { + try { + byte[] descriptorBytes = readFileBytes(descriptorSetPath); + return FileDescriptorSet.parseFrom(descriptorBytes, ExtensionRegistry.getEmptyRegistry()); + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to load FileDescriptorSet from path: " + descriptorSetPath, e); + } + } + + private static byte[] readFileBytes(String path) throws IOException { + return Files.readAllBytes(Paths.get(path)); + } + + public static void main(String[] args) { + CelPolicyCompilerTool compilerTool = new CelPolicyCompilerTool(); + CommandLine cmd = new CommandLine(compilerTool); + cmd.setTrimQuotes(false); + int exitCode = cmd.execute(args); + System.exit(exitCode); + } + + CelPolicyCompilerTool() {} +} diff --git a/policy/src/test/java/dev/cel/policy/tools/BUILD.bazel b/policy/src/test/java/dev/cel/policy/tools/BUILD.bazel new file mode 100644 index 000000000..21f9f92f0 --- /dev/null +++ b/policy/src/test/java/dev/cel/policy/tools/BUILD.bazel @@ -0,0 +1,60 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("@rules_proto//proto:defs.bzl", "proto_descriptor_set") +load("//:testing.bzl", "junit4_test_suites") +load("//policy/tools:compile_cel_policy.bzl", "compile_cel_policy") + +package( + default_applicable_licenses = ["//:license"], + default_testonly = True, +) + +proto_descriptor_set( + name = "test_all_types_fds", + deps = ["@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto"], +) + +compile_cel_policy( + name = "compiled_test_policy", + config = "//testing/environment:proto3_message_variables", + optimize_field_selection = True, + policy = ":test_policy.yaml", + proto_srcs = ["@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto"], +) + +java_library( + name = "tests", + testonly = True, + srcs = glob(["*Test.java"]), + data = [ + ":compiled_test_policy.binarypb", + ":test_all_types_fds", + ":test_policy.yaml", + "//testing/environment:proto3_message_variables", + ], + deps = [ + "//:java_truth", + "//common:cel_ast", + "//common:cel_source", + "//common:proto_ast", + "//extensions:optional_library", + "//policy/tools", + "//runtime", + "@bazel_tools//tools/java/runfiles", + "@cel_spec//proto/cel/expr:checked_java_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:info_picocli_picocli", + "@maven//:junit_junit", + ], +) + +junit4_test_suites( + name = "test_suites", + sizes = [ + "small", + ], + src_dir = "src/test/java", + deps = [":tests"], +) diff --git a/policy/src/test/java/dev/cel/policy/tools/CelPolicyCompilerToolTest.java b/policy/src/test/java/dev/cel/policy/tools/CelPolicyCompilerToolTest.java new file mode 100644 index 000000000..5257bfa0b --- /dev/null +++ b/policy/src/test/java/dev/cel/policy/tools/CelPolicyCompilerToolTest.java @@ -0,0 +1,877 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// https://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 dev.cel.policy.tools; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; + +import dev.cel.expr.CheckedExpr; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Files; +import com.google.devtools.build.runfiles.AutoBazelRepository; +import com.google.devtools.build.runfiles.Runfiles; +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.TextFormat; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelProtoAbstractSyntaxTree; +import dev.cel.common.CelSource; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.extensions.CelOptionalLibrary; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Optional; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import picocli.CommandLine; + +@RunWith(JUnit4.class) +@AutoBazelRepository +public final class CelPolicyCompilerToolTest { + + @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); + + private Runfiles runfiles; + private CelRuntime celRuntime; + + @Before + public void setUp() throws Exception { + runfiles = + Runfiles.preload().withSourceRepository(AutoBazelRepository_CelPolicyCompilerToolTest.NAME); + celRuntime = + CelRuntimeFactory.plannerRuntimeBuilder() + .addLibraries(CelOptionalLibrary.INSTANCE) + .addMessageTypes(TestAllTypes.getDescriptor()) + .build(); + } + + private String resolveRunfile(String rlocationPath) throws IOException { + String resolvedPath = runfiles.rlocation(rlocationPath); + if (resolvedPath == null) { + throw new IOException("Unmapped runfile path: " + rlocationPath); + } + File file = new File(resolvedPath); + if (!file.exists()) { + throw new IOException( + String.format( + "Runfile not found on disk at '%s' (unresolved path: '%s')", + resolvedPath, rlocationPath)); + } + return resolvedPath; + } + + @Test + public void compile_basicPolicy_binarypb_success() throws Exception { + String configPath = + createFile("config.yaml", "name: test-env\nvariables:\n - name: age\n type: int\n"); + String policyPath = + createFile( + "policy.yaml", + "name: age-policy\n" + + "rule:\n" + + " match:\n" + + " - condition: age >= 18\n" + + " output: '\"adult\"'\n"); + + File outputFile = tempFolder.newFile("output.binarypb"); + + CommandLine cmd = new CommandLine(new CelPolicyCompilerTool()); + int exitCode = + cmd.execute( + "--policy", + policyPath, + "--config", + configPath, + "--output", + outputFile.getAbsolutePath(), + "--output_format", + "binarypb"); + + assertThat(exitCode).isEqualTo(0); + + CheckedExpr checkedExpr = + CheckedExpr.parseFrom(Files.toByteArray(outputFile), ExtensionRegistry.getEmptyRegistry()); + CelAbstractSyntaxTree ast = CelProtoAbstractSyntaxTree.fromCheckedExpr(checkedExpr).getAst(); + + Object result = celRuntime.createProgram(ast).eval(ImmutableMap.of("age", 25L)); + assertThat(result).isEqualTo(Optional.of("adult")); + } + + @Test + public void compile_textpb_format_success() throws Exception { + String configPath = + createFile("config.yaml", "name: test-env\nvariables:\n - name: user\n type: string\n"); + String policyPath = + createFile( + "policy.yaml", + "name: user-policy\n" + + "rule:\n" + + " match:\n" + + " - condition: user == \"alice\"\n" + + " output: 'true'\n"); + + File outputFile = tempFolder.newFile("output.textpb"); + + CommandLine cmd = new CommandLine(new CelPolicyCompilerTool()); + int exitCode = + cmd.execute( + "--policy", + policyPath, + "--config", + configPath, + "--output", + outputFile.getAbsolutePath(), + "--output_format", + "textpb"); + + assertThat(exitCode).isEqualTo(0); + + String content = Files.asCharSource(outputFile, UTF_8).read(); + assertThat(content).contains("call_expr"); + + CheckedExpr checkedExpr = TextFormat.parse(content, CheckedExpr.class); + CelAbstractSyntaxTree ast = CelProtoAbstractSyntaxTree.fromCheckedExpr(checkedExpr).getAst(); + Object result = celRuntime.createProgram(ast).eval(ImmutableMap.of("user", "alice")); + assertThat(result).isEqualTo(Optional.of(true)); + } + + @Test + public void compile_outputVersion_v1alpha1_success() throws Exception { + String configPath = + createFile("config.yaml", "name: test-env\nvariables:\n - name: x\n type: int\n"); + String policyPath = + createFile( + "policy.yaml", + "name: p\nrule:\n match:\n - condition: x > 0\n output: 'true'\n"); + + File outputFile = tempFolder.newFile("output.v1alpha1.binarypb"); + + CommandLine cmd = new CommandLine(new CelPolicyCompilerTool()); + int exitCode = + cmd.execute( + "--policy", + policyPath, + "--config", + configPath, + "--output", + outputFile.getAbsolutePath(), + "--output_version", + "v1alpha1", + "--output_format", + "binarypb"); + + assertThat(exitCode).isEqualTo(0); + + com.google.api.expr.v1alpha1.CheckedExpr v1alpha1Expr = + com.google.api.expr.v1alpha1.CheckedExpr.parseFrom( + Files.toByteArray(outputFile), ExtensionRegistry.getEmptyRegistry()); + assertThat(v1alpha1Expr.hasExpr()).isTrue(); + } + + @Test + public void compile_withBaseConfig_success() throws Exception { + String baseConfigPath = + createFile( + "base_config.yaml", + "name: base-env\nvariables:\n - name: base_var\n type: string\n"); + String configPath = + createFile( + "config.yaml", "name: sub-env\nvariables:\n - name: sub_var\n type: string\n"); + String policyPath = + createFile( + "policy.yaml", + "name: p\n" + + "rule:\n" + + " match:\n" + + " - condition: base_var == \"hello\" && sub_var == \"world\"\n" + + " output: 'true'\n"); + + File outputFile = tempFolder.newFile("output.binarypb"); + + CommandLine cmd = new CommandLine(new CelPolicyCompilerTool()); + int exitCode = + cmd.execute( + "--policy", + policyPath, + "--base_config", + baseConfigPath, + "--config", + configPath, + "--output", + outputFile.getAbsolutePath()); + + assertThat(exitCode).isEqualTo(0); + + CheckedExpr checkedExpr = + CheckedExpr.parseFrom(Files.toByteArray(outputFile), ExtensionRegistry.getEmptyRegistry()); + CelAbstractSyntaxTree ast = CelProtoAbstractSyntaxTree.fromCheckedExpr(checkedExpr).getAst(); + Object result = + celRuntime + .createProgram(ast) + .eval(ImmutableMap.of("base_var", "hello", "sub_var", "world")); + assertThat(result).isEqualTo(Optional.of(true)); + } + + @Test + public void compile_withSimpleVariables_success() throws Exception { + String configPath = createFile("config.yaml", "name: test-env\n"); + String policyPath = + createFile( + "policy.yaml", + "name: p\n" + + "rule:\n" + + " variables:\n" + + " - my_sum: 10 + 20\n" + + " match:\n" + + " - condition: variables.my_sum == 30\n" + + " output: 'true'\n"); + + File outputFile = tempFolder.newFile("output.binarypb"); + + CommandLine cmd = new CommandLine(new CelPolicyCompilerTool()); + int exitCode = + cmd.execute( + "--policy", + policyPath, + "--config", + configPath, + "--output", + outputFile.getAbsolutePath()); + + assertThat(exitCode).isEqualTo(0); + + CheckedExpr checkedExpr = + CheckedExpr.parseFrom(Files.toByteArray(outputFile), ExtensionRegistry.getEmptyRegistry()); + CelAbstractSyntaxTree ast = CelProtoAbstractSyntaxTree.fromCheckedExpr(checkedExpr).getAst(); + Object result = celRuntime.createProgram(ast).eval(); + assertThat(result).isEqualTo(Optional.of(true)); + } + + @Test + public void compile_withOptimizeFieldSelection_rewritesSelectAndEvaluates() throws Exception { + String configRlocation = + "cel_java/testing/src/test/resources/environment/proto3_message_variables.yaml"; + String fdsRlocation = + "cel_java/policy/src/test/java/dev/cel/policy/tools/test_all_types_fds.pb"; + + String configPath = resolveRunfile(configRlocation); + String fdsPath = resolveRunfile(fdsRlocation); + + String policyPath = + createFile( + "proto_policy.yaml", + "name: proto-policy\n" + + "rule:\n" + + " match:\n" + + " - condition: proto3.single_int32 == 1\n" + + " output: '\"OK\"'\n"); + + File outputFile = tempFolder.newFile("output_optimized.binarypb"); + + CommandLine cmd = new CommandLine(new CelPolicyCompilerTool()); + int exitCode = + cmd.execute( + "--policy", + policyPath, + "--config", + configPath, + "--transitive_descriptor_set", + fdsPath, + "--output", + outputFile.getAbsolutePath(), + "--optimize_field_selection"); + + assertThat(exitCode).isEqualTo(0); + + CheckedExpr checkedExpr = + CheckedExpr.parseFrom(Files.toByteArray(outputFile), ExtensionRegistry.getEmptyRegistry()); + CelAbstractSyntaxTree ast = CelProtoAbstractSyntaxTree.fromCheckedExpr(checkedExpr).getAst(); + + // Verify Extension tag "select_optimization" is attached to source info + assertThat(ast.getSource().getExtensions()) + .contains( + CelSource.Extension.create( + "select_optimization", + CelSource.Extension.Version.of(1L, 0L), + CelSource.Extension.Component.COMPONENT_RUNTIME)); + + // Verify AST was rewritten to call cel.@attribute + String unparsedText = checkedExpr.toString(); + assertThat(unparsedText).contains("cel.@attribute"); + + // Verify end-to-end evaluation with the planner runtime + Object matched = + celRuntime + .createProgram(ast) + .eval(ImmutableMap.of("proto3", TestAllTypes.newBuilder().setSingleInt32(1).build())); + assertThat(matched).isEqualTo(Optional.of("OK")); + + Object unmatched = + celRuntime + .createProgram(ast) + .eval(ImmutableMap.of("proto3", TestAllTypes.newBuilder().setSingleInt32(2).build())); + assertThat(unmatched).isEqualTo(Optional.empty()); + } + + @Test + public void compile_presenceTest_withOptimizeFieldSelection_rewritesHasFieldAndEvaluates() + throws Exception { + String configRlocation = + "cel_java/testing/src/test/resources/environment/proto3_message_variables.yaml"; + String fdsRlocation = + "cel_java/policy/src/test/java/dev/cel/policy/tools/test_all_types_fds.pb"; + + String configPath = resolveRunfile(configRlocation); + String fdsPath = resolveRunfile(fdsRlocation); + + String policyPath = + createFile( + "presence_policy.yaml", + "name: presence-policy\n" + + "rule:\n" + + " match:\n" + + " - condition: has(proto3.single_int32)\n" + + " output: '\"EXISTS\"'\n"); + + File outputFile = tempFolder.newFile("output_presence.binarypb"); + + CommandLine cmd = new CommandLine(new CelPolicyCompilerTool()); + int exitCode = + cmd.execute( + "--policy", + policyPath, + "--config", + configPath, + "--transitive_descriptor_set", + fdsPath, + "--output", + outputFile.getAbsolutePath(), + "--optimize_field_selection"); + + assertThat(exitCode).isEqualTo(0); + + CheckedExpr checkedExpr = + CheckedExpr.parseFrom(Files.toByteArray(outputFile), ExtensionRegistry.getEmptyRegistry()); + CelAbstractSyntaxTree ast = CelProtoAbstractSyntaxTree.fromCheckedExpr(checkedExpr).getAst(); + + assertThat(ast.getSource().getExtensions()) + .contains( + CelSource.Extension.create( + "select_optimization", + CelSource.Extension.Version.of(1L, 0L), + CelSource.Extension.Component.COMPONENT_RUNTIME)); + + assertThat(checkedExpr.toString()).contains("cel.@hasField"); + + Object present = + celRuntime + .createProgram(ast) + .eval(ImmutableMap.of("proto3", TestAllTypes.newBuilder().setSingleInt32(42).build())); + assertThat(present).isEqualTo(Optional.of("EXISTS")); + + Object absent = + celRuntime + .createProgram(ast) + .eval(ImmutableMap.of("proto3", TestAllTypes.getDefaultInstance())); + assertThat(absent).isEqualTo(Optional.empty()); + } + + @Test + public void compile_policyWithCelBind_success() throws Exception { + String configRlocation = + "cel_java/testing/src/test/resources/environment/proto3_message_variables.yaml"; + String fdsRlocation = + "cel_java/policy/src/test/java/dev/cel/policy/tools/test_all_types_fds.pb"; + + String configPath = resolveRunfile(configRlocation); + String fdsPath = resolveRunfile(fdsRlocation); + + String policyPath = + createFile( + "bind_policy.yaml", + "name: bind-policy\n" + + "rule:\n" + + " match:\n" + + " - condition: >\n" + + " cel.bind(\n" + + " val,\n" + + " proto3.single_int32,\n" + + " val == 1 || val == 2\n" + + " )\n" + + " output: '\"MATCH\"'\n"); + + File outputFile = tempFolder.newFile("output_bind.binarypb"); + + CommandLine cmd = new CommandLine(new CelPolicyCompilerTool()); + int exitCode = + cmd.execute( + "--policy", + policyPath, + "--config", + configPath, + "--transitive_descriptor_set", + fdsPath, + "--output", + outputFile.getAbsolutePath(), + "--optimize_field_selection"); + + assertThat(exitCode).isEqualTo(0); + + CheckedExpr checkedExpr = + CheckedExpr.parseFrom(Files.toByteArray(outputFile), ExtensionRegistry.getEmptyRegistry()); + CelAbstractSyntaxTree ast = CelProtoAbstractSyntaxTree.fromCheckedExpr(checkedExpr).getAst(); + + assertThat(ast.getSource().getExtensions()) + .contains( + CelSource.Extension.create( + "select_optimization", + CelSource.Extension.Version.of(1L, 0L), + CelSource.Extension.Component.COMPONENT_RUNTIME)); + + Object result = + celRuntime + .createProgram(ast) + .eval(ImmutableMap.of("proto3", TestAllTypes.newBuilder().setSingleInt32(2).build())); + assertThat(result).isEqualTo(Optional.of("MATCH")); + } + + @Test + public void compile_macroTarget_verifiedAndEvaluated() throws Exception { + String macroArtifactRlocation = + "cel_java/policy/src/test/java/dev/cel/policy/tools/compiled_test_policy.binarypb"; + File compiledFile = new File(resolveRunfile(macroArtifactRlocation)); + assertThat(compiledFile.exists()).isTrue(); + + CheckedExpr checkedExpr = + CheckedExpr.parseFrom( + Files.toByteArray(compiledFile), ExtensionRegistry.getEmptyRegistry()); + CelAbstractSyntaxTree ast = CelProtoAbstractSyntaxTree.fromCheckedExpr(checkedExpr).getAst(); + + assertThat(ast.getSource().getExtensions()) + .contains( + CelSource.Extension.create( + "select_optimization", + CelSource.Extension.Version.of(1L, 0L), + CelSource.Extension.Component.COMPONENT_RUNTIME)); + + assertThat(checkedExpr.toString()).contains("cel.@attribute"); + assertThat(checkedExpr.toString()).contains("cel.@hasField"); + + TestAllTypes matchingProto = + TestAllTypes.newBuilder() + .setSingleInt32(100) + .setSingleNestedMessage(TestAllTypes.NestedMessage.newBuilder().setBb(1)) + .build(); + assertThat(celRuntime.createProgram(ast).eval(ImmutableMap.of("proto3", matchingProto))) + .isEqualTo("ALLOW"); + + TestAllTypes nonMatchingProto = TestAllTypes.newBuilder().setSingleInt32(100).build(); + assertThat(celRuntime.createProgram(ast).eval(ImmutableMap.of("proto3", nonMatchingProto))) + .isEqualTo("DENY"); + } + + @Test + public void compile_error_missingPolicy() throws Exception { + String stdErr = executeExpectingError("--config", "foo.yaml", "--output", "out.binarypb"); + + assertThat(stdErr) + .contains( + "Error: Policy file path must be specified via --policy or as a positional argument."); + } + + @Test + public void compile_error_invalidPolicyYaml() throws Exception { + String configPath = createFile("config.yaml", "name: test-env\n"); + String policyPath = createFile("bad_policy.yaml", "not a valid yaml: [unclosed list\n"); + File outputFile = tempFolder.newFile("output.binarypb"); + + String stdErr = + executeExpectingError( + "--policy", + policyPath, + "--config", + configPath, + "--output", + outputFile.getAbsolutePath()); + + assertThat(stdErr).contains("Failed to parse CEL policy: [" + policyPath); + } + + @Test + public void compile_stdout_textpb_format_success() throws Exception { + String configPath = + createFile("config.yaml", "name: test-env\nvariables:\n - name: user\n type: string\n"); + String policyPath = + createFile( + "policy.yaml", + "name: user-policy\n" + + "rule:\n" + + " match:\n" + + " - condition: user == \"alice\"\n" + + " output: 'true'\n"); + + StringWriter out = new StringWriter(); + CommandLine cmd = new CommandLine(new CelPolicyCompilerTool()); + cmd.setOut(new PrintWriter(out)); + int exitCode = + cmd.execute("--policy", policyPath, "--config", configPath, "--output_format", "textpb"); + assertThat(exitCode).isEqualTo(0); + assertThat(out.toString()).contains("call_expr"); + } + + @Test + public void compile_stdout_textproto_withDashOutput_success() throws Exception { + String configPath = + createFile("config.yaml", "name: test-env\nvariables:\n - name: user\n type: string\n"); + String policyPath = + createFile( + "policy.yaml", + "name: user-policy\n" + + "rule:\n" + + " match:\n" + + " - condition: user == \"alice\"\n" + + " output: 'true'\n"); + + StringWriter out = new StringWriter(); + CommandLine cmd = new CommandLine(new CelPolicyCompilerTool()); + cmd.setOut(new PrintWriter(out)); + int exitCode = + cmd.execute( + "--policy", + policyPath, + "--config", + configPath, + "--output", + "-", + "--output_format", + "textproto"); + assertThat(exitCode).isEqualTo(0); + assertThat(out.toString()).contains("call_expr"); + } + + @Test + public void compile_stdout_binarypb_withDashOutput_success() throws Exception { + String configPath = + createFile("config.yaml", "name: test-env\nvariables:\n - name: age\n type: int\n"); + String policyPath = + createFile( + "policy.yaml", + "name: age-policy\n" + + "rule:\n" + + " match:\n" + + " - condition: age >= 18\n" + + " output: '\"adult\"'\n"); + + PrintStream originalOut = System.out; + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + try { + System.setOut(new PrintStream(outContent, true, UTF_8.name())); + CommandLine cmd = new CommandLine(new CelPolicyCompilerTool()); + int exitCode = + cmd.execute( + "--policy", + policyPath, + "--config", + configPath, + "--output", + "-", + "--output_format", + "binarypb"); + assertThat(exitCode).isEqualTo(0); + } finally { + System.setOut(originalOut); + } + + CheckedExpr checkedExpr = + CheckedExpr.parseFrom(outContent.toByteArray(), ExtensionRegistry.getEmptyRegistry()); + assertThat(checkedExpr.hasExpr()).isTrue(); + } + + @Test + public void compile_stdout_defaultOmittedOutput_binarypb_success() throws Exception { + String configPath = + createFile("config.yaml", "name: test-env\nvariables:\n - name: age\n type: int\n"); + String policyPath = + createFile( + "policy.yaml", + "name: age-policy\n" + + "rule:\n" + + " match:\n" + + " - condition: age >= 18\n" + + " output: '\"adult\"'\n"); + + PrintStream originalOut = System.out; + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + try { + System.setOut(new PrintStream(outContent, true, UTF_8.name())); + CommandLine cmd = new CommandLine(new CelPolicyCompilerTool()); + int exitCode = cmd.execute("--policy", policyPath, "--config", configPath); + assertThat(exitCode).isEqualTo(0); + } finally { + System.setOut(originalOut); + } + + CheckedExpr checkedExpr = + CheckedExpr.parseFrom(outContent.toByteArray(), ExtensionRegistry.getEmptyRegistry()); + assertThat(checkedExpr.hasExpr()).isTrue(); + } + + @Test + public void compile_withPositionalPolicyPath_success() throws Exception { + String configPath = + createFile("config.yaml", "name: test-env\nvariables:\n - name: age\n type: int\n"); + String policyPath = + createFile( + "policy.yaml", + "name: age-policy\n" + + "rule:\n" + + " match:\n" + + " - condition: age >= 18\n" + + " output: '\"adult\"'\n"); + + File outputFile = tempFolder.newFile("output.binarypb"); + + CommandLine cmd = new CommandLine(new CelPolicyCompilerTool()); + int exitCode = + cmd.execute(policyPath, "--config", configPath, "--output", outputFile.getAbsolutePath()); + + assertThat(exitCode).isEqualTo(0); + + CheckedExpr checkedExpr = + CheckedExpr.parseFrom(Files.toByteArray(outputFile), ExtensionRegistry.getEmptyRegistry()); + assertThat(checkedExpr.hasExpr()).isTrue(); + } + + @Test + public void compile_withNestedOutputDirectory_createsDirectories_success() throws Exception { + String configPath = + createFile("config.yaml", "name: test-env\nvariables:\n - name: age\n type: int\n"); + String policyPath = + createFile( + "policy.yaml", + "name: age-policy\n" + + "rule:\n" + + " match:\n" + + " - condition: age >= 18\n" + + " output: '\"adult\"'\n"); + + File nestedOutputFile = new File(tempFolder.getRoot(), "sub/nested/dir/output.binarypb"); + + CommandLine cmd = new CommandLine(new CelPolicyCompilerTool()); + int exitCode = + cmd.execute( + "--policy", + policyPath, + "--config", + configPath, + "--output", + nestedOutputFile.getAbsolutePath()); + + assertThat(exitCode).isEqualTo(0); + assertThat(nestedOutputFile.exists()).isTrue(); + } + + @Test + public void compile_withYmlConfigExtension_success() throws Exception { + String configPath = + createFile("config.yml", "name: test-env\nvariables:\n - name: age\n type: int\n"); + String policyPath = + createFile( + "policy.yaml", + "name: age-policy\n" + + "rule:\n" + + " match:\n" + + " - condition: age >= 18\n" + + " output: '\"adult\"'\n"); + + File outputFile = tempFolder.newFile("output.binarypb"); + + CommandLine cmd = new CommandLine(new CelPolicyCompilerTool()); + int exitCode = + cmd.execute( + "--policy", + policyPath, + "--config", + configPath, + "--output", + outputFile.getAbsolutePath()); + + assertThat(exitCode).isEqualTo(0); + } + + @Test + public void compile_error_unsupportedOutputVersion() throws Exception { + String configPath = createFile("config.yaml", "name: test-env\n"); + String policyPath = + createFile( + "policy.yaml", + "name: p\nrule:\n match:\n - condition: 'true'\n output: 'true'\n"); + + String stdErr = + executeExpectingError( + "--policy", + policyPath, + "--config", + configPath, + "--output_version", + "unsupported_version"); + + assertThat(stdErr) + .contains( + "Unsupported output version: unsupported_version. Supported versions: canonical," + + " v1alpha1"); + } + + @Test + public void compile_error_unsupportedOutputFormat() throws Exception { + String configPath = createFile("config.yaml", "name: test-env\n"); + String policyPath = + createFile( + "policy.yaml", + "name: p\nrule:\n match:\n - condition: 'true'\n output: 'true'\n"); + + String stdErr = + executeExpectingError( + "--policy", policyPath, "--config", configPath, "--output_format", "json"); + + assertThat(stdErr) + .contains( + "Unsupported output format: json. Supported formats: binarypb, textpb, textproto"); + } + + @Test + public void compile_error_invalidConfigExtension() throws Exception { + String configPath = createFile("config.json", "{\"name\": \"test-env\"}"); + String policyPath = + createFile( + "policy.yaml", + "name: p\nrule:\n match:\n - condition: true\n output: 'true'\n"); + + String stdErr = executeExpectingError("--policy", policyPath, "--config", configPath); + + assertThat(stdErr) + .contains( + "Failed to create a CEL compilation environment. Reason: Only YAML format is" + + " supported for CEL environment."); + } + + @Test + public void compile_error_invalidBaseConfigExtension() throws Exception { + String baseConfigPath = createFile("base_config.json", "{\"name\": \"base-env\"}"); + String configPath = createFile("config.yaml", "name: test-env\n"); + String policyPath = + createFile( + "policy.yaml", + "name: p\nrule:\n match:\n - condition: true\n output: 'true'\n"); + + String stdErr = + executeExpectingError( + "--policy", policyPath, "--base_config", baseConfigPath, "--config", configPath); + + assertThat(stdErr) + .contains( + "Failed to create a CEL compilation environment. Reason: Only YAML format is" + + " supported for base CEL environment."); + } + + @Test + public void compile_error_policyCompilationFailure() throws Exception { + String configPath = createFile("config.yaml", "name: test-env\n"); + String policyPath = + createFile( + "undeclared_policy.yaml", + "name: undeclared-policy\n" + + "rule:\n" + + " match:\n" + + " - condition: undeclared_identifier == 42\n" + + " output: 'true'\n"); + + String stdErr = executeExpectingError("--policy", policyPath, "--config", configPath); + + assertThat(stdErr).contains("Failed to compile CEL policy: [" + policyPath); + assertThat(stdErr).contains("undeclared reference to 'undeclared_identifier'"); + } + + @Test + public void compile_error_nonExistentPolicyFile() throws Exception { + String configPath = createFile("config.yaml", "name: test-env\n"); + + String stdErr = + executeExpectingError("--policy", "non_existent_policy.yaml", "--config", configPath); + + assertThat(stdErr).contains("Failed to parse CEL policy: [non_existent_policy.yaml]"); + } + + @Test + public void compile_error_nonExistentConfigFile() throws Exception { + String policyPath = + createFile( + "policy.yaml", + "name: p\nrule:\n match:\n - condition: true\n output: 'true'\n"); + + String stdErr = + executeExpectingError("--policy", policyPath, "--config", "non_existent_config.yaml"); + + assertThat(stdErr).contains("Failed to create a CEL compilation environment."); + assertThat(stdErr).contains("non_existent_config.yaml"); + } + + @Test + public void compile_error_nonExistentDescriptorSet() throws Exception { + String configPath = createFile("config.yaml", "name: test-env\n"); + String policyPath = + createFile( + "policy.yaml", + "name: p\nrule:\n match:\n - condition: true\n output: 'true'\n"); + + String stdErr = + executeExpectingError( + "--policy", + policyPath, + "--config", + configPath, + "--transitive_descriptor_set", + "non_existent_descriptors.pb"); + + assertThat(stdErr) + .contains("Failed to load FileDescriptorSet from path: non_existent_descriptors.pb"); + } + + private String createFile(String fileName, String content) throws IOException { + File file = tempFolder.newFile(fileName); + Files.asCharSink(file, UTF_8).write(content); + return file.getAbsolutePath(); + } + + private static String executeExpectingError(String... args) { + StringWriter out = new StringWriter(); + PrintWriter pw = new PrintWriter(out); + CommandLine cmd = new CommandLine(new CelPolicyCompilerTool()); + cmd.setOut(pw); + cmd.setErr(pw); + int exitCode = cmd.execute(args); + assertThat(exitCode).isEqualTo(-1); + return out.toString(); + } +} diff --git a/policy/src/test/java/dev/cel/policy/tools/test_policy.yaml b/policy/src/test/java/dev/cel/policy/tools/test_policy.yaml new file mode 100644 index 000000000..afed6485d --- /dev/null +++ b/policy/src/test/java/dev/cel/policy/tools/test_policy.yaml @@ -0,0 +1,7 @@ +# Copyright 2026 Google LLC +name: "test_policy" +rule: + match: + - condition: proto3.single_int32 == 100 && has(proto3.single_nested_message) + output: '"ALLOW"' + - output: '"DENY"' diff --git a/policy/tools/BUILD.bazel b/policy/tools/BUILD.bazel new file mode 100644 index 000000000..3596d7c23 --- /dev/null +++ b/policy/tools/BUILD.bazel @@ -0,0 +1,27 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//visibility:public"], +) + +exports_files(["compile_cel_policy.bzl"]) + +java_library( + name = "tools", + visibility = ["//:internal"], + exports = ["//policy/src/main/java/dev/cel/policy/tools"], +) + +alias( + name = "cel_policy_compiler_tool", + actual = "//policy/src/main/java/dev/cel/policy/tools:cel_policy_compiler_tool", +) + +bzl_library( + name = "compile_cel_policy_bzl", + srcs = ["compile_cel_policy.bzl"], + visibility = ["//visibility:private"], + deps = ["@rules_proto//proto:defs"], +) diff --git a/policy/tools/compile_cel_policy.bzl b/policy/tools/compile_cel_policy.bzl new file mode 100644 index 000000000..2f8b42301 --- /dev/null +++ b/policy/tools/compile_cel_policy.bzl @@ -0,0 +1,102 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://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. +"""Rule for compiling CEL policies at build time.""" + +load("@rules_proto//proto:defs.bzl", "proto_descriptor_set") + +def compile_cel_policy( + name, + policy, + config, + base_config = None, + proto_srcs = [], + file_descriptor_set = None, + output = None, + output_format = "binarypb", + output_version = "canonical", + optimize_field_selection = False, + visibility = None): + """Compiles a CEL policy into a CheckedExpr binarypb or textpb with optional select optimization. + + This macro wraps an invocation of cel_policy_compiler_tool with a genrule. The rule output + will be a CheckedExpr message in the requested version (canonical or v1alpha1) and format + (binarypb, textpb, or textproto). + + Args: + name: str name for the generated artifact + policy: label of a file describing a CEL policy (.celpolicy or .yaml) + config: label of a file describing the CEL policy environment in YAML + base_config: (optional) label of a file describing the base environment configuration in YAML + proto_srcs: (optional) list of str label(s) pointing to proto_library rule(s) + file_descriptor_set: (optional) str label or filename pointing to a FileDescriptorSet message + output: (optional) str file name for the output checked expression (default derived from label name and format) + output_format: (optional) str either "binarypb", "textpb", or "textproto" (default "binarypb") + output_version: (optional) str either "canonical" or "v1alpha1" (default "canonical") + optimize_field_selection: (optional) bool whether to enable AST select optimization (default False) + visibility: (optional) visibility to use on the genrule macro (default None) + """ + if output_format not in ("binarypb", "textpb", "textproto"): + fail("output_format only supports 'binarypb', 'textpb', and 'textproto'") + + if output_version not in ("canonical", "v1alpha1"): + fail("output_version only supports 'canonical' and 'v1alpha1'") + + if output == None: + output = name + "." + output_format + + args = [] + srcs = [policy, config] + + args.append("--policy=$(location %s)" % policy) + args.append("--config=$(location %s)" % config) + + if base_config != None: + args.append("--base_config=$(location %s)" % base_config) + srcs.append(base_config) + + if len(proto_srcs) > 0 and file_descriptor_set != None: + fail("Cannot specify both proto_srcs and file_descriptor_set in compile_cel_policy") + + if len(proto_srcs) > 0: + transitive_descriptor_set_name = "%s_transitive_descriptor_set" % name + proto_descriptor_set( + name = transitive_descriptor_set_name, + deps = proto_srcs, + ) + file_descriptor_set = transitive_descriptor_set_name + + if file_descriptor_set != None: + args.append("--file_descriptor_set=$(location %s)" % file_descriptor_set) + srcs.append(file_descriptor_set) + + args.append("--output=$(location %s)" % output) + args.append("--output_format=" + output_format) + args.append("--output_version=" + output_version) + + if optimize_field_selection: + args.append("--optimize_field_selection") + + cmd = ( + "$(location //policy/tools:cel_policy_compiler_tool) " + + " ".join(args) + ) + + native.genrule( + name = name, + cmd = cmd, + srcs = srcs, + outs = [output], + tools = ["//policy/tools:cel_policy_compiler_tool"], + visibility = visibility, + )