building-the-package.md 9.7 KB

Building the Package

NetBox package artifacts (a wheel and a source distribution) can be built and verified locally. During the v4.6.x preview period, published artifacts are intended for maintainer validation only, and installing NetBox via pip is not a supported installation path; experimental support for installing from production PyPI is planned for NetBox v4.7.0. This page is intended for maintainers and contributors working on the packaging itself; routine development does not require building a package.

The artifacts are always built by CI from a clean checkout (see .github/workflows/release.yml). A local build is useful for testing packaging changes before they are merged.

Prerequisites

Install the minimum local build tooling (all three are also included in the dev optional dependency group):

python -m pip install --upgrade build packaging twine

Building

Build both the source distribution (sdist) and the wheel into dist/:

python -m build

To build only the wheel (faster, and the form most useful for a quick local install test):

python -m build --wheel

The package version is derived from netbox/release.yaml by the build hook in scripts/packaging/hatch_metadata.py; you do not set it in pyproject.toml. The wheel's runtime dependency metadata (Requires-Dist) is generated from the pinned requirements.txt by the same hook, so the wheel ships the exact dependency versions NetBox is tested against.

Clean-tree caveat

Always build release artifacts from a clean checkout. The build configuration keeps deployment-local files out of the artifacts: the Hatch excludes drop every configuration*.py and ldap_config*.py except the two tracked configuration templates (configuration_example.py and configuration_testing.py, which are force-included explicitly), and CI verifies the contents of both the wheel and the sdist before anything is published.

These checks are defense in depth, not a license to build from a dirty tree: other untracked files under netbox/ can still be picked up by a local build. CI builds from a clean checkout, so the published artifacts are unaffected. For a comparable local build, use a fresh git clone or a separate clean worktree rather than your day-to-day development tree.

Verifying

Check the built artifacts for valid metadata and rendering:

