diff --git a/BUILD b/BUILD index a871abe..cdba95f 100644 --- a/BUILD +++ b/BUILD @@ -1 +1,7 @@ -# Do not remove, this empty BUILD file is necessary for _android_ndk_repository_impl in rules.bzl. +# Note that this BUILD file is necessary for `android_ndk_repository` in `rules.bzl`. + +exports_files([ + "LICENSE", +] + glob([ + "*.tpl", +])) diff --git a/BUILD.ndk_clang.tpl b/BUILD.ndk_clang.tpl index 9fa3cab..5c7cdf6 100644 --- a/BUILD.ndk_clang.tpl +++ b/BUILD.ndk_clang.tpl @@ -1,5 +1,6 @@ """Declarations for the NDK's Clang directory.""" +load("@rules_cc//cc/toolchains:cc_toolchain_suite.bzl", "cc_toolchain_suite") load("@rules_cc//cc/toolchains:cc_toolchain.bzl", "cc_toolchain") load("@@{repository_name}//:ndk_cc_toolchain_config.bzl", "ndk_cc_toolchain_config_rule") load("//:target_systems.bzl", "TARGET_SYSTEM_NAMES") @@ -52,7 +53,7 @@ filegroup( "lib64/**/*", "lib/**/*", ], - # Need to allow_empty here because previous NDK versions had + # Need to allow_empty here because previous NDK versions had # "lib" & "lib64" directories but recent ones only have "lib". allow_empty = True, ), diff --git a/BUILD.ndk_root.tpl b/BUILD.ndk_root.tpl index f944cb1..3e9370a 100644 --- a/BUILD.ndk_root.tpl +++ b/BUILD.ndk_root.tpl @@ -19,6 +19,7 @@ alias( "@platforms//os:android", CPU_CONSTRAINT[target_system_name], ], + exec_compatible_with = {exec_compatible_with}, toolchain = "//{clang_directory}:cc_toolchain_%s" % target_system_name, toolchain_type = "@bazel_tools//tools/cpp:toolchain_type", ) for target_system_name in TARGET_SYSTEM_NAMES] diff --git a/extension.bzl b/extension.bzl index 22c8076..b1d7295 100644 --- a/extension.bzl +++ b/extension.bzl @@ -14,7 +14,7 @@ """A bzlmod extension for loading the NDK.""" -load(":rules.bzl", "android_ndk_repository") +load(":rules.bzl", "DEFAULT_API_LEVEL", "android_ndk_repository") def _android_ndk_repository_extension_impl(module_ctx): root_modules = [m for m in module_ctx.modules if m.is_root and m.tags.configure] @@ -36,12 +36,19 @@ def _android_ndk_repository_extension_impl(module_ctx): **kwargs ) +_CONFIGURE_TAG_CLASS = tag_class(attrs = { + "api_level": attr.int( + doc = "The minimum Android API level to target.", + default = DEFAULT_API_LEVEL, + ), + "path": attr.string( + doc = "The path to the local Android NDK installation. If not set, ANDROID_NDK_HOME environment variable is used.", + ), +}) + android_ndk_repository_extension = module_extension( implementation = _android_ndk_repository_extension_impl, tag_classes = { - "configure": tag_class(attrs = { - "path": attr.string(), - "api_level": attr.int(), - }), + "configure": _CONFIGURE_TAG_CLASS, }, ) diff --git a/rules.bzl b/rules.bzl index 53b9f7c..12a0e75 100644 --- a/rules.bzl +++ b/rules.bzl @@ -14,30 +14,64 @@ """A repository rule for integrating the Android NDK.""" -def _android_ndk_repository_impl(ctx): +DEFAULT_API_LEVEL = 31 + +_EXEC_CONSTRAINTS = { + "darwin-arm64": [ + "@platforms//os:macos", + "@platforms//cpu:aarch64", + ], + "darwin-x86_64": [ + "@platforms//os:macos", + "@platforms//cpu:x86_64", + ], + "linux-x86_64": [ + "@platforms//os:linux", + "@platforms//cpu:x86_64", + ], + "windows-x86_64": [ + "@platforms//os:windows", + "@platforms//cpu:x86_64", + ], +} + +def _get_clang_resource_dir(ctx, clang_directory, is_windows): + clang_resource_dir = getattr(ctx.attr, "clang_resource_dir", None) + if clang_resource_dir: + return clang_resource_dir + + result = ctx.execute([clang_directory + "/bin/clang", "--print-resource-dir"]) + if result.return_code != 0: + fail("Failed to execute clang: %s" % result.stderr) + stdout = result.stdout.strip() + if is_windows: + stdout = stdout.replace("\\", "/") + return stdout.split(clang_directory)[1].strip("/") + +def _android_ndk_repository_common(ctx, ndk_path): """Install the Android NDK files. Args: ctx: An implementation context. + ndk_path: The path to the ndk Returns: A final dict of configuration attributes and values. """ - ndk_path = ctx.attr.path or ctx.getenv("ANDROID_NDK_HOME", None) - if not ndk_path: - fail("Either the ANDROID_NDK_HOME environment variable or the " + - "path attribute of android_ndk_repository must be set.") - if ndk_path.startswith("$WORKSPACE_ROOT"): - ndk_path = str(ctx.workspace_root) + ndk_path.removeprefix("$WORKSPACE_ROOT") - is_windows = False executable_extension = "" - if ctx.os.name == "linux": + exec_compatible_with = None + platform = ctx.os.name + if hasattr(ctx.attr, "platform"): + platform = ctx.attr.platform + exec_compatible_with = _EXEC_CONSTRAINTS[platform] + + if platform.startswith("linux"): clang_directory = "toolchains/llvm/prebuilt/linux-x86_64" - elif ctx.os.name == "mac os x": + elif platform.startswith(("mac", "darwin")): # Note: darwin-x86_64 does indeed contain fat binaries with arm64 slices, too. clang_directory = "toolchains/llvm/prebuilt/darwin-x86_64" - elif ctx.os.name.startswith("windows"): + elif platform.startswith("windows"): clang_directory = "toolchains/llvm/prebuilt/windows-x86_64" is_windows = True executable_extension = ".exe" @@ -48,15 +82,9 @@ def _android_ndk_repository_impl(ctx): _create_symlinks(ctx, ndk_path, clang_directory, sysroot_directory) - api_level = ctx.attr.api_level or 31 + api_level = ctx.attr.api_level or DEFAULT_API_LEVEL - result = ctx.execute([clang_directory + "/bin/clang", "--print-resource-dir"]) - if result.return_code != 0: - fail("Failed to execute clang: %s" % result.stderr) - stdout = result.stdout.strip() - if is_windows: - stdout = stdout.replace("\\", "/") - clang_resource_directory = stdout.split(clang_directory)[1].strip("/") + clang_resource_directory = _get_clang_resource_dir(ctx, clang_directory, is_windows) # Use a label relative to the workspace from which this repository rule came # to get the workspace name. @@ -67,6 +95,7 @@ def _android_ndk_repository_impl(ctx): ctx.attr._template_ndk_root, { "{clang_directory}": clang_directory, + "{exec_compatible_with}": repr(exec_compatible_with), }, executable = False, ) @@ -83,11 +112,11 @@ def _android_ndk_repository_impl(ctx): "%s/BUILD.bazel" % clang_directory, ctx.attr._template_ndk_clang, { - "{repository_name}": repository_name, "{api_level}": str(api_level), "{clang_resource_directory}": clang_resource_directory, - "{sysroot_directory}": sysroot_directory, "{executable_extension}": executable_extension, + "{repository_name}": repository_name, + "{sysroot_directory}": sysroot_directory, }, executable = False, ) @@ -124,16 +153,77 @@ def _create_symlinks(ctx, ndk_path, clang_directory, sysroot_directory): # TODO(#32): Remove this hack ctx.symlink(ndk_path + "sources", "ndk/sources") +_COMMON_ATTR = { + "api_level": attr.int( + doc = "The minimum Android API level to target.", + default = DEFAULT_API_LEVEL, + ), + "_build": attr.label( + default = Label("//:BUILD"), + allow_single_file = True, + ), + "_template_ndk_clang": attr.label( + default = Label("//:BUILD.ndk_clang.tpl"), + allow_single_file = True, + ), + "_template_ndk_root": attr.label( + default = Label("//:BUILD.ndk_root.tpl"), + allow_single_file = True, + ), + "_template_ndk_sysroot": attr.label( + default = Label(":BUILD.ndk_sysroot.tpl"), + allow_single_file = True, + ), + "_template_target_systems": attr.label( + default = Label("//:target_systems.bzl.tpl"), + allow_single_file = True, + ), +} + +def _exec_configuration_android_ndk_repository_impl(ctx): + ndk_path = ctx.path(Label(ctx.attr.anchor)).dirname + + return _android_ndk_repository_common(ctx, ndk_path) + +exec_configuration_android_ndk_repository = repository_rule( + doc = "A repository rule that integrates the Android NDK from a workspace. Uses an anchor label to locate the NDK and requires the host platform and Clang resource directory to be specified. For local NDK installations, use android_ndk_repository instead.", + implementation = _exec_configuration_android_ndk_repository_impl, + attrs = _COMMON_ATTR | { + "anchor": attr.string( + doc = "A label to a file in the NDK directory. The directory containing this file is used as the NDK root path.", + mandatory = True, + ), + "clang_resource_dir": attr.string( + doc = "The Clang resource directory path. Pass an empty string to auto-detect by running clang --print-resource-dir.", + mandatory = True, + ), + "platform": attr.string( + doc = "The execution platform for the NDK toolchain (e.g., 'linux-x86_64', 'darwin-arm64', 'windows-x86_64'). Determines which prebuilt toolchain directory is used.", + values = _EXEC_CONSTRAINTS.keys(), + mandatory = True, + ), + }, +) + +def _android_ndk_repository_impl(ctx): + ndk_path = ctx.attr.path or ctx.getenv("ANDROID_NDK_HOME", None) + if not ndk_path: + fail("Either the ANDROID_NDK_HOME environment variable or the " + + "path attribute of android_ndk_repository must be set.") + + if ndk_path.startswith("$WORKSPACE_ROOT"): + ndk_path = str(ctx.workspace_root) + ndk_path.removeprefix("$WORKSPACE_ROOT") + + return _android_ndk_repository_common(ctx, ndk_path) + android_ndk_repository = repository_rule( - attrs = { - "path": attr.string(), - "api_level": attr.int(), - "_build": attr.label(default = ":BUILD", allow_single_file = True), - "_template_ndk_root": attr.label(default = ":BUILD.ndk_root.tpl", allow_single_file = True), - "_template_target_systems": attr.label(default = ":target_systems.bzl.tpl", allow_single_file = True), - "_template_ndk_clang": attr.label(default = ":BUILD.ndk_clang.tpl", allow_single_file = True), - "_template_ndk_sysroot": attr.label(default = ":BUILD.ndk_sysroot.tpl", allow_single_file = True), + doc = "A repository rule that integrates the Android NDK from a local path. Uses ANDROID_NDK_HOME environment variable or the path attribute. This is the rule used by the bzlmod extension.", + implementation = _android_ndk_repository_impl, + attrs = _COMMON_ATTR | { + "path": attr.string( + doc = "The path to the local Android NDK installation. If not set, ANDROID_NDK_HOME environment variable is used. May start with $WORKSPACE_ROOT to reference the workspace root.", + ), }, + environ = ["ANDROID_NDK_HOME"], local = True, - implementation = _android_ndk_repository_impl, )