Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ bazel_dep(name = "bazel_features", version = "1.27.0")
bazel_dep(name = "bazel_skylib", version = "1.3.0")
bazel_dep(name = "platforms", version = "0.0.9")
bazel_dep(name = "rules_cc", version = "0.2.15")
bazel_dep(name = "rules_shell", version = "0.3.0")

apple_cc_configure = use_extension("//crosstool:setup.bzl", "apple_cc_configure_extension")
use_repo(apple_cc_configure, "local_config_apple_cc", "local_config_apple_cc_toolchains")

register_toolchains("@local_config_apple_cc_toolchains//:all")

bazel_dep(name = "rules_shell", version = "0.3.0", dev_dependency = True)
register_toolchains("//rules/lipo:default_lipo_toolchain")

bazel_dep(name = "stardoc", version = "0.8.0", dev_dependency = True)

# TODO: Remove when transitives bump past this
Expand Down
4 changes: 1 addition & 3 deletions doc/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,7 @@ universal_binary(<a href="#universal_binary-name">name</a>, <a href="#universal_

This rule produces a multi-architecture ("fat") binary targeting Apple macOS
platforms *regardless* of the architecture of the macOS host platform. The
`lipo` tool is used to combine built binaries of multiple architectures. For
non-macOS platforms, this simply just creates a symbolic link of the input
binary.
`lipo` tool is used to combine built binaries of multiple architectures.

**ATTRIBUTES**

Expand Down
77 changes: 52 additions & 25 deletions lib/lipo.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ def _create(
actions,
inputs,
output,
apple_fragment,
xcode_config):
apple_fragment = None,
xcode_config = None,
toolchain = None):
"""Creates a universal binary by combining other binaries.

Args:
Expand All @@ -39,35 +40,61 @@ def _create(
output: A `File` representing the universal binary that will be the
output of the action.
apple_fragment: The `apple` configuration fragment used to configure
the action environment.
the action environment. Required when `toolchain` is not provided.
xcode_config: The `apple_common.XcodeVersionConfig` provider used to
configure the action environment.
configure the action environment. Required when `toolchain` is not
provided.
toolchain: An optional `LipoInfo` provider. When provided, the
action is registered via `ctx.actions.run` using the tool,
env, and execution requirements from the toolchain.
"""
if not inputs:
fail("lipo.create requires at least one input file.")

# Explicitly create the containing directory to avoid an occasional error
# from lipo; "can't create temporary output file [...] (Permission denied)"
command = [
"mkdir -p {} &&".format(shell.quote(output.dirname)),
"/usr/bin/lipo",
"-create",
]
command.extend([
shell.quote(input_file.path)
for input_file in inputs
])
command.extend(["-output", shell.quote(output.path)])
if toolchain:
args = actions.args()
args.add("-create")
args.add_all(inputs)
args.add("-output")
args.add(output)
actions.run(
executable = toolchain.tool,
arguments = [args],
mnemonic = "AppleLipo",
inputs = inputs,
outputs = [output],
env = toolchain.env,
execution_requirements = toolchain.execution_requirements,
use_default_shell_env = True,
)
elif apple_fragment and xcode_config:
# Explicitly create the containing directory to avoid an occasional error
# from lipo; "can't create temporary output file [...] (Permission denied)"
command = [
"mkdir -p {} &&".format(shell.quote(output.dirname)),
"/usr/bin/lipo",
"-create",
]
command.extend([
shell.quote(input_file.path)
for input_file in inputs
])
command.extend(["-output", shell.quote(output.path)])

apple_support.run_shell(
actions = actions,
command = " ".join(command),
mnemonic = "AppleLipo",
inputs = inputs,
outputs = [output],
apple_fragment = apple_fragment,
xcode_config = xcode_config,
)
apple_support.run_shell(
actions = actions,
command = " ".join(command),
mnemonic = "AppleLipo",
inputs = inputs,
outputs = [output],
apple_fragment = apple_fragment,
xcode_config = xcode_config,
use_default_shell_env = True,
)
else:
fail("""\
lipo.create requires either a `toolchain` or both `apple_fragment` and `xcode_config` to be provided.
""")

