Replies: 1 comment
|
Short version: use 1. First-party sources
2. Prebuilt sourcesA field describing them can't exist. # TODO: enumerate directory?
return ManifestInfo(manifest = ..., artifacts = [(extracted, param)])The real file list comes from But you want wheels verbatim, not their contents, and that's already exposed — source = binary_src # the .whl, untouched
providers.append(DefaultInfo(default_output = source, sub_targets = sub_targets))So 3. The rule# tools/blender_extension.bzl
load("@prelude//:paths.bzl", "paths")
load("@prelude//python:python.bzl", "PythonLibraryInfo", "PythonLibraryManifestsTSet")
def _blender_extension_impl(ctx: AnalysisContext) -> list[Provider]:
tree = {} # dest path in the zip -> Artifact
# First-party Python, from the transitive manifest tset.
manifests = ctx.actions.tset(
PythonLibraryManifestsTSet,
children = [d[PythonLibraryInfo].manifests for d in ctx.attrs.deps],
)
for node in manifests.traverse():
# Skip prebuilt libs: their "srcs" is one opaque extracted directory.
if str(node.label.raw_target()) in ctx.attrs.wheel_labels:
continue
for manifest in [node.srcs, node.default_resources[0] if node.default_resources else None]:
if manifest == None:
continue
for artifact, dest in manifest.artifacts:
existing = tree.get(dest)
if existing != None and existing != artifact:
fail("conflicting entries for {}: {} and {}".format(dest, existing, artifact))
tree[dest] = artifact
# Third-party wheels, exactly as downloaded.
wheels, seen = [], {}
for dep in ctx.attrs.wheels + (ctx.attrs.wheels_query or []):
if PythonLibraryInfo not in dep:
continue
for whl in dep[DefaultInfo].default_outputs:
if whl.basename not in seen:
seen[whl.basename] = True
wheels.append(whl)
wheels = sorted(wheels, key = lambda w: w.basename) # deterministic manifest
for whl in wheels:
tree[paths.join("wheels", whl.basename)] = whl
# blender_manifest.toml. TOML string arrays are JSON-compatible.
tree["blender_manifest.toml"] = ctx.actions.write(
"blender_manifest.toml",
[
'schema_version = "1.0.0"',
'id = "{}"'.format(ctx.attrs.extension_id),
'version = "{}"'.format(ctx.attrs.version),
"name = {}".format(json.encode(ctx.attrs.extension_name)),
"tagline = {}".format(json.encode(ctx.attrs.tagline)),
"maintainer = {}".format(json.encode(ctx.attrs.maintainer)),
'type = "add-on"',
'blender_version_min = "{}"'.format(ctx.attrs.blender_version_min),
"license = {}".format(json.encode(ctx.attrs.license)),
"wheels = {}".format(json.encode(["./wheels/" + w.basename for w in wheels])),
],
)
staged = ctx.actions.copied_dir("__staged__", tree)
out = ctx.actions.declare_output("{}.zip".format(ctx.attrs.extension_id))
ctx.actions.run(
cmd_args(
ctx.attrs._mkzip[RunInfo],
cmd_args(out.as_output(), format = "--output={}"),
cmd_args(staged, format = "--root={}"),
),
category = "blender_extension",
)
return [DefaultInfo(default_output = out)]
blender_extension = rule(
impl = _blender_extension_impl,
attrs = {
"blender_version_min": attrs.string(default = "4.2.0"),
"deps": attrs.list(attrs.dep(providers = [PythonLibraryInfo]), default = []),
"extension_id": attrs.string(),
"extension_name": attrs.string(),
"license": attrs.list(attrs.string(), default = ["SPDX:GPL-3.0-or-later"]),
"maintainer": attrs.string(),
"tagline": attrs.string(),
"version": attrs.string(),
"wheel_labels": attrs.list(attrs.string(), default = []),
"wheels": attrs.list(attrs.dep(), default = []),
"wheels_query": attrs.option(attrs.query(), default = None),
"_mkzip": attrs.default_only(attrs.exec_dep(default = "//tools:mkzip")),
},
)# tools/mkzip.py
import argparse, os, zipfile
p = argparse.ArgumentParser()
p.add_argument("--output", required=True)
p.add_argument("--root", required=True)
args = p.parse_args()
with zipfile.ZipFile(args.output, "w", zipfile.ZIP_DEFLATED) as z:
for dirpath, dirnames, filenames in os.walk(args.root):
dirnames.sort()
for name in sorted(filenames):
path = os.path.join(dirpath, name)
info = zipfile.ZipInfo(os.path.relpath(path, args.root))
info.date_time = (1980, 1, 1, 0, 0, 0) # reproducible output
info.external_attr = 0o644 << 16
info.compress_type = zipfile.ZIP_DEFLATED
with open(path, "rb") as f:
z.writestr(info, f.read())# BUCK
load("//tools:blender_extension.bzl", "blender_extension")
native.python_bootstrap_binary(name = "mkzip", main = "tools/mkzip.py")
http_file(
name = "requests-whl",
urls = ["https://files.pythonhosted.org/.../requests-2.32.3-py3-none-any.whl"],
sha256 = "...",
out = "requests-2.32.3-py3-none-any.whl",
)
prebuilt_python_library(name = "requests", binary_src = ":requests-whl")
python_library(
name = "my_addon_lib",
srcs = glob(["my_addon/**/*.py"]),
base_module = "", # so files land at the zip root, not under the package path
deps = [":requests"],
)
blender_extension(
name = "my_addon",
deps = [":my_addon_lib"],
wheels = [":requests"],
wheel_labels = ["root//:requests"],
extension_id = "my_addon",
extension_name = "My Addon",
tagline = "Does a thing",
maintainer = "You <you@example.com>",
version = "1.0.0",
)
Note The one awkward part is |
Uh oh!
There was an error while loading. Please reload this page.
I want to write a rule to package a Blender extension. Such packages are a zip archive of:
I see
PythonLibraryManifestsInterfacedefines many fields, which I guess are functions that return transitive sets of information about a library and its transitive dependencies? This looks like it could be used to gather all the first-party source code, though I haven't found a way to actually convert any of them to a list of paths I could pass toAnalysisActions.copied_dir. I think this is possible but I don't understand how to use transitive sets; guidance/examples would be very welcome.More fundamentally, I don't see any fields that look like they describe all the
prebuilt_python_librarysources. Does such a thing exist? If not, could it be added? I believe this is useful for Python packaging in general: see applications discussed at astral-sh/uv#1681.All reactions