-
Notifications
You must be signed in to change notification settings - Fork 1.5k
[ABLD-395] MVP macos .pkg file rule. #54115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
aiuto
wants to merge
8
commits into
main
Choose a base branch
from
aiuto/macos_pkg
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+510
−0
Open
Changes from 4 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
ed2d5e0
[ABLD-395] MVP .pkg file rule.
aiuto 6b1c91c
pretty
aiuto 5433ef9
buildify
aiuto 1f92770
add analysis_test
aiuto f291f55
Merge branch 'main' into aiuto/macos_pkg
aiuto 15c830d
account for PR #54739
aiuto b4d7d96
Merge branch 'main' into aiuto/macos_pkg
aiuto e9808be
linty.fresh
aiuto File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| """Macos packaging rules.""" | ||
|
|
||
| load("@rules_pkg//pkg:mappings.bzl", "pkg_filegroup", "pkg_files", "pkg_mklink") | ||
| load("@rules_python//python:py_binary.bzl", "py_binary") | ||
| load(":pkg_mac_pkg.bzl", "pkg_mac_pkg") | ||
| load(":pkg_mac_pkg_test.bzl", "pkg_mac_pkg_test_suite") | ||
|
|
||
| package(default_visibility = ["//visibility:private"]) | ||
|
|
||
| pkg_mac_pkg_test_suite(name = "pkg_mac_pkg_tests") | ||
|
|
||
| py_binary( | ||
| name = "build_mac_pkg", | ||
| srcs = ["build_mac_pkg.py"], | ||
| tags = ["manual"], | ||
| visibility = ["//visibility:public"], | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| #!/usr/bin/env python3 | ||
| """Wraps macOS `pkgbuild` so it can be invoked as a Bazel action. | ||
|
|
||
| Bazel presents action inputs (including declare_directory outputs like | ||
| --root) as a symlink farm inside the sandbox, wrapping every payload entry in | ||
| a symlink pointing at its real, ephemeral sandbox/exec-root/cache location. | ||
| `pkgbuild` faithfully packages whatever it finds under --root, including | ||
| symlink-ness, so a naive copy either ships broken symlinks (plain | ||
| `cp -R`) or, if symlinks are blindly dereferenced (`cp -RL`), loses two things | ||
| pkg_install's NativeInstaller deliberately set: exact permission bits (a plain | ||
| `cp` applies umask to the new file's mode instead of copying the source mode) | ||
| and intentional payload symlinks (e.g. pkg_mklink entries), which get | ||
| flattened into duplicate regular files. | ||
|
|
||
| materialize_root.py resolves exactly the sandbox's own indirection layer | ||
| (dereferencing until it hits either real content or the payload's own | ||
| intended symlink) and preserves source file modes explicitly, so it is not | ||
| subject to the umask under which this action runs. | ||
| """ | ||
|
|
||
| import argparse | ||
| import os | ||
| import shutil | ||
| import subprocess | ||
| import sys | ||
| import tempfile | ||
|
|
||
|
|
||
| def _install_script(src, dst): | ||
| shutil.copyfile(src, dst) | ||
| os.chmod(dst, 0o755) | ||
|
|
||
|
|
||
| def main(): | ||
| parser = argparse.ArgumentParser(description=__doc__) | ||
| parser.add_argument("--materialize-root-py", required=True, help="Path to materialize_root.py.") | ||
| parser.add_argument("--root", required=True, help="pkg_install-materialized root directory (symlink farm).") | ||
| parser.add_argument("--pkgbuild", required=True, help="Path to the pkgbuild binary.") | ||
| parser.add_argument("--identifier", required=True, help="pkgbuild --identifier.") | ||
| parser.add_argument("--version", required=True, help="pkgbuild --version.") | ||
| parser.add_argument("--install-location", required=True, help="pkgbuild --install-location.") | ||
| parser.add_argument("--output", required=True, help="Path to write the built .pkg to.") | ||
| parser.add_argument("--preinstall", default="", help="Optional path to a preinstall script.") | ||
| parser.add_argument("--postinstall", default="", help="Optional path to a postinstall script.") | ||
| parser.add_argument("--signing-identity", default="", help="Optional pkgbuild --sign identity name.") | ||
|
|
||
| args = parser.parse_args() | ||
|
|
||
| real_root_dir = tempfile.mkdtemp() | ||
| scripts_dir = tempfile.mkdtemp() | ||
| try: | ||
| subprocess.run( | ||
| [sys.executable, args.materialize_root_py, args.root, real_root_dir], | ||
| check=True, | ||
| ) | ||
|
|
||
| # fmt: off | ||
| pkgbuild_args = [ | ||
| args.pkgbuild, | ||
| "--root", real_root_dir, | ||
| "--identifier", args.identifier, | ||
| "--version", args.version, | ||
| "--install-location", args.install_location, | ||
| ] | ||
|
|
||
| if args.preinstall or args.postinstall: | ||
| if args.preinstall: | ||
| _install_script(args.preinstall, os.path.join(scripts_dir, "preinstall")) | ||
| if args.postinstall: | ||
| _install_script(args.postinstall, os.path.join(scripts_dir, "postinstall")) | ||
| pkgbuild_args += ["--scripts", scripts_dir] | ||
|
|
||
| if args.signing_identity: | ||
| pkgbuild_args += ["--sign", args.signing_identity] | ||
|
|
||
| os.makedirs(os.path.dirname(args.output), exist_ok=True) | ||
| pkgbuild_args.append(args.output) | ||
| subprocess.run(pkgbuild_args, check=True) | ||
| finally: | ||
| shutil.rmtree(real_root_dir, ignore_errors=True) | ||
| shutil.rmtree(scripts_dir, ignore_errors=True) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| #!/usr/bin/env python3 | ||
| """Copies a pkg_install-materialized tree out of Bazel's sandbox symlink farm. | ||
|
|
||
| Usage: materialize_root.py <src_dir> <dst_dir> | ||
|
|
||
| Bazel presents a declare_directory() action input as a symlink farm: every | ||
| entry is (transitively) a symlink to its real, ephemeral location under the | ||
| sandbox/exec-root/cache. This walks that farm and, for each entry, resolves | ||
| symlinks one hop at a time until it finds either real content (a regular | ||
| file/directory) or another symlink -- the latter case means the payload | ||
| itself (via pkg_install's NativeInstaller) intentionally created that | ||
| symlink, e.g. for a pkg_mklink() entry, so its target text is preserved | ||
| verbatim rather than being flattened into a duplicate file. | ||
|
|
||
| File/directory modes are copied explicitly via shutil.copymode()/copystat() | ||
| so the result isn't affected by this process's umask. | ||
| """ | ||
|
|
||
| import os | ||
| import shutil | ||
| import sys | ||
|
|
||
|
|
||
| def _resolve_one_hop(path): | ||
| """If path is a symlink, returns (resolved_path, target_is_symlink).""" | ||
| if not os.path.islink(path): | ||
| return path, False | ||
| target = os.readlink(path) | ||
| if os.path.isabs(target): | ||
| resolved = target | ||
| else: | ||
| resolved = os.path.join(os.path.dirname(path), target) | ||
| resolved = os.path.normpath(resolved) | ||
| return resolved, os.path.islink(resolved) | ||
|
|
||
|
|
||
| def materialize(src, dst): | ||
| real_src, is_intentional_symlink = _resolve_one_hop(src) | ||
| if is_intentional_symlink: | ||
| os.symlink(os.readlink(real_src), dst) | ||
| return | ||
|
|
||
| if os.path.isdir(real_src): | ||
| os.makedirs(dst, exist_ok=True) | ||
| for entry in os.listdir(real_src): | ||
| materialize(os.path.join(real_src, entry), os.path.join(dst, entry)) | ||
| shutil.copystat(real_src, dst) | ||
| else: | ||
| shutil.copyfile(real_src, dst) | ||
| shutil.copymode(real_src, dst) | ||
|
|
||
|
|
||
| def main(): | ||
| src_dir, dst_dir = sys.argv[1], sys.argv[2] | ||
| for entry in os.listdir(src_dir): | ||
| materialize(os.path.join(src_dir, entry), os.path.join(dst_dir, entry)) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| """pkg_mac_pkg - build a macOS .pkg installer from rules_pkg-style file mappings. | ||
|
|
||
| Consumes the same `srcs` shape as `pkg_deb`/`pkg_rpm` (pkg_files/pkg_filegroup | ||
| targets) and calls the macOS `pkgbuild` tool to produce a single component | ||
| package. See ABLD-395 (~/mac_package.md has the design writeup). | ||
|
|
||
| v1 scope, deliberately: no `productbuild`/Distribution.xml composition -- a | ||
| single flat component package is directly installable via `installer(8)`, | ||
| Installer.app, or MDM. v1 also materializes `srcs` to a real directory on disk | ||
| before invoking `pkgbuild --root`, reusing rules_pkg's own `pkg_install` | ||
| installer (and its codesign-safe atomic file copy) rather than writing a | ||
| bespoke manifest interpreter. | ||
| """ | ||
|
|
||
| load("@rules_pkg//pkg:install.bzl", "pkg_install") | ||
|
|
||
| def _pkg_mac_pkg_impl(ctx): | ||
| # `pkg_install` produces a `bazel run`-able installer whose CLI insists on | ||
| # an absolute --destdir (or BUILD_WORKSPACE_DIRECTORY for resolving a | ||
| # relative one), since it's normally invoked interactively. Inside a | ||
| # Bazel action the process cwd is already the exec root, so a relative | ||
| # destdir resolves correctly on disk -- we just need to satisfy that | ||
| # check. Setting BUILD_WORKSPACE_DIRECTORY to "." (a literal we choose, | ||
| # not an inherited env var) does that without needing our own installer. | ||
| root_dir = ctx.actions.declare_directory(ctx.label.name + "_root") | ||
| ctx.actions.run( | ||
| mnemonic = "MacPkgRoot", | ||
| progress_message = "Materializing pkg root for %s" % ctx.label, | ||
| executable = ctx.attr.installer[DefaultInfo].files_to_run, | ||
| arguments = ["--destdir", root_dir.path, "--wipe_destdir"], | ||
| env = {"BUILD_WORKSPACE_DIRECTORY": "."}, | ||
| outputs = [root_dir], | ||
| ) | ||
|
|
||
| pkgbuild_toolchain = ctx.toolchains["@macos_pkgbuild//:pkgbuild_toolchain_type"].pkgbuild | ||
| if not pkgbuild_toolchain.valid: | ||
| fail("No pkgbuild available on this machine.") | ||
|
|
||
| output = ctx.outputs.out | ||
| inputs = [root_dir, ctx.file._materialize_root_py] | ||
|
|
||
| args = ctx.actions.args() | ||
| args.add("--materialize-root-py", ctx.file._materialize_root_py) | ||
| args.add("--root", root_dir.path) | ||
|
|
||
| # pkgbuild_toolchain.path is a plain string pointing at a system binary | ||
| # found via `which` (see bazel/toolchains/common/defs.bzl) -- it isn't a | ||
| # Bazel-tracked File, so it must not be added to `inputs`. | ||
| args.add("--pkgbuild", pkgbuild_toolchain.path) | ||
|
|
||
| args.add("--identifier", ctx.attr.identifier) | ||
| args.add("--version", ctx.attr.version) | ||
| args.add("--install-location", ctx.attr.install_location) | ||
| args.add("--output", output) | ||
|
|
||
| if ctx.file.preinstall: | ||
| args.add("--preinstall", ctx.file.preinstall) | ||
| inputs.append(ctx.file.preinstall) | ||
|
|
||
| if ctx.file.postinstall: | ||
| args.add("--postinstall", ctx.file.postinstall) | ||
| inputs.append(ctx.file.postinstall) | ||
|
|
||
| if ctx.attr.signing_identity: | ||
| args.add("--signing-identity", ctx.attr.signing_identity) | ||
|
|
||
| ctx.actions.run( | ||
| mnemonic = "MacPkgBuild", | ||
| progress_message = "Building macOS pkg %s" % ctx.label, | ||
| executable = ctx.executable._pkgbuild_wrapper, | ||
| arguments = [args], | ||
| inputs = inputs, | ||
| outputs = [output], | ||
| ) | ||
|
|
||
| return [ | ||
| DefaultInfo(files = depset([output])), | ||
| OutputGroupInfo(pkg = depset([output])), | ||
| ] | ||
|
|
||
| _pkg_mac_pkg = rule( | ||
| implementation = _pkg_mac_pkg_impl, | ||
| attrs = { | ||
| "installer": attr.label( | ||
| mandatory = True, | ||
| executable = True, | ||
| cfg = "target", | ||
| providers = [DefaultInfo], | ||
| doc = "pkg_install target used to materialize srcs onto disk.", | ||
| ), | ||
| "identifier": attr.string(mandatory = True, doc = "pkgbuild --identifier"), | ||
| "version": attr.string(mandatory = True, doc = "pkgbuild --version"), | ||
| "install_location": attr.string(default = "/", doc = "pkgbuild --install-location"), | ||
| "preinstall": attr.label(allow_single_file = True, doc = "Script run as `preinstall`."), | ||
| "postinstall": attr.label(allow_single_file = True, doc = "Script run as `postinstall`."), | ||
| "signing_identity": attr.string(doc = "pkgbuild --sign identity name."), | ||
| "out": attr.output(mandatory = True), | ||
| "_pkgbuild_wrapper": attr.label( | ||
| default = Label("//bazel/rules/macos/pkg:build_mac_pkg"), | ||
| executable = True, | ||
| cfg = "exec", | ||
| ), | ||
| "_materialize_root_py": attr.label( | ||
| default = Label("//bazel/rules/macos/pkg:materialize_root.py"), | ||
| allow_single_file = True, | ||
| ), | ||
| }, | ||
| toolchains = ["@macos_pkgbuild//:pkgbuild_toolchain_type"], | ||
| ) | ||
|
|
||
| def pkg_mac_pkg( | ||
| name, | ||
| srcs, | ||
| identifier, | ||
| version, | ||
| install_location = "/", | ||
| preinstall = None, | ||
| postinstall = None, | ||
| signing_identity = "", | ||
| out = None, | ||
| **kwargs): | ||
| """Builds a macOS .pkg installer from pkg_filegroup/pkg_files srcs. | ||
|
|
||
| Args: | ||
| name: rule name. | ||
| srcs: pkg_filegroup framework mapping/grouping targets (same shape | ||
| accepted by pkg_tar/pkg_install). | ||
| identifier: the package identifier, e.g. "com.datadoghq.agent". | ||
| version: the package version string. | ||
| install_location: root install path baked into the package | ||
| (pkgbuild --install-location). Defaults to "/". | ||
| preinstall: optional label of a script to run as `preinstall`. | ||
| postinstall: optional label of a script to run as `postinstall`. | ||
| signing_identity: optional codesigning identity for `pkgbuild --sign`. | ||
| Leave unset for unsigned builds; callers that need env-gated | ||
| signing (e.g. Omnibus's SIGN_MAC) should select() this at the | ||
| call site rather than reading the env from within the rule. | ||
| out: output file name. Defaults to "{name}.pkg". | ||
| **kwargs: standard attributes. | ||
| """ | ||
| pkg_install( | ||
| name = name + "_installer", | ||
| srcs = srcs, | ||
| visibility = ["//visibility:private"], | ||
| ) | ||
|
|
||
| _pkg_mac_pkg( | ||
| name = name, | ||
| installer = ":" + name + "_installer", | ||
| identifier = identifier, | ||
| version = version, | ||
| install_location = install_location, | ||
| preinstall = preinstall, | ||
| postinstall = postinstall, | ||
| signing_identity = signing_identity, | ||
| out = out or (name + ".pkg"), | ||
| target_compatible_with = ["@platforms//os:macos"], | ||
| **kwargs | ||
| ) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.