def _extract_or_thin(
*,
Expand Down
1 change: 1 addition & 0 deletions rules/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ filegroup(
name = "for_bazel_tests",
testonly = 1,
srcs = glob(["**"]) + [
"//rules/lipo:for_bazel_tests",
"//rules/private:for_bazel_tests",
],
visibility = ["//:__pkg__"],
Expand Down
49 changes: 49 additions & 0 deletions rules/lipo/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
load(":xcode_toolchain.bzl", "xcode_lipo_toolchain")

licenses(["notice"])

package(default_visibility = ["//visibility:public"])

toolchain_type(name = "toolchain_type")

sh_binary(
name = "lipo",
srcs = ["lipo.sh"],
visibility = ["//visibility:private"],
)

xcode_lipo_toolchain(
name = "lipo_toolchain",
tool = ":lipo",
)

toolchain(
name = "default_lipo_toolchain",
exec_compatible_with = ["@platforms//os:macos"],
target_compatible_with = ["@platforms//os:macos"],
toolchain = ":lipo_toolchain",
toolchain_type = ":toolchain_type",
)

bzl_library(
name = "toolchain_bzl",
srcs = ["toolchain.bzl"],
)

bzl_library(
name = "xcode_toolchain_bzl",
srcs = ["xcode_toolchain.bzl"],
deps = [
":toolchain_bzl",
"//lib:apple_support",
],
)

filegroup(
name = "for_bazel_tests",
testonly = 1,
srcs = glob(["**"]),
visibility = ["//:__subpackages__"],
)
5 changes: 5 additions & 0 deletions rules/lipo/lipo.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

set -euo pipefail

exec /usr/bin/lipo "$@"
63 changes: 63 additions & 0 deletions rules/lipo/toolchain.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Copyright 2026 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Toolchain rule for providing a custom `lipo` tool."""

LipoInfo = provider(
doc = "Provides a `lipo` tool for creating and manipulating universal binaries.",
fields = {
"tool": "A `FilesToRunProvider` for the `lipo` tool.",
"env": "A `dict` of environment variables to set when running the tool.",
"execution_requirements": """\
A `dict` of execution requirements for the action (e.g. `requires-darwin`).
""",
},
)

def _lipo_toolchain_impl(ctx):
return [
platform_common.ToolchainInfo(
lipo_info = LipoInfo(
tool = ctx.attr.tool[DefaultInfo].files_to_run,
env = ctx.attr.env,
execution_requirements = ctx.attr.execution_requirements,
),
),
]

lipo_toolchain = rule(
attrs = {
"tool": attr.label(
doc = "The `lipo` tool binary.",
mandatory = True,
allow_files = True,
executable = True,
cfg = "exec",
),
"env": attr.string_dict(
doc = "Additional environment variables to set when running lipo.",
default = {},
),
"execution_requirements": attr.string_dict(
doc = "Additional execution requirements for the action.",
default = {},
),
},
doc = """\
Defines a toolchain for the `lipo` tool used to create and manipulate universal
binaries. Use this to provide a custom `lipo` implementation by defining a
`lipo_toolchain` target and registering it as a toolchain.
""",
implementation = _lipo_toolchain_impl,
)
67 changes: 67 additions & 0 deletions rules/lipo/xcode_toolchain.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Copyright 2026 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Xcode-aware toolchain rule for providing a `lipo` tool."""

load("//lib:apple_support.bzl", "apple_support")
load(":toolchain.bzl", "LipoInfo")

def _xcode_lipo_toolchain_impl(ctx):
env = dict(ctx.attr.env)
execution_requirements = dict(ctx.attr.execution_requirements)

xcode_config = ctx.attr._xcode_config[apple_common.XcodeVersionConfig]
if xcode_config:
env.update(apple_common.apple_host_system_env(xcode_config))
env.update(
apple_common.target_apple_env(xcode_config, ctx.fragments.apple.single_arch_platform),
)
execution_requirements.update(xcode_config.execution_info())

return [
platform_common.ToolchainInfo(
lipo_info = LipoInfo(
tool = ctx.attr.tool[DefaultInfo].files_to_run,
env = env,
execution_requirements = execution_requirements,
),
),
]

xcode_lipo_toolchain = rule(
attrs = apple_support.action_required_attrs() | {
"tool": attr.label(
doc = "The `lipo` tool binary.",
mandatory = True,
allow_files = True,
executable = True,
cfg = "exec",
),
"env": attr.string_dict(
doc = "Additional environment variables to set when running lipo.",
default = {},
),
"execution_requirements": attr.string_dict(
doc = "Additional execution requirements for the action.",
default = {},
),
},
doc = """\
Defines a toolchain for the `lipo` tool used to create and manipulate universal
binaries. This toolchain automatically sets environment variables and execution
requirements required to run Xcode's lipo hermetically.
""",
fragments = ["apple"],
implementation = _xcode_lipo_toolchain_impl,
)
37 changes: 19 additions & 18 deletions rules/universal_binary.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@

"""Implementation for macOS universal binary rule."""

load("//lib:apple_support.bzl", "apple_support")
load("//lib:lipo.bzl", "lipo")
load("//lib:transitions.bzl", "macos_universal_transition")

_LIPO_TOOLCHAIN_TYPE = "//rules/lipo:toolchain_type"

def _universal_binary_impl(ctx):
inputs = [
binary[DefaultInfo].files.to_list()[0]
Expand All @@ -31,12 +32,15 @@ def _universal_binary_impl(ctx):
output = ctx.actions.declare_file(ctx.label.name)

if len(inputs) > 1:
lipo_toolchain = ctx.toolchains[_LIPO_TOOLCHAIN_TYPE]
if not lipo_toolchain:
fail("{} requires a lipo toolchain to build a universal binary but no lipo toolchain was found.".format(ctx.attr.name))

lipo.create(
actions = ctx.actions,
apple_fragment = ctx.fragments.apple,
inputs = inputs,
output = output,
xcode_config = ctx.attr._xcode_config[apple_common.XcodeVersionConfig],
toolchain = lipo_toolchain.lipo_info,
)

else:
Expand All @@ -60,25 +64,22 @@ def _universal_binary_impl(ctx):
]

universal_binary = rule(
attrs = apple_support.action_required_attrs() |
{
"binary": attr.label(
cfg = macos_universal_transition,
doc = "Target to generate a 'fat' binary from.",
mandatory = True,
),
"_allowlist_function_transition": attr.label(
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
),
},
attrs = {
"binary": attr.label(
cfg = macos_universal_transition,
doc = "Target to generate a 'fat' binary from.",
mandatory = True,
),
"_allowlist_function_transition": attr.label(
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
),
},
doc = """
This rule produces a multi-architecture ("fat") binary targeting Apple macOS
platforms *regardless* of the architecture of the macOS host platform. The
`lipo` tool is used to combine built binaries of multiple architectures. For
non-macOS platforms, this simply just creates a symbolic link of the input
binary.
`lipo` tool is used to combine built binaries of multiple architectures.
""",
executable = True,
fragments = ["apple"],
implementation = _universal_binary_impl,
toolchains = [config_common.toolchain_type(_LIPO_TOOLCHAIN_TYPE, mandatory = False)],
)