twine check dist/*

Confirm the wheel's version, dependency metadata, and extras match netbox/release.yaml, the pinned requirements.txt, and the declared optional-dependency groups:

python scripts/verify_wheel_metadata.py dist/*.whl

Confirm the artifacts ship only the two tracked configuration templates, and that the wheel carries the runtime-critical bundled data (_data/release.yaml, templates, translations, static assets, and the deployment examples; the same content checks CI runs before publishing):

python scripts/verify_wheel_contents.py dist/*.whl
python scripts/verify_sdist_contents.py dist/*.tar.gz

Confirm requirements.txt is still consistent with the maintainer policy in base_requirements.txt (the same drift guard CI runs before publishing):

python scripts/verify_dependencies.py

Test-installing the wheel

Install the wheel into a throwaway virtual environment and run the system checks to confirm the package is importable and runnable:

python -m venv /tmp/netbox-build-test
/tmp/netbox-build-test/bin/python -m pip install --upgrade pip
/tmp/netbox-build-test/bin/python -m pip install dist/*.whl
PYTHONPATH=$PWD/scripts \
NETBOX_CONFIGURATION=smoketest_configuration \
NETBOX_SMOKETEST_BASE=/tmp/netbox-build-test-root \
/tmp/netbox-build-test/bin/netbox check

Without configuration, a wheel-installed NetBox looks for $NETBOX_ROOT/conf/configuration.py (default /opt/netbox/conf/configuration.py), which normally does not exist on a development workstation. The environment variables above point netbox check at the same minimal configuration module the release workflow's smoke-test job uses (scripts/smoketest_configuration.py); run the command from the repository root so PYTHONPATH can find it. NETBOX_SMOKETEST_BASE sets the writable scratch directory under which the module creates its media, reports, scripts, and static roots. Any other importable configuration module works the same way via NETBOX_CONFIGURATION (and PYTHONPATH, if the configuration lives outside the package). To exercise the full post-install task sequence from the wheel, run netbox upgrade --no-input against a throwaway database; this is what the release workflow's smoke-test job does.

Packaging architecture

This section is a developer-facing overview of how the package is assembled and how a pip-installed NetBox behaves at runtime. User-facing installation documentation for the pip install path will be added alongside experimental PyPI support (planned for NetBox v4.7.0); this page does not cover end-user installation steps.

Dynamic metadata

scripts/packaging/hatch_metadata.py is a Hatchling metadata hook (wired in via [tool.hatch.metadata.hooks.custom]). It computes the package version from netbox/release.yaml and the runtime dependencies from the pinned requirements.txt, so the published wheel's Requires-Dist carries the exact versions NetBox is tested against. Both fields are declared dynamic in pyproject.toml; the optional-dependency extras stay static.

sdist and the sdist-to-wheel guard

python -m build produces both an sdist and a wheel (the wheel is built from the sdist). The release workflow's verify-sdist job rebuilds a wheel from the published sdist and runs scripts/verify_wheel_metadata.py and scripts/verify_wheel_contents.py against it, so a missing build input (for example the metadata hook or base_requirements.txt) cannot regress unnoticed.

Wheel data layout

Source assets that are not Python modules are force-included with a netbox/netbox/_data/ target path by [tool.hatch.build.targets.wheel.force-include]; because the wheel's sources = ["netbox"] setting strips one leading netbox/, they install under netbox/_data/: templates, translations, the compiled project-static bundles, release.yaml, and the bundled deployment examples under _data/examples/ (gunicorn.py, the systemd units, nginx.conf, apache.conf, netbox.env). The documentation is deliberately not part of the wheel: neither the docs/ sources nor mkdocs.yml ship (the sdist carries both), so netbox upgrade --build-docs skips with a notice and DOCS_ROOT resolves to None, disabling the embedded docs. The service and web server examples are the canonical contrib/ files, bundled unmodified; netbox setup adapts the systemd units for a pip install at render time (an optional EnvironmentFile, no --pythonpath, console-script rqworker), and the path renderer already collapses the source-layout static path for nginx/apache, so both install methods share a single source. A unit test pins the transform anchors against contrib/, so an upstream contrib change fails CI rather than netbox setup. At runtime settings.py detects the bundled _data directory and sets BASE_DIR to it (install mode wheel); a source checkout has no _data, so BASE_DIR stays the project root (install mode checkout).

Wheel-mode runtime

A pip-installed NetBox keeps mutable instance state out of the immutable, disposable virtual environment. settings.py resolves NETBOX_ROOT (default /opt/netbox, overridable via the environment) as the instance root and defaults the writable paths (MEDIA_ROOT, REPORTS_ROOT, SCRIPTS_ROOT, STATIC_ROOT) beneath it. In a checkout NETBOX_ROOT equals BASE_DIR, so archive and Git installs are unaffected.

Configuration loading is handled by load_configuration() in netbox/netbox/settings_utils.py. An explicit NETBOX_CONFIGURATION module always wins; otherwise, in wheel mode it prefers NETBOX_ROOT/conf/configuration.py, loading it by file path, and falls back to a legacy NETBOX_ROOT/netbox/netbox/configuration.py with a migration warning. The configuration directory is added to sys.path only while the configuration file executes, so sibling imports can resolve; NETBOX_ROOT itself is never added, which avoids a stale source tree shadowing the installed package. A checkout keeps importing netbox.configuration. For LDAP deployments, settings.py exposes the active configuration file's directory as the CONFIGURATION_DIR setting, and load_ldap_config() loads ldap_config.py from that same directory: the active ldap_config.py is always the one beside the active configuration.py, regardless of install method. One compatibility exception: in checkout mode only, when no sibling file exists, the historical netbox/netbox/ldap_config.py module is imported with a RuntimeWarning, so existing source installs that use a custom NETBOX_CONFIGURATION keep working.

Console script

pyproject.toml registers a single entry point, netbox (netbox.cli:main). The wrapper resolves a few commands itself before importing Django, so they work without a configuration present:

  • netbox version / netbox --version print the installed package version.
  • netbox setup copies the bundled _data/examples/* into the instance root and --systemd-dir, and scaffolds the conf/ package from the bundled configuration_example.py. It never overwrites an existing configuration.py, conf/__init__.py, local_requirements.txt, or netbox.env, even with --force.
  • netbox secret-key prints a new 50-character SECRET_KEY value.

These names are reserved by the wrapper. Every other command falls through to the Django management commands (netbox upgrade, netbox check, and so on), which require a valid configuration.