Skip to content
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

Fix pyoxidizer_binary to support python_distribution targets that depend on others (Cherry-pick of #14620) #14626

Merged
merged 1 commit into from Feb 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions build-support/bin/generate_docs.py
Expand Up @@ -213,6 +213,7 @@ def run_pants_help_all() -> dict[str, Any]:
"pants.backend.experimental.python",
"pants.backend.experimental.python.lint.autoflake",
"pants.backend.experimental.python.lint.pyupgrade",
"pants.backend.experimental.python.packaging.pyoxidizer",
"pants.backend.experimental.scala",
"pants.backend.experimental.scala.lint.scalafmt",
"pants.backend.google_cloud_function.python",
Expand Down
13 changes: 12 additions & 1 deletion src/python/pants/backend/python/packaging/pyoxidizer/rules.py
Expand Up @@ -16,8 +16,10 @@
PyOxidizerDependenciesField,
PyOxidizerEntryPointField,
PyOxidizerOutputPathField,
PyOxidizerTarget,
PyOxidizerUnclassifiedResources,
)
from pants.backend.python.target_types import GenerateSetupField, WheelField
from pants.backend.python.util_rules.pex import Pex, PexProcess, PexRequest
from pants.core.goals.package import BuiltPackage, BuiltPackageArtifact, PackageFieldSet
from pants.engine.fs import (
Expand All @@ -38,9 +40,11 @@
FieldSetsPerTargetRequest,
HydratedSources,
HydrateSourcesRequest,
InvalidTargetException,
Targets,
)
from pants.engine.unions import UnionRule
from pants.util.docutil import doc_url
from pants.util.logging import LogLevel

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -105,7 +109,7 @@ async def package_pyoxidizer_binary(
)

