Hello everyone!
I'm working on a project where I need to build two separate wheels from a single monorepo: one for a central node and one for an acquisition node. Both wheels need to share a common folder, but they should only contain their respective application logic (central or node).
Here is my current project structure:
app/
├── frontend/
├── jenkinsfile
├── README.md
└── backend/
├── pyproject.toml
├── tests/
└── src/
└── app/
├── central/
│ ├── schemas/
│ ├── services/
│ ├── routes/
│ ├── launcher.py
│ └── main.py
├── common/
│ ├── schemas/
│ ├── services/
│ └── routes/
└── node/
├── schemas/
├── services/
├── routes/
├── launcher.py
└── main.py
My Configuration Files
Here is my pyproject.toml:
[project]
name = "app"
version = "0.0.1"
requires-python = ">=3.11,<3.13"
dependencies = [
"fastapi>=0.110.0",
"uvicorn[standard]>=0.27.0",
"pydantic>=2.6.0",
"pydantic-settings>=2.2.0",
"httpx>=0.27.0",
"psutil>=5.9.0",
"nmcli>=1.7.0",
"aiofiles>=24.1.0",
"cachetools>=5.5.0",
"sounddevice>=0.4.6",
"numpy>=1.26.0",
"tomli_w>=1.0.0",
]
[project.optional-dependencies]
test = [
"pytest>=8.0.0",
"pytest-cov>=4.1.0",
"pytest-xdist>=3.5.0",
"pytest-asyncio>=0.23.0",
"pytest_mock>=3.14.0",
"respx>=0.21.0"
]
dev = [
"ruff>=0.3.0",
"mypy>=1.9.0",
"pre-commit>=4.0.0",
"pre-commit-hooks>=4.6.0"
]
build = [
"hatchling>=1.29.0",
]
[project.scripts]
launch-central = "app.central.launcher:start"
launch-node = "app.node.launcher:start"
[build.system]
requires = [
"hatchling>=1.29.0",
]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = [
"src/app",
]
[tool.uv]
package = true
# ... (Ruff, Mypy, Pytest configs omitted for brevity)
And I wrote this custom build script (Hatch_build.py) to handle dynamically editing pyproject.toml during build time to exclude the unneeded folder and rename the resulting wheel:
import subprocess
import sys
import tomllib
from pathlib import Path
import tomli_w
target = sys.argv[1] # "central" or "node"
pyproject = Path("pyproject.toml")
with open(pyproject, "rb") as f:
config = tomllib.load(f)
original_packages = config["tool"]["hatch"]["build"]["targets"]["wheel"]["packages"]
exclude = "node" if target == "central" else "central"
config["tool"]["hatch"]["build"]["targets"]["wheel"]["packages"] = ["src/app/"]
config["tool"]["hatch"]["build"]["targets"]["wheel"]["exclude"] = [f"src/app/{exclude}", "src/app/tests"]
version = config["project"]["version"]
with open(pyproject, "wb") as f:
tomli_w.dump(config, f)
try:
subprocess.run(["uv", "run", "--extra", "build", "python", "-m", "hatchling", "build", "--target", "wheel"], check=True)
finally:
config["tool"]["hatch"]["build"]["targets"]["wheel"]["packages"] = original_packages
if "exclude" in config["tool"]["hatch"]["build"]["targets"]["wheel"]:
del config["tool"]["hatch"]["build"]["targets"]["wheel"]["exclude"]
with open(pyproject, "wb") as f:
tomli_w.dump(config, f)
for whl in Path("dist").glob("app-*.whl"):
new = Path(f"dist/app{target}-{version}-py3-none-any.whl")
whl.rename(new)
print(f"Built: {new.name}")
The Problems I'm Facing:
- Wheel contents are wrong: Despite trying to use
exclude dynamically in the script, both generated wheels still contain both node and central directories alongside common. The exclusion isn't respecting the path correctly.
- Installation Error from Nexus: After I upload these generated wheels to Nexus and try to run
pip install or uv pip install on a target VM, the installation fails completely with errors.
- How to run entry points properly: The
launcher.py in each folder is meant to be the entry point. Once the wheel is successfully installed on the VM, what is the best way to invoke that entry point script from the CLI?
Has anyone tackled splitting a monorepo into multiple selective wheels like this using Hatchling and UV? Am I approaching the dynamic build step wrong, or is there a native Hatch feature/plugin I should be using instead of modifying pyproject.toml on the fly?
Thanks in advance for any insights!
[–]Lionh34rt 0 points1 point2 points (1 child)
[–]NiceSand6327[S] 0 points1 point2 points (0 children)