How uv Handles Universal Markers and Fork Strategies for Platform-Specific Dependencies
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. This structure stores two distinct MarkerTree instances:
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:
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 creates unique extra names:
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 controls how uv creates separate resolution paths:
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:
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, the resolver decides whether to fork based on this strategy:
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 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 manages these per-fork values and filters them by environment:
if env.included_by_marker(entry.marker) { … }
This check, utilizing functionality from 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 file handles this serialization, creating static markers for supported platforms:
static LINUX_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
UniversalMarker::new(pep508, ConflictMarker::TRUE)
});
The lock file records the strategy used via the fork_strategy method:
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:
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:
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. - 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, whileFewestminimizes version counts across platforms. - The resolver stores fork-specific constraints as
UniversalMarkervectors inForkMap, filtering requirements viaincluded_by_markerchecks 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, 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, 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, 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, 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. 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →