DEV Community

Alessio
Alessio

Posted on Originally published at py-forge.com

How to Build Offline Python Wheelhouses for Air-Gapped Systems

Deploying Python applications into isolated, regulated, or air-gapped environments presents unique operational hurdles. When target servers lack access to the public internet, a standard installation command pointing to public package indexes cannot function.

Engineers often attempt to solve this by manually collecting package archives on an internet-connected workstation and transferring them across a network boundary via secure storage media. However, manual archive collection quickly exposes operational pitfalls: transitive dependencies are easily missed, source archives may accidentally be collected that fail to build on systems lacking development tools, and verifying the cryptographic integrity of dozens of packages by hand is tedious.

Wheelhouse Packer addresses this exact workflow. It downloads target dependencies, audits the resulting artifacts, verifies their cryptographic digests, outputs a hash-pinned dependency file, and writes automated installation scripts to ensure reliable execution on isolated machines.

Install & Usage

The tool requires no external third-party libraries and runs directly using the Python standard library.

To install the tool, download the single file, save it as wheelhouse_packer_offline_install_bundles.py, and run it directly with your Python interpreter:

python wheelhouse_packer_offline_install_bundles.py --help
Enter fullscreen mode Exit fullscreen mode

Basic Packaging Workflow

To collect dependencies for a project, pass your existing requirements file and specify an output directory where the bundle will be assembled:

python wheelhouse_packer_offline_install_bundles.py \
    -r requirements.txt \
    -o ./offline-bundle
Enter fullscreen mode Exit fullscreen mode

You can also target specific packages directly by name from the command line:

python wheelhouse_packer_offline_install_bundles.py \
    cryptography requests urllib3 \
    -o ./offline-bundle
Enter fullscreen mode Exit fullscreen mode

Targeting Specific Environments

When preparing a bundle on a developer workstation for deployment to a different host environment, you can specify platform tags, Python versions, and ABI configurations:

python wheelhouse_packer_offline_install_bundles.py \
    -r requirements.txt \
    -o ./offline-bundle \
    --python-version 311 \
    --platform manylinux2014_x86_64 \
    --implementation cp
Enter fullscreen mode Exit fullscreen mode

Once the bundle is built, the output directory contains:

  • wheels/: A directory containing all downloaded package archives.
  • manifest.json: A detailed inventory of every file, its package version, archive type, and cryptographic hash.
  • requirements.pinned.txt: A dependency specification file with strict SHA-256 hashes.
  • install.sh, install.bat, and install.py: Standalone installation scripts to execute the deployment on the target machine without network access.

How It Works

The tool orchestrates several standard library modules—including hashlib, subprocess, json, and pathlib—to automate the preparation and auditing of offline bundles.

1. Artifact Retrieval

When invoked, the tool constructs a download command using the host system's packaging utilities. It translates command-line flags directly into corresponding package management parameters, directing all downloaded packages into a dedicated subfolder within the specified output directory.

2. Cryptographic Auditing and Artifact Categorization

Once files are downloaded, the tool inspects every archive in the target directory. It calculates a SHA-256 digest in chunks to handle large files without excessive memory consumption, and parses file naming conventions defined by packaging standards:

def calculate_sha256(filepath: Path) -> str:
    """Compute the hexadecimal SHA-256 digest of a file in streaming chunks."""
    h = hashlib.sha256()
    with open(filepath, "rb") as f:
        while chunk := f.read(65536):
            h.update(chunk)
    return h.hexdigest()
Enter fullscreen mode Exit fullscreen mode

The script categorizes each artifact by parsing filenames according to packaging specifications. Pre-built distribution wheels are separated from raw source archives (.tar.gz, .zip). This distinction is critical for offline machines that may not have C compilers or system headers installed.

The resulting metadata is exported into manifest.json, along with a hash-pinned requirements file formatted for strict verification:

def generate_pinned_requirements(
    self, wheels: List[Dict[str, str]], sdists: List[Dict[str, str]]
) -> str:
    """
    Generate a hash-pinned requirements.txt compliant with:
    pip install --require-hashes
    """
    lines = [
        f"# Generated by {TOOL_NAME} v{TOOL_VERSION}",
        f"# Generated at: {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}",
        "# Strict offline hash-pinned dependencies",
        "",
    ]

    pkg_groups: Dict[str, List[Tuple[str, str]]] = {}
    for item in wheels + sdists:
        name = item.get("distribution", "unknown").lower().replace("_", "-")
        ver = item.get("version", "")
        key = f"{name}=={ver}" if ver and ver != "unknown" else name
        h = item.get("sha256", "")
        pkg_groups.setdefault(key, []).append((item["filename"], h))

    for pkg_spec in sorted(pkg_groups.keys()):
        lines.append(pkg_spec)
        for _, digest in pkg_groups[pkg_spec]:
            lines.append(f"    --hash=sha256:{digest}")

    lines.append("")
    return "\n".join(lines)
Enter fullscreen mode Exit fullscreen mode

3. Standalone Installer Generation

To avoid requiring operators to remember complex installation flags on target servers, the tool automatically generates three self-contained scripts (install.sh, install.bat, and install.py).

These scripts configure package installation to:

  1. Disable remote package lookups entirely (--no-index).
  2. Point strictly to the local directory (--find-links).
  3. Enforce cryptographic digest matching (--require-hashes).

On the target machine, the operator only needs to execute the generated script:

python install.py
Enter fullscreen mode Exit fullscreen mode

Limitations

While Wheelhouse Packer simplifies offline bundle preparation, it operates within specific technical constraints visible in its implementation:

  • Reliance on Host Tooling: The script relies on the presence of package management tools accessible through sys.executable. If the host Python environment lacks these tools, the download process cannot run.
  • Cross-Platform Resolution Constraints: When generating bundles for a target architecture that differs from the host workstation, package dependency resolution can encounter limitations. Package managers cannot always resolve conditional dependencies (environment markers) for an arbitrary remote platform without explicit pins.
  • Source Distribution Handling: If a package does not provide pre-built wheels for the target environment, the script will collect the source distribution (.tar.gz). While the script flags these files as warnings in manifest.json, it does not compile them. The target environment must have the necessary compilers, or the bundle will fail to install.
  • Target Runtime Prerequisites: The output bundle does not include a Python interpreter itself. The target system must already have a compatible Python runtime provisioned.

FAQ

Does this tool require any third-party packages to run?

No. The tool is written exclusively using the Python standard library. It does not require installing any external dependencies to execute the collection, hashing, and script generation logic.

Why does the tool generate hash-pinned requirement files?

In secure and regulated environments, verifying the integrity of dependencies is mandatory. By using hash pinning, the installer validates that every package installed on the destination machine matches the exact SHA-256 digest recorded during the packaging stage, guarding against artifact tampering or file corruption during transfer.

What happens if one of the downloaded dependencies is a source archive?

The tool inspects archive extensions and marks source distributions separately in manifest.json under sdist_count and sdist_warning. If your destination machine does not possess a C toolchain, you will need to replace the source archive with a pre-built wheel suitable for that platform before transferring the bundle.


Full version and the rest of the toolkit: Wheelhouse Packer offline install bundles on PyForge. Product page: https://py-forge.com/products/wheelhouse-packer-offline-install-bundles

Top comments (0)