codesign.bzl provides a hermetic Bazel rule for signing Apple binaries.
It wraps the rcodesign binary from the apple-codesign project and ships prebuilt binaries for macOS, Linux, and Windows. That means you can sign Mach-O binaries from any supported execution platform without depending on Apple's /usr/bin/codesign being installed on the machine running Bazel.
This is useful when you want:
- reproducible codesigning actions in Bazel
- remote execution for Apple packaging pipelines
- codesigning from Linux or Windows build workers
- a toolchain-backed rule instead of ad hoc
genruleshelling out to host tools
Add the module to your MODULE.bazel and register the prebuilt toolchains:
bazel_dep(name = "codesign.bzl", version = "0.0.1")
register_toolchains("@codesign.bzl//toolchain:all")Load the rule:
load("@codesign.bzl//:codesign.bzl", "codesign")Then use it to sign a binary:
codesign(
name = "signed_app",
binary = ":unsigned_app",
binary_identifier = "com.example.myapp",
entitlements = "entitlements.plist",
)The rule produces a signed executable target named signed_app.
binary
: The unsigned Mach-O binary to sign.
binary_identifier
: The bundle or binary identifier to embed in the signature, for example com.example.myapp.
entitlements
: An XML entitlements plist to apply while signing.
The codesign build rule above creates an ad-hoc signature. For distribution
with a Developer ID Application identity, use codesign_developer_id:
load("@codesign.bzl//:codesign.bzl", "codesign_developer_id")
codesign_developer_id(
name = "sign_release",
binary = ":unsigned_app",
binary_identifier = "com.example.myapp",
# Optional; omit when no additional entitlements are needed.
entitlements = "entitlements.plist",
)bazel build //:sign_release builds the unsigned binary and a signing launcher.
It does not read credentials, contact Apple's timestamp service, or sign anything.
Run the launcher to sign to a new, absolute output path:
bazel run //:sign_release -- \
--p12-file /secure/developer-id.p12 \
--p12-password-file /secure/password.txt \
--output /releases/signed_appThe launcher uses rcodesign sign --for-notarization, which requires a Developer
ID certificate, enables hardened runtime, and requires a secure timestamp. It
then runs rcodesign verify and propagates either command's failure. It disables
implicit rcodesign configuration files. Existing output paths are rejected;
callers must choose a fresh path and only consume the output after success.
Provide secret files at runtime from your CI secret store with restricted file
permissions. Do not declare them as Bazel inputs, commit them, or pass password
contents on the command line. For a separate credential-free build phase, use
bazel run --script_path=/path/to/sign-release.sh //:sign_release, then execute
that script in the credential-bearing phase with the arguments above. Keep its
Bazel output tree and runfiles available at the same paths.
This launcher requires Bash on Linux or macOS, with the signer toolchain selected for that machine's execution platform. The unsigned input can be cross-compiled for macOS. Native Windows launchers and directory/app-bundle inputs are not supported by this rule.
Signing does not notarize the output. Submit the signed artifact to Apple's notary service, require acceptance, and validate with Apple's native tools on macOS before distributing. Notarization orchestration and storage policy belong to the release pipeline; this rule does not implement them.
The unsigned build and launcher can use Bazel's cache. Credentials and the signed output are outside build actions and are not uploaded to the action cache by this rule. Fresh secure timestamps can change the signed bytes on every run: cache eviction must not be treated as permission to replace an immutable release. Persist and reuse the accepted signed artifact when retrying a release.
See the upstream rcodesign signing documentation and notarization documentation.
Run python3 tests/developer_id_test.py on Linux or macOS with Bazel/Bazelisk,
Bash, and Python 3 installed. The consumer integration tests build the actual
rule using a recording signer and check that the packaged signer rejects an
invalid identity; they need no Apple credentials. They test runfiles, argument
handling, failure propagation, and the build/publication boundary, not Apple
notarization acceptance or Gatekeeper behavior. The tests ignore user and
repository bazelrc files and do not use remote execution, caching, or BES.