# How uv Handles Universal Markers and Fork Strategies for Platform-Specific Dependencies

> Understand how uv manages platform-specific Python dependencies using universal markers and fork strategies like RequiresPython or Fewest for efficient resolution.

- Repository: [Astral/uv](https://github.com/astral-sh/uv)
- Tags: deep-dive
- Published: 2026-03-01

---

**uv resolves Python packages using universal markers that combine PEP 508 conditions with conflict markers, and employs fork strategies like `RequiresPython` or `Fewest` to create separate resolution paths for different platforms and Python versions.**

The `astral-sh/uv` package resolver introduces sophisticated mechanisms for handling complex dependency trees across multiple Python versions and operating systems. By implementing **universal markers and fork strategies for platform-specific dependencies**, uv can generate lockfiles that work correctly across Linux, macOS, Windows, and other platforms while managing extras and dependency groups.

## Understanding Universal Markers in uv

### The UniversalMarker Structure

At the core of uv's platform-specific resolution is the `UniversalMarker` struct defined in [`crates/uv-resolver/src/universal_marker.rs`](https://github.com/astral-sh/uv/blob/main/crates/uv-resolver/src/universal_marker.rs). This structure stores two distinct `MarkerTree` instances:

```rust
pub struct UniversalMarker {
    marker: MarkerTree,   // combined PEP 508 + conflict
    pep508: MarkerTree,   // only PEP 508
}

```

The `marker` field contains the full combined marker including both PEP 508 conditions and conflict markers, while `pep508` stores only the platform-specific portion computed via `without_extras()`.

### Combining PEP 508 and Conflict Markers

When constructing a universal marker, uv merges a standard PEP 508 marker with a `ConflictMarker` using bitwise AND operations on the underlying marker trees:

```rust
pub(crate) fn new(mut pep508_marker: MarkerTree, conflict_marker: ConflictMarker) -> Self {
    pep508_marker.and(conflict_marker.marker);
    Self::from_combined(pep508_marker)
}

```

This combination allows the resolver to simultaneously evaluate platform constraints (like `sys_platform == 'linux'`) and conflict constraints (like whether an extra is activated).

### Encoding Conflicts as Extras

To integrate conflict markers into the existing PEP 508 evaluation engine, uv encodes conflicts as synthetic extra values. The `encode_package_extra` function in [`crates/uv-resolver/src/universal_marker.rs`](https://github.com/astral-sh/uv/blob/main/crates/uv-resolver/src/universal_marker.rs) creates unique extra names:

```rust
fn encode_package_extra(package: &PackageName, extra: &ExtraName) -> ExtraName {
    // format: extra-<len>-<package>-<extra>
    ExtraName::from_owned(format!("extra-{}-{}-{}", package.as_str().len(), package, extra)).unwrap()
}

```

This encoding strategy lets uv treat a conflict as a normal extra marker, enabling the unified marker evaluation engine to reason about both platform constraints and extra activation.

## Fork Strategies for Platform-Specific Dependencies

### The ForkStrategy Enum

The `ForkStrategy` enum defined in [`crates/uv-resolver/src/fork_strategy.rs`](https://github.com/astral-sh/uv/blob/main/crates/uv-resolver/src/fork_strategy.rs) controls how uv creates separate resolution paths:

```rust
pub enum ForkStrategy {
    Fewest,
    #[default]
    RequiresPython,
}

```

This strategy is exposed in the public API and appears in the `ResolverOptions` struct in [`crates/uv-resolver/src/options.rs`](https://github.com/astral-sh/uv/blob/main/crates/uv-resolver/src/options.rs):

```rust
pub struct ResolverOptions {
    pub fork_strategy: ForkStrategy,
    …
}

```

### RequiresPython Strategy (Default)

The default `RequiresPython` strategy creates forks whenever a package's `requires-python` marker is disjoint from the current environment's Python version. This yields a separate resolution for each supported Python range, ensuring that dependencies are resolved correctly for each Python version constraint.

In [`crates/uv-resolver/src/resolver/mod.rs`](https://github.com/astral-sh/uv/blob/main/crates/uv-resolver/src/resolver/mod.rs), the resolver decides whether to fork based on this strategy:

```rust
match (self.options.fork_strategy, self.mode) {
    (ForkStrategy::Fewest, _) | (_, ResolutionMode::Lowest) => { … }
    (ForkStrategy::RequiresPython, _) => {
        // create a separate fork for each distinct requires‑python range
    }
}

```

### Fewest Strategy

The `Fewest` strategy chooses the smallest set of versions that satisfies all environments, preferring older versions that work on more Python versions or platforms. This minimizes the number of distinct package versions in the lockfile, potentially reducing disk space and installation complexity at the cost of using older package versions.

## Implementation in the Resolver

### Creating Forks During Resolution

When examining requirements, uv builds a **fork marker** representing the intersection of the current environment's `requires-python` with any platform markers. The core resolution loop in [`crates/uv-resolver/src/resolver/mod.rs`](https://github.com/astral-sh/uv/blob/main/crates/uv-resolver/src/resolver/mod.rs) handles the branching logic, creating new forks when the current strategy detects disjoint constraints.

### Storing Fork Markers

Each fork stores a vector of `UniversalMarker` instances (`fork_markers`) that encode the platform-specific constraints for that fork. The `ForkMap` struct in [`crates/uv-resolver/src/fork_map.rs`](https://github.com/astral-sh/uv/blob/main/crates/uv-resolver/src/fork_map.rs) manages these per-fork values and filters them by environment:

```rust
if env.included_by_marker(entry.marker) { … }

```

This check, utilizing functionality from [`crates/uv-resolver/src/resolver/environment.rs`](https://github.com/astral-sh/uv/blob/main/crates/uv-resolver/src/resolver/environment.rs), determines whether a requirement is compatible with the current fork's marker constraints.

### Lock File Integration

During lock generation, universal markers are serialized for each platform. The [`crates/uv-resolver/src/lock/mod.rs`](https://github.com/astral-sh/uv/blob/main/crates/uv-resolver/src/lock/mod.rs) file handles this serialization, creating static markers for supported platforms:

```rust
static LINUX_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
    UniversalMarker::new(pep508, ConflictMarker::TRUE)
});

```

The lock file records the strategy used via the `fork_strategy` method:

```rust
pub fn fork_strategy(&self) -> ForkStrategy { self.options.fork_strategy }

```

## Practical Examples

The following examples demonstrate how to work with universal markers and fork strategies programmatically:

```rust
use uv_resolver::{UniversalMarker, ConflictMarker, ForkStrategy};
use uv_pep508::MarkerTree;
use uv_normalize::PackageName;

// 1️⃣ Build a PEP 508 marker: python_version >= "3.9" and sys_platform == "linux"
let pep508 = MarkerTree::from_str(
    "python_version >= '3.9' and sys_platform == 'linux'"
).unwrap();

// 2️⃣ Build a conflict marker for the extra `gui` of package `my_pkg`
let conflict = ConflictMarker::extra(
    &PackageName::from_str("my_pkg").unwrap(),
    &ExtraName::from_str("gui").unwrap(),
);

// 3️⃣ Combine them into a universal marker
let uni = UniversalMarker::new(pep508, conflict);

// 4️⃣ Evaluate it in a concrete environment (e.g. Python 3.10 on Linux, with the extra enabled)
let env = MarkerEnvironment::new(/* python_version = ... */);
let satisfied = uni.evaluate(
    &env,
    std::iter::empty(),                                 // projects
    std::iter::once((&PackageName::from_str("my_pkg").unwrap(), &ExtraName::from_str("gui").unwrap())),
    std::iter::empty(),                                 // groups
);
assert!(satisfied);

```

To configure the fork strategy when initializing the resolver:

```rust
use uv_resolver::{ResolverOptions, ForkStrategy};

// Choose the fork strategy when creating a resolver
let mut opts = ResolverOptions::default();
opts.fork_strategy = ForkStrategy::Fewest; // prefer fewest versions across platforms
let resolver = Resolver::new(opts, ...);

```

## Summary

- **UniversalMarker** combines PEP 508 platform markers with conflict markers (extras/groups) into a single evaluable structure in [`crates/uv-resolver/src/universal_marker.rs`](https://github.com/astral-sh/uv/blob/main/crates/uv-resolver/src/universal_marker.rs).
- **Conflict markers** are encoded as synthetic extras (e.g., `extra-<len>-<package>-<extra>`) to leverage existing marker evaluation engines.
- **ForkStrategy** determines how the resolver handles platform-specific divergences: `RequiresPython` (default) creates forks for disjoint Python version ranges, while `Fewest` minimizes version counts across platforms.
- The resolver stores fork-specific constraints as `UniversalMarker` vectors in `ForkMap`, filtering requirements via `included_by_marker` checks against the current environment.
- Lock files serialize universal markers per-platform and record the active fork strategy to ensure reproducible installations across different environments.

## Frequently Asked Questions

### What are universal markers in uv?

Universal markers are a unified representation in uv that combines standard PEP 508 environment markers (like `sys_platform` or `python_version`) with **conflict markers** that track extras and dependency groups. Defined in [`crates/uv-resolver/src/universal_marker.rs`](https://github.com/astral-sh/uv/blob/main/crates/uv-resolver/src/universal_marker.rs), the `UniversalMarker` struct stores both the combined marker and the isolated PEP 508 portion, allowing the resolver to evaluate platform constraints and extra activation simultaneously.

### How does the RequiresPython fork strategy work?

The `RequiresPython` fork strategy, which is the default in [`crates/uv-resolver/src/fork_strategy.rs`](https://github.com/astral-sh/uv/blob/main/crates/uv-resolver/src/fork_strategy.rs), creates separate resolution forks whenever a package's `requires-python` marker is disjoint from the current environment's Python version. As implemented in [`crates/uv-resolver/src/resolver/mod.rs`](https://github.com/astral-sh/uv/blob/main/crates/uv-resolver/src/resolver/mod.rs), this strategy yields distinct resolution paths for each supported Python range, ensuring that dependencies are resolved correctly for each specific Python version constraint rather than finding a single version that satisfies all ranges.

### When should I use the Fewest fork strategy?

You should use the `Fewest` fork strategy when you want to minimize the number of distinct package versions across all supported platforms and Python versions. As defined in [`crates/uv-resolver/src/fork_strategy.rs`](https://github.com/astral-sh/uv/blob/main/crates/uv-resolver/src/fork_strategy.rs), this strategy prefers older package versions that work on more Python versions or platforms, reducing lockfile size and installation complexity. This is particularly useful for libraries that need to support a wide matrix of environments while keeping the dependency tree as simple as possible.

### How are conflict markers encoded in universal markers?

Conflict markers are encoded as synthetic extra markers using a specific string format in [`crates/uv-resolver/src/universal_marker.rs`](https://github.com/astral-sh/uv/blob/main/crates/uv-resolver/src/universal_marker.rs). The `encode_package_extra` function creates extra names following the pattern `extra-<len>-<package>-<extra>`, such as `extra-6-my_pkg-gui`. This encoding allows the resolver to treat conflicts (extras and groups) as standard extra markers, enabling the existing PEP 508 evaluation engine to reason about both platform constraints and extra activation without requiring separate logic paths.