Skip to content

Instantly share code, notes, and snippets.

@alexeagle
Created September 12, 2024 19:01
Show Gist options
  • Save alexeagle/ad8ceccf382300dd2e247095c38d6e68 to your computer and use it in GitHub Desktop.
Save alexeagle/ad8ceccf382300dd2e247095c38d6e68 to your computer and use it in GitHub Desktop.
"""
rules_kotlin support for 'aspect configure'
"""
KT_JVM_LIBRARY = "kt_jvm_library"
KT_JVM_BINARY = "kt_jvm_binary"
RULES_KOTLIN_REPO_NAME = "io_bazel_rules_kotlin"
PROVIDER_NAME = "kt"
LANG_NAME = "kotlin"
aspect.register_rule_kind(KT_JVM_LIBRARY, {
"From": "@" + RULES_KOTLIN_REPO_NAME + "//kotlin:jvm.bzl",
"NonEmptyAttrs": ["srcs"],
"MergeableAttrs": ["srcs"],
"ResolveAttrs": ["deps"],
})
aspect.register_rule_kind(KT_JVM_BINARY, {
"From": "@" + RULES_KOTLIN_REPO_NAME + "//kotlin:jvm.bzl",
"NonEmptyAttrs": ["srcs", "main_class"],
})
def prepare(_):
return aspect.PrepareResult(
# All source files to be processed
sources = [
aspect.SourceExtensions(".kt", ".kts"),
],
queries = {
"imports": aspect.AstQuery(
grammar = "kotlin",
filter = "*.kt*",
query = """
(source_file
(import_list
(import_header (identifier) @imp (".*")? @is_star)
)
)
""",
),
"package_name": aspect.AstQuery(
grammar = "kotlin",
filter = "*.kt*",
query = """
(source_file
(package_header (identifier) @pkg)
)
""",
),
"has_main": aspect.AstQuery(
grammar = "kotlin",
filter = "*.kt*",
query = """
(source_file
(function_declaration
(simple_identifier) @variable.funcname
(#eq? @variable.funcname "main")
)
)
""",
),
},
)
# ctx:
# rel string
# properties map[string]string
# sources []TargetSource
#
# TargetSource:
# path string
# query_results QueryResults
#
# query_results:
# [query_key] bool|string|None
def declare_targets(ctx):
"""
This function declares targets based on the context.
Args:
ctx: The context object.
Returns:
a 'DeclareTargetsResult'
"""
# Every BUILD can have 1 library and multiple binaries
lib = {
"srcs": [],
"packages": [],
"imports": [],
}
bins = []
for file in ctx.sources:
pkg = file.query_results["package_name"][0].captures["pkg"] if "package_name" in file.query_results and len(file.query_results["package_name"]) > 0 else None
if "imports" not in file.query_results:
print(file.query_results)
import_paths = [
i.captures["imp"] if "is_star" in i.captures and i.captures["is_star"] else i.captures["imp"][:i.captures["imp"].rindex(".")]
for i in file.query_results["imports"]
if not is_native(i.captures["imp"])
]
# Trim the class name from the import for non-.* imports.
# Convert to TargetImport, exclude native imports
imports = [
aspect.Import(
id = i,
provider = PROVIDER_NAME,
src = file.path,
)
for i in import_paths
]
if len(file.query_results["has_main"]) > 0:
bins.append({
"src": file.path,
"imports": imports,
"package": aspect.Symbol(
id = pkg,
provider = PROVIDER_NAME,
) if pkg else None,
})
else:
lib["srcs"].append(file.path)
lib["imports"].extend(imports)
if pkg:
lib["packages"].append(aspect.Symbol(
id = pkg,
provider = PROVIDER_NAME,
))
lib_name = path.base(ctx.rel) if ctx.rel else ctx.repo_name
if len(lib["srcs"]) > 0:
ctx.targets.add(
name = lib_name,
kind = KT_JVM_LIBRARY,
attrs = {
"srcs": lib["srcs"],
"deps": lib["imports"] if len(lib["imports"]) > 0 else None,
},
symbols = lib["packages"],
)
else:
ctx.targets.remove(lib_name)
for bin in bins:
no_ext = bin["src"].removesuffix(path.ext(bin["src"]))
ctx.targets.add(
name = no_ext.lower() + "_bin",
kind = KT_JVM_BINARY,
attrs = {
"srcs": [bin["src"]],
"main_class": (bin["package"].id + "." + no_ext) if bin["package"] else no_ext,
"deps": bin["imports"] if len(bin["imports"]) > 0 else None,
},
symbols = [bin["package"]] if bin["package"] else [],
)
NATIVE_LIBS = [
"kotlin",
"kotlinx",
# Java, see rules_jvm gazelle plugin:
# https://github.com/bazel-contrib/rules_jvm/blob/v0.24.0/java/gazelle/private/java/java.go#L28-L147
"java",
"javax",
"com.sun",
"jdk",
"netscape.javascript",
"org.ietf.jgss",
"org.jcp.xml.dsig.internal",
"org.w3c.dom",
"org.xml.sax",
"sun",
]
def is_native(imp):
for lib in NATIVE_LIBS:
if imp == lib or imp.startswith(lib + "."):
return True
return False
aspect.register_configure_extension(
id = LANG_NAME,
properties = {},
prepare = prepare,
declare = declare_targets,
)
"Maven starlark plugin"
# Directive name and default value from the rules_jvm gazelle plugin
JAVA_MAVEN_INSTALL_FILE = "java_maven_install_file"
DEFAULT_JAVA_MAVEN_INSTALL_FILE = "maven_install.json"
def prepare(ctx):
return aspect.PrepareResult(
# All source files to be processed
sources = [
aspect.SourceExtensions(ctx.properties[JAVA_MAVEN_INSTALL_FILE]),
],
queries = {
"imports": aspect.JsonQuery(
filter = DEFAULT_JAVA_MAVEN_INSTALL_FILE,
query = """.dependency_tree.dependencies[] | select(.packages) | {packages,coord}""",
),
},
)
def analyze_source(ctx):
for dep in ctx.source.query_results["imports"]:
coord = dep["coord"].rsplit(":", 1)[0].replace(".", "_").replace(":", "_")
for pkg in dep["packages"]:
ctx.add_symbol(
id = pkg,
provider_type = "java_info",
label = aspect.Label(
repo = "maven",
name = coord,
),
)
aspect.register_configure_extension(
id = "maven",
properties = {
JAVA_MAVEN_INSTALL_FILE: aspect.Property(
type = "String",
default = DEFAULT_JAVA_MAVEN_INSTALL_FILE,
),
},
prepare = prepare,
analyze = analyze_source,
)
BZL_LIBRARY = "bzl_library"
LANG_NAME = "starlark"
BZL_EXT = ".bzl"
aspect.register_rule_kind(BZL_LIBRARY, {
"From": "@bazel_skylib//:bzl_library.bzl",
"NonEmptyAttrs": ["srcs"],
"MergeableAttrs": ["srcs"],
"ResolveAttrs": ["deps"],
})
def prepare(_):
return aspect.PrepareResult(
sources = [
aspect.SourceExtensions(".bzl"),
],
queries = {
"loads": aspect.AstQuery(
query = """(module
(expression_statement
(call
function: (identifier) @id
arguments: (argument_list
(string) @path
(string)
)
)
(#eq? @id "load")
)
)""",
),
},
)
def declare_targets(ctx):
# TODO
# Loop through the existing bzl_library targets in this package and
# delete any that are no longer needed.
for file in ctx.sources:
label = file.path.removesuffix(".bzl").replace("/", "_")
file_pkg = path.dirname(file.path)
loads = [ld.captures["path"].strip("\"") for ld in file.query_results["loads"]]
loads = [ld.removeprefix("//").replace(":", "/") if ld.startswith("//") else path.join(file_pkg, ld.removeprefix(":")) for ld in loads]
loads = [ld.strip("/") for ld in loads]
ctx.targets.add(
kind = BZL_LIBRARY,
name = label,
attrs = {
"srcs": [file.path],
"visibility": [checkInternalVisibility(ctx.rel, "//visibility:public")],
"deps": [
aspect.Import(
id = ld,
src = file.path,
provider = LANG_NAME,
)
for ld in loads
] if len(loads) > 0 else None,
},
# TODO
# load("@bazel_tools//tools/build_defs/repo:http.bzl")
# Note that the Go extension has a special case for it:
# if impLabel.Repo == "bazel_tools" {
# // The @bazel_tools repo is tricky because it is a part of the "shipped
# // with bazel" core library for interacting with the outside world.
symbols = [
aspect.Symbol(
id = path.join(ctx.rel, file.path),
provider = LANG_NAME,
),
],
)
return {}
aspect.register_configure_extension(
id = LANG_NAME,
properties = {},
prepare = prepare,
declare = declare_targets,
)
# See https://github.com/bazelbuild/bazel-skylib/blob/1.7.1/gazelle/bzl/gazelle.go#L340
def checkInternalVisibility(rel, visibility):
i = rel.find("internal")
if i > 0:
return "//%s:__subpackages__" % rel[:i - 1]
elif i == 0:
return "//:__subpackages__"
i = rel.find("private")
if i > 0:
return "//%s:__subpackages__" % rel[:i - 1]
elif i == 0:
return "//:__subpackages__"
return visibility
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment