Skip to content

Commit deaea52

Browse files
committed
Initial crack at entrypoint support
1 parent b6d81d1 commit deaea52

File tree

8 files changed

+255
-15
lines changed

8 files changed

+255
-15
lines changed

e2e/MODULE.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,9 @@ uv.lockfile(
9090
lockfile = "//cases/uv-deps-650:uv-airflow.lock",
9191
venv_name = "airflow",
9292
)
93+
94+
# Ask uv to make entrypoints for setuptools
95+
uv.discover_entrypoints(
96+
requirement = "setuptools",
97+
)
9398
use_repo(uv, "pypi")

uv/private/extension.bzl

Lines changed: 124 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,8 @@ def _raw_whl_repos(module_ctx, lock_specs):
296296
#
297297
# Assume (potentially a problem!)
298298
name = _whl_repo_name(package, whl)
299-
print("Creating whl repo", name)
299+
300+
# print("Creating whl repo", name)
300301
downloaded_file_path = url.split("/")[-1]
301302
spec = dict(
302303
name = name,
@@ -331,7 +332,8 @@ def _sbuild_repos(module_ctx, lock_specs):
331332
continue
332333

333334
name = _sbuild_repo_name(hub_name, venv_name, package)
334-
print("Creating sdist repo", name)
335+
336+
# print("Creating sdist repo", name)
335337
sdist_build(
336338
name = name,
337339
src = "@" + _sdist_repo_name(package) + "//file",
@@ -348,6 +350,90 @@ def _whl_install_repo_name(hub, venv, package):
348350
package["name"],
349351
)
350352

353+
def debug(it):
354+
return "<%s %s>" % (type(it), {
355+
k: getattr(it, k)
356+
for k in dir(it)
357+
})
358+
359+
def parse_ini(lines):
360+
dict = {}
361+
heading = None
362+
for line in lines.split("\n"):
363+
line = line.strip()
364+
if line.startswith("[") and line.endswith("]"):
365+
heading = line[1:-2]
366+
dict[heading] = {}
367+
368+
elif "=" in line and heading:
369+
key, value = line.split("=", 1)
370+
key = key.strip()
371+
value = value.strip()
372+
dict[heading][key] = value
373+
374+
return dict
375+
376+
def _collect_entrypoints(module_ctx, lock_specs):
377+
# Collect all the packages anyone's allowing entrypoints for
378+
packages_to_collect = {}
379+
for mod in module_ctx.modules:
380+
for it in mod.tags.discover_entrypoints:
381+
packages_to_collect[normalize_name(it.requirement)] = 1
382+
383+
entrypoints = {}
384+
385+
# Collect predeclared entrypoints
386+
for mod in module_ctx.modules:
387+
for it in mod.tags.declare_entrypoint:
388+
r = normalize_name(it.requirement)
389+
entrypoints.setdefault(r, {})
390+
entrypoints[r][normalize_name(it.name)] = it.entrypoint
391+
392+
print(packages_to_collect)
393+
394+
for hub_name, venvs in lock_specs.items():
395+
for venv_name, lock in venvs.items():
396+
for package in lock.get("package", []):
397+
# We use packages_to_collect as both a set and a worklist
398+
if package["name"] not in packages_to_collect:
399+
continue
400+
401+
reference_prebuild = None
402+
403+
# Find the smallest available wheel
404+
for whl in package.get("wheels", []):
405+
reference_prebuild = whl
406+
break
407+
408+
if not reference_prebuild:
409+
continue
410+
411+
whl_filename = whl["url"].split("/")[-1]
412+
file = "private/" + whl_filename
413+
module_ctx.download(reference_prebuild["url"], sha256 = reference_prebuild["hash"][len("sha256:"):], output = file)
414+
res = module_ctx.execute(
415+
[
416+
"tar", # FIXME: Use a hermetic tar here somehow?
417+
"-xOzf",
418+
file,
419+
"*.dist-info/entry_points.txt",
420+
],
421+
)
422+
print(debug(res))
423+
if res.return_code == 0:
424+
entrypoints.setdefault(package["name"], {})
425+
whl_entrypoints = parse_ini(res.stdout)
426+
print(package["name"], res.stdout, whl_entrypoints)
427+
for name, entrypoint in whl_entrypoints.get("console_script", {}).items():
428+
entrypoints[package["name"]][normalize_name(name)] = entrypoint
429+
430+
packages_to_collect.pop(package["name"])
431+
432+
else:
433+
print(file, res.exit_code, res.stderr)
434+
435+
return entrypoints
436+
351437
def _whl_install_repos(module_ctx, lock_specs):
352438
for hub_name, venvs in lock_specs.items():
353439
for venv_name, lock in venvs.items():
@@ -366,7 +452,8 @@ def _whl_install_repos(module_ctx, lock_specs):
366452
# only with the single venv. Shouldn't be possible to force this
367453
# target to build when the venv hub is not pointed to this venv.
368454
name = _whl_install_repo_name(hub_name, venv_name, package)
369-
print("Creating install repo", name)
455+
456+
# print("Creating install repo", name)
370457
whl_install(
371458
name = name,
372459
prebuilds = json.encode(prebuilds),
@@ -385,7 +472,7 @@ def _marker_sha(marker):
385472
else:
386473
return None
387474

388-
def _group_repos(module_ctx, lock_specs):
475+
def _group_repos(module_ctx, lock_specs, entrypoint_specs):
389476
# Hub -> requirement -> venv -> True
390477
# For building hubs we need to know what venv configurations a given
391478

@@ -534,7 +621,8 @@ def _group_repos(module_ctx, lock_specs):
534621
# their direct dependencies beyond the scc. So we can just lay down
535622
# targets.
536623
name = _venv_hub_name(hub_name, venv_name)
537-
print("Creating venv hub", name)
624+
625+
# print("Creating venv hub", name)
538626
venv_hub(
539627
name = name,
540628
aliases = scc_aliases, # String dict
@@ -546,13 +634,14 @@ def _group_repos(module_ctx, lock_specs):
546634
package: _whl_install_repo_name(hub_name, venv_name, {"name": package})
547635
for package in sorted(graph.keys())
548636
},
637+
entrypoints = json.encode(entrypoint_specs),
549638
)
550639

551640
return package_venvs
552641

553-
def _hub_repos(module_ctx, lock_specs, package_venvs):
642+
def _hub_repos(module_ctx, lock_specs, package_venvs, entrypoint_specs):
554643
for hub_name, packages in package_venvs.items():
555-
print("Creating uv hub", hub_name)
644+
# print("Creating uv hub", hub_name)
556645
hub_repo(
557646
name = hub_name,
558647
hub_name = hub_name,
@@ -561,6 +650,7 @@ def _hub_repos(module_ctx, lock_specs, package_venvs):
561650
package: venvs.keys()
562651
for package, venvs in packages.items()
563652
},
653+
entrypoints = json.encode(entrypoint_specs),
564654
)
565655

