diff --git a/.github/workflows/grpc-clib-checksum.yml b/.github/workflows/grpc-clib-checksum.yml new file mode 100644 index 000000000..35347af00 --- /dev/null +++ b/.github/workflows/grpc-clib-checksum.yml @@ -0,0 +1,27 @@ +name: Verify gRPC CLib Checksum is Up-to-Date + +on: + pull_request: + +permissions: + contents: read + +jobs: + verify-platforms-table: + name: Run Verification + runs-on: ubuntu-latest + steps: + - name: Checkout Sources + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + - name: Run gRPC Checksum Generation + run: ./gradlew grpc:grpc-core:computeSha256AllTargetsForCLibGrpc_fat --info --stacktrace + - name: Check if prebuilt gRPC library checksum is up-to-date + run: | + if [[ -n "$(git status --porcelain | grep cinterop-c/prebuilt-deps/grpc_fat/)" ]]; then + echo "Prebuilt gRPC library checksum is not up to date. Please run './gradlew grpc:grpc-core:computeSha256AllTargetsForCLibGrpc_fat' and commit changes" + exit 1 + fi diff --git a/cinterop-c/.gitignore b/cinterop-c/.gitignore index 4b23e2e04..d5a638ef2 100644 --- a/cinterop-c/.gitignore +++ b/cinterop-c/.gitignore @@ -12,5 +12,3 @@ /.aswb/ /.clwb/ .idea/ - -out/ \ No newline at end of file diff --git a/cinterop-c/BUILD.bazel b/cinterop-c/BUILD.bazel index 6383bade5..7fa7c5b5c 100644 --- a/cinterop-c/BUILD.bazel +++ b/cinterop-c/BUILD.bazel @@ -1,12 +1,6 @@ +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file") load("@rules_cc//cc:defs.bzl", "cc_library") -cc_static_library( - name = "kgrpc_static", - deps = [ - ":kgrpc", - ], -) - cc_library( name = "kgrpc", srcs = ["src/kgrpc.cpp"], @@ -15,12 +9,12 @@ cc_library( includes = ["include"], visibility = ["//visibility:public"], deps = [ - "@com_github_grpc_grpc//:grpc", + "//prebuilt-deps/grpc_fat:grpc_core_prebuilt", ], ) cc_static_library( - name = "protowire_static", + name = "protowire_fat", deps = [ ":protowire", ], @@ -34,6 +28,6 @@ cc_library( includes = ["include"], visibility = ["//visibility:public"], deps = [ - "@com_google_protobuf//:protobuf_lite", + "@com_github_protobuffers_protobuf//:protobuf_lite", ], ) diff --git a/cinterop-c/MODULE.bazel b/cinterop-c/MODULE.bazel index a749399ca..ba132687f 100644 --- a/cinterop-c/MODULE.bazel +++ b/cinterop-c/MODULE.bazel @@ -11,17 +11,22 @@ bazel_dep( # required to build for apple targets (like iOS) bazel_dep(name = "apple_support", version = "1.22.1", repo_name = "build_bazel_apple_support") +bazel_dep(name = "platforms", version = "1.0.0") # Protobuf bazel_dep( name = "protobuf", version = "31.1", - repo_name = "com_google_protobuf", + repo_name = "com_github_protobuffers_protobuf", ) # gRPC library +GRPC_VERSION = "1.74.1" + +## gRPC source dependency + bazel_dep( name = "grpc", - version = "1.74.1", + version = GRPC_VERSION, repo_name = "com_github_grpc_grpc", ) diff --git a/cinterop-c/MODULE.bazel.lock b/cinterop-c/MODULE.bazel.lock index 12f0973a3..6e65d1ad5 100644 --- a/cinterop-c/MODULE.bazel.lock +++ b/cinterop-c/MODULE.bazel.lock @@ -146,13 +146,14 @@ "https://bcr.bazel.build/modules/opentracing-cpp/1.6.0/source.json": "da1cb1add160f5e5074b7272e9db6fd8f1b3336c15032cd0a653af9d2f484aed", "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/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc", + "https://bcr.bazel.build/modules/platforms/1.0.0/MODULE.bazel": "f05feb42b48f1b3c225e4ccf351f367be0371411a803198ec34a389fb22aa580", + "https://bcr.bazel.build/modules/platforms/1.0.0/source.json": "f4ff1fd412e0246fd38c82328eb209130ead81d62dcd5a9e40910f867f733d96", "https://bcr.bazel.build/modules/prometheus-cpp/1.2.4/MODULE.bazel": "0fbe5dcff66311947a3f6b86ebc6a6d9328e31a28413ca864debc4a043f371e5", "https://bcr.bazel.build/modules/prometheus-cpp/1.3.0.bcr.1/MODULE.bazel": "116ad46e97c1d2aeb020fe2899a342a7e703574ce7c0faf7e4810f938c974a9a", "https://bcr.bazel.build/modules/prometheus-cpp/1.3.0.bcr.1/source.json": "e813cce2d450708cfcb26e309c5172583a7440776edf354e83e6788c768e5cca", diff --git a/cinterop-c/build_target.sh b/cinterop-c/build_target.sh index 511ca075a..d07493472 100755 --- a/cinterop-c/build_target.sh +++ b/cinterop-c/build_target.sh @@ -33,4 +33,4 @@ out="$(bazel cquery "$LABEL" --platforms="$PLATFORM" --apple_platform_type="$OS" cp -f "$out" "$DST" -echo "Done. Binary written to: $DST" +echo "Done. Binary written to: $DST" >&2 diff --git a/cinterop-c/extract_include_dir.sh b/cinterop-c/extract_include_dir.sh new file mode 100755 index 000000000..e42248e99 --- /dev/null +++ b/cinterop-c/extract_include_dir.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# +# Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. +# + +set -Eeuo pipefail +trap 'echo "ERROR: Build failed at ${BASH_SOURCE}:${LINENO}" >&2' ERR + +# Extract all headers in the /include directory of the given target. +# +# Usage: +# ./extract_include_dir.sh +# Example: +# ./extract_include_dir.sh //prebuilt-deps/grpc_fat:grpc_fat prebuilt-deps/grpc_fat +# +# The example will produce the prebuilt-deps/grpc_fat/include directory with all headers. + +LABEL="${1:?need bazel target label}" +DST="${2:?need output destination}" + +CONFIG=release + +mkdir -p $(dirname "$DST") + +bazel build "$LABEL" >/dev/null + +# Ask Bazel what file(s) this target produced +out="$(bazel cquery "$LABEL" --output=files | head -n1)" +[[ -n "$out/include" ]] || { echo "No output for $LABEL"; exit 1; } + +rm -rf "$DST/include" +cp -rLf "$out/include" "$DST/include" +chmod -R u+w "$DST" \ No newline at end of file diff --git a/cinterop-c/include/kgrpc.h b/cinterop-c/include/kgrpc.h index 5c2d72e19..33536fe48 100644 --- a/cinterop-c/include/kgrpc.h +++ b/cinterop-c/include/kgrpc.h @@ -1,6 +1,4 @@ -/* - * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ +// Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. /* * Helper functions required for gRPC Core cinterop. @@ -25,10 +23,11 @@ typedef struct { void *user_data; } kgrpc_cb_tag; -/* + + /* * Call to grpc_iomgr_run_in_background(), which is not exposed as extern "C" and therefore must be wrapped. */ -bool kgrpc_iomgr_run_in_background(); + bool kgrpc_iomgr_run_in_background(); #ifdef __cplusplus } diff --git a/cinterop-c/prebuilt-deps/grpc_fat/BUILD.bazel b/cinterop-c/prebuilt-deps/grpc_fat/BUILD.bazel new file mode 100644 index 000000000..14a543aaa --- /dev/null +++ b/cinterop-c/prebuilt-deps/grpc_fat/BUILD.bazel @@ -0,0 +1,99 @@ +load("@rules_cc//cc:defs.bzl", "cc_import", "cc_library") +load("//:tools/collect_headers.bzl", "cc_headers_only", "include_dir") + +package(default_visibility = ["//visibility:public"]) + +#### Compile gRPC from scratch #### + +cc_static_library( + name = "grpc_fat", + deps = [ + "@com_github_grpc_grpc//:grpc", + ], +) + +include_dir( + name = "grpc_include_dir", + target = "@com_github_grpc_grpc//:grpc", +) + +#### Targets for pre-built dependency #### + +# extract only the header files from grpc without compiling it +cc_headers_only( + name = "grpc_hdrs_ccinfo", + dep = "@com_github_grpc_grpc//:grpc", +) + +cc_library( + name = "grpc_core_prebuilt", + visibility = ["//visibility:public"], + deps = [":grpc_hdrs_ccinfo"] + + # pick the right prebuilt .a: + select({ + ":ios_device_arm64": [":grpc_core_ios_arm64"], + ":ios_simulator_arm64": [":grpc_core_ios_sim_arm64"], + ":ios_simulator_x64": [":grpc_core_ios_x64"], + ":macos_arm64": [":grpc_core_macos_arm64"], + "//conditions:default": [], + }), +) + +config_setting( + name = "ios_device_arm64", + constraint_values = [ + "@platforms//os:ios", + "@platforms//cpu:arm64", + "@build_bazel_apple_support//constraints:device", + ], +) + +config_setting( + name = "ios_simulator_arm64", + constraint_values = [ + "@platforms//os:ios", + "@platforms//cpu:arm64", + "@build_bazel_apple_support//constraints:simulator", + ], +) + +config_setting( + name = "ios_simulator_x64", + constraint_values = [ + "@platforms//os:ios", + "@platforms//cpu:x86_64", + "@build_bazel_apple_support//constraints:simulator", + ], +) + +config_setting( + name = "macos_arm64", + constraint_values = [ + "@platforms//os:macos", + "@platforms//cpu:arm64", + ], +) + +cc_import( + name = "grpc_core_ios_arm64", + static_library = ":libgrpc_fat.ios_arm64.a", + visibility = ["//visibility:public"], +) + +cc_import( + name = "grpc_core_ios_sim_arm64", + static_library = ":libgrpc_fat.ios_simulator_arm64.a", + visibility = ["//visibility:public"], +) + +cc_import( + name = "grpc_core_ios_x64", + static_library = ":libgrpc_fat.ios_x64.a", + visibility = ["//visibility:public"], +) + +cc_import( + name = "grpc_core_macos_arm64", + static_library = ":libgrpc_fat.macos_arm64.a", + visibility = ["//visibility:public"], +) diff --git a/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.ios_arm64.a b/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.ios_arm64.a new file mode 100755 index 000000000..82160bec3 Binary files /dev/null and b/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.ios_arm64.a differ diff --git a/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.ios_arm64.a.sha256 b/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.ios_arm64.a.sha256 new file mode 100644 index 000000000..a46a7e870 --- /dev/null +++ b/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.ios_arm64.a.sha256 @@ -0,0 +1 @@ +7bc13a15c59850048e77b5719ea262a6fcc22884ab42c9f14f38859200faac9b diff --git a/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.ios_simulator_arm64.a b/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.ios_simulator_arm64.a new file mode 100644 index 000000000..1155cd432 Binary files /dev/null and b/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.ios_simulator_arm64.a differ diff --git a/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.ios_simulator_arm64.a.sha256 b/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.ios_simulator_arm64.a.sha256 new file mode 100644 index 000000000..148d04fbd --- /dev/null +++ b/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.ios_simulator_arm64.a.sha256 @@ -0,0 +1 @@ +55fde875508372860125ee8505ef4e84b95c9620744378a99f8f4095292dcae5 diff --git a/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.ios_x64.a b/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.ios_x64.a new file mode 100644 index 000000000..78611c68a Binary files /dev/null and b/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.ios_x64.a differ diff --git a/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.ios_x64.a.sha256 b/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.ios_x64.a.sha256 new file mode 100644 index 000000000..9319711fa --- /dev/null +++ b/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.ios_x64.a.sha256 @@ -0,0 +1 @@ +7544113fe9b6c9d0a57c8b8864b5272e01e3a64ea36cd0802d4a76e98ad5e232 diff --git a/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.macos_arm64.a b/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.macos_arm64.a new file mode 100644 index 000000000..d63d0afa3 Binary files /dev/null and b/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.macos_arm64.a differ diff --git a/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.macos_arm64.a.sha256 b/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.macos_arm64.a.sha256 new file mode 100644 index 000000000..512674bdf --- /dev/null +++ b/cinterop-c/prebuilt-deps/grpc_fat/libgrpc_fat.macos_arm64.a.sha256 @@ -0,0 +1 @@ +6cf466a285832ecd0f41041164c412e9b541388210058310d7bb59f6a27d09de diff --git a/cinterop-c/src/kgrpc.cpp b/cinterop-c/src/kgrpc.cpp index cd860db75..e2be28299 100644 --- a/cinterop-c/src/kgrpc.cpp +++ b/cinterop-c/src/kgrpc.cpp @@ -1,9 +1,6 @@ -/* - * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ +// Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. #include - #include "src/core/lib/iomgr/iomgr.h" extern "C" { diff --git a/cinterop-c/tools/collect_headers.bzl b/cinterop-c/tools/collect_headers.bzl new file mode 100644 index 000000000..46c3dbaef --- /dev/null +++ b/cinterop-c/tools/collect_headers.bzl @@ -0,0 +1,106 @@ +# This files contains Bazel rules to collect headers by Bazel CcInfo targets. +# +# The 'include_dir' rule is used to extract the include headers +# from the dependency, so we can use them for our cinterop bindings. +# The target is called by the extract_include_dir.sh script, which +# is executed by the ./gradlew bundleIncludeDirLibC* task. +# +# The 'cc_header_only` rule returns a CcInfo that only contains the +# header files of the compilation dependency. This allows a +# library to depend on the header files, without compiling +# the whole dependency. +# E.g. it is used to bundle the +# //prebuilt-deps/grpc_fat:grpc_core_prebuilt +# target including the grpc headers. + +HeaderInfo = provider(fields = ["headers_dir", "headers"]) + +# determines if the file is in the given repository +def _same_repo(file, repo_name): + p = file.path + if repo_name: + return ("external/%s/" % repo_name) in p + + return "external/" not in p + +def _include_dir_impl(ctx): + cc = ctx.attr.target[CcInfo].compilation_context + + # transitive headers + all_hdrs = cc.headers + hdrs = all_hdrs.to_list() if type(all_hdrs) == "depset" else all_hdrs + + repo_name = ctx.attr.target.label.repo_name + + # filter: same repo + paths that contain /include/ + hdrs = [ + f + for f in hdrs + if "/include/" in f.path and _same_repo(f, repo_name) + ] + + manifest = ctx.actions.declare_file(ctx.label.name + ".headers.list") + outdir = ctx.actions.declare_directory(ctx.label.name + "_includes") + + ctx.actions.write(manifest, "\n".join([f.path for f in hdrs])) + + # copy them to some output directory + ctx.actions.run_shell( + inputs = hdrs + [manifest], + outputs = [outdir], + command = r""" +set -euo pipefail +OUT="$1"; MAN="$2" +python3 - <<'PY' "$OUT" "$MAN" +import os, shutil, sys +out, man = sys.argv[1], sys.argv[2] +for p in open(man): + p = p.strip() + if not p: continue + # keep include/ prefix once + rel = p.split("/include/", 1)[1] + dst = os.path.join(out, "include", rel) + os.makedirs(os.path.dirname(dst), exist_ok=True) + shutil.copy2(p, dst) +PY +""", + arguments = [outdir.path, manifest.path], + progress_message = "Collecting repo-scoped headers into %s" % outdir.path, + mnemonic = "CollectIncludes", + ) + + return [ + DefaultInfo(files = depset([outdir])), + HeaderInfo(headers_dir = outdir, headers = depset(hdrs)), + ] + +# rule to copy the include directory of some target to some location +include_dir = rule( + implementation = _include_dir_impl, + attrs = { + "target": attr.label(mandatory = True), + }, +) + +def _cc_headers_only_impl(ctx): + dep_cc = ctx.attr.dep[CcInfo].compilation_context + + # keep only source headers; this skips generated headers and their actions. + all_hdrs = dep_cc.headers.to_list() + src_hdrs = [f for f in all_hdrs if getattr(f, "is_source", False)] + cc_ctx = cc_common.create_compilation_context( + headers = depset(src_hdrs), + includes = dep_cc.includes, + quote_includes = dep_cc.quote_includes, + system_includes = dep_cc.system_includes, + framework_includes = dep_cc.framework_includes, + defines = dep_cc.defines, + ) + return [CcInfo(compilation_context = cc_ctx)] + +# rule to build a CcInfo that only contains the headers of a given CcInfo target. +# this allows us to combine the headers of the dependency sources with our pre-compiled dependencies. +cc_headers_only = rule( + implementation = _cc_headers_only_impl, + attrs = {"dep": attr.label(mandatory = True, providers = [CcInfo])}, +) diff --git a/compiler-plugin/gradle.properties b/compiler-plugin/gradle.properties index 9792d43e3..681d8e98f 100644 --- a/compiler-plugin/gradle.properties +++ b/compiler-plugin/gradle.properties @@ -42,3 +42,7 @@ kotlinx.rpc.plugin.internalDevelopment=true # set to true when building IDE compiler plugin artifacts kotlinx.rpc.forIdeBuild=false + +# enable code sharing between similar native targets: +# https://www.jetbrains.com/help/kotlin-multiplatform-dev/multiplatform-share-on-platforms.html +kotlin.mpp.enableCInteropCommonization=true diff --git a/dokka-plugin/gradle.properties b/dokka-plugin/gradle.properties index 9792d43e3..681d8e98f 100644 --- a/dokka-plugin/gradle.properties +++ b/dokka-plugin/gradle.properties @@ -42,3 +42,7 @@ kotlinx.rpc.plugin.internalDevelopment=true # set to true when building IDE compiler plugin artifacts kotlinx.rpc.forIdeBuild=false + +# enable code sharing between similar native targets: +# https://www.jetbrains.com/help/kotlin-multiplatform-dev/multiplatform-share-on-platforms.html +kotlin.mpp.enableCInteropCommonization=true diff --git a/gradle-conventions/src/main/kotlin/conventions-root.gradle.kts b/gradle-conventions/src/main/kotlin/conventions-root.gradle.kts index 1aa408526..36de65276 100644 --- a/gradle-conventions/src/main/kotlin/conventions-root.gradle.kts +++ b/gradle-conventions/src/main/kotlin/conventions-root.gradle.kts @@ -2,23 +2,12 @@ * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ -import util.other.capitalized import util.other.isPublicModule import util.other.libs -import util.other.maybeNamed import util.setupPage -import util.tasks.ValidatePublishedArtifactsTask -import util.tasks.configureNpm -import util.tasks.registerChangelogTask -import util.tasks.registerDumpPlatformTableTask -import util.tasks.registerVerifyPlatformTableTask -import kotlin.io.path.exists -import kotlin.io.path.isDirectory -import kotlin.io.path.listDirectoryEntries -import kotlin.io.path.name -import kotlin.io.path.readText +import util.tasks.* import java.nio.file.Path -import kotlin.io.path.createFile +import kotlin.io.path.* plugins { id("org.jetbrains.dokka") @@ -121,3 +110,9 @@ gradle.afterProject { } } } + + +tasks.register("checkBazel") { + exec = "bazel" + helpMessage = "Install Bazel: https://bazel.build/" +} \ No newline at end of file diff --git a/gradle-conventions/src/main/kotlin/util/cinterop.kt b/gradle-conventions/src/main/kotlin/util/cinterop.kt index 672c196d6..4f888336e 100644 --- a/gradle-conventions/src/main/kotlin/util/cinterop.kt +++ b/gradle-conventions/src/main/kotlin/util/cinterop.kt @@ -4,10 +4,13 @@ package util -import org.gradle.api.GradleException import org.gradle.api.NamedDomainObjectContainer import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.gradle.api.provider.Provider import org.gradle.api.tasks.Exec +import org.gradle.api.tasks.TaskContainer +import org.gradle.api.tasks.TaskProvider import org.gradle.internal.extensions.stdlib.capitalized import org.gradle.kotlin.dsl.* import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension @@ -22,51 +25,30 @@ import java.io.File fun KotlinMultiplatformExtension.configureCLibCInterop( project: Project, bazelTask: String, - configureCinterop: NamedDomainObjectContainer.(cinteropCLib: File) -> Unit, + cinteropTaskDependsOn: List = emptyList(), + configureCinterop: NamedDomainObjectContainer.(cLibSource: File, cLibOutDir: File) -> Unit, ) { - val globalRootDir: String by project.extra - - val cinteropCLib = project.layout.projectDirectory.dir("$globalRootDir/cinterop-c").asFile - - // TODO: Replace function implementation, so it does not use an internal API - fun findProgram(name: String) = org.gradle.internal.os.OperatingSystem.current().findInPath(name) - val checkBazel = project.tasks.register("checkBazel") { - doLast { - val bazelPath = findProgram("bazel") - if (bazelPath != null) { - logger.debug("bazel: {}", bazelPath) - } else { - throw GradleException("'bazel' not found on PATH. Please install Bazel (https://bazel.build/).") - } - } - } + val buildTargetName = bazelTask.split(":").last() + val cLibSource = project.cinteropLibDir + val cLibOutDir = project.bazelBuildDir.dir(buildTargetName).asFile targets.withType().configureEach { - val buildTargetName = bazelTask.split(":").last() - // bazel library build task - val taskName = "buildCLib${buildTargetName.capitalized()}_$targetName" - val buildCinteropCLib = project.tasks.register(taskName) { - val platform = bazelPlatformName - val os = bazelOsName - - // the name used for the static library files (e.g. iosSimulatorArm64 -> ios_simulator_arm64) - val platformShortName = konanTarget.visibleName - val fileName = "lib$buildTargetName.$platformShortName.a" - val outFile = cinteropCLib.resolve("out").resolve(fileName) - - group = "build" - workingDir = cinteropCLib - commandLine("bash", "-c", "./build_target.sh $bazelTask $outFile $platform $os") - inputs.files(project.fileTree(cinteropCLib) { exclude("bazel-*/**", "out/**") }) - outputs.files(outFile) - - dependsOn(checkBazel) - } + // the name used for the static library files (e.g. iosSimulatorArm64 -> ios_simulator_arm64) + val platformShortName = konanTarget.visibleName + val fileName = "lib$buildTargetName.$platformShortName.a" + val outFile = project.bazelBuildDir.file("$buildTargetName/$fileName") + + val buildCinteropCLib = project.tasks.registerBuildClibTask( + name = "build${canonicalCLibTaskPostfix(buildTargetName)}", + bazelTask = bazelTask, + outFile = outFile.asFile, + target = this, + ) // cinterop klib build config compilations.getByName("main") { cinterops { - configureCinterop(cinteropCLib) + configureCinterop(cLibSource, cLibOutDir) } cinterops.all { @@ -75,12 +57,122 @@ fun KotlinMultiplatformExtension.configureCLibCInterop( val interopTask = "cinterop${interop.name.capitalized()}${this@configureEach.targetName.capitalized()}" project.tasks.named(interopTask, CInteropProcess::class) { dependsOn(buildCinteropCLib) + cinteropTaskDependsOn.forEach { dependsOn(it) } } } } } } + +/** + * Creates tasks to build a C library dependency and places it to + * the `prebuilt-deps` directory in the cinterop-c Bazel project. + * + * It also creates tasks to compute the SHA256 checksum of the library and + * extract the include directory. + * + * @param project The Gradle project in which this configuration is applied. + * @param bazelTask The Bazel task responsible for building the C library. + */ +fun KotlinMultiplatformExtension.configureCLibDependency( + project: Project, + bazelTask: String, +) { + val buildTargetName = bazelTask.split(":").last() + val prebuiltLibDir = project.cLibPrebuiltDepsDir.resolve(buildTargetName) + + targets.withType().configureEach { + val platformShortName = konanTarget.visibleName + val fileName = "lib$buildTargetName.$platformShortName.a" + val staticLibFile = prebuiltLibDir.resolve(fileName) + + project.tasks.registerBuildClibTask( + name = "buildDependency${canonicalCLibTaskPostfix(buildTargetName)}", + bazelTask = bazelTask, + outFile = staticLibFile, + target = this, + ) + + project.tasks.register("computeSha256${canonicalCLibTaskPostfix(buildTargetName)}") { + val shaFile = prebuiltLibDir.resolve("$fileName.sha256") + workingDir = prebuiltLibDir + commandLine("bash", "-c", "sha256sum $staticLibFile | awk '{print $1}' > $shaFile") + inputs.files(staticLibFile) + outputs.file(shaFile) + } + } + + project.tasks.register("buildDependencyAllTargetsForCLib${buildTargetName.capitalized()}") { + group = "build" + targets.withType().forEach { target -> + dependsOn("buildDependency${target.canonicalCLibTaskPostfix(buildTargetName)}") + } + } + + project.tasks.register("computeSha256AllTargetsForCLib${buildTargetName.capitalized()}") { + targets.withType().forEach { target -> + dependsOn("computeSha256${target.canonicalCLibTaskPostfix(buildTargetName)}") + } + } +} + +fun Project.registerBuildCLibIncludeDirTask( + bazelTask: String, + bazelExtractIncludeOutputDir: Provider, +): TaskProvider { + val buildTargetName = bazelTask.split(":").last() + val includeDir = bazelExtractIncludeOutputDir.get().asFile + return project.tasks.register("buildIncludeDirCLib${buildTargetName.capitalized()}") { + dependsOn(":checkBazel") + group = "build" + workingDir = project.cinteropLibDir + commandLine( + "bash", + "-c", + "./extract_include_dir.sh //prebuilt-deps/grpc_fat:grpc_include_dir $includeDir" + ) + outputs.dir(includeDir.resolve("include")) + } +} + + +private val Project.cLibPrebuiltDepsDir: File get() = cinteropLibDir.resolve("prebuilt-deps") + +private val Project.bazelBuildDir: Directory + get() = layout.buildDirectory.dir("bazel-out").get() + + +private fun KotlinNativeTarget.canonicalCLibTaskPostfix(buildTargetName: String): String { + return "CLib${buildTargetName.capitalized()}_$targetName" +} + +private fun TaskContainer.registerBuildClibTask( + name: String, + bazelTask: String, + outFile: File, + target: KotlinNativeTarget, +): TaskProvider { + return register(name) { + val platform = target.bazelPlatformName + val os = target.bazelOsName + + group = "build" + workingDir = project.cinteropLibDir + commandLine("bash", "-c", "./build_target.sh $bazelTask $outFile $platform $os") + inputs.files(project.fileTree(project.cinteropLibDir) { exclude("bazel-*/**", "prebuilt-deps/**") }) + outputs.files(outFile) + + dependsOn(":checkBazel") + } +} + +private val Project.cinteropLibDir: File + get() { + val globalRootDir: String by project.extra + return layout.projectDirectory.dir("$globalRootDir/cinterop-c").asFile.absoluteFile + } + /** * Returns the Bazel platform name for the given [KotlinNativeTarget]. * diff --git a/gradle-conventions/src/main/kotlin/util/tasks/checkExecutable.kt b/gradle-conventions/src/main/kotlin/util/tasks/checkExecutable.kt new file mode 100644 index 000000000..37e9da4db --- /dev/null +++ b/gradle-conventions/src/main/kotlin/util/tasks/checkExecutable.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package util.tasks + +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.* +import org.gradle.process.ExecOperations +import javax.inject.Inject + +/** + * A task that verifies the existence of an executable in the system's PATH. + * + * This task is typically used to ensure that a specific executable is available before executing related tasks. + * If the executable is not found, a GradleException is thrown with an appropriate error message. + * + * The task generates an output file containing the path to the executable if it is found. + * The executable path can be accessed via the [execPath] property. + */ +abstract class CheckExecutableTask() : DefaultTask() { + + @get:Input + abstract val exec: Property + + @get:Input + @get:Optional + abstract val helpMessage: Property + + @get:OutputFile + abstract val output: RegularFileProperty + + @get:Inject + abstract val execOps: ExecOperations + + @get:Internal + val execPath: Provider = project.providers.fileContents(output).asText.map { it.trim() } + private val checkExecDir = project.layout.buildDirectory.dir("check-executable") + + init { + group = "verification" + description = "Checks that the executable file exists" + output.convention(project.provider { + checkExecDir.get().file(exec.get()) + }) + } + + + @TaskAction + fun check() { + val exec = exec.get() + val helpMessage = helpMessage.orElse("").get() + val result = execOps.exec { + commandLine("which", exec) + isIgnoreExitValue = true + standardOutput = output.get().asFile.outputStream() + } + if (result.exitValue != 0) { + if (result.exitValue != 0) { + throw GradleException("'$exec' not found on PATH. $helpMessage") + } + } + } + + +} \ No newline at end of file diff --git a/gradle-plugin/gradle.properties b/gradle-plugin/gradle.properties index 9792d43e3..681d8e98f 100644 --- a/gradle-plugin/gradle.properties +++ b/gradle-plugin/gradle.properties @@ -42,3 +42,7 @@ kotlinx.rpc.plugin.internalDevelopment=true # set to true when building IDE compiler plugin artifacts kotlinx.rpc.forIdeBuild=false + +# enable code sharing between similar native targets: +# https://www.jetbrains.com/help/kotlin-multiplatform-dev/multiplatform-share-on-platforms.html +kotlin.mpp.enableCInteropCommonization=true diff --git a/gradle.properties b/gradle.properties index 9792d43e3..681d8e98f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -42,3 +42,7 @@ kotlinx.rpc.plugin.internalDevelopment=true # set to true when building IDE compiler plugin artifacts kotlinx.rpc.forIdeBuild=false + +# enable code sharing between similar native targets: +# https://www.jetbrains.com/help/kotlin-multiplatform-dev/multiplatform-share-on-platforms.html +kotlin.mpp.enableCInteropCommonization=true diff --git a/grpc/grpc-core/build.gradle.kts b/grpc/grpc-core/build.gradle.kts index d3cb5e667..c71250119 100644 --- a/grpc/grpc-core/build.gradle.kts +++ b/grpc/grpc-core/build.gradle.kts @@ -7,6 +7,8 @@ import kotlinx.rpc.internal.InternalRpcApi import kotlinx.rpc.internal.configureLocalProtocGenDevelopmentDependency import util.configureCLibCInterop +import util.configureCLibDependency +import util.registerBuildCLibIncludeDirTask plugins { alias(libs.plugins.conventions.kmp) @@ -70,14 +72,32 @@ kotlin { } } - configureCLibCInterop(project, ":kgrpc_static") { cinteropCLib -> + + // configure task to extract the include dir from the gRPC core library + val grpcIncludeDir = project.layout.buildDirectory.dir("bazel-out/grpc-include") + val grpcIncludeDirTask = project.registerBuildCLibIncludeDirTask( + "//prebuilt-deps/grpc_fat:grpc_include_dir", + grpcIncludeDir + ) + + // configure pre-built gRPC core library + configureCLibDependency(project, "//prebuilt-deps/grpc_fat:grpc_fat") + + configureCLibCInterop( + project, ":kgrpc", + // depends on the grpc include dir + cinteropTaskDependsOn = listOf(grpcIncludeDirTask) + ) { cLibSource, cLibOutDir -> + val grpcPrebuiltDir = cLibSource.resolve("prebuilt-deps/grpc_fat") + @Suppress("unused") val libkgrpc by creating { includeDirs( - cinteropCLib.resolve("include"), - cinteropCLib.resolve("bazel-cinterop-c/external/grpc+/include"), + cLibSource.resolve("include"), + cLibSource.resolve("${grpcIncludeDir.get()}/include"), ) - extraOpts("-libraryPath", "${cinteropCLib.resolve("out")}") + extraOpts("-libraryPath", "$grpcPrebuiltDir") + extraOpts("-libraryPath", "$cLibOutDir") } } } diff --git a/grpc/grpc-core/src/nativeInterop/cinterop/libkgrpc.def b/grpc/grpc-core/src/nativeInterop/cinterop/libkgrpc.def index 2066569dd..27211bb19 100644 --- a/grpc/grpc-core/src/nativeInterop/cinterop/libkgrpc.def +++ b/grpc/grpc-core/src/nativeInterop/cinterop/libkgrpc.def @@ -8,11 +8,10 @@ headerFilter= kgrpc.h grpc/slice.h grpc/byte_buffer.h grpc/grpc.h \ noStringConversion = grpc_slice_from_copied_buffer my_grpc_slice_from_copied_buffer strictEnums = grpc_status_code grpc_connectivity_state grpc_call_error - -staticLibraries.macos_arm64 = libkgrpc_static.macos_arm64.a -staticLibraries.ios_arm64 = libkgrpc_static.ios_arm64.a -staticLibraries.ios_simulator_arm64 = libkgrpc_static.ios_simulator_arm64.a -staticLibraries.ios_simulator_x64 = libkgrpc_static.ios_simulator_x64.a +staticLibraries.macos_arm64 = libkgrpc.macos_arm64.a libgrpc_fat.macos_arm64.a +staticLibraries.ios_arm64 = libkgrpc.ios_arm64.a libgrpc_fat.ios_arm64.a +staticLibraries.ios_simulator_arm64 = libkgrpc.ios_simulator_arm64.a libgrpc_fat.ios_simulator_arm64.a +staticLibraries.ios_simulator_x64 = libkgrpc.ios_simulator_x64.a libgrpc_fat.ios_simulator_x64.a # TODO: Uncomment when activating WatchOS -# staticLibraries.watchos_arm64 = libkgrpc_static.watchos_arm64.a -# staticLibraries.watchos_simulator_arm64 = libkgrpc_static.watchos_simulator_arm64.a +# staticLibraries.watchos_arm64 = libkgrpc_fat.watchos_arm64.a +# staticLibraries.watchos_simulator_arm64 = libkgrpc_fat.watchos_simulator_arm64.a diff --git a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/CompletionQueue.kt b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/CompletionQueue.kt index 157428ccd..799964abc 100644 --- a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/CompletionQueue.kt +++ b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/CompletionQueue.kt @@ -84,7 +84,7 @@ internal class CompletionQueue { @Suppress("unused") private val shutdownFunctorCleaner = createCleaner(shutdownFunctor) { nativeHeap.free(it) } - + init { // Assert grpc_iomgr_run_in_background() to guarantee that the event manager provides // IO threads and supports the callback API. diff --git a/protobuf/protobuf-core/build.gradle.kts b/protobuf/protobuf-core/build.gradle.kts index 666079da2..820c2a2ce 100644 --- a/protobuf/protobuf-core/build.gradle.kts +++ b/protobuf/protobuf-core/build.gradle.kts @@ -53,13 +53,13 @@ kotlin { } } - configureCLibCInterop(project, ":protowire_static") { cinteropCLib -> + configureCLibCInterop(project, ":protowire_fat") { cLibSource, cLibOutDir -> @Suppress("unused") val libprotowire by creating { includeDirs( - cinteropCLib.resolve("include") + cLibSource.resolve("include") ) - extraOpts("-libraryPath", "${cinteropCLib.resolve("out")}") + extraOpts("-libraryPath", "$cLibOutDir") } } } diff --git a/protobuf/protobuf-core/src/nativeInterop/cinterop/libprotowire.def b/protobuf/protobuf-core/src/nativeInterop/cinterop/libprotowire.def index 977acbd9c..8d1c717f1 100644 --- a/protobuf/protobuf-core/src/nativeInterop/cinterop/libprotowire.def +++ b/protobuf/protobuf-core/src/nativeInterop/cinterop/libprotowire.def @@ -4,10 +4,10 @@ headerFilter = protowire.h noStringConversion = pw_encoder_write_string -staticLibraries.macos_arm64 = libprotowire_static.macos_arm64.a -staticLibraries.ios_arm64 = libprotowire_static.ios_arm64.a -staticLibraries.ios_simulator_arm64 = libprotowire_static.ios_simulator_arm64.a -staticLibraries.ios_simulator_x64 = libprotowire_static.ios_simulator_x64.a +staticLibraries.macos_arm64 = libprotowire_fat.macos_arm64.a +staticLibraries.ios_arm64 = libprotowire_fat.ios_arm64.a +staticLibraries.ios_simulator_arm64 = libprotowire_fat.ios_simulator_arm64.a +staticLibraries.ios_simulator_x64 = libprotowire_fat.ios_simulator_x64.a # TODO: Uncomment when activating WatchOS -# staticLibraries.watchos_arm64 = libprotowire_static.watchos_arm64_32.a -# staticLibraries.watchos_simulator_arm64 = libprotowire_static.watchos_sim_arm64.a \ No newline at end of file +# staticLibraries.watchos_arm64 = libprotowire_fat.watchos_arm64_32.a +# staticLibraries.watchos_simulator_arm64 = libprotowire_fat.watchos_sim_arm64.a \ No newline at end of file diff --git a/protoc-gen/gradle.properties b/protoc-gen/gradle.properties index 9792d43e3..681d8e98f 100644 --- a/protoc-gen/gradle.properties +++ b/protoc-gen/gradle.properties @@ -42,3 +42,7 @@ kotlinx.rpc.plugin.internalDevelopment=true # set to true when building IDE compiler plugin artifacts kotlinx.rpc.forIdeBuild=false + +# enable code sharing between similar native targets: +# https://www.jetbrains.com/help/kotlin-multiplatform-dev/multiplatform-share-on-platforms.html +kotlin.mpp.enableCInteropCommonization=true