rv

repository·main·Indexed 18 days ago

https://github.com/a2-ai/rv

A configuration-driven tool for managing and installing R packages in a reproducible, fast, and declarative manner. rv uses TOML configuration files to define R versions, repositories, and dependencies, utilizing a 'plan' and 'sync' workflow to maintain environment state. It includes features for migrating from renv, exporting configurations, and verifying system dependencies via 'rv sysdeps', with specific support for mapping RHEL-based distributions to Posit Package Manager APIs.

Tokens
15.3K
Snippets
55
Records
68
Agent score
57%

What's inside rv

  1. Detect installed RPM packages using `rpm -q`

    main

    For RPM-based distributions (CentOS, AlmaLinux, RedHat, RockyLinux, Fedora, OpenSUSE, SLE), rv detects system dependencies by executing the rpm -q command.

    Detection Process:

    1. Execute rpm -q {package_list}.
    2. Parse stdout to identify installed packages. The package name is extracted by finding the first hyphen followed by a digit (the start of the version string).
    3. Parse stderr for the string "is not installed" to explicitly mark packages as SysInstallationStatus::Absent.
    4. Fallback to checking the system PATH for known tools if the package status is Unknown.
    # Example manual verification of RPM query parsing
    rpm -q libcurl-devel openssl-devel fake-pkg 2>&1
    # Expected behavior: stdout contains installed, stderr contains 'fake-pkg is not installed'
  2. Error handling and package status in RPM environments

    main

    When checking system dependencies on RPM-based systems, rv follows these error handling and status rules:

    • Output Parsing: The tool parses both stdout (to identify installed packages) and stderr (to capture error messages).
    • Package Status:
      • Packages installed via dnf are reported as present.
      • Packages not installed are reported as absent.
      • Packages that are not recognized as system dependencies are marked as Unknown rather than causing an error, ensuring consistency with the existing dpkg implementation.
    • Flags: The --only-absent flag and --json output format are supported to allow for automated integration.
  3. Map RHEL-based distributions to Posit Package Manager APIs

    main

    When working with RHEL-based distributions, you must map the specific OS to the compatible Posit Package Manager API distribution to ensure correct pre_install hooks are used.

    DistributionVersion 8 MappingVersion 9 Mapping
    CentOScentos8Unsupported
    RedHatredhatredhat
    RockyLinuxN/Arockylinux9
    AlmaLinuxcentos8rockylinux9

    Key differences in pre_install hooks:

    • CentOS 8: Enables powertools repo.
    • RedHat 8: Uses subscription-manager to enable codeready-builder.
    • RockyLinux 9: Enables crb repo instead of powertools.
  4. How rv works: plan and sync

    main

    The rv workflow is based on a declarative configuration file that specifies your desired project state (R version, repositories, and dependencies). The core workflow involves two primary commands:

    1. rv plan: Provides a detailed preview of what changes will occur without actually modifying your environment.
    2. rv sync: Synchronizes your R library, configuration file, and lock file to match the desired state defined in your configuration.

    This ensures that your R environment is reproducible and matches your project specifications.

    rv plan # detail what will occur if sync is run
    rv sync # synchronize the library, config file, and lock file
  5. Map RHEL-like distributions to Posit Package Manager API endpoints

    main

    The rv project uses a distribution mapping strategy to ensure compatibility with the Posit Package Manager API. When using RHEL-like distributions, certain OS types must be mapped to specific API endpoints to retrieve correct system requirements.

    Mapping Logic:

    • AlmaLinux 8 $\rightarrow$ centos
    • AlmaLinux 9 $\rightarrow$ rockylinux
    • CentOS 8 $\rightarrow$ centos
    • CentOS 9 $\rightarrow$ rockylinux (since CentOS 9 is unsupported)
    • Oracle Linux $\rightarrow$ redhat
    • RockyLinux 8/9 $\rightarrow$ rockylinux
    • RedHat 8/9 $\rightarrow$ redhat
    /// Returns the distribution name to use for Posit Package Manager API
    /// Some distros need to be mapped to compatible API endpoints
    pub fn api_distribution(&self) -> &'static str {
        match self.os_type {
            OsType::Linux(distrib) => match distrib {
                "almalinux" => {
                    match self.version {
                        Version::Semantic(major, _, _) if major < 9 => "centos",
                        _ => "rockylinux",
                    }
                },
                "centos" => {
                    match self.version {
                        Version::Semantic(major, _, _) if major >= 9 => "rockylinux",
                        _ => "centos",
                    }
                },
                "oracle" => "redhat",
                _ => distrib,
            },
            _ => "invalid",
        }
    }
  6. How RPM package name parsing works

    main
    When identifying RPM packages from system dependencies, rv uses a heuristic to extract the package name from versioned strings. It identifies the package name by finding the first hyphen that occurs immediately before a digit (which signifies the start of the version number). This approach is designed to handle packages that contain hyphens in their actual names, such as libcurl-devel or abseil-cpp-devel.
  7. How distribution mapping is handled for RPM systems

    main

    To support various RHEL-based distributions where API support might vary, rv uses a centralized mapping strategy via the api_distribution() method. This allows the tool to map specific distributions to more stable or widely tested counterparts for dependency resolution:

    • AlmaLinux 8 is mapped to centos8 (chosen for stability and testing).
    • AlmaLinux 9 is mapped to rockylinux9 (as centos9 is unsupported by the API).

    This mapping layer ensures that sysreq_data() and is_supported() function correctly even when the underlying distribution is not explicitly supported by the primary API.

  8. Build and install rv from source

    main

    If you are developing rv or want to build it from the repository, you need Rust installed. You can use just (if available) or cargo to build and install the project.

    To build and run:

    just run <args>
    # or
    cargo run --features=cli --release -- <args>

    To install the current version as a binary:

    just install
    # or
    cargo install --path . --features cli
  9. Install rv via shell script

    main

    To install rv quickly, use the provided installation script via curl. After installation, verify the installation by checking the version.

    curl -sSL https://raw.githubusercontent.com/A2-ai/rv/refs/heads/main/scripts/install.sh | bash
    rv --version
  10. Track non-repository package metadata with LocalMetadata

    main

    For packages not installed via a standard repository (e.g., Git, URL, or local tarballs), rv uses LocalMetadata to ensure the installed version matches the requested source. This prevents unnecessary re-installs if the source hasn't changed.

    LocalMetadata supports two modes:

    • Mtime(i64): Used for local folders. It stores the modification time (mtime) of the source folder.
    • Sha(String): Used for Git, URL, or RUniverse sources. It stores a SHA hash to verify the content.

    You can load and write this metadata to a file named LIBRARY_METADATA_FILENAME within the package folder.

    use crate::library::LocalMetadata;
    
    // Example: Creating and writing metadata for a local folder
    let metadata = LocalMetadata::Mtime(1672531200);
    metadata.write(package_folder_path)?;
  11. Manage R package libraries with the Library struct

    main

    The Library struct is the primary abstraction for managing R package installations. It tracks installed packages, their versions, and metadata for non-repository packages (like those from Git or local folders).

    There are two ways to initialize a Library:

    1. Standard Library: Automatically determines the path based on the project directory, system architecture, and R version. The path follows the pattern: {project_dir}/rv/library/{R_version}/{arch}/{library_identifier}/.
    2. Custom Library: Allows you to specify an arbitrary path. If the path is relative, it is resolved against the project directory.

    Use find_content() to scan the library directory and populate the internal state of installed packages and metadata.

    use crate::library::Library;
    use crate::SystemInfo;
    
    // Standard library initialization
    let system_info = SystemInfo::new();
    let r_version = [4, 2, 0]; // Example R version
    let mut library = Library::new(project_dir, &system_info, r_version);
    library.find_content();
    
    // Custom library initialization
    let mut custom_lib = Library::new_custom(project_dir, "/path/to/custom/library");
    // Note: find_content() ignores custom libraries to prevent accidental overwrites
  12. Understand ResolvedDependency and Resolution outputs

    main

    The resolve method returns a Resolution<'d> object. This object contains the results of the dependency tree traversal:

    • found: A collection of ResolvedDependency objects that were successfully matched.
    • failed: A collection of UnresolvedDependency objects representing packages that could not be found or had errors (e.g., invalid DESCRIPTION files, network errors).
    • ignore: Packages that were explicitly skipped during the resolution process.

    A ResolvedDependency contains the specific Source (Local, Git, URL, or Repository), the PackageType (Source or Binary), and any associated env_vars required for the package.