deps_field_sets = await Get(
FieldSetsPerTarget, FieldSetsPerTargetRequest(PackageFieldSet, [direct_deps[0]])
FieldSetsPerTarget, FieldSetsPerTargetRequest(PackageFieldSet, direct_deps)
)
built_packages = await MultiGet(
Get(BuiltPackage, PackageFieldSet, field_set) for field_set in deps_field_sets.field_sets
Expand All @@ -116,6 +120,13 @@ async def package_pyoxidizer_binary(
for artifact in built_pkg.artifacts
if artifact.relpath is not None and artifact.relpath.endswith(".whl")
]
if not wheel_paths:
raise InvalidTargetException(
f"The `{PyOxidizerTarget.alias}` target {field_set.address} must include "
"in its `dependencies` field at least one `python_distribution` target that produces a "
f"`.whl` file. For example, if using `{GenerateSetupField.alias}=True`, then make sure "
f"`{WheelField.alias}=True`. See {doc_url('python-distributions')}."
)

config_template = None
if field_set.template.value is not None:
Expand Down
Expand Up @@ -9,8 +9,32 @@


def test_end_to_end() -> None:
"""We test a couple edge cases:

* Third-party dependencies can be used.
* A `python_distribution` (implicitly) depending on another `python_distribution`.
"""
sources = {
"hellotest/main.py": "import colors; print('Hello test')",
"hellotest/utils/greeter.py": "GREET = 'Hello world!'",
"hellotest/utils/BUILD": dedent(
"""\
python_sources(name="lib")

python_distribution(
name="dist",
dependencies=[":lib"],
provides=python_artifact(name="utils-dist", version="0.0.1"),
)
"""
),
"hellotest/main.py": dedent(
"""\
import colors
from hellotest.utils.greeter import GREET

print(GREET)
"""
),
"hellotest/BUILD": dedent(
"""\
python_requirement(name="req", requirements=["ansicolors==1.1.8"])
Expand All @@ -20,13 +44,13 @@ def test_end_to_end() -> None:
python_distribution(
name="dist",
dependencies=[":lib"],
provides=python_artifact(name="dist", version="0.0.1"),
provides=python_artifact(name="main-dist", version="0.0.1"),
)

pyoxidizer_binary(
name="bin",
entry_point="hellotest.main",
dependencies=[":dist"],
dependencies=[":dist", "{tmpdir}/hellotest/utils:dist"],
)
"""
),
Expand All @@ -44,4 +68,30 @@ def test_end_to_end() -> None:
# Check that the binary is executable.
bin_path = next(Path("dist", f"{tmpdir}.hellotest", "bin").glob("*/debug/install/bin"))
bin_stdout = subprocess.run([bin_path], check=True, stdout=subprocess.PIPE).stdout
assert bin_stdout == b"Hello test\n"
assert bin_stdout == b"Hello world!\n"


def test_requires_wheels() -> None:
sources = {
"hellotest/BUILD": dedent(
"""\
python_distribution(
name="dist",
wheel=False,
provides=python_artifact(name="dist", version="0.0.1"),
)

pyoxidizer_binary(name="bin", dependencies=[":dist"])
"""
),
}
with setup_tmpdir(sources) as tmpdir:
args = [
"--backend-packages=['pants.backend.python', 'pants.backend.experimental.python.packaging.pyoxidizer']",
f"--source-root-patterns=['/{tmpdir}']",
"package",
f"{tmpdir}/hellotest:bin",
]
result = run_pants(args)
result.assert_failure()
assert "InvalidTargetException" in result.stderr
Expand Up @@ -3,6 +3,7 @@

from __future__ import annotations

from pants.backend.python.target_types import GenerateSetupField, WheelField
from pants.core.goals.package import OutputPathField
from pants.engine.target import (
COMMON_TARGET_FIELDS,
Expand All @@ -12,6 +13,7 @@
StringSequenceField,
Target,
)
from pants.util.docutil import doc_url


class PyOxidizerOutputPathField(OutputPathField):
Expand Down Expand Up @@ -47,14 +49,32 @@ class PyOxidizerEntryPointField(StringField):


class PyOxidizerDependenciesField(Dependencies):
pass
required = True
supports_transitive_excludes = True
help = (
"The addresses of `python_distribution` target(s) to include in the binary, e.g. "
"`['src/python/project:dist']`.\n\n"
"The distribution(s) must generate at least one wheel file. For example, if using "
f"`{GenerateSetupField.alias}=True`, then make sure `{WheelField.alias}=True`. See "
f"{doc_url('python-distributions')}.\n\n"
"Usually, you only need to specify a single `python_distribution`. However, if "
"that distribution depends on another first-party distribution in your repository, you "
"must specify that dependency too, otherwise PyOxidizer would try installing the "
"distribution from PyPI. Note that a `python_distribution` target might depend on "
"another `python_distribution` target even if it is not included in its own `dependencies` "
f"field, as explained at {doc_url('python-distributions')}; if code from one distribution "
"imports code from another distribution, then there is a dependency and you must "
"include both `python_distribution` targets in the `dependencies` field of this "
"`pyoxidizer_binary` target.\n\n"
"Target types other than `python_distribution` will be ignored."
)


class PyOxidizerUnclassifiedResources(StringSequenceField):
alias = "filesystem_resources"
help = (
"Adds support for listing dependencies that MUST be installed to the filesystem "
"(e.g. Numpy). See"
"(e.g. Numpy). See "
"https://pyoxidizer.readthedocs.io/en/stable/pyoxidizer_packaging_additional_files.html#installing-unclassified-files-on-the-filesystem"
)

Expand Down Expand Up @@ -88,5 +108,14 @@ class PyOxidizerTarget(Target):
PyOxidizerUnclassifiedResources,
)
help = (
"A single-file Python executable with a Python interpreter embedded, built via PyOxidizer."
"A single-file Python executable with a Python interpreter embedded, built via "
"PyOxidizer.\n\n"
"To use this target, first create a `python_distribution` target with the code you want "
f"included in your binary, per {doc_url('python-distributions')}. Then add this "
f"`python_distribution` target to the `dependencies` field. See the `help` for "
f"`dependencies` for more information.\n\n"
f"You may optionally want to set the `{PyOxidizerEntryPointField.alias}` field. For "
"advanced use cases, you can use a custom PyOxidizer config file, rather than what Pants "
f"generates, by setting the `{PyOxidizerConfigSourceField.alias}` field. You may also want "
"to set `[pyoxidizer].args` to a value like `['--release']`."
)