diff --git a/.bazelignore b/.bazelignore new file mode 100644 index 0000000..b726568 --- /dev/null +++ b/.bazelignore @@ -0,0 +1 @@ +examples \ No newline at end of file diff --git a/.github/actions/build-test/action.yaml b/.github/actions/build-test/action.yaml index 22e004e..2d75d3f 100644 --- a/.github/actions/build-test/action.yaml +++ b/.github/actions/build-test/action.yaml @@ -25,3 +25,15 @@ runs: --test_output=errors \ //... shell: bash + - name: Build bzlmod example + run: | + cd examples/bzlmod + bazel build //... + shell: bash + - name: Build and check gazelle example + run: | + cd examples/gazelle + bazel run //:gazelle_check + bazel run //:gazelle + bazel build //... + shell: bash diff --git a/MODULE.bazel b/MODULE.bazel index eccee37..e1901fc 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -22,4 +22,12 @@ use_repo( register_toolchains("@cue_tool_toolchains//:all") -bazel_dep(name = "gazelle", version = "0.44.0", dev_dependency = True) +bazel_dep(name = "gazelle", version = "0.47.0") + +go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps") +go_deps.from_file(go_mod = "//:go.mod") +use_repo( + go_deps, + "com_github_iancoleman_strcase", + "org_cuelang_go", +) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index af51775..dc1f8f6 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -39,8 +39,8 @@ "https://bcr.bazel.build/modules/gazelle/0.33.0/MODULE.bazel": "a13a0f279b462b784fb8dd52a4074526c4a2afe70e114c7d09066097a46b3350", "https://bcr.bazel.build/modules/gazelle/0.34.0/MODULE.bazel": "abdd8ce4d70978933209db92e436deb3a8b737859e9354fb5fd11fb5c2004c8a", "https://bcr.bazel.build/modules/gazelle/0.36.0/MODULE.bazel": "e375d5d6e9a6ca59b0cb38b0540bc9a05b6aa926d322f2de268ad267a2ee74c0", - "https://bcr.bazel.build/modules/gazelle/0.44.0/MODULE.bazel": "fd3177ca0938da57a1e416cad3f39b9c4334defbc717e89aba9d9ddbbb0341da", - "https://bcr.bazel.build/modules/gazelle/0.44.0/source.json": "7fb65ef9c1ce470d099ca27fd478673d9d64c844af28d0d472b0874c7d590cb6", + "https://bcr.bazel.build/modules/gazelle/0.47.0/MODULE.bazel": "b61bb007c4efad134aa30ee7f4a8e2a39b22aa5685f005edaa022fbd1de43ebc", + "https://bcr.bazel.build/modules/gazelle/0.47.0/source.json": "aeb2e5df14b7fb298625d75d08b9c65bdb0b56014c5eb89da9e5dd0572280ae6", "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb", "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4", "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6", @@ -49,8 +49,8 @@ "https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075", "https://bcr.bazel.build/modules/jsoncpp/1.9.5/source.json": "4108ee5085dd2885a341c7fab149429db457b3169b86eb081fa245eadf69169d", "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", - "https://bcr.bazel.build/modules/package_metadata/0.0.2/MODULE.bazel": "fb8d25550742674d63d7b250063d4580ca530499f045d70748b1b142081ebb92", - "https://bcr.bazel.build/modules/package_metadata/0.0.2/source.json": "e53a759a72488d2c0576f57491ef2da0cf4aab05ac0997314012495935531b73", + "https://bcr.bazel.build/modules/package_metadata/0.0.5/MODULE.bazel": "ef4f9439e3270fdd6b9fd4dbc3d2f29d13888e44c529a1b243f7a31dfbc2e8e4", + "https://bcr.bazel.build/modules/package_metadata/0.0.5/source.json": "2326db2f6592578177751c3e1f74786b79382cd6008834c9d01ec865b9126a85", "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", "https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f", "https://bcr.bazel.build/modules/platforms/0.0.11/source.json": "f7e188b79ebedebfe75e9e1d098b8845226c7992b307e28e1496f23112e8fc29", @@ -81,6 +81,7 @@ "https://bcr.bazel.build/modules/rules_cc/0.0.14/MODULE.bazel": "5e343a3aac88b8d7af3b1b6d2093b55c347b8eefc2e7d1442f7a02dc8fea48ac", "https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc", "https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87", + "https://bcr.bazel.build/modules/rules_cc/0.0.17/MODULE.bazel": "2ae1d8f4238ec67d7185d8861cb0a2cdf4bc608697c331b95bf990e69b62e64a", "https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c", "https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f", "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e", @@ -93,7 +94,7 @@ "https://bcr.bazel.build/modules/rules_go/0.41.0/MODULE.bazel": "55861d8e8bb0e62cbd2896f60ff303f62ffcb0eddb74ecb0e5c0cbe36fc292c8", "https://bcr.bazel.build/modules/rules_go/0.42.0/MODULE.bazel": "8cfa875b9aa8c6fce2b2e5925e73c1388173ea3c32a0db4d2b4804b453c14270", "https://bcr.bazel.build/modules/rules_go/0.46.0/MODULE.bazel": "3477df8bdcc49e698b9d25f734c4f3a9f5931ff34ee48a2c662be168f5f2d3fd", - "https://bcr.bazel.build/modules/rules_go/0.51.0/MODULE.bazel": "b6920f505935bfd69381651c942496d99b16e2a12f3dd5263b90ded16f3b4d0f", + "https://bcr.bazel.build/modules/rules_go/0.53.0/MODULE.bazel": "a4ed760d3ac0dbc0d7b967631a9a3fd9100d28f7d9fcf214b4df87d4bfff5f9a", "https://bcr.bazel.build/modules/rules_go/0.55.1/MODULE.bazel": "a57a6fc59a74326c0b440d07cca209edf13c7d1a641e48cfbeab56e79f873609", "https://bcr.bazel.build/modules/rules_go/0.55.1/source.json": "827a740c8959c9d20616889e7746cde4dcc6ee80d25146943627ccea0736328f", "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", diff --git a/examples/gazelle/.bazelversion b/examples/gazelle/.bazelversion new file mode 100644 index 0000000..f18e4ab --- /dev/null +++ b/examples/gazelle/.bazelversion @@ -0,0 +1,2 @@ +8.3.0 + diff --git a/examples/gazelle/.gitignore b/examples/gazelle/.gitignore new file mode 100644 index 0000000..5acf2af --- /dev/null +++ b/examples/gazelle/.gitignore @@ -0,0 +1,9 @@ +# Bazel build directories +bazel-* + +# Bazel convenience symlinks +bazel-bin +bazel-out +bazel-testlogs +bazel-gazelle + diff --git a/examples/gazelle/BUILD.bazel b/examples/gazelle/BUILD.bazel new file mode 100644 index 0000000..a6dc63e --- /dev/null +++ b/examples/gazelle/BUILD.bazel @@ -0,0 +1,32 @@ +load("@gazelle//:def.bzl", "gazelle") + +# gazelle:prefix github.com/abcue/rules_cue/examples/gazelle + +# Run this to update BUILD.bazel files: +# bazel run //:gazelle +gazelle( + name = "gazelle", + gazelle = "@rules_cue//gazelle:gazelle_binary", +) + +# Run this to verify BUILD.bazel files are up to date: +# bazel run //:gazelle_check +gazelle( + name = "gazelle_check", + command = "fix", + extra_args = ["-mode=diff"], + gazelle = "@rules_cue//gazelle:gazelle_binary", +) + +# Run this to update go dependencies: +# bazel run //:gazelle_update_repos +gazelle( + name = "gazelle_update_repos", + args = [ + "-from_file=go.mod", + "-to_macro=deps.bzl%go_dependencies", + "-prune", + ], + command = "update-repos", + gazelle = "@rules_cue//gazelle:gazelle_binary", +) diff --git a/examples/gazelle/MODULE.bazel b/examples/gazelle/MODULE.bazel new file mode 100644 index 0000000..6f11626 --- /dev/null +++ b/examples/gazelle/MODULE.bazel @@ -0,0 +1,23 @@ +module( + name = "rules_cue_examples_gazelle", + version = "0.0.0", +) + +bazel_dep(name = "bazel_skylib", version = "1.7.1") +bazel_dep(name = "rules_cue", version = "0.0.0") +bazel_dep(name = "gazelle", version = "0.47.0") + +# Override rules_cue to use the local version +local_path_override( + module_name = "rules_cue", + path = "../../", +) + +# Set up Go dependencies for Gazelle CUE extension +go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps") +go_deps.from_file(go_mod = "@rules_cue//:go.mod") +use_repo( + go_deps, + "com_github_iancoleman_strcase", + "org_cuelang_go", +) diff --git a/examples/gazelle/MODULE.bazel.lock b/examples/gazelle/MODULE.bazel.lock new file mode 100644 index 0000000..d9e2e86 --- /dev/null +++ b/examples/gazelle/MODULE.bazel.lock @@ -0,0 +1,252 @@ +{ + "lockFileVersion": 18, + "registryFileHashes": { + "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497", + "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2", + "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589", + "https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0", + "https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb", + "https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16", + "https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915", + "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed", + "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/source.json": "9be551b8d4e3ef76875c0d744b5d6a504a27e3ae67bc6b28f46415fd2d2957da", + "https://bcr.bazel.build/modules/bazel_features/1.1.0/MODULE.bazel": "cfd42ff3b815a5f39554d97182657f8c4b9719568eb7fded2b9135f084bf760b", + "https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd", + "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", + "https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d", + "https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d", + "https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a", + "https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58", + "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87", + "https://bcr.bazel.build/modules/bazel_features/1.30.0/source.json": "b07e17f067fe4f69f90b03b36ef1e08fe0d1f3cac254c1241a1818773e3423bc", + "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7", + "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a", + "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", + "https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e", + "https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686", + "https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a", + "https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5", + "https://bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel": "a0dcb779424be33100dcae821e9e27e4f2901d9dfd5333efe5ac6a8d7ab75e1d", + "https://bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel": "3bd40978e7a1fac911d5989e6b09d8f64921865a45822d8b09e815eaa726a651", + "https://bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel": "32880f5e2945ce6a03d1fbd588e9198c0a959bb42297b2cfaf1685b7bc32e138", + "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917", + "https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d", + "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b", + "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/source.json": "f121b43eeefc7c29efbd51b83d08631e2347297c95aac9764a701f2a6a2bb953", + "https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84", + "https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8", + "https://bcr.bazel.build/modules/gazelle/0.32.0/MODULE.bazel": "b499f58a5d0d3537f3cf5b76d8ada18242f64ec474d8391247438bf04f58c7b8", + "https://bcr.bazel.build/modules/gazelle/0.33.0/MODULE.bazel": "a13a0f279b462b784fb8dd52a4074526c4a2afe70e114c7d09066097a46b3350", + "https://bcr.bazel.build/modules/gazelle/0.34.0/MODULE.bazel": "abdd8ce4d70978933209db92e436deb3a8b737859e9354fb5fd11fb5c2004c8a", + "https://bcr.bazel.build/modules/gazelle/0.36.0/MODULE.bazel": "e375d5d6e9a6ca59b0cb38b0540bc9a05b6aa926d322f2de268ad267a2ee74c0", + "https://bcr.bazel.build/modules/gazelle/0.47.0/MODULE.bazel": "b61bb007c4efad134aa30ee7f4a8e2a39b22aa5685f005edaa022fbd1de43ebc", + "https://bcr.bazel.build/modules/gazelle/0.47.0/source.json": "aeb2e5df14b7fb298625d75d08b9c65bdb0b56014c5eb89da9e5dd0572280ae6", + "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb", + "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4", + "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6", + "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/source.json": "41e9e129f80d8c8bf103a7acc337b76e54fad1214ac0a7084bf24f4cd924b8b4", + "https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f", + "https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075", + "https://bcr.bazel.build/modules/jsoncpp/1.9.5/source.json": "4108ee5085dd2885a341c7fab149429db457b3169b86eb081fa245eadf69169d", + "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", + "https://bcr.bazel.build/modules/package_metadata/0.0.5/MODULE.bazel": "ef4f9439e3270fdd6b9fd4dbc3d2f29d13888e44c529a1b243f7a31dfbc2e8e4", + "https://bcr.bazel.build/modules/package_metadata/0.0.5/source.json": "2326db2f6592578177751c3e1f74786b79382cd6008834c9d01ec865b9126a85", + "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", + "https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f", + "https://bcr.bazel.build/modules/platforms/0.0.11/source.json": "f7e188b79ebedebfe75e9e1d098b8845226c7992b307e28e1496f23112e8fc29", + "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", + "https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37", + "https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615", + "https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814", + "https://bcr.bazel.build/modules/platforms/0.0.8/MODULE.bazel": "9f142c03e348f6d263719f5074b21ef3adf0b139ee4c5133e2aa35664da9eb2d", + "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7", + "https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c", + "https://bcr.bazel.build/modules/protobuf/27.1/MODULE.bazel": "703a7b614728bb06647f965264967a8ef1c39e09e8f167b3ca0bb1fd80449c0d", + "https://bcr.bazel.build/modules/protobuf/29.0-rc2.bcr.1/MODULE.bazel": "52f4126f63a2f0bbf36b99c2a87648f08467a4eaf92ba726bc7d6a500bbf770c", + "https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df", + "https://bcr.bazel.build/modules/protobuf/29.0/MODULE.bazel": "319dc8bf4c679ff87e71b1ccfb5a6e90a6dbc4693501d471f48662ac46d04e4e", + "https://bcr.bazel.build/modules/protobuf/29.0/source.json": "b857f93c796750eef95f0d61ee378f3420d00ee1dd38627b27193aa482f4f981", + "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0", + "https://bcr.bazel.build/modules/protobuf/3.19.2/MODULE.bazel": "532ffe5f2186b69fdde039efe6df13ba726ff338c6bc82275ad433013fa10573", + "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858", + "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e", + "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/source.json": "be4789e951dd5301282729fe3d4938995dc4c1a81c2ff150afc9f1b0504c6022", + "https://bcr.bazel.build/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206", + "https://bcr.bazel.build/modules/re2/2023-09-01/source.json": "e044ce89c2883cd957a2969a43e79f7752f9656f6b20050b62f90ede21ec6eb4", + "https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8", + "https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e", + "https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647", + "https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002", + "https://bcr.bazel.build/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191", + "https://bcr.bazel.build/modules/rules_cc/0.0.14/MODULE.bazel": "5e343a3aac88b8d7af3b1b6d2093b55c347b8eefc2e7d1442f7a02dc8fea48ac", + "https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc", + "https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87", + "https://bcr.bazel.build/modules/rules_cc/0.0.17/MODULE.bazel": "2ae1d8f4238ec67d7185d8861cb0a2cdf4bc608697c331b95bf990e69b62e64a", + "https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c", + "https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f", + "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e", + "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5", + "https://bcr.bazel.build/modules/rules_cc/0.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513", + "https://bcr.bazel.build/modules/rules_cc/0.1.1/source.json": "d61627377bd7dd1da4652063e368d9366fc9a73920bfa396798ad92172cf645c", + "https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6", + "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8", + "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/source.json": "c8b1e2c717646f1702290959a3302a178fb639d987ab61d548105019f11e527e", + "https://bcr.bazel.build/modules/rules_go/0.41.0/MODULE.bazel": "55861d8e8bb0e62cbd2896f60ff303f62ffcb0eddb74ecb0e5c0cbe36fc292c8", + "https://bcr.bazel.build/modules/rules_go/0.42.0/MODULE.bazel": "8cfa875b9aa8c6fce2b2e5925e73c1388173ea3c32a0db4d2b4804b453c14270", + "https://bcr.bazel.build/modules/rules_go/0.46.0/MODULE.bazel": "3477df8bdcc49e698b9d25f734c4f3a9f5931ff34ee48a2c662be168f5f2d3fd", + "https://bcr.bazel.build/modules/rules_go/0.53.0/MODULE.bazel": "a4ed760d3ac0dbc0d7b967631a9a3fd9100d28f7d9fcf214b4df87d4bfff5f9a", + "https://bcr.bazel.build/modules/rules_go/0.55.1/MODULE.bazel": "a57a6fc59a74326c0b440d07cca209edf13c7d1a641e48cfbeab56e79f873609", + "https://bcr.bazel.build/modules/rules_go/0.55.1/source.json": "827a740c8959c9d20616889e7746cde4dcc6ee80d25146943627ccea0736328f", + "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", + "https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86", + "https://bcr.bazel.build/modules/rules_java/6.0.0/MODULE.bazel": "8a43b7df601a7ec1af61d79345c17b31ea1fedc6711fd4abfd013ea612978e39", + "https://bcr.bazel.build/modules/rules_java/6.4.0/MODULE.bazel": "e986a9fe25aeaa84ac17ca093ef13a4637f6107375f64667a15999f77db6c8f6", + "https://bcr.bazel.build/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31", + "https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a", + "https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6", + "https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab", + "https://bcr.bazel.build/modules/rules_java/7.3.2/MODULE.bazel": "50dece891cfdf1741ea230d001aa9c14398062f2b7c066470accace78e412bc2", + "https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe", + "https://bcr.bazel.build/modules/rules_java/8.12.0/MODULE.bazel": "8e6590b961f2defdfc2811c089c75716cb2f06c8a4edeb9a8d85eaa64ee2a761", + "https://bcr.bazel.build/modules/rules_java/8.12.0/source.json": "cbd5d55d9d38d4008a7d00bee5b5a5a4b6031fcd4a56515c9accbcd42c7be2ba", + "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7", + "https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909", + "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036", + "https://bcr.bazel.build/modules/rules_jvm_external/5.3/MODULE.bazel": "bf93870767689637164657731849fb887ad086739bd5d360d90007a581d5527d", + "https://bcr.bazel.build/modules/rules_jvm_external/6.1/MODULE.bazel": "75b5fec090dbd46cf9b7d8ea08cf84a0472d92ba3585b476f44c326eda8059c4", + "https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0", + "https://bcr.bazel.build/modules/rules_jvm_external/6.3/source.json": "6f5f5a5a4419ae4e37c35a5bb0a6ae657ed40b7abc5a5189111b47fcebe43197", + "https://bcr.bazel.build/modules/rules_kotlin/1.9.0/MODULE.bazel": "ef85697305025e5a61f395d4eaede272a5393cee479ace6686dba707de804d59", + "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3", + "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5", + "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0", + "https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d", + "https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c", + "https://bcr.bazel.build/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb", + "https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc", + "https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff", + "https://bcr.bazel.build/modules/rules_pkg/1.0.1/source.json": "bd82e5d7b9ce2d31e380dd9f50c111d678c3bdaca190cb76b0e1c71b05e1ba8a", + "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06", + "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7", + "https://bcr.bazel.build/modules/rules_proto/6.0.0/MODULE.bazel": "b531d7f09f58dce456cd61b4579ce8c86b38544da75184eadaf0a7cb7966453f", + "https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73", + "https://bcr.bazel.build/modules/rules_proto/7.0.2/MODULE.bazel": "bf81793bd6d2ad89a37a40693e56c61b0ee30f7a7fdbaf3eabbf5f39de47dea2", + "https://bcr.bazel.build/modules/rules_proto/7.0.2/source.json": "1e5e7260ae32ef4f2b52fd1d0de8d03b606a44c91b694d2f1afb1d3b28a48ce1", + "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f", + "https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300", + "https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382", + "https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed", + "https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58", + "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c", + "https://bcr.bazel.build/modules/rules_python/0.40.0/MODULE.bazel": "9d1a3cd88ed7d8e39583d9ffe56ae8a244f67783ae89b60caafc9f5cf318ada7", + "https://bcr.bazel.build/modules/rules_python/0.40.0/source.json": "939d4bd2e3110f27bfb360292986bb79fd8dcefb874358ccd6cdaa7bda029320", + "https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c", + "https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b", + "https://bcr.bazel.build/modules/rules_shell/0.5.0/MODULE.bazel": "8c8447370594d45539f66858b602b0bb2cb2d3401a4ebb9ad25830c59c0f366d", + "https://bcr.bazel.build/modules/rules_shell/0.5.0/source.json": "3038276f07cbbdd1c432d1f80a2767e34143ffbb03cfa043f017e66adbba324c", + "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8", + "https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c", + "https://bcr.bazel.build/modules/stardoc/0.5.6/MODULE.bazel": "c43dabc564990eeab55e25ed61c07a1aadafe9ece96a4efabb3f8bf9063b71ef", + "https://bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel": "05e3d6d30c099b6770e97da986c53bd31844d7f13d41412480ea265ac9e8079c", + "https://bcr.bazel.build/modules/stardoc/0.7.1/MODULE.bazel": "3548faea4ee5dda5580f9af150e79d0f6aea934fc60c1cc50f4efdd9420759e7", + "https://bcr.bazel.build/modules/stardoc/0.7.1/source.json": "b6500ffcd7b48cd72c29bb67bcac781e12701cc0d6d55d266a652583cfcdab01", + "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", + "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", + "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27", + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca", + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806", + "https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198" + }, + "selectedYankedVersions": {}, + "moduleExtensions": { + "@@rules_cue+//cue:extensions.bzl%cue": { + "general": { + "bzlTransitiveDigest": "T7Ftx428/RIoWaSR7Fu+QDd+fGZXIh0Pu3uo/T05dek=", + "usagesDigest": "9iY6UYjN4vIteSAoV4u4w4237UiuT04lVOsDxKym+gk=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "cue_tool": { + "repoRuleId": "@@rules_cue+//cue/private/tools/cue:toolchain.bzl%_download_tool", + "attributes": { + "version": "v0.15.1" + } + }, + "cue_tool_toolchains": { + "repoRuleId": "@@rules_cue+//cue/private/tools/cue:toolchain.bzl%_toolchains_repo", + "attributes": { + "tool_repo": "cue_tool", + "version": "v0.15.1" + } + } + }, + "recordedRepoMappingEntries": [] + } + }, + "@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": { + "general": { + "bzlTransitiveDigest": "hUTp2w+RUVdL7ma5esCXZJAFnX7vLbVfLd7FwnQI6bU=", + "usagesDigest": "QI2z8ZUR+mqtbwsf2fLqYdJAkPOHdOV+tF2yVAUgRzw=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "com_github_jetbrains_kotlin_git": { + "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_compiler_git_repository", + "attributes": { + "urls": [ + "https://github.com/JetBrains/kotlin/releases/download/v1.9.23/kotlin-compiler-1.9.23.zip" + ], + "sha256": "93137d3aab9afa9b27cb06a824c2324195c6b6f6179d8a8653f440f5bd58be88" + } + }, + "com_github_jetbrains_kotlin": { + "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_capabilities_repository", + "attributes": { + "git_repository_name": "com_github_jetbrains_kotlin_git", + "compiler_version": "1.9.23" + } + }, + "com_github_google_ksp": { + "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:ksp.bzl%ksp_compiler_plugin_repository", + "attributes": { + "urls": [ + "https://github.com/google/ksp/releases/download/1.9.23-1.0.20/artifacts.zip" + ], + "sha256": "ee0618755913ef7fd6511288a232e8fad24838b9af6ea73972a76e81053c8c2d", + "strip_version": "1.9.23-1.0.20" + } + }, + "com_github_pinterest_ktlint": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_file", + "attributes": { + "sha256": "01b2e0ef893383a50dbeb13970fe7fa3be36ca3e83259e01649945b09d736985", + "urls": [ + "https://github.com/pinterest/ktlint/releases/download/1.3.0/ktlint" + ], + "executable": true + } + }, + "rules_android": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "sha256": "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806", + "strip_prefix": "rules_android-0.1.1", + "urls": [ + "https://github.com/bazelbuild/rules_android/archive/v0.1.1.zip" + ] + } + } + }, + "recordedRepoMappingEntries": [ + [ + "rules_kotlin+", + "bazel_tools", + "bazel_tools" + ] + ] + } + } + } +} diff --git a/examples/gazelle/README.md b/examples/gazelle/README.md new file mode 100644 index 0000000..4540e75 --- /dev/null +++ b/examples/gazelle/README.md @@ -0,0 +1,389 @@ +# Gazelle Example for rules_cue + +This example demonstrates how to use [Gazelle](https://github.com/bazelbuild/bazel-gazelle) to automatically generate Bazel BUILD files for CUE code. + +## Table of Contents + +- [Overview](#overview) +- [Quick Start](#quick-start) +- [Project Structure](#project-structure) +- [Usage](#usage) +- [Configuration](#configuration) +- [Generated Rules](#generated-rules) +- [Customization](#customization) +- [How It Works](#how-it-works) +- [Benefits](#benefits) +- [Common Issues](#common-issues) +- [Next Steps](#next-steps) +- [Learn More](#learn-more) + +## Overview + +Gazelle is a build file generator for Bazel projects. The `rules_cue` Gazelle extension can automatically generate: + +- `cue_module` rules for `cue.mod` directories +- `cue_instance` rules for CUE packages +- `cue_consolidated_instance` rules for consolidated output +- `cue_exported_instance` rules for exporting to various formats + +This example includes three different CUE packages demonstrating various use cases: +- **contacts**: Schema validation with contact information +- **config**: Application configuration management +- **services/api**: Service API definitions + +## Quick Start + +### Prerequisites + +- Bazel 7.0 or later +- Basic familiarity with CUE + +### Setup Steps + +1. **Add dependencies to your MODULE.bazel:** + +```python +bazel_dep(name = "rules_cue", version = "0.0.0") +bazel_dep(name = "gazelle", version = "0.47.0") + +# Set up Go dependencies for Gazelle CUE extension +go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps") +go_deps.from_file(go_mod = "@rules_cue//:go.mod") +use_repo( + go_deps, + "com_github_iancoleman_strcase", + "org_cuelang_go", +) +``` + +2. **Create a root BUILD.bazel file:** + +```python +load("@gazelle//:def.bzl", "gazelle") + +# gazelle:prefix your.domain/your-project + +gazelle( + name = "gazelle", + gazelle = "@rules_cue//gazelle:gazelle_binary", +) +``` + +3. **Create a CUE module:** + +```bash +mkdir -p cue.mod +cat > cue.mod/module.cue < config/app.cue <0 +} + +products: [...#Product] +products: [ + {id: "001", name: "Widget", price: 9.99}, + {id: "002", name: "Gadget", price: 19.99}, +] +``` + +Then run: + +```bash +bazel run //:gazelle +bazel build //inventory:inventory_cue_instance +``` + +## How It Works + +1. **Language Extension**: The Gazelle CUE extension is implemented in `//gazelle/cue` +2. **Binary**: A custom `gazelle_binary` target includes the CUE extension along with Go and Proto extensions +3. **Configuration**: The `gazelle()` rule in `BUILD.bazel` uses this custom binary +4. **Discovery**: Gazelle scans for `.cue` files and parses their package declarations and imports +5. **Generation**: Based on package structure, it generates appropriate `cue_instance` and `cue_consolidated_instance` rules +6. **Module Detection**: Automatically finds the nearest `cue.mod` directory and links it via the `ancestor` attribute +7. **Resolution**: Dependencies between CUE packages are automatically resolved + +## Benefits + +1. **Zero Manual Maintenance**: BUILD files are generated automatically from CUE code +2. **Consistency**: All CUE packages follow the same BUILD file pattern +3. **Type Safety**: CUE schema validation happens at build time +4. **CI Integration**: Can verify BUILD files are current in CI pipelines using `gazelle_check` +5. **Hermetic Builds**: All dependencies are explicitly declared and managed by Bazel +6. **Easy Updates**: Re-running Gazelle after code changes keeps BUILD files in sync + +## Common Issues + +### Gazelle not finding CUE files + +Make sure your CUE files: +- Have the `.cue` extension +- Contain valid CUE code +- Have a package declaration (except for standalone files) + +### Build failures after running Gazelle + +If builds fail after running Gazelle: + +1. Check that all CUE files in a package have the same package name +2. Verify imports are correct +3. Ensure `cue.mod/module.cue` exists if using module imports +4. Check that the `ancestor` attribute points to the correct `cue_module` + +### Version mismatch warnings + +If you see warnings about version mismatches between `go.mod` and `MODULE.bazel`, ensure both files specify the same version of Gazelle. + +## Next Steps + +### For This Example + +1. Explore the three included packages (`contacts`, `config`, `services/api`) +2. Modify existing CUE files and re-run Gazelle to see incremental updates +3. Add new packages to see Gazelle handle them automatically +4. Try different output formats using `# gazelle:cue_output_format yaml` + +### For Your Projects + +1. **Integrate with CI**: Add `bazel run //:gazelle_check` to your CI pipeline to ensure BUILD files stay in sync +2. **Golden File Testing**: Explore `# gazelle:cue_test_golden_suffix` for validation tests +3. **Export to Multiple Formats**: Use directives to export to JSON, YAML, or text +4. **Complex Schemas**: Build larger CUE schemas with imports and dependencies +5. **Module Dependencies**: Add external CUE modules to `cue.mod/pkg/` + +### Tips for Success + +- Run `bazel run //:gazelle` whenever you add or modify CUE files +- Use `bazel run //:gazelle_check` in CI to verify BUILD files are up to date +- Generated BUILD files can be customized; Gazelle will preserve manual changes where possible +- Use directives to control Gazelle behavior per-package or per-file + +## Learn More + +### Documentation + +- [Gazelle Documentation](https://github.com/bazelbuild/bazel-gazelle) - Official Gazelle documentation +- [rules_cue Documentation](https://github.com/abcue/rules_cue) - CUE rules for Bazel +- [CUE Language](https://cuelang.org/) - Official CUE language website + +### Related Files + +- `gazelle/cue/README.md` - Implementation details of the Gazelle CUE extension +- `.bazelversion` - Specifies the Bazel version for this example +- `MODULE.bazel` - Bazel module configuration with all dependencies diff --git a/examples/gazelle/config/BUILD.bazel b/examples/gazelle/config/BUILD.bazel new file mode 100644 index 0000000..f87cc18 --- /dev/null +++ b/examples/gazelle/config/BUILD.bazel @@ -0,0 +1,19 @@ +load("@rules_cue//cue:cue.bzl", "cue_consolidated_instance", "cue_instance") + +cue_instance( + name = "config_cue_instance", + package_name = "config", + srcs = [ + "app.cue", + "defaults.cue", + ], + ancestor = "//cue.mod:cue.mod", + visibility = ["//visibility:public"], +) + +cue_consolidated_instance( + name = "config_cue_def", + instance = ":config_cue_instance", + output_format = "cue", + visibility = ["//visibility:public"], +) diff --git a/examples/gazelle/config/app.cue b/examples/gazelle/config/app.cue new file mode 100644 index 0000000..d611d19 --- /dev/null +++ b/examples/gazelle/config/app.cue @@ -0,0 +1,23 @@ +package config + +#AppConfig: { + app: { + name: string + version: string + port: int & >0 & <65536 + } + database: { + host: string + port: int & >0 & <65536 + name: string + user: string + password: string + } + logging: { + level: "debug" | "info" | "warn" | "error" + format: "json" | "text" + } +} + +config: #AppConfig + diff --git a/examples/gazelle/config/defaults.cue b/examples/gazelle/config/defaults.cue new file mode 100644 index 0000000..aae748f --- /dev/null +++ b/examples/gazelle/config/defaults.cue @@ -0,0 +1,21 @@ +package config + +config: { + app: { + name: "example-app" + version: "1.0.0" + port: 8080 + } + database: { + host: "localhost" + port: 5432 + name: "mydb" + user: "dbuser" + password: "changeme" + } + logging: { + level: "info" + format: "json" + } +} + diff --git a/examples/gazelle/contacts/BUILD.bazel b/examples/gazelle/contacts/BUILD.bazel new file mode 100644 index 0000000..34fcf2f --- /dev/null +++ b/examples/gazelle/contacts/BUILD.bazel @@ -0,0 +1,21 @@ +load("@rules_cue//cue:cue.bzl", "cue_consolidated_instance", "cue_instance") + +# gazelle:prefix github.com/abcue/rules_cue/examples/gazelle/contacts + +cue_instance( + name = "contacts_cue_instance", + package_name = "contacts", + srcs = [ + "data.cue", + "schema.cue", + ], + ancestor = "//cue.mod:cue.mod", + visibility = ["//visibility:public"], +) + +cue_consolidated_instance( + name = "contacts_cue_def", + instance = ":contacts_cue_instance", + output_format = "cue", + visibility = ["//visibility:public"], +) diff --git a/examples/gazelle/contacts/data.cue b/examples/gazelle/contacts/data.cue new file mode 100644 index 0000000..9160a03 --- /dev/null +++ b/examples/gazelle/contacts/data.cue @@ -0,0 +1,34 @@ +package contacts + +contacts: [ + { + name: { + first: "Alice" + last: "Johnson" + } + email: "alice.johnson@example.com" + phone: "+1-555-0101" + age: 30 + tags: ["friend", "work"] + }, + { + name: { + first: "Bob" + last: "Smith" + } + email: "bob.smith@example.com" + age: 25 + tags: ["family"] + }, + { + name: { + first: "Charlie" + last: "Brown" + } + email: "charlie.brown@example.com" + phone: "+1-555-0103" + age: 35 + tags: ["friend", "hobby"] + }, +] + diff --git a/examples/gazelle/contacts/schema.cue b/examples/gazelle/contacts/schema.cue new file mode 100644 index 0000000..42e8319 --- /dev/null +++ b/examples/gazelle/contacts/schema.cue @@ -0,0 +1,24 @@ +package contacts + +import "strings" + +// Contact represents a person's contact information +#Contact: { + // Name of the person + name: { + first: strings.MinRunes(1) + last: strings.MinRunes(1) + } + // Email address + email: =~"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$" + // Phone number (optional) + phone?: string + // Age must be a positive integer + age?: int & >0 + // Tags for categorizing contacts + tags?: [...string] +} + +// List of contacts must conform to the Contact schema +contacts: [...#Contact] + diff --git a/examples/gazelle/cue.mod/BUILD.bazel b/examples/gazelle/cue.mod/BUILD.bazel new file mode 100644 index 0000000..1647a80 --- /dev/null +++ b/examples/gazelle/cue.mod/BUILD.bazel @@ -0,0 +1,6 @@ +load("@rules_cue//cue:cue.bzl", "cue_module") + +cue_module( + name = "cue.mod", + visibility = ["//visibility:public"], +) diff --git a/examples/gazelle/cue.mod/module.cue b/examples/gazelle/cue.mod/module.cue new file mode 100644 index 0000000..3680a82 --- /dev/null +++ b/examples/gazelle/cue.mod/module.cue @@ -0,0 +1,3 @@ +module: "github.com/abcue/rules_cue/examples/gazelle" +language: version: "v0.15.1" + diff --git a/examples/gazelle/services/api/BUILD.bazel b/examples/gazelle/services/api/BUILD.bazel new file mode 100644 index 0000000..c652143 --- /dev/null +++ b/examples/gazelle/services/api/BUILD.bazel @@ -0,0 +1,19 @@ +load("@rules_cue//cue:cue.bzl", "cue_consolidated_instance", "cue_instance") + +cue_instance( + name = "api_cue_instance", + package_name = "api", + srcs = [ + "config.cue", + "spec.cue", + ], + ancestor = "//cue.mod:cue.mod", + visibility = ["//visibility:public"], +) + +cue_consolidated_instance( + name = "api_cue_def", + instance = ":api_cue_instance", + output_format = "cue", + visibility = ["//visibility:public"], +) diff --git a/examples/gazelle/services/api/config.cue b/examples/gazelle/services/api/config.cue new file mode 100644 index 0000000..c64e564 --- /dev/null +++ b/examples/gazelle/services/api/config.cue @@ -0,0 +1,24 @@ +package api + +service: { + name: "user-service" + version: "v1.0.0" + endpoints: [ + { + path: "/users" + method: "GET" + description: "List all users" + }, + { + path: "/users/{id}" + method: "GET" + description: "Get a user by ID" + }, + { + path: "/users" + method: "POST" + description: "Create a new user" + }, + ] +} + diff --git a/examples/gazelle/services/api/spec.cue b/examples/gazelle/services/api/spec.cue new file mode 100644 index 0000000..4878bfa --- /dev/null +++ b/examples/gazelle/services/api/spec.cue @@ -0,0 +1,17 @@ +package api + +// API specification +#Endpoint: { + path: string + method: "GET" | "POST" | "PUT" | "DELETE" + description?: string +} + +#Service: { + name: string + version: string + endpoints: [...#Endpoint] +} + +service: #Service + diff --git a/gazelle/BUILD.bazel b/gazelle/BUILD.bazel new file mode 100644 index 0000000..cc4807f --- /dev/null +++ b/gazelle/BUILD.bazel @@ -0,0 +1,13 @@ +load("@gazelle//:def.bzl", "gazelle_binary") + +# gazelle_binary is a Gazelle binary that includes the CUE extension. +# This can be used in other workspaces by depending on @rules_cue//gazelle:gazelle_binary +gazelle_binary( + name = "gazelle_binary", + languages = [ + "@gazelle//language/go", + "@gazelle//language/proto", + "//gazelle/cue", + ], + visibility = ["//visibility:public"], +) diff --git a/gazelle/cue/BUILD.bazel b/gazelle/cue/BUILD.bazel new file mode 100644 index 0000000..a6f54e3 --- /dev/null +++ b/gazelle/cue/BUILD.bazel @@ -0,0 +1,25 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "cue", + srcs = [ + "config.go", + "cue.go", + "generate.go", + "resolve.go", + ], + importpath = "github.com/seh/rules_cue/gazelle/cue", + visibility = ["//visibility:public"], + deps = [ + "@com_github_iancoleman_strcase//:go_default_library", + "@gazelle//config", + "@gazelle//label", + "@gazelle//language", + "@gazelle//pathtools", + "@gazelle//repo", + "@gazelle//resolve", + "@gazelle//rule", + "@org_cuelang_go//cue/ast:go_default_library", + "@org_cuelang_go//cue/parser:go_default_library", + ], +) diff --git a/gazelle/cue/README.md b/gazelle/cue/README.md new file mode 100644 index 0000000..f7bab0d --- /dev/null +++ b/gazelle/cue/README.md @@ -0,0 +1,69 @@ +# Gazelle CUE Language Support + +This directory contains the Gazelle language extension for CUE, which enables automatic generation of Bazel BUILD files for CUE projects. + +## Overview + +The Gazelle CUE extension automatically generates: +- `cue_module` rules for `cue.mod` directories +- `cue_instance` rules for CUE packages +- `cue_consolidated_instance` rules for consolidated output +- `cue_exported_instance` rules for exports (with appropriate directives) +- `cue_test` rules for golden file testing (with appropriate directives) + +## Usage + +See the [examples/gazelle](../../examples/gazelle) directory for a complete working example. + +### Quick Start + +1. Add dependencies to your `MODULE.bazel`: + +```python +bazel_dep(name = "rules_cue", version = "0.0.0") +bazel_dep(name = "gazelle", version = "0.47.0") + +go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps") +go_deps.from_file(go_mod = "@rules_cue//:go.mod") +use_repo(go_deps, "com_github_iancoleman_strcase", "org_cuelang_go") +``` + +2. Create a `BUILD.bazel` file with Gazelle target: + +```python +load("@gazelle//:def.bzl", "gazelle") + +gazelle( + name = "gazelle", + gazelle = "@rules_cue//gazelle:gazelle_binary", +) +``` + +3. Run Gazelle: + +```bash +bazel run //:gazelle +``` + +## Directives + +Configure Gazelle behavior with directives in BUILD files: + +- `# gazelle:prefix ` - Set the import path prefix +- `# gazelle:cue_output_format ` - Set output format (json, yaml, text, cue) +- `# gazelle:cue_gen_exported_instance` - Generate cue_exported_instance rules +- `# gazelle:cue_test_golden_suffix ` - Enable golden file testing with suffix +- `# gazelle:cue_test_golden_filename ` - Specify golden file name + +## Implementation + +The extension implements the `language.Language` interface from Gazelle: + +- `config.go` - Configuration and directive handling +- `cue.go` - Language definition and rule kinds +- `generate.go` - Rule generation logic +- `resolve.go` - Dependency resolution + +## Note + +This implementation is based on https://github.com/tnarg/rules_cue/tree/master/gazelle/cue with enhancements for `@rules_cue` rules. \ No newline at end of file diff --git a/gazelle/cue/config.go b/gazelle/cue/config.go new file mode 100644 index 0000000..057c2ba --- /dev/null +++ b/gazelle/cue/config.go @@ -0,0 +1,146 @@ +package cuelang + +import ( + "flag" + "fmt" + "go/build" + "log" + "path" + "strings" + + "github.com/bazelbuild/bazel-gazelle/config" + "github.com/bazelbuild/bazel-gazelle/rule" +) + +type cueConfig struct { + // prefix is a prefix of an import path, used to generate importpath + // attributes. Set with -go_prefix or # gazelle:prefix. + prefix string + prefixRel string + + // cueTestGoldenSuffix is the suffix used for golden test files in CUE tests. + // This allows customizing the suffix for generated test files. + // set with #gazelle:cue_test_golden_suffix + // example: #gazelle:cue_test_golden_suffix -gen.yml + cueTestGoldenSuffix string + + // cueTestGoldenFilename is the filename used for golden test files in CUE tests. + // This allows specifying a custom filename for the golden test file. + // set with #gazelle:cue_test_golden_filename + // example: #gazelle:cue_test_golden_filename main.gen.json + cueTestGoldenFilename string + + // cueGenExportedInstance controls whether to generate cue_exported_instance rules + // for each cue_instance. When true, a corresponding cue_exported_instance rule will be created. + // #gazelle:cue_exported_instance + cueGenExportedInstance bool + + // cueOutputFormat specifies the output format for CUE exports. + // Valid values are "json", "yaml", and "text". + // Default is "json" if not specified. + cueOutputFormat string +} + +// KnownDirectives returns a list of directive keys that this +// Configurer can interpret. Gazelle prints errors for directives that +// are not recognized by any Configurer. +func (s *cueLang) KnownDirectives() []string { + return []string{"prefix", "cue_test_golden_suffix", "cue_test_golden_filename", "cue_output_format", "cue_gen_exported_instance"} +} + +// RegisterFlags registers command-line flags used by the +// extension. This method is called once with the root configuration +// when Gazelle starts. RegisterFlags may set an initial values in +// Config.Exts. When flags are set, they should modify these values. +func (s *cueLang) RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) { + c.Exts[cueName] = &cueConfig{ + cueOutputFormat: "json", // Set default output format to json + } +} + +// CheckFlags validates the configuration after command line flags are +// parsed. This is called once with the root configuration when +// Gazelle starts. CheckFlags may set default values in flags or make +// implied changes. +func (s *cueLang) CheckFlags(fs *flag.FlagSet, c *config.Config) error { + return nil +} + +// Configure modifies the configuration using directives and other +// information extracted from a build file. Configure is called in +// each directory. +// +// c is the configuration for the current directory. It starts out as +// a copy of the configuration for the parent directory. +// +// rel is the slash-separated relative path from the repository root +// to the current directory. It is "" for the root directory itself. +// +// f is the build file for the current directory or nil if there is no +// existing build file. +func (s *cueLang) Configure(c *config.Config, rel string, f *rule.File) { + var conf *cueConfig + if raw, ok := c.Exts[cueName]; !ok { + conf = &cueConfig{ + cueOutputFormat: "json", // Set default output format to json + } + } else { + tmp := *(raw.(*cueConfig)) + conf = &tmp + } + c.Exts[cueName] = conf + + // We follow the same pattern as the go language to allow + // vendoring of cue repositories. + if path.Base(rel) == "vendor" { + conf.prefix = "" + conf.prefixRel = rel + } + + if f != nil { + for _, d := range f.Directives { + switch d.Key { + case "prefix": + if err := checkPrefix(d.Value); err != nil { + log.Print(err) + return + } + conf.prefix = d.Value + conf.prefixRel = rel + case "cue_test_golden_suffix": + conf.cueTestGoldenSuffix = d.Value + // cue_test depends on exported instance + conf.cueGenExportedInstance = true + case "cue_test_golden_filename": + // cue_test depends on exported instance + conf.cueGenExportedInstance = true + conf.cueTestGoldenFilename = d.Value + conf.cueTestGoldenSuffix = strings.TrimPrefix(path.Ext(d.Value), ".") + case "cue_gen_exported_instance": + conf.cueGenExportedInstance = true + case "cue_output_format": + conf.cueOutputFormat = d.Value + } + } + } +} + +// checkPrefix checks that a string may be used as a prefix. We forbid local +// (relative) imports and those beginning with "/". We allow the empty string, +// but generated rules must not have an empty importpath. +func checkPrefix(prefix string) error { + if strings.HasPrefix(prefix, "/") || build.IsLocalImport(prefix) { + return fmt.Errorf("invalid prefix: %q", prefix) + } + return nil +} + +// GetConfig returns the cueConfig from the provided config.Config +func GetConfig(c *config.Config) *cueConfig { + if raw, ok := c.Exts[cueName]; ok { + return raw.(*cueConfig) + } + return &cueConfig{ + cueOutputFormat: "json", // Set default output format to json + } +} diff --git a/gazelle/cue/cue.go b/gazelle/cue/cue.go new file mode 100644 index 0000000..903275f --- /dev/null +++ b/gazelle/cue/cue.go @@ -0,0 +1,163 @@ +package cuelang + +import ( + "fmt" + + "github.com/bazelbuild/bazel-gazelle/config" + "github.com/bazelbuild/bazel-gazelle/language" + "github.com/bazelbuild/bazel-gazelle/rule" +) + +const ( + cueName = "cue" +) + +var _ = fmt.Printf + +type cueLang struct{} + +// NewLanguage returns an instace of the Gazelle plugin for rules_cue. +func NewLanguage() language.Language { + return &cueLang{} +} + +func (cl *cueLang) Name() string { return cueName } + +// Kinds returns a map of maps rule names (kinds) and information on +// how to match and merge attributes that may be found in rules of +// those kinds. All kinds of rules generated for this language may be +// found here. +func (cl *cueLang) Kinds() map[string]rule.KindInfo { + return map[string]rule.KindInfo{ + // @rules_cue kinds + "cue_module": { + MatchAttrs: []string{"file"}, + }, + "cue_instance": { + MatchAttrs: []string{"package_name"}, + NonEmptyAttrs: map[string]bool{ + "srcs": true, + "deps": true, + "ancestor": true, + }, + MergeableAttrs: map[string]bool{ + "deps": true, + "package_name": true, + "ancestor": true, + "srcs": true, + }, + ResolveAttrs: map[string]bool{"deps": true, "ancestor": true}, + }, + // cue_exported_instance: Takes a CUE instance and exports it to a specified output format, preserving the structure defined in the instance + "cue_exported_instance": { + MatchAttrs: []string{"instance"}, + NonEmptyAttrs: map[string]bool{ + "instance": true, + }, + MergeableAttrs: map[string]bool{ + "escape": true, + "output_format": false, + "result": true, + }, + }, + // cue_consolidated_instance: Takes a CUE instance (a package with imports) and outputs a consolidated definition with all dependencies resolved. + "cue_consolidated_instance": { + MatchAttrs: []string{"instance"}, + NonEmptyAttrs: map[string]bool{ + "instance": true, + }, + }, + // cue_consolidated_files: Consolidates CUE files that are part of a module into a single output file in a specified format. + "cue_consolidated_files": { + NonEmptyAttrs: map[string]bool{ + "deps": true, + }, + MergeableAttrs: map[string]bool{ + "output_format": false, + "result": true, + }, + }, + //cue_consolidated_standalone_files: Processes standalone CUE files (not part of a module) and outputs a consolidated definition in a specified format. + "cue_consolidated_standalone_files": { + MergeableAttrs: map[string]bool{ + "output_format": false, + "result": true, + }, + ResolveAttrs: map[string]bool{"deps": true}, + }, + //cue_exported_files: Exports CUE files that are part of a CUE module to a specified output format (JSON, YAML, etc.). + "cue_exported_files": { + MatchAttrs: []string{"srcs", "module"}, + NonEmptyAttrs: map[string]bool{ + "deps": true, + "module": true, + }, + MergeableAttrs: map[string]bool{ + "escape": true, + "output_format": false, + "result": true, + "srcs": true, + }, + ResolveAttrs: map[string]bool{"deps": true}, + }, + // cue_exported_standalone_files processes CUE files without requiring them to be part of a CUE module structure. + "cue_exported_standalone_files": { + MergeableAttrs: map[string]bool{ + "escape": true, + "output_format": false, + "result": true, + }, + ResolveAttrs: map[string]bool{"deps": true}, + }, + "cue_test": { + MatchAttrs: []string{"generated_output_file"}, + NonEmptyAttrs: map[string]bool{ + "golden_file": true, + }, + }, + "cue_gen_golden": { + MatchAttrs: []string{"srcs"}, + }, + } +} + +// Loads returns .bzl files and symbols they define. Every rule +// generated by GenerateRules, now or in the past, should be loadable +// from one of these files. +func (cl *cueLang) Loads() []rule.LoadInfo { + return []rule.LoadInfo{ + { + Name: "@rules_cue//cue:deps.bzl", + Symbols: []string{ + "cue_register_toolchains", + }, + After: []string{ + "gazelle_dependencies", + }, + }, { + Name: "@rules_cue//cue:cue.bzl", + Symbols: []string{ + "cue_module", + "cue_instance", + "cue_exported_instance", + "cue_consolidated_instance", + "cue_consolidated_files", + "cue_consolidated_standalone_files", + "cue_exported_files", + "cue_exported_standalone_files", + "cue_test", + "cue_gen_golden", + }, + After: []string{ + "cue_register_toolchains", + }, + }, + } +} + +// Fix repairs deprecated usage of language-specific rules in f. This +// is called before the file is indexed. Unless c.ShouldFix is true, +// fixes that delete or rename rules should not be performed. +func (cl *cueLang) Fix(c *config.Config, f *rule.File) { + // No fixes needed currently +} diff --git a/gazelle/cue/generate.go b/gazelle/cue/generate.go new file mode 100644 index 0000000..f9d8c70 --- /dev/null +++ b/gazelle/cue/generate.go @@ -0,0 +1,691 @@ +package cuelang + +import ( + "fmt" + "log" + "os" + "path" + "path/filepath" + "sort" + "strings" + + "cuelang.org/go/cue/ast" + "cuelang.org/go/cue/parser" + "github.com/bazelbuild/bazel-gazelle/config" + "github.com/bazelbuild/bazel-gazelle/language" + "github.com/bazelbuild/bazel-gazelle/rule" + "github.com/iancoleman/strcase" +) + +// GenerateRules extracts build metadata from source files in a +// directory. GenerateRules is called in each directory where an +// update is requested in depth-first post-order. +// +// args contains the arguments for GenerateRules. This is passed as a +// struct to avoid breaking implementations in the future when new +// fields are added. +// +// empty is a list of empty rules that may be deleted after merge. +// +// gen is a list of generated rules that may be updated or added. +// +// Any non-fatal errors this function encounters should be logged +// using log.Print. +func (cl *cueLang) GenerateRules(args language.GenerateArgs) language.GenerateResult { + // Get the configuration + conf := GetConfig(args.Config) + + // Parse CUE files + cueFiles := parseCueFiles(args) + if len(cueFiles) == 0 { + return language.GenerateResult{} + } + + // list cue_test golden files + cueTestGoldenfiles, err := listGoldenFiles(args, conf.cueTestGoldenSuffix) + if err != nil { + log.Printf("error listing golden files: %v", err) + } + // Match golden files with their corresponding CUE files + + // Setup context for rule generation + ctx := &ruleGenerationContext{ + config: conf, + implicitPkgName: path.Base(args.Rel), + baseImportPath: computeImportPath(args), + isCueModDir: path.Base(args.Dir) == "cue.mod", + moduleLabel: findNearestCueModule(args.Dir, args.Rel), + instances: make(map[string]*cueInstance), + exportedFiles: make(map[string]*cueExportedFiles), + exportedInstances: make(map[string]*cueExportedInstance), + consolidatedInstances: make(map[string]*cueConsolidatedInstance), + exportedGoldenFiles: make(map[string]*GoldenFile), // key: filename + cueTestRules: make(map[string]*cueTest), + genConsolidatedInstances: true, + genExportedInstances: conf.cueGenExportedInstance, + } + + // Process each CUE file + for fname, cueFile := range cueFiles { + pkg := cueFile.ast.PackageName() + imports := extractImports(cueFile.ast) + + if conf.cueTestGoldenSuffix != "" { + if gd, found := cueTestGoldenfiles[cueFile.rel]; found { + // log.Printf("Found golden file %s for CUE file %s", gd.name, cueFile.rel) + ctx.exportedGoldenFiles[gd.name] = gd + } + } + + if pkg == "" { + processStandaloneFile(ctx, fname, imports) + } else { + processPackageFile(ctx, cueFile.rel, fname, pkg, imports) + } + } + + // Generate rules + var res language.GenerateResult + + // Generate cue_module rule if in cue.mod directory + // log.Printf("DEBUG dir: %s, args: %s\n, modelLabel: %s", args.Dir, args.Rel, ctx.moduleLabel) + if ctx.isCueModDir { + moduleRule := generateCueModuleRule(args.Rel) + res.Gen = append(res.Gen, moduleRule) + } + + // Generate all rules + res.Gen = append(res.Gen, generateRules(ctx)...) + + // Set imports for dependency resolution + res.Imports = make([]any, len(res.Gen)) + for i, r := range res.Gen { + res.Imports[i] = r.PrivateAttr(config.GazelleImportsKey) + } + + // Generate empty rules + res.Empty = generateEmpty(args.File, ctx.isCueModDir, ctx.instances, + ctx.exportedInstances, ctx.exportedFiles, ctx.consolidatedInstances, ctx.cueTestRules, ctx.exportedGoldenFiles) + + return res +} + +// Context to hold all the data needed during rule generation +type ruleGenerationContext struct { + config *cueConfig + implicitPkgName string + baseImportPath string + isCueModDir bool + moduleLabel string + instances map[string]*cueInstance + exportedInstances map[string]*cueExportedInstance + exportedFiles map[string]*cueExportedFiles + cueTestRules map[string]*cueTest + exportedGoldenFiles map[string]*GoldenFile + consolidatedInstances map[string]*cueConsolidatedInstance + genExportedFiles bool + genExportedInstances bool + genConsolidatedInstances bool +} + +// cueFile represents a CUE file with its AST and path information + +// GoldenFile represents a golden file used in CUE tests +type GoldenFile struct { + path string + rel string + name string +} + +// ListGoldenFiles finds all golden files in the given directory that match the configured suffix +// Returns a map of base name (without suffix) to GoldenFile +func listGoldenFiles(args language.GenerateArgs, goldenSuffix string) (map[string]*GoldenFile /*key: pth*/, error) { + result := make(map[string]*GoldenFile) + if goldenSuffix == "" { + return result, nil + } + + for _, f := range args.RegularFiles { + if !strings.HasSuffix(f, goldenSuffix) { + continue + } + // Extract base name without the golden suffix + pth := filepath.Join(args.Dir, f) + rel := args.Rel + //NOTE(yuan): only supports one golden file per directory + result[rel] = &GoldenFile{ + path: pth, + rel: rel, + name: f, + } + } + + return result, nil +} + +type cueFile struct { + ast *ast.File + rel string + pth string +} + +// Parse all CUE files in the directory +func parseCueFiles(args language.GenerateArgs) map[string]*cueFile { + cueFiles := make(map[string]*cueFile) + for _, f := range append(args.RegularFiles, args.GenFiles...) { + // Only generate Cue entries for cue files (.cue) + if !strings.HasSuffix(f, ".cue") { + continue + } + + pth := filepath.Join(args.Dir, f) + a, err := parser.ParseFile(pth, nil) + if err != nil { + log.Printf("parsing cue file: path=%q, err=%+v", pth, err) + continue + } + + cueFiles[f] = &cueFile{ + ast: a, + rel: args.Rel, + pth: pth, + } + } + return cueFiles +} + +// Extract imports from a CUE file +func extractImports(cueFile *ast.File) []string { + var imports []string + for _, imprt := range cueFile.Imports { + imports = append(imports, strings.Trim(imprt.Path.Value, "\"")) + } + return imports +} + +// Process a standalone CUE file (no package) +func processStandaloneFile(ctx *ruleGenerationContext, fname string, imports []string) { + tgt := exportName(fname) + + // if exportedFiles inited, then process exported files + if ctx.genExportedFiles { + exportedFilesName := fmt.Sprintf("%s_cue_exported_files", tgt) + exportedFile, ok := ctx.exportedFiles[exportedFilesName] + if !ok { + exportedFile = &cueExportedFiles{ + Name: exportedFilesName, + Module: ctx.moduleLabel, + Imports: make(map[string]bool), + Srcs: []string{fname}, + OutputFormat: ctx.config.cueOutputFormat, + } + ctx.exportedFiles[exportedFilesName] = exportedFile + } + for _, imprt := range imports { + exportedFile.Imports[imprt] = true + } + } +} + +// Process a CUE file with a package +func processPackageFile( + ctx *ruleGenerationContext, + rel string, // relative path of cue file + fname, pkg string, + imports []string, +) { + // Process instance + instanceTgt := fmt.Sprintf("%s_cue_instance", pkg) + instance, ok := ctx.instances[instanceTgt] + + if !ok { + instance = &cueInstance{ + Name: instanceTgt, + PackageName: pkg, + Imports: make(map[string]bool), + Module: ctx.moduleLabel, + RelativePath: rel, + } + // find parent instance with prefix + ctx.instances[instanceTgt] = instance + } + + instance.Srcs = append(instance.Srcs, fname) + for _, imprt := range imports { + instance.Imports[imprt] = true + } + + // Process exported files + if ctx.genExportedFiles { + exportedFilesName := fmt.Sprintf("%s_cue_exported_files", pkg) + exportedFile, ok := ctx.exportedFiles[exportedFilesName] + if !ok { + exportedFile = &cueExportedFiles{ + Name: exportedFilesName, + Module: ctx.moduleLabel, + Imports: make(map[string]bool), + OutputFormat: ctx.config.cueOutputFormat, + } + ctx.exportedFiles[exportedFilesName] = exportedFile + } + + for _, imprt := range imports { + exportedFile.Imports[imprt] = true + } + + if len(instance.Srcs) > 0 { + exportedFile.Srcs = instance.Srcs + } + } + + if ctx.genConsolidatedInstances { + // Process consolidated instance + consolidatedName := fmt.Sprintf("%s_cue_def", pkg) + consolidated, ok := ctx.consolidatedInstances[consolidatedName] + if !ok { + consolidated = &cueConsolidatedInstance{ + Name: consolidatedName, + Instance: instanceTgt, + PackageName: pkg, + Imports: make(map[string]bool), + } + ctx.consolidatedInstances[consolidatedName] = consolidated + } + for _, imprt := range imports { + consolidated.Imports[imprt] = true + } + } +} + +// Generate a cue_module rule +func generateCueModuleRule(rel string) *rule.Rule { + cueModule := &cueModule{ + Name: "cue.mod", + } + moduleRule := cueModule.ToRule() + + // Register this cue_module for later use in resolution + RegisterCueModule(fmt.Sprintf("//%s:cue.mod", rel), rel) + + return moduleRule +} + +// genCueTestRule generates a cue_test rule for validating CUE code against golden files. +// It creates test rules that compare the output of CUE evaluation with expected results. +func genCueTestRule(ctx *ruleGenerationContext, tgt string, exportedFilesName string) { + // NOTE(yuan): The cue_test rule will generate a target with suffix _test, so we use _cue as the target name + // then the final target name will be %s_cue_test + tn := fmt.Sprintf("%s_cue", tgt) + if _, ok := ctx.cueTestRules[tn]; ok { + return + } + goldenFileName := ctx.config.cueTestGoldenFilename + if goldenFileName == "" { + // If no golden file name is specified, get the first available golden file + for _, gf := range ctx.exportedGoldenFiles { + goldenFileName = gf.name + break + } + if goldenFileName == "" { + return + } + } + + if !strings.HasSuffix(goldenFileName, "."+ctx.config.cueOutputFormat) { + // If the golden file doesn't have the correct output format extension, + // break and skip this file as the formats don't match + return + } + + testRule := &cueTest{ + Name: tn, + GoldenFile: ":" + goldenFileName, + GeneratedOutputFile: ":" + exportedFilesName + "." + ctx.config.cueOutputFormat, + } + ctx.cueTestRules[tn] = testRule +} + +// Generate all rules from the context +func generateRules(ctx *ruleGenerationContext) []*rule.Rule { + var rules []*rule.Rule + + // Generate @rules_cue instance + for _, instance := range ctx.instances { + rules = append(rules, instance.ToRule()) + + if ctx.genExportedInstances { + if len(ctx.exportedGoldenFiles) == 0 { + continue + } + // Create a cue_exported_instance rule for each instance + exportedInstanceName := instance.Name + "_exported" + if _, ok := ctx.exportedInstances[exportedInstanceName]; !ok { + exportedInstance := &cueExportedInstance{ + Name: exportedInstanceName, + Instance: instance.TargetName(), + Imports: instance.Imports, + OutputFormat: ctx.config.cueOutputFormat, + } + ctx.exportedInstances[exportedInstanceName] = exportedInstance + // Generate test rule if golden suffix or filename is specified + // Log the exported golden files for debugging + // Generate test rule if we have golden files and golden suffix or filename is configured + if ctx.config.cueTestGoldenSuffix != "" || ctx.config.cueTestGoldenFilename != "" { + genCueTestRule(ctx, instance.Name, exportedInstanceName) + } + } + } + } + + for _, exportedInstance := range ctx.exportedInstances { + rules = append(rules, exportedInstance.ToRule()) + } + + for _, exportedFile := range ctx.exportedFiles { + rules = append(rules, exportedFile.ToRule()) + } + + for _, consolidated := range ctx.consolidatedInstances { + rules = append(rules, consolidated.ToRule()) + } + + for _, ts := range ctx.cueTestRules { + rules = append(rules, ts.ToRule()) + } + + for _, es := range ctx.exportedGoldenFiles { + r := rule.NewRule("cue_gen_golden", "golden_"+es.name) + r.SetAttr("srcs", []string{es.name}) + rules = append(rules, r) + } + + return rules +} + +// findNearestCueModule searches for a cue.mod directory up the directory tree +// and returns the label to the cue_module rule. +func findNearestCueModule(dir, rel string) string { + currentDir := dir + currentRel := rel + + for { + cueModPath := filepath.Join(currentDir, "cue.mod") + if info, err := os.Stat(cueModPath); err == nil && info.IsDir() { + if currentRel == "" || currentRel == "." { + return "//cue.mod:cue.mod" + } + return fmt.Sprintf("//%s/cue.mod:cue.mod", currentRel) + } + + // If we're at the root, we're done + if currentRel == "" || currentRel == "." { + break + } + + // Move up one directory + currentDir = filepath.Dir(currentDir) + currentRel = filepath.Dir(currentRel) + if currentRel == "." { + currentRel = "" + } + } + return "" +} + +func computeImportPath(args language.GenerateArgs) string { + conf := GetConfig(args.Config) + + suffix, err := filepath.Rel(conf.prefixRel, args.Rel) + if err != nil { + log.Printf("Failed to compute importpath: rel=%q, prefixRel=%q, err=%+v", args.Rel, conf.prefixRel, err) + return args.Rel + } + if suffix == "." { + return conf.prefix + } + + return filepath.Join(conf.prefix, suffix) +} + +func exportName(basename string) string { + parts := strings.Split(basename, ".") + return strcase.ToSnake(strings.Join(parts[:len(parts)-1], "_")) +} + +func generateEmpty( + f *rule.File, + isCueModDir bool, + instances map[string]*cueInstance, + exportedInstances map[string]*cueExportedInstance, + exportedFiles map[string]*cueExportedFiles, + consolidatedInstances map[string]*cueConsolidatedInstance, + cueTestRules map[string]*cueTest, + goldenFiles map[string]*GoldenFile, +) []*rule.Rule { + if f == nil { + return nil + } + + var empty []*rule.Rule + + // Helper function to check if a rule should be marked as empty + checkAndMarkEmpty := func(kind, name string, exists bool) { + if !exists { + empty = append(empty, rule.NewRule(kind, name)) + } + } + + for _, r := range f.Rules { + kind := r.Kind() + name := r.Name() + + switch kind { + case "cue_library", "cue_export": + // NOTE: mark all cue_library and cue_export as empty + // since cue_library is depreacted + checkAndMarkEmpty(kind, name, true) + case "cue_instance": + _, exists := instances[name] + checkAndMarkEmpty(kind, name, exists) + case "cue_consolidated_instance": + if len(consolidatedInstances) > 0 { + _, exists := consolidatedInstances[name] + checkAndMarkEmpty(kind, name, exists) + } + case "cue_exported_instance", "cue_exported_standalone_files": + if len(exportedInstances) > 0 { + _, exists := exportedInstances[name] + checkAndMarkEmpty(kind, name, exists) + } + case "cue_exported_files": + if len(exportedFiles) > 0 { + _, exists := exportedFiles[name] + checkAndMarkEmpty(kind, name, exists) + } + case "cue_gen_golden": + _, exists := goldenFiles[name] + checkAndMarkEmpty(kind, name, exists) + case "cue_test": + _, exists := cueTestRules[name] + checkAndMarkEmpty(kind, name, exists) + case "cue_module": + if !isCueModDir { + checkAndMarkEmpty(kind, name, false) + } + } + // Don't mark other rule types as empty + } + + return empty +} + +// @rules_cue rule types +type cueInstance struct { + Name string + PackageName string + Srcs []string + Imports map[string]bool + RelativePath string + Module string // Reference to the nearest cue_module +} + +func (ci *cueInstance) ToRule() *rule.Rule { + rule := rule.NewRule("cue_instance", ci.Name) + sort.Strings(ci.Srcs) + rule.SetAttr("srcs", ci.Srcs) + rule.SetAttr("package_name", ci.PackageName) + rule.SetAttr("visibility", []string{"//visibility:public"}) + + // set ancestor to module and update later after resolve + if ci.Module != "" { + rule.SetAttr("ancestor", ci.Module) + } + + var deps []string + for dep := range ci.Imports { + deps = append(deps, dep) + } + sort.Strings(deps) + rule.SetPrivateAttr(config.GazelleImportsKey, deps) + return rule +} + +// Implement TargetName method for cueInstance +func (ci *cueInstance) TargetName() string { + return ci.Name +} + +type cueExportedInstance struct { + Name string + Instance string + Src string + Imports map[string]bool + OutputFormat string +} + +func (cei *cueExportedInstance) ToRule() *rule.Rule { + var r *rule.Rule + if cei.Instance != "" { + r = rule.NewRule("cue_exported_instance", cei.Name) + r.SetAttr("instance", ":"+cei.Instance) + } else { + r = rule.NewRule("cue_exported_standalone_files", cei.Name) + r.SetAttr("srcs", []string{cei.Src}) + } + r.SetAttr("visibility", []string{"//visibility:public"}) + + // Use the outputFormat field + if cei.OutputFormat != "" { + r.SetAttr("output_format", cei.OutputFormat) + } + + return r +} + +// cueExportedFiles represents a cue_exported_files rule that exports CUE files to various formats. +// It supports exporting multiple source files with configurable output formats, expressions, and injected values. +// +// Example: +// ```python +// cue_exported_files( +// +// name = "config_exported", +// module = ":cue.mod", +// srcs = [ +// "config.cue", +// "defaults.cue", +// ], +// qualified_srcs = { +// "values.yaml": "yaml", +// }, +// output_format = "yaml", +// result = "config.yaml", +// expression = "config", +// inject = { +// "environment": "production", +// "version": "1.2.3", +// }, +// inject_system_variables = True, +// with_context = True, +// visibility = ["//visibility:public"], +// +// ) +type cueExportedFiles struct { + Name string + Module string + Srcs []string + Imports map[string]bool + OutputFormat string +} + +func (cef *cueExportedFiles) ToRule() *rule.Rule { + r := rule.NewRule("cue_exported_files", cef.Name) + r.SetAttr("module", cef.Module) + r.SetAttr("visibility", []string{"//visibility:public"}) + r.SetAttr("srcs", cef.Srcs) + + // Use the outputFormat field + if cef.OutputFormat != "" { + r.SetAttr("output_format", cef.OutputFormat) + } + + var imprts []string + for imprt := range cef.Imports { + imprts = append(imprts, imprt) + } + sort.Strings(imprts) + r.SetPrivateAttr(config.GazelleImportsKey, imprts) + return r +} + +// Add cue_module type +type cueModule struct { + Name string +} + +func (cm *cueModule) ToRule() *rule.Rule { + r := rule.NewRule("cue_module", cm.Name) + r.SetAttr("visibility", []string{"//visibility:public"}) + return r +} + +// cueConsolidatedInstance definition +type cueConsolidatedInstance struct { + Name string + Instance string + Src string + PackageName string + Imports map[string]bool +} + +func (cci *cueConsolidatedInstance) ToRule() *rule.Rule { + var r *rule.Rule + if cci.Instance != "" { + r = rule.NewRule("cue_consolidated_instance", cci.Name) + r.SetAttr("instance", ":"+cci.Instance) + } else { + r = rule.NewRule("cue_consolidated_standalone_files", cci.Name) + r.SetAttr("srcs", []string{cci.Src}) + } + r.SetAttr("visibility", []string{"//visibility:public"}) + + // Always set output_format to "cue" for consolidated instances + r.SetAttr("output_format", "cue") + return r +} + +// cueTest represents a CUE test rule that can be used to validate CUE code +// against expected outputs (golden files). It allows for testing CUE configurations +// and ensuring they produce the expected results when evaluated. +type cueTest struct { + Name string + GoldenFile string + GeneratedOutputFile string +} + +func (ct *cueTest) ToRule() *rule.Rule { + r := rule.NewRule("cue_test", ct.Name) + r.SetAttr("generated_output_file", ct.GeneratedOutputFile) + r.SetAttr("golden_file", ct.GoldenFile) + return r +} diff --git a/gazelle/cue/resolve.go b/gazelle/cue/resolve.go new file mode 100644 index 0000000..14cc48d --- /dev/null +++ b/gazelle/cue/resolve.go @@ -0,0 +1,610 @@ +package cuelang + +import ( + "fmt" + "log" + "os" + "path" + "path/filepath" + "sort" + "strings" + "sync" + + "cuelang.org/go/cue/parser" + "github.com/bazelbuild/bazel-gazelle/config" + "github.com/bazelbuild/bazel-gazelle/label" + "github.com/bazelbuild/bazel-gazelle/pathtools" + "github.com/bazelbuild/bazel-gazelle/repo" + "github.com/bazelbuild/bazel-gazelle/resolve" + "github.com/bazelbuild/bazel-gazelle/rule" +) + +// CueModuleInfo stores information about a cue_module +type CueModuleInfo struct { + Label string // Bazel label of the cue_module + Dir string // Directory path where cue.mod is located +} + +var ( + cueModulesMu sync.RWMutex + cueModules map[string]*CueModuleInfo // Maps module label to info + cueModIndex map[*CueModuleInfo]map[string]string // Maps CueModuleInfo to import paths to Bazel targets + debugModIndex bool + + cueModuleKnownDirs = []string{"gen", "usr", "pkg"} +) + +// Initialize the maps +func init() { + cueModules = make(map[string]*CueModuleInfo) + cueModIndex = make(map[*CueModuleInfo]map[string]string) +} + +// RegisterCueModule registers a cue_module for later use in resolution +func RegisterCueModule(label, dir string) { + cueModulesMu.Lock() + defer cueModulesMu.Unlock() + + cueModules[label] = &CueModuleInfo{ + Label: label, + Dir: dir, + } + + // Index all relevant directories if they exist + for _, subdir := range cueModuleKnownDirs { + dirPath := filepath.Join(dir, subdir) + if _, err := os.Stat(dirPath); err == nil { + indexCueModuleDir(cueModules[label], subdir, dirPath) + } + } +} + +// indexCueModuleDir builds an index of available targets in a cue.mod subdirectory +// also build cuePathIndex for ancestor +func indexCueModuleDir(moduleInfo *CueModuleInfo, subdir, dirPath string) { + // Initialize the index map for this module if it doesn't exist + if _, ok := cueModIndex[moduleInfo]; !ok { + cueModIndex[moduleInfo] = make(map[string]string) + } + err := filepath.Walk(dirPath, func(filePath string, fileInfo os.FileInfo, walkErr error) error { + if walkErr != nil { + return walkErr + } + + // Skip directories + if fileInfo.IsDir() { + return nil + } + + // Only look at .cue files to identify packages + if !strings.HasSuffix(filePath, ".cue") { + return nil + } + + // Get the relative path from the subdirectory + relPath, err := filepath.Rel(dirPath, filepath.Dir(filePath)) + if err != nil { + return err + } + + // Convert to import path format + importPath := strings.ReplaceAll(relPath, string(filepath.Separator), "/") + + // Parse the CUE file using the CUE parser + cueFile, err := parser.ParseFile(filePath, nil) + if err != nil { + log.Printf("Warning: Error parsing CUE file %s: %v", filePath, err) + return nil + } + + // Extract package name directly from the AST + pkgName := cueFile.PackageName() + if pkgName == "" { + // Skip files without package declarations + return nil + } + + // Extract module path from label (removing the target part) + modulePath := moduleInfo.Label + if idx := strings.LastIndex(modulePath, ":"); idx >= 0 { + modulePath = modulePath[:idx] + } + + // Create target label with module path prefix + instanceTarget := fmt.Sprintf("%s/%s/%s:%s_cue_instance", modulePath, subdir, importPath, pkgName) + + // Index by different references + indexReferences(moduleInfo, importPath, pkgName, instanceTarget) + + return nil + }) + + if err != nil { + log.Printf("Warning: Error walking cue.mod/%s directory: %v", subdir, err) + } +} + +// indexReferences adds various import path patterns to the index +func indexReferences(moduleInfo *CueModuleInfo, importPath, pkgName, instanceTarget string) { + // 1. Index by full import path + cueModIndex[moduleInfo][importPath] = instanceTarget + + // 2. Index by just package name for simple imports + if pkgName != "" { + cueModIndex[moduleInfo][pkgName] = instanceTarget + } + + // 3. Index all possible colon-style imports (path prefixes + pkg) + indexColonStyles(moduleInfo, importPath, pkgName, instanceTarget) + + // 4. Index domain-specific imports + indexDomainSpecific(moduleInfo, importPath, pkgName, instanceTarget) + + if debugModIndex { + log.Printf("Debug: Indexed CUE module target: %s -> %s", importPath, instanceTarget) + } +} + +// indexColonStyles adds colon-style import references to the index +func indexColonStyles(moduleInfo *CueModuleInfo, importPath, pkgName, instanceTarget string) { + // Add colon-style import paths for each directory level + // This allows imports like "domain.com/pkg/path:pkg" to be resolved + pathParts := strings.Split(importPath, "/") + for i := 0; i < len(pathParts); i++ { + if i > 0 { + // For each valid path prefix, create a colon import with the package name + prefix := strings.Join(pathParts[:i+1], "/") + colonImport := fmt.Sprintf("%s:%s", prefix, pkgName) + cueModIndex[moduleInfo][colonImport] = instanceTarget + + if debugModIndex { + log.Printf("Debug: Added colon import index: %s -> %s", colonImport, instanceTarget) + } + } + } + + // Add full path with colon to support imports like "full/import/path:pkg" + fullColonImport := fmt.Sprintf("%s:%s", importPath, pkgName) + cueModIndex[moduleInfo][fullColonImport] = instanceTarget + + if debugModIndex { + log.Printf("Debug: Added full colon import index: %s -> %s", fullColonImport, instanceTarget) + } +} + +// indexDomainSpecific adds domain-specific import references to the index +func indexDomainSpecific(moduleInfo *CueModuleInfo, importPath, pkgName, instanceTarget string) { + // Index common domain imports (k8s.io, sigs.k8s.io, github.com, etc.) + for _, prefix := range []string{"k8s.io/", "sigs.k8s.io/", "github.com/", "nvda.ai/"} { + if strings.HasPrefix(importPath, prefix) { + parts := strings.SplitN(importPath, "/", 3) + if len(parts) >= 3 { + // Index the domain import + domainImport := strings.Join(parts, "/") + cueModIndex[moduleInfo][domainImport] = instanceTarget + + // Add colon-style import for domain + colonDomainImport := fmt.Sprintf("%s:%s", domainImport, pkgName) + cueModIndex[moduleInfo][colonDomainImport] = instanceTarget + + if debugModIndex { + log.Printf("Debug: Added domain import index: %s -> %s", domainImport, instanceTarget) + log.Printf("Debug: Added domain colon import index: %s -> %s", colonDomainImport, instanceTarget) + } + break + } + } + } +} + +// Imports returns a list of ImportSpecs that can be used to import +// the rule r. This is used to populate RuleIndex. +// +// If nil is returned, the rule will not be indexed. If any non-nil +// slice is returned, including an empty slice, the rule will be +// indexed. +func (cl *cueLang) Imports(c *config.Config, r *rule.Rule, f *rule.File) []resolve.ImportSpec { + conf := GetConfig(c) + switch r.Kind() { + case "cue_instance": + if pkgName := r.AttrString("package_name"); pkgName != "" { + // Get the file package path from the context + filePkg := f.Pkg + + var importPath string + if conf.prefix != "" { + // If we have a prefix, trim the prefixRel from f.Pkg + relativePath := filePkg + if strings.HasPrefix(filePkg, conf.prefixRel) { + relativePath = strings.TrimPrefix(filePkg, conf.prefixRel) + // Remove leading slash if present + relativePath = strings.TrimPrefix(relativePath, "/") + } + + // Join the prefix with the relative path + if pkgName == path.Base(relativePath) { + // When pkgName matches the last component of the path + // Use the pattern: prefix + relativePath + importPath = path.Join(conf.prefix, relativePath) + } else { + // Otherwise append the pkgName + // Use the pattern: prefix + relativePath + ":" + pkgName + importPath = path.Join(conf.prefix, relativePath) + ":" + pkgName + } + } else { + // If no prefix, just use the file package path + pkgName + if pkgName == path.Base(filePkg) { + importPath = filePkg + } else { + importPath = filePkg + ":" + pkgName + } + } + + // Create the primary import spec + specs := []resolve.ImportSpec{ + { + Lang: cueName, + Imp: importPath, + }, + } + // Add a non-colon version if the import path contains a colon + if strings.Contains(importPath, ":") { + // Extract the path part without the colon and package name + pathPart := importPath[:strings.LastIndex(importPath, ":")] + + // Only add if it's different from the original import path + if pathPart != importPath { + specs = append(specs, resolve.ImportSpec{ + Lang: cueName, + Imp: pathPart, + }) + } + } + return specs + } + case "cue_module": + // Register this cue_module for later use in resolution + if f != nil && f.Pkg != "" { + moduleLabel := "//" + f.Pkg + ":" + r.Name() + moduleDir := filepath.Join(c.RepoRoot, f.Pkg) + RegisterCueModule(moduleLabel, moduleDir) + } + } + return nil +} + +// Embeds returns a list of labels of rules that the given rule +// embeds. If a rule is embedded by another importable rule of the +// same language, only the embedding rule will be indexed. The +// embedding rule will inherit the imports of the embedded rule. +func (cl *cueLang) Embeds(r *rule.Rule, from label.Label) []label.Label { + // Cue doesn't have a concept of embedding as far as I know. + return nil +} + +// Resolve translates imported libraries for a given rule into Bazel +// dependencies. A list of imported libraries is typically stored in a +// private attribute of the rule when it's generated (this interface +// doesn't dictate how that is stored or represented). Resolve +// generates a "deps" attribute (or the appropriate language-specific +// equivalent) for each import according to language-specific rules +// and heuristics. +func (cl *cueLang) Resolve(c *config.Config, ix *resolve.RuleIndex, rc *repo.RemoteCache, r *rule.Rule, imports interface{}, from label.Label) { + if imports == nil { + return + } + imps := imports.([]string) + r.DelAttr("deps") + depSet := make(map[string]bool) + + // Get the ancestor attribute for cue_instance or module attribute for cue_exported_files + var cueModule string + if r.Kind() == "cue_instance" { + cueModule = r.AttrString("ancestor") + } else if r.Kind() == "cue_exported_files" { + cueModule = r.AttrString("module") + } + + for _, imp := range imps { + // Skip standard library imports + if _, ok := stdlib[imp]; ok { + continue + } + + if debugModIndex { + log.Printf("DEBUG Resolve: kind: %s, cueModule: %s, import_path: %s\n", r.Kind(), cueModule, imp) + } + + // Stage 1: Try to find the import in cue.mod indexes + if resolved := tryResolveFromModuleIndex(imp, cueModule, depSet, debugModIndex); resolved { + continue + } + + // Stage 2: Try to find in rule index + if resolved := tryResolveFromRuleIndex(c, ix, imp, from, depSet, debugModIndex); resolved { + continue + } + + // Stage 3: Resort to creating labels based on import path + createLabelFromImportPath(imp, rc, depSet, r.Kind(), debugModIndex) + } + + if len(depSet) > 0 { + deps := make([]string, 0, len(depSet)) + for dep := range depSet { + if debugModIndex { + log.Printf("Resolve: Adding dependency: %s", dep) + } + deps = append(deps, dep) + } + sort.Strings(deps) + r.SetAttr("deps", deps) + } + + ResolveAncestor(c, r, ix, from) +} + +// ResolveAncestor resolves the ancestor path for a cue_instance rule +// It takes the relative path stored in the CueAncestorKey private attribute +// and resolves it to the appropriate ancestor attribute value +func ResolveAncestor(c *config.Config, r *rule.Rule, ix *resolve.RuleIndex, from label.Label) { + if r.Kind() != "cue_instance" { + return + } + + // Start with the current label and walk up the directory tree + currentLabel := from + for { + // Get the import path based on the current path + importPath := LabelPkgToImportPath(c, currentLabel) + + // If we've reached cue.mod level (empty import path), stop + if importPath == "" { + break + } + + // Look for rules that might match this import path + ancestorResults := ix.FindRulesByImportWithConfig(c, + resolve.ImportSpec{ + Lang: cueName, + Imp: importPath, + }, cueName) + + // Find a rule with the same name as the current package + for _, res := range ancestorResults { + if res.Label.Name == currentLabel.Name { + ancestorLabel := res.Label.Rel(from.Repo, "") + r.SetAttr("ancestor", ancestorLabel.String()) + return + } + } + + // Move up one directory level + parentPkg := path.Dir(currentLabel.Pkg) + if parentPkg == currentLabel.Pkg { + // We've reached the root, stop + break + } + + // Create a new label for the parent directory with the same name + currentLabel = label.Label{ + Repo: currentLabel.Repo, + Pkg: parentPkg, + Name: currentLabel.Name, // Use the current directory name for ancestor lookup + } + } +} + +func LabelPkgToImportPath(c *config.Config, label label.Label) string { + // LabelPkgToImportPath converts a Bazel package path to a Go import path + // using the Gazelle prefix and prefix relative path configuration. + // + // For example: + // - Gazelle Prefix: example.com + // - Gazelle PrefixRel: test/testdata/gazelle + // - Pkg: test/testdata/gazelle + // Result: example.com/gazelle (using base name since pkg equals prefixRel) + // + // - Gazelle Prefix: example.com + // - Gazelle PrefixRel: test/testdata/gazelle + // - Pkg: test/testdata/gazelle/subdir + // Result: example.com/gazelle/subdir (using relative path) + + conf := GetConfig(c) + pkg := label.Pkg + prefix := conf.prefix + prefixRel := conf.prefixRel + + // NOTE(yuan): use cue.mod as ancestor for cue.mod + if strings.Contains(pkg, "cue.mod") { + return "" + } + + if pkg == prefixRel { + // If pkg equals prefixRel, use the base name of the package + return path.Join(prefix, path.Base(pkg)) + } + + // If pkg is under prefixRel, compute the relative path and join with prefix + if strings.HasPrefix(pkg, prefixRel+"/") { + relPath := strings.TrimPrefix(pkg, prefixRel+"/") + return path.Join(prefix, path.Dir(relPath)) + } + + // Get the parent directory of the package path + // If the package path contains "cue.mod", split it and use the second part + return path.Dir(pkg) +} + +// tryResolveFromModuleIndex checks if the import exists in cue module indexes +func tryResolveFromModuleIndex(imp, cueModule string, depSet map[string]bool, debugMode bool) bool { + // Case 1: Check specific module if provided + if cueModule != "" { + cueModulesMu.RLock() + defer cueModulesMu.RUnlock() + + if moduleInfo, ok := cueModules[cueModule]; ok { + modIndex := cueModIndex[moduleInfo] + if target, ok := modIndex[imp]; ok { + if debugMode { + log.Printf("DEBUG: Found import %s in cue.mod index: %s", imp, target) + } + depSet[target] = true + return true + } + } + return false + } + + // Case 2: Check all modules if no specific module provided + cueModulesMu.RLock() + defer cueModulesMu.RUnlock() + + for _, moduleInfo := range cueModules { + modIndex := cueModIndex[moduleInfo] + if target, ok := modIndex[imp]; ok { + if debugMode { + log.Printf("DEBUG: Found import %s in global cue.mod index: %s", imp, target) + } + depSet[target] = true + return true + } + } + + if debugMode { + log.Printf("%s not found in module, trying rule index next\n", imp) + } + return false +} + +// tryResolveFromRuleIndex tries to find the import in the rule index +func tryResolveFromRuleIndex(c *config.Config, ix *resolve.RuleIndex, imp string, from label.Label, depSet map[string]bool, debugMode bool) bool { + res := ix.FindRulesByImportWithConfig(c, + resolve.ImportSpec{ + Lang: cueName, + Imp: imp, + }, cueName) + + if len(res) > 0 { + for _, entry := range res { + l := entry.Label.Rel(from.Repo, from.Pkg) + depSet[l.String()] = true + } + return true + } + return false +} + +// createLabelFromImportPath creates a label based on the import path +func createLabelFromImportPath(imp string, rc *repo.RemoteCache, depSet map[string]bool, ruleKind string, debugMode bool) { + // Check if the import has a colon (like "path/to/pkg:pkgname") + colonIndex := strings.LastIndex(imp, ":") + hasColon := colonIndex >= 0 + + // Extract path and package name + var pathPart, pkgName string + var importToResolve string + + if hasColon { + pathPart = imp[:colonIndex] + pkgName = imp[colonIndex+1:] + importToResolve = pathPart // Use the path part for root resolution + if debugMode { + log.Printf("DEBUG: Processing colon import: path=%s, pkg=%s", pathPart, pkgName) + } + } else { + importToResolve = imp + } + + prefix, repo, err := rc.Root(importToResolve) + // Handle error from rc.Root() + if err != nil { + log.Printf("Error resolving import %q: %v", importToResolve, err) + return + } + + // rc.Root succeeded - create appropriate label + if hasColon { + // For colon imports, trim prefix from path part if needed + if pathtools.HasPrefix(pathPart, prefix) { + pathPart = pathtools.TrimPrefix(pathPart, prefix) + } + instanceLabel := label.New(repo, pathPart, fmt.Sprintf("%s_cue_instance", pkgName)) + if isInstanceKind(ruleKind) { + depSet[instanceLabel.String()] = true + if debugMode { + log.Printf("DEBUG: Created dependency for colon import %s: %s", imp, instanceLabel.String()) + } + } + } else { + // Regular import (no colon) + var pkg string + if pathtools.HasPrefix(imp, prefix) { + pkg = pathtools.TrimPrefix(imp, prefix) + } + + if pkg != "" { + cuePkg := path.Base(pkg) + instanceLabel := label.New(repo, pkg, fmt.Sprintf("%s_cue_instance", cuePkg)) + + if isInstanceKind(ruleKind) { + depSet[instanceLabel.String()] = true + if debugMode { + log.Printf("DEBUG: Created dependency for regular import %s: %s", imp, instanceLabel.String()) + } + } + } + } +} + +// Helper function to check if a rule kind should use instance labels +func isInstanceKind(kind string) bool { + instanceKinds := map[string]bool{ + "cue_exported_files": true, + "cue_instance": true, + "cue_exported_instance": true, + "cue_exported_standalone_files": true, + "cue_consolidated_instance": true, + } + return instanceKinds[kind] +} + +var stdlib = map[string]bool{ + "crypto/md5": true, + "crypto/sha1": true, + "crypto/sha256": true, + "crypto/sha512": true, + "encoding/base64": true, + "encoding/csv": true, + "encoding/hex": true, + "encoding/json": true, + "encoding/yaml": true, + "html": true, + "list": true, + "math": true, + "math/bits": true, + "net": true, + "path": true, + "regexp": true, + "strconv": true, + "strings": true, + "struct": true, + "text/tabwriter": true, + "text/template": true, + "time": true, + "tool": true, + "tool/cli": true, + "tool/exec": true, + "tool/file": true, + "tool/http": true, + "tool/os": true, + // New in CUE 0.12 + "crypto/hmac": true, + "encoding/binary": true, + "encoding/pem": true, + "io": true, + "math/rand": true, + "net/url": true, + "path/filepath": true, + "uuid": true, +} diff --git a/go.mod b/go.mod index 5106250..5448363 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,17 @@ module github.com/seh/rules_cue -go 1.17 +go 1.24.0 + +require ( + cuelang.org/go v0.15.1 + github.com/bazelbuild/bazel-gazelle v0.47.0 + github.com/iancoleman/strcase v0.3.0 +) + +require ( + github.com/bazelbuild/buildtools v0.0.0-20250930140053-2eb4fccefb52 // indirect + github.com/cockroachdb/apd/v3 v3.2.1 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/tools/go/vcs v0.1.0-deprecated // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3d08a22 --- /dev/null +++ b/go.sum @@ -0,0 +1,17 @@ +cuelang.org/go v0.15.1 h1:MRnjc/KJE+K42rnJ3a+425f1jqXeOOgq9SK4tYRTtWw= +cuelang.org/go v0.15.1/go.mod h1:NYw6n4akZcTjA7QQwJ1/gqWrrhsN4aZwhcAL0jv9rZE= +github.com/bazelbuild/bazel-gazelle v0.47.0 h1:g3Rr1ZbkC1Pk20aOgBITxSD/efS1WbaSty5jC786Z3Q= +github.com/bazelbuild/bazel-gazelle v0.47.0/go.mod h1:8Ozf20jhv+in87nCUHdmUPPcVGTfKg/gotZ/hce3T+w= +github.com/bazelbuild/buildtools v0.0.0-20250930140053-2eb4fccefb52 h1:njQAmjTv/YHRm/0Lfv9DXHFZ4MdT2IA/RKHTnqZkgDw= +github.com/bazelbuild/buildtools v0.0.0-20250930140053-2eb4fccefb52/go.mod h1:PLNUetjLa77TCCziPsz0EI8a6CUxgC+1jgmWv0H25tg= +github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= +github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools/go/vcs v0.1.0-deprecated h1:cOIJqWBl99H1dH5LWizPa+0ImeeJq3t3cJjaeOWUAL4= +golang.org/x/tools/go/vcs v0.1.0-deprecated/go.mod h1:zUrvATBAvEI9535oC0yWYsLsHIV4Z7g63sNPVMtuBy8=