opam-nix

repository·main·Indexed 41 days ago

https://github.com/tweag/opam-nix

A tool that turns opam-based OCaml projects into Nix derivations, automatically resolving OCaml and system dependencies for reproducible builds across Linux and macOS (x86_64 and aarch64). It provides a public API via its lib output, including functions like queryToScope, buildOpamProject, and buildDuneProject, and supports build acceleration through materialization to avoid expensive Import From Derivation (IFD) and opam resolution at evaluation time.

Tokens
3.5K
Snippets
12
Records
14
Agent score
1%

What's inside opam-nix

  1. Understand opam-nix Terminology: Package, Repository, Query, and Scope

    main

    To use opam-nix, you must understand its core abstractions which map OCaml/opam concepts to Nix concepts:

    • Package: A Nix derivation representing an opam package. It includes a nix-support/is-opam-nix-package file and a nix-support/setup-hook for environment variable propagation. Metadata about the original opam file is stored in passthru.pkgdef.
    • Repository: A directory (or derivation producing one) containing repo/ and packages/ directories. The packages/ directory must follow the structure packages/<name>/<name>.<version>/<opam-file>.
    • Query: An attribute set mapping package names to versions (e.g., { pkg = "1.0.0"; }). Use "*" to specify the latest version for functions like opamList.
    • Scope: A Nix package set representing an opam switch. It is self-referential, meaning packages within the scope can depend on each other. Note that a scope can only contain one version of any given package due to OCaml linking constraints.
  2. Manipulate Scopes with overrideScope and callPackage

    main

    A Scope is a Nix package set representing an opam switch. You can manipulate scopes using two primary methods:

    • overrideScope: Used to apply overlays to the entire scope.
    • callPackage: Used to retrieve Packages from the output of opam2nix, allowing you to supply dependencies from the existing scope.

    A scope is self-referential, allowing packages within the set to refer to one another.

    {
      overrideScope = (Scope → Scope → Scope) → Scope;
      callPackage = (Dependencies → Package) → Dependencies → Package;
      ${package_name} = package : Package;
      ...
    }
  3. Work with Opam Monorepos

    main

    The project provides tools to replicate the opam-monorepo workflow, allowing you to treat a collection of packages as a single unit.

    Creating a Monorepo

    • buildOpamMonorepo: Creates a monorepo for an opam project. It produces an attribute set of dev-repo sources for all dependencies of the packages in the project directory (and other packages in the Query).
    • queryToMonorepo: Similar to queryToScope, but produces an attribute set mapping package names to sources.

    Monorepo Materialization

    To avoid IFD when using monorepos, you can materialize the monorepo definition to JSON.

    1. Generate definitions:
      # Use the CLI to generate monorepo-defs.json
      cat $(nix eval --raw --allow-import-from-derivation --impure --expr '...') | jq > monorepo-defs.json
    2. Import definitions: Use unmaterializeQueryToMonorepo to turn the JSON file back into an attribute set.

    Usage Example:

    (unmaterializeQueryToMonorepo { sourceMap.my-monorepo = ./.; } ./monorepo-defs.json).Zarith
  4. Speed up builds using Materialization

    main

    Materialization avoids expensive Import From Derivation (IFD) and opam resolution at evaluation time by splitting the process into two steps: resolving package definitions to a JSON file, and then loading those definitions into a Nix scope.

    Step 1: Generate the definition file

    Use materialize or materializeOpamProject to resolve a query and produce a JSON file containing package definitions.

    Using the CLI tool:

    # Generate package-defs.json for a project
    opam-nix-gen my-package . package-defs.json

    Using Nix directly:

    # Evaluate the result of materializeOpamProject to a file
    cat $(nix eval --raw .#package-defs) > package-defs.json

    Step 2: Load the definitions into a Scope

    Use materializedDefsToScope to turn the JSON file into a usable Nix Scope. This step is fast and does not require opam or opam2json at evaluation time (though opam2json is still needed for the actual build).

    Usage Example:

    (materializedDefsToScope { sourceMap.my-package = ./.; } ./package-defs.json).my-package

    Regeneration

    The generated JSON file includes a __opam_nix_regen attribute. You can use the opam-nix-regen command to automatically regenerate the file using that command.

    opam-nix-gen my-package . package-defs.json
  5. Configure opam environment variables via overrideAttrs

    main

    You can set opam environment variables (used for substitution or package filters) by using overrideAttrs on a package. To do this, you must transform the variable name using these rules:

    1. Prepend opam__.
    2. Replace - and + with _.
    3. Replace : (the separator between package name and variable) with __.

    Example: To set cmdliner's conf-g++:installed variable to true, use the transformed name opam__conf_g____installed.

    cmdliner.overrideAttrs (_: { opam__conf_g____installed = "true"; })
  6. Build or shell into packages from opam-repository

    main

    The opam-nix flake provides access to all packages from opam-repository via its legacyPackages output. This allows you to build specific versions or enter a development shell for a package without modifying your local opam switches.

    Use the syntax github:tweag/opam-nix#<package>.<version> or github:tweag/opam-nix#<package>.latest to access these packages.

    # Build a specific version of utop
    nix build 'github:tweag/opam-nix#utop."2.10.0"'
    
    # Enter a shell with the latest version of camyll
    nix shell github:tweag/opam-nix#camyll.latest
  7. Initialize an opam-nix Nix flake

    main

    You can use opam-nix templates to quickly set up a Nix flake for your OCaml project. These templates assume you already have an OCaml project packaged with opam files.

    If you are starting from scratch, first initialize your project using dune (e.g., nix-shell -p dune_3 --run "dune init project <project_name> ."), then use one of the following templates:

    • Simple package build: A no-frills build setup.
    • Executable flake: Builds an executable and provides a development shell.
    • Multi-package flake: Builds multiple packages from the same workspace and provides a development shell.

    Important: If you are using Git, you must run git add flake.nix after initialization, as Nix operates on the Git index contents.

    # Simple package build
    nix flake init -t github:tweag/opam-nix
    
    # Executable and development shell
    nix flake init -t github:tweag/opam-nix#executable
    
    # Multi-package workspace with development shell
    nix flake init -t github:tweag/opam-nix#multi-package
  8. Access the opam-nix Public-facing API

    main

    The public API is provided via the lib output of the opam-nix flake. Because functions are platform-specific, they are mapped over available platforms (e.g., lib.x86_64-linux). To use the API, you must access the functions through the per-platform package set.

    Using with a Flake

    When using flake-utils, you can bring the per-platform library into scope using with opam-nix.lib.${system};.

    Using without a Flake

    You can import the library directly from a tarball using builtins.fetchTarball and selecting the current system.

    # Flake example
    { 
      inputs.opam-nix.url = "github:tweag/opam-nix";
      inputs.flake-utils.url = "github:numtide/flake-utils";
    
      outputs = { self, opam-nix, flake-utils }: 
        flake-utils.lib.eachDefaultSystem (system: 
          with opam-nix.lib.${system}; { 
            packages.default = # ...
          });
    }
    
    # Flake-less example
    with (import (builtins.fetchTarball "https://github.com/tweag/opam-nix/archive/main.tar.gz")).lib.${builtins.currentSystem};
  9. Try opam-nix examples

    main

    The opam-nix repository contains various examples demonstrating different use cases (e.g., static linking, GUI packages, Rocq packages, or multi-package repositories). You can test these examples directly using Nix commands without cloning the repository manually.

    • To build a specific example: nix build github:tweag/opam-nix#<example_name>
    • To build all examples: nix flake check github:tweag/opam-nix
    # Build the opam-ed example
    nix build github:tweag/opam-nix#opam-ed
    
    # Build all examples in the repository
    nix flake check github:tweag/opam-nix
  10. Build an opam project with `buildOpamProject`

    main

    The buildOpamProject function is a convenience wrapper around queryToScope. It treats a local directory as an opam project (similar to opam install .). It creates a repository from the project directory and prepends it to the repos list.

    Arguments

    • name: The name of the package to extract from the project.
    • project: The path to the project directory.
    • Query: The set of other packages to include in the scope.
    • pinDepends: If true, pins dependencies specified in the package's pin-depends field.
    • recursive: If true, searches subdirectories for opam files; if false, only checks the top-level and opam/ directory.

    Usage Examples

    Build from a local directory:

    (buildOpamProject { } "my-package" ./. { }).my-package

    Force a non-system compiler:

    (buildOpamProject { } "my-package" ./. { ocaml-base-compiler = "*"; }).my-package

    Build with tests enabled:

    (buildOpamProject { resolveArgs.with-test = true; } "my-package" ./. { }).my-package
  11. Convert a Query to a Scope with `queryToScope`

    main

    The queryToScope function turns an opam Query (a set of package names and versions) into a Scope (a set of Nix derivations).

    Arguments

    • repos: A list of [Repository] to use. Defaults to opam-repository. Package versions from earlier repositories take precedence.
    • pkgs: The Nixpkgs set to use. Defaults to the nixpkgs input of the flake.
    • overlays: Nix overlays to apply. Defaults to defaultOverlay and staticOverlay if targeting static builds.
    • resolveArgs: A ResolveArgs object to configure the opam admin list --resolve call.

    Version Resolution

    • Use "*" as a version in the Query to let opam resolve the latest possible version.
    • The env field in ResolveArgs allows passing environment variables to opam admin list to influence resolution.

    Usage Examples

    Build a package using defaults:

    (queryToScope { } { opam-ed = "*"; ocaml-system = "*"; }).opam-ed

    Build a specific version with overrides:

    let
      scope = queryToScope { } { opam-ed = "0.4"; ocaml-system = "*"; };
      overlay = self: super: { 
        opam-file-format = super.opam-file-format.overrideAttrs(oa: { opam__ocaml__native = "true"; }); 
      };
    in (scope.overrideScope overlay).opam-ed

    Use static Nixpkgs:

    let
      scope = queryToScope { pkgs = pkgsStatic; } { opam-ed = "*"; ocaml-system = "*"; };
    in scope.opam-ed
  12. Build a Dune project with `buildDuneProject`

    main

    The buildDuneProject function is a wrapper around buildOpamProject. It is specifically designed for projects using dune's generate_opam_files feature. It runs dune build ${name}.opam in an environment containing dune_3 and ocaml from Nixpkgs before proceeding with the opam build.

    Usage Example

    (buildDuneProject { } "my-package" ./. { }).my-package