BinaryBuilder.jl

repository·master·Indexed 19 days ago

https://github.com/juliapackaging/binarybuilder.jl

A tool for creating reproducible, cross-compiled binary packages (JLLs) for the Julia ecosystem. It automates the process of building libraries and executables in controlled Linux environments to ensure deployability across all Julia-supported platforms. It includes a build wizard, support for various toolchains (CMake, Autoconf, Meson, Go, Rust, OCaml), and integration with the Yggdrasil community buildtree.

Tokens
13.8K
Snippets
52
Records
83
Agent score
65%

What's inside BinaryBuilder.jl

  1. How BinaryBuilder.jl handles binary compatibility and portability

    master

    BinaryBuilder.jl automatically manages several low-level compatibility issues to ensure binaries are portable across different machines and operating systems. These include:

    • Uniform Compiler Interface: Provides a consistent environment where gcc, g++, and gfortran are available via standard names or triplet-prefixed names (e.g., x86_64-linux-gnu-gcc).
    • glibc Versioning: Compiles against older glibc versions to ensure maximum compatibility with newer Linux distributions.
    • gfortran ABI Compatibility: Handles the broken ABI transitions in gfortran (6.X $\rightarrow$ 7.X and 7.X $\rightarrow$ 8.X) by building against multiple versions and selecting the correct one at runtime.
    • Library Dependencies: Automatically fixes improper library linkage (especially on macOS) by using RPATH/RUNPATH semantics.
    • Embedded Absolute Paths: Automatically converts symlinks within the build prefix to relative paths and warns if files contain absolute paths to the build prefix.
    • Instruction Set Architecture (ISA): Disassembles binaries to ensure they do not use instructions that exceed the target architecture's minimum supported instruction set. It also detects cpuid instructions, which indicate the binary is handling instruction set detection internally.
  2. Build platform-independent packages

    master

    For packages that do not contain platform-specific binaries (e.g., header-only libraries or datasets), you can use the special AnyPlatform() platform.

    When building for AnyPlatform():

    • The build environment will appear as x86_64-linux-musl.
    • You can only use FileProduct (since LibraryProduct and ExecutableProduct are platform-dependent).
    • The resulting JLL package will be platform-independent and installable on any machine.
    platforms = [AnyPlatform()]
  3. Distinguish between Dependency, BuildDependency, and HostBuildDependency

    master

    Because BinaryBuilder uses cross-compilation, you must categorize dependencies based on whether they are for the target (where the code runs) or the host (where the code is built):

    1. Dependency (Target): Binary libraries the final product needs to link to. These are built for the target platform and will be included in the final JLL package.
    2. BuildDependency (Target): Binary libraries or executables (like shell scripts) needed only during the build process for the target platform. These are not included in the final JLL package.
    3. HostBuildDependency (Host): Binary executables needed during the build that cannot run on the target platform (e.g., a build tool like cmake or ninja).
      • These are installed under ${host_prefix}.
      • Executables are located in ${host_bindir}, which is automatically added to the ${PATH}.
      • Use this for tools available in a JLL package for the x86_64-linux-musl platform.

    Note: If your target library requires another library to link against, you must use a Dependency (Target), not a HostBuildDependency.

  4. Understand the BinaryBuilder build strategy

    master

    BinaryBuilder creates a tarball from all files found inside the ${prefix} directory at the end of a build, excluding files that come from listed dependencies.

    To ensure your package is correctly captured, your build script must install relevant files into the appropriate subdirectories under ${prefix}. Common directories include:

    • ${libdir}: for libraries
    • ${bindir}: for binary executables

    If a package does not have an automated build system (like CMake or Autoconf), you must manually move or compile files into these directories.

  5. Understand the anatomy of a JLL package

    master

    JLL packages are automatically generated Julia packages with a specific structure. Key components include:

    • Artifacts.toml: Contains metadata and download URLs for the platform-specific tarballs.
    • src/NAME_jll.jl: The main entry point. It detects the current platform and loads the appropriate wrapper.
    • src/wrappers/: A directory containing platform-specific files (e.g., x86_64-linux-gnu.jl) that handle the actual loading of binaries.
    • Project.toml: Defines package dependencies and compatibility.
    • LICENSE: The license for the Julia wrapper (which may differ from the wrapped library's license).
  6. Understand the BinaryBuilder project flow

    master

    The workflow for distributing a binary dependency via BinaryBuilder follows these steps:

    1. Local Development: Compile your library (C, C++, Rust, etc.) locally and write Julia bindings using Libdl.dlopen() and ccall().
    2. Create a Build Recipe: Create a build_tarballs.jl file. This file defines the name, version, source locations, build steps (as a bash script), and the resulting products. You can use the Wizard interface to generate this or manually edit/copy existing recipes from the Yggdrasil repository.
    3. Build Tarballs: Run the build process to generate compiled binaries for various platforms.
    4. Generate JLL Package: A successful build results in an autogenerated JLL (Julia Library) package. These are typically uploaded to the JuliaBinaryWrappers GitHub organization.
    5. Registration: A registration request is opened against the General Julia registry. This allows end-users to simply run pkg> add libfoo_jll to install your binary dependency.
  7. Configure package name and version

    master

    Name

    The name must be the upstream package name and a valid Julia identifier (no spaces, dashes, or dots; use underscores instead). It cannot start with a number. BinaryBuilder automatically appends _jll to the generated JLL package name. Use Base.isidentifier(name) to validate.

    Version

    Use a version matching the upstream package, but it must only contain major, minor, and patch numbers (e.g., v"1.2.3"). Versions with extra levels (e.g., v"1.2.3.4") or suffixes (e.g., v"1.2.3-alpha") are not supported. Truncate to the patch number if necessary.

  8. Understand the RootFS execution environment

    master

    All BinaryBuilder.jl builds run within a controlled execution environment called the RootFS (root filesystem).

    Key characteristics include:

    • Base Image: The RootFS is based on the alpine Docker image.
    • Toolchain Location: Target platform compiler toolchains are stored in /opt/${triplet}. For example, 64-bit Linux compilers (using glibc) are located at /opt/x86_64-linux-gnu/bin.
    • Shard System: To avoid downloading multi-gigabyte files, the RootFS is split into shards (e.g., a 'root' shard, platform support shards, GCC shards, LLVM shards, and Rust shards). BinaryBuilder.jl downloads these shards on-demand using the Julia Pkg.Artifacts system.
    • Process Environment: When BinaryBuilder.jl launches a process inside the RootFS, it automatically configures specific environment variables to enable target-specific compiler toolchains.
  9. Linking to BLAS/LAPACK libraries

    master

    When building numerical libraries, you must choose an integer model for array indexing: LP64 (32-bit integers) or ILP64 (64-bit integers).

    For 64-bit systems, it is recommended to use the ILP64 interface via libblastrampoline_jll. This matches Julia's default behavior and allows switching backends at runtime.

    Implementation details:

    • Dependency: Use Dependency("libblastrampoline_jll"; compat="5.4.0") and pass julia_compat="1.9" to build_tarballs.
    • Linking (Unix): Use -lblastrampoline.
    • Linking (Windows): Use -lblastrampoline-5 (where 5 is the major version).
    • Naming: On 64-bit systems, BLAS/LAPACK function calls must use the _64 suffix (e.g., dgemm_64).

    Alternative: Always use LP64

    If renaming symbols to the _64 suffix is too difficult, you can always use the LP64 interface, even on 64-bit systems.

    • Option A: Link to libblastrampoline, but you must ensure an LP64 library is backing it. Note that Julia does not provide a default LP64 backing for libblastrampoline on 64-bit systems.
    • Option B: Link directly to a library that provides an LP64 interface on 64-bit platforms, such as OpenBLAS32_jll.
  10. How BinaryBuilder achieves reproducibility

    master

    BinaryBuilder implements several practices to support reproducible builds:

    • Sandboxed Environment: Builds run inside a chroot jail using a carefully constructed RootFS, ensuring a fixed tree structure and reproducible build paths.
    • Controlled Toolchain: The toolchain sets specific environment variables and enforces compiler flags to minimize randomness.
    • Deterministic Tarballs: Tarballs are created using Tar.jl, which helps ensure that the same git tree hash results in the same tarball hash.

    Verification: At the end of a successful build, the SHA256 sum and git tree hash are printed to the screen and stored in the Artifacts.toml file of the generated JLL package. If you rebuild the same package version with the same BinaryBuilder version, these hashes should remain consistent.