566656
def _uv_impl(module_ctx):
@@ -578,6 +668,10 @@ def _uv_impl(module_ctx):
578668
# of conditions.
579669
configurations = _collect_configurations(module_ctx, lock_specs)
580670

671+
# Collect declared entrypoints for packages
672+
entrypoints = _collect_entrypoints(module_ctx, lock_specs)
673+
print("got entrypoints", entrypoints)
674+
581675
# Roll through and create sdist and whl repos for all configured sources
582676
# Note that these have no deps to this point
583677
_raw_sdist_repos(module_ctx, lock_specs)
@@ -587,13 +681,18 @@ def _uv_impl(module_ctx):
587681
_sbuild_repos(module_ctx, lock_specs)
588682

589683
# Roll through and create per-venv whl installs
684+
#
685+
# Note that we handle entrypoints at the venv level NOT the install level.
686+
# This is because we handle cycle breaking and deps at the venv level, so we
687+
# can't just take a direct dependency on the installed whl in its
688+
# implementation repo.
590689
_whl_install_repos(module_ctx, lock_specs)
591690

592691
# Roll through and create per-venv group/dep layers
593-
package_venvs = _group_repos(module_ctx, lock_specs)
692+
package_venvs = _group_repos(module_ctx, lock_specs, entrypoints)
594693

595694
# Finally the hubs themselves are fully trivialized
596-
_hub_repos(module_ctx, lock_specs, package_venvs)
695+
_hub_repos(module_ctx, lock_specs, package_venvs, entrypoints)
597696

598697
configurations_hub(
599698
name = "aspect_rules_py_pip_configurations",
@@ -622,11 +721,27 @@ _lockfile_tag = tag_class(
622721
},
623722
)
624723

724+
_declare_entrypoint = tag_class(
725+
attrs = {
726+
"requirement": attr.string(mandatory = True),
727+
"name": attr.string(mandatory = True),
728+
"entrypoint": attr.string(mandatory = True),
729+
},
730+
)
731+
732+
_create_entrypoints = tag_class(
733+
attrs = {
734+
"requirement": attr.string(mandatory = True),
735+
},
736+
)
737+
625738
uv = module_extension(
626739
implementation = _uv_impl,
627740
tag_classes = {
628741
"declare_hub": _hub_tag,
629742
"declare_venv": _venv_tag,
630743
"lockfile": _lockfile_tag,
744+
"declare_entrypoint": _declare_entrypoint,
745+
"discover_entrypoints": _create_entrypoints,
631746
},
632747
)

uv/private/hub/repository.bzl

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ pip = struct(
106106
################################################################################
107107
# Lay down the hub aliases
108108

109+
entrypoints = json.decode(repository_ctx.attr.entrypoints)
110+
109111
# FIXME: since we're creating a package per target, we may have to implement
110112
# name mangling to ensure that the pip packages become valid Bazel packages.
111113
for name, spec in repository_ctx.attr.packages.items():
@@ -119,6 +121,7 @@ load("//:defs.bzl", "pip")
119121
"//venv:{}".format(it): "@venv__{0}__{1}//{2}:{2}".format(repository_ctx.attr.hub_name, it, name)
120122
for it in spec
121123
}
124+
error = "Available only in venvs " + ", ".join([it.split(":")[1][1:] for it in select_spec.keys()])
122125

123126
# TODO: Find a way to add a dist-info target here
124127
# TODO: Find a way to add entrypoint targets here?
@@ -140,18 +143,53 @@ alias(
140143
name = name,
141144
select = repr(select_spec),
142145
compat = repr(spec),
143-
error = "Available only in venvs " + ", ".join([it.split(":")[1][1:] for it in select_spec.keys()]),
146+
error = error,
144147
),
145148
)
146149

147150
repository_ctx.file(name + "/BUILD.bazel", content = "\n".join(content))
148151

152+
content = [
153+
"""load("//:defs.bzl", "pip")""",
154+
]
155+
for entrypoint_name, entrypoint_coordinate in entrypoints.get(name, {}).items():
156+
select_spec = {
157+
"//venv:{}".format(it): "@venv__{0}__{1}//{2}/entrypoints:{3}".format(repository_ctx.attr.hub_name, it, name, entrypoint_name)
158+
for it in spec
159+
}
160+
161+
content.append(
162+
"""
163+
alias(
164+
name = "{name}",
165+
actual = select(
166+
{select},
167+
no_match_error = "{error}",
168+
),
169+
target_compatible_with = pip.compatible_with({compat}),
170+
visibility = ["//visibility:public"],
171+
)
172+
""".format(
173+
name = entrypoint_name,
174+
select = repr(select_spec),
175+
compat = repr(spec),
176+
error = error,
177+
),
178+
)
179+
180+
repository_ctx.file(name + "/entrypoints/BUILD.bazel", content = "\n".join(content))
181+
149182
hub_repo = repository_rule(
150183
implementation = _hub_impl,
151184
attrs = {
152185
"hub_name": attr.string(),
153186
"venvs": attr.string_list(),
154187
"packages": attr.string_list_dict(),
155188
"configurations": attr.string_list_dict(),
189+
"entrypoints": attr.string(
190+
doc = """
191+
JSON encoded map of pkg -> entrypoint -> coordinate
192+
""",
193+
),
156194
},
157195
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
exports_files(["entrypoint.tmpl"])
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
load("@bazel_lib//lib:expand_template.bzl", "expand_template")
2+
load("//py/unstable:defs.bzl", "py_venv_binary")
3+
4+
5+
def py_entrypoint_binary(
6+
name,
7+
coordinate,
8+
deps,
9+
visibility = ["//visibility:public"],
10+
):
11+
main = "_{}_entrypoint".format(name)
12+
# <name> = <package_or_module>[:<object>[.<attr>[.<nested-attr>]*]]
13+
package, symbol = coordinate.split(":")
14+
15+
if "." in symbol:
16+
fn, tail = symbol.split(".", 1)
17+
alias = "{fn} = {fn}.{tail}\n".format(fn=fn, tail=tail)
18+
else:
19+
fn = symbol
20+
tail = ""
21+
alias = ""
22+
23+
expand_template(
24+
name = main,
25+
template = Label("@aspect_rules_py//uv/private/py_entrypoint_binary:entrypoint.tmpl"),
26+
out = main + ".py",
27+
substitutions = {
28+
"{{package}}": package,
29+
"{{fn}}": fn,
30+
"{{alias}}": alias
31+
},
32+
)
33+
34+
py_venv_binary(
35+
name = name,
36+
main = main + ".py",
37+
srcs = [main],
38+
deps = deps,
39+
visibility = visibility
40+
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env python3
2+
from {{package}} import {{fn}}
3+
{{alias}}
4+
if __name__ == '__main__':
5+
{{fn}}()

0 commit comments

Comments
